Jun 10, 2024
安全

逆向基础 - 堆栈图(重点)

这篇笔记是前面笔记的一个阶段性应用,会用到不少前面笔记的知识点,也是逆向学习中极其重要的部分,因此篇幅较长,希望对大家能有所帮助。

逆向基础 - 堆栈图(重点)
Posted by
SYSTEM· 91 次阅读
专栏系列逆向基础

堆栈图

首先给定一段反汇编代码,分析该段代码的堆栈的变化情况,并绘制出堆栈图

函数调用

X86ASM
00401168  |.  6A 02         push 0x2
0040116A  |.  6A 01         push 0x1
0040116C  |.  E8 99FEFFFF   call HelloWor.0040100A
00401171  |.  83C4 08       add esp,0x8

CALL内部

X86ASM
00401040  /> \55            push ebp
00401041  |.  8BEC          mov ebp,esp
00401043  |.  83EC 40       sub esp,0x40
00401046  |.  53            push ebx
00401047  |.  56            push esi
00401048  |.  57            push edi
00401049  |.  8D7D C0       lea edi,dword ptr ss:[ebp-0x40]
0040104C  |.  B9 10000000   mov ecx,0x10
00401051  |.  B8 CCCCCCCC   mov eax,0xCCCCCCCC
00401056  |.  F3:AB         rep stos dword ptr es:[edi]
00401058  |.  8B45 08       mov eax,dword ptr ss:[ebp+0x8]
0040105B  |.  0345 0C       add eax,dword ptr ss:[ebp+0xC]
0040105E  |.  5F            pop edi                                  ;  HelloWor.00401171
0040105F  |.  5E            pop esi                                  ;  HelloWor.00401171
00401060  |.  5B            pop ebx                                  ;  HelloWor.00401171
00401061  |.  8BE5          mov esp,ebp
00401063  |.  5D            pop ebp                                  ;  HelloWor.00401171
00401064  \.  C3            retn

开始分析

分析流程较为冗长,可能会有些乏味,可以先看最后的流程总结,再来看分析的细节

我们现在开始逐语句分析堆栈的变化情况:

执行前

image-20210228194114427
image-20210228194114427
寄存器状态
image-20210228194206740
image-20210228194206740
堆栈内容
image-20210228194402844
image-20210228194402844
初始堆栈图

我们观察堆栈的情况:

此时ESP        EBP

结合寄存器和堆栈内容绘出简易堆栈图

image-20210228194835221
image-20210228194835221

执行中

压入参数
X86ASM
00401168  |.  6A 02         push 0x2                        
image-20210228195308258
image-20210228195308258
image-20210228195225298
image-20210228195225298

可以看到执行后ESP减少了4=0012FF30        并且0012FF30里的内容为2,这就是所谓的入栈操作

X86ASM
0040116A  |.  6A 01         push 0x1                        
image-20210228195558316
image-20210228195558316
image-20210228195756412
image-20210228195756412

可以看到执行后ESP又减少了4=0012FF2C ,并且0012FF2C里的内容为1

上面的两条push语句是将两个立即数 2和1压入到堆栈中,我们可以画出对应的堆栈图:

image-20210228200135180
image-20210228200135180
CALL指令
X86ASM
0040116C  |.  E8 99FEFFFF   call HelloWor.0040100A

F7单步步入

image-20210228210527980
image-20210228210527980
image-20210228210546996
image-20210228210546996
image-20210228210615310
image-20210228210615310

可以看到CALL之后跳转到了0040100A,并且esp又减少了4=0012FF28

而且我们可以注意到此时堆栈中0012FF28存放的内容是:00401171正好是我们call指令的下一行指令的地址

X86ASM
0040116C  |.  E8 99FEFFFF   call HelloWor.0040100A
00401171  |.  83C4 08       add esp,0x8

所以应证了前面所学的call指令会将要返回的地址压入栈中来保存现场

此时的堆栈图为

image-20210228211140508
image-20210228211140508

接着我们就跳转到了call的内部

CALL内部指令
初始化堆栈
image-20210228211439223
image-20210228211439223
X86ASM
00401040  /> \55            push ebp

ESP变化

image-20210228211546005
image-20210228211546005

堆栈内容

image-20210228211605725
image-20210228211605725

EBP被压入到堆栈中,此时堆栈图为

image-20210228211708008
image-20210228211708008

接着执行

X86ASM
00401041  |.  8BEC          mov ebp,esp

ebp赋值为esp,此时堆栈图为

image-20210228212308060
image-20210228212308060

接着执行

X86ASM
00401043  |.  83EC 40       sub esp,0x40
image-20210228212435133
image-20210228212435133
image-20210228212508587
image-20210228212508587

将esp的值减去0x4064,我们这里的相差的数据宽度为4即16,64/416,因此堆栈图里多了16格(蓝色部分),这种操作常被叫做提升堆栈,此时堆栈图为:

image-20210228212917496
image-20210228212917496

我们可以发现提升完堆栈以后,堆栈的数据有些★意义不明★,这是因为堆栈中存放的是临时的数据,可能是之前使用时没有清理的垃圾数据


接着执行

X86ASM
00401046  |.  53            push ebx
00401047  |.  56            push esi
00401048  |.  57            push edi

将三个通用寄存器压入堆栈,用于保护现场,注意CALL之前和CALL之后,其前后环境要一致,这就是所谓的堆栈平衡

image-20210228213353827
image-20210228213353827

堆栈内容

image-20210228213410658
image-20210228213410658

根据此时的堆栈内容绘制堆栈图

image-20210228221004379
image-20210228221004379

接着执行

X86ASM
00401049  |.  8D7D C0       lea edi,dword ptr ss:[ebp-0x40]
image-20210228220848768
image-20210228220848768

将ebp-40所指向的内存地址赋给edi

前面我们执行了sub esp,0x40 所以这里其实就是将那时esp的地址传给了edi(就是push ebx esi edi)之前的的esp

此时堆栈图并发生没有变化

接着看下一行

X86ASM
0040104C  |.  B9 10000000   mov ecx,0x10
00401051  |.  B8 CCCCCCCC   mov eax,0xCCCCCCCC

分别给ecx和eax赋值,堆栈图依旧没有发生变化

image-20210228222347833
image-20210228222347833
image-20210228222402431
image-20210228222402431
image-20210228223255706
image-20210228223255706

接着看下一行

X86ASM
00401056  |.  F3:AB         rep stos dword ptr es:[edi]

这条语句用到了我们前面所学的逆向基础笔记五 标志寄存器中的内容(如有疑惑可前往查看)

rep的作用是,重复执行 stos dword ptr es:[edi],每次执行都会使ecx-1,直到ecx为0再执行下一条语句

前面赋值ecx为0x10=16,正好对应我们堆栈图中蓝色的格子数,所以将会执行16次

stos dword ptr es:[edi]则是将eax的值赋值给edi所指向的内存地址里的值,并且每执行一次edi都会增加4(D标志位为0所以是增加)

结合前面edi==esp,这里其实是将我们提升堆栈的那部分内存区域初始化

此时的堆栈内容为

image-20210228223550564
image-20210228223550564

很明显地看到原本的垃圾数据被我们初始化为了CCCCCCCC

堆栈图也变成了

image-20210228223651626
image-20210228223651626

实际执行内容

接着看下面的代码

X86ASM
00401058  |.  8B45 08       mov eax,dword ptr ss:[ebp+0x8]
0040105B  |.  0345 0C       add eax,dword ptr ss:[ebp+0xC]
image-20210228224415383
image-20210228224415383
image-20210228224436248
image-20210228224436248

根据堆栈图我们可以很清晰地看出

[ebp+0x8]正是我们call外部push的参数:1

[ebp+0xc]正是我们call外部push的参数:2

这里是将eax赋值为1,然后再给eax+2,最终结果eax=3


还原现场并返回

此时堆栈图依旧没有发生变化,接着看下面的语句

X86ASM
0040105E  |.  5F            pop edi                                  ;  HelloWor.00401171
0040105F  |.  5E            pop esi                                  ;  HelloWor.00401171
00401060  |.  5B            pop ebx                                  ;  HelloWor.00401171

出栈,还原现场,堆栈图

image-20210228224826624
image-20210228224826624

下一条

X86ASM
00401061  |.  8BE5          mov esp,ebp
image-20210228225841386
image-20210228225841386

还原esp,前面mov ebp,esp对应也要还原

此时堆栈图为:

image-20210228225604814
image-20210228225604814

继续看下一条指令

X86ASM
00401063  |.  5D            pop ebp                                  ;  HelloWor.00401171
image-20210228225904289
image-20210228225904289

将ebp出栈,恢复现场,此时的堆栈图为


最后一句

X86ASM
00401064  \.  C3            retn

此时栈顶为

image-20210228230325578
image-20210228230325578

返回,相当于于pop eip

执行后

image-20210228230612505
image-20210228230612505

执行后的堆栈图为

image-20210228231716869
image-20210228231716869

执行返回后

此时返回到了

image-20210228231812185
image-20210228231812185

也就是之前call的下一句指令

X86ASM
00401171  |.  83C4 08       add esp,0x8
image-20210228231947017
image-20210228231947017

此时的堆栈图

image-20210228232141747
image-20210228232141747

我们可以发现此时的ESP和EBP又变回到了原本执行前的状态,(寄存器也一样),这就是所谓的堆栈平衡

总结

通过上面的分析,我们可以得出这段代码所处理的大致流程

可分为三个部分:压入参数、调用CALL、CALL返回后

压入参数

压入参数部分十分简单,就是将调用CALL所需的参数压入堆栈,方便CALL内部执行时调用

这里对应的语句为

X86ASM
00401168  |.  6A 02         push 0x2
0040116A  |.  6A 01         push 0x1

即这个CALL得到的参数为2和1

调用CALL

调用CALL又可以分为六个部分:

  1. 提升堆栈

  2. 保护现场

  3. 初始化提升的堆栈

  4. 执行实际内容

  5. 恢复现场

  6. 返回

提升堆栈

对应语句为

X86ASM
00401040  /> \55            push ebp
00401041  |.  8BEC          mov ebp,esp
00401043  |.  83EC 40       sub esp,0x40

将堆栈提升了0x40

保护现场

对应语句为

X86ASM
00401046  |.  53            push ebx
00401047  |.  56            push esi
00401048  |.  57            push edi

将ebx、esi、edi三个通用寄存器保存到堆栈中,前面的push ebp其实也属于保护现场

初始化提升的堆栈

X86ASM
00401049  |.  8D7D C0       lea edi,dword ptr ss:[ebp-0x40]
0040104C  |.  B9 10000000   mov ecx,0x10
00401051  |.  B8 CCCCCCCC   mov eax,0xCCCCCCCC
00401056  |.  F3:AB         rep stos dword ptr es:[edi]

这里将我们提升的堆栈中的内容全部初始化为CCCCCCCC

为什么是初始化为CC?防止缓冲溢出

CC的硬编码对应的指令为int 3,即断点

这么做有什么好处呢?当程序执行超过缓冲区时,遇到int 3就会自动停下来

执行实际的内容

对应语句为

X86ASM
00401058  |.  8B45 08       mov eax,dword ptr ss:[ebp+0x8]
0040105B  |.  0345 0C       add eax,dword ptr ss:[ebp+0xC]

就是将前面压入的参数2和1进行相加得到3

恢复现场

对应语句为

X86ASM
0040105E  |.  5F            pop edi                                  ;  HelloWor.00401171
0040105F  |.  5E            pop esi                                  ;  HelloWor.00401171
00401060  |.  5B            pop ebx                                  ;  HelloWor.00401171
00401061  |.  8BE5          mov esp,ebp
00401063  |.  5D            pop ebp                                  ;  HelloWor.00401171

与前面保护现场相对应

返回

对应语句为

X86ASM
00401064  \.  C3            retn

CALL返回后

对应语句为

X86ASM
00401171  |.  83C4 08       add esp,0x8

作用为平衡堆栈

逆推C语言代码

根据我们前面的分析,我们不难发现这其实就是个简单的加法函数

C
int add(int x,int y) {
    x=x+y;        //这里的x和y分别对应压入的参数
    return x;        //对应RETN 默认采用eax作为返回值的传递载体
}

事后感言

一个小小的加法函数其对应的汇编代码却不少,而其中的关键代码只有两句

X86ASM
00401058  |.  8B45 08       mov eax,dword ptr ss:[ebp+0x8]
0040105B  |.  0345 0C       add eax,dword ptr ss:[ebp+0xC]

其它的大部分代码主要都是为保护现场和恢复现场所服务

编译器编译出的Debug和Release版本对应的汇编代码会有所差异,但只要掌握了核心思想,万变不离其宗

评论交流
暂无评论,欢迎分享您的见解