首页
社区
课程
招聘
[原创]Forti8.0固件解包
发表于: 2天前 1049

[原创]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)的静态内存中。研究人员可以通过 liefobjdump 轻松提取 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内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回