55、内存逆向(四):堆溢出利用、栈溢出利用、整数溢出

各位好,我是老林。今天咱们聊内存逆向里最刺激的三个话题——堆溢出、栈溢出、整数溢出。这三个家伙,说白了就是程序员的噩梦,也是我们逆向工程师的“提款机”。

我刚开始搞逆向那会儿,总觉得溢出漏洞离自己很远。直到有一次,我调试一个看似稳定的网络服务程序,结果发现只要发一个特制的数据包,整个程序就崩了。嗯,从那以后,我再也不敢小看任何一行代码了。

一、栈溢出:最经典的漏洞

栈溢出,说白了就是往栈里写数据时写过头了。你想想看,栈就像个杯子,你非要往里倒两杯水,那肯定溢出来。

为什么会这样?因为很多程序员写代码时,对输入长度不做检查。比如下面这个经典例子:

void vulnerable_function(char *input) {
    char buffer[64];
    strcpy(buffer, input);  // 这里没检查长度!
    printf("Hello, %s\n", buffer);
}

如果input长度超过64字节,strcpy就会把数据写到buffer之外。而buffer后面紧跟着的,往往是函数的返回地址。

核心思路:栈溢出的利用,就是覆盖返回地址,让程序跳转到我们指定的shellcode。

我个人习惯把栈溢出利用分成三步:

  1. 找溢出点——用fuzzer或者静态分析找到不安全的函数调用
  2. 计算偏移——确定从buffer到返回地址的距离
  3. 构造payload——填充数据 + 覆盖返回地址 + shellcode

我在项目中遇到过这样一个案例:一个老旧的FTP服务器,登录验证时有个栈溢出。当时我花了整整两天才找到正确的偏移量——因为编译器优化导致栈布局变了。嗯,这里要注意,不同编译选项下,栈的布局可能完全不同。

避坑指南:我曾经在32位和64位系统上踩过坑。64位系统下,返回地址是8字节,而且很多系统启用了ASLR和NX保护。这时候单纯覆盖返回地址已经不够用了,得配合ROP技术。

二、堆溢出:更隐蔽的威胁

堆溢出比栈溢出难搞多了。为什么?因为堆的管理机制更复杂。堆不像栈那样有固定的布局,它是由malloc/free动态管理的。

堆溢出的本质,是在堆上分配的内存块中写入了超出边界的数据。这些多余的数据会覆盖相邻堆块的管理信息。

void heap_overflow_example() {
    char *a = malloc(16);
    char *b = malloc(16);
    
    // 假设用户输入了32字节
    read(0, a, 32);  // 这里溢出了!
    
    free(b);  // 这里可能崩溃,或者被利用
}

你看,a只分配了16字节,但用户输入了32字节。多出来的16字节就写到了b的内存区域。如果这些数据覆盖了b的堆块头部信息,那free(b)的时候就会出问题。

堆溢出的利用方式,说白了就是利用堆管理器的元数据来做文章。常见的攻击手法包括:

  • unlink攻击——修改空闲链表指针,实现任意地址写
  • fastbin攻击——伪造fastbin链表,分配内存到任意位置
  • use-after-free配合——先释放再使用,结合堆溢出实现控制流劫持

我记得有一次,我在分析一个浏览器漏洞时,遇到了一个非常巧妙的堆溢出。攻击者通过溢出覆盖了相邻对象的虚函数表指针,然后触发虚函数调用,直接跳到了shellcode。说实话,那是我见过最优雅的利用之一。

警告:堆溢出的调试非常痛苦。我建议你使用AddressSanitizer或者Valgrind来辅助定位。另外,不同glibc版本的堆管理算法有差异,利用代码往往需要针对特定版本编写。

三、整数溢出:被低估的杀手

整数溢出,很多人觉得它不算什么大问题。但我要告诉你,整数溢出往往是栈溢出和堆溢出的“导火索”。

为什么会这样?你想想看,如果整数运算的结果超出了变量能表示的范围,会发生什么?

void integer_overflow_example() {
    unsigned short size;
    char *buffer;
    
    size = 65535 + 1;  // 溢出!size变成了0
    buffer = malloc(size);  // 分配了0字节
    read(0, buffer, 65535);  // 堆溢出!
}

这个例子很典型。size是unsigned short,最大值是65535。加1之后溢出变成0。malloc(0)返回一个很小的缓冲区,但后面却读了65535字节——妥妥的堆溢出。

整数溢出有三种常见类型:

类型 说明 例子
无符号溢出 超过最大值后回绕到0 65535 + 1 = 0
有符号溢出 超过最大值后变成负数 2147483647 + 1 = -2147483648
截断溢出 大类型赋值给小类型时截断 int转short时丢失高位

我个人习惯在逆向分析时,特别关注那些涉及长度计算、循环计数、索引计算的整数运算。这些地方最容易出现整数溢出。

实用技巧:我曾经在分析一个图片解析库时,发现一个整数溢出漏洞。攻击者通过构造特殊的图片尺寸,让内存分配计算溢出,最终导致堆溢出。修复方法很简单——在运算前做范围检查。但很多开发者就是忘了这一步。

四、三种溢出的关系与实战思路

这三种溢出不是孤立的。在实际漏洞利用中,它们经常组合出现。比如:

  • 整数溢出导致分配了过小的缓冲区 → 触发堆溢出
  • 堆溢出覆盖了栈上的指针 → 触发栈溢出
  • 栈溢出覆盖了整数变量 → 触发整数溢出

下面这张图展示了它们之间的关系:

三种溢出漏洞的关系与利用链 栈溢出 覆盖返回地址 堆溢出 覆盖堆块元数据 整数溢出 运算结果异常 分配过小缓冲区 循环计数错误 覆盖栈上指针 典型利用链 整数溢出 绕过长度检查 堆溢出 覆盖关键数据结构 栈溢出 劫持控制流

从这张图可以看出,整数溢出往往是整个攻击链的起点。它导致分配错误,进而引发堆溢出或栈溢出,最终实现控制流劫持。

在实际逆向分析中,我建议你按以下思路来:

  1. 先找输入点——用户输入、网络数据、文件读取等
  2. 跟踪数据流——看输入数据经过了哪些运算和拷贝
  3. 检查边界——重点关注memcpy、strcpy、sprintf等不安全的函数
  4. 验证溢出——用fuzzer或者手动构造输入来触发异常

核心总结:栈溢出靠覆盖返回地址,堆溢出靠篡改堆管理元数据,整数溢出靠运算结果异常。三者经常组合出现,形成完整的利用链。

好了,今天的内容就到这里。这三种溢出漏洞,说白了都是程序员对边界控制不当造成的。作为逆向工程师,我们要做的就是找到这些边界,然后——嗯,你懂的。


公众号:蓝海资料掘金营,微信deep3321