x86_32 Reverse Enginnering Last Update: 2015 05/07 Contact: [email protected] Author: Ryoma Kawaguchi (B2, Keio University) Jun Murai Lab., ARCH(Internet Architecture Research Team) @ Delta North. gdbコマンドのメモ ブレークポイントとレジスタの状態の表示 (gdb) b main Breakpoint 1 at 0x1edc Breakpoint 1, 0x00001edc in main () (gdb) info register ebp ebp 0xbffff9c8 0xbffff9c8 (gdb) i r ebp ebp 0xbffff9c8 0xbffff9c8 examineコマンド <form>: x/(回数)<arg0><arg1> <arg0>: o.. octal x.. hex u.. unsigned deciaml t.. binary <arg1>: b.. byte h.. half word(2byte) w.. word(4byte) g.. giant(8byte) 32回16進数形式で4byteのデータを表示している状態 。 一時的に計算させたい場合はprintコマンドを使えばgdb内で使える一時的な変数として利用できる。 (gdb) x/32xw $eip 0x1edc <main+12>: 0x1eec <main+28>: 0x1efc <main+44>: 0x1f0c <main+60>: 0x1f1c <main+76>: 0x1f2c <main+92>: 0x1f3c <main+108>: 0x1f4c <main+124>: (gdb) 0x00b3888d 0x00f845c7 0x658d0f00 0x83ca01c2 0x8b000000 0x00000042 0x0000cb88 0x00000022 0x45c70000 0x89000000 0x8b000000 0xd029fee2 0x888df045 0xe9ec4589 0xf4558b00 0xe9e84589 0x000000fc 0x7d81f045 0xc189f845 0x0000003d 0x000000bd 0x0000001b 0x89240c89 0x00000000 0xf44d8900 0x000005f8 0x891fe9c1 0x19850f00 0xe8240c89 0x8df0458b 0xe8042454 0x05f8458b (gdb) print $eip $1 = (void (*)()) 0x1edc <main+12> (gdb) print $eax+0xb3 $2 = 8078 (gdb) x/3s $2 0x1f8e: "Hi,Hello!" 0x1f98: "Hello,World!\n" 0x1fa6: "%s\n" (gdb) x/32c $2 0x1f8e: 72 'H' 0x1f96: 33 '!' 0x1f9e: 87 'W' 0x1fa6: 37 '%' 105 'i' 0 '\0' 111 'o' 115 's' 44 ',' 72 'H' 114 'r' 10 '\n' 72 'H' 101 'e' 108 'l' 0 '\0' 101 'e' 108 'l' 108 'l' 111 'o' 108 'l' 108 'l' 111 'o' 44 ',' 100 'd' 33 '!' 10 '\n' 0 '\0' 1 '\001' 0 '\0' 0 '\0' 0 '\0' 呼び出し規約 __cdecl方式 int __cdecl sum(int a, int b) { int c = a+b; return c; } int main(void) { sum(1,2); return 0; } これを逆アセンブルすると次のようになる sum: push ebp mov ebp, esp push ecx mov eax, dword ptr [ebp + 08h] add eax, dword ptr [ebp + 10h] mov dword ptr [ebp - 04h], eax mov eax, dword ptr [ebp - 04h] mov esp, ebp ; ebp = esp pop ebp ; ebpに退避させておいたebpの値を入れ直す retn ; ret main: push 2 push 1 call sum.xxxxxxxxx add esp, 8 ; sumはint(4byte)の引数を2つ取るので8byte分スタックを修正 ここで、cdecl方式であるという判断基準は call命令直後にadd esp, 8などのようにスタックポインタを引数サイズ分修正している部分が見受けられる事 cdecl方式はIntel x86系のCPUに多く、eax, ecx, edxレジスタは元の値を保存する事なく呼び出されたルーチン内で利用する事が許されている。 __stdcall方式 sum: push ebp mov ebp, esp push ecx mov eax, dword ptr add eax, dword ptr mov dword ptr [ebp mov eax, dword ptr mov esp, ebp ; ebp pop ebp retn 8 [ebp + 08h] [ebp + 10h] - 04h], eax [ebp - 04h] = esp sum: push 2 push 1 call sum.xxxxxxxxx ここで、stdcall方式であるという判断基準は先ほどのcdeclではcall命令を呼び出した直後にルーチン外でスタックポインタの修正を行っていた のに対して、stdcall方式ではルーチン内のretn命令でスタックポインタの修正を行うという点が特徴的である。 stdcall方式はWindows APIで多く利用されているデファクトスタンダードである。 __fastcall方式 sum: push ebp mov ebp, esp sub esp, 0Ch mov dword ptr [ebp mov dword ptr [ebp mov eax, dword ptr add eax, dword ptr mov dword ptr [ebp mov eax, dword ptr mov esp, ebp pop ebp retn - 0Ch], edx ; arg2 - 08h], ecx ; arg1 [ebp - 08h] [ebp - 0Ch] - 4], eax [ebp - 4] main: push ebp mov ebp, esp mov edx, 2 mov ecx, 1 call sum.xxxxxxx xor eax, eax pop ebp retn fastcall方式は、名前の通り引数が2以下であった時引数を直接ecx, edxレジスタに渡して通常2以下の引数を何度も繰り返し呼び出して高速に処 理したい時に利用される。(メモリを介すのとレジスタで直接受け渡しをするのでは速度面で大きな差が生じる) 特徴としてはサブルーチン呼び出し前にスタックへのpushがなくedx,ecxレジスタにmov命令で値のコピーを行っている箇所がある事だ。 __thiscall方式 thiscallはC++において主に利用される呼び出し規約であり、オブジェクトのthisポインタをecxレジスタに渡す特徴がある。 他はstdcallとほぼ同じ方式になっている。クラスで仕様しているメンバ変数などはecxレジスタをポインタとして ecx+x , ecx + y , ecx+z など のオフセットと合わせて表現する事ができる。C++コードのアセンブリを読みながらでないとthiscallの実際の使われ方は理解できにくいため thiscallの詳細については後述する。 分岐命令と文字列処理 分岐命令(分岐が一つしかない場合) int main(void) { int a = 0x200; if(x > 200) { printf("x>200\n"); } return 0; } この時、 call $+5 となっている時 $ は命令自身のアドレスを示している。 変数aはebp-Chに入っており、cmp命令で比較した後にjle命令を置き、x>0x200が成立しない場合はloc_1f62にジャンプしてそのままeaxに0を 代入してreturnしている。 なので上のアセンブリの流れをC言語の int main(void) { int a = 0x200; if(x < 200) { goto loc_1f62; } printf("x>200\n"); goto loc_1f62; loc_1f62: // mov eax, 0 // add esp, 18h return 0; } このようになる。 文字列操作 (strcpy, strcat) #include <stdio.h> int main(void) { char *s = "Hello World !"; char buff[SIZE]; strcpy(s,buff); printf("%s\n",s); return 0; } 以下のソースコードをコンパイルする事によって得られる逆アセンブル結果は以下のようになる。 main: or ecx, ffffffffh xor eax, eax lea edx, dword ptr ss:[esp] push esi push edi mov edi, str32.hello ; "Hello World !" repne scas byte ptr es:[edi] ; ecx: 0xffffffff -> 0xfffffff1 not ecx ; ~0xfffffff1 = 0xe (ASCIIの文字列の長さを取得) sub edi, eax mov eax, ecx mov esi, edi ; ASCII -> esi mov edi, edx ; lea edx, dword ptr ss:[esp] shr ecx, 2 ; ecx / 4 rep movs byte ptr es:[edi], dword ptr ds:[esi] mov ecx, eax and ecx, 3 ; mod rep movs byte ptr es:[edi], byte ptr ds:[edi] lea ecx, dword ptr ss:[edp + 08h] push ecx push str32.xxxxx push str32.xxxxx この中の下のコードはASCII文字列の先頭アドレスをediに代入して、repne(ecx>0の時ZF=0繰り返し)命令を利用している。この時最初の部分で ecxには0xffffffffがmovされているので、repne命令が実行された時点で0xfffffff1にecxが変更される。今求めたいのは、ASCII文字列の長さなので not演算を行う事によって0xffffffff - 0xfffffff1 = 0xeとなり、文字列の長さが得られる。 mov edi, str32.hello ; "Hello World !" repne scas byte ptr es:[edi] ; ecx: 0xffffffff -> 0xfffffff1 not ecx ; ~0xfffffff1 = 0xe (ASCIIの文字列の長さを取得) repne命令が実行されるとその文ediレジスタのポインタが動くので、Hello,World!の一番前の部分にediのポインタを戻さなければならない。そ のため、 sub edi,ecx の命令によってポインタを文字列の最初の部分に戻している。 その後、 shr ecx,2 を利用して左2bitシフトをする事によってカウンタレジスタを4で割っている。 その後dword * ecx = 4 * 3byte分をrep movs命令でediレジスタにコピーを行う。残りの2byte分はまだ転送されていないので、残りの余りバイト 数を求めるために and ecx,3 を実行して余りを求め、その後に同じrep movs命令を実行。引数をpushしてstrcpy命令が構成されている。 C++のアセンブリコード 本の丸写しではおもしろくないので自分でコードを書いて実際に読んでみる事にした。 以下のC++コードがあったとして-m32オプションでコンパイル。これをIDAで逆アセンブルすると次のようになる。 #include <stdio.h> #include <string.h> class Test { public: int n; char name[128]; void show(); }; void Test::show() { printf("%d - %s", n, name); } int main(int argc, char *argv) { Test t; printf("Size: %d\n", sizeof(t)); t.n = 0xfffe; strcpy(t.name,"gucchan"); t.show(); } __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E30 __text:00001E31 __text:00001E33 __text:00001E34 __text:00001E35 __text:00001E3B __text:00001E40 __text:00001E41 __text:00001E44 __text:00001E47 __text:00001E4D __text:00001E4F __text:00001E52 __text:00001E58 __text:00001E5D __text:00001E60 __text:00001E63 __text:00001E66 __text:00001E6E __text:00001E74 __text:00001E7A __text:00001E7F __text:00001E85 __text:00001E8B __text:00001E91 __text:00001E9B __text:00001EA1 __text:00001EA4 __text:00001EA8 __text:00001EAE __text:00001EB3 __text:00001EB9 __text:00001EBC __text:00001EC2 __text:00001EC7 __text:00001ECD __text:00001ED3 __text:00001ED5 __text:00001ED8 __text:00001EDA __text:00001EE0 __text:00001EE5 __text:00001EEB __text:00001EEC __text:00001EED __text:00001EEE ; int __cdecl main(int argc, const char **argv, const char **envp) public _main _main proc near var_A8 var_A4 var_A0 var_9C var_98 var_14 var_10 var_C argc argv envp = = = = = = = = = = = dword dword dword dword dword dword dword dword dword dword dword push mov push push sub call pop mov mov mov mov mov lea mov mov mov mov mov mov mov call mov lea lea mov add mov mov mov call lea mov mov call mov mov mov mov cmp jnz mov add pop pop pop retn ptr ptr ptr ptr ptr ptr ptr ptr ptr ptr ptr -0A8h -0A4h -0A0h -9Ch -98h -14h -10h -0Ch 8 0Ch 10h ebp ebp, esp edi esi esp, 0B0h $+5 eax ecx, [ebp+argv] edx, [ebp+argc] esi, ds:(___stack_chk_guard_ptr - 1E40h)[eax] esi, [esi] [ebp+var_C], esi esi, (aSizeD - 1E40h)[eax] ; "Size: %d\n" edi, 84h [ebp+var_10], edx [ebp+var_14], ecx [esp], esi ; char * dword ptr [esp+4], 84h [ebp+var_9C], eax [ebp+var_A0], edi _printf ecx, [ebp+var_9C] edx, [ecx+104h] esi, [ebp+var_98] [ebp+var_98], 0FFFEh esi, 4 [esp], esi ; char * [esp+4], edx ; char * [ebp+var_A4], eax _strcpy ecx, [ebp+var_98] [esp], ecx ; this [ebp+var_A8], eax __ZN4Test4showEv ; Test::show(void) eax, [ebp+var_9C] ecx, ds:(___stack_chk_guard_ptr - 1E40h)[eax] ecx, [ecx] edx, [ebp+var_C] ecx, edx loc_1EEF eax, 0 esp, 0B0h esi edi ebp この部分が printf("Size:%d\n", sizeof(t)) の部分だろうと推定される。 次にTクラスの中の属性に0xfffeを代入する部分をみていく。 __text:00001E52 __text:00001E58 省略 __text:00001E63 __text:00001E66 __text:00001E7A lea mov esi, (aSizeD - 1E40h)[eax] ; "Size: %d\n" edi, 84h mov mov call [esp], esi ; char * dword ptr [esp+4], 84h _printf ここで mov [ebp+var_98], 0xfffeh という記述がある、先頭から解釈されるためここはクラスTのメンバnであると分かる。 その後esiを4byte加算して char name[128] を指す値と"gucchan"を指すアドレスをスタックに乗せてcall _strcpyをしている __text:00001E8B __text:00001E91 __text:00001E9B __text:00001EAE lea mov add call esi, [ebp+var_98] [ebp+var_98], 0FFFEh esi, 4 _strcpy C++コードの逆アセンブルにおいて、__thiscall方式ではecxにそのクラスのthisポインタを乗せる事を先に書いていた。 t.show() を実行する時にもそれが行われている。 __text:00001EB3 __text:00001EB9 __text:00001EBC __text:00001EC2 lea mov mov call ecx, [ebp+var_98] [esp], ecx ; this [ebp+var_A8], eax __ZN4Test4showEv ; Test::show(void) 引数無しのメンバ関数でも第一引数としてthisを受け取っており、 ebp+var_98のアドレスに存在するものがそのクラスのthisポインタという事になる。 C++の場合関数、クラスのオーバーロードが存在するために同じ関数名だと定義が衝突してしまう可能性があるために名前マングルを行ってい る。 以下はTest::show()の中身。 __text:00001DF0 __text:00001DF0 __text:00001DF0 __text:00001DF0 __text:00001DF0 __text:00001DF0 __text:00001DF0 __text:00001DF0 __text:00001DF0 __text:00001DF0 __text:00001DF0 __text:00001DF1 __text:00001DF3 __text:00001DF6 __text:00001DFB __text:00001DFC __text:00001DFF __text:00001E05 __text:00001E08 __text:00001E0B __text:00001E0D __text:00001E13 __text:00001E16 __text:00001E1A __text:00001E1E __text:00001E23 __text:00001E26 __text:00001E29 __text:00001E2A __text:00001E2A ; Attributes: bp-based frame ; _DWORD __cdecl Test::show(Test *this) public __ZN4Test4showEv __ZN4Test4showEv proc near ; CODE XREF: _main+92p var_8 var_4 this = dword ptr -8 = dword ptr -4 = dword ptr 8 push mov sub call pop mov lea mov mov mov add mov mov mov call mov add pop retn __ZN4Test4showEv endp ebp ebp, esp esp, 18h $+5 eax ecx, [ebp+this] eax, (aDS - 1DFBh)[eax] ; "%d - %s" [ebp+var_4], ecx ecx, [ebp+var_4] edx, [ecx] ecx, 4 [esp], eax ; char * [esp+4], edx [esp+8], ecx _printf [ebp+var_8], eax esp, 18h ebp
© Copyright 2025 ExpyDoc