x86_32 Reverse Enginnering

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