浅谈逆向——32位逆向分析技术(1.函数)

 

浅谈逆向-32位逆向分析技术

 

启动函数

编写Win32应用程序时,必须在源码中实现一个WinMain函数,但windows执行并不是从WinMain开始的,首先被执行的是启动函数的相关代码,这段代码是由编译器生成的,在启动代码初始化进程完成后,才会调用WinMain函数。
对VC++程序来说,调用了C/C++运行时的启动函数,对运行库进行初始化,C/C++程序运行,启动函数作用基本相同,检索指向新进程的命令行指针,检索指向新进程的环境变量指针,全局变量初始化,内存栈初始化,当所有初始化操作完成后,启动函数就会调用应用程序进入点程序(main和WinMain)。
进入点返回时,启动函数便调用C运行库的exit函数,将返回值(nMainRetVal)传递给它,进行一些有必要的操作,最后调用系统函数ExitProcess退出。
call KERNEL32.GetVersion 确定windows系统版本
call KERNEL32.GetCommandLineA 指向进程的完整命令行指针
call KERNEL32.GetStartupInfoA 获取一个进程的启动信息
call KERNEL32.GetModuleHandleA 返回进程地址空间执行文件基地址
call 00401000 调用用户编写的进入函数Winmain
call 004012EC 退出程序

函数

函数是一个程序模块,用来实现一个特定的功能。一个函数包括:1.函数名,2.入口参数,3.返回值,4.函数功能。

  • 函数的识别
    程序通过调用程序来调用函数,函数返回后又返回调用程序继续执行。调用函数的代码中保存了一个返回地址,该地址会和参数一起传递给被调用函数。

    call指令保存返回信息,将其之后的指令地址压入栈的顶部,当遇到ret指令时返回这个地址。call指令给出的地址就是被调用函数的起始地址,ret 指令则用于结束函数的执行。(可以区分函数调用和其他跳转)

    故此,我们可以通过定位call机器指令或者利用ret指令结束的标志来识别函数。call指令的操作数就是调用函数的首地址。

    还有一种间接调用方法,即通过寄存器传递地址或动态计算函数地址调用。

  • 函数的参数
    函数传递参数有三种方式:1.栈方式;2.寄存器方式;3.通过全局变量进行隐含参数传递

利用栈传递参数

需要定义参数在栈中的顺序,约定函数被调用后谁来平衡栈。

栈(Last-in-First-out,LIFO存储区),栈顶指针esp指向栈中第一个可以用的数据项。在调用函数时,调用者依次把参数压入栈,然后调用函数。函数被调用之后,在栈中取得数据并进行相关计算,函数计算结束后,由调用者或者函数本身修改栈,使栈恢复原样(平衡栈)。

调用约定(为了实现函数调用而建立的协议),规定了函数中参数传递方式,参数是否可变和由谁来处理栈问题。
约定类型 | _cdecl(C规范) |  pascal    | stdcall   | Fastcall
传递顺序 |     从右到左     | 从左到右 |从右到左| 使用寄存器和栈
平衡栈者 |        调用者      |   子程序  | 子程序   | 子程序
VARARG |         是            |     否       |   是        |
(VARARG表示参数的个数可以是不确定的,stdcall如果使用VARARG参数类型,就调用程序平衡栈,否则就是被调用程序平衡栈)

//子程序使用ebp指针+偏移量对栈中参数进行寻址并取出,完成操作
//子程序使用ret或retf指令返回,此时CPU将eip置为栈中保存的地址,并继续执行
eg(按stdcall约定调用函数test(Par1,Par2)):
push Par2       					;																							
push Par1							;	从右向左传递参数
call test							;	调用子程序test
{
	push ebp						;保护现场原来的ebp指针(入栈)
	mov ebp,esp						;设置新的ebp指针指向栈顶
	mov eax,dword ptr[ebp+0C]		;调用栈中参数ebp+偏移量	 		
	mov ebx,dword ptr[ebp+08]		;		
	sub esp,8						;若函数要使用局部变量,则要在栈中留出空间
	...
	add esp,8						;释放局部变量占用的栈
	pop ebp							;恢复ebp指针
	ret 8							;平衡栈,返回(等同于ret;add esp 8)
									;ret后面的值等于参数个数*4h
}
//函数栈的创建
K-00h     | 起始堆栈
K-04h     | 参数二
K-08h     | 参数一
K-0Ch     | 返回地址
K-10h     | 保存的ebp
K-14h     | 局部变量1
K-18h     | 局部变量2
esp     | 当前esp指针
  •  
// enter和leave可以帮助栈的维护
//enter的作用就是push ebp;mov ebp,esp;sub esp,xxx
//leave的作用是add esp,xxx;pop ebp;

enter xxxx,0	;0表示创建XXXX空间来放置局部变量
...
leave 			;恢复现场
ret 8			;返回
  •  
/*编译器按照优化方式编译程序的时候,栈寻址稍有不同。编译器为了节省ebp寄存器或尽可能减少代码提升速度,直接通过esp寻址,esp在函数执行期间会发生变化,在数据进出栈时,要想知道对那个变量进行寻址,就知道当前位置的esp值,必须从函数开始部分进行跟踪*/
push Par2
push Par1
call test
{
	mov eax,dword ptr[esp+04]
	mov ecx,dword ptr[esp+08]
	...
	ret 8
}
  •  

2.利用寄存器传递参数
确定参数存放在那个寄存器中,寄存器传递参数的方式没有标准,所有与平台相关的方式都是由编译器开发人员制定得,大多数使用Fastcall规范。

Microsoft Visual C++采用Fastcall传参,左边的2个不大于4字节(DWORD)的参数分别放在ecx和edx寄存器中,寄存器用完后就要使用栈,其余参数依然按照从右到左的顺序压入栈,被调用的函数在返回前清理传送参数的栈,浮点值,远指针和__int 64类型总是通过栈传递。

Borland Delphi/C++编译器使用Fastcall规范时,左边的3个不大于4字节(DWORD)的参数分别放在eax,edx,ecx 寄存器中。寄存器用完后,其余参数按照从左到右的方式压入栈。

另有一款编译器Watcom C总是通过寄存器来传递参数,它严格的为每一个参数分配一个寄存器,默认第一个参数用eax,第二个用edx,第三个ebx,第四个ecx。寄存器用完就用栈,Watcom C可以由程序员指定任意一个寄存器来传递参数。

// Microsoft Visual C++编译Fastcall调用实例

int __fastcall Add(char ,long ,int ,int);

main(void)
{
	Add(1,2,3,4);
	return 0;
}

int __fastcall Add(char a,long b,int c,int d)
{
	return (a+b+c+d);
}


//反汇编代码如下
push ebp
mov ebp, esp

push 00000004			;后两个参数从右往左入栈
push 00000003
mov edx,00000002		;将第二个参数2h放入寄存器edxx
mov cl,01				;将第一个参数01h传递
call 00401017			;调用Add函数
xor eax,eax				
pop ebp
ret

//Add 函数的代码
push ebp
mov ebp, esp

sub esp, 00000008		;为局部变量分配8字节空间
mov [ebp-08], edx		;将第二个参数放到局部变量[ebp-08]
mov [ebp-04], cl		;将第一个变量放到局部变量[ebp-04]
movsx eax, [ebp-04]		;将字符型整数符号扩展为一个双字
add eax, [ebp-08]		;将左边的两个参数相加
add eax, [ebp+08]		;将eax加上第三个参数
add eax, [ebp+0C]		;将eax加上第四个参数

mov esp, ebp			;函数出口
pop ebp
ret 0008

/*另一个调用规范thiscall,C++非静态类成员函数默认调用约定,对象的每个函数隐含接收this参数,采用
thiscall约定时,参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,仅通过ecx寄存
器传送一个额外的参数--this指针*/

class Csum
{
	public:
	int Add(int a,int b)
	{
		return (a+b);
	}
};

void main()
{
	Csum sum;
	sum.Add(1,2);
}

//反汇编代码
push ebp
mov ebp, esp
push ecx

push 00000002
push 00000001
lea ecx, [ebp-04]			;this指针通过ecx寄存器传递
call 00401020
mov esp,ebp
pop ebp
ret
//sum.Add函数实现代码
push ebp
mov ebp, esp
push ecx
mov [ebp-04], ecx
mov eax, [ebp+08]
add eax, [ebp+0c]
mov esp, ebp
pop ebp
ret 0008
  •  

函数的返回值

函数被调用执行后,将向调用者返回1-N个执行结果(函数返回值)。
1.return操作符返回值
函数返回值放在eax寄存器中返回,处理结果大小超过eax寄存器的容量,其高32位就会放到edx寄存器中。

//
Myadd(int x,int y)
{
	int temp;
	temp = x + y;
	return temp;
}

//汇编代码 主程序
push x
push y
call MyAdd		;调用参数
				;栈在函数内平衡
mov ..., eax	;返回值在eax中

//汇编代码
push ebp
mov ebp, esp

sub esp, 4
mov ebx, [ebp+0c]	;取第一个参数
mov ecx, [ebp+08]	;取第二个参数
add ebx, ecx		;相加
mov [ebp-04], ebx	;将 ebx (结果)存入局部变量中
mov eax, [ebp-04]	;将结果从局部变量中转移到eax中
mov esp, ebp		;恢复现场

add esp, 4			;平衡栈
ret 				;返回
  •  

2.通过参数按传引用方式返回值
给函数传递参数的方式有两种,传值和传引用。

进行传值调用中,会建立参数的一份副本,并把他传给调用函数。在调用函数中修改参数值的副本不会影响原始变量值,传引用调用允许调用函数修改原始变量的值。调用某个函数,当把变量的地址传递给函数时,可以在函数中用间接引用运算符修改调用函数内存单元中该变量的值

调用函数max,需要用两个地址(或者两个指向整数的指针)作为参数,函数会把结果较大的数字放到参数a所在内存单元地址中返回。

#include<stdio.h>
void max(int *a, int *b);

main()
{
	int a=5;
	int b=6;
	max(&a, &b);
	printf("a,b中较大的数是%d",a);
}

void max(int *a, int *b)
{
	if(*a< *b)
	*a=*b;
}


//汇编代码可能如下
sub esp, 00000008				;设此时esp=k,为局部变量分配内存
lea eax, dword ptr[esp+04]		;eax指向变量,值为k-4h
lea ecx, dword ptr[esp]			;ecx指向变量,值为k-8h

push eax						;指向参数B的字符指针入栈
push ecx						;指向参数A的字符指针入栈
mov [esp+08], 00000005			;[esp+08]=k-08h,将参数a的值放入
mov [esp+0C], 00000006			;[esp+0C]=k-04h,将参数b的值放入
call 00401040		

mov edx, [esp+08]				;利用变量[esp+08]返回函数值
push edx	
push 00407030
call 00401060					;printf函数
xor eax, eax
add esp, 18
retn

//max(&a,&b)函数的汇编代码
mov eax, dword ptr [esp+08]			;执行后,eax就是指向参数B的指针
mov ecx, dword ptr [esp+04]	
mov eax, dword ptr [eax]			;参数B的值加载到eax中
mov edx, dword ptr [ecx]
cmp edx, eax						;比较大小
jge 00401052						;若a<b则不转跳
mov dword ptr [ecx], eax			;参数大的放到参数a所指的数据区中
ret 
  •