pwn challenges list baby, easyについて
これはCTF Advent Calendar 2017 - Adventarの22日目です。
pwn challenges listのbabyとeasyの一部を解いて、どんな問題が多かったか、何に躓いたかなどを書いていきたいと思います。問題のネタバレを防ぐために個々の問題については深く言及しないです。
これからpwnを始めようとしている人の参考になれば幸いです。
解き状況は以下のような感じです。
(https://ctf.katsudon.org/ctf4u/)
baby
書式文字列攻撃やROPを知っている、簡単なシェルコードを書けるならば問題はないです。もしこれらがわからない場合は、
あたりが役立つと思います。
ほとんどの問題は、バイナリのサイズが小さく解析をしやすくて、使う攻撃もそこまで複雑ではないものが多いので、もし解けなくてもwriteupを見ればすぐ理解できると思います(一部を除く)。.bssセクションを使えばどうにかなる問題が多かった気がします。
ただ、greenhorndは唯一のwindows問で、windowsのシェルコードを書く必要がありLinuxのように簡単にいかないため少し難しいです。これは後回しにしていいと思います。windowsのシェルコードに関してはももいろテクノロジーさんの記事が参考になりました。
easy
まだ13問しか解いていないため断言はできませんが、使う攻撃手法に関してはbabyとほぼ変わらないです。babyと違うのはUAF(use after free)を使う問題が出てくるぐらい。大きく異なると感じた部分はバイナリが大体大きいこと。babyまではobjdump
の出力にメモする程度の解析で間に合っていましたが、easyからその出力を追うのは大変になってきました。そのため、解析しながら擬似コードを書くようになったり、radare2を使い始めるようになりました。radare2便利。babyとの違いはこのくらいなので他にはあまり書くことはないです。
baby, easyを全部解くべきなのか
よくわからないです。ただ、コンテストで安定して1, 2問は解けるようになったので、安定した土台を作るにはいいんじゃないかと思っています。baby, easyを埋めるにしてもこれだけやっていてはheap問が全くできないままなので、コンテストで解けなかったheap問を復習したり、勉強会に参加してheapの勉強をするなどは必要だと思います。
躓いたこととその解決方法
ここで、pwnの問題を解いているときに躓いたことを紹介しようと思います。
1. exploitがどこまでうまくいっているかわからない
攻撃対象のバイナリpwnable
と以下のようなexploitコードがあったとします。(pwntoolsを使っています)
#!/usr/bin/env python from pwn import * if __name__ == '__main__': context(os='linux', arch='i386') conn = process('./pwnable') conn.sendline('-4') payload = '' payload += p32(0x80491e4) payload += p32(0x80491e8) payload += asm(shellcraft.sh()) conn.sendline(payload) conn.interactive()
そのまま実行してシェルが取れればいいですがそんなに簡単にいくものではありません。
うまくいかない場合、どこで失敗したかわからないためgdbでexploitのデバッグを行います。conn.sendline(payload)
の前などにraw_input()
またはpwntoolsの場合pause()
を挿入して、実行をそこで停止させる。その状態で他のコンソールを開いてsudo gdb -q -p `pgrep pwnable`
を実行するとgdbでアタッチできて、デバッグを行うことができます。
2. main関数がどこにあるのかわからない
file
コマンドでバイナリの情報を見ると最後にstripped
かnot stripped
と表示されます。
% file a.out a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=44840fb9b6312a23e5f3339099e536d1df7cea89, stripped
stripped
の場合、シンボル情報がなくobjdump
で見るとmainも他の関数もつながっているように見えます。そのためmainがどこから始まっているかわからない状態になっています。そこでまず、__libc_start_main
が呼ばれているかどうかを確認します。__libc_start_main
の第一引数はmainのアドレスなので、もし__libc_start_main
が呼ばれていばmov rdi, 0x8048724
かpush 0x8048724
が直前の命令にあるはずで、そこのアドレス(この場合0x8048724
)をmainのアドレスであると特定することができます。以下の例では0x400666
からmain関数。
0000000000400570 <.text>: 400570: 31 ed xor ebp,ebp 400572: 49 89 d1 mov r9,rdx 400575: 5e pop rsi 400576: 48 89 e2 mov rdx,rsp 400579: 48 83 e4 f0 and rsp,0xfffffffffffffff0 40057d: 50 push rax 40057e: 54 push rsp 40057f: 49 c7 c0 40 07 40 00 mov r8,0x400740 400586: 48 c7 c1 d0 06 40 00 mov rcx,0x4006d0 40058d: 48 c7 c7 66 06 40 00 mov rdi,0x400666 <--- 400594: e8 a7 ff ff ff call 400540 <__libc_start_main@plt> 400599: f4 hlt 40059a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0] 4005a0: b8 57 10 60 00 mov eax,0x601057 ..... 40065e: ff d0 call rax 400660: 5d pop rbp 400661: e9 7a ff ff ff jmp 4005e0 <fgets@plt+0x90> 400666: 55 push rbp 400667: 48 89 e5 mov rbp,rsp 40066a: 48 81 ec 10 01 00 00 sub rsp,0x110 400671: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 400678: 00 00 40067a: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 40067e: 31 c0 xor eax,eax 400680: bf 54 07 40 00 mov edi,0x400754
__libc_start_main
が呼ばれてないときはreadelf -h a.out
で表示されるEntry pointをスタートとして一命令ずつ見ていってmainを特定するしかなさそうです。
3. バイナリを実行しても何をしているかわからない(出力がなかったり,エラーで終了したり)
これにはいくつかパターンがあります(ここに書くもの以外もあるかもしれません)。
- fork-server型の問題で、特定のポートで待機している。
- 特定のユーザやファイルを作成しないと実行できない。
- recvのflagにMSG_WAITALL(0x100)が指定されていて、recvの引数に指定した長さだけ入力しないとブロックされたままになってしまう。
これらは大体strace
で解決できます。
fork-server型の場合は
% strace ./a.out ... socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3 bind(3, {sa_family=AF_INET, sin_port=htons(12345), sin_addr=inet_addr("0.0.0.0")}, 16) = 0 listen(3, 3) = 0 accept(3,
このようにポート番号(12345)もわかります。
必要なユーザやファイルもstrace
で大体わかると思います。
recvでブロックされてしまうことに関しては、-f
オプションで子プロセスも追うことによってわかります。
% strace -f ./a.out ... accept(3, strace: Process 3298 attached <unfinished ...> [pid 3298] recvfrom(4, "aaaaaaaaaaaaaaaa", 16, MSG_WAITALL, NULL, NULL) = 16 [pid 3298] close(4) = 0 [pid 3298] exit_group(0) = ? [pid 3298] +++ exited with 0 +++ <... accept resumed> 0x7fff05b59180, 0x7fff05b59170) = ? ERESTARTSYS (To be restarted if SA_RESTART is set) --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=3298, si_uid=1000, si_status=0, si_utime=0, si_stime=0} --- accept(3,
入力待ちのときはrecvfrom(4,
まで表示されて、適当に入力するとMSG_WAITALLが指定されていたことがわかります。
4. gdbで実行できない
gdbで実行できない状況は自分の知る限り2通りあります。
1つ目は、実行中にsystem
などが呼ばれて子プロセスが生成されるとき。この場合は実行前にgdb上でset follow-fork-mode parent
をしておけば良い。
2つ目は、PIE(位置独立実行形式。実行時に実行ファイルのアドレスが決まる)が有効のとき。この場合実行ファイルのアドレスがわからないため、実行前にbreak pointを設定できません。1つ目の方法としてはプログラムを普通に実行して入力待ちのときにgdbでアタッチする方法(入力の処理まで進んでしまうためその前は見れない)があります。2つ目の方法では、まずgdb上でb *0
のように適当にbreak pointを設定してr
で実行します。すると、
gdb-peda$ b *0 Breakpoint 1 at 0x0 gdb-peda$ r Starting program: /home/ubuntu/workspace/a.out Warning: Cannot insert breakpoint 1. Cannot access memory at address 0x0
プログラムが実行されてからbreak pointを挿入できないため停止します。その後にbreak pointを無効にするためdisable breakpoints
をしてから、メモリマップを見てみます。
gdb-peda$ disable breakpoints gdb-peda$ info proc map process 2794 Mapped address spaces: Start Addr End Addr Size Offset objfile 0x555555554000 0x555555555000 0x1000 0x0 /home/ubuntu/workspace/a.out 0x555555754000 0x555555756000 0x2000 0x0 /home/ubuntu/workspace/a.out 0x7ffff7dd7000 0x7ffff7dfd000 0x26000 0x0 /lib/x86_64-linux-gnu/ld-2.23.so 0x7ffff7ff8000 0x7ffff7ffa000 0x2000 0x0 [vvar] 0x7ffff7ffa000 0x7ffff7ffc000 0x2000 0x0 [vdso] 0x7ffff7ffc000 0x7ffff7ffe000 0x2000 0x25000 /lib/x86_64-linux-gnu/ld-2.23.so 0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0 0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack] 0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
これで実行ファイルのアドレスが特定できたのでmainに相当するアドレスにbreak pointをつけて実行(c
)することができます。
最後に
ほとんどpwn challenges listの話をしていない気がしますが…
良問ばかりでやって損はないのでたくさん解いていきましょう。
今年度中にeasyは埋めたいですね。