逆向——C语言的汇编表示之堆栈图

C语言的汇编表示之堆栈图

 

写在前面:push一个数据到stack后,esp指向的是栈顶push的最新数据。

 

 

前天把C语言的汇编表示给看完了,但却没有怎么自己操作过,不过看懂了永远不能代表学会了,今天的话就从中挑选一个简单例子完整的再操作一遍,加深自己对它的理解!(之所以没怎么操作是因为VC6.0环境总是出问题!!!)

1. 在VC6.0创建一个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
#include<stdlib>
#include<windows.h>
int plus(int x,int y)
{
return x+y;
};
int main()
{
int a=10;
push(2,3);
return 0;
}
 ==》代码修改为:
#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 单步步入

Z4buqS.png

总结:

1.C语言中传参方式 是堆栈传参 参数的调用从右到左依次push

2.函数名就是编译器给内存地址的别名

画一下 堆栈变化图

首先我们调出 Register(寄存器)窗口,与Memory窗口

1.Alt+5 Register(寄存器)窗口

Z4bQaQ.png

2.Alt+6 Memory 窗口

Z4bMVg.png

看栈顶 esp 地址为0012FF30

看栈底 ebp 地址为0012FF80

F10执行完push 3
ESP就会减4个字节 变成0012FF2c
Z4bZxP.png

同理 再 F10执行完 push 2后 猜测 ESP会变成 0012FF28
Z4bnr8.png

所以 堆栈图可以这么画

Z4bl5j.png
Z4b3Ps.png

然后下一步执行call指令 F11进去
这时

Z4btMV.png

紧跟的是 02 03

Z4b8Gn.png

call (0040100a)指令本质

1.jmp 0040100a //即EIP为0040100A 下一次要执行的地址

2.把下一行地址压到堆栈去 /即栈顶
由2得3 esp的值减4 //因为下一行地址压到堆栈

所以 查看一下 栈顶0010FF24内容 就是call指令的下一行地址00 40 B7 83 (小端存储)
Z4bJx0.png

此时观察 反汇编页面

发现它并没有直接 call到 函数部分 而是 这样的
Z4bG2q.png

这时VC6 给我们生成的。
调用函数时VC6会给我们生成 一个jmp无条件跳转到 函数部分、

jmp 00401010实质:

mov eip,00401010//即 下一行执行 00401010指令
Z4b0IJ.png
所以 执行到这步,目前反汇编该执行 00401010了

观察下面框住的部分
此为编译器想要ebp寻址.
我们分析下
Z4bDi9.png

push ebp //即 把 ebp的地址压入栈顶,同时esp-4

mov ebp,esp //即把esp的地址赋给ebp的地址,此时ebp就和esp在同一地址了
然后

sub esp,40h//目的是给 提升堆栈

堆栈一个地址占4个字节 40h的话就是40h/4=10h个 地址 换成10进制就是16个地址
即esp被向上提升了16个地址,
堆栈图如下:
Z4bdZF.png

然后继续看反汇编
Z4bNrT.png
Z4bUqU.png

这个比较简单了,连续把寄存器ebx,esi,edi的地址压入栈
目的是:保存原来的寄存器值 以便最后的恢复,因为你在过程中会发生变化
执行完堆栈图为
Z4bwa4.png
而绿色的部分就是我们常提到的 缓冲区

为了程序使用完 正常运行 编译器会在没用到的缓冲区 填充CC即我们调试时的断点

缓冲区溢出 就是

当前的函数在执行的过程中需要内存,需要用内存 就提升堆栈 自己给自己分配内存,

对缓冲区做手脚,通过一些方式 如改变函数返回地址 来达到控制程序的目的;

下一个指令

Z4byxx.png

首先

lea edi,[ebp-40h] 的含义就是 把ebp现在的地址减40h 后的地址赋给edi

stos dword PTR [edi]的含义是将eax的值存储到[edi]指定的地址

rep指令:

按计数寄存器(ecx)指定的次数重复执行执行字符串指令

如 rep stosd(rep stos dword PTR [edi]的简写)

所以上面的几行指令就是给没用到的缓冲区填充 CC

所以 现在的堆栈结构图 是这个样子
Z4b2qO.png

这是 我们该分析这步了

Z4bhIH.png

看下图

红色1: ebp+4 地址里存放的是call指令的下一行地址

红色2(未画) ebp+8 地址里存放的是 第一个实参 2

红色2(未画) ebp+0C 地址里存放的是 第二个实参 3

Z4brGR.png
mov eax,dword PTR [ebp+8] 含义是 把esp+8 地址里的值 赋给eax
add eax,dword PTR [ebp+12] 含义是 把esp+8 地址里的值 加上eax 存在eax

此过程 堆栈以及缓冲区 没有改变只是 把 2+3的结果存在了eax中了

此时 看起来 我们似乎觉得 程序已经结束了

对的,我们希望的过程已经实现了,
但还要注意 堆栈平衡 原来是什么样子
记得用完后 把它恢复过来

继续分析

Z4bsR1.png

我们把edi esi ebx pop出去 ,同时esp 一共减去减12即0C

这是 esp地址又会变成0012FEE0
Z4b5id.png
F10 单步 验证下
Z4bcM6.png
正确

还有一点点就要分析完毕了

Z4bgsK.png

mov esp,ebp //即把ebp的地址赋给esp

此时esp又和ebp在同一位置了
Z4bWZD.png

然后pop ebp//把0012FF80 pop给ebp地址

实质:把ebp pop最初的ebp地址 同时esp+4

堆栈图为:

Z4bHQP.png

F10 验证下
Z4bfde.png
正确!
然后该执行 ret 了

ret的本质 是

mov eip ,esp

add esp,4

Z4boRI.png

所以 会 pop 0040B783给了EIP 同时esp+4

所以 堆栈图为

Z4bIJA.png

原来的堆栈是黄色部分 现在的话 esp还没有恢复原位置
也就是堆栈还没有平衡

继续看汇编页面
add esp,8
执行后 esp将恢复到原来的地址 从0012FF28 到 0012FF30(原)

Z4bbsf.png

有内平 (直接 return ?(?代表16进制数字))
和外平(我记录这个就是外平,return过后,再add esp,?)

此时:堆栈已经平衡
而 再堆栈平衡的状态下
我们完成了 2+3 并把值存在了EAX中
除此之外,我们在还留下了 大量的垃圾(框住的部分)危害是有的,具体的还暂时不是太清楚,继续深入吧!

Z4bTzt.png

上面有一个地方出了点小错,
call指令的下一行的地址为0040B788 一下为修正图(还请注意):
Z4bqL8.png



堆栈传参

当函数有很多参数的时候,不止8个,那我们使用通用寄存器去传参,明显不够用,所以我们需要使用堆栈帮助我们传递参数。

还是以加法举例,实际场景:

images/download/attachments/8684311/image2021-1-27_0-18-42.png

如上图所示实现算术1+2,首先将1、2依次压入堆栈,CALL指令也会将其下一行指令地址压入堆栈,所以堆栈地址[ESP+8]为第一个压入的数据,堆栈地址[ESP+4]为第二个压入的数据。

堆栈平衡

我们知道当执行函数调用CALL指令的时候,会把CALL指令下一条指令的内存地址压入堆栈(ESP值减4);在函数内我们可以随意使用堆栈,比如PUSH指令压入堆栈,使用堆栈传参等等...

我们需要保证,在函数调用结束的时候(即执行RET指令之前,要把ESP栈顶指针的值修改为执行CALL指令压入堆栈或堆栈传参压入堆栈前的那个ESP栈顶指针的值),保证函数运行前与运行后ESP栈顶指针的值不变,这个我们称之为堆栈平衡

平衡堆栈有两个方法:

1.外平栈:使用ADD指令。

images/download/attachments/8684311/image2021-1-27_0-51-7.png

2.内平栈:使用RET指令,例如压入了2个32位(4字节)数据就可以写为RET 8。

images/download/attachments/8684311/image2021-1-27_0-52-6.png


 

 
posted @ 2023-03-23 09:26  bonelee  阅读(121)  评论(0编辑  收藏  举报