Linux下的栈溢出案例分析-GDB调试操练

摘要:

 本文主要演示linux平台下的栈溢出,首先根据理论对示例代码进行溢出攻击;结果是溢出攻击成立,但是与设想的有差别;然后采用GDB调试工具对发生的意外,进行深入的分析。

测试的平台:

1.  ubuntu 9;   gcc 4.4.1;   Gdb 7.0-ubuntu

2.  ubuntu系统安装在virtual box 3.2.8虚拟机上;

示例代码如下:

#include<string.h>
void overflow(char* arg)
{
	char buf[12];
	strcpy(buf, arg);
}

int main(int argc, char *argv[])
{
	if(argc > 1)
		overflow(argv[1]);
	return 0;
}

如果按照一般的方式编译:

gcc –o stackoverflow stackoverflow.c

linux系统能够探测到程序中的stack  overflow,从而终止程序,如下图所示:

那有没有办法让系统不探测到stack overflow,此处可以在编译时,禁用堆栈保护,具体命令如下:

gcc –fno-stack-protector –o stackoverflow stackoverflow.c

然后采用gdb调试stackoverflow,

这里的输出跟设想的存在很大的差别,因为设想中的函数栈如下:

前面的12个字节填充buf,然后接下来的4个字节填充ebp,最后的4个字节填充RET地址,那么照理说,这里的eip应该是0x65656565,那为什么此处是0x61616161,刚好是aaaa的值呢?

根据单步调试的结果,发现eip变为0x61616161是在main函数退出后达到的,按照设想应该是在overflow退出时,eip变为0x65656565。

为什么overflow退出后还能回到main函数?可能的原因:输入的字符串没有覆盖掉ret地址,但是字符串却意外地将main的返回地址给覆盖掉了?

但就算是覆盖,为什么覆盖的值没有采用e的值,而是采用的a的值?要知道a是在字符串的起始处?这点的确让人奇怪。

1. 我们还是采用一步步调试的方式来观察问题所在,先看下gcc编译后的反汇编代码:

使用到的命令:

set disassembly-flavor intel  //将汇编设定为intel风格;

disassemble main  //反汇编main函数;

Main函数:

1. push ebp
2. mvo ebp, esp
3. and esp, 0xfffffff0
4. sub esp, 0x10
5. cmp DWORD PTR [ebp+0x8], 0x1
6. jle  0x804841d <main+31>
7. mov eax, DWORD PTR [ebp+0xc]
8. add eax, 0x4
9. mov eax, DWORD PTR [eax]
10. mov DWORD PTR [esp], eax
11. call 0x80483e4 <overflow>
12. mov eax, 0x0
13. leave
14. ret

然后再来看下,overflow的反汇编代码,命令:disassemble overflow

Overflow函数:

1. push ebp
2. mov ebp, esp
3. sub esp, 0x28
4. mov  eax, DWORD PTR[ebp+0x8]
5. mov  DWORD PTR[esp+0x4], eax
6. lea  eax, [ebp-0x14]
7. mov  DWORD  PTR [esp], eax
8. call  0x804831c <[email protected]>
9. leave
10. ret

我们单步调试上述的指令,关注其中esp值的变化。总图如下,后面是对其中每一步的分析:

在完成main.1后,命令p $esp后,esp的值变为:Esp = 0xbffff438

Main.3后,esp的值变为0xbffff430,估计是用于对齐;

Main.4后,esp的值为0xbffff420;

Main.7-10,这里主要将argv[]的arg[1]字符串的首地址取出来,并且将其放置在esp中,此时esp的值为0xbffff420;

执行overflow.1后,esp的值变为:0xbffff41c,其中存放着main的下一条语句的地址,

通过命令:x $esp可以看到overflow返回的地址:

0xbffff41c   0x0804841d

Overflow.1执行后,esp的值变为0xbffff418,用于存放ebp的值;

Overflow.3执行后,esp的值变为0xbffff3f0,

Overflow.5-7执行后,将字符串的地址放置在3f0+0x4地址处,然后再将临时字符串也即buf的首地址[ebp-0x14]放置在3f0地址处(当前的esp指向处);

执行完overflow.8后,我们查看buf起始处后,发现的确完成了内容的赋值,命令如下:

X 地址;

执行完leave指令后,发现esp的值变为:0xbffff41c,刚好指向的返回main的地址;然后再执行ret指令后,发现esp变为0xbffff420;然后,程序跳回到main函数;

然后再执行main函数中的leave指令,按照设想中leave指令执行后,esp的值应该变为43c,指向main的返回地址;但是实际我们执行后,esp的值变为0xbffff404,其中的内容刚好是0x61616161,也就是aaaa的值;这里我们测试时使用的参数的头四个字符刚好是aaaa;

到这里为止,就明白整个问题的症结:

Main函数中调用leave指令时,esp的值并没有调整到位。本来应该指向43c(前面的地址忽略)的,此处却指向的404?

那么为什么会产生这样的状况?照理说这是编译器该干的事,为什么此处编译器没有尽责呢?

奇怪的是:

不发生栈溢出时,也即我们输入的字符串长度不超过12时,main中的leave指令执行后,程序可以正常的返回到43c的位置,顺利退出;

那这里的问题就很奇怪了:栈溢出是发生在overflow中,程序可以从overflow中顺利返回到main中,然后main的leave指令就不正常了;如果overflow中没有栈溢出,程序也顺利返回到main函数,然后main的leave指令可以正常工作。

上述的问题的症结在于搞清楚leave指令本身;猜想其可能会依赖某些寄存器,或者依赖特定的存储单元来达到恢复目的。

首先看看能不能stepi进入leave;答案是leave是单条指令,不是一个处理函数;

所以我们试试能不能从寄存器上发现点什么,发现其实对照vistual studio中的操作,

Leave指令应该等效为:

Mov  esp, ebp

Pop  ebp

之前关注的都是esp,那按照上面的等效的话,接下来应该关注ebp寄存器的变化。所以,接下来要做的1. 首先验证上述的的等效成立,2. 要盯着ebp在执行过程中的变化。

对于问题1的验证,我们偷个懒,直接baidu之,发现的确符合我们的猜想,leave指令主要就是恢复esp和ebp之前的存储值。(后面顺带地测试了下,的确也主要做了对应的操作。)

如此我们就回到问题2,主要查看进入overflow和退出overflow时,以及进入main和退出main时,寄存器ebp值的变化。

会溢出的版本下,我们查看call strcpy前和后的ebp的值,如下图所示,我们发现调用strcpy函数的前与后过程中,ebp的值都是418(省略前面的),也就是说,调用strcpy函数过程前后ebp的值是正常的。

那么后续的执行leave指令,按照正常的版本(经过正常版本的验证),那么esp = 418,然后再经过pop ebp后,esp的值变为41c;而ebp的值应该恢复为438;

实际的执行后,esp如预期的发生变化,但是其中ebp的值却没有按照预想中的变化;那么问题只可能出在pop ebp这个语句,也只有一个原因:就是ebp存储的栈中的地址的内容被修改了,也就意味着418地址处的值在函数调用过程中被修改为400,原来应该是438。

那可能出现这种情况的也只有在overflow函数调用中,可能发生这样的情况。根据上述的分析,那我们在栈溢出版本中,在strcpy调用前后查看418处的值,即可证明这点。果然下面的图示正好说明该问题:

执行完strcpy函数后,看倒数最后一行,0xbffff418处的内容被修改为0xbffff400;作为对比,我们来看看非溢出版本的情况:

可以发现最后一行的ebp值没有被修改,因此,不会发生错误。

现在可以确定:ebp的值在溢出的strcpy调用中被修改了,从而导致主函数退出时的问题。那下面的问题是:strcpy是怎么样修改ebp值的?这个问题的解答要深入到strcpy的汇编代码里面,才能得到答案,本文不作探讨,有兴趣的可以再深入研究。

结论与启发:

1. 虽然函数是否可以顺利返回取决于栈上的返回地址,但是此例也让我们看到通过间接地修改ebp的值,也可以达到控制返回地址的目的;只不过这里的修改ebp值不会影响到当前函数的返回,但会影响上一级函数的返回;

2. 虽然对linux下的汇编不熟,但是借鉴visual studio下的代码的熟悉,还是容易读通linux下的汇编的;由此,可以体会的借鉴的价值,而借鉴的前提在于对某些技术的深入;

3. 找问题过程中,采用了对比的手法,比如对比BSD下的上述代码版本,可以按照设想的执行;让我确认上述代码的确存在问题;由此,可以体会对比的价值;

时间: 2024-10-12 19:36:04

Linux下的栈溢出案例分析-GDB调试操练的相关文章

[原理分析]Linux下的栈溢出案例分析-GDB调试操练[4]

摘要: 本系列的3介绍了现有的linux系统对栈的保护,在那种栈保护措施下,要修改SIP(saved instruction pointer)不可能:但是栈保护对象有限,对程序中的数据不一定都能保护到.本文就是探讨程序中有内存操作漏洞时,如何利用漏洞改写数据,达到控制的目的. 测试平台: 1. ubuntu 9, gcc4.4.1, gdb 7.0 2. ubuntu系统安装在virtualBox 3.2.8系统上: 正文: 首先看个如下的示例代码: #include<stdio.h> #in

[原理分析]Linux下的栈溢出案例分析-GDB调试操练[3]

摘要: 本文主要在之前版本的代码基础上,分析下gcc如何进行栈保护以避免栈溢出攻击的. 测试的平台: 1.  ubuntu 9;   gcc 4.4.1;   Gdb 7.0-ubuntu 2.  ubuntu系统安装在virtual box 3.2.8虚拟机上: 正文: #include<string.h> void overflow(char* arg) { char buf[12]; strcpy(buf, arg); } int main(int argc, char *argv[])

[原理分析]Linux下的栈溢出案例分析-GDB调试操练-加强版

摘要: 原来的版本:http://blog.csdn.net/bigbug_zju/article/details/39892129 原版本中的问题主要在于调试过程中,蛮力的痕迹太重,没有很好地体现常用的调试准则:本文在原版本的基础上,融入参考文献中提及的调试原则,重新审视和操练该问题,希望尽量体现出调试中常用的思维法则. 测试的平台: 1.  ubuntu 9;   gcc 4.4.1;   Gdb 7.0-ubuntu 2.  ubuntu系统安装在virtual box 3.2.8虚拟机上

Linux服务器挂死案例分析

问题现象: 在linux服务器上运行一个指定的脚本时,就会出现无数个相同进程的,而且不停的产生,杀也杀不掉,最后系统就陷入死循环,无法登陆,只能人工去按机器的电源键才可以.这够崩溃的吧? 问题分析过程: 在分析过程中发现这个特定的脚本有些特别,和系统中已有的命令的名字是相同的. 以free命令为例: 这个脚本名字就叫做free(后面没有带.sh),而且这个脚本文件里又去调用了free命令. 这个脚本的本意应该是要去调用free命令来完成一个任务. 那是否就是因为这样就会导致问题呢? 其实光这样是

nginx源码分析--GDB调试

利用gdb[i]调试nginx[ii]和利用gdb调试其它程序没有两样,不过nginx可以是daemon程序,也可以以多进程运行,因此利用gdb调试和平常会有些许不一样.当然,我们可以选择将nginx设置为非daemon模式并以单进程运行,而这需做如下设置即可: daemon off; master_process off; 这是第一种情况: 这种设置下的nginx在gdb下调试很普通,过程可以[iii]是这样: 执行命令: [email protected]:/usr/local/nginx/

linux下常用的日志分析命令

linux下常用的日志分析命令 本文介绍下,在linux中常用的一些分析日志的命令行或脚本,有需要的朋友参考下. 形如下面这样的access.log日志内容: 211.123.23.133 – - [10/Dec/2010:09:31:17 +0800] “GET /query/trendxml/district/todayreturn/month/2009-12-14/2010-12-09/haizhu_tianhe.xml HTTP/1.1″ 200 1933 “-” “Mozilla/5.

linux下简单抓包分析

有时候会遇到一些问题需要我们来抓包分析,当手头又没有专业的抓包工具的时候,可以用tcpdump来替代一下(一般的发行版都自带这个工具) 比如我们要分析一下eth0接口下跟192.168.7.188 这个目的IP地址22端口的发包情况 tcpdump -i eth0 dst 192.168.7.188 and port 22 tcpdump -i eth0 dst 192.168.7.188 and port 22 tcpdump: verbose output suppressed, use -

Linux 下的段错误(Segmentation fault)调试方法

我们在用C/C++语言写程序的时侯,内存管理的绝大部分工作都是需要我们来做的.实际上,内存管理是一个比较繁琐的工作,无论你多高明,经验多丰富,难免会在此处犯些小错误,而通常这些错误又是那么的浅显而易于消除.但是手工“除虫”(debug),往往是效率低下且让人厌烦的,本文将就"段错误"这个内存访问越界的错误谈谈如何快速定位这些"段错误"的语句. 下面将就以下的一个存在段错误的程序介绍几种调试方法: 1 dummy_function (void) 2 { 3 unsig

linux下的ELF格式分析

ELF格式文档详解 一,ELF格式综述 ELF(Executable and Linkable Format)是Linux下的一种格式标准,Linux中的ELF格式文件一共有四种: ●可重定位文件(Relocatable File):这类文件包含了代码和数据,可被用来链接成可执行文件或者共享目录文件,扩展名为.o ●可执行文件(Executable File):这类文件包含了可以直接执行的程序,一般没有扩展名 ●共享目录文件(Shared Object File):这类文件包含了代码和数据,扩展