栈溢出原理浅析

​作者:李泉 爱好终端安全的朋友们大家好。最近小编在工作中遇到了许多基于堆栈的缓冲区溢出的例子,而一直没有一个时间记录下来分享给大家,所以我决定写一个简单的BLOG让我们讨论一下这个老生常谈的话题。在开始栈溢出原理分析之前,我们先看一些基础知识。

0x1 什么是栈

堆栈是一种具有一定规则的数据结构,我们可以按照一定的规则进行添加和删除数据。它使用的是后进先出的原则。在x86等汇编集合中堆栈与弹栈的操作指令分别为:

PUSH:将目标内存推入栈顶。

POP:从栈顶中移除目标。

下面我们展示的是一段普通C程序的内存布局,包括堆栈,他们常被用于函数的调用与返回。

图表 1:一个普通的C程序的内存分布

TEXT:包含了要执行的程序代码。

Data:包含程序需要的全局数据、资源等。

Stack:包含函数的输入参数,返回地址以及保存函数的局部变量等。Stack是后进先出的结构。随着函数的调用,它在内存中(从高地址到低地址)向下寻址。

Heap:保存所有动态分配的内存。每当我们用malloc分配获取内存指针时,这个地址就是从堆中分配的。

在基于栈的缓冲区溢出的情况下,我们要重点关注EBP、EIP和ESP这三个寄存器。函数中,EBP指向栈底的高地址,ESP寄存器指向栈顶的低地址。

图表 2:ESP、EBP在栈中的位置

当执行一个函数的时候,相关的参数以及局部变量等等都会被记录在ESP、EBP中间的区域。一旦函数执行完毕,相关的栈帧就会从堆栈中弹出,然后从预先保存好的上下文中进行恢复,以便保持堆栈平衡。CPU必须要知道函数调用完了之后要去哪里执行(EIP指向),这往往从堆栈弹出的过程中进行EIP赋值的。

为了便于理解,我们假设有一个main函数调用func()函数的程序。程序运行之后,调用func函数,将所需要的参数,全部堆入栈中,然后是返回值入栈此时的栈内分布如下:

图表 3:函数调用的堆栈分布

在func执行完成之后,相应的栈帧被弹出,此时存储返回值的地址被加载到了EIP寄存器中以继续执行main中剩余的部分。而我们的目的就是控制这个返回值,这样我们就能劫持func返回到指定的恶意代码中去。

0x2 堆栈溢出

请看如下C代码:

#include

#include

void function2() {

printf(“Execution flow changed\n”);

}

void function1(char *str){

char buffer[5];

strcpy(buffer, str);

}

void main(int argc, char *argv[])

{

function1(argv[1]);

printf(“Executed normally\n”);

}

1) Main函数调用function1并打印“Executed normally”消息。

2) function1创建了长度为5的一块缓冲区,并且复制Main函数传递过来的字符串

3) function2打印了“Execution flow changed”,程序中的其它位置没有调用过function2函数。

我们的目标是通过以上程序,传入一个特定的参数,来控制整个程序流执行function2函数。下面我们开始对这段代码进行编译。

gcc -g -fno-stack-protector -z execstack -o bufferoverflow overflow.c

-g:告诉GCC编译器将调试信息以及符号等编译到程序中。

-fno-stack-protector:关闭堆栈保护机制。

-z execstack:打开堆栈可执行机制。(关闭堆栈执行保护)

下面我们运行程序。如图

图表 4:以AAAA作为参数运行程序

下一步使用AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA作为参数来破坏这个程序,这个程序会因为堆栈被破坏而崩溃。

图表 5:程序崩溃

我们打开GDB来分析一下程序崩溃的原因。使用GDB打开崩溃的二进制文件。使用list选项来显示源代码,然后在函数中调用,在strcpy和函数返回的地方添加断点。

图表 6:GDB调试

图表 7:EBP、ESP

strcpy执行过后,EBP与ESP的内容是:

函数退出时EBP与ESP为:

结合之前我们看过的堆栈分布图,返回地址应该是在EBP下方。在计算出EBP以及返回地址后,我们可以尝试劫持程序执行的位置。

下面我们寻找function2的地址来劫持执行。要获取function2的起始地址,请使用以下命令。

disass function2

图表 8:获取function2的起始地址

我们要知道,本例我们使用的是基于INTEL架构的VM虚拟机,使用的是小字节序,所在我们生成我们需要的载荷(PAYLOAD)之前,将所有字节反转过来。现在使用PYTHON来生成我们的有效载荷,并使用生成的字符串作为程序参数的内容。

run $(python -c 'print "A"*17 + "\x1b\x84\x04\x08"')

运行,function2函数被执行,执行流被更改。

图表 9:运行结果

至此,我们成功的利用了堆栈缓冲区溢出。

作者|李泉(liquan165) 梆梆安全研究院高级安全研究员

主要研究领域|车联网、工控、物联网安全、终端安全等

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180321A14T4500?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券