缓冲区是存储器存储区,当数据从一处传送到另一处时,它会暂时保存数据。缓冲区溢出(或缓冲区溢出)发生在数据量超过缓冲区容量时。其结果是,试图向缓冲区写入数据的程序覆盖了相邻存储器。

关于缓冲区溢出漏洞那些事之C-gets函数插图
关于缓冲区溢出漏洞那些事之C -gets函数

缓冲区溢出是指当数据超过处理常识回传堆栈位置限制时所发生的异常操作。原因如下:

  1. 程序设计有缺陷
  2. 特别是 C语言,它不像其他高级语言那样,会自动检查数组或指针堆栈区的边界,从而增加了溢出的风险;
  3. C标准库在 C语言中还有一些非常危险的操作函数,如果使用不当,可能会造成溢出。

什么是缓冲区溢出攻击

攻击者利用缓冲区溢出问题来覆盖应用程序的内存。这会改变程序的执行路径,触发文件损坏或暴露个人信息。例如,攻击者可能会引入额外的代码,向应用程序发送新的指令以便访问 IT系统。

如果攻击者知道程序的内存布局,他们会故意提供缓冲区无法存储的输入,以覆盖保存可执行代码的区域。例如,攻击者可能覆盖指针(对象指向另一内存区域),并将其指向漏洞利用负载,从而控制程序。

缓冲区溢出攻击的类型

基于栈的缓冲区溢出更常见,并且只在函数执行过程中使用堆栈内存。

基于堆的攻击执行起来会更加困难,并且会使分配给程序的内存空间超出当前运行时操作的内存。

哪些编程语言更容易受到攻击?

C和 C++是最容易受到缓冲区溢出攻击的两种语言,因为它们不具备覆盖或访问内存数据的内置保护机制。Mac OSX, Windows和 Linux使用 C++编写代码。

像 PERL, Java, JavaScript, C#这样的语言都使用内建的安全机制将缓冲区溢出的可能性降到最低。

如何防止缓冲区溢出

开发者可以在代码中设置安全措施,或者使用提供内建保护的语言来防止缓冲区溢出

此外,现代操作系统有运行时保护功能。有三个常见的保护措施:

地址空间随机化 (ASLR)——随机移动数据区域的地址空间位置——随机移动数据区的地址空间位置。通常,缓冲区溢出攻击需要知道可执行代码的位置,随机化地址空间几乎不可能做到。

数据保护——标记特定的内存区域不能执行或可执行,从而阻止攻击在非执行区域执行代码。

结构化异常处理器覆盖保护—帮助阻止恶意代码攻击结构化异常处理(SEH)。这样就能防止攻击者利用 SEH覆盖技术。在功能层面, SEH覆盖使用基于堆栈缓冲区溢出来覆盖线程栈中存储的异常注册记录。

在代码和操作系统保护方面没有足够的安全措施。当一个组织发现缓冲区溢出漏洞时,他们必须迅速做出反应,修复受影响的软件,确保软件用户能够访问补丁。

示例代码

根据STACK1_VS_2017.cpp代码进行修改

#include <stdlib.h>
#include  <stdio.h>
#include "Windows.h"

int main(int argc, char **argv) {

MessageBoxA((HWND)-0, (LPCSTR) "缓冲区溢出测试\n", (LPCSTR)"功能", (UINT)0);

int cookie;
char buf[2];
    int *a = &cookie;
    char * b = buf;
printf("buf: %08x cookie: %08x\n", b, a);
    u_int64 p =(u_int64)a-(u_int64)b;
    printf("两变量内存地址之差=%d\n",p);
gets(buf);
if (cookie == 0x41424344)
printf("缓冲区溢出成功!\n");

}

运行效果

使用MessageBoxA函数检测程序是否正常运行,点击确定开始测试

使用printf()函数输出提示信息,使用gets()函数获取用户输入信息

任意输入两个数值,不满足条件,程序运行完毕

代码分析漏洞成因

诱因:char buf[2];

代码部分解析

代码使用 char,声明变量 buf为2个元素阵列,其中字符 buf有2个字节长度。

#include <stdlib.h>
#include  <stdio.h>
#include "Windows.h"

int main() {
    char test[] = "test1";
    printf("test1初始值为%s\n清输入st值:",test);
    char st[2];
    gets(st);
    printf("输出test:%s\n",test);
    printf("输出st:%s\n",st);
}

提示:u_int64 p=(u_int64)a-(u_int64) b;代码部分对程序变量的内存地址做了减分运算,并将其赋值给变量 p (为了使运算成立,还对变量进行了类型转义),结果显示了两个变量的内存地址距离,方便溢出。

隐患:使用 gets ()函数获取输入数据,因为 gets函数可以无限读取数据而不检查缓冲区大小限制,因此会继续向堆栈中写入超出缓冲区的数据,从而造成溢出风险。

关于缓冲区溢出漏洞那些事之C-gets函数插图4

从运行效果来看,超过堆栈空间继续写入堆栈会覆盖 test在堆栈中对应的值,导致值发生变化:test1-3456

对运行过程堆栈进行反汇编分析

通过运行得知程序初始关键字:test1初始值test1

通过这个关键字,可以快速定位到程序相关函数的运行区域。

修复建议

可使用fgets或gets_s函数替换gets函数

函数解析

  1. fgets()函数的第2个参数指明了读入字符的最大数量。
  2. 如果该参数的值是n,那么fgets()将读入n-1个字符,或者读到遇到的第一个换行符为止;
  3. 这里为3,故获取的数值为12,共输入6个数读取到第2个结束