一、使用Ollydebug分析画出堆栈图
1.C语言编程版本
-
分为Debug版和Release版。
-
Debug版: Debug 是“调试”的意思,Debug 版本就是为调试而生的,编译器在生成 Debug 版本的程序时会加入调试辅助信息,并且很少会进行优化,便于程序员调试程序。不是任何一个程序都可以调试的,程序中必须包含额外的辅助信息才能调试,否则调试器也无从下手。
学习时,推荐使用debug版本,代码写好方便我们逆向分析
-
Release版: Release 是“发行”的意思,Release 版本就是最终交给用户的程序,编译器会使尽浑身解数对它进行优化,以提高执行效率,虽然最终的运行结果仍然是我们期望的,但底层的执行流程可能已经改变了。编译器还会尽量降低 Release 版本的体积,把没用的数据一律剔除,包括调试信息。使得程序在代码大小和运行速度上都是最优的,以便用户很好的使用。
由于会做优化,那么有些代码可能会缺失,不方便我们分析
-
2.准备知识
-
堆栈是用来保存文件执行时的一些临时变量的值或者地址,或者其他一些相关信息的!
-
栈的地址上面大还是下面大,是自己规定的,都可以,这里我们就按照OD上的来,栈顶的指针小,逐渐+4,最后到栈底,即压栈栈顶地址-4,出栈栈顶地址+4
-
为什么文件中函数一上来就各种push指令:
- 因为CPU的寄存器就那么几个,不同的指令都会影响寄存器中的值,那么有些寄存器的值如果后面还要用,但是其他的值又需要使用此寄存器,此时就需要将这个状态、数据、信息、地址、值压入到堆栈中临时的存储起来,后面如果要使用这个值就可以在堆栈中找到,所以有时候执行到不同的函数时,一进函数就各种PUSH 寄存器的操作,就是为了保留现场,记下函数执行前的状态,如果函数执行完后,能够还原现场!
-
我们直接用debug版写好c语言程序,将可执行文件通过OD打开,如果我想分析代码中的堆栈的部分,就要先定位到堆栈操作开始的代码部分,然后通过CTRL+G输入地址将地址定位到堆栈操作开始代码。
-
接着就可以按F2设置断点,按F8开始单步调试。
-
用OD打开可执行文件可以看到指令前面都对应了一个地址,而且地址间隔大小也不是像堆栈中每个存储单元大小固定、所以相邻地址相差4字节。后面如果学到硬编码,就知道每一个指令最多占几个字节,这是语法规定的
-
期间观察右上部分的寄存器部分:
- EIP这个寄存器是最忙碌的,因为当中时刻记录着CPU正在执行此文件的哪个地址部分,所以EIP也是我们学习的重点,如果可以控制EIP,那么我就可以通过EIP跳转执行文件的任何地方
-
call指令说明:如果调试时遇到call指令,表示这是一个函数,需要调用。那么此时就不能按F8,不然会直接跳过函数,不会执行函数。我们要按F7来进入函数,即调用函数。
-
那么call指令作用:
-
将EIP的地址改为函数的首地址。
call指令可以改变EIP的值,因为函数的地址肯定不在现在显示的地址处,所以调用函数就要先找到函数所在地址,那么函数在哪,就改变EIP中的值为哪
-
将下一个要执行的指令的地址(返回地址)压入堆栈中保留起来
因为call指令会让CPU从现在的地址,去到函数的首地址开始执行,但是执行完函数肯定还要回来的,接着执行下一条指令,那么就要记录一下该指令的地址的下一跳指令的地址,这种下一个要执行的指令地址称为返回地址!
-
-
-
JMP指令:当调试遇到call函数,按F7先会跳转到一个JMP指令所在的地址,这个JMP只是做一个跳板,用来过度,没有任何意义。
-
填充缓冲区:往缓冲区填一堆0xCCCCCCCC,这个cc…就是int3的机器码,int3是软件中断指令,先记住,程序debug调试的时候都会在缓冲区中添加cc…
MOV ECX,12 MOV EAX,CCCCCCCC REP STOS DWORD PTR ES:[EDI] -
REP STOS DWORD PTR ES:[EDI]:循环次数由ECX中的值决定;加入到EDI中的值由EAX中的值决定。且不能重复加到EDI中,每次添加完后默认情况(D 对应的 0)EDI的地址会自动加4,即将值添加到[EDI+4]这个地址中;如果改变D 对应的为 1,则每次添加都是往小的添加/往上面添加,即[EDI-4]。
-
一般在一个函数中见到将一个值mov进
[ebp-4]这个地址,就表示这个值是局部变量;见到一个值MOV进[ebp+8]这个地址的,就表示这个值是参数 -
如果见到
[ebp+4]想要去改函数的返回地址的时候,就要注意了,可能是病毒。很多杀毒软件就是靠[ebp+4]来判断这是否是一个病毒的,因为它想要改函数的返回地址 -
经验:如果你要找函数的返回值重点关注:[EAX]中的值;如果你要找参数重点关注[ebp+8]后面的东西;如果你要找局部变量重点关注[ebp-4]后面的东西
-
函数结尾会有很多pop指令是什么含义:
- 用来恢复现场,把最开始函数没执行时存在堆栈临时储存起来的一些数值的值或者地址还原回去
-
可以看到在函数调用完以后,函数执行时的一些垃圾信息还在堆栈中,而且如果下一个函数的局部变量没有赋初始值,那么可能局部变量会使用到堆栈中的垃圾数据,因为局部变量一般在栈中存储的位置是[ebp-4],如果局部变量没有赋初始值去覆盖掉此时[ebp-4]地址中的值,那么局部变量就可能用到了[ebp-4]原来存储的垃圾值
-
retn:将堆栈中的函数返回地址弹出到EIP(pop EIP)
-
平衡堆栈:函数调用完为了恢复堆栈原来的状态,需要将栈顶和栈底地址变为和初试状态一样,相当于栈顶栈底指针没有发生变化!此时就要平衡堆栈,分为内平栈和外平栈。外平栈就是调用函数中我不管堆栈平衡,我在函数调用完接着的外面的代码来平衡堆栈;而内平栈是在retn后面就把堆栈平衡了。
3.开始画堆栈图
-
写好一个C程序:我们画堆栈图需要从
int r = Plus(3,4)这个地方开始画,所以我们先在此语句行设置断点,f5进入调试,alt+8打开反汇编,找到这代码指令的地址为0x00401078。接着打开OD,CTRL+G定位此地址

-
每次执行一条指令时:通过查看寄存器的值变化,以及栈中存储的值,来画出栈图

二、课后作业
1.利用堆栈执行死循环函数
-
程序如下
#include <stdio.h> void Attack(){ //这是一个死循环的程序 while(1){ printf("攻击程序\n"); } getchar(); } int main(int argc,char* argv[]){ int arr[5]={0}; arr[6] = (int)Attack;//这里没有直接调用Attack()函数,而是将Attack函数的地址 return 0; } -
程序的入口是main函数,所以我们先找到main函数中第一条语句的地址,即
int arr[5]={0}语句的地址为0x004010E8,设断点,调试,可以看到栈顶和栈底
-
执行第一条指令
mov dword ptr [ebp-14], 0:由于数组的大小定义为5,且赋了初始值为0,即所有初始值都为0,ebp-0x14,0x14十进制为20,一个栈单元4字节,所以20/4=5,所以将栈底向上的第五个单元存入0
-
XOR eax,eax:表示取eax中的值与eax中的值做亦或,得到的结果存入eax,因为eax中的值此时为cccccccc,所以做完亦或结果为0,将0存入eax寄存器中
-
然后将eax中的值依次存入到数组中,从数组头地址依次向下存入
-
但是此时数组的空间地址是从0x0019ff1c到0x0019ff2c。但是下面一条语句
mov dword ptr [ebp+4], 40100A,将一个地址值存入了[ebp+4]地址中,但是ebp+4这个地址已经不属于栈了,所以发生了缓冲区溢出,前面准备知识中说过:如果见到[ebp+4]想要去改函数的返回地址的时候,就要注意了,可能是病毒。很多杀毒软件就是靠[ebp+4]来判断这是否是一个病毒的,因为它想要改函数的返回地址。因为ebp+4中存的是返回地址,此函数指令执行完时是要将ebp+4的值存入EIP的,EIP中值是什么地址,执行完此函数就会跳转到什么地址,开始执行这个地址后面的指令。
-
而此时我们查看一下40100A这个地址是什么,是一条jmp指令,跳转到Attack函数
-
后面几行代码就是还原现场
-
最后执行
retn指令:将堆栈中的函数返回地址弹出到EIP(pop EIP)
-
执行完后:esp值+4变成0019ff38,且EIP值变为0040100A,执行的是跳转Attack函数的指令,则最后CPU会去执行Attack函数中的指令
-
Attack函数中有死循环,则会一直执行下列标红的语句
2.利用覆盖局部变量死循环
-
代码如下:
#include <stdio.h> void HelloWorld() { int i = 0; int a[] = {1,2,3,4,5,6,7,8,9,10}; for(i=0;i<=10;i++) #数组只有占栈中的0-9,但是i=10时 { a[i] = 0; #a[10]表示栈中的局部变量i所在地址,将0覆盖了10,又从0开始 printf("Hello World\n"); } } int main(int argc, char* argv[]) { HelloWorld(); getchar(); //清空缓存区,'\n'这个字符清除掉 return 0; } -
原因如下图所示:
i从0到9都是正常的
当i=10时就不正常了,本来应该为10,但是被0覆盖了,i又从0开始了
再接着无限的循环下去
本文介绍了如何使用Ollydebug分析C程序的堆栈,讲解了Debug与Release版本的区别,以及堆栈的工作原理。通过示例程序展示了如何利用堆栈执行死循环函数,详细解析了函数调用过程中的堆栈变化,包括call指令、返回地址、局部变量和缓冲区溢出。此外,还分析了如何通过覆盖局部变量导致无限循环的情况。
3881

被折叠的 条评论
为什么被折叠?



