Chunnomi Online
テーブル初期化
最終更新:
chunnomi
-
view
RtlInitializeGenericTable(p146~)
先述したように、汎用テーブルAPIを調べるためには、そのデータ構造から
見ていくのがベストだ。データ構造について全てを把握する必要は無いにしても、
その概要を知ることは、このAPIの役割を解き明かす上で大いに役立つ。
といいつつも、データ構造に関していくつかのヒントを提示してくれる
可能性の高い(その名前から判断して)関数:RtlInitializeGenericTableから解析を始めよう。
見ていくのがベストだ。データ構造について全てを把握する必要は無いにしても、
その概要を知ることは、このAPIの役割を解き明かす上で大いに役立つ。
といいつつも、データ構造に関していくつかのヒントを提示してくれる
可能性の高い(その名前から判断して)関数:RtlInitializeGenericTableから解析を始めよう。
この関数が何をどのように処理するのかを判断する前にまず、より基本的な事項、
つまり、関数のコーリングコンベンションは何か、この関数は何個の引数を受け取るのか、
を調べる。コーリングコンベンションとは、引数をどう渡すか、関数終了時にスタックを
後始末するのは誰か、についてのルールだ。コーリングコンベンションについて
いくつかのスタンダードがあるが、Windowsでは、stdcallコーリングコンベンションが
使われることが多い。stdcallでは、関数(呼び出される側)がスタックの後始末を行い、
スタックにおいて第1引数が最も下位アドレスに置かれる(つまり、呼び出し側が引数を
逆順にスタックにプッシュする)ことになる。
コーリングコンベンションについてのより詳しい説明については、付録Cを参照してほしい。
つまり、関数のコーリングコンベンションは何か、この関数は何個の引数を受け取るのか、
を調べる。コーリングコンベンションとは、引数をどう渡すか、関数終了時にスタックを
後始末するのは誰か、についてのルールだ。コーリングコンベンションについて
いくつかのスタンダードがあるが、Windowsでは、stdcallコーリングコンベンションが
使われることが多い。stdcallでは、関数(呼び出される側)がスタックの後始末を行い、
スタックにおいて第1引数が最も下位アドレスに置かれる(つまり、呼び出し側が引数を
逆順にスタックにプッシュする)ことになる。
コーリングコンベンションについてのより詳しい説明については、付録Cを参照してほしい。
使われているコーリングコンベンションを決定するためにはまず、関数を終了させるRET命令を探す。
あなたはすぐに、関数の一番最後にRET 14という命令があることに気づくだろう。これは
数値を引数として受け取るRET命令であり、このことから重要なことが分かる。
RETに渡された値は、プロセッサに対して、スタックポインタを何バイト戻すかを指定している。
これは、関数(呼び出される側)の方でスタックの後始末を行っている、ということを意味するので、
少なくとも、cdeclコーリングコンベンション(常に関数呼び出し側にスタックの後始末を任せる)
ではない。
あなたはすぐに、関数の一番最後にRET 14という命令があることに気づくだろう。これは
数値を引数として受け取るRET命令であり、このことから重要なことが分かる。
RETに渡された値は、プロセッサに対して、スタックポインタを何バイト戻すかを指定している。
これは、関数(呼び出される側)の方でスタックの後始末を行っている、ということを意味するので、
少なくとも、cdeclコーリングコンベンション(常に関数呼び出し側にスタックの後始末を任せる)
ではない。
次に、この関数で使われるレジスタは全てこの関数内だけで初期化されているので、
レジスタを使った引数渡しは一切無いことにお気づきだろうか。このことから、
レジスタECXとEDXを使って引数を渡す_fastcallコーリングコンベンションではないことが判明する。
レジスタを使った引数渡しは一切無いことにお気づきだろうか。このことから、
レジスタECXとEDXを使って引数を渡す_fastcallコーリングコンベンションではないことが判明する。
さらに、修飾された関数名を持つC++メンバ関数コーリングコンベンションでも無い。
このコーリングコンベンションでは、C++の関数名はクラス名と引数型名を使って修飾される。
これは、非アルファベット文字と複数名(少なくともクラス名と関数名)から構成されるので、
特定するのは簡単だ。エクスポートディレクトリから取得した関数名RtlInitializeGenericTableは
明らかにこれに該当しない。
このコーリングコンベンションでは、C++の関数名はクラス名と引数型名を使って修飾される。
これは、非アルファベット文字と複数名(少なくともクラス名と関数名)から構成されるので、
特定するのは簡単だ。エクスポートディレクトリから取得した関数名RtlInitializeGenericTableは
明らかにこれに該当しない。
以上の消去法によって、ここではstdcallコーリングコンベンションが使われていることが判明した。
また、RETに付随する14(10進数では20)という値によって、何個の引数が渡されているかが分かる。
32ビット環境で動作しているので、各引数は32bit(4byte)にアライメントされる。したがって、
最大で5個の引数を受け取ることが分かる。
また、RETに付随する14(10進数では20)という値によって、何個の引数が渡されているかが分かる。
32ビット環境で動作しているので、各引数は32bit(4byte)にアライメントされる。したがって、
最大で5個の引数を受け取ることが分かる。
関数のプロローグ(冒頭の処理)を目に向けると、典型的なEBPスタックフレームを用いていることが分かる。
つまり、古いEBPの値をスタックに保存して、ESPの値をEBPに設定している。
そのスタックフレームにいる間は、EBPの値は変更されることが無いので(エピローグで破棄されるまで)、
オフセット値を使ってスタック上の引数にアクセスすることが容易になる。一般的な構成では、
第1引数は[EBP+8]、第2引数は[EBP+c]...になる。何故そうなるかをよく知らなければ、
スタックフレームについて詳しく説明した付録Cを参照してほしい。
つまり、古いEBPの値をスタックに保存して、ESPの値をEBPに設定している。
そのスタックフレームにいる間は、EBPの値は変更されることが無いので(エピローグで破棄されるまで)、
オフセット値を使ってスタック上の引数にアクセスすることが容易になる。一般的な構成では、
第1引数は[EBP+8]、第2引数は[EBP+c]...になる。何故そうなるかをよく知らなければ、
スタックフレームについて詳しく説明した付録Cを参照してほしい。
通常は、スタックポインタ(ESP)から必要なバイト数を減算することで、ローカル変数に
必要な領域を確保する。しかし、この関数内では、そのような命令が見当たらないので、
ローカル変数は一切使っていないことが分かる。
必要な領域を確保する。しかし、この関数内では、そのような命令が見当たらないので、
ローカル変数は一切使っていないことが分かる。
7C921A3E MOV EAX, DWORD PTR SS:[EBP+8] 7C921A41 XOR EDX, EDX 7C921A43 LEA ECX, DWORD PTR DS:[EAX+4]
1行目では[ebp+8]、つまり第1引数として渡された値をEAXにロードしている。2行目ではEDXに対して、
自身とのXORを代入し直している。mov edx, 0の方がずっと直感的だが、ここではより短いコードを使って、
EDXを0で初期化しているのである。コンパイラは最適化のために、プログラマにとって読みやすいコードよりも
より小さく効率的なコードを生成することを好むのだが、これはその1例を示している。
自身とのXORを代入し直している。mov edx, 0の方がずっと直感的だが、ここではより短いコードを使って、
EDXを0で初期化しているのである。コンパイラは最適化のために、プログラマにとって読みやすいコードよりも
より小さく効率的なコードを生成することを好むのだが、これはその1例を示している。
3行目のLEA命令は初見では分かりにくいかもしれないが、DWORD PTRという接頭辞に
惑わされてはいけない。メモリアクセスは一切行われず、実質的にはアドレスを計算するための
算術演算に過ぎない。ここではECX := EAX + 4が実行される。
惑わされてはいけない。メモリアクセスは一切行われず、実質的にはアドレスを計算するための
算術演算に過ぎない。ここではECX := EAX + 4が実行される。
ここまでで出てきたデータ型、特に第1引数([ebp+8])のデータ型については、ほぼ何も分かっていない。
何か他に分かることがないか、次のコード列を見てみよう。
何か他に分かることがないか、次のコード列を見てみよう。
7C921A46 MOV DWORD PTR DS:[EAX], EDX 7C921A48 MOV DWORD PTR DS:[ECX+4], ECX 7C921A4B MOV DWORD PTR DS:[ECX], ECX 7C921A4D MOV DWORD PTR DS:[EAX+C], ECX
このコード列から分かる重要なことがある。この関数の第1引数はあるデータ構造に対するポインタであること、
そして、そのデータ構造はこの関数内で初期化されている、ということだ。このデータ構造は
汎用テーブルのキーとなっている可能性が非常に高い。したがって、このデータ構造を調べることが、
汎用テーブルの使い方を解明するための鍵になる。
そして、そのデータ構造はこの関数内で初期化されている、ということだ。このデータ構造は
汎用テーブルのキーとなっている可能性が非常に高い。したがって、このデータ構造を調べることが、
汎用テーブルの使い方を解明するための鍵になる。
興味深いのは、この関数が2つのレジスタEAX, ECXを使って、該当のデータ構造にアクセスしていること
である。EAXは第1引数の値を保持し、ECXはそれにオフセットを4足した値を保持している。
そして、データ構造のメンバ変数に対して、EAXかECXのいずれかを使ってアクセスしている。
である。EAXは第1引数の値を保持し、ECXはそれにオフセットを4足した値を保持している。
そして、データ構造のメンバ変数に対して、EAXかECXのいずれかを使ってアクセスしている。
先ほどのコード列が行っている処理を順番に見ていこう。
(1) 構造体の第1メンバ変数を(EDXを使って)0初期化する。構造体にはEAXを使ってアクセスしている。
(2) 第3メンバ変数を、第2メンバ変数へのアドレス(ECX + 4 = EAX + 4 + 4 = EAX + 8)を使って初期化する。
(3) 第2メンバ変数を同じアドレス(自分自身のアドレス)にて初期化する。
(4) 第4メンバ変数を同じアドレスにて初期化する。
(1) 構造体の第1メンバ変数を(EDXを使って)0初期化する。構造体にはEAXを使ってアクセスしている。
(2) 第3メンバ変数を、第2メンバ変数へのアドレス(ECX + 4 = EAX + 4 + 4 = EAX + 8)を使って初期化する。
(3) 第2メンバ変数を同じアドレス(自分自身のアドレス)にて初期化する。
(4) 第4メンバ変数を同じアドレスにて初期化する。
もしもこのコード列をC言語で書くとすると、次のようになるだろう。
UnknownStruct->Member1 = 0; UnknownStruct->Member3 = &UnknownStruct->Member2; UnknownStruct->Member2 = &UnknownStruct->Member2; UnknownStruct->Member4 = &UnknownStruct->Member2;
一見すると、第2,3,4メンバ変数がポインタであること以外は分からないかもしれない。
少し不思議なことに、この3変数は全て第2メンバ変数へのポインタによって初期化されているが、
これは一体何を意味しているのだろうか? 本質的には、これらのメンバ変数はポインタグループ
(3つのポインタ)へのポインタであることを示唆している。少々分かりにくいのは、それ自身への
ポインタで初期化されている点であるが、これはあくまで一時的なもので、後ほど他の場所を
指すように修正されるのだろうと推測できる。次のコード列に進もう。
少し不思議なことに、この3変数は全て第2メンバ変数へのポインタによって初期化されているが、
これは一体何を意味しているのだろうか? 本質的には、これらのメンバ変数はポインタグループ
(3つのポインタ)へのポインタであることを示唆している。少々分かりにくいのは、それ自身への
ポインタで初期化されている点であるが、これはあくまで一時的なもので、後ほど他の場所を
指すように修正されるのだろうと推測できる。次のコード列に進もう。
7C921A50 MOV ECX,DWORD PTR SS:[EBP+C] 7C921A53 MOV DWORD PTR DS:[EAX+18], ECX 7C921A56 MOV ECX, DWORD PTR SS:[EBP+10] 7C921A59 MOV DWORD PTR DS:[EAX+1C], ECX
最初の2行では、第2引数の値を構造体の第7変数(オフセット0x18)へと代入している。
次の2行では、第3引数の値を構造体の第8変数(オフセット0x1C)へと代入している。
C言語で書くと、次のようになるだろう。
次の2行では、第3引数の値を構造体の第8変数(オフセット0x1C)へと代入している。
C言語で書くと、次のようになるだろう。
UnknownStruct->Member7 = Param2; UnknownStruct->Member8 = Param3;
さらに次のコード列を見てみよう。
7C921A5C MOV ECX, DWORD PTR SS:[EBP+14] 7C921A5F MOV DWORD PTR DS:[EAX+20], ECX 7C921A62 MOV ECX, DWORD PTR SS:[EBP+18] 7C921A65 MOV DWORD PTR DS:[EAX+14], EDX 7C921A68 MOV DWORD PTR DS:[EAX+10], EDX 7C921A6B MOV DWORD PTR DS:[EAX+24], ECX
前回のコード列と全く同様に、残りのメンバ変数が初期化されているだけだ。
第9メンバ変数(オフセット0x20)が第4引数、第5, 6メンバ変数が0、第10メンバ変数が
第5引数([EBP+18])に初期化される。
第9メンバ変数(オフセット0x20)が第4引数、第5, 6メンバ変数が0、第10メンバ変数が
第5引数([EBP+18])に初期化される。
以上で、構造体の初期化シーケンスは完了する。残念だが、実際にデバッガを使って、
引数として渡される値を見てみなければ、そのデータ型についての詳しいことは分からない。
1つ言えることはおそらく、この構造体のサイズは40バイトであり、各メンバ変数がそれぞれ
4バイトであると仮定するならば、10個のメンバ変数を含むだろうということだ。
この時点でおぼろげながら判明したデータ構造の定義を次に示すが、より詳しいことは
後々明らかになっていくだろう。
引数として渡される値を見てみなければ、そのデータ型についての詳しいことは分からない。
1つ言えることはおそらく、この構造体のサイズは40バイトであり、各メンバ変数がそれぞれ
4バイトであると仮定するならば、10個のメンバ変数を含むだろうということだ。
この時点でおぼろげながら判明したデータ構造の定義を次に示すが、より詳しいことは
後々明らかになっていくだろう。
struct TABLE { UNKNOWN Member1; UNKNOWN_PTR Member2; UNKNOWN_PTR Member3; UNKNOWN_PTR Member4; UNKNOWN Member5; UNKNOWN Member6; UNKNOWN Member7; UNKNOWN Member8; UNKNOWN Member9; UNKNOWN Member10; }