ファイルが与えられる。
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)になってる、つまり実行できないようになっていたりする。今回はそうでないのでできるけど。
コメント