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分析一下,它会告诉你架构。
我的小技巧: 加载ROMMON时,IDA可能会提示“未知文件格式”。别慌,选“Binary File”强行加载。然后手动设置基地址。我一般设成0x80000000,这是MIPS Linux内核的典型起始地址。ROMMON通常也在这个范围。

4.2 寻找签名验证函数的入口点

ROMMON加载完后,第一件事不是瞎翻,而是找字符串。签名验证函数里通常会有一些特征字符串,比如:

  • verify_signaturecheck_signature
  • Image signature verification failed
  • Signature OK
  • RSASHA256

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(小于则置位)

我一般会这样找:

  1. 在签名验证函数里,找到调用memcmpstrncmp的地方。很多ROMMON会直接用C库函数做比较。
  2. 如果没有库函数,那就找循环比较的代码。通常是两个寄存器逐字节比对,然后bne跳转到失败处理。
  3. 还有一种情况:用硬件加速模块做比较。这时候比较指令可能藏在某个协处理器指令里,比如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,它就跳转到失败处理。

注意: 有些ROMMON会做双重验证。比如先验证签名,再验证哈希。或者用不同的比较方式。我曾经遇到过一个,它把比较结果取反了——beqz $v0, fail,意思是如果相等才跳转到失败。这明显是个陷阱,专门坑逆向分析的人。

4.5 绕过思路:修改比较指令

找到关键比较指令后,绕过就简单了。最常见的做法是:

  • bnez改成beqz,或者直接改成nop(空操作)。
  • li $v0, -1改成li $v0, 0,让失败路径返回成功。
  • 直接跳转到成功处理代码,跳过比较。

我个人习惯用第二种——修改返回值。因为改跳转指令有时候会影响后续的代码流程,而改返回值更干净。

比如上面那个例子,我把0x802A108Cli $v0, -1改成li $v0, 0,这样即使签名验证失败,函数也会返回成功。

避坑指南: 我曾经改过一个ROMMON,把比较指令改成nop后,设备直接变砖了。后来发现,那个比较指令后面还有一段代码会检查比较结果,如果发现没比较,就会触发看门狗复位。所以,改之前一定要把整个函数的控制流图(CFG)看清楚。

4.6 本章知识体系

下面这张图总结了签名验证逆向的核心流程:

签名验证逆向核心流程 1. 加载ROMMON IDA Pro + Binary File 2. 搜索特征字符串 "Signature failed"等 3. 定位验证函数 交叉引用(X键) 是否有memcmp/strncmp调用? 检查函数内部调用 4a. 定位memcmp参数 找到比较的两个缓冲区 4b. 找循环比较代码 逐字节比对 + bne跳转 5. 定位关键比较指令 bnez/beq + 跳转到失败处理

这张图把整个流程串起来了。从加载ROMMON开始,到搜索字符串、定位函数、找比较指令,每一步都有对应的操作。你跟着走一遍,基本就能找到关键点了。

嗯,这一章的内容就到这里。记住,找到比较指令只是第一步,后面怎么改、怎么绕过,才是真正考验技术的地方。下一章我们会讲如何patch二进制文件,以及如何验证patch是否生效。

核心要点回顾:

  • 用字符串搜索定位签名验证函数
  • 找memcmp或循环比较代码
  • 关键比较指令通常是bnez或beq
  • 修改返回值比改跳转更稳妥
  • 改之前一定要看控制流图

专注资料整理