h_nosonの日記

競プロ、CTFなど

pwn challenges list baby, easyについて

これはCTF Advent Calendar 2017 - Adventarの22日目です。

pwn challenges listのbabyとeasyの一部を解いて、どんな問題が多かったか、何に躓いたかなどを書いていきたいと思います。問題のネタバレを防ぐために個々の問題については深く言及しないです。
これからpwnを始めようとしている人の参考になれば幸いです。

解き状況は以下のような感じです。 f:id:h_noson:20171216000028p:plain
(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コマンドでバイナリの情報を見ると最後にstrippednot 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, 0x8048724push 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は埋めたいですね。