FortiGate 飞塔固件自加解密逆向分析
字数 1029 2025-08-30 06:50:27

FortiGate飞塔固件自加解密逆向分析教程

1. 前言与背景

在IoT漏洞挖掘过程中,经常会遇到固件加密的问题,导致在没有硬件设备的情况下无法进行漏洞挖掘。本教程将详细分析FortiGate OS固件的自加密、自解密机制,通过逆向工程方法找到加密位置并实现解密。

2. 准备工作

2.1 环境搭建

  1. 安装必要的工具:
apt-get install qemu
apt install qemu-utils
  1. 加载FortiGate VMDK镜像:
modprobe nbd max_part=16
qemu-nbd --connect=/dev/nbd1 FortiGate7_4_2-disk1.vmdk
mount /dev/nbd1p1 /mnt
  1. 查看启动配置文件:
cat extlinux.conf

3. 固件加载流程分析

3.1 Linux内核rootfs加载机制

Linux内核通过populate_rootfs函数加载initramfs:

static int __init populate_rootfs(void) {
    /* Load the built in initramfs */
    char *err = unpack_to_rootfs(__initramfs_start, __initramfs_size);
    if (err)
        panic("%s", err);
    
    /* If available load the bootloader supplied initrd */
    if (initrd_start && !IS_ENABLED(CONFIG_INITRAMFS_FORCE)) {
        err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start);
        if (!err) {
            free_initrd();
            goto done;
        } else {
            clean_rootfs();
            unpack_to_rootfs(__initramfs_start, __initramfs_size);
        }
    }
    // ...
}

3.2 FortiOS实现分析

FortiOS基于Linux内核4.19.13(从7.4.3版本开始),通过逆向flatkc文件(使用vmlinux-to-elf转换为ELF格式)可以找到相关功能。

逆向后的关键函数:

__int64 populate_rootfs() {
    v0 = _initramfs_size;
    v1 = unpack_to_rootfs(_initramfs_start, _initramfs_size);
    if (v1)
        panic((unsigned int)&aS_18[1], v1, v2, v3, v4, v5);
    
    if (initrd_start) {
        v6 = unpack_to_rootfs(initrd_start, initrd_end - initrd_start);
        if (!v6) {
            free_initrd();
            goto LABEL_10;
        }
        // ...
    }
    LABEL_10:
    flush_delayed_fput();
    load_default_modules();
    return 0;
}

4. 签名验证机制

4.1 fgt_verify_initrd函数分析

该函数负责ramdisk的签名检查和解密:

__int64 fgt_verify_initrd() {
    size = ::initrd_end - initrd_start;
    if (::initrd_end - initrd_start <= 0x100u || (unsigned int)fgt_verifier_open(public_key)) {
        err_code = -1;
        goto LABEL_19;
    }
    
    // 计算SHA-256哈希(不包括最后256字节)
    sha256_init(sha_context);
    sha256_update_0(sha_context, initrd_start, (unsigned int)(size - 256));
    sha256_final_0(sha_context, local_hash_1);
    
    // 读取并验证签名数据
    raw_data = mpi_read_raw_data(::initrd_end - 256LL, 256);
    if (raw_data) {
        if ((int)mpi_cmp_ui(raw_data, 0) < 0 || 
            (int)mpi_cmp(signature_data, public_key[0]) >= 0) {
            err_code = -22;
        } else {
            // RSA签名验证
            mod_pow_result = mpi_alloc(0);
            err_code = mpi_powm(mod_pow_result, signature_data, public_key[1], public_key[0]);
            
            // 检查签名格式(PKCS#1 v1.5)
            verify_result2 = verify_result | temp1 | *(BYTE *)(initrd_end - 256) ^ 1;
            ptr = (BYTE *)(initrd_end - 255);
            do {
                verify_result2 |= (unsigned __int8)~*ptr++;
            } while ((BYTE *)(initrd_end - 53) != ptr);
            
            // 检查哈希匹配
            for (n32 = 0; n32 != 32; ++n32)
                err_code |= (*((BYTE *)(local_hash + n32)) ^ *(BYTE *)(initrd_end + n32 - 33));
        }
    }
    
    ::initrd_end -= 256LL; // 移除签名数据
    fgt_verify_decrypt();  // 解密操作
    return err_code;
}

4.2 签名验证流程

  1. 计算initrd内容的SHA-256哈希(不包括最后256字节)
  2. 读取initrd末尾的256字节签名数据
  3. 使用RSA公钥验证签名
  4. 检查签名格式是否符合PKCS#1 v1.5标准:
    • 第一个字节必须为0x01的取反(0xFE)
    • 接下来的201个字节必须全为0xFF
  5. 验证签名中的哈希值与计算得到的哈希值一致

5. 公钥解密机制

5.1 fgt_verifier_open函数

该函数初始化RSA公钥:

__int64 __fastcall fgt_verifier_open(__int64 *p_public_key) {
    FFFFFFFF814967A8 = (int *)kmem_cache_alloc(qword_FFFFFFFF814967A8, 0x6000C0);
    n_e = fgt_verifier_pub_key(FFFFFFFF814967A8);
    n_e = rsa_parse_pub_key((__int64)v14, FFFFFFFF814967A8_1, 270);
    
    e = mpi_read_raw_data(v14[1], (__int64)v14[9]);  // 读取公钥e
    n = mpi_read_raw_data(v14[0], (__int64)v14[8]);  // 读取公钥n
    
    public_key = {n, e, v11_chunk, v12_chunk};
    return n_e;
}

5.2 fgt_verifier_pub_key函数

从固件中提取加密的公钥并使用ChaCha20解密:

unsigned __int64 __fastcall fgt_verifier_pub_key(int *initrd_start) {
    // 生成32字节密钥
    sha256_init(buf_);
    sha256_update_0((__int64)buf_, &unk_FFFFFFFF817932E3, 29);
    sha256_update_0((__int64)buf_, &EKY, 3);
    sha256_final_0((__int64)buf_, (__int64)key);
    
    // 生成8字节的IV
    sha256_init(buf_);
    sha256_update_0((__int64)buf_, byte_FFFFFFFF817932E1, 31);
    sha256_update_0((__int64)buf_, &EKY, 1);
    sha256_final_0((__int64)buf_, (__int64)iv);
    
    // ChaCha20解密
    crypto_chacha20_init(p_KEY, v6, iv);
    chacha20_docrypt((__int64)p_KEY, initrd_start, initrd_start_, 0x10E);
}

5.3 ChaCha20实现

unsigned __int64 __fastcall chacha20_docrypt(__int64 KEY, int *initrd_start, int *initrd_start_2, __int64 size) {
    if (initrd_start != initrd_start_2)
        memcpy(initrd_start, initrd_start_2, (unsigned int)size);
    
    if (size_1 <= 63) {
        initrd_start_3 = initrd_start;
    } else {
        do {
            chacha20_block((unsigned __int64 *)KEY, (__int64)enc_block);
            initrd_start_4 = initrd_start_1;
            do {
                *((_QWORD *)initrd_start_4 - 1) ^= *(_QWORD *)(enc_block + 8 * v11);
            } while (v11);
            initrd_start_1 += 16;
        } while (initrd_start_1 != initrd_start_3);
    }
    
    if (size_1) {
        chacha20_block((unsigned __int64 *)KEY, (__int64)enc_block);
        _crypto_xor(initrd_start_3, initrd_start_3, enc_block, size_1);
    }
}

6. 解密工具实现

6.1 解密RSA公钥

/* gcc decrypt_rsapubkey.c chacha20.c -o decrypt_rsapubkey -lssl -lcrypto */
#include <stdio.h>
#include <openssl/evp.h>
#include "chacha20.h"

int main(int argc, char **argv) {
    // 初始化
    EVP_MD_CTX *mdctx;
    unsigned char *md1 = NULL;
    unsigned char *md2 = NULL;
    
    // 生成密钥和IV
    sha256_init(buf_);
    sha256_update_0((__int64)buf_, &unk_FFFFFFFF817932E3, 29);
    sha256_update_0((__int64)buf_, &EKY, 3);
    sha256_final_0((__int64)buf_, (__int64)key);
    
    // ChaCha20解密
    struct chacha20_context ctx;
    chacha20_init_context(&ctx, md1, md2);
    chacha20_xor(&ctx, g_RSA_PubKey, 270);
    
    printf("BER-encoded pub key:\n");
    printhex(g_RSA_PubKey, 270);
}

6.2 解密rootfs

/* gcc decrypt_rootfs.c chacha20.c -o decrypt_rootfs -lssl -lcrypto */
#include <stdio.h>
#include <stdlib.h>
#include <openssl/evp.h>
#include "chacha20.h"

int main(int argc, char **argv) {
    // 读取输入文件
    FILE *f = fopen(argv[1], "rb");
    fseek(f, 0, SEEK_END);
    long fsize = ftell(f);
    fseek(f, 0, SEEK_SET);
    fsize -= 256; // 跳过尾部签名
    
    // 解密
    struct chacha20_context ctx;
    chacha20_init_context(&ctx, md1, md2);
    chacha20_xor(&ctx, data, fsize);
    
    // 检查是否为GZ文件
    uint16_t magic = *(int16_t *)data;
    if (magic != 0x8B1F) {
        fprintf(stderr, "Failed to decrypt (not a GZ, magic=%X)\n", magic);
        return 1;
    }
    
    // 写入输出文件
    FILE *f_out = fopen(argv[2], "wb");
    fwrite(data, fsize, 1, f_out);
    fclose(f_out);
}

7. 自动化提取密钥

使用Python脚本自动提取固件中的密钥:

from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.core.locationdb import LocationDB

def get_rootfs_key(target_binary):
    fdesc = open(target_binary, 'rb')
    loc_db = LocationDB()
    cont = Container.from_stream(fdesc, loc_db)
    machine = Machine(cont.arch)
    
    # 根据架构设置寄存器
    if cont.arch == "x86_64":
        ret_val_reg = machine.mn.regs.RAX
        arg_val_reg = machine.mn.regs.RSI
    elif cont.arch == "aarch64l":
        ret_val_reg = machine.mn.regs.X0
        arg_val_reg = machine.mn.regs.X1
    
    # 反汇编分析
    mdis = machine.dis_engine(cont.bin_stream, loc_db=cont.loc_db)
    addr = loc_db.get_name_offset("fgt_verifier_pub_key")
    asmcfg = mdis.dis_multiblock(addr)
    
    # 符号执行提取密钥
    lifter = machine.lifter_model_call(mdis.loc_db)
    ircfg = lifter.new_ircfg_from_asmcfg(asmcfg)
    symb = SymbolicExecutionEngine(lifter)
    
    all_seeds = []
    while True:
        irblock = ircfg.get_block(addr)
        if irblock is None:
            break
        
        addr = symb.eval_updt_irblock(irblock, step=False)
        if ret_val_reg in symb.symbols.symbols_id:
            reg_expr = symb.symbols.symbols_id[ret_val_reg]
            if reg_expr.is_function_call():
                target = reg_expr.args[0]
                target_func = loc_db.get_offset_location(target.arg)
                target_func = list(loc_db.get_location_names(target_func))[0]
                if target_func == "sha256_update":
                    all_seeds.append(symb.symbols.symbols_id[arg_val_reg].arg)
    
    seed_addr = min(all_seeds)
    seed_data = cont.executable.get_virt().get(seed_addr, seed_addr + 32)
    return binascii.hexlify(seed_data).upper()

8. 总结

  1. 固件解密流程

    • 通过逆向分析找到固件自解密机制
    • 提取并解密RSA公钥
    • 绕过签名验证
    • 解密rootfs内容
  2. 关键技术点

    • FortiOS使用修改的Linux内核加载机制
    • 采用PKCS#1 v1.5格式的RSA签名验证
    • 使用ChaCha20算法加密关键数据
    • 通过SHA-256派生密钥和IV
  3. 应用场景

    • 无硬件设备时的固件漏洞挖掘
    • 固件修改与重新打包
    • 安全研究与分析

这种方法不仅适用于FortiGate固件,也可应用于其他采用类似自加密机制的IoT设备固件分析。

FortiGate飞塔固件自加解密逆向分析教程 1. 前言与背景 在IoT漏洞挖掘过程中,经常会遇到固件加密的问题,导致在没有硬件设备的情况下无法进行漏洞挖掘。本教程将详细分析FortiGate OS固件的自加密、自解密机制,通过逆向工程方法找到加密位置并实现解密。 2. 准备工作 2.1 环境搭建 安装必要的工具: 加载FortiGate VMDK镜像: 查看启动配置文件: 3. 固件加载流程分析 3.1 Linux内核rootfs加载机制 Linux内核通过 populate_rootfs 函数加载initramfs: 3.2 FortiOS实现分析 FortiOS基于Linux内核4.19.13(从7.4.3版本开始),通过逆向 flatkc 文件(使用vmlinux-to-elf转换为ELF格式)可以找到相关功能。 逆向后的关键函数: 4. 签名验证机制 4.1 fgt_ verify_ initrd函数分析 该函数负责ramdisk的签名检查和解密: 4.2 签名验证流程 计算initrd内容的SHA-256哈希(不包括最后256字节) 读取initrd末尾的256字节签名数据 使用RSA公钥验证签名 检查签名格式是否符合PKCS#1 v1.5标准: 第一个字节必须为0x01的取反(0xFE) 接下来的201个字节必须全为0xFF 验证签名中的哈希值与计算得到的哈希值一致 5. 公钥解密机制 5.1 fgt_ verifier_ open函数 该函数初始化RSA公钥: 5.2 fgt_ verifier_ pub_ key函数 从固件中提取加密的公钥并使用ChaCha20解密: 5.3 ChaCha20实现 6. 解密工具实现 6.1 解密RSA公钥 6.2 解密rootfs 7. 自动化提取密钥 使用Python脚本自动提取固件中的密钥: 8. 总结 固件解密流程 : 通过逆向分析找到固件自解密机制 提取并解密RSA公钥 绕过签名验证 解密rootfs内容 关键技术点 : FortiOS使用修改的Linux内核加载机制 采用PKCS#1 v1.5格式的RSA签名验证 使用ChaCha20算法加密关键数据 通过SHA-256派生密钥和IV 应用场景 : 无硬件设备时的固件漏洞挖掘 固件修改与重新打包 安全研究与分析 这种方法不仅适用于FortiGate固件,也可应用于其他采用类似自加密机制的IoT设备固件分析。