天天看点

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

实验环境:

编译器:vs2015

系统:win10 64位

实验原理

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

如上图所示,栈地址增长方向是向低地址方向增长的,每次调用函数时,先把参数压入栈底,然后会把被调用函数的返回地址(此地址为call指令下一条指令)压到栈底。另外还需要保存main函数的栈底地址在栈里面,被调用函数的栈顶指针esp被保存为该函数的栈底,接下来的低地址位分配局部变量。

如果c/c++没有检测局部变量内容越界问题,那么,局部变量长度过长时,分配的地址空间直接覆盖了old ebp,同时覆盖了ret addr。而当ret addr指向的是有意义的地址,就会直接执行相关的代码,这就是缓冲区攻击。

参考:

Return-into-libc攻击及其防御

实验步骤:

初建项目

在vs2015中新建一个项目,命名为SecurityLab,创建Lab.cpp文件,并把以下代码拷贝到项目中:

#include <stdio.h>
#include <string.h>
void overflow(const char* input)
{
    char buf[];
    printf("Virtual address of 'buf' = Ox%p\n", buf);
    strcpy(buf, input);
}
void fun()
{
    printf("Function 'fun' has been called without an explicitly invocation.\n");
    printf("Buffer Overflow attack succeeded!\n");
    // your other codes, e.g. deleting files
    // what happens when return?
}
int main()
{
    printf("Virtual address of 'overflow' = Ox%p\n", overflow);
    printf("Virtual address of 'fun' = Ox%p\n", fun);
//  char input[] = "AAAbbbbbbaaa\x44\x12\xC4\x00";//bad input
    //注意这里是7个A,但其实默认添加了’\0’在最后面
char input[] = "AAAAAAA";//good input, ASCII code of 'A' is 41
    overflow(input);
    return ;
}
           

设置断点

在overflow函数入口设置断点,同时在overflow函数结束处设置断点,启动调试,发现出现以下错误:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

分析:这个是一个警告来的,如今上升到错误标准,导致无法进行编译。

解决方案:

调试→Security属性→C/C++→SDL检查

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

当启动安全检查时,如果启动sdl检查,就会把额外的安全警告作为错误。

查看汇编指令

在main函数overflow入口的断点处,查看反汇编指令(Ctrl+Alt+D)如下:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

按F10,转到push语句,可以发现eax指向的是input存储的地址0x00ccfec0,如下为input指向地址空间存储的内容:(内存窗口搜索得到)

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

可见此时将参数压入栈中,而add指令对应的地址是作为call结束之后需要跳到的地址,0x00F319A6 这个地址就是overflow函数调用后需要返回的地址。

初探overflow

进入overflow函数内部,查看汇编指令:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

这里ebp指向的值为0x00ccfde4作为该函数入口,然后push压站操作都是为保护现场。通过查看内存栈空间,可以发现该值再偏移4个字节的地址所存的值正是call overflow下一条指令的地址。寄存器存储的值如下:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

而这句话:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

这里开始分配空间,

内存细探

当执行到char buf[8]时,通过内存查找buf位置,位置为0x00ccfdd4,得到内存地址如下:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

这里地址是向低位增长的,留意前面一段:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

后面四位a6 19 f3 00正好是overflow的返回地址,实质为0x00f319a6,可见此时已经将返回地址压入里面了,再前面四位恰好是edi的地址,继续执行,直到执行完strcpy,发现栈空间变化如下:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

RTC检查

查看汇编指令,我们会发现有这个函数:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

这个函数与基本运行时检查有关

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

当被设置如下表示时:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

编译器会对栈空间情况进行检查,如果buf越界,就会报以下错误:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

可见,编译器是可以检查数组越界问题的,这也是一种安全策略。

Security检查

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

这里pop把之前push的内容弹出来,恢复现场,并进行安全检查,这个与下面设置有关:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

其可以检测出堆栈缓冲区溢出,而且能够与sdl检查结合,提醒用户使用安全的类,规避缓冲区溢出风险。

当检测出堆栈缓冲区溢出时,会报以下错误:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

返回main函数

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

如上图指令空间所示,为即将弹出的地址,对比buf的地址0x00ccfdd4与弹出对应的地址相差16字节,而这里的地址偏移量+4byte正好是overflow出口地址。

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

在这个汇编指令里,esp为栈顶指针,始终指向栈顶,而ebp为临时指向栈顶的指针,是用来操作相关栈顶操作的,显然,把栈顶弹出后,esp就会偏移4个字节。

当执行完pop指令,esp指向栈顶地址,此时偏移4字节,正好指向overflow出口地址。

从这里可以看出,我们在编译器无安全检查的机制下,完全可以把这个返回地址覆盖掉,使得返回的地址指向其他函数入口地址,从而实现缓冲区攻击。

缓冲区溢出攻击准备

那么接下来,我们把安全检查禁用,基本运行检查设置为默认值,来通过调试实现缓冲区攻击:

因为vs在每次build过程中都会重新分配堆栈空间,所以每次fun函数地址都会更替,那么我们可以在调试过程中,根据fun的地址直接修改input的值,来完成自定义的其他函数的调用。

另外一方面,在设置安全检查,以及基本运行检查,因为涉及到保护现场状态,故栈堆中会各增加4个字节,即去掉两个保护时,buf初始化地址离返回地址会相差12个字节。

缓冲区溢出攻击

在input定义的时候设置断点,在没有赋值前直接修改buf的值,因为已经经过编译,所以此时修改重新生成代码fun的入口地址是一样的。

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

这里修改input的值为

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

则会发现,返回的时候会直接调用到fun函数,观察内存地址如下:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

后四位在fun函数调用时的压栈变成41414141。

此时,窗口打印出fun函数里的内容:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

执行完,查看汇编指令

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结
vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

此时esp指向0051F854地址,此地址存储着

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

0x0051f890 地址为返回会调用地址,因为该地址不是指向某函数入口或者可执行代码地址,而是

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

所以会有访问冲突,出现以下信息:

vs2015实现缓冲区溢出攻击实验环境:实验原理实验步骤:编译器保护机制思考与总结

至此,缓冲区攻击完成。

编译器保护机制

就vs2015编译器而言,其对于缓冲区溢出的保护是多重的,主要分为以下几种:

基本运行时检查

a /RTCc

向较小的数据类型赋值导致数据丢失,如int a= 4; char c= a;

b./RTCs

堆栈运行时错误检查,其可以检测到局部变量的溢出和不足,也可以进行堆栈指针认证,确保操作指针不是损坏的。

c./RTCu

保证使用的变量被初始化。

d./RTC1 == /RTCs+/RTCu

参考:

运行时错误检查

安全检查

/GS 针对检查缓冲区溢出

对于可能出现缓冲区溢出问题的函数,编译器将在堆栈上返回地址之前分配空间,在进入函数时,用安全Cookie加载分配的空间。在推出函数时,以及在64bit操作系统上展开帧的过程中,将调用helper函数,以确保Cookie值保持不变,不同的值则表示可能已覆盖堆栈。如果检测到不同的值,则终止进程。

受保护项:

函数调用的返回地址,用于函数异常处理程序的地址,易受攻击的函数参数

SDL安全周期检查

启用/GS后,将会执行缓冲区溢出检测的严格模式,等同于#pragma strict_gs_checking(push,on)进行编译,其会把可能造成缓冲区溢出的warning升级为error提醒,导致程序无法编译通过。

思考与总结

实验的关键:

利用局部变量分配栈空间,以及栈空间向低地址增长的特点,来实现局部变量数据溢出而覆盖高地址位的栈空间,通过恰到好处得覆盖esp栈顶地址对应的值,从而跳转到相应代码的指令执行位置,进而执行相关恶意代码,实现缓冲区溢出攻击。

实验的局限性:

  1. 编译器会有相关的安全机制防止缓冲区溢出。
  2. 代码的执行位置是不可预测的。
  3. 指令地址过大也无法用输入字符填充。因为手工输入的字符转换为16进制,只能表示7f以内的,以上的无法用字符填充。

参考:

简明x86汇编语言教程_司徒彦南

继续阅读