1+2在堆栈中是如何实现的
2023-3-2 00:2:58 Author: 白帽子(查看原文) 阅读量:11 收藏

首先,我们先来了解一下什么是堆栈?

堆栈是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除。(来源于百度百科)

顾名思义,在程序运行的时候,计算机会在内存中给这段程序开辟出一段只属于它的空间用来存放数据,而这段空间,一端是固定的,我们成为栈底,一端是浮动的,我们成为栈顶,数据只能在浮动的栈顶按照“先进后出”的规则存入或者取出,这就是堆栈。

简单了解了堆栈的概念,那么程序在运行过程中,堆栈是如何变化的呢?下面让我们通过一个简单1+2来深入了解一下。

这是一个debug版本的程序,在这里我用的是x32dbg进行动态调试,使用od什么的都可以的,右下角这个窗口就是堆栈窗口,主要用于存放程序产生的临时数据,左上角就是寄存器窗口,用来观察寄存器的变化。

在画堆栈图之前我们需要了解一些基本的汇编指令和寄存器,本次调试我们用到的汇编指令有

mov:传送(分配)数值
add:两个数值相加
sub:从一个数值中减去另一个数值
jmp:跳转到一个新位置
call:调用一个子程序
ret:返回到call的下一跳的地址
push:压栈
pop:出栈
lea:将一个操作地址赋给目的操作数
rep stosd:rep stos dword ptr es:[edi] 用eax的值填充edi

在通用寄存器中,我们暂时知道EBP是栈底,ESP是栈顶指针,EAX用来存放返回结果就可以了。

接下来我们进入正题,通过画堆栈图的方式来看一下1+2这个简单的操作在堆栈中是怎样运行的,拿到程序,ctrl+g跳到0x401068,按F2把断点下到0x401068的位置,F9运行程序到断点处。

在函数调用之前的堆栈是这样的

ESP和EBP的地址分别是0x0019FE4C和0x0019FE98

首先是两个入栈的操作

push 2
push 1

因为堆栈是向低地址扩展的,所以2和1入栈后,堆栈发生变化,ESP的值会减8

这里按F8继续执行。

下一条指令是call 0x00401005

call指令的作用是程序跳到指向的地址,并且会把下一跳的地址存入堆栈,这时程序下一跳的地址是0x00401071,一般我们在逆向中看到call指令就是函数调用,所以我们会把找关键call作为重点。这里我们按F7单步执行进入call。

此时的堆栈图是这样的

一般来说EBP的内容不会发生变化,无论是入栈还是出栈操作,变化的都是ESP。

这里程序走到一个jmp指令,jmp指令的作用是直接跳转到后面的内存地址的位置,不会对堆栈产生操作。

继续F7单步执行,程序来到调用的函数内部。

push ebp
mov ebp,esp
sub esp,40
push ebx
push esi
push edi
lea edi,dword ptr ss:[ebp-40]
mov ecx,10
mov eax,CCCCCCCC
rep stosd
mov eax,dword ptr ss:[ebp+8]
add eax,dword ptr ss:[ebp+C]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret

这段汇编就是两数相加函数的汇编代码,我们一条条的来分析下,首先

push ebp

这里把EBP的值放入堆栈,后面会用到,主要是为了函数调用完成能做到堆栈平衡。

mov ebp,esp
sub esp,40

重新为当前函数开辟一段堆栈空间,因为之前的堆栈空间我们都已经使用了,在函数调用的时候,程序为了不破坏之前堆栈里存储的内容,会单独开辟出一段堆栈空间来使用,这块空间就是缓冲区,新开辟的堆栈空间的大小是由编译器来决定的,这里我们看到程序开辟了一段大小为0x40的堆栈空间。此时的EBP和ESP都发生了变化。

push ebx
push esi
push edi

在开辟新的堆栈后,程序会把ebx,esi,edi的值压入堆栈中,这里也是为后期堆栈平衡做准备。

lea edi, ss:[ebp-0x40]
mov ecx, 0x10
mov eax, 0xCCCCCCCC
rep stosd

这里的意思是将[ebp-0x40]这个地址赋值给edi,并使用0xCCCCCCCC进行填充,每次填充edi的地址会加4,循环0x10次,因为堆栈是在内存中开辟的一段地址,这段地址可能被其他的程序使用过,在使用过程中会留下一些垃圾数据没有清除,这里就需要我们把新开辟的堆栈全都用0xCCCCCCCC来填充。

这里有个小的知识点,程序存入堆栈的0xCCCC是“烫”的unicode编码,在每次函数调用开辟新的堆栈的时候,程序都会把0xCC写入新开辟的堆栈,这时,如果出现程序访问这些未初始化的堆栈时,会直接输出“烫烫烫烫烫”,这也就是为什么我们在调试程序有时候会“烫烫烫烫烫”了。

mov eax, dword ptr ss:[ebp+0x8]
add eax, dword ptr ss:[ebp+0xC]

接下来就是程序的核心所在了,前面铺垫了那么多,其实就是为了这两行,在前面画的堆栈图中我们可以看到ebp+0x8是1,ebp+0xC是2,add指令的作用是两数相加,那么程序的结果就是1+2然后把结果存到eax里面,这就是1+2最终在汇编中是怎么实现的了。

但是到这里还没结束,虽然函数的功能已经实现,但是程序还需要把堆栈恢复到调用之前的状态,如果不恢复堆栈,程序运行就会崩溃,所以函数在使用完毕后必须恢复堆栈,这里也就是我们常说的堆栈平衡。

pop edi
pop esi
pop ebx

堆栈嘛,肯定是先入后出,出栈的时候ESP的值是加4的,堆栈图如下

mov esp,ebp
pop ebp
ret

这里是把堆栈恢复到call之前的样子,之前我们把ebp的地址存入到了堆栈中,通过pop指令出栈恢复之前ebp的地址,这时不知道大家还记不记得call的时候我们把当时下一跳的地址存入堆栈了,ret指令就是跳转到那个下一跳的地址,堆栈图如下。

其实到这里还没有结束,因为堆栈还没有完全恢复。

add esp, 0x8

最后一步esp+0x8

最终函数运算返回的结果存在EAX里,堆栈也恢复了之前的样子,这里因为堆栈是在函数调用完后做到平衡的,所以叫外平栈。堆栈平衡还有内平栈,内平栈就是函数调用的最后通过ret把栈顶指针还原保持堆栈平衡。

下面是整个的堆栈图

自此至终一个简单的x+y的函数就调用结束了

最后简单总结一下,通过画堆栈图了解了函数调用在堆栈中是如何实现的,1+2虽然很简单,但是程序底层真正运行的时候却要复杂的多,在逆向的过程中,找到关键call,能够搞明白函数的作用至关重要。

在日常的程序中,函数的调用要比这个复杂的多,可能函数里面再调用函数,函数再调用函数,而且涉及到JCC指令的话函数的功能也复杂的多,所以了解堆栈对一个学习逆向的人来说至关重要,通过画堆栈图是最好的了解堆栈的方法。

E

N

D

Tide安全团队正式成立于2019年1月,是新潮信息旗下以互联网攻防技术研究为目标的安全团队,团队致力于分享高质量原创文章、开源安全工具、交流安全技术,研究方向覆盖网络攻防、系统安全、Web安全、移动终端、安全开发、物联网/工控安全/AI安全等多个领域。

团队作为“省级等保关键技术实验室”先后与哈工大、齐鲁银行、聊城大学、交通学院等多个高校名企建立联合技术实验室。团队公众号自创建以来,共发布原创文章370余篇,自研平台达到26个,目有15个平台已开源。此外积极参加各类线上、线下CTF比赛并取得了优异的成绩。如有对安全行业感兴趣的小伙伴可以踊跃加入或关注我们


文章来源: http://mp.weixin.qq.com/s?__biz=MzAwMDQwNTE5MA==&mid=2650246547&idx=2&sn=5398aee300dad3bf37ce29dd29b2f53f&chksm=82ea563ab59ddf2c1ed123314847dd3234346a311070bf1919e2eb6699fd3d510bb67ccaeab1#rd
如有侵权请联系:admin#unsafe.sh