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 で出た問題の一つを取り上げてみます。
問題としては、このファイルを渡されて、回答してください。というもの。
d70ec858ac1f7b3094b0e7461b3be67f.txt
うちのチームですと、まずかーみーさんが 「MATZ」という文字列に気づいて、
Ruby 処理系ということに気づき、
私が最初の一文をコピーして検索して、 mruby であることに気づきました。
mruby – github ( https://github.com/mruby/ )
これを git で落としてきて、Linux でてけてけ make して動かす。
・・・一応私がやった順番通りに。
まず最初に動かしてみますと、内部で gets という関数を呼び出しているのですが、
mruby さんにはそういうのは無いようで、困ります。
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
そしてその後、
れっつ VM の挙動をみていきましょうー!!
・・・・はい、詰みます。ておくれます。
実際私は、CTF 中はこんな感じで VM の挙動を見ていたわけですが、
そんなことしなくても [ –verbose ] オプションをつければ、
だだだだーっと人間が読めるコードが出てくるんですよね。
・・・・マニュアル読めばわかったことなのに。。。 orz
ということで気を取り直して。
こんなに読みやすくなったよ!! やったねたえ(ry
まずこの画面から読めますのは、
$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); }
実行すればこんにちは!
mruby ::= RiteVM + parser + code generator + mruby Ruby lib + mruby C lib + binary I/O
— Hiroshi Nakamuraさん (@nahi) 2月 3, 2012
というわけでこんな感じです。
そして今回、この問題に手間取った非常に大きな理由は、
ヘルプ見ましょう。ということ。
ばっちり書いてあったんですよねぇ。。。 うーん。。。
絶対 –verbose オプションを知っていたら解けてた。。。
前回は LLVM が出て、そのときは逆コンパイラ見つけてきて、
元に戻して、コード書いて、探してみたいなことをしていましたが、
それに比べればこっちの方はオプションを知っていれば楽だったように思います。
まぁ、ドキュメント見ようねってことですね!!
ってことで、とっても楽しかったです!!