第 4 回 手動で逆コンパイル

第
4回
手動で逆コンパイル
文●愛甲健二
逆コンパイルとは
一般的に、逆アセンブルとは「マシン語から
アセンブラ命令 へコードを変換 すること」で
すが、逆コンパイルはそれをさらに一歩進めて
「マシン語 から十分に可読性 の ある高級言語
へ変換すること」を指します。Java や .NET など
は、バイトコードからソースコードへの復元が
容易に行えるため、いくつかの逆コンパイラも
すでに存在しますが、ネイティブのマシン語に
ついてはやはり難しく、例えば、x86 系のマシ
ン語から C/C++ のソースコードへ 正確に変換
する逆コンパイラといったものは、今のところ
ありません。しかし、マシン語といえども人間
が読めば当然理解できますので、人 の 手によ
る手動での 逆コンパイルは可能です。今回は
sample.exe を使って、手動による逆コンパイ
ルを行っていきましょう。
前回「 sample.exe と全く同じプログラムを
C 言語で作成してください」という課題を出さ
せていただきました( 前回までの 記事 は付録
DVD-ROM に収録していますので、必要に応じ
て参照してください)。C 言語にすると 20 行程
度エンコード処理 の 解析動 で 逆コンパイルを
行う」というのはアセンブルコード解読の練習
にもなりますので、ぜひとも面倒くさがらずに
やってみてください。
では sample.exe を解析していきましょう。
エンコード処理の解析
IDAPro ※ で出力されたアセンブルコードを元
に解析していきますので、まずは sample.exe
を IDAPro で開いてください。
先頭から読 み 進めてもよいのですが、課題
では、すでに encode.cpp が記述されており、
黒塗りされた 4 行のみを特定すればよいので、
メインとなるエンコード処理のみを解析します。
下記が sample.exe の該当部分です。
.text:0040109F sub_40109F proc near
.text:0040109F push edx
.text:004010A0 and eax, 3Fh
.text:004010A3
.text:004010A8
.text:004010AA
.text:004010AC
.text:004010AE
.text:004010AF
mov
add
xor
mov
pop
retn
edx, offset aFkl
edx, eax
eax, eax
al, [edx]
edx
sub_40109F が 1 文 字 に 対 するエンコ ード
処 理 で あり、C 言 語 風 に 書くと return (aFkl
[eax & 0x3F]) となります。004010A0 の and
で eax の 値 の 下 位 6 ビット の みを 有 効 にして
eax を 64 未満 の 値にし、aFkl の 配列に対応さ
せます。IDAPro で見るとわかりやすいですが、
004010A3 の aFkl は「 FKLBaCAcTUDgGHsIxR
yJzMhiNjOtPulvQwEpqSXVmWoYkZ0bdefnr
1¥x00MessageBo 」という途中に 0x00 を含む
64 バイトのデータ列 のアドレスになっており、
aFkl に eax を加算することで、このデータ列の
中のいずれか 1 つの文字のアドレスを指します。
その指した値を戻り値として al にコピーし、関
数を終了します。
sub_40109F は 1 文字に対するエンコード処
理ですが、これを文字列に対して行うのが、次
の sub_4010B0 です。内部で sub_40109F を呼
び出します。
.text:004010B0
.text:004010B0
.text:004010B0
.text:004010B0
.text:004010B1
.text:004010B3
.text:004010B4
.text:004010B5
.text:004010B8
.text:004010BB
.text:004010BB
.text:004010BD
.text:004010BE
.text:004010C1
.text:004010C6
.text:004010C9
.text:004010CB
sub_4010B0 proc near
arg_0 = dword ptr 8
arg_4 = dword ptr 0Ch
push ebp
mov ebp, esp
push edx
push ecx
mov edx, [ebp+arg_0]
mov ecx, [ebp+arg_4]
loc_4010BB:
xor eax, eax
dec ecx
mov al, [edx+ecx]
call sub_40109F
mov [edx+ecx], al
test ecx, ecx
jnz short loc_4010BB
※ 無料で入手できるIDAProのバージョンが、4.9から5.0に上がっています。ジャンプ系の命令を適切に解釈し、逆アセンブルウィンドウ
においてもグラフィカルに分岐を表示する機能も加わっているので、ぜひ使ってみてください。http://www.hex-rays.com/idapro/
idadownfreeware.htm
1
.text:004010CD
.text:004010CE
.text:004010CF
.text:004010D0
pop ecx
pop edx
leave
retn 8
sub_4010B0 は文字列に対してエンコードを
行う関数で、004010C1 にて 1 文字エンコーダー
の sub_40109F を呼び出しています。関数とし
ては 2 つの 引数 arg_0( = 文字列 のアドレス)
、
arg_4( = 文字列のサイズ)を受け取ります。
実 際 に エ ン コ ード を 行 っ て い る 部 分 は
004010BB ∼ 004010CB ま で の ル ー プ 処 理
で、arg_0 の後ろから 1 文字ずつを al にコピー
し、sub_40109F を 呼 び 出し、 そ の 戻り値 を
ま た arg_0 の 同 じ 場 所 に 格 納 し て い ま す。
004010C9 にて、文字列のサイズである ecx が
0 になるまでエンコード処理を行っているため、
すべての 文字に対して sub_40109F を実行し
ます。
以上から、課題となっていた encode.cpp は
以下のように書けます。
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
unsigned int i;
char data[256];
char t[] = "FKLBaCAcTUDgGHsIx"
"RyJzMhiNjOtPulvQwEpqSXVmWo"
"YkZ0bdefnr1""¥x00""MessageBo";
if(argc != 2)
sprintf(data, "HELLO");
else
sprintf(data, "%s", argv[1]);
for(i=0; i < strlen(data); i++)
data[i] = t[data[i] & 0x3F];
printf("data=%s¥n", data);
return 0;
}
ちなみに、 このアルゴリズムは不可逆 です
のでデコードできません。当たり前ですが 1 バ
イト値、256 個 の いずれかの 値を示 すデータ
を 0x3F で AND 演算して、64 個 のいずれかの
値に置換するので、 例えば encode.cpp の 配
列 t の先頭である「 F 」という値をデコードしよ
うにも 1100 0000( 192 )
、1000 0000( 128 )
、
0100 0000( 64 )、0000 0000( 0 )の 4 つの値
に戻る可能性を持っています。ただ、入力が表
示可能文字だけであったり、テキストとして意
味を成しているものならば、 ある程度 の 推測
はできるかと思います。
また、ここまで読 み 進めて気がついた方も
いるかもしれませんが、 アセンブルコードは、
1 命令ごとの意味を理解するのは簡単ですが、
関数やループといった「まとまった処理」とし
ての意味を理解するのは難しいのです。しかし、
これが正確にできなければ解析のしようがあり
ませんので、命令をひととおり覚えたら、次は
まとまった処理を解読する訓練をしましょう。
全体の解析
今回 の 課題 はすでに encode.cpp が 与えら
れていたので、sub_40109F、sub_4010B0 の
2 つの関数を読むだけでよかったのですが、本
来の解析業務では、右も左もわからない状況
ですので、最初からアセンブルコードを読み進
めていくしかありません。とはいっても、いち
ばん初めの取っ掛かりは特徴的な文字列であっ
たり、call されてそうな API にブレイクポイント
を仕掛けて、といった感じで少しずつ解析場所
を狭めていき、ある程度のところまで来たら
「こ
れはどのような処理をする関数なのか ? 」を特
定していきます。まぁsample.exe のような小
さいプログラムは、 最初から全部 の 関数を読
んでいってもよいですが。
では、せっかくなのでエンコードには直接関
係ない関数の方も読 んでいきましょう。なお、
誌面の関係上、アセンブルコードを載せられな
いので、以降は IDAPro で閲覧しながら読み進
めてください。
・sub_401000
まず sub_401000 ですが、 これは「 esi から
edi へ ecx の 値だけデータをコピーする関数」
です。00401001 でコピー 先が al なので、1 バ
イト単位でコピーされます。コードも短いので
比較的簡単に解読できると思います。
void sub_401000(void)
{
do{
ecx--;
edi[ecx] = esi[ecx];
}while(ecx != 0);
}
・sub_40100C
続 い て sub_40100C で す が、 これ は 引 数
( arg_0 ) を 1 つ 受 け 取る関 数 で す。 アドレ
ス 00401011 で、arg_0 は esi へ 格 納 さ れ、
00401018 で al へコピーされ、0040101A でそ
の al が 0 か否かを評価されます。そして 0 でな
ければ、loc_401016 へ戻ります。
つまり、引数 arg_0 にはおそらく文字列のよ
うなものが格納され、 その arg_0 の 終端文字
0x00 が見つかるまで loc_401016 へ戻る、とい
2
う処理だと推測できます。そして loc_401016
のループ処理の中で唯一関係のないレジスタ
ecx が、00401017 でインクリメントされており、
0040101E で関数の戻り値となる eax へ格納さ
れるため、終端文字 0x00 が見つかるまでの長
さが関数の戻り値として返されます。
以上 から sub_40100C は、 引数として 受 け
取った文字列 の「 0x00 を含めた」長さを返す
関数だとわかります。また、1 文字目は無視し
ているので、1 文字目 が 0x00 だった場合 はス
ルーされます。
int sub_40100C(char *arg_0)
{
ecx = 0; esi = arg_0;
do{
esi++; ecx++;
}while(*esi != 0);
return (ecx + 1);
}
・sub_401027
sub_401027 も引数 arg_0 を 1 つ受け取って
処理する関数ですが、今度は 00401041 以降に
エラー 処理らしきものがあります。eax に -1 を
入れていますので、戻り値が -1 になる場合が
あるということです。戻り値が -1 になるから必
ずしもエラー処理というわけではありませんが、
一般的にプログラムを作成する際、関数の戻り
値を負にするのはエラー 処理である場合が多
いので、確定はできませんがそう推測しましょう。
そして loc_401041 へ進むのは 00401033 か
らの jz であり、条件は al が 0 の場合です。つま
り、引数 arg_0 の先頭から探していって終端文
字 0x00 が見つかったらエラーとなるわけです。
そして 00401035 で al と 0x20 を比較していま
す。0x20 はスペースなので、終端文字が見つ
かる前にスペースが見つかったら、00401039
と 0040103B でスペ ース の 次 の 文字 の アドレ
スを戻り値として eax へコピーし、関数を終了
しています。
以上から、sub_401027 は引数 arg_0 を先頭
から検索し、スペースが見つかったらその次の
文字のアドレスを戻り値として返し、もしスペー
スが見つからなければ -1 を返す関数だとわか
ります。
int sub_401027(char *arg_0)
{
esi = arg_0;
do{
esi++;
if(*esi == 0)
return -1;
}while(*esi != 0x20);
3
}
return (esi + 1);
・sub_40104B
s u b _40104B は G e t C o m m a n d L i n e 、
CommandLineToArgvW と い った API を 呼
び 出して い ます。 これらはプログラム 実 行
時 にコ マ ンドラ イン に 入 力 さ れ た 文 字 列 を
取 得 する 関 数 で、 ここで 取 得した 文 字 列 を
0040106F に て sub_401027 へ 渡 し て い ま
す。GetCommandLineW は UNICODE 版 で、
GetCommandLineA は ASCII 版です。
最 初 に、CommandLineToArgvW を 使 っ
て プ ロ グ ラ ム へ 渡 さ れ た 引 数 の 数 pNum
Args を 取得して、 そ の 数 が 2 だったならば、
引 数 が 渡 さ れ て いると判 断し、 改 め て Get
CommandLineA を使い ASCII 文字としてコマ
ンドライン文字列を取得します。
この GetCommandLine は、実行されたプロ
グラム名も含めたコマンドライン文字列を取得
しますので、sub_401027 を呼び出してスペー
スを探索し、引数となる部分だけを取り出しま
す。ただ、この実装だと実行プログラムパスの
中にスペースが使われていたらアウトですが、
そこは仕様ということでお許しください(汗)。
スペースがなければ -1 を返し、スペースが
あ れば「プログラム 実 行 時 にコマンドライン
から引数 が 渡 された」 と考 えて loc_401083
へ ジャン プし ま す。 loc_401083 以 降 で は、
sub_40100C を呼び出し、 引数 の 長さを取得
して、その 引数 の サイズ分だけ arg_0 の 指す
アドレスへデータをコピーしています。
int sub_40104B(char *arg_0)
{
int pNumArgs;
LPWSTR *lpszArgs = CommandLineToArgvW
(GetCommandLineW(), &pNumArgs);
if(pNumArgs != 2)
return -1;
esi = sub_401027(GetCommandLineA());
if(esi == -1)
return -1;
ecx = sub_40100C(esi);
edi = arg_0;
sub_401000();
return 0
}
エントリポイント
最後にエントリポイントの 処理 です。 最初
に 256 バ イト の 領 域 を ス タックに 確 保して、
sub_40104B を呼 び出して、 プログラムへ 渡
された引数を確認します。もし引数がなければ
crackme.exe パスワード比較箇所
00402858 PUSH crackme_.004071E4
0040285D LEA EDX,DWORD PTR SS:[ESP+10]
00402861 PUSH EDX
00402862 CALL DWORD PTR DS:[<&KERNEL32.lstrcmpA>]
00402868 TEST EAX,EAX
;
;
;
;
/String2 = "WizardBible"
|
|String1
\lstrcmpA
「 HELLO 」という文字列を buff へ格納します。
sub_4010B0 はエンコードを行う関数なので、
buff の 中にあるデータ列をエンコードし再 び
buff に入れ、それを MessageBoxA で表示して
プログラム終了です。
しかし、ことリバースエンジニアリングに限れ
ばそういったものよりも、地道に解読していく
根気こそが最も重要なスキルかもしれません。
void start(void)
{
char buff[256];
if(sub_40104B(buff) != 0){
ecx = 6;
esi = "HELLO";
edi = buff;
sub_401000();
}
sub_4010B0(buff,
sub_40100C(buff) - 1);
MessageBoxA(GetActiveWindow(),
buff, "MessageBox", 0);
}
第 2 回 目 以 降 何 も 触 っ て い な か っ た
crackme.exe ですが、アセンブラも学習したこ
とですし、再び crackme.exe の解析に戻りましょ
う。第 2 回目の最後、ユーザー 名とパスワード
を要求するダイアログボックスが表示されたと
ころで解析は止まっていますから、ここから始
めましょう(図 1 )。もし内容を忘れている方が
いましたら、付録 DVD 収録の第 2 回を参照して
ください。また、元々の crackme.exe と、日付
制限を回避した crackme_ex.exe も DVD に収
録しています。
で は 解 析 を 始 め ま す。 ま ず テ キ ストボッ
クス から 入 力 さ れ た テ キ スト を 得 て い る た
め、GetWindowText、GetDlgItemText、Get
DlgItemInt 辺りにブレイクポイントを仕掛けて
プログラムを実行します。そしてテキストボッ
クスに適当な文字列を入力して、OK ボタンを
クリックすると、GetWindowTextA 関数で処理
が止まるので、そのまま関数を抜けるとアドレ
ス 00402818 へたどり着きます。
00402818 がテキストボックスから入力され
たテキストを得ている場所なので、ここから下
に降りていくとパスワードと比較している箇所
が見つかります(上掲の部分)。
String2 が「 WizardBible 」で、String1 がユー
ザー名として入力された文字列です。その 2 つ
を lstrcmpA で比較しています。以上から、ユー
ザー 名は「 WizardBible 」だと考えて間違いな
さそうです。試しにこのユーザー名を再度入力
してみましょう(図 2 )。
図 1 では、ユーザー名が間違っているとのメッ
セージが表示されましたが、図 2 ではパスワー
ドが間違っているとのメッセージに変わりました。
つまり、ユーザー 名は合っているというわけで
す。これでユーザー名は判明しました。次はパ
スワードです。
以上で sample.exe のすべてのコードを読み
終えましたが、いかがだったでしょうか。基本
的なアセンブラ命令を覚えてさえいれば、 そ
れほど難しくはなかったと思います。
逆コンパイル はとても地味 で 面倒くさい 作
業ですが、マシン語を読み続けていれば必ず
答えが出るという点においては、 努力が報わ
れやすい技術だとも言えます。
コンピューター 技術には時として、天才的な
発想力や、高度な数学的知識や、的確に問題点
を探す嗅覚などが必要になる場合もあります。
図1 crackme_ex.exeを実行。
「ユーザー名が間違っています」
と表示される
再びcrackme.exe へ
パスワードは何?
図 2 ユーザー名「 WizardBible 」が判明。今度は「パスワード
が間違っています」となる
00402868 以降 の 処理を読 み 進めていけば
パスワードも判明しそうですが、今回はここま
でとし、この続きは次回にて行いたいと思いま
す。では、またお会いしましょう。
4