目录
逆向是什么
32位软件逆向技术
1.启动函数
2.函数
函数的识别
函数的参数
利用栈进行传递
下面是通过esp来寻址
通过寄存器来传递参数
例子
例子
函数的返回值
例子
例子
逆向是什么
将可执行程序反汇编 通过分析反汇编代码来理解其代码功能(各个接口的数据结构)
用高级语言重述这个代码 逆向分析原始软件思路
这就是逆向分析
32位软件逆向技术
使用 VC6.0编译的32位程序
1.启动函数
在写32位程序的时候 源代码必须实现一个 WinMain函数
但是执行并不是从WinMain开始
首先执行的是启动函数的相关代码
是通过编译器生成
对于 Visual C++程序来说
它调用的是 C/C++运行时启动函数
该函数负责对C/C++运行库进行初始化
(Visual C++配有C运行库的函数 可以在 crt/src/crt0.c 文件中找到启动函数的源代码)
C/C++程序运行时 启动函数的作用基本相同
检索指向新进程的命令行指针
检索指向新进程的环境变量指针
全局变量初始化
内存栈初始化
等
所有的初始化完成后
启动函数就会调用应用程序的入口函数(main、WinMain)
调用WinMain的函数大致如下
GetStartupInfo(&StartupInfo);
Int nMainRetVal = WinMain(GetModuleHandle(Null),Null,pszCommandLineAnsi,
\(StartupInfo.dwFlags&STARTF_USESHOWWINDOW)?StartupInfo.\wShowWindow:sw_SHOWEFAULT);
这里主要就是调用WinMain函数 然后返回值为一个 INT的 nMainRetVal
进入点返回时
启动函数便调用 C运行库的 exit函数,把返回值(nMainRetVal)传给他
进行必要的处理 然后调用 ExitProcess 退出
2.函数
每一个程序都是由不同功能的函数组成
所以在逆向中 重点是放在函数的识别和参数的传递
这样就可以把注意力集中在某一段代码中
函数是一个程序模块 用来实现特定功能
其包括了(函数名,入口参数,返回值,函数功能等部分)
函数的识别
函数通过调用程序来调用函数 然后在函数返回的时候继续执行程序
函数要知道如何返回的地址呢
实际上 大多数情况下都是使用call 和 ret来调用函数和保存返回地址
call指令和跳转指令功能类似
call:保存返回信息 即将之后的指令地址压入栈的顶部
然后遇到ret 就返回该地址
该地址会和参数一起传给被调用函数
也就是说 call指令给出的地址 就是被调用函数的其实地址
ret则是用于结束函数的执行(并不是所有ret都是标志函数的结束)
源代码是这个
我们看看程序的执行
1.main函数
2.call sub 就是 add函数
函数的参数
这里就是主要的传递参数的方式
(1)栈方式:
需要定义参数在栈中的顺序 并且约定函数调用的栈平衡
(2)寄存器方式:
需要确定参数是通过那个寄存器进行传参
(3)通过全局变量进行隐含参数传递
利用栈进行传递
栈是 后进先出的存储区
指针esp指向栈的第一个可用的数据项
调用函数
先把参数入栈
然后调用函数
函数被调用后
在栈中取得数据
计算结束后
依照调用约定 平衡栈
在传递有一个很重要的事情
就是调用约定
这个是由不同语言 来确定的
1.栈的介绍-C语言调用函数(一)_双层小牛堡的博客-CSDN博客
1.栈的介绍-C语言调用函数(二)_双层小牛堡的博客-CSDN博客
这里给出例子
有一个test1(Par1,Par2,Par3)
_cdecl | pascal | stdcall |
push par3;参数从右往左 push par2 push par1 call test1 add esp,0C ;栈平衡 | push par1;参数从左往右 push par2 push par3 call test1 在函数内完成栈平衡 | push par3;参数从右往左 push par2 push par1 call test1 在函数内完成栈平衡 |
这里就很清楚可以看见
_cdecl和stdcall是参数右往左进入栈 pascal则相反
在栈平衡上 _cdecl是自己加0c来保证栈平衡 则 pascal和stdcall相反
函数对参数和局部变量的取值都是通过栈来定义的
1. 调用者将函数执行完毕时返回地址和参数入栈
2.函数使用 ebp+偏移量 对栈中的参数进行寻址和去除
3.使用ret和retf返回 这个时候 eip是设置为栈中保存到地址
栈 只有一个出口 就是栈顶
这里给出一个例子 使用 stdcall约定来调用 test1
push par2
push par1
call test2{
push ebp ;保护栈
mov ebp,esp ;设置新的ebp 指向栈顶
mov eax,dword ptr [ebp+0c] ;取得par2
mov edx, dword ptr [ebp+08] ;取得par1
sub esp,8 ;如果要使用局部变量 就要预留空间
.........
add esp,8 ;释放局部变量的栈
pop ebp ;恢复现场的的ebp
ret 8h ;返回(相当于 ret;add esp,8)
ret 后面为参数个数x4h
}
给出建立的过程
因为esp为栈指针 所以ebp来存取栈
1.此函数具有两个参数 假设执行函数前 esp=k
2.依据stdcall调用 从右至左入栈 先将Par2入栈 esp=K-04h
3.将Par1入栈 esp为 K-08h
4.参数入栈了 现在就是调用函数 call
5.call 函数 把返回地址入栈 esp=K-0ch
6.现在已经在子程序(函数)中了 开始使用ebp来调用参数 但是为了恢复之前的栈
我们先需要 push ebp来保护 这个时候 esp=K-10h
7.执行mov ebp,esp ebp是用来寻找调用者压入栈的参数 这个时候 [ebp+8]就是Par1 [ebp+c]就是参数2
8.sub esp,8表示在栈中定义局部变量 局部变量1和局部变量2的地址分别是[ebp-4] [ebp-8]
调用结束使用 add esp,8 来释放 也就是说 调用结束后 局部变量就消失了
9. 调用 ret 8 来平衡栈
此外 使用 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
在许多时候 编译器会选择最优化的方式来编译程序
栈寻址方式会有点不一样
下面是通过esp来寻址
是通过 visual 6.0 的 "Maximize Speed"的优化选项
push par2
push par1
call test1
{
mov eax,dword ptr [esp+04] ;参数1
mov ecx,dword ptr [esp+08] ;参数2
.......
ret 8
}
1.假设 esp=K
2.依据stdcall调用 从右至左入栈 Par2先进去 esp=K-04h
3.Par1 入栈 esp=K-08h
4.参数入栈 开始执行call call把返回地址压入栈 esp=K-0Ch
5.通过esp来选择参数
通过寄存器来传递参数
利用寄存器传参并没有具体的要求 虽然没有要求
但是都会在不声明的情况下进行遵守 Fastcall规范
Visual C++ Fastcall规范
左边的两个不大于4字节(dword)的参数分别存放在ecx edx中
寄存器就要用栈,将其余的参数依然使用从右往左的方式入栈,被调用函数在返回前清理栈
注意 浮点值 _int64 远指针都是通过栈来传递
Delphi/C++ Fastcall规范
左边的3个不大于4字节 (dword)参数分别放在 eax edx ecx 中
寄存器用完后 通过从左至右的Pascal方式入栈
特别的编译器 Watcom C
总是通过寄存器来传递参数
1. eax
2. edx
3. ebx
4.ecx
如果寄存器用完 就是用栈来存入参数
例子
我们分析一下汇编代码
这里就是用Fastcall 来调用
push 4
push 3 从右往左 先把后两个压入栈
mov edx,2
mov cl,1 (char 大小为8个字节 用8位寄存器即可)
然后就是调用函数
接下来我们看看add函数
push ebp
mov ebp,esp 保护栈
sub esp,8 开辟局部变量空间
mov [ebp+var_8],edx edx存放的是2 2先进入局部变量[ebp-8]的地方
mov [ebp+var_4],cl cl存放的是1 1进入局部变量[ebp-4]
movsx eax,[ebp+var_4] 将字符型整数 扩展到双位 就是把cl存放的8位 扩充到16位
add eax,[ebp+var_8] 把左边两个参数相加 即 1+2
add eax,[ebp+arg_0] 把3+(1+2)
add eax,[ebp+arg_4] 把4+((1+2)+3)
mov esp,ebp 清空栈 返回
pop ebp
retn 8
从这里看出 寄存器需要先存入局部变量地址 然后通过局部变量读取出来进行计算
还存在一个调用规范 也是通过寄存器传值
thiscall
非静态的类成员函数调用
对象的每个函数隐含接收到this参数
采用this约定的时候
寄存器按照从右往左入栈
被调用函数在返回前清空栈
并且仅仅通过ecx寄存器传递额外的参数
this指针
例子
main
push ebp
mov ebp,esp
push ecx 保护ecx 先暂存
push 2
push 1 从右至左入栈
lea ecx,[ebp+var_4] 此处是通过ecx传递this指针
push ebp
mov ebp,esp
push ecx
mov [ebp+var_4],ecx 把局部变量ebp-4的位置存入this指针
mov eax,[ebp+arg_0] 把1存入eax中
add eax,[ebp+arg_4] 把2和1相加
mov esp,ebp
pop ebp
retn 8
函数的返回值
return返回值
函数返回值一般情况下存放在eax中
如果超过了eax的容量
那么高32位会存放在edx寄存器中
例子
MyAdd(int x,int y)
{
int temp;
temp=x+y;
return temp;
}
主函数 | MyAdd |
push x push y call MyAdd mov ....,eax | push ebp mov ebp,esp sub esp,4 mov ebx,[ebp+0ch] mov ecx,[ebp+08h] add ebx,ecx mov [ebp-4],ebx ;结果存入局部变量中 mov eax,[ebp-4] ;存入eax用来返回 mov esp,ebp add esp,4 ret |
通过参数按传引用方式返回值
给函数传递参数的方式有两个
一个是传值
一个是传引用
在传递引用的 时候 修改参数值的复本不会影响的原本参数的值
传引用调用 允许调用函数修改原始变量的值
例子
main函数
sub esp,8 开辟局部变量空间 设esp为K
lea eax,[esp+8-4] 为 K-4
lea ecx,[esp+8-8] 为 K-8
push eax 指向参数B的字符指针入栈
push ecx 指向参数A的字符指针入栈
call 调用函数
mov edx,[esp+8] 利用esp+8返回值
max函数
mov eax,[esp+8] eax 就是指向B的指针
mov ecx,[esp+4] ecx 就是指向A的指针
mov eax,dword ptr [eax] B的值存入eax
mov edx,dword ptr [ecx] A的值存入edx
cmp edx,eax
jge 如果小于 就不跳转
mov [ecx],eax 把较大的值存入A的地址中