逆向——C语言的汇编表示之堆栈图
C语言的汇编表示之堆栈图
写在前面:push一个数据到stack后,esp指向的是栈顶push的最新数据。
前天把C语言的汇编表示给看完了,但却没有怎么自己操作过,不过看懂了永远不能代表学会了,今天的话就从中挑选一个简单例子完整的再操作一遍,加深自己对它的理解!(之所以没怎么操作是因为VC6.0环境总是出问题!!!)
1. 在VC6.0创建一个文件
1
|
|
#include<stdio.h> #include<windows.h> int plus(int x, int y) { return x + y; }; int main() { int a = 10; plus(2, 3); return 0; }
Cral+s 保存
F7 bluid(构建exe)
查看反汇编:
F10 单步步过
F11 单步步入
总结:
1.C语言中传参方式 是堆栈传参 参数的调用从右到左依次push
2.函数名就是编译器给内存地址的别名
画一下 堆栈变化图
首先我们调出 Register(寄存器)窗口,与Memory窗口
1.Alt+5 Register(寄存器)窗口
2.Alt+6 Memory 窗口
看栈顶 esp 地址为0012FF30
看栈底 ebp 地址为0012FF80
F10执行完push 3
ESP就会减4个字节 变成0012FF2c
同理 再 F10执行完 push 2后 猜测 ESP会变成 0012FF28
所以 堆栈图可以这么画
然后下一步执行call指令 F11进去
这时
紧跟的是 02 03
call (0040100a)指令本质
1.jmp 0040100a //即EIP为0040100A 下一次要执行的地址
2.把下一行地址压到堆栈去 /即栈顶
由2得3 esp的值减4 //因为下一行地址压到堆栈
所以 查看一下 栈顶0010FF24内容 就是call指令的下一行地址00 40 B7 83 (小端存储)
此时观察 反汇编页面
这时VC6 给我们生成的。
调用函数时VC6会给我们生成 一个jmp无条件跳转到 函数部分、
jmp 00401010实质:
mov eip,00401010//即 下一行执行 00401010指令
所以 执行到这步,目前反汇编该执行 00401010了
push ebp //即 把 ebp的地址压入栈顶,同时esp-4
mov ebp,esp //即把esp的地址赋给ebp的地址,此时ebp就和esp在同一地址了
然后
sub esp,40h//目的是给 提升堆栈
堆栈一个地址占4个字节 40h的话就是40h/4=10h个 地址 换成10进制就是16个地址
即esp被向上提升了16个地址,
堆栈图如下:
这个比较简单了,连续把寄存器ebx,esi,edi的地址压入栈
目的是:保存原来的寄存器值 以便最后的恢复,因为你在过程中会发生变化
执行完堆栈图为
而绿色的部分就是我们常提到的 缓冲区
为了程序使用完 正常运行 编译器会在没用到的缓冲区 填充CC即我们调试时的断点
缓冲区溢出 就是
当前的函数在执行的过程中需要内存,需要用内存 就提升堆栈 自己给自己分配内存,
对缓冲区做手脚,通过一些方式 如改变函数返回地址 来达到控制程序的目的;
首先
lea edi,[ebp-40h] 的含义就是 把ebp现在的地址减40h 后的地址赋给edi
stos dword PTR [edi]的含义是将eax的值存储到[edi]指定的地址
rep指令:
按计数寄存器(ecx)指定的次数重复执行执行字符串指令
如 rep stosd(rep stos dword PTR [edi]的简写)
所以上面的几行指令就是给没用到的缓冲区填充 CC
看下图
红色1: ebp+4 地址里存放的是call指令的下一行地址
红色2(未画) ebp+8 地址里存放的是 第一个实参 2
红色2(未画) ebp+0C 地址里存放的是 第二个实参 3
mov eax,dword PTR [ebp+8] 含义是 把esp+8 地址里的值 赋给eax
add eax,dword PTR [ebp+12] 含义是 把esp+8 地址里的值 加上eax 存在eax
此过程 堆栈以及缓冲区 没有改变只是 把 2+3的结果存在了eax中了
此时 看起来 我们似乎觉得 程序已经结束了
对的,我们希望的过程已经实现了,
但还要注意 堆栈平衡 原来是什么样子
记得用完后 把它恢复过来
继续分析
我们把edi esi ebx pop出去 ,同时esp 一共减去减12即0C
这是 esp地址又会变成0012FEE0
F10 单步 验证下
正确
还有一点点就要分析完毕了
mov esp,ebp //即把ebp的地址赋给esp
此时esp又和ebp在同一位置了
然后pop ebp//把0012FF80 pop给ebp地址
实质:把ebp pop最初的ebp地址 同时esp+4
ret的本质 是
mov eip ,esp
add esp,4
所以 会 pop 0040B783给了EIP 同时esp+4
原来的堆栈是黄色部分 现在的话 esp还没有恢复原位置
也就是堆栈还没有平衡
继续看汇编页面
add esp,8
执行后 esp将恢复到原来的地址 从0012FF28 到 0012FF30(原)
有内平 (直接 return ?(?代表16进制数字))
和外平(我记录这个就是外平,return过后,再add esp,?)
此时:堆栈已经平衡
而 再堆栈平衡的状态下
我们完成了 2+3 并把值存在了EAX中
除此之外,我们在还留下了 大量的垃圾(框住的部分)危害是有的,具体的还暂时不是太清楚,继续深入吧!
上面有一个地方出了点小错,
call指令的下一行的地址为0040B788 一下为修正图(还请注意):
堆栈传参
当函数有很多参数的时候,不止8个,那我们使用通用寄存器去传参,明显不够用,所以我们需要使用堆栈帮助我们传递参数。
还是以加法举例,实际场景:
如上图所示实现算术1+2,首先将1、2依次压入堆栈,CALL指令也会将其下一行指令地址压入堆栈,所以堆栈地址[ESP+8]为第一个压入的数据,堆栈地址[ESP+4]为第二个压入的数据。
堆栈平衡
我们知道当执行函数调用CALL指令的时候,会把CALL指令下一条指令的内存地址压入堆栈(ESP值减4);在函数内我们可以随意使用堆栈,比如PUSH指令压入堆栈,使用堆栈传参等等...
我们需要保证,在函数调用结束的时候(即执行RET指令之前,要把ESP栈顶指针的值修改为执行CALL指令压入堆栈或堆栈传参压入堆栈前的那个ESP栈顶指针的值),保证函数运行前与运行后ESP栈顶指针的值不变,这个我们称之为堆栈平衡。
平衡堆栈有两个方法:
1.外平栈:使用ADD指令。
2.内平栈:使用RET指令,例如压入了2个32位(4字节)数据就可以写为RET 8。