查看原文
其他

0基础也能看懂的函数栈结构分析

iDreamer 看雪学院 2021-03-06

本文为看雪论坛精华文章

看雪论坛作者ID:iDreamer





0x01 就给一张图,你能看出啥?


本文为科锐一阶段学员笔记。手上路,就被老师难住。


题曰:看图写结论,写一条得10分,不及格的统统参加补课呀!

图曰:(如图1)

图1





0x02 打眼一看,云遮雾罩


看见

1.左边一溜0018XXXX;
2.中间几片CCCCCCCC;
3.右边有几个认识的字母,比如“hell”。


然而就看见3条,还得不出结论,怎么能及格?!

到底有什么子丑寅卯?




0x03 仔细思量,大有乾坤


看整体布局,有内存地址、有十六进制数据、有字符,初步判断这张图片貌似是用WinHex之类的工具抓取的内存片段。内存里都有啥?代码、数据、栈、堆。那这一段像啥?且瞧瞧去。


1. 找标志性信息


1.1 0018XXXX


0018开头的内存地址是Win7系统栈空间在VC6下的特征,如图2:

图2

VC6下,类似的特征值还有:
WinXP-0x0012XXXX
Win10-0x0019XXXX

在VS的环境下,由于引入了内存地址随机化机制,我们就不能看到固定的特征地址了。


1.2 0xCCCCCCCC


函数局部变量初始化为一片0xCCCCCCCC,符合debug版的特性,这是编译环境为我们调试程序赋予的便利。

有4片0xCCCCCCCC(如图3),说明有4个函数。

图3


1.3 栈底


18FF88、18FF48、18FED4、18FE74,貌似一串连续的栈底(如图4),形成4个栈结构,且存在函数的嵌套调用,因为四个栈堆起来,如果不是嵌套调用,一个函数执行完毕,栈空间会释放掉,调用下一个函数时,栈还在原来的位置,就不会堆起来。


图4


2. 回顾函数调用过程,逐一分析四个函数


2.1 函数调用过程


1.按调用约定传递参数
2.保存返回代码的地址
3.流程转移到被调方代码处
4.保存调用方的栈底
5.更新栈环境为被调函数位置
6.为局部变量申请空间
7.保存CPU环境(/Zi 稳定保存12字节)
8.执行函数体
9.恢复CPU环境
10.释放局部变量空间
11.恢复调用方的栈底
12.取出当前栈顶作为返回地址
13.如果是_stdcall或者_fastcall约定,则此时释放参数空间
14.函数返回,流程回到调用方(如果是__cdecl,则此时释放参数空间)


2.2 简化过程


本图中,由于只体现了函数被调用过程,没有被执行之后的过程,所以简化为:

1.参数入栈
2.返回地址入栈
3.保存栈底
4.变量入栈
5.保存CPU环境(/Zi 稳定保存12字节)

此时,便能按图索骥,去把云里雾里的一片不知道什么鬼,变成可读的、有意义的信息吧!


2.3 main函数


要知道,main函数是程序员的入口,可不是程序的真正入口,它也是被调用的,它的调用方是mainCRTStartup。

main函数有三个参数:命令行个数、命令行字符指针数组、环境变量字符指针数组。三个参数的特征是,第一个是整型十六进制值,后两个是指向堆空间的、离得不太远的两个地址(如图5)。


图5

1.参数入栈--------------------------------------01 00 00 00 B8 0F 2D 00 50 10 2D 00
2.返回地址入栈-------------------------------------------------------------------B9 12 40 00

3.保存栈底-------------------------------------------------------------------------88 FF 18 00

4.变量入栈---------------------------------------------------42 CC CC CC --0A 00 00 00

5.保存CPU环境(/Zi 稳定保存12字节)-----00 00 00 00 00 00 00 00 00 E0 FD 7E

翻译一下:

1.三个参数说明是main函数,命令行个数是1个,命令行字符指针数组和环境变量字符指针数组的首地址分别为0x002D0FB8和0x2D1050;

2.main函数返回地址是0x004012B9;

3.调用方栈底是0x0018FF88;

4.变量看形式有5个,1个int型0A 00 00 00(值为10),1个char型字符串68 65 6C 6C 00(内容为“hell”,00为字符串结束字符‘\0’),1个double型33 33 33 33 33 33 2F 40(值为15.6),1个float型00 00 28 41(值为10.5),1个char型0x42(值为‘B’);

5.当前CPU三个寄存器的值分别为0x7EFDE000、0x00000000、0x00000000。
从main函数自下至上,三个函数暂时称作fun1、fun2、fun3(先声明,再使用)。


2.4 fun1


fun1被main函数调用,栈结构如图6:


图6

1.参数入栈--------------------------------------------------------------------------2C FF 18 00

2.返回地址入栈---------------------------------------------------------------------02 11 40 00

3.保存栈底--------------------------------------------------------------------------48 FF 18 00

4.变量入栈-------------------------------------------------------CD CC 5C 41 0A 00 00 00

5.保存CPU环境(/Zi 稳定保存12字节)------48 FF 18 00 00 00 00 00 00 E0 FD 7E

翻译一下:

1.参数是0x0018FF2C,是指向main函数char型变量的地址,由此可以推断main函数的5个变量有可能是一个结构体的5个成员,否则,仅仅传地址要么造成后面的变量无意义,要么造成不合理的指针运算;

2.fun1函数返回地址是0x00401102;

3.fun1函数的调用方main函数的栈底是0x0018FF48;

4.变量有两个,1个int型0A 00 00 00(值为10),1个float型CD CC 5C 41(值是13.8f);

5.当前CPU三个寄存器的值分别为0x7EFDE000、0x00000000、0x0018FF48,此时fun1的栈顶被更新为其栈底保存的值。


2.5 fun2


fun2被fun1函数调用,栈结构如图7:

图7

1.参数入栈--------------------------------------------------------------------------0A 00 00 00

2.返回地址入栈---------------------------------------------------------------------95 10 40 00

3.保存栈底--------------------------------------------------------------------------D4 FE 18 00

4.变量入栈--------------------------------------------------------CD CC 64 41 14 00 00 00

5.保存CPU环境(/Zi 稳定保存12字节)-----D4 FE 18 00 00 00 00 00 00 E0 FD 7E

翻译一下:

1.参数是0x0A000000(值为0000000A);
2.fun2函数返回地址是0x00401095;

3.fun2函数的调用方fun1函数的栈底是0x0018FED4;

4.变量有两个,1个int型14 00 00 00(值为20),1个float型CD CC 64 41(值是14.3f);

5.当前CPU三个寄存器的值分别为0x7EFDE000、0x00000000、0x0018FED4,此时fun2的栈顶被更新为其栈底保存的值。


2.6 fun3


fun3被fun2函数调用,栈结构如图8:


图8

1.参数入栈-------------------------------------------------------14 00 00 00 CD CC 64 41

2.返回地址入栈--------------------------------------------------------------------43 07 41 00

3.保存栈底--------------------------------------------------------------------------74 FE 18 00

4.变量入栈-------------------------------------------------------CD CC 74 41 1E 00 00 00

5.保存CPU环境(/Zi 稳定保存12字节)-----74 FE 18 00 00 00 00 00 00 E0 FD 7E

翻译一下:

1.参数有两个,一个是0xCD CC 64 41(值为14.3f);

2.fun3函数返回地址是0x00410743;

3.fun3函数的调用方fun2函数的栈底是0x0018FE74;

4.变量有两个,1个int型1E 00 00 00(值为30);1个float型CD CC 74 41(值是15.3f);

5.当前CPU三个寄存器的值分别为0x7EFDE000、0x00000000、0x0018FE74,此时fun3的栈顶被更新为其栈底保存的值。





0x04 写结论


由以上分析,终于可以写结论了,如表:

 
这下不止写10条了,超额完成任务!





0x05 验证


#include <string.h> int fun3(int x, float y){ int n = 30; float m = 15.3f; return 0;}int fun2(int x){ int n = 20; float m = 14.3f; fun3(n, m); return 0;}int fun1(int *p){ int n = 10; float m = 13.8f; fun2(10); return 0;}int main(){ struct tagTest { char ch; float f; double d; char strBuf[5]; int n; }test; test.ch = 'B'; test.f = 10.5; test.d = 15.6; strcpy(test.strBuf, "hell"); test.n = 10; fun1((int *)&test); return 0;}

是不是答对了呢,验证一下吧!


恭喜你已经完成了一次逆向之旅,就是这么简单哦!



- End -


看雪ID:iDreamer

https://bbs.pediy.com/user-891389.htm

  *本文由看雪论坛 iDreamer 原创,转载请注明来自看雪社区。



推荐文章++++

* 对比总结32/64位下Windows部分数据结构的异同

* CVE-2020-1350分析与复现

* 关于抓包的碎碎念

* 不能说的秘密

* 某Nginx后门分析复现与改写




公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



ps. 觉得对你有帮助的话,别忘点分享点赞在看,支持看雪哦~


“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存