解密并提取 FortiOS 8.0.0 固件镜像。
本脚本扩展了 Bishop Fox 的 Forticrack,以支持 8.0.0 固件镜像。此外,RandoriSec 的文章 关于 FortiGate 7.4.7 固件加密的内容,对逆向 FortiOS 8.0.0 的加密机制非常有帮助,因为它看起来像是 7.4.7 加密机制的更新迭代版本。
同时适用于 FGT 和 FFW 镜像。
已在 FGT 和 FFW v8.0.0.F-build0167 上完成测试。其他构建版本可能需要重新逆向内核,以找到 RSA 公钥和 XOR 密钥的确切内核段及虚拟地址。
该脚本会尝试根据文件名自动检测 .out 文件对应的是 FGT 还是 FFW。也可以作为可选参数显式指定,以防万一。
$ python3 forticrack_v8.py
[x] Usage: python3 forticrack_v8.py <.out file> [FGT|FFW]
演示:

生成的目录:

关于 FortiOS 解密,已经有很多文章和脚本(例如前面提到的那些)。然而,它们都不适用于 FortiOS 8.0.0,因为 Fortinet 再次修改了其加密机制。
Fortinet 允许在 https://support.fortinet.com/ > 登录 > 支持 > VM 镜像 下载 FortiFirewall 和 FortiGate 的升级镜像。这些镜像是 .out 文件,这正是本脚本所期望的输入。执行时,它会执行 4 个主要操作:
.out 文件(Bishop Fox 的工作).out 升级文件使用基于自定义 XOR 的分组密码进行加密。Bishop Fox 对其进行了逆向工程,并发布了 forticrack 以及一篇出色的分析文章。脚本的这一部分实际上使用了与 Bishop Fox 原始 forticrack 相同的代码,它会提取相应的 32 字节密钥并解密 .out 文件。如果你想了解更多信息,建议阅读那篇分析文章。
解密后的文件是标准的 Fortinet 固件镜像。脚本使用 binwalk 对其进行提取,生成以下文件系统:
ext-root
├── boot
│ ├── cert.der
│ └── grub
│ ├── BOOTX64.EFI
│ ├── grub.cfg
│ └── grubx64.efi
├── boot.msg
├── datafs.tar.gz
├── datafs.tar.gz.bak
├── datafs.tar.gz.chk
├── datafs.tar.gz.chk.bak
├── extlinux.conf
├── filechecksum
├── flatkc
├── flatkc.chk
├── flatkc.sig
├── hash_bin.sha256
├── ldlinux.c32
├── ldlinux.sys
├── rootfs.gz
└── rootfs.gz.chk
其中:
boot/ : 包含引导加载程序文件的目录datafs.tar.gz : 数据文件系统flatkc : Linux 内核 bzImagerootfs.gz : 加密的文件系统漏洞研究人员感兴趣的所有文件(例如 /sbin/init)都加密在 rootfs.gz 中。
rootfs.gz(新颖的部分)这是针对 8.0.0 版本新增的部分。为了弄清楚这一点,我大量使用了 Claude Code 来逆向相应的解密逻辑并获取内核镜像中硬编码的虚拟地址,以 RandoriSec 关于 FortiGate 7.4.7 的文章 作为参考。根据我的经验,AI 辅助逆向在分析加密相关内容时确实大放异彩,而这在以前只能手动逆向时是一项经典的硬核任务。
文件 rootfs.gz 使用名为 FORT-RC4 的自定义流密码进行加密。解密所需的密钥嵌入在文件末尾附加的 PKCS#1 RSA 签名中。为了解密此签名,必须使用对应的 RSA 公钥,该公钥可以从内核镜像中恢复。
由于 flatkc 是 bzImage,可以通过定位其中的 gzip 负载并解压来轻松提取内核 ELF。在 ELF 内部,虚拟地址 0xffffffff8179a1a0 处有 270 字节的 XOR 编码 DER 数据,代表 RSA 公钥。用于解码的 32 字节 XOR 密钥位于 0xffffffff8179a2c0。解码只需 decoded[i] = encoded[i] ^ xor_key[i & 0x1f],结果可解析为标准 PKCS#1 RSAPublicKey DER 结构(一个 RSA-2048 公钥)。
恢复 RSA 公钥后,通过计算 m = sig^e mod n 来解密签名块(rootfs.gz 的最后 256 字节)。256 字节的结果是 PKCS#1 v1.5 Type 1 填充的消息,布局如下:
m[0x00] = 0x00
m[0x01] = 0x01
m[0x02..0x9E] = 0xFF (157 padding bytes)
m[0x9F] = 0x00
m[0xA0..0xBF] = SHA256(rootfs.gz[:-256])
m[0xC0..0xDF] = (unused)
m[0xE0..0xFF] = RC4 key (32 bytes)
SHA-256 哈希会与 rootfs.gz 的主体进行校验,作为完整性检查,而末尾的 32 字节 RC4 密钥才是实际用于解密文件的密钥。
关于 FORT-RC4,它完全是 Claude 凭感觉逆向(vibe-reversed)出来的。其工作原理如下:
FORT-RC4 具有标准的 KSA,但 PRGA 经过修改:它不是每轮通过单次 S-box 查找生成一个密钥流字节,而是使用
i和j的位混合版本执行两次额外的查找,将0xAA异或到混合索引中,并组合两个 S-box 值来生成最终字节。FGT 和 FFW 之间也存在差异:在 FGT 中,PRGA 开始前,KSA 之后i和j都被重置为 0,而在 FFW 中j会从 KSA 延续下来。这可以在 FGT 内核中密码函数偏移+0x83处看到,字节序列为31 c0 31 d2(xor eax,eax; xor edx,edx),而 FFW 中没有此序列。这就是脚本需要知道变体的原因。
如前所述,RandoriSec 关于 7.4.7 的文章 是一个有用的参考,但加密机制发生了足够大的变化,他们的方法无法直接应用于 8.0.0,因此主要差异如下:
.init.data 段。在 8.0.0 中,该段全为零,因此改用纯 XOR 方案。rootfs 使用 AES-CTR 加密。在 8.0.0 中,使用 FORT-RC4(一种自定义加密算法)。rsa_parse_pub_key 的交叉引用来定位 RSA 密钥。而 8.0.0 的内核已被剥离符号,因此必须通过直接逆向解密例程来找到虚拟地址。RSA 密钥数据块的虚拟地址在版本之间很接近(偏移了 0x3000),这也为逆向过程提供了帮助。
解密后的输出是真正的 gzip 文件。解压后可以获得 CPIO 归档,这是标准的 Linux initrd 格式,可以使用 cpio -idmv 轻松提取。