-
-
[原创]Forti8.0固件解包
-
发表于: 2天前 1049
-
一、 背景
在嵌入式固件分析和安全测试中,解密固件的根文件系统(rootfs)通常是进行白盒审计的第一步。
2024 年 4 月,GreyNoise Labs 发表了一篇关于《Decrypting FortiOS 7.0.x》的博客。博客指出,在飞塔旧版的 7.0.x 固件中,官方使用了标准的 ChaCha20 算法对 rootfs.gz 进行加密,并且其解密所需的 32 字节 Key 和 16 字节 IV 均以明文形式硬编码在内核(flatkc)的静态内存中。研究人员可以通过 lief 和 objdump 轻松提取 Key 并完成全量解密。
然而,在面对飞塔后续更新的某新版固件(x86_64 架构)时,当我们再次尝试寻找经典的 ChaCha20 常量字符串 "expand 32-byte k" 时,却没有找到这个字符串。飞塔官方在最新版本中彻底重构了固件的安全启动(Secure Boot)与完整性校验矩阵。
二、 新版固件算法
通过逆向其内核引导加载流程,我们发现了两个的算法演进:
1. 放弃硬编码,引入“非对称密钥包裹矩阵”
新版固件不再直接在内存中暴露明文对称密钥,而是引入了 RSA-2048 签名与密钥释放机制。
在核心函数 sub_FFFFFFFF81710483() 中,固件将解密主文件系统所需的 32 字节核心密钥(Key)通过私钥加密,作为一整段 256 字节的 RSA 签名块,强行拼接在加密的 rootfs.gz 文件的最末尾。
2. 放弃 ChaCha20,引入“高度魔改的变体 RC4”
在最终的解密核心函数 sub_FFFFFFFF81710334() 中,飞塔彻底废弃了 ChaCha20 或标准 AES,转而采用了一种高度定制化的、基于经典 RC4 算法演变而来的流密码(Stream Cipher)。
飞塔魔改 RC4 的底层逻辑:
KSA(密钥调度)阶段:标准 RC4 初始化 256 字节 S 盒。但在打乱 S 盒时,利用我们释放出的 32 字节核心 Key 进行
i % 32(即n256_1 & 0x1F)的循环置换。PRGA(密钥流生成)阶段:这是飞塔混淆力度最高的地方。普通的 RC4 只要交换
S[i]和S[j]后直接查表输出。而飞塔在此处引入了复杂的位移干扰与双重 S 盒交叉索引:计算两个非线性的动态索引:
idx1 = (j >> 3) ^ (i << 5),idx2 = (i >> 3) ^ (j << 5)。将两个索引对应在 S 盒中的值相加,并对索引执行
^ 0xAA异或混淆。最终将三个不同的 S 盒查表分量异或组合,生成终极的密钥流字节。
三、获取证书和种子
网络安全设备的固件在引导阶段都有一个原则:签名只要校验失败,设备必须立刻锁死。
我们首先在 IDA 的核心内核镜像中搜索系统停机函数(如 machine_halt() 或 panic())。通过对 machine_halt() 连环查看交叉引用,可以非常轻松地锁定负责固件安全验证的关键核心函数 —— sub_FFFFFFFF81710483()。
点进这个校验函数后,虽然满眼都是未命名变量,但其密码学特征根本藏不住。
往下滚动鼠标,很快就能看到一个经典的结构:一个长达 270 次的 for 循环,并且内部带有 & 0x1F(即循环模 32)的异或操作。而在该循环的正下方,紧跟了一个典型的外部密码学调用:
C
rsa_parse_pub_key(v36, n6291648_1, 270);
这个极其显眼的函数名(或其内部的大数初始化逻辑)瞬间暴露了意图:上面的那个 XOR 循环,就是在即将要被解析的 RSA 公钥
在 IDA 中双击循环里的两个源数据指针:
解密种子:指向
.init.data段的byte_FFFFFFFF8179A2C0。我们在 IDA 视图里直接选中这连续的 32 字节,右键导出成 Hex 数组。混淆证书:指向紧邻的
byte_FFFFFFFF8179A1A0。同样操作,连续向下截取 270 字节的硬编码密文。
四、 逆向过程中的问题与调试轨迹
在还原这段全新加密矩阵的过程中,我们经历了数次经典的密码学对抗与 Bug 调试:
问题 1:误把“要是盒的钥匙”当成最终密钥
在内核数据段中,我们首先注意到了一个硬编码的 32 字节数组 byte_FFFFFFFF8179A2C0:
5C 19 C6 E1 7D C9 77 2B EA 49 B4 A8...
许多人会误以为这就是解密大包的 Key。但通过逆向发现,它其实只是一个 静态异或混淆种子。内核在启动时,用它对另一段 270 字节的硬编码密文进行循环 XOR 异或,其真实目的仅仅是为了在内存中还原出那把 RSA-2048 公钥。
问题2:数学对齐中的隐患
当我们成功截获了文件末尾的 256 字节 RSA 密文,并用还原出的公钥执行标准模幂运算 $m = c^e \pmod n$ 后,解密出了一段 96 字节的有效载荷。
在最初编写解密脚本时,由于 Python 的 pow(c, e, n) 在大端序转换时会在高位自动补 0x00,而 C 语言内核指针在读取缓冲区(v11 + 223)时具有独特的架构对齐,导致我们切片出来的 Key 整体向左错位了 1 个字节(抢跑了 1 字节),解密出来的文件始终是无序的二进制 data。
通过重新推演 PKCS#1 v1.5 的填充边界,我们将切片精确校准到缓冲区的最后 32 字节,终于锁定了真正的核心 RC4 Key:
1EA548D27DF49AABE469A07B1C222506AAC5032544DE5605992F9C8ECAFE2D40
五、 最终的完美破局与自动化解密
当密钥边界完全对齐、飞塔变体 RC4 的索引优先级被我们用 Python 完美重构后,奇迹发生了。
我们计算出的固件主体实际 SHA256 哈希,与从 RSA 签名块中动态释放出的预期哈希实现了 100% 完美的字节比对(SHA256 校验完美通过)。这在数学上宣告了飞塔固件防护矩阵的彻底破产。
完整的自动化解密与还原核心逻辑如下(Python 实现):
import hashlib
from Crypto.PublicKey import RSA
CERT_FILE = "forti_pubkey.der"
ROOTFS_ENCRYPTED_FILE = "rootfs.gz" # 你的原始加密固件文件
ROOTFS_DECRYPTED_FILE = "rootfs_decrypted.gz"
rootfs_tail_hex = "31C6C1E3E33F19F477E38CB15845DFC919C60D0CE33975E34A63815BB8BCB47C4FDCA8AC8F30C64053C00A045FA876F2B3C2D0AA2D82B64842E2C6B43C7A4A6C169577A860171DE17019EE1CE7C89E6F44833B959104E4E0B61525C214EA68AED4557BC902597E15B8C5A38C18217FB0E9F77EE1F3E50CEE4D57166DC6FA358B5E6FAF48B11D26399209D792018729E1AF9040C14026250EF18DA11EBE14A8F9B9D2F002B2772DA82796E755DCFD238DD563611C323175ED4E238B23EB76D6FAEE0A6FB93EA9CC5149A62AB2685B44ECFB43BE3185D3976B0CE58E6BC9A2B01F0D8D2BCA4CD928C9F874E4F6D5A81460257C31C6CE2CC09B49F772878E08BEA0"
def fortnite_custom_rc4_decrypt(ciphertext: bytes, key: bytes) -> bytes:
# KSA 阶段:初始化并打乱 S 盒
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i & 0x1F]) & 0xFF
S[i], S[j] = S[j], S[i]
# PRGA 阶段:生成魔改密钥流并原位解密
plaintext = bytearray(len(ciphertext))
i = 0
j = 0
for idx, ciphertext_byte in enumerate(ciphertext):
i = (i + 1) & 0xFF
old_si = S[i]
j = (j + old_si) & 0xFF
old_sj = S[j]
S[i], S[j] = old_sj, old_si
comp_x = S[(j + old_sj) & 0xFF]
idx1 = ((j >> 3) ^ (i << 5)) & 0xFF
idx2 = ((i >> 3) ^ (j << 5)) & 0xFF
sum_idx = (S[idx1] + S[idx2]) & 0xFF
comp_y = S[(sum_idx ^ 0xAA) & 0xFF]
comp_z = S[(old_sj + old_si) & 0xFF]
keystream_byte = comp_x ^ ((comp_y + comp_z) & 0xFF)
plaintext[idx] = ciphertext_byte ^ keystream_byte
return bytes(plaintext)
print("正在解析 RSA 公钥证书...")
with open(CERT_FILE, "rb") as f:
pub_key = RSA.import_key(f.read())
n = pub_key.n
e = pub_key.e
print(f"成功提取 N (前16位): {hex(n)[:18]}...")
print("正在执行 RSA 模幂运算解密尾部数据...")
c_tail = int.from_bytes(bytes.fromhex(rootfs_tail_hex), byteorder='big')
m_tail_int = pow(c_tail, e, n)
m_tail_bytes = m_tail_int.to_bytes(256, byteorder='big')
expected_sha256 = m_tail_bytes[-96:-64]
real_rc4_key = m_tail_bytes[-32:]
print(f"\n提取到预期 SHA256 : {expected_sha256.hex()}")
print(f"提取到核心 RC4 Key: {real_rc4_key.hex()}")
print("\n正在读取并校验固件主体...")
with open(ROOTFS_ENCRYPTED_FILE, "rb") as f:
full_data = f.read()
encrypted_body = full_data[:-256]
actual_sha256 = hashlib.sha256(encrypted_body).digest()
print(f"主体的实际 SHA256 : {actual_sha256.hex()}")
if actual_sha256 == expected_sha256:
print("SHA256 校验完美通过!边界对齐 100% 正确!")
else:
print("警告:SHA256 校验不匹配。正在检查边界状态...")
print("\n正在激活自定义变体 RC4 引擎解密全盘文件系统...")
decrypted_body = fortnite_custom_rc4_decrypt(encrypted_body, real_rc4_key)
with open(ROOTFS_DECRYPTED_FILE, "wb") as f:
f.write(decrypted_body)
print(f"\n解密全部完成!文件已保存为: {ROOTFS_DECRYPTED_FILE}")脚本全量运行后,新生成的 rootfs_decrypted.gz 返回了解密后的压缩包:
$ file rootfs_decrypted.gz rootfs_decrypted.gz: gzip compressed data, last modified...
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。