第一章:漏洞挖掘前置知识——MIPS/ARM架构基础、栈溢出原理、堆溢出原理、格式化字符串漏洞
各位同学好,我是老周。做固件安全这些年,我踩过的坑比写过的代码还多。今天咱们聊的这几个前置知识,说白了就是漏洞挖掘的「内功心法」。你想想看,不了解CPU怎么想问题,你怎么能找到它的漏洞?
我个人习惯把这一章叫做「地基」。地基打不牢,后面盖多少层楼都得塌。咱们一个一个来。
1.1 MIPS与ARM架构基础——CPU的「方言」
思科路由器里,MIPS和ARM是最常见的两种架构。它们就像两种方言,语法不同,但表达的意思差不多。
MIPS架构特点
- 固定指令长度:所有指令都是4字节。这让我在分析固件时省了不少心——反汇编时对齐问题少很多。
- 延迟槽:这是MIPS最坑的地方。分支指令后面的那条指令,无论分支是否跳转,都会被执行。我当年第一次逆向MIPS固件时,被这个特性坑了整整两天。
- 寄存器约定:$ra(返回地址)、$sp(栈指针)、$gp(全局指针)这些是硬性规定,不能乱用。
避坑指南:我曾经在分析一个MIPS路由器漏洞时,因为忽略了延迟槽,导致ROP链一直构造不对。后来发现,分支指令后面那条指令其实已经执行了,但我的payload里没考虑到这个偏移量。嗯,从那以后我每次都会手动检查延迟槽。
ARM架构特点
- Thumb/Thumb-2模式:ARM有两种指令集模式,ARM模式(4字节)和Thumb模式(2字节)。切换模式时,地址最低位用来标识模式——1表示Thumb,0表示ARM。
- 条件执行:ARM指令可以带条件码,比如EQ、NE、GT等。这其实是个双刃剑——代码密度高了,但分析起来也更复杂。
- 栈帧结构:ARM的栈帧通常用FP(帧指针)来管理,但编译器优化后可能不用。我建议你在分析时先确认一下编译选项。
| 特性 | MIPS | ARM |
|---|---|---|
| 指令长度 | 固定4字节 | 4字节/2字节(Thumb) |
| 延迟槽 | 有 | 无 |
| 条件执行 | 无 | 有 |
| 返回地址寄存器 | $ra | LR(R14) |
| 常见于 | 路由器、交换机 | 嵌入式设备、手机 |
1.2 栈溢出原理——最经典的漏洞类型
栈溢出,说白了就是往栈里写数据时写过头了。为什么会这样?因为C语言里像strcpy()、sprintf()这些函数,根本不检查目标缓冲区有多大。
我在项目中遇到过一台思科交换机,就是因为一个sprintf()没限制长度,导致远程攻击者可以发送一个超长的HTTP请求头,直接覆盖返回地址。嗯,那台设备后来被我们打了补丁。
栈溢出的核心逻辑
// 一个典型的栈溢出示例
void vulnerable_function(char *input) {
char buffer[64]; // 局部变量在栈上
strcpy(buffer, input); // 没有长度检查!
// 函数返回时,返回地址可能已经被覆盖
}
攻击流程其实很简单:
- 输入超长数据,填满buffer
- 继续写入,覆盖保存的$ra(MIPS)或LR(ARM)
- 函数返回时,跳转到攻击者控制的地址
注意:MIPS和ARM的栈布局略有不同。MIPS的返回地址在$ra寄存器里,但函数开头会把它压栈。ARM的LR寄存器也是类似。所以覆盖的位置需要根据架构调整。
1.3 堆溢出原理——比栈溢出更隐蔽
堆溢出和栈溢出最大的区别是什么?栈溢出直接控制返回地址,堆溢出则要绕个弯子。堆上的数据通常是指针、长度、对象引用这些东西。你覆盖了它们,就能间接控制程序流程。
我记得有一次分析一个思科无线AP的固件,发现一个堆溢出漏洞。攻击者发送一个精心构造的802.11管理帧,就能覆盖堆上的一个函数指针。嗯,那个漏洞的利用链我写了整整一周。
堆溢出的常见利用方式
- 覆盖堆块元数据:比如覆盖size字段,造成堆块重叠
- 覆盖函数指针:堆上如果有虚函数表或回调函数指针,覆盖后就能劫持控制流
- 覆盖长度字段:造成后续的越界读写
我的经验:分析堆溢出时,我建议你先搞清楚目标设备用的是哪个堆分配器。思科固件里常见的是dlmalloc或jemalloc。不同分配器的元数据结构不同,覆盖的位置也不同。
1.4 格式化字符串漏洞——被低估的「瑞士军刀」
格式化字符串漏洞,很多人觉得它过时了。其实不然。我在思科固件里至少发现过3个这种漏洞,都是因为开发人员偷懒,直接用了printf(user_input)而不是printf("%s", user_input)。
这个漏洞能干什么?
- 读任意内存:用
%x、%s等格式符,可以泄露栈上的数据 - 写任意内存:用
%n格式符,可以把已输出的字符数写入指定地址
// 漏洞代码示例
void log_message(char *user_msg) {
char buffer[256];
snprintf(buffer, sizeof(buffer), user_msg); // 漏洞!应该用 "%s", user_msg
syslog(LOG_INFO, buffer);
}
你想想看,如果攻击者输入%x.%x.%x.%x,就能把栈上的值一个个打印出来。如果输入%n,就能往栈上的某个地址写入值。配合好偏移量,就能实现任意地址写。
实战技巧:在MIPS架构上利用格式化字符串漏洞时,要注意参数传递方式。MIPS用$a0-$a3传递前4个参数,多余的参数在栈上。ARM则是用R0-R3。所以格式化字符串的偏移量计算,不同架构不一样。
知识体系总览
下面这张图,是我自己总结的本章知识体系。你可以把它当作一个思维导图来看。
这张图把四个知识点串起来了。你注意看,它们之间其实有联系——栈溢出和堆溢出都是内存破坏,格式化字符串则是信息泄露+任意写。MIPS/ARM架构知识,则是你分析这些漏洞时的「翻译工具」。
我的建议:学完这一章后,你可以找个真实的思科固件练练手。比如CVE-2019-1862,就是一个典型的栈溢出漏洞。逆向分析时,重点关注strcpy、sprintf、printf这些函数调用。嗯,你会发现,漏洞其实就藏在这些不起眼的代码里。
好了,这一章的内容就到这里。记住,漏洞挖掘没有捷径,但掌握了这些前置知识,你至少不会在起跑线上摔倒。下一章咱们聊具体的漏洞分析方法,到时候见。