CODEGATE2012 Binary200 Writeup

先日のCODEGATE2012 予選。
一応、少しでも取りかかった問題については、いろいろな人の書かれたWriteupを参考に
「自分で理由をつけながら答えを導出できる」レベルまで落としこんでいたのですが、
Binary200でかなりつまずいたので、メモがてらWriteup。

使用したツール

  • OllyDbg 1.10
  • OllyDbg 2.01
  • IDA 5.0
  • PEiD 0.95
  • ImpREC 1.7c

問題


Find a printable string that the program would print ultimately.
Download

ファイルの解析

まず、ファイルの種類を調べる。


[dolphin@siro BINARY200]$ file A1A81BBD9D2FD44FAE8013E753830464
A1A81BBD9D2FD44FAE8013E753830464: PE32 executable for MS Windows (DLL) (GUI) Intel 80386 32-bit


WindowsのDLLだと分かるので、とりあえずOllyDbg 1.10で開く。すると、こんなメッセージが。

圧縮されているらしい。確かに、Pack系の目印ともいえるPUSHADが見受けられる。


1001112B . 60 PUSHAD


パッカーを調べるため、PEiDを使う。

PEtiteでパックされていることが分かる。
適当なアンパッカーを試してみたが、うまくアンパックできないので、手動アンパック(manual unpacking)を行う。

手動アンパック

手動アンパックの手順は、以下の通りである。IATには、外部DLL関数のインポート情報が格納されている。

  1. 実行させる
  2. アンパックされた実行コードがメモリ上にロードされる
  3. メモリ上にロードされた実行コードをダンプする
  4. ダンプしたファイルのIAT(Import Address Table)を修正する


[実行させる]
実行させるには、EXEであればそのまま実行させればよいが、
今回はDLLなので、OllyDbg 1.10を用いて、デバッガ上で実行させる。
単純に、OllyDbg 1.10で開いて、F9(Run)を押すだけである。
その際、以下のようなメッセージが表示されるが、OKボタンを押して閉じ、Shift+F9などで例外を抜ければよい。


[アンパックされた実行コードがメモリ上にロードされる]
すると、OEP(Original Entry Point: プログラムの開始地点)へとJMPする命令でが現れる。


1001110F > $- E9 8414FFFF JMP binary20.10002598
この場合、10002598がOEPとなる。このOEPがないと、IATを修正できないので、メモしておく。


[メモリ上にロードされた実行コードをダンプする]
OEPへJMPし(F8などで1命令実行させればよい)、OllyDbgのプラグインであるOllyDumpを用いてダンプする。
このとき、OEPにジャンプしても、命令が表示されない場合があるが、Ctrl+Aを押して再解析すればよい。
Dumpボタンを押して、適当にbin200.dllのような名前をつけて保存する。


[ダンプしたファイルのIATを修正する]
今度はImpRECでIATを修正する。

  1. ダンプしたファイルをOllyDbgで開く。(エラーが出ても、とりあえずOKを押せばよい。)
  2. ImpRECを起動し、Attach to an Active Process から loaddll.exe を選ぶ。
  3. Pick DLL ボタンを押して、問題のDLLファイルを選択する。
  4. OEPの値を、メモした値に書き変える。(10002598なら、00002598と入力する。)
  5. AutoSearch ボタンを押す。
  6. Get Imports ボタンを押す。
  7. Fix Dump ボタンを押し、ダンプしたファイルを選択する。
  8. ダンプしたファイル名に"_"がついた名前で保存される。


こんな感じになっていれば成功である。


以上で手動アンパックは完了となる。

実行コードの解析

アンパックしたDLLファイルをOllyDbg 1.10で開こうとしても、エラーが出る。
そこで、IDA か OllyDbg 2.01 を使って開く。
Exportされている関数が、"x" と "_c@4"(IDAで見ると"c(x)")となっている。
ここで、各関数を見ると、"SetServiceState" や "RegisterServiceCtrlHandler" などの呼び出しが見つかり、
サービスとして動作するということが分かる。
特に、IDAで見るとよく分かるが、 "x" には fopen やら fputs やらが見受けられ、怪しいことが分かる。
実際、OllyDbg等でDLLを実行してやり、適当なところで "x" を呼び出すと、
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\
にサービスが登録され、"x" が呼び出されることも分かる。


IDAで "x" を見ると、fopen やら fputs やらがあるので、
その上を見てみる。すると、いくつか CALL がある。
特に怪しい CALL が 1BC0 で、xor、shl、shrを大量に使っている。
ここが暗号化、あるいは復号化のコードだと見て間違いないだろう。


もう一つ、気になるものとして、次のコードがある。

localtimeで取得した値の8バイト目と 6 を比較している。
localtimeについて調べればすぐ分かるが、8バイト目は「時間」を表す。
つまり、6時かどうかをチェックしているコードである。
6時なら、var_238には6が入る、という仕組となっているようだ。
この関数は、6時に実行される必要性が高いと考えられる。

直接実行によるアプローチ

一度は成功したけど、後から再現しようとしたけれど、うまくいかなかった。
なので、ここでは記事にしていません。
下のほうに書いてある、参考サイトの3つ目を見てもらったほうが早そうなので、割愛。

静的解析によるアプローチ

暗号化アルゴリズムの特定

先にふれた、怪しいCALL先 10001BC0 を見てみる。
すると、これまた怪しい定数が現れる。

この定数でぐぐると、TEA(Tiny Encryption Algorithm)がヒットする。
その後の shl や shr 、add や sub などを参考にしながら、TEAのコードと比較すると、
どうもTEAではないことがわかる。
TEAのWikipediaには、See Alsoとして、RC4やXTEA等が並んでいる。
それぞれのコードを見ていくと、XTEAのコードであると分かる。

引数解析

地道に10001BC0の引数を解析していく。
x+21Eでは、復号化として10001BC0を呼び出していることが分かる。
鍵は16バイト(128ビット)、暗号化されるデータは8バイトなので、
大体こんな感じ。

6時に実行した場合、ecxは6なので、1000A158(hex)に48(decimal)を足して、
1000A188のValueが暗号化されたデータであると分かる。

ここまでくれば、もう一息。

鍵解析

鍵は、10001B40を呼び出して生成している。
見てみると、単純に、引数で渡された1000B440から読み込んでいるだけと分かる。

早速この鍵を使って復号化してみてもいいのだが、うまくいかない。
IDAだと親切に表示してくれるので分かるが、これらの値は実行時に若干書き換わる。
順を追って見ていこう。


IDAの場合、定数の右側にあるコメントにカーソルを当てると、
その定数に別の値を代入しようとしているコードが表示される。
ダブルクリックすることで、そのコードまで飛んでくれるので、確認する。
その一例を以下に示す。



これは、1000B44Fに0xDEを書き込もうとしている処理である。
分岐があり、CreateThreadの戻り値が0でなければ、書き込む。
API名でぐぐればすぐに分かるが、戻り値が0というのは、関数の実行が失敗したときである。
なので、ここは0xDEが書き込まれなければならない。


このようなトラップがいくつかあるので、全て潰していく。

復号化

適当にプログラムを書いて、復号化する。
ただし、エンディアン問題に注意する。
vは、IDA上では4バイトの整数として表示されているので、逆転する。
また、下記のプログラムで表示される結果も、逆転しなければならない。
(XTEAのコードは、Wikipediaのコピペである。num_roundsは、10001CE9から分かる。)



[dolphin@siro bin200]$ cat mofu.c
#include

void decipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
unsigned int i;
uint32_t v0=v[0], v1=v[1], delta=0x9E3779B9, sum=delta*0x20;
for (i=0; i < num_rounds; i++) {
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
sum -= delta;
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
}
v[0]=v0; v[1]=v1;
}

int main(int argc, char *argv[])
{
int i, j;
uint32_t v[2] = { 0x1EAEA1C7, 0xB3506D02 };
uint32_t key[4] = { 0x1Ea0f5c6, 0xd9ec02f6, 0x59187c2e, 0x6f855dde };
decipher( 0x20, v, key );
printf("%s\n", (char*)v );
printf("\n");
return 0;

}

[dolphin@siro bin200]$ ./mofu
W%I&l)K=


よって答えは、"&I%W=K)l"となる。

感想

これで200かぁ・・・^^;;;