Windows堆栈溢出原理

1.栈是什么?

栈是一种运算受限的线性表

其限制是仅允许在表的一端进行插入和删除运算

这一端称为栈顶(TOP),相对的另一端称为栈底(BASE)

向一个栈插入新元素,称作进栈、入栈或压栈(PUSH)

它是把新元素放到栈顶元素的上边,使之成为新的栈顶元素;

从一个栈删除元素,又称出栈或退栈(POP)

它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素

进程使用的内存可以分成4个部分

代码区:存储二进制机器码,存储器在这里取指令

数据区:用于存储全局变量

堆区:动态分配和全局变量

栈区:动态存储函数间的调用关系,保证被调用函数返回时恢复到母函数中继续运行

寄存器与函数栈帧

ESP:栈顶指针寄存器,永远指向系统栈顶

EBP:基址指针寄存器,永远指向系统栈最上边一个栈的栈底

ESP和EBP之间的内存空间为当前栈帧

2.栈的溢出

栈溢出是由于C语言系列没有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小

因此当这个数据足够大的时候,将会溢出缓冲区的范围

3.如何利用

通过程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,

从而破坏程序的堆栈,使程序转而执行其它指令,以达到攻击的目的

造成缓冲区溢出的原因是 程序中没有仔细检查用户输入的参数

覆盖邻接变量

例如buffer大小是8字节

输入8个字符,加上字符串截断字符NULL字符,即可覆盖相邻变量,改变程序运行流程

修改函数返回地址

上述覆盖相邻变量的方法虽然很管用,但是漏洞利用对代码环境很苛刻

更通用的攻击缓冲区的方法是,瞄准栈帧最下方EBP和函数返回地址等栈帧的状态值

如果继续增加输入字符,超出buffer[8]字符边界

将依次淹没 相邻变量、前栈帧EBP、返回地址

4.实例

1)创建一个password.txt文件,内容为1234

2)C语言实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <windows.h>
#define PASSWORD "1234567"
int verify_password (char *password) //密码验证函数
{
int authenticated;
char buffer[44];//缓冲区大小
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}
main()
{
int valid_flag=0;
char password[1024];
FILE *fp;
LoadLibrary("user32.dll"); //准备¸messagebox
if(!(fp=fopen("password.txt","rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Good password is OK,You Win!\n");
}
system("pause");
fclose(fp);
}

代码环境

操作系统 Widows XP SP2
编译器 Visual C++ 6.0
编译选项 默认编译选项
build版本 debug版本

运行测试一下,更改密码文件对比结果

根据函数栈溢出原理,实现栈溢出需要以下过程

(1) 分析并调试程序,获得淹没返回地址的偏移

(2) 获得buffer的起始地址,根据获得的偏移将其覆盖返回地址,使得函数返回时执行buffer起始地址保存的代码

(3) 提取弹框操作的机器码并保存于buffer的起始地址处,在函数返回时得到执行

为什么会覆盖?

如果在password.txt中写入恰好44个字符,那么第45个隐藏的截断符 null 将冲刷

变量authenticated低字节中的 1,从而突破密码验证的限制

出于字节对齐、容易辨认的目的,我们把”4321”作为一个输入单元

buffer[44]共需要11个这样的单元

第12个输入单元将authenticated覆盖;

第13个单元将前栈帧EBP的值覆盖;

第14个单元将返回地址覆盖;

​ 调试栈的布局

通过动态调试,可以得到以下信息

(1) buffer数组的起始地址为:0x0012FAF0

(2) password.txt 文件中第53~56个字符的ASCII码值,将写入栈帧中的返回地址,成为函数返回后执行的指令地址

也就是说,在buffer的起始地址写入password.txt文件中的第53~56个字节

在 verify_password 函数返回时,会跳到我们输入的字符串开始取指执行

(3) 给password.txt中植入机器码,弹出消息框

MessageBoxA是动态链接库user32.dll的导出函数,本实验中未默认加载

在汇编语言中调用这个函数需要获得这个函数的入口地址。

获取弹窗函数入口参数信息

MessageBoxA的入口参数可以通过user32.dll 在系统中加载的基址和MessageBoxA在库中的偏移得到。

用VC6.0自带的小工具”Dependency Walker”可以获得这些信息(可在Tools目录下找到)

随便把一个有GUI界面的程序扔进去,结果如图所示

user32.dll的基址为:0x77D10000

MessageBoxA 的偏移地址为:0x000407EA

基址+偏移地址=MessageBoxA内存中的入口地址:0x77D507EA

我们要弹窗的字符设成”wintry”,用python转换成16进制的ASCII

然后借助OD写汇编代码,获得机器码

将上边的机器码,以十六进制形式逐字写入到 password.txt

第53~56字节填入buffer的起址:0x0012FAF0 ,其余字节用 90(nop) 填充

上边的机器码可能是字符没对齐的原因,会弹出内存读取错误,把字符串改为”wintry00”

成功弹出窗口