wivern ロゴ

サイバーセキュリティ研究所

サイバーセキュリティを中心に、軍事、防犯、サーバーの管理と監視、その他、最新技術を研究しています。


「リバースエンジニアリングバイブル」勉強メモ#2

C++ のクラスとリバースエンジニアリング

「リバースエンジニアリングバイブル」#2

前回に続き、「リバースエンジニアリングバイブル」の勉強メモ第2弾です。 実行環境は、Windows7(64bit版)です。実行環境は、Windows7(64bit版)です。

2章 C 言語の文法と逆アセンブル
プログラミングの基本はCの文法であり、リバースエンジニアリングの基本はCコードがアセンブラに変わったときにどう解釈するかである。

アセンブラコードを眺めているときに、アセンブラを表現するコードの塊をCコードに変換する機構が、頭の中で作動するようになる必要がある。

リバースエンジニアリングをするときに最も厄介なものの1つは、プログラマが直接コーディングした部分ではなく、ビルド時にコンパイラが自動生成してくれたコードをフィルタリングする作業である。

[関数の基本的な構造]
EBP はスタックのベースポインターである。
push  ebp   ;これまでのベースアドレスをスタックに保存する。
mov   ebp,esp ;現在のスタックポインター esp を ebp に入れる。

→これまで基準となっていたスタックのベースポインターをいったんバックアップしておいて、新しいポインターを使う。
 ・
 ・ (実際の関数内部の処理:プログラマが直接コーディングした部分)
 ・
mov  esp,ebp ;関数が終了すると、今まで使用していた
pop  ebp   ;スタックの位置を元に戻しておく。
ret

スタックを使用しない簡単な関数の場合はこのようなパターンにならないが、ほとんどの関数は「push ebp」で関数が始まる。

[関数の呼び出し規約( calling conversion )]
代表的なものは、__cdecl、__stdcall、__fastcall、__thiscall の4つ。

①__cdecl 方式:関数の呼び出し元でスタックを補正する
常に call の次の行を見てスタックを整理しているところがあるかをチェックしなければならない。
スタックを調整するコードが登場したら、それは __cdecl 方式の関数だと推測できる(__cdecl 方式は、関数の外でスタックを調整する)。
call の後で「add esp,n」というようにスタックを調整する。n は4の倍数で、n ÷4が引数の数。

②__stdcall 方式:関数内でスタックを処理する。
Win32 API では、主に __stdcall 方式を使用する。
関数の最後で「retn n」というようにスタックを調整する。n は4の倍数で、n ÷4が引数の数。
retn があって、「retn n」のように数値を指定せずに call した後に「add esp,n」も見当たらなかったら、その関数は__stdcall 方式で、パラメーターがないタイプだと言える。

③__fastcall 方式
関数のパラメーターが2つ以下の場合、引数を渡すときに push を使用せず、ECX レジスタと EDX レジスタを使用する。
メモリを使用するよりもレジスタを使用した方がはるかに高速になる。
引数が2個以下で頻繁に使用される関数に使われるのが一般的である。

④__thiscall 方式
__thiscall は C++ のクラスで使用される方式である。
特徴は、現在のオブジェクトのポインター( C++ の this ポインター)を ECX に入れて渡すという点である。
そのクラスで使用しているメンバー変数や各種の値は、ECX ポインターにオフセットのアドレス値を足す方法( ecx + x )で使用できる。

C 言語のコードを作成して逆アセンブルするとき、書籍や Web などに出たものとは少し違ったり、どこかが省略されていたりする。
それは、コンパイラの最適化オプションをどう設定したかによって、バイナリのサイズと生成されるコードが少し異なってくるからである。
自分が作成したコードをそのままの形で確認したい場合は、最適化オプションをデフォルトに変更する必要がある。

[ if 文]
[ebp-4]:ローカル変数( b )
[ebp+8]:1番目の引数( a )
EAX には関数の戻り値が入るため void 型ではなく、関数の後半には常に EAX に値を設定するコードが登場する。
「 pop ecx 」が抜けているのでないか?
[ループ]
引数     c:[bp+8]
ローカル変数 d:[bp-4]
カウンタ   i:[bp-8]

[構造体と API Call]
リバースエンジニアリングをするときは、スタックポインターだけを見て構造体の大きさがどのくらいで、その API の引数にはどのようなものが入っているかを把握することが必須である。
リバースエンジニアリングをするときに、スタック空間確保のコードを見るだけで、それが構造体を表現しているとすぐに判断することはできない。
→構造体ではなく、ローカル変数だけでそのサイズを使用する場合もあるから。
ZeroMemory() はマクロ関数で、(実体としては)memset() を使用する。
CALL の前に PUSH するのは、関数の引数である。
引数はスタックの LIFO( Last In First Out) の特性のため、元の順番と逆方向に入る。

リバースエンジニアリングが上達するには、まずコーディングの経験が豊富である必要があり、加えて、たくさんのコードを逆アセンブルした経験が必要である。
「 コーディングの経験」とは、C 言語の経験?アセンブラの経験?それとも両方?


記述に際しては、細心の注意をしたつもりですが、間違いやご指摘がありましたら、こちらからお知らせいただけると幸いです。


→「リバースエンジニアリングバイブル」勉強メモ#3
←「リバースエンジニアリングバイブル」勉強メモ#1


« 戻る