pawnable.tw #1 startのwriteup

CTF

ファイルが与えられる。
fileコマンドで調べるとELF32 (Linuxでよく使われる実行ファイル)とのこと。
objdumpで逆アセンブルする。

 08048060 <_start>:
 8048060:    54                       push   %esp
 8048061:    68 9d 80 04 08           push   $0x804809d
 8048066:    31 c0                    xor    %eax,%eax
 8048068:    31 db                    xor    %ebx,%ebx
 804806a:    31 c9                    xor    %ecx,%ecx
 804806c:    31 d2                    xor    %edx,%edx
 804806e:    68 43 54 46 3a           push   $0x3a465443
 8048073:    68 74 68 65 20           push   $0x20656874
 8048078:    68 61 72 74 20           push   $0x20747261
 804807d:    68 73 20 73 74           push   $0x74732073
 8048082:    68 4c 65 74 27           push   $0x2774654c
 8048087:    89 e1                    mov    %esp,%ecx
 8048089:    b2 14                    mov    $0x14,%dl
 804808b:    b3 01                    mov    $0x1,%bl
 804808d:    b0 04                    mov    $0x4,%al
 804808f:    cd 80                    int    $0x80
 8048091:    31 db                    xor    %ebx,%ebx
 8048093:    b2 3c                    mov    $0x3c,%dl
 8048095:    b0 03                    mov    $0x3,%al
 8048097:    cd 80                    int    $0x80
 8048099:    83 c4 14                 add    $0x14,%esp
 804809c:    c3                       ret    

 0804809d <_exit>:
 804809d:    5c                       pop    %esp
 804809e:    31 c0                    xor    %eax,%eax
 80480a0:    40                       inc    %eax
 80480a1:    cd 80                    int    $0x80

文字を表示しているのにどこにもcallなどでprintfみたいな感じの関数を呼んでいる気配がない。
実はシステムコールで表示している。
各種レジスタやスタックに引数やシステムコール番号を突っ込んだあとにINT 80hでその機能が実行される。

straceコマンドを使い、システムコール呼び出しを調べると、sys_writeとsys_readが使われている。

gdbなどでretの直前まで実行しスタックをダンプしてみて、図示すると

——
一回前のESP
——
リターンアドレス(0x804809dが入れられている、_exitへ飛ぶため。)
——
20バイトの”Let’s start the CTF”

——

となっている。

なお、スタックの見方は…

$gdb start
(gdb)run
<...中略...>
(gdb)disas
<...中略...>
(gdb) b *0x08048099
Breakpoint 1 at 0x8048099
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/user1/ダウンロード/start 
Let's start the CTF:aaaaaaaaaaaa

Breakpoint 1, 0x08048099 in _start ()
(gdb) i r esp
esp            0xffffd0d4    0xffffd0d4
(gdb) x/50xb 0xffffd0d4
0xffffd0d4:    0x61    0x61    0x61    0x61    0x61    0x61    0x61    0x61
0xffffd0dc:    0x61    0x61    0x61    0x61    0x0a    0x68    0x65    0x20
0xffffd0e4:    0x43    0x54    0x46    0x3a    0x9d    0x80    0x04    0x08
0xffffd0ec:    0xf0    0xd0    0xff    0xff    0x01    0x00    0x00    0x00
0xffffd0f4:    0xc1    0xd2    0xff    0xff    0x00    0x00    0x00    0x00
0xffffd0fc:    0xe6    0xd2    0xff    0xff    0xf1    0xd2    0xff    0xff
0xffffd104:    0x03    0xd3

とやるとスタックの内容がわかる。

sys_writeで標準出力(fd = 1)に出力するのは、「スタックに確保された20バイトの”Let’s start the CTF”」だ。

システムコールでは、EAXにシステムコール番号を、EBX、ECX、EDXに引数を入れるようで、
sys_writeを呼び出す前、
読み込んでほしい場所の開始アドレスを格納するECXレジスタにこの20バイトの先頭アドレスが保存され、次に長さを格納するEDLレジスタに14h(= 20)を入れるので、sys_writeはちゃんと20バイト読んでくれる。

しかし、その後のsys_readを呼び出すときもECXを変えておらず、それなのに長さを入れるEDLレジスタに0x3c ( =60)を入れている。(本来なら、スタックに新たに60バイト、確保してそのアドレスをECXに設定すべきである。)

つまり、20バイト以上のデータを入力するとスタックの内容を破壊してしまう。

1回目のretまで

これを利用してリターンアドレスを書き換える。
20バイト書き込んで、そのあとに、retしたとき行きたい先のアドレスを書き込めば良い。

具体的には、戻る先はsys_writeの読み込み先頭アドレスをECXに格納し準備をしている
0x8048087 mov    %esp,%ecx
だ。

2回目のretまで

ここにretした時点でスタックの一番上には、ESPの値が格納されている。
(一番最初に0x8048060     push   %esp によってESPの値がスタックにプッシュされていたから。そのあとに積まれたリターンアドレスなどはretでpopされた)

0x8048087 mov    %esp,%ecxにより、sys_writeにより読み出される先頭アドレスが格納されるECXには、ESPが格納される。
よってsys_writeによって現在のESPの指すところから20バイト表示されると、その先頭に現在のESPアドレスが表示される。

これでスタックアドレスがリークした。

次にまたsys_readで入力を受け付けてくれる。

またバッファオーバーフローさせ、
最終的に、リターンアドレスにはシェルコードのアドレスを入れたい。
「リークしたスタックアドレスの20バイト先」を指すようにする。そしてそのあとにシェルコードを仕込む。
https://www.reddit.com/r/LiveOverflow/comments/5r47h1/a_little_help_needed_with_a_simple_pwnable/

なぜ「リークしたスタックアドレスの20バイト先」なのか。

他の人のWriteupを見ると、ウマイ人はこんなの当たり前だと思ってるのか書いてなかったりする。

リークしたESP、これをleaked_ESPとすると、リークした時点での現在のESPはESP = -4 + leaked_ESPである。
(-4 + leaked_ESP)から(0 + leaked_ESP)の4バイトには、leaked_ESP自体の値が入っている。

リークしたあとに0x8048099 add    $0x14,%espで+20されているので、ESP = 16+ leaked_ESP。
最後にretすると4バイトのリターンアドレスがpopされてESP=20 + leaked_ESP
となる。

最後のretでシェルコードに飛ぶために、ESP=(16 + leaked_ESP)から(20+leaked_ESP)の4バイトに後続のシェルコードへのリターンアドレス、20+leaked_ESPを積みたい。

0x8048087 mov    %esp,%ecx 以来、書き込み先アドレスが格納されるECXが変更されていないため、シェルコード書き込み時のsys_readは、リークのためretしたあとのESP、つまりESP=-4  + leaked_ESPのときのアドレスから書き込み始める。

2回目のretの直前、スタックのトップのアドレスはESP=16+leaked_ESPである。
retで読み込まれるのは,ESP=16+leaked_ESPから4バイト (ESP=16+leaked_ESP   〜 20+leaked_ESP)である。

つまり、sys_readではアドレスleaked_ESPから16-(-4) = 20バイト、ぶん無駄に書き込み、そのあとに4バイトぶん(開始位置はESP=16 + leaked_ESPで、ESP=20 + leaked_ESPまで)、2回目のretで使うリターンアドレスを書き込む必要がある。そのあとにシェルコードを書き込む。

書き込むリターンアドレスというのは、そのアドレスが書き込まれている場所の次から始まるシェルコードの先頭アドレスであるから、20 + leaked_ESPを指定すれば良い。

これがリークしたスタックアドレスの20バイト先を、リターンアドレスとして指定する理由である。

このへん厳密に計算するのが面倒なら適当にNOPで埋めてジャンプすれば良い…と思う。試してないけど。
(本当は、たくさんのNOPを使うのは、アドレスのランダマイズが有効な時にたくさん注入して適当にジャンプして成功を祈るシェルコード(Heap spray攻撃とか)のわかりやすい特徴なので、ホントに攻撃に使う場合は検知されやすいかもしれない。NOPの代わりに、XORなど結局何もしないけどNOPとは違う系の命令を入れるという手もある。)

さてシェルコード自体はどうするか。

http://hackoftheday.securitytube.net/2013/04/demystifying-execve-shellcode-stack.html
に詳しく書いてある。

シェルを起動する常套手段として、sys_execveを呼びたい。
システムコール番号は11。
execve関数は第一引数が実行するプログラム名、第二引数以降が実行するプログラムに渡す引数の配列となる。この配列は可変長なので最後にnullを入れないといけない。そのあとに環境変数へのポインタの配列。やっぱりこっちも最後はnull。

よってEAX = 11、EBX = (実行するコードの文字列の先頭アドレス、文字列の終わりはちゃんと0x00にする)、ECX=(実行するコードの文字列の先頭アドレスへのアドレス(ポインタなので)、nullと並ぶ配列の先頭アドレス)、EDXにはnull(= 0x00000000)を入れてINT 80hを実行すれば良いようだ。

適当にスタック上にnullや文字列や、そのアドレスを配置し、それらを指すアドレスを各レジスタに格納する。参考URLに詳しく書いてある。

EBXに入れるの は0x6e69622f68732f2fへのアドレスで、バイトオーダーがリトルエンディアンなのに気を付けてASCIIでデコードすると”/bin//sh”となるようだ。余計なスラッシュ”/”が付いているが、これはぴったり8バイトにしてアラインメントを維持するためのものらしい。いやな場合はスペースか何か入れれば良いと思う。さっきも言ったように末尾に0x00をつけてシメなくてはならない。(文字列はヌル文字で終端)

以上が手順の解説で、実際のエクスプロイトコードは貼ったリンクなどを参照して下さい。

なお、このようにスタック上のコードを実行できるとこのような条件ではぽんぽんエクスプロイトされるのでスタック上のコードは実行不可属性(NXビットが1)になってる、つまり実行できないようになっていたりする。今回はそうでないのでできるけど。

コメント