基于栈的缓冲区溢出原理及两个小例子
栈结构:
1.方向:高地址(栈底)->低地址(栈顶)
2.加载完成pe后,系统会为这个pe分配一个栈,这个栈用于 实现(类似C)高级语言中函数的调用。
3.(2)所说的分配的栈是系统自动维护,并且push pop等平衡细节都是透明的。一般来说只有在使用汇编代码的时候,心会和它直接打交道。
函数调用与栈
#include<stdio.h>
void fuc_B()
{
printf("This is Fuc_B !\n");
}
void fuc_A()
{
fuc_B();
printf("This is Fuc_A !\n");
}
int main()
{
fuc_A();
printf("This is Fuc_main !\n");
}
上面这段代码,在经过编译器编译后,各个函数对应的机器指令在代码区可能是这样分布的
可以看到,他们并不是连续的,根据操作系统的不同,编译器和编译选项的不同,同一文件不同函数的代码在内存代码区的分布可能相邻,也可能相离甚远,可能先后有序,也可能无序;但他们都在同一个PE文件中的"节",我们可以简单把他们在内存代码区中的分布位置理解成散乱无关的。
问题: 那散乱无关的代码区段,在我们的代码中又没有直接说明跳转到哪儿,那他是怎么执行main的时候,跳转到fuc_A去,而又在执行fuc_A的时候又去跳转到fuc_B去执行的呢?
简单用OD跟了一下,了解一下这个过程
:
(1)进函数之前:
我们要注意两点
一个是函数的下一跳地址:00d81233
一个是当前主函数所在函数堆的ebp:002DFE34
(2)进函数之后:
可以看到只要我们进了函数,系统会自动帮我们把下一跳指令的地址压进栈
流程:
首先压栈下一跳指令地址,
然后再压栈上层函数的ebp(保存),
把当前的esp赋值给ebp(也就是重新给该函数重新开辟一个栈区)。
效果:
// 当前函数栈空间
//
-----esp==ebp(当前)
//然后把这个esp作为当前函数栈空间的ebp
局部变量
异常处理代码入口地址 //如果函数设置了异常处理
安全cookie //如果编译器加GS选项
ebp(上层)
下一跳指令地址
调用参数
-----ebp //上层函数的ebp(上层)
(3)函数结束,要返回原先位置
步骤:
//保存返回值到eax,可以作为第0步。
1.add esp //clear self stack
2.pop ebp //ebp=ebp(上层)
3.retn== pop eip jmp eip //jump has saved eip
.汇编的retn指令
//不恢复CS寄存器
pop eip
jmp eip
//相对的是retf 恢复cs
pop eip
pop cs
jmp eip
总结图:
实践1(利用溢出原理修改邻接变量)
直接上代码:
#include<stdio.h>
#include<windows.h>
char *PASSWORD = "1234567";
//验证密码
BOOL v_password(char *password)
{
BOOL authenticated=false;//注意定义的顺序
char buffer[8]; //注意定义的顺序
authenticated = strcmp(password, PASSWORD);
strcpy(buffer,password);
return authenticated;
}
int main()
{
BOOL Flag = false;
char password[1024];
while (1)
{
printf("please input password \n");
scanf("%s",password);
if (v_password(password))
{
printf("foolish boy !\n");
}
else
printf("clever boy !\n");
}
}
//注意:strcpy和scanf在较高版本编译器不允许使用,可自行找办法解决。
代码原理
1.输入密码。
2.判断密码得到authenticated。
3.通过strcpy大于password[8]的数,修改栈中的authenticated,使得返回值为真。
上述代码在栈区的存储方式
当然这只是个简略的概图。
本次实验中,vs2008编译器会对在栈区的局部变量存储进行优化,会在authenticated和buffer之间放入8字节的安全区,防止栈溢出。(不管设置对齐系数是多少,变量之间插入的安全区大小都是8)。
红色为优化的安全区,绿色为变量内存
这也就是为什么需要注意定义顺序的原因,由于栈的方向以及先定义先push等原因,只能由定义在authenticated后面的变量溢出,然后去填充authenticated的值。
该实验环境要求:
选项 | 推荐使用的环境 | 备注 |
---|---|---|
操作系统 | windws XP SP2 | 其他Win32操作系统也可以进行本实验 |
编译器 | VisuaL Studio2008 | 如使用其他编译器,需要重新调试 |
编译选项 | 默认编译选项 | 需要关掉GS编译选项 |
Build版本 | debug版本 | 如使用release版本,则需要重新调试 |
点击简单了解GS选项?
通过了解了GS原理以及所处的位置,其实发现GS并不影响我们本次实验。
从上面代码可以看出:
当strcmp的返回值:authenticated为0时,说明密码正确。
假如我们输入密码88888888(8个8)password的值就是“88888888\0"
这时这个\0就会被填充到8字节的安全缓冲区的最低位上去。
当我们想通过填充authenticated为0时,就需要输入8888888888888888(十六个8,将安全区也填充),这时就能达到效果,尽管密码不对,但是authenticated的值也已经是0,程序逻辑觉得我们输入了正确密码
修改成功。
实践2(利用溢出原理修改函数返回地址)
上面的实验方法是很有用的,但是对代码环境的要求比较苛刻,我们需要一个通用的,强大的方法,那就瞄准栈帧最下方的EBP,和函数返回地址。同样的原理去填充
带有GS编译选项的栈内存
不带GS编译选项的栈内存
填充时注意:别忘了01000000(局部变量)与安全Cookie/EBP之间还有四字节的安全区。
上面我们也发现了个问题,从键盘输入的ASCII码有限(0x11,0x12等符号无法直接用键盘输入),所以我们把填充的值改为从文本获取。这样我们就可以把不能键盘注入的ASCII字符用十六进制编辑器写入文件。
代码:
#include<stdio.h>
#include<windows.h>
char *PASSWORD = "1234567";
//验证密码
BOOL v_password(char *password)
{
BOOL authenticated=false;//注意定义的顺序
char buffer[8]; //注意定义的顺序
authenticated = strcmp(password, PASSWORD);
strcpy(buffer,password);
return authenticated;
}
int main()
{
char password[100];
FILE *fp;
if(fp=fopen("password.txt","r+"))
{
fscanf(fp,"%s",password);
if (v_password(password))
{
printf("foolish boy !\n");
}
else
printf("clever boy !\n");
fclose(fp);
}
}
编译成PE(Debug模式)文件,用OD调试看看该怎么改EIP。
这里为了方便,设置了固定基址为0x400000。找到OD中的v_password函数。
下一跳指令地址 : 0x004328F5
EBP : 0x0012FF34
意图修改到该EIP: 0x0043290B
我们从代码中知道,执行完v_password函数就要判断返回值。
进入函数,我们只需要将txt中读取到password copy到buffer中去。
我们先尝试123456789看找的地方对不对:
发现123456789的ASCII已经进来
那我们现在只需要把0x004328F5改为0x43290B
接下来跟实践同样的原理,用8填充栈存储EIP上面的内存,用0x43290B填充存储EIP的位置
计算buffer到EBP存储位置所差字节数=36字节
我们用36刚好填充EIP存储位置,然后由于栈中高地址存储的是高位,所以修改最后四个8为倒序的新的EIP地址
虽然最后会报缓冲区被填充的错误:
只要忽略就好了,只是这次EBP仍然被填充了,函数返回会出错,但是没关系,仍能执行到我们想要的结果。
上一篇: pwn-栈迁移-ROP
下一篇: php合并两个有序数组案例
推荐阅读