12.1 コマンドライン引数による main 関数の特定#
本章のプログラム TEST_REVERSER.exe は、この演習から静的逆向きとデバッグの新しい知識を学ぶものです。
まずプログラムの構造を見て、IDE で確認します。
上の図から、このプログラムは 32 ビットアーキテクチャで、vc++ 2015 でコンパイルされています。
次に、プログラムを実行し、ユーザー名とパスワードの入力を促されます。適当にユーザー名とパスワードを入力すると、bad reverser と表示されます。
次に、IDA を開き、ターゲットプログラムをロードします。
重要な部分に移動する方法の一つは、文字列を検索することです。argc、argv などのコマンドライン引数を検索します。C++ で書かれたプログラムなので、関数のプロトタイプは以下の通りです:
int main(int argc, char *argv[])
name から arg などの引数を検索し、ctrl + F で呼び出し検索ボックスを開きます。
上の図の_p_argc_をダブルクリックすると、内容は以下の通りです:
X キーを押して参照を検索します。
上の図のプログラムは_p_argc_
、_p_argv
関数を呼び出し、その後値を main 関数に渡します。
ダブルクリックして main 関数に入ります。
main 関数の 3 つの引数
main 関数の参照(符号付き)
12.2 main 関数のスタック分析#
任意の関数の引数またはローカル変数をダブルクリックして、静的スタックビューに移動します。
上の図から、最下部には関数の引数があり、常に戻りアドレス return address (r) の下にあります。関数を呼び出す前に、引数は最初に push 命令でスタックに渡され、その後戻りアドレスが渡されます。
その上には、main 関数を呼び出す上位関数の ebp 値があります。
上の図で、main 関数が最初に実行する命令 push ebp
は、それをスタックに保存し、esp の値を ebp に渡します。ebp は下の関数引数と上のローカル変数の参照の基準として使われ、最後に sub esp,94h で、0x94 だけ esp を移動させてローカル変数とバッファのためのスペースを作ります。このプログラムでは移動距離は 0x94 で、コンパイラはソースコードに基づいてローカル変数が占めるスペースを計算します。
esp の値はローカル変数の上を指し、ebp は基準を指します。基準の上はローカル変数、下は戻りアドレスと関数引数です。以下の図のように示されます。
したがって、ebp を基準とする関数では、上位関数の ebp 値がpush ebp
でスタックに保存された後、esp の値が ebp に渡されます。00000000 は基準線として、上のアドレスは負(-)、下のアドレスは正(+)です。
上の図で、var_4 の相対アドレスは - 00000004 で、ebp の値を基準とすると、var_4 の実際のアドレスは ebp-4 です。
逆アセンブルビューで、var_4 を使用している任意の場所を右クリックすると、上記の内容を確認できます。
var_4 の上には変数がない空白の領域があり、これはバッファである可能性があります。
ビューを上に移動すると、空白の領域の上に最初の変数 Buf が見えます。以下の図のように。
右クリックして ARRAY を選択すると、以下のウィンドウがポップアップし、配列は 120 個の 1 バイト要素で構成されていることがわかります。したがって、配列のサイズは 120 です。
関数スタックビュー
上の図は ebp 基準を示しており、mov ebp, esp 命令の後、esp は再び 0x94 減少し、最終的にローカル変数領域の上部を指します。以下の図のように、sub esp, 0x94 の後の esp の値です。
上の図で、左側の 00000094 は esp=ebp-0x94 を表し、関数内部で他の関数を呼び出すと、esp はさらに上に移動します。main 関数内部では、main 関数を終了するまで - 0x94 以前のローカル変数に対して操作を行います。
12.3 main 関数のローカル変数#
次に、静的スタックビューからローカル変数を逆向きに分析します。main 関数の引数は既知です。
ローカル変数
上の図で、プログラムはある値を読み取り、ebp の値と排他的論理和を取り、演算後に var_4 に保存します。これはスタックオーバーフローを防ぐためのものです。
ダブルクリックしてsub_4011B0
に入ると、sub_401040
関数が見えます。
sub_401040 関数内部には printf 関数があり、これによりこの関数は文字を印刷するためのものであると判断できます。
その後、size 変数に 8 が代入され、参照から 2 つの参照があることがわかりますが、内容を読み取るだけで変更はされていません。
次に、gets_s 関数があり、gets_s 関数はユーザーの入力を制限します。上の図では最大入力が 8 文字であることが示されており、push eax で引数を渡し、lea で変数 buf、つまりバッファのアドレスを取得します。
ユーザーが 8 文字未満を入力して Enter を押すと、関数は入力を中断し、戻ります。したがって、Buf バッファには最大 8 文字が含まれます。
その後、プログラムはPUSH EDX
を使用してバッファのアドレスをstrlen()
という API 関数に引数として渡し、strlen()
は Buf の文字列の長さを取得し、その結果を var_90 変数に保存します。
12.4 ループとコードブロックのグループ化#
上の図で、青い矢印が指す戻りジャンプはループである可能性があり、var_84
変数がこのループのカウンタとして機能します。0x4019f5
で条件ジャンプがあり、条件が満たされるとループが終了します。カウンタは 0 から始まり、var_90
変数以上になるまで加算されます。
カウンタに 1 を加算
カウンタ変数の値が EAX に渡され、EAX に 1 を加算した後、再びカウンタ変数に戻されます。
上の図で、プログラムはEBP+EDX+BUF
からBUFFER
の最初のバイトを取得します。EBP+BUF
とカウンタを加算し、カウンタは最初は 0 で、毎回ループするごとに 1 を加算し、次のバイトを読み取ります。このようにして、BUFFER
の各バイトの 16 進数をvar_88(初期値0)
変数に加算します。
このループの内容は文字の加算です。
すべて同じ色にマークし、最下部のコードブロックをドラッグして近づけます。
これらの 3 つのブロックをグループ化することができ、ctrl キーを押しながらタブの上でクリックして、各ブロックを順に選択すると、上の色がシアンに変わります。
その後、右クリックしてノードをグループ化します。
最終的な効果
具体的な内容を見たい場合は、右クリックしてノードのグループ化を解除します。
12.5 登録アルゴリズム分析#
ループの内容の下のコード分析を続けます。
上の図で、ユーザーにユーザー名とパスワードの入力を求め、下の sub_4011b0 は printf 関数で、次に gets_s 関数を呼び出します。ユーザー名とパスワードは同じバッファ Buf と最大文字制限 Size を使用します。
プログラムはすでにユーザー名の各文字の合計を計算しているため、ユーザー名の文字列はもはや使用されず、パスワードは同じバッファを使用できます。
次に、atoi 関数を呼び出して入力された内容を 10 進数に変換し、変数 var_94 に保存します。これがパスワード変数です。
その後、プログラムは push edx を通じて var_94 パスワード変数を渡し、push eax を通じて var_88 変数を渡し、これら 2 つの変数を 0x401010 関数の引数として渡します。
0x401010 関数内部に入ると、2 つの引数があり、arg_4 はパスワード変数であるべきです。なぜなら、パスワード変数が最初にスタックに渡されたからです。上の arg_0 引数は var_88 変数の値です。
では、0x401010 関数はこれら 2 つの引数をどのように使用するのでしょうか?
cmp 比較を行う前に、プログラムはパスワード変数 arg_4 を eax に渡し、shl eax,1 を実行します。
shl は eax のビットを左にシフトし、右側の低位を 0 で埋めます。特例として、shl reg,1 は 2 倍に相当します。
したがって、プログラムはパスワード変数を 2 倍にし、arg_0 と比較します。
python の ord 関数を使用して ascii に対応する 10 進数値を計算します。
もし pepe をユーザー名として使用するなら、pepe の文字の合計は以下の通りです:
結果は 0x1aa で、入力されたパスワードは 2 倍にされ、0x1aa と比較されます。したがって、正しいパスワードは 2 倍にした結果が 0x1aa になる必要があります。結果は以下の通りです:
結果は 213 です。
この時、プログラムを開いてユーザー名 pepe、パスワード 213 を入力できます。
成功メッセージが表示されます。
上の図からわかるように、これら 2 つの値が等しくない場合は赤いコードブロックに移動し、0 を返します。等しい場合は緑のコードブロックに移動し、戻り値は 1 です。
戻り値の役割は何でしょうか?
上の図からわかるように、戻り値は var_7D 変数に渡されます。
上の図からわかるように、戻り値が 0 の場合、bad reverser にジャンプし、1 の場合は good reverser にジャンプします。
この章では、登録を回避するための逆向き分析の方法について主に説明しました。主な内容には、関数スタック、ローカル変数、登録アルゴリズム分析などが含まれます。