4、固件签名验证流程逆向:使用IDA Pro静态分析思科引导加载程序(ROMMON)中的签名验证函数,定位关键比较指令
好,咱们进入正题。上一章我们聊了怎么把ROMMON从固件里抠出来,这一章我们就要动真格的了——用IDA Pro去逆向分析它的签名验证逻辑。
说实话,我第一次干这事儿的时候,心里也没底。ROMMON这东西,说白了就是思科设备上电后第一个跑起来的程序,它负责初始化硬件、加载IOS镜像。而签名验证,就是它检查“这个IOS是不是官方发的”的关键步骤。
你想想看,如果我能找到它验证签名的地方,并且绕过它,那是不是就能加载任意修改过的固件了?嗯,这就是我们这节课的目标。
4.1 准备工作:加载ROMMON到IDA Pro
先把上一章提取出来的ROMMON二进制文件拖进IDA Pro。我习惯用IDA 7.5以上版本,对MIPS架构支持得比较好。思科的ROMMON大多是MIPS或PowerPC架构,咱们这节课以MIPS为例。
加载的时候,IDA会让你选择处理器类型。选MIPSEL(小端序)或者MIPSB(大端序),具体看你的固件。怎么判断?
- 看文件头:如果第一个字节是0x7F,后面跟着ELF标识,那就是ELF格式,处理器类型在e_machine字段里。
- 看字符串:如果固件里有“MIPS”字样,基本就是MIPS了。
- 实在不行,用binwalk -Y分析一下,它会告诉你架构。
4.2 寻找签名验证函数的入口点
ROMMON加载完后,第一件事不是瞎翻,而是找字符串。签名验证函数里通常会有一些特征字符串,比如:
verify_signature或check_signatureImage signature verification failedSignature OKRSA或SHA256
按Shift+F12打开字符串窗口,搜一下这些关键词。我遇到过好几次,直接就能定位到验证失败时的打印信息。然后交叉引用(按X键)找到调用它的函数,那就是签名验证函数。
举个例子,我在一个Cisco 2901的ROMMON里找到了这么一段:
.text:0x80123456 la $a0, aSignatureVerif # "Signature verification failed"
.text:0x8012345a jal printf
.text:0x8012345e nop
.text:0x80123460 li $v0, 0xFFFFFFFF # 返回-1,表示失败
.text:0x80123464 jr $ra
.text:0x80123468 nop
看到没?这就是典型的失败处理路径。从printf往上翻,就能找到比较指令。
4.3 定位关键比较指令
签名验证的核心,说白了就是“比对”。比对计算出的哈希值和固件里存储的哈希值,或者比对解密后的签名和预期值。
在MIPS汇编里,比较指令通常是:
bne(不相等则跳转)beq(相等则跳转)slt/sltu(小于则置位)
我一般会这样找:
- 在签名验证函数里,找到调用
memcmp或strncmp的地方。很多ROMMON会直接用C库函数做比较。 - 如果没有库函数,那就找循环比较的代码。通常是两个寄存器逐字节比对,然后
bne跳转到失败处理。 - 还有一种情况:用硬件加速模块做比较。这时候比较指令可能藏在某个协处理器指令里,比如
mtc0/mfc0。
我曾经在一个项目里,花了整整两天才找到比较指令。原因是那个ROMMON用了自修改代码(self-modifying code),比较指令是在运行时动态生成的。嗯,那真是让人头大。后来我用了硬件断点,才把它逮住。
4.4 实战案例:Cisco ISR 4331 ROMMON分析
咱们来看一个具体的例子。这是我从Cisco ISR 4331的ROMMON里逆向出来的签名验证函数片段:
// 伪代码
int verify_signature(uint8_t *image, uint32_t image_len, uint8_t *signature) {
uint8_t hash[32];
SHA256(image, image_len, hash);
// 关键比较在这里
if (memcmp(hash, signature, 32) == 0) {
return 0; // 成功
} else {
printf("Signature verification failed\n");
return -1; // 失败
}
}
对应的汇编代码:
.text:0x802A1000 addiu $sp, -0x40
.text:0x802A1004 sw $ra, 0x3C($sp)
.text:0x802A1008 sw $a0, 0x20($sp) # image
.text:0x802A100C sw $a1, 0x24($sp) # image_len
.text:0x802A1010 sw $a2, 0x28($sp) # signature
...
.text:0x802A1050 jal SHA256 # 计算哈希
.text:0x802A1054 nop
.text:0x802A1058 lw $a0, 0x20($sp) # hash
.text:0x802A105C lw $a1, 0x28($sp) # signature
.text:0x802A1060 li $a2, 32 # 长度
.text:0x802A1064 jal memcmp
.text:0x802A1068 nop
.text:0x802A106C bnez $v0, fail # 关键比较!如果memcmp返回非0,跳转到失败
.text:0x802A1070 nop
.text:0x802A1074 li $v0, 0 # 返回成功
.text:0x802A1078 b done
.text:0x802A107C nop
fail:
.text:0x802A1080 la $a0, aSignatureVerif
.text:0x802A1084 jal printf
.text:0x802A1088 nop
.text:0x802A108C li $v0, -1
done:
.text:0x802A1090 lw $ra, 0x3C($sp)
.text:0x802A1094 addiu $sp, 0x40
.text:0x802A1098 jr $ra
.text:0x802A109C nop
看到0x802A106C那行了吗?bnez $v0, fail——这就是我们要找的关键比较指令。只要memcmp返回非0,它就跳转到失败处理。
beqz $v0, fail,意思是如果相等才跳转到失败。这明显是个陷阱,专门坑逆向分析的人。
4.5 绕过思路:修改比较指令
找到关键比较指令后,绕过就简单了。最常见的做法是:
- 把
bnez改成beqz,或者直接改成nop(空操作)。 - 把
li $v0, -1改成li $v0, 0,让失败路径返回成功。 - 直接跳转到成功处理代码,跳过比较。
我个人习惯用第二种——修改返回值。因为改跳转指令有时候会影响后续的代码流程,而改返回值更干净。
比如上面那个例子,我把0x802A108C的li $v0, -1改成li $v0, 0,这样即使签名验证失败,函数也会返回成功。
nop后,设备直接变砖了。后来发现,那个比较指令后面还有一段代码会检查比较结果,如果发现没比较,就会触发看门狗复位。所以,改之前一定要把整个函数的控制流图(CFG)看清楚。
4.6 本章知识体系
下面这张图总结了签名验证逆向的核心流程:
这张图把整个流程串起来了。从加载ROMMON开始,到搜索字符串、定位函数、找比较指令,每一步都有对应的操作。你跟着走一遍,基本就能找到关键点了。
嗯,这一章的内容就到这里。记住,找到比较指令只是第一步,后面怎么改、怎么绕过,才是真正考验技术的地方。下一章我们会讲如何patch二进制文件,以及如何验证patch是否生效。
核心要点回顾:
- 用字符串搜索定位签名验证函数
- 找memcmp或循环比较代码
- 关键比较指令通常是bnez或beq
- 修改返回值比改跳转更稳妥
- 改之前一定要看控制流图