SECCON CTF つくば大会 にいってきました。


OLYMPUS DIGITAL CAMERA

http://www.seccon.jp/2012/05/ctf-1.html
なんか公式ページで「物量作戦」と紹介されていますが、そんなことないです。きっと。。

みむらです。
福岡大会に続いて、つくば大会にも参加してきました。

 

なお、前回大会については、ここを見てください。
SECCON CTF 福岡大会に参加してきた。 – みむらの手記手帳

 

チーム名は前回と同じ 「 wasamusume 」 ですが、
よりによって「ておくれ要員」を確保し忘れた(し損ねた) ため、
前回と比較しまして、ネタ性のないチームになりました。

でも、それはそれは和気藹々と、非常に仲のいいチームができあがっておりましたので、
前回同様、とても楽しかったのは間違いありません。

 

バイナリアンやWeb 屋さんや、ネットワーク屋さんや、
それはそれは、大変すばらしい皆さんの中で参加できたように思えます。

今回のチームメンバは、
私, かーみさん, ほもっきんちゃん, あたがわさん, いっくん, みっとさん の計6人。
会場には計4 人しか入れませんでしたので、
控え室に臨時で(?)島を作り、そこからと会場の二つの場所から問題を解く形を取りました。

CTFの難易度としましては、
今回は前回と比較しまして、問題の難易度がかなり高くなっており、
たとえば exe ファイルを覗いて、中の挙動を見て、答えを探すものは、
今回は無かったように思います。

一方で、ダンプファイルが渡されて解析してね、というような、より業務向けというか、
実際の現場により近いというか。。 そういう問題がありました。

 

反省としては、
毎回思うことですが、次回に向けてもっと勉強しないと、というのが一つ。
それともう一つ。ヘルプファイルやドキュメントは読もう、ということ。

 

次回に向けて思ったこととしては、
会場にあわせて機材を用意して、自分たちの環境を作って楽しくやりましょう。と。

なんていうのか、修学旅行のあれと一緒ですよね。
禁止事項をしっかり読んで、その中で問題の無い範囲でがんばる、というか。
最初の画像なんか、まさにそうなのですがw

 

まじーめな記事はこんなところw


2日間行われるということで、
今回の大会では、かーみーさんが我が家に来ました。

山奥へようこそ(ぁ

来る途中、夜中の町中を走る電車の車窓が、「まだ地下鉄の方が明るい」と言わしめて、
プチガッツポーズと、なんか悲しさを覚えました。

自宅近くのスーパー銭湯で入浴と食事。
意外と夜の露天で、星空見ながら入浴するのもいいですね・・!

にしても暑かったです。

食事処では、かーみーさんと前回参加して欠席したメンバ
(まひるんちゃん、わさお)の話をして、「いいやつだったのに。。。。」 と
二人で、かの二人を偲んで泣いておりました。

・・・うそうそ。

でも次に機会があったら福岡大会メンバとも一緒にやりたいよね!
関東メンバと一緒に混ざると、化学反応が起きるかもね!

 

CTF 中は、なぜかておくれと言われてみたり、
なぜか写真を割かし多めに撮られてみたり。
はっとりせんせーがよくこっちに来たり。

でも1日目は会場では無くて、控え室に陣取ってやっていたため、
会場のうるさいBGMは無く、ちょっかいを出しに来る人もあまりいないということで、
ある意味集中してできましたが、CTF らしさというか、そういうものに欠けました。

夜もみんなで問題を。

コンビニで燃料調達したり、焼き肉屋に突撃したり。
.pcap じゃなくて .txt だぞこれ! ということに気づいたり、
mruby さんが動くようになったり。

寝ずにがんばっていたのですが、途中で体力切れ・・。
でも夜があったから、問題がいくつか解けて、2日目にいいスタートを切れたんだと思います。

2日目は会場内で。やはり会場内の方が盛り上がりますね。

いっくんさんが Web 系問題をばしばしといていったり、
みっとさんが気合いで脆弱な暗号問題を解いて、全員で踊ったり (注:ておくれダンス)
とにかくとにかく注目を浴びたチームであり、チームとしてもいい線まで行けたように思います。

 

そして、CTF が終わって、我が家に帰ってきて、初日と同じ風呂屋に。
かーみーさんの足が、なかなかに強力。。

抓られますね! うかつに {足,手} 出せないです。

次の日の朝、金環日食を我が家からいい感じで見ました。

 

こんな感じで楽しく過ごして、SECCON CTF とそのたいろいろと、
結構楽しみました。

楽しい人たちで CTF チームを構成して、
また楽しく CTF に次回も参加したいです!

 

・・・続きを読むの先に、今回出た問題の一つの解説記事みたいなのを書いてみました。

 


さて、今回もSECCON CTF で出た問題の一つを取り上げてみます。

image

問題としては、このファイルを渡されて、回答してください。というもの。
d70ec858ac1f7b3094b0e7461b3be67f.txt

 

うちのチームですと、まずかーみーさんが 「MATZ」という文字列に気づいて、
Ruby 処理系ということに気づき、
私が最初の一文をコピーして検索して、 mruby であることに気づきました。

mruby – github ( https://github.com/mruby/ )

これを git で落としてきて、Linux でてけてけ make して動かす。

 

・・・一応私がやった順番通りに。

 

まず最初に動かしてみますと、内部で gets という関数を呼び出しているのですが、
mruby さんにはそういうのは無いようで、困ります。

image

print.c : 40

mrb_value
mrb_printstr(mrb_state *mrb, mrb_value self)
{
  mrb_value argv;

  mrb_get_args(mrb, "o", &argv);
  printstr(mrb, argv);

  return argv;
}

mrb_value
mrb_input(mrb_state *mrb, mrb_value self)
{
  mrb_value argv;
  char text[1024] = {0};

  scanf("%s",text);

  return mrb_str_new_cstr(mrb,text);
}

void
mrb_init_print(mrb_state *mrb)
{
  struct RClass *krn;

  krn = mrb->kernel_module;

  mrb_define_method(mrb, krn, "__printstr__", mrb_printstr, ARGS_REQ(1));
  mrb_define_method(mrb, krn, "__inputstr__", mrb_input, ARGS_REQ(0));
}

しょうが無いので、こんな感じでprint.c をいじって文字入力関数を内部的に用意して、

mrblib のどこかに、

  def gets()
    return __inputstr__
  end

と書いてあげれば、内部から gets 関数が使えるようになります。

・・・・ってか、 return で正解を返すようにすればいいんですけれどね・・ww

 

そしてその後、

pics01

れっつ VM の挙動をみていきましょうー!!

 

・・・・はい、詰みます。ておくれます。

 

実際私は、CTF 中はこんな感じで VM の挙動を見ていたわけですが、

そんなことしなくても [ –verbose ] オプションをつければ、

だだだだーっと人間が読めるコードが出てくるんですよね。

・・・・マニュアル読めばわかったことなのに。。。 orz

 

ということで気を取り直して。

image

こんなに読みやすくなったよ!! やったねたえ(ry

output.txt

 

まずこの画面から読めますのは、

$encrypted_message という配列 (ARRAY) があって、長さは 20

それで、 R2 からということなので、

int em[] = { 151, 73, 85, 81, 31, 64, 85, 24, 94, 72, 15, 23, 64, 3, 49, 85, 120, 106, 110, 68 };

C のコードにしますと、こういう感じでしょうか。

 

そして、グローバル変数の v1 と v2 というのがあって、

それぞれ初期値が 255, 98 ということ。

 

続けて読んでいきまして、

156 OP_LAMBDA	R3	I(118)	1
157 OP_METHOD	R2	'ord'
158 OP_TCLASS	R2
159 OP_LAMBDA	R3	I(119)	1
160 OP_METHOD	R2	'correct'
161 OP_TCLASS	R2
162 OP_LAMBDA	R3	I(120)	1
163 OP_METHOD	R2	'wrong'
164 OP_TCLASS	R2
165 OP_LAMBDA	R3	I(121)	1
166 OP_METHOD	R2	'input_the_key'
167 OP_TCLASS	R2
168 OP_LAMBDA	R3	I(122)	1
169 OP_METHOD	R2	'check_length'
170 OP_TCLASS	R2
171 OP_LAMBDA	R3	I(123)	1
172 OP_METHOD	R2	'compare_encrypted_message'
173 OP_TCLASS	R2
174 OP_LAMBDA	R3	I(125)	1
175 OP_METHOD	R2	'main'

ここの部分。

関数名と番号(?)を結びつけていると見えます。

ord = 118

correct = 119

wrong = 120

input_the_key = 121

check_length = 122

compare_encrypted_message = 123

main = 125

こんな感じ。

 

その後を読んでみますと、

178 OP_SEND    R2    ‘main’    0

179 OP_STOP

とありますので、 main 関数を見に行きます。

 

irep 125 nregs=6 nlocals=4 pools=0 syms=4
000 OP_ENTER	0:0:0:0:0:0:0
001 OP_LOADSELF	R4
002 OP_LOADNIL	R5
003 OP_SEND	R4	'input_the_key'	0
004 OP_MOVE	R2	R4
005 OP_LOADSELF	R4
006 OP_MOVE	R5	R2
007 OP_LOADNIL	R6
008 OP_SEND	R4	'check_length'	1
009 OP_LOADSELF	R4
010 OP_MOVE	R5	R2
011 OP_LOADNIL	R6
012 OP_SEND	R4	'compare_encrypted_message'	1
013 OP_LOADSELF	R4
014 OP_LOADNIL	R5
015 OP_SEND	R4	'correct'	0
016 OP_RETURN	R4

main 関数では、 input_the_key されて、

check_length されて、 compare_encrypted_message されて、 correct されてることがわかります。

 

関数の名前から check_length で長さ見て、

compare_encrypted_message で入力文字列との比較をやっていると予想されます。

コードは割愛しますが、 check_length では encrypted_message の size の結果との比較

つまり、入力文字数が 20 文字であるかどうかをチェックしています。

ここから、入力文字列が 20 文字であることがわかります。

続いて、 compare_encrypted_message 内で呼ばれているラムダの中身を見ていきます。

irep 124 nregs=8 nlocals=5 pools=0 syms=8
000 OP_ENTER	1:0:0:0:0:0:0
001 OP_LOADSELF	R5
002 OP_GETUPVAR	R6	1	0
003 OP_MOVE	R7	R1
004 OP_LOADNIL	R8
005 OP_SEND	R5	'ord'	2
006 OP_GETGLOBAL	R6	'$v1'
007 OP_LOADNIL	R7
008 OP_SEND	R5	'^'	1
009 OP_MOVE	R3	R5
010 OP_MOVE	R5	R3
011 OP_LOADI	R6	85
012 OP_LOADNIL	R7
013 OP_SEND	R5	'^'	1
014 OP_SETGLOBAL	'$v2'	R5
015 OP_GETGLOBAL	R5	'$v1'
016 OP_GETGLOBAL	R6	'$v2'
017 OP_LOADNIL	R7
018 OP_SEND	R5	'^'	1
019 OP_SETGLOBAL	'$v1'	R5
020 OP_MOVE	R5	R3
021 OP_GETGLOBAL	R6	'$encrypted_message'
022 OP_MOVE	R7	R1
023 OP_LOADNIL	R8
024 OP_SEND	R6	'[]'	1
025 OP_LOADNIL	R7
026 OP_SEND	R5	'!='	1
027 OP_JMPNOT	R5	032
028 OP_LOADSELF	R5
029 OP_LOADNIL	R6
030 OP_SEND	R5	'wrong'	0
031 OP_LOADNIL	R5
032 OP_RETURN	R5

ord 関数は見た感じ、 ASCII コードと対応させているような感じ。

 

これをまずは普通に C# に起こしてわかりやすくします。

    static bool question(int c, int n)
    {
        int r5 = c ^ v1;
        int r3 = r5;
        r5 = r5 ^ 85;
        v2 = r5;
        r5 = v1 ^ v2;
        v1 = r5;
        r5 = r3;

        if (encrypted_message[n] == r5)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

こんな感じですかね・・w

 

        static void Main(string[] args)
        {
            int[] em = { 151, 73, 85, 81, 31, 64, 85, 24, 94, 72, 15, 23, 64, 3, 49, 85, 120, 106, 110, 68 };
            int v1 = 255, v2 = 98;
            var n = new string(
                                em.Select(e =>
                                    Enumerable.Range(33, 127)
                                    .SkipWhile(x => (x ^ v1) != e)
                                    .Select(x => { v2 = x ^ v1 ^ 85; v1 ^= v2; return x; })
                                    .First()
                                ).Select<int, char>(x => { return (char)x; })
                                .ToArray()
                                );

            Console.WriteLine("Answer : {0:G}\n", n);
        }

あとこれを、こんなのでぐるぐる回してもいいのですが、

(ただし、 v1 と v2 の値が不正な値の時にも更新されてしまうので、そこを考慮する)

それはひっじょーにつまらない。

・・・というか、某 N さんに 「あったよねー、ブルートフォースで探してるところーw」 と、

某所で食事をした際に言われてしまったので、ちゃんと解いてみます。

 


    static bool question(int c, int n)
    {
        int r5 = c ^ v1;
        int r3 = r5;
        r5 = r5 ^ 85;
        v2 = r5;
        r5 = v1 ^ v2;
        v1 = r5;
        r5 = r3;

        if (encrypted_message[n] == r3)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

これを見ると、

まず正答かどうかは r3 との比較になっていることがわかります。

ここから、 
(入力文字) ^ v1  の値が、 encrypted_message の所定の位置の値と同じであれば、

正しい文字列と見なして良さそうです。

さらにこのコードを見やすくするために整理し、

その過程で v2 あたりが削れそうなので削り、コードを縮めますと、

    static bool question(int c, int n)
    {
        int r5 = c ^ v1;
        int r3 = r5;
        v1 = v1 ^ r5 ^ 85;

        if (encrypted_message[n] == r3)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

こうなってきます。

 

次に、出力から逆に見ていくと、

encrypted_message[n] から元の文字列を出すためには

v1 と排他的論理和をとってあげればいいことがわかります。

そして、

v1 は、暗号文に対して ( v1, 暗号文字, 85 ) とで

排他的論理和をとった値で更新されいていくことがわかります。

int ans = encrypted_message[n] ^ v1;
v1 = v1 ^ encrypted_message[n] ^ 85;

コードに直すとこんな感じ。

 

ここまでくれば、後はコードに起こして、

        static void Main(String[] args)
        {
            int[] em = { 151, 73, 85, 81, 31, 64, 85, 24, 94, 72, 15, 23, 64, 3, 49, 85, 120, 106, 110, 68 };
            int v1 = 255;

            var n = new string(
                            em.Select(e => { var r = e ^ v1; v1 ^= e ^ 85; return r; })
                              .Select<int, char>(x => { return (char)x; })
                              .ToArray()
                              );

            Console.WriteLine("Answer : {0:G}\n", n);
        }

実行すればこんにちは!

 

 

というわけでこんな感じです。

 

そして今回、この問題に手間取った非常に大きな理由は、

ヘルプ見ましょう。ということ。

image

 

ばっちり書いてあったんですよねぇ。。。 うーん。。。

絶対 –verbose オプションを知っていたら解けてた。。。

 

前回は LLVM が出て、そのときは逆コンパイラ見つけてきて、

元に戻して、コード書いて、探してみたいなことをしていましたが、

それに比べればこっちの方はオプションを知っていれば楽だったように思います。

まぁ、ドキュメント見ようねってことですね!!

 

ってことで、とっても楽しかったです!!


 

関連記事