在嵌入式固件分析和安全测试中,解密固件的根文件系统(rootfs)通常是进行白盒审计的第一步。
此前,GreyNoise Labs 曾发表过一篇关于《Decrypting FortiOS 7.0.x》的博客,指出旧版 7.0.x 固件使用标准的 ChaCha20 算法对 rootfs.gz 进行加密,且 32 字节的 Key 和 16 字节的 IV 均以明文形式硬编码在内核(flatkc)的静态内存中,通过 lief 和 objdump 就能轻松提取并解密。
但在面对飞塔后续更新的某新版固件(x86_64 架构)时,当我们再次尝试寻找经典的 ChaCha20 常量字符串 "expand 32-byte k" 时,却发现它消失了——飞塔官方在最新版本中彻底重构了固件的安全启动(Secure Boot)与完整性校验矩阵。
通过逆向其内核引导加载流程,我们发现新版固件主要有两处重大改动:
新版固件不再直接在内存中暴露明文对称密钥,而是引入了 RSA-2048 签名与密钥释放机制。在核心函数 sub_FFFFFFFF81710483() 中,固件将解密主文件系统所需的 32 字节核心密钥通过私钥加密,作为一整段 256 字节的 RSA 签名块,强行拼接在加密的 rootfs.gz 文件最末尾。
在最终的解密核心函数 sub_FFFFFFFF81710334() 中,飞塔彻底废弃了 ChaCha20 与标准 AES,转而采用了一种高度定制化、基于经典 RC4 算法演变而来的流密码。整体分为两个阶段:
网络安全设备在引导阶段遵循一个铁律:签名只要校验失败,设备必须立刻锁死。
我们首先在 IDA 的核心内核镜像中搜索系统停机函数(如 machine_halt() 或 panic())。通过对 machine_halt() 进行交叉引用回溯,可以非常轻松地锁定负责固件安全验证的关键核心函数 —— sub_FFFFFFFF81710483()。
顺着校验函数往下看,虽然满眼都是未命名变量,但其密码学特征非常明显:一个长达 270 次的 for 循环,内部带有 & 0x1F(即循环模 32)的异或操作。而在该循环正下方,紧跟了一个典型的外部密码学调用:
这个显眼的函数名瞬间暴露了意图:上面的 XOR 循环,实际上是在还原即将被解析的 RSA 公钥。在 IDA 中双击循环里的两个源数据指针:
在还原这段加密矩阵的过程中,我们遇到了两个经典的密码学对抗与排错点:
内核数据段中有一个硬编码的 32 字节数组 byte_FFFFFFFF8179A2C0(如 5C 19 C6 E1 ...)。许多人会误以为这就是解密大包的 Key,但通过逆向发现,它其实只是一个静态异或混淆种子。
内核在启动时,用它对另一段 270 字节的硬编码密文进行循环 XOR 异或,其真实目的仅仅是为了在内存中还原出那把 RSA-2048 公钥。
当我们成功截获了文件末尾的 256 字节 RSA 密文,并用还原出的公钥执行标准模幂运算 m = c^e mod n 后,解密出了一段 96 字节的有效载荷。
在最初编写脚本时,由于 Python 的 pow(c, e, n) 在大端序转换时会在高位自动补 0x00,而 C 语言内核指针在读取缓冲区(v11 + 223)时具有独特的架构对齐方式,导致切片出来的 Key 整体向左错位了 1 个字节,解密出来的文件始终是一堆无序的二进制乱码。
通过重新推演 PKCS#1 v1.5 的填充边界,我们将切片精确校准到缓冲区的最后 32 字节,终于锁定了真正的核心 RC4 Key:
当密钥边界完全对齐、飞塔变体 RC4 的索引优先级被用 Python 完美重构后,计算出的固件主体实际 SHA256 哈希,与从 RSA 签名块中动态释放出的预期哈希实现了 100% 的完美匹配。
完整的自动化解密与还原脚本如下:
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于 2天前
被aln1lam编辑
,原因: