55、内存逆向(四):堆溢出利用、栈溢出利用、整数溢出
各位好,我是老林。今天咱们聊内存逆向里最刺激的三个话题——堆溢出、栈溢出、整数溢出。这三个家伙,说白了就是程序员的噩梦,也是我们逆向工程师的“提款机”。
我刚开始搞逆向那会儿,总觉得溢出漏洞离自己很远。直到有一次,我调试一个看似稳定的网络服务程序,结果发现只要发一个特制的数据包,整个程序就崩了。嗯,从那以后,我再也不敢小看任何一行代码了。
一、栈溢出:最经典的漏洞
栈溢出,说白了就是往栈里写数据时写过头了。你想想看,栈就像个杯子,你非要往里倒两杯水,那肯定溢出来。
为什么会这样?因为很多程序员写代码时,对输入长度不做检查。比如下面这个经典例子:
void vulnerable_function(char *input) {
char buffer[64];
strcpy(buffer, input); // 这里没检查长度!
printf("Hello, %s\n", buffer);
}
如果input长度超过64字节,strcpy就会把数据写到buffer之外。而buffer后面紧跟着的,往往是函数的返回地址。
核心思路:栈溢出的利用,就是覆盖返回地址,让程序跳转到我们指定的shellcode。
我个人习惯把栈溢出利用分成三步:
- 找溢出点——用fuzzer或者静态分析找到不安全的函数调用
- 计算偏移——确定从buffer到返回地址的距离
- 构造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时丢失高位 |
我个人习惯在逆向分析时,特别关注那些涉及长度计算、循环计数、索引计算的整数运算。这些地方最容易出现整数溢出。
实用技巧:我曾经在分析一个图片解析库时,发现一个整数溢出漏洞。攻击者通过构造特殊的图片尺寸,让内存分配计算溢出,最终导致堆溢出。修复方法很简单——在运算前做范围检查。但很多开发者就是忘了这一步。
四、三种溢出的关系与实战思路
这三种溢出不是孤立的。在实际漏洞利用中,它们经常组合出现。比如:
- 整数溢出导致分配了过小的缓冲区 → 触发堆溢出
- 堆溢出覆盖了栈上的指针 → 触发栈溢出
- 栈溢出覆盖了整数变量 → 触发整数溢出
下面这张图展示了它们之间的关系:
从这张图可以看出,整数溢出往往是整个攻击链的起点。它导致分配错误,进而引发堆溢出或栈溢出,最终实现控制流劫持。
在实际逆向分析中,我建议你按以下思路来:
- 先找输入点——用户输入、网络数据、文件读取等
- 跟踪数据流——看输入数据经过了哪些运算和拷贝
- 检查边界——重点关注memcpy、strcpy、sprintf等不安全的函数
- 验证溢出——用fuzzer或者手动构造输入来触发异常
核心总结:栈溢出靠覆盖返回地址,堆溢出靠篡改堆管理元数据,整数溢出靠运算结果异常。三者经常组合出现,形成完整的利用链。
好了,今天的内容就到这里。这三种溢出漏洞,说白了都是程序员对边界控制不当造成的。作为逆向工程师,我们要做的就是找到这些边界,然后——嗯,你懂的。
公众号:蓝海资料掘金营,微信deep3321