内存问题

由于C/C++不是内存安全的语言,实际使用中会出现各种内存不安全使用的问题。本章会详细介绍每种问题,给出例程和说明。

如果想要使用下列例程,请使用 wfuzz-cc/wfuzz-c++ 编译器编译这些程序。通过 main 函数直接调用下列的函数即可。 注意编译时请加上 -O0 编译参数,因为部分例程本身没有副作用,会被优化器整体删除。

WINGFUZZ可检测的内存问题包含以下类型:

1. 栈缓冲区溢出 (stack-buffer-overflow)

风险

极高

说明

当程序读写的内存超出了预定范围时,就有可能造成越界读写的问题。最常见的原因是对字符串的结束符未正确处理,除此之外还可能是因为对边界条件的检查出错引起的。

对于越界读取来说,有可能会读到本身用户无权读取的数据,造成数据泄露。 对于越界写入来说,会造成其他的有效数据被错误的覆盖,甚至覆盖到程序运行时的重要结构,比如栈帧。这些问题可能会造成如远程代码执行等极严重的安全漏洞。

WINGFUZZ平台会给出发生溢出时的代码调用栈。

例程

#include <stdio.h>

void stack_buffer_overflow() {
    char a[3] = { 'a', 'b', 'c' };
    printf("%s\n", a);
}

2. 堆缓冲区溢出 (heap-buffer-overflow)

风险

极高

说明

类似于栈缓冲区溢出问题,不过发生在动态申请的堆内存中。两者的风险也是类似的,都有非常高的安全风险。

WINGFUZZ平台会给出两个调用栈:其一是发生溢出时的代码调用栈,其二是溢出的内存块本身申请时的代码调用栈。

例程

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

void heap_buffer_overflow() {
    int *a = (int*)malloc(3 * sizeof(int));
    printf("%d\n", a[3]);
    free(a);
}

3. 全局缓冲区溢出 (global-buffer-overflow)

风险

极高

说明

类似于栈缓冲区溢出问题,不过发生在全局变量的内存地址中。两者的风险也是类似的,都有非常高的安全风险。

WINGFUZZ平台会给出发生溢出时的代码调用栈,以及溢出的全局变量的名称。

例程

int buf[3];

int main() {
    return buf[3];
}

4. 双重释放 (double-free)

风险

极高

说明

如果针对同一个内存地址释放了两次,就会触发此问题。 一般来说,双重释放说明程序中对象的生命周期管理出现了问题。导致程序中使用了野指针。 这可能会造成如远程代码执行等极严重的安全漏洞。

WINGFUZZ平台会给出三个调用栈: 其一是第二次释放时的代码调用栈; 其二是对应的内存块第一次释放时的代码调用栈; 其三是对应的内存块申请的代码调用栈。

例程

#include <stdlib.h>

void double_free() {
    int *a = (int*)malloc(sizeof(int));
    free(a);
    free(a);
}

5. 释放后使用 (use-after-free)

风险

极高

说明

如果一个内存块释放之后,还在程序中使用,就会触发此问题。 因为释放后的内存块有可能被分配给其他的程序使用, 此时就可能读取或改写其他程序的内存空间。这可能会造成如远程代码执行等极严重的安全漏洞。

WINGFUZZ平台会给出三个调用栈: 其一是使用释放后内存的代码调用栈; 其二是对应的内存块释放时的代码调用栈; 其三是对应的内存块申请时的代码调用栈。

例程

#include <stdlib.h>

void heap_use_after_free() {
  char *x = (char*)malloc(10 * sizeof(char));
  free(x);
  int res = x[5];  
}

6. 申请释放不匹配 (alloc-dealloc-mismatch)

风险

说明

如果内存申请和释放的方式不匹配,就会造成这种问题。错误的释放方式可能会造成其他该释放的系统资源未释放,造成资源泄漏。 更严重的情况下可能会造成程序本身崩溃。

WINGFUZZ平台会给出两个调用栈,其一是释放时的调用栈,其二是对应的内存申请时的调用栈。

例程

#include <string>
#include <stdlib.h>

void alloc_dealloc_mismatch() {
    std::string *a = new std::string;
    free(a);
}

7. 错误的释放 (bad-free)

风险

说明

类似于alloc-dealloc-mismatch,如果释放的指针本身不是通过new或malloc申请出来的,而是其他的指针,就会造成这种问题。 错误的释放方式可能会造成其他该释放的系统资源未释放,造成资源泄漏。 更严重的情况下可能会造成程序本身崩溃。

WINGFUZZ平台会给出释放时的调用栈,以及对应内存地址的动态信息。

例程

#include <stdlib.h>

void bad_free(){
    char a[4];
    char *b = &a[0];
    free(b);             
}

8. 申请内存过大 (allocation-size-too-big)

风险

说明

动态申请内存过大 会导致进程占用过多系统资源,造成系统卡顿或者崩溃。 如果系统将外界可控的输入数据作为申请内存的大小,就可能导致攻击者故意构造极大的数字作为参数。 另一种常见的问题是符号处理的问题,将负数作为参数传给了malloc,会被解读成一个巨大的无符号数。

WINGFUZZ平台会给出内存申请的调用栈,以及此次内存申请的参数。

例程

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

void allocation_size_too_big() {
    void *p = malloc(-1);
    printf("malloc returned: %zu\n", (size_t)p);
}

9. 返回后使用 (use-after-return)

风险

说明

当一个函数返回后,该函数中所有的局部变量的声明周期也会结束,对应的析构函数也已完成调用。 此时这段内存的状态不确定的,如果通过指针/引用等方式访问到了这段内存,可能会造成程序状态错误,导致程序崩溃或者出现信息泄露等问题。

WINGFUZZ平台会给出使用时的调用栈,以及使用到的变量在存活时的栈信息。

例程

int *ptr;

void func(){
  int a = 0;
  ptr = &a;
}

int main(){
  func();
  return *ptr;
}

10. 作用域结束后使用 (use-after-scope)

风险

说明

当一个变量的作用域结束后,该变量的声明周期也会结束,对应的析构函数也已完成调用。 此时这段内存的状态不确定的,如果通过指针/引用等方式访问到了这段内存,可能会造成程序状态错误,导致程序崩溃或者出现信息泄露等问题。

WINGFUZZ平台会给出使用时的调用栈,以及使用到的变量在存活时的栈信息。

例程

void use_after_scope() {
    int *p;
    {
        int a[1];
        p = a;
    }
    return p[0];
}

11. 栈溢出 (stack-overflow)

风险

说明

在软件中,如果调用栈指针超出栈边界,则会发生栈溢出。调用栈可能由有限数量的地址空间组成,通常在程序开始时确定。调用堆栈的大小取决于许多因素,包括编程语言、机器架构、多线程和可用内存量。当程序尝试使用比调用栈上的可用空间更多的空间时(即,当它试图访问超出调用栈边界的内存时,这本质上是缓冲区溢出),被称为栈溢出,通常会导致程序崩溃。

栈溢出的最常见原因是过深或无限递归,其中函数调用自身的次数太多,以至于存储与每次调用相关的变量和信息所需的空间超出了栈的容量。

WINGFUZZ平台会给出溢出时的调用栈。

例程

void stack_overflow() {
    char data[1024];
    stack_overflow();
}

12. 内存泄漏 (memory-leak)

风险

说明

当申请的内存未被释放时,就会出现内存泄漏问题。 内存泄漏可能会造成程序使用的内存持续增加,直到占满全部可用内存,导致系统卡顿或程序崩溃。

内存泄漏的常见原因有两点,其一是申请出的内存指针未被释放就被抛弃。 其二是使用智能指针时,构造了环状引用,导致整个环无法被释放。

WINGFUZZ平台会给出已泄漏内存在申请时的调用栈。

例程

#include <memory>

struct circular_ref {
    std::shared_ptr<circular_ref> ref;
};

int main() {
    std::shared_ptr<circular_ref> a = std::make_shared<circular_ref>();
    a->ref = a;
}

13. 段错误 (segv)

风险

说明

现代操作系统均使用段式内存管理,每个内存段有其读写权限,如果尝试读取/写入没有对应权限的页,就会触发段错误,导致程序崩溃。

最常见的段错误是对NULL指针,或其附近的地址进行了读写。

WINGFUZZ平台会给出发生段错误时程序的调用栈。

例程

int segv() {
    volatile int *x = NULL;
    return *x;
}