堆溢出(Heap Overflow)是一种常见的内存安全漏洞,主要发生在程序动态分配的内存区域(堆)中。当程序向堆中写入的数据超过了分配的内存空间时,就会发生堆溢出。这种溢出可能导致程序崩溃、数据损坏,甚至被攻击者利用来执行任意代码
基本概念
堆是程序运行时动态分配的内存区域,用于存储程序运行期间需要的数据。与栈不同,堆的内存分配和释放由程序员显式控制(如使用 malloc、free 等函数)。堆的管理通常由操作系统的内存管理器或运行时库(如 glibc)负责
原因
- 缓冲区溢出:程序向堆中分配的缓冲区写入超过其大小的数据
例如,使用 malloc(10) 分配了 10 字节的内存,但写入了 20 字节的数据 - 内存管理错误:程序错误地释放内存或重复释放同一块内存
- 未初始化内存:使用未初始化的堆内存可能导致不可预测的行为
- 整数溢出:在计算内存大小时发生整数溢出,导致分配的内存过小
例如:
char *buffer = (char *)malloc(10); // 分配 10 字节的堆内存
strcpy(buffer, "This string is too long!"); // 写入超过 10 字节的数据,触发堆溢出
影响
- 覆盖相邻内存:溢出数据可能覆盖堆中的其他内存块,包括内存管理结构(如 chunk 头)
- 破坏堆管理结构:攻击者可以通过覆盖堆管理结构(如 malloc 的元数据)来控制内存分配和释放行为。
- 执行任意代码:通过覆盖函数指针、返回地址等,攻击者可以控制程序执行流程
利用过程
- 触发堆溢出
输入恶意数据,比如
- 像堆缓冲区写入超长字符串
- 利用整数李处或符号错误导致分配的内存过小
- 覆盖关键数据
- 堆管理结构:如 malloc 的 chunk 头,包含内存块的大小和状态信息
- 函数指针:如动态链接的函数指针(GOT/PLT)、虚表指针(vtable)
- 返回地址:如果堆溢出覆盖了栈上的返回地址,可以控制程序执行流程
- 控制程序执行流程
覆盖函数指针:将函数指针指向攻击者提供的代码(如 shellcode)
利用堆管理机制:通过破坏堆管理结构,触发特定的内存分配或释放行为
返回导向编程(ROP):利用堆溢出覆盖返回地址,构造 ROP 链执行任意代码 - 执行恶意代码
执行 shellcode:将程序流重定向到攻击者提供的 shellcode
调用系统函数:通过覆盖函数指针调用系统函数(如 system("/bin/sh"))
提权:利用堆溢出漏洞提升权限,获取系统控制权
利用技术
- Unlink 攻击
在早期的堆管理实现中,free 函数会通过 unlink 操作将释放的内存块从链表中移除。攻击者可以通过堆溢出覆盖 chunk 头,伪造链表指针,从而在 unlink 操作时写入任意地址 - Use-After-Free (UAF)
利用堆溢出触发 Use-After-Free 漏洞:
- 释放一个堆块后,程序继续使用该内存块
- 攻击者通过堆溢出覆盖释放的内存块,控制其内容
- Fastbin 攻击
Fastbin 是 glibc 中用于管理小内存块的机制。攻击者可以通过堆溢出覆盖 Fastbin 链表中的指针,从而控制内存分配行为 - House of Spirit
攻击者通过堆溢出伪造一个假的 chunk,并将其释放到堆管理器中。随后,攻击者可以分配该假 chunk 并控制其内容 - House of Force
通过堆溢出覆盖 top chunk 的大小,攻击者可以控制后续的内存分配行为,从而分配任意内存区域
示例
简单的堆溢出
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void vulnerable_function() {
char *buffer = (char *)malloc(10); // 分配 10 字节的堆内存
if (buffer == NULL) {
perror("malloc failed");
return;
}
strcpy(buffer, "This string is too long for the buffer!"); // 触发堆溢出
printf("%s\n", buffer);
free(buffer); // 释放内存
}
int main() {
vulnerable_function();
return 0;
}
以上内容只是框架,在之后会慢慢补充丰富