リバースエンジニアリングチャレンジ2009の結果

今年の成績は、こんな感じ。
(一問目は5秒ではなく5分。二問目も同様)

設問 経過時間 順位
一問目 00:05 三位
二問目 00:53 八位
三問目 未クリア -
四問目 未クリア -

情けない話ですが、三問目の途中で時間切れ。
いや、心が折れたというべきか。

以下、解けた部分だけ解説。

一問目

去年と比べるととても簡単。
変換すらないので、逆アセンブルして出てきたマジックナンバーを引数に渡すだけ。
「00401157」でコマンドライン引数の数が4つであることを確認。
以下は、順番にatol()関数を使って、引数に渡された数字の文字列を数値に変換、マジックマンバーを一致するかどうかをチェックしていく。

二問目

まさかのブロック崩し

ゲームをクリアすれば答えが出るような気がするが、とても不可能なゲーム内容になっている。(バーの左右はマウス操作できるが、自動的に上方向に移動してしまう!)
しかし、コードをみるとその必要はない。
「004043A0」からWord値が11個あり、「00401423」のMessageBoxWで表示とわかる。
この11個のWord値は、コードのいろいろな部分から、それぞれバラバラに設定される。
さらに「00401080」の関数内で、各Word値から「00404754」にある値を引いていることがわかる。
この「00404754」を追っかけていくと、「00401143」で5がセットされる。
なので、11個のWord値を全て取り出して、5を引けば答えとなる。

三問目

去年も次から次にいろんなフォーマットが出てきた三問目。
今年は6種類のエンコーダを全て適応して変換されたデータ元に戻す。
つまり、6つのデコーダを作らないといけない。

encode6

エンコードされたデータをみてみると、「Salted__」というヘッダーがヒントになっている。
またエンコーダのコードをみると内部にPerlスクリプトを持っていて、実行してみるとPerlスクリプトを一度ファイルに落としてからそれを実行しているのが分かる。
そのPerlスクリプトが以下のような感じ。

package main; shift @INC;
#line 1 "script/encode6.pl"
#!/usr/local/bin/perl

use Crypt::CBC;

$key1 = 'I LOVE PAR!';
$key2 = 'ACTIVE PERL';
$key3 = 'CRYPT::CBC';

open(R, "$ARGV[0]") or die "err";
open(W, ">$ARGV[1]") or die "err";

binmode R;
binmode W;

$cipher1 = Crypt::CBC->new($key1, 'DES');
$cipher2 = Crypt::CBC->new($key2, 'DES');
$cipher3 = Crypt::CBC->new($key3, 'DES');

$data = ""; $new = "";
while(read(R, $new, 1)){
	$data .= $new;
}

$data = $cipher1->encrypt($data);
$data = $cipher2->encrypt($data);
$data = $cipher3->encrypt($data);

print W ($data);

close W;
close R;

これでTriple-DESのキーが三つ判明した。
あとはopensslコマンド等で、デコードできる。

encode5

encode5のデータをfileコマンドにかけてみると、waveファイルだと分かる。
さらにコードをみると、「00401150」の関数でwaveファイルのヘッダーを作り、「00401207」の関数で入力データを変換して出力している。
では、どう変換しているかというと、入力データ1バイトを上位4ビット、下位4ビットにわけ、ランダムに生成した値の中に埋め込んでいる。
シフトして埋め込んであるので、デコーダを書くとこんな感じ。

f = open('password.e0.e1.e2.e3.e4.e5', 'rb')
f.read(0x2c)

dataList = []
while True:
	try:
		i1 = ord(f.read(1))
		i2 = ord(f.read(1))
		i3 = ord(f.read(1))
		i4 = ord(f.read(1))
	except:
		break
	i = i2 << 8 | i1
	i = (i >> 2) & 0x0f

	j = i4 << 8 | i3
	j = (j >> 6) & 0xf0

	i = i | j

	dataList.append(i)
f.close()

f = open('password.e0.e1.e2.e3.e4', 'wb')
for i in dataList:
	f.write(chr(i))
f.close()

最初に0x2C読み飛ばしている分が、waveのヘッダーの部分。

encode4

データをみると256個の同じデータが連続して出てくるので、encode5のデコーダを何か間違えたんじゃないかと不安になるが、それであっている。
このエンコーダはamd64Windows用の実行体なので、逆アセンブルするのがちょっと大変だけど、中身は割と簡単。
「00000000004011A0」から始まるループで(VAが長い!)、入力値を0x55でxorして256回出力している。
それだけ、たぶん。(ここからちょっと自信がなくなってきた)

encode3

これが解けなかった。
エンコーダはARMのLinux環境用。Linux Zaurusでなんとか動かないかと思ったが、ライブラリの配置が違うのか?バージョンが違うのか?うまくいかなかった。
コードを読むしかないが、慣れないARMのコードでよくわからず。
他の参加者の方々をみるとqemuでエミュレートさせて実行なんという人もいた。
なるほど、勉強になるなぁ。

encode2

encode3が解けてないので、ざっくりみただけ。
たぶん、JNIでJavaを呼び出している。
classファイルを取り出して、ごにょごにょするんじゃないかな?

encode1

実はこっちのほうが簡単という罠。
16bitのx86命令で、入力データからキーを引き算。
キーは0x00から0x41の範囲をぐるぐるする。

四問目

公式で全問公開されたら、確認したい。


去年は一問目しか解けなかったので、進歩はしているようだが、もうちょっと頑張りたかった。
やはりx86以外のCPU命令がまったく分からないのが、難点か。
勉強、勉強。