ネイティブコード生成モジュールを作ってみよう
前回作成した「ExeCreator.exe」を改造して、ネイティブコードを自由に生成できるモジュールを作ってみましょう。
今回のサンプルでは、MessageBoxA
およびwsprintfA
を呼び出したいため、前回の「ExeCreator.cpp」から、インポートセクションにおいて若干コードの変更が生じています。この変更に関してはソースコードをダウンロードして確かめてみてください。ここでは、新たに追加する「opcode.cpp」の内容を重点的に解説していきます。
それでは前回のExeCreatorプロジェクトを開き、「opcode.cpp」を新規作成しましょう。
まず、「opcode.cpp」の先頭部分では、ModR/M、SIBバイトに必要なフラグを定義しています。REG_EAX
~REG_EDI
がレジスタを識別するための定数になります。REGISTER_OPERAND
は下位3ビット以外のビットを排除し、正確なレジスタ識別値を得るためのマクロです。
これから定義するネイティブコード生成モジュールでは、NativeBuffer
バッファにネイティブコードを書き込んでいきます。
#include "stdafx.h" ///////////////////////////////////////////////// // ModR/Mバイト、SIBバイト、ディスプレースメント ///////////////////////////////////////////////// //スケール #define SCALE_NON (char)0x00 #define SCALE_2 (char)0x40 #define SCALE_4 (char)0x80 #define SCALE_8 (char)0xC0 //インデックスなし #define INDEX_NON 0x04 //Mod(モード) #define MOD_BASE (char)0x00 #define MOD_DISP32 (char)0xFF #define MOD_BASE_DISP8 (char)0x40 #define MOD_BASE_DISP32 (char)0x80 #define MOD_REG (char)0xC0 //レジスタを示す定数 #define REG_EAX 0 #define REG_ECX 1 #define REG_EDX 2 #define REG_EBX 3 #define REG_ESP 4 #define REG_EBP 5 #define REG_ESI 6 #define REG_EDI 7 #define REGISTER_OPERAND(reg) (reg&0x07) extern BYTE NativeBuffer[65536]; extern int iNativeLength;
次に、ネイティブコードの中核となるアドレッシングモードを解決するためのset_mod_rm_sib_disp
関数を定義します。
パラメータ | 説明 |
mod |
アドレッシングモード(MOD_BASE 、MOD_DISP32 、MOD_BASE_DISP8 、MOD_BASE_DISP32 、MOD_REG のいずれか) |
reg |
レジスタ識別値 |
scale |
スケール(SCALE_NON 、SCALE_2 、SCALE_4 、SCALE_8 のいずれか) |
index_reg |
インデックスレジスタ識別値 |
base_reg |
ベースレジスタ識別値 |
disp |
ディスプレースメント |
これらの値をパラメータとして受け取り、ModR/M(1バイト)、SIB(1バイト)、disp(1~4バイト)を必要に応じて生成します。
void set_mod_rm_sib_disp(char mod,int reg,int scale,int index_reg, int base_reg,int disp){ if(mod==MOD_DISP32){ //ModR/Mバイト NativeBuffer[iNativeLength++]=(char) (REGISTER_OPERAND(reg)<<3 | REGISTER_OPERAND(0x04)); base_reg=0x05; index_reg=INDEX_NON; } else{ //ModR/Mバイト NativeBuffer[iNativeLength++]=(char)(mod | REGISTER_OPERAND(reg)<<3 | REGISTER_OPERAND(base_reg)); } //レジスタモードの場合は、ここで終了 if(mod==MOD_REG) return; if(REGISTER_OPERAND(base_reg)==0x04||mod==MOD_DISP32){ ////////////////////// // SIBバイトを使う ////////////////////// NativeBuffer[iNativeLength++]=(char)(scale| REGISTER_OPERAND(index_reg)<<3 | REGISTER_OPERAND(base_reg)); } //ディスプレースメントを必要としない場合は、ここで終了 if(mod==MOD_BASE) return; ////////////////////////// // ディスプレースメント ////////////////////////// if(mod==MOD_BASE_DISP8) NativeBuffer[iNativeLength++]=(char)disp; else{ *((long *)(NativeBuffer+iNativeLength))=disp; iNativeLength+=sizeof(long); } }
与えられたオペコードの書き込みとset_mod_rm_sib_disp
関数の呼び出しを行い、一つの命令を生成するための関数__op_format
を定義します。
x86アーキテクチャでは命令の表現がさまざまな形式でなされ、その中には例外扱いに近い一意でないフォーマットも存在します。今回は、__op_format
関数をオーバーロードさせることで、出現回数が高いと思われるフォーマットに絞って定義を行います。
1つ目の__op_format
関数は1バイトのオペコードのみで表現される命令に対応します。この関数は1バイトのオペコードのみで構成される命令のみに対応するため、オペランドは存在しません。
2つ目の__op_format
関数は1バイトのオペコードで表現される命令なのですが、レジスタ識別値をバイト中に持つことができます。例えば、「push eax
」「push ecx
」などはそれぞれ1バイトの命令コードで表現されます。
3つ目の__op_format
関数は1バイトのオペコード(レジスタ識別値含む)に加え、32ビットの即値が必要な命令に対応するものです。ここまでは、アドレッシングモードを指定する必要がないため、set_mod_rm_sib_disp
関数は呼び出しません。
4つ目の__op_format
関数はアドレッシングモードを指定する必要がある命令を生成可能にします。オペコードに加え、内部でset_mod_rm_sib_disp
関数を呼び出し、ModR/M、SIB、dispを生成します。
5つ目の__op_format
関数は4つ目のものに32ビットの即値生成を追加したものです。
void __op_format(char opcode){ //オペコード NativeBuffer[iNativeLength++]=opcode; } void __op_format(char op_prefix,char opcode,int reg){ //命令プリフィックス if(op_prefix) NativeBuffer[iNativeLength++]=op_prefix; //オペコード、レジスタ NativeBuffer[iNativeLength++]=(char)(opcode|REGISTER_OPERAND(reg)); } void __op_format(char op_prefix,char opcode,int reg,int const_value){ //命令プリフィックス if(op_prefix) NativeBuffer[iNativeLength++]=op_prefix; //オペコード、レジスタ NativeBuffer[iNativeLength++]=(char)(opcode|REGISTER_OPERAND(reg)); //即値 *((long *)(NativeBuffer+iNativeLength))=const_value; iNativeLength+=sizeof(long); } void __op_format(char op_prefix,char opcode1,char opcode2, int reg,int base_reg,int offset,char mod){ //命令プリフィックス if(op_prefix) NativeBuffer[iNativeLength++]=op_prefix; //オペコード if(opcode1) NativeBuffer[iNativeLength++]=opcode1; if(opcode2) NativeBuffer[iNativeLength++]=opcode2; //ModR/M, SIB, disp set_mod_rm_sib_disp(mod,reg,SCALE_NON,INDEX_NON,base_reg,offset); } void __op_format(char op_prefix,char opcode1,char opcode2, int reg,int base_reg,int disp,char mod,int const_value){ //命令プリフィックス if(op_prefix) NativeBuffer[iNativeLength++]=op_prefix; //オペコード if(opcode1) NativeBuffer[iNativeLength++]=opcode1; if(opcode2) NativeBuffer[iNativeLength++]=opcode2; //ModR/M, SIB, disp set_mod_rm_sib_disp(mod,reg,SCALE_NON,INDEX_NON,base_reg,disp); //即値 *((long *)(NativeBuffer+iNativeLength))=const_value; iNativeLength+=sizeof(long); }
上記で定義した__op_format
関数を駆使すれば、オペコード、レジスタ、アドレッシングモードの指定を行って実行可能なネイティブコードを生成できるようになります。ここからは実際の命令と対になる生成モジュールの定義に移ります。
x86の命令は非常に多くの命令に対応しており、数え切れないほどのオペコードが存在します。今回は、後述するサンプルコードで必要になってくる命令に絞り、命令生成のための関数サンプルを定義していきます。
まずは関数を定義する前に、オペコードを定義します。ここで定義するオペコードはx86が定めているものであり、おまじないだと思っておきましょう。
今回は下に示す関数を定義します。
op_mov_RV
op_add_RR
op_add_RV
op_sub_RV
op_push_R
op_push_V
op_call_M
op_ret
関数名の最後尾に付加される「R」「V」「M」には、下のようなオペランドの意味を持たせています。2文字重なったときは、指定された2つのオペランドを命令が保有することを意味します。
接尾辞 | 保有するオペランド |
R | レジスタ(Register) |
V | 即値(Value) |
M | メモリ(Memory) |
//オペランドなしのオペコード(1バイトで表現可能) #define OPCODE_RET_0 (char)0xC3 #define OPCODE_INT_3 (char)0xCC #define OPCODE_ADD_RR (char)0x03 #define OPCODE_SUB_RR (char)0x2B #define OPCODE_ADD_RV (char)0x81 #define OPCODE_SUB_RV (char)0x81 //オペコードバイト中にレジスタ識別ビットを保有するもの #define OPCODE_PUSH_R (char)0x50 //オペコードバイト中にレジスタ識別ビットを保有し、即値を必要とするもの #define OPCODE_CALL_M (char)0x15 //レジスタ=0 #define OPCODE_PUSH_V (char)0x68 //レジスタ=0 #define OPCODE_MOV_RV (char)0xB8 void op_mov_RV(int reg,int iData){ //mov reg,value __op_format(0,OPCODE_MOV_RV,reg,iData); } void op_add_RR(int reg1,int reg2){ //add reg1,reg2 __op_format(0,OPCODE_ADD_RR,0,reg1,reg2,0,MOD_REG); } void op_add_RV(int reg,int iData){ //add reg,iData int opcode_in_mod_rm=(char)0x00; __op_format(0,OPCODE_ADD_RV,0,opcode_in_mod_rm,reg,0,MOD_REG,iData); } void op_sub_RV(int reg,int iData){ //sub reg,iData int opcode_in_mod_rm=(char)0x05; __op_format(0,OPCODE_SUB_RV,0,opcode_in_mod_rm,reg,0,MOD_REG,iData); } void op_push_R(int reg){ //push reg //オペコード、レジスタ __op_format(0,OPCODE_PUSH_R,reg); } void op_push_V(int iData){ //push 32ビット即値 __op_format(0,OPCODE_PUSH_V,0,iData); } void op_call_M(long addr){ //命令プリフィックス int op_prefix=(char)0xFF; __op_format(op_prefix,OPCODE_CALL_M,0,addr); } void op_ret(void){ //オペコード __op_format(OPCODE_RET_0); }
それでは、ここまでで用意された関数を使用し、ネイティブコードを生成するプログラムを書いてみましょう。各種関数を呼び出すことで、コメントに記載したアセンブラコードと対をなす命令がNativeBuffer
バッファに書き込まれていきます。
NativeBuffer
バッファは「ExeCreator.cpp」で定義している処理により、EXEファイルに埋め込まれます。
void create_native_buffer(void){ //ネイティブコードを生成 //mov eax,10 op_mov_RV(REG_EAX,10); //mov ecx,20 op_mov_RV(REG_ECX,20); //add eax,ecx op_add_RR(REG_EAX,REG_ECX); //push eax op_push_R(REG_EAX); //push dword ptr["10+20=%d"] op_push_V(0x00403000); //push dword ptr["0123456789abcdef"] op_push_V(0x00403017); //call dword ptr[wsprintf in import_table] op_call_M(0x00402062); //add esp,sizeof(long)*3 op_add_RV(REG_ESP,sizeof(long)*3); //push 0 op_push_V(0); //push dword ptr["opcode test"] op_push_V(0x0040300A); //push dword ptr["0123456789abcdef"] op_push_V(0x00403017); //push 0 op_push_V(0); //call dword ptr[MessageBoxA in import_table] op_call_M(0x0040205E); //ret 0 op_ret(); }
まとめ
ネイティブコードは1ビット単位で意味付けがされていることを実感できたでしょうか。このまま機械語の世界を突き進んでいってもよいのですが、今以上にディープな内容になりそうなので、まずはここで一区切りさせていただきます。
次回からは気持ちを切り替え、デバッガの挙動を解説していきます。