问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
FortiGate 飞塔固件自加解密逆向分析
漏洞分析
前言:在进行IOT漏洞挖掘中,我们常常遇到固件加密问题导致在没有硬件设备时无法进行漏洞挖掘,本篇主要来进行分析固件解密的一些思路,因为作者认为在很多固件当中大部分加密都会在它固件里进行自加密、自解密,所以只要找到它的加密的位置进行逆向分析,就可以去进行解密,根据项目需求,这里分析一波飞塔解密的流程,通过这个流程更加深入的去理解这个加解密的思想,废话不多说,下面进行工作分析
FortiGate OS 加解密分析 ================== 前言:在进行IOT漏洞挖掘中,我们常常遇到固件加密问题导致在没有硬件设备时无法进行漏洞挖掘,本篇主要来进行分析固件解密的一些思路,因为作者认为在很多固件当中大部分加密都会在它固件里进行自加密、自解密,所以只要找到它的加密的位置进行逆向分析,就可以去进行解密,根据项目需求,这里分析一波飞塔解密的流程,通过这个流程更加深入的去理解这个加解密的思想,废话不多说,下面进行工作分析 逆向分析 ---- 挂载fortigate vmdk ```php apt-get install qemu apt install qemu-utils modprobe nbd max\_part\=16 qemu-nbd \--connect\=/dev/nbd1 FortiGate7\_4\_2-disk1.vmdk mount /dev/nbd1p1 /mnt ``` 查看extlinux.conf 启动配置文件 ```php cat extlinux.conf DISPLAY boot.msg TIMEOUT 10 TOTALTIMEOUT 9000 DEFAULT flatkc ro panic\=5 endbase\=0xA0000 console\=ttyS0, root\=/dev/ram0 ramdisk\_size\=65536 initrd\=/rootfs.gz ``` 拷贝rootfs.gz和flatkc 使用enc 命令查看rootfs.gz  通过通读Linux kernel源码 populate\_rootfs 函数如下 ```c 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); /\* Failed to decompress INTERNAL initramfs \*/ /\* If available load the bootloader supplied initrd \*/ if (initrd\_start && !IS\_ENABLED(CONFIG\_INITRAMFS\_FORCE)) { #ifdef CONFIG\_BLK\_DEV\_RAM int fd; printk(KERN\_INFO "Trying to unpack rootfs image as initramfs...\\n"); err \= unpack\_to\_rootfs((char \*)initrd\_start, initrd\_end \- initrd\_start); if (!err) { free\_initrd(); goto done; } else { clean\_rootfs(); #清理clean\_rootfs unpack\_to\_rootfs(\_\_initramfs\_start, \_\_initramfs\_size); #使用start和size进行解包 } printk(KERN\_INFO "rootfs image is not initramfs (%s)" "; looks like an initrd\\n", err); fd \= ksys\_open("/initrd.image", O\_WRONLY|O\_CREAT, 0700); if (fd >\= 0) { ssize\_t written \= xwrite(fd, (char \*)initrd\_start, initrd\_end \- initrd\_start); if (written !\= initrd\_end \- initrd\_start) pr\_err("/initrd.image: incomplete write (%zd != %ld)\\n", written, initrd\_end \- initrd\_start); ksys\_close(fd); free\_initrd(); } done: /\* empty statement \*/; #else printk(KERN\_INFO "Unpacking initramfs...\\n"); err \= unpack\_to\_rootfs((char \*)initrd\_start, initrd\_end \- initrd\_start); if (err) printk(KERN\_EMERG "Initramfs unpacking failed: %s\\n", err); free\_initrd(); #endif } flush\_delayed\_fput(); /\* \* Try loading default modules from initramfs. This gives \* us a chance to load before device\_initcalls. \*/ load\_default\_modules(); #挂载rootfs return 0; } ``` 通过分析源代码,主要是通过populate\_rootfs 函数里的全局变量**initramfs\_start和**initramfs\_end来进行处理,解压缩 ramdisk 的 GZIP 压缩的 CPIO 映像并挂载 *rootfs* FortiOS 是基于 Linux 的,并使用了修改后的内核 4.19.13(从 7.4.3 版本开始),因此我们可以反汇编 flatkc(使用 vmlinux-to-elf 转换为 ELF 格式[**后\[8\])**](https://github.com/marin-m/vmlinux-to-elf)并寻找上述功能: 以下是反编译出来的伪代码 ```c \_\_int64 populate\_rootfs() { int v0; // esi \_\_int64 v1; // rax int v2; // edx int v3; // ecx int v4; // r8d int v5; // r9d \_\_int64 v6; // r12 int v7; // edx int v8; // ecx int v9; // r8d int v10; // r9d int v11; // eax unsigned int v12; // ebx \_\_int64 v13; // rax int v14; // ecx int v15; // r8d int v16; // r9d v0 \= (int)off\_FFFFFFFF817DA3C8; v1 \= unpack\_to\_rootfs(a070701000002d1, off\_FFFFFFFF817DA3C8);// "070701000002D1000041ED0000000000000000000000026580E65900000000000000030000000100000000000000000000000400000000dev" if ( v1 ) panic((unsigned int)&aS\_18\[1\], v1, v2, v3, v4, v5);// "\\t%s" if ( p\_\_070701000002D1000041ED0000000000000000000000026580E659000000 ) { printk((unsigned int)&unk\_FFFFFFFF813CE3D0, v0, v2, v3, v4, v5); v6 \= unpack\_to\_rootfs( p\_\_070701000002D1000041ED0000000000000000000000026580E659000000, qword\_FFFFFFFF81838078 \- p\_\_070701000002D1000041ED0000000000000000000000026580E659000000); if ( !v6 ) { LABEL\_9: free\_initrd(); goto LABEL\_10; } clean\_rootfs(); unpack\_to\_rootfs(a070701000002d1, off\_FFFFFFFF817DA3C8);// "070701000002D1000041ED0000000000000000000000026580E65900000000000000030000000100000000000000000000000400000000dev" printk((unsigned int)&unk\_FFFFFFFF813CE408, v6, v7, v8, v9, v10); v11 \= do\_sys\_open(4294967196LL, aInitrdImage, 32833, 448);// "/initrd.image" v12 \= v11; if ( v11 >\= 0 ) { v13 \= xwrite( (unsigned int)v11, p\_\_070701000002D1000041ED0000000000000000000000026580E659000000, qword\_FFFFFFFF81838078 \- p\_\_070701000002D1000041ED0000000000000000000000026580E659000000); if ( qword\_FFFFFFFF81838078 \- p\_\_070701000002D1000041ED0000000000000000000000026580E659000000 !\= v13 ) printk( (unsigned int)&unk\_FFFFFFFF813CE448, v13, qword\_FFFFFFFF81838078 \- p\_\_070701000002D1000041ED0000000000000000000000026580E659000000, v14, v15, v16); \_close\_fd(\*(\_QWORD \*)(\_\_readgsqword((unsigned int)&off\_14D80) + 1576), v12); goto LABEL\_9; } } LABEL\_10: flush\_delayed\_fput(); load\_default\_modules(); return 0; } ``` 我们对照源码将以上伪代码进行修正命名,修正后的代码如下 ```c \_\_int64 populate\_rootfs() { v0 \= \_initramfs\_size; \_070701000002D1000041ED0000000000000000000000026580E65900000000 \= unpack\_to\_rootfs(\_initramfs\_start, \_initramfs\_size);// "070701000002D1000041ED0000000000000000000000026580E65900000000000000030000000100000000000000000000000400000000dev" if ( \_070701000002D1000041ED0000000000000000000000026580E65900000000 ) panic((unsigned int)&aS\_18\[1\], \_070701000002D1000041ED0000000000000000000000026580E65900000000, v2, v3, v4, v5);// "\\t%s" if ( initrd\_start ) { printk((unsigned int)&unk\_FFFFFFFF813CE3D0, v0, v2, v3, v4, v5); v6 \= unpack\_to\_rootfs(initrd\_start, initrd\_end \- initrd\_start);// 解包通过initrd\_start,initrd\_end-initrd\_start 来进行解包 if ( !v6 ) { LABEL\_9: free\_initrd(); goto LABEL\_10; } clean\_rootfs(); unpack\_to\_rootfs(\_initramfs\_start, \_initramfs\_size);// "070701000002D1000041ED0000000000000000000000026580E65900000000000000030000000100000000000000000000000400000000dev" printk((unsigned int)&KERN\_INFO, v6, v7, v8, v9, v10); \_\_initrd.image\_ \= do\_sys\_open(4294967196LL, aInitrdImage, 32833, 448);// "/initrd.image" \_\_initrd.image\_\_1 \= \_\_initrd.image\_; if ( \_\_initrd.image\_ >\= 0 ) { v13 \= xwrite((unsigned int)\_\_initrd.image\_, initrd\_start, initrd\_end \- initrd\_start); if ( initrd\_end \- initrd\_start !\= v13 ) printk((unsigned int)&unk\_FFFFFFFF813CE448, v13, initrd\_end \- initrd\_start, v14, v15, v16); \_close\_fd(\*(\_QWORD \*)(\_\_readgsqword((unsigned int)&off\_14D80) + 1576), \_\_initrd.image\_\_1); goto LABEL\_9; } } LABEL\_10: flush\_delayed\_fput(); load\_default\_modules(); // 加载rootfs包 return 0; } ``` 现在,已经分析出来了大概的rootfs的加载流程,在加载rootfs包的时候都是从initrd\_start和initrd\_end 的全局变量来加载,从头逆向不太可能,这里交叉引用一下,  看到了两个可疑的函数分别是fgt\_verify\_decrypt和fgt\_verify\_initrd, 通过它的名字可以得知一个是解密和初始化的函数】,针对这两个函数进行逆向分析下 ### **fgt\_verify\_initrd 签名检测** 此函数负责 ramdisk 签名检查和解密: ```c \_\_int64 fgt\_verify\_initrd() { int size; // ebx unsigned int err\_code; // r12d \_\_int64 initrd\_end; // rbx \_\_int64 last\_256b1; // r14 \_\_int64 raw\_data; // rax \_\_int64 signature\_data; // r13 \_\_int64 mod\_pow\_result; // rax \_\_int64 mod\_pow\_result2; // r15 int verify\_result; // eax int temp1; // r12d int verify\_result2; // eax \_BYTE \*ptr; // rcx \_\_int64 n19; // rax \_\_int64 n32; // rax \_\_int64 local\_hash; // \[rsp+0h\] \[rbp-D0h\] int temp; // \[rsp+Ch\] \[rbp-C4h\] BYREF \_QWORD public\_key\[3\]; // \[rsp+10h\] \[rbp-C0h\] BYREF \_\_int64 last\_256b2; // \[rsp+28h\] \[rbp-A8h\] \_\_int64 local\_hash\_1; // \[rsp+30h\] \[rbp-A0h\] \_BYTE sha\_context\[104\]; // \[rsp+38h\] \[rbp-98h\] BYREF unsigned \_\_int64 v21; // \[rsp+A0h\] \[rbp-30h\] v21 \= \_\_readgsqword(0x28u); size \= ::initrd\_end \- initrd\_start; if ( ::initrd\_end \- initrd\_start <= 0x100u || (unsigned int)fgt\_verifier\_open(public\_key) )// 签名检查 { err\_code \= \-1; LABEL\_19: machine\_halt(); // 验证失败,停机处理 goto LABEL\_20; } sha256\_init(sha\_context); // 初始化SHA-256上下文并计算initrd内容的哈希(不包括最后256字节) sha256\_update\_0(sha\_context, initrd\_start, (unsigned int)(size \- 256)); sha256\_final\_0(sha\_context, local\_hash\_1); initrd\_end \= ::initrd\_end; last\_256b1 \= ::initrd\_end \- 256LL; // 不包括最后256个字节 last\_256b2 \= ::initrd\_end \- 256LL; // 不包括最后256个字节 local\_hash \= local\_hash\_1; raw\_data \= mpi\_read\_raw\_data(::initrd\_end \- 256LL, 256);// // 读取initrd末尾的256字节签名数据 signature\_data \= raw\_data; err\_code \= \-12; if ( raw\_data ) { if ( (int)mpi\_cmp\_ui(raw\_data, 0) < 0 || (int)mpi\_cmp(signature\_data, public\_key\[0\]) \>= 0 )// / 检查签名数据有效性:必须为正数且小于RSA模数 { err\_code \= \-22; } else { mod\_pow\_result \= mpi\_alloc(0); // // 分配内存用于存储模幂运算结果 mod\_pow\_result2 \= mod\_pow\_result; err\_code \= \-12; if ( mod\_pow\_result ) { err\_code \= mpi\_powm(mod\_pow\_result, signature\_data, public\_key\[1\], public\_key\[0\]);// // 执行RSA签名验证:s^e mod n if ( !err\_code ) { verify\_result \= mpi\_read\_buffer(mod\_pow\_result2, last\_256b2, 256, &temp, 0);// // 将验证结果读取到缓冲区 temp1 \= temp; LOBYTE(temp1) \= ~(\_BYTE)temp; // // 检查签名格式:第一个字节必须为0x01的取反 verify\_result2 \= verify\_result | temp1 | \*(\_BYTE \*)(initrd\_end \- 256) ^ 1; ptr \= (\_BYTE \*)(initrd\_end \- 255); do verify\_result2 |= (unsigned \_\_int8)~\*ptr++;// // 检查接下来的201个字节是否全为0xFF while ( (\_BYTE \*)(initrd\_end \- 53) != ptr ); err\_code \= verify\_result2 | \*(unsigned \_\_int8 \*)(last\_256b1 + 203); for ( n19 \= 0; n19 != 19; ++n19 ) // 检查前19个字节是否与预设值匹配 err\_code |= (unsigned \_\_int8)(byte\_FFFFFFFF817931A0\[n19\] ^ \*(\_BYTE \*)(initrd\_end + n19 \- 52)); for ( n32 \= 0; n32 != 32; ++n32 ) // 对比sha256计算的hash结果是否符合预期 err\_code |= (unsigned \_\_int8)(\*(\_BYTE \*)(local\_hash + n32) ^ \*(\_BYTE \*)(initrd\_end + n32 \- 33)); } mpi\_free(mod\_pow\_result2); } } mpi\_free(signature\_data); } fgt\_verifier\_close(public\_key); // // 关闭验证器并释放公钥资源 if ( err\_code ) // 如果err\_code非0验证失败,进入错误处理 goto LABEL\_19; LABEL\_20: ::initrd\_end \-= 256LL; // 验证成功,调整initrd结束位置(移除签名数据) fgt\_verify\_decrypt(); // 解密操作 return err\_code; } ``` 这里笔者在逆向的时候 第一次接触签名问题,所以有点问题,这里借助大模型的时候分析出来了疑问点 核心逻辑是对签名数据的第一个字节进行 按位取反后与 1 比较。这一设计源自 PKCS#1 v1.5 签名格式规范: 在 RSA 签名中,标准格式要求签名数据以 0x00 0x01 padding 0x00 data 开头,其中 padding 由全 0xFF 字节组成。这里的第一个字节实际是签名解码后的第一个有效字节,而 0x01 的取反(0xFE)是对签名格式的校验 —— 当签名正确时,解码后的第一个字节应为 0x01,取反后为 0xFE,与实际数据比对可验证格式合法性。 这个函数实现了一个完整的签名验证过程,主要包括: 1. **哈希验证**:对 initrd 的主要内容计算 SHA-256 哈希 2. **RSA 签名验证**:使用公钥解密签名数据,并检查其有效性 3. **格式验证**:检查解密后的签名数据是否符合 PKCS#1 v1.5 格式 4. **内容验证**:确保签名中的哈希值与计算得到的哈希值一致 **关键步骤解析**: 1. **起始地址**:`initrd_end - 255` 指向签名数据的第二个字节(第一个字节已验证为 0x01); 2. **循环范围**:从 `initrd_end - 255` 到 `initrd_end - 53`,共 201 个字节(255 - 53 + 1 = 201); 3. **验证逻辑**: - 对每个字节执行 `~*ptr`(按位取反); - 若字节为 0xFF,则 `~0xFF = 0x00`,`verify_result2` 保持不变; - 若存在非 0xFF 字节,`~x ≠ 0`,`verify_result2` 被置为非零值,验证失败。 ### **函数功能概述** `fgt_verifier_open`函数的主要功能是从 initrd 中提取 RSA 公钥并进行初始化,为公钥验证做准备。它完成以下工作: 1. 初始化内存区域和公钥结构 2. 从 initrd 加载公钥数据 3. 解析公钥数据,提取模数 (n) 和指数 (e) 4. 分配内存存储公钥组件 5. 返回操作结果状态码 逆向分析fgt\_verifier\_open ----------------------- ### **1. `fgt_verifier_open()` 函数解析** 此函数的核心功能是初始化 RSA 公钥,为后续的签名验证工作做好准备。其执行流程可分为以下几个关键步骤: ```c \_\_int64 \_\_fastcall fgt\_verifier\_open(\_\_int64 \*p\_public\_key) { unsigned \_\_int8 \*\*\_\_\_\_v2\_32\_\_\_\_\_\_0; // rdi \_\_int64 n32; // rcx \_\_int64 n10; // rcx \_\_int64 \*p\_public\_key\_1; // rdi int \*\_\_\_\_\_\_\_\_FFFFFFFF814967A8\_\_\_\_\_\_\_\_\_; // rax unsigned int \_\_\_\_n\_e; // r14d \_\_int64 \_\_\_\_\_\_\_\_FFFFFFFF814967A8\_\_\_\_\_\_\_\_\_\_1; // r13 \_\_int64 \_\_\_\_\_e; // rax \_\_int64 \_\_\_\_\_n; // rax \_\_int64 \_\_\_\_; // rax \_\_int64 public\_key\_\_n\_e\_v11\_chunk\_v12\_chunk\_; // rax unsigned \_\_int8 \*v14\[20\]; // \[rsp+0h\] \[rbp-A0h\] BYREF v14\[16\] \= (unsigned \_\_int8 \*)\_\_readgsqword(0x28u); \_\_\_\_v2\_32\_\_\_\_\_\_0 \= v14; for ( n32 \= 32; n32; \--n32 ) { \*(\_DWORD \*)\_\_\_\_v2\_32\_\_\_\_\_\_0 \= 0; // 初始化 v2的32个长度数组为0 \_\_\_\_v2\_32\_\_\_\_\_\_0 \= (unsigned \_\_int8 \*\*)((char \*)\_\_\_\_v2\_32\_\_\_\_\_\_0 + 4); } n10 \= 10; p\_public\_key\_1 \= p\_public\_key; while ( n10 ) // 初始化 p\_public\_key\_1的10个长度数组为0 { \*(\_DWORD \*)p\_public\_key\_1 \= 0; p\_public\_key\_1 \= (\_\_int64 \*)((char \*)p\_public\_key\_1 + 4); \--n10; } \_\_\_\_\_\_\_\_FFFFFFFF814967A8\_\_\_\_\_\_\_\_\_ \= (int \*)kmem\_cache\_alloc(qword\_FFFFFFFF814967A8, 0x6000C0);// 分配内存的位置为FFFFFFFF814967A8 猜测是固件的内存地址 \_\_\_\_n\_e \= \-12; if ( \_\_\_\_\_\_\_\_FFFFFFFF814967A8\_\_\_\_\_\_\_\_\_ ) { \_\_\_\_\_\_\_\_FFFFFFFF814967A8\_\_\_\_\_\_\_\_\_\_1 \= (\_\_int64)\_\_\_\_\_\_\_\_FFFFFFFF814967A8\_\_\_\_\_\_\_\_\_; fgt\_verifier\_pub\_key(\_\_\_\_\_\_\_\_FFFFFFFF814967A8\_\_\_\_\_\_\_\_\_);// 从固件中提取密钥 \_\_\_\_n\_e \= rsa\_parse\_pub\_key((\_\_int64)v14, \_\_\_\_\_\_\_\_FFFFFFFF814967A8\_\_\_\_\_\_\_\_\_\_1, 270);// 解析公钥n和e if ( !\_\_\_\_n\_e ) { \_\_\_\_\_e \= mpi\_read\_raw\_data(v14\[1\], (\_\_int64)v14\[9\]);// 读取公钥 e p\_public\_key\[1\] \= \_\_\_\_\_e; if ( \_\_\_\_\_e ) { \_\_\_\_\_n \= mpi\_read\_raw\_data(v14\[0\], (\_\_int64)v14\[8\]);// 读取公钥 n \*p\_public\_key \= \_\_\_\_\_n; if ( \_\_\_\_\_n ) { \_\_\_\_ \= kmem\_cache\_alloc(qword\_FFFFFFFF814967C0, 0x6000C0);// 分配内存 p\_public\_key\[2\] \= \_\_\_\_; if ( \_\_\_\_ ) { public\_key\_\_n\_e\_v11\_chunk\_v12\_chunk\_ \= kmem\_cache\_alloc(qword\_FFFFFFFF81496788, 0x6000C0); p\_public\_key\[4\] \= public\_key\_\_n\_e\_v11\_chunk\_v12\_chunk\_;// public\_key={n,e,v11\_chunk,v12\_chunk} if ( public\_key\_\_n\_e\_v11\_chunk\_v12\_chunk\_ ) { kfree(\_\_\_\_\_\_\_\_FFFFFFFF814967A8\_\_\_\_\_\_\_\_\_\_1); return \_\_\_\_n\_e; } } } } \_\_\_\_n\_e \= \-12; } kfree(\_\_\_\_\_\_\_\_FFFFFFFF814967A8\_\_\_\_\_\_\_\_\_\_1); fgt\_verifier\_close(p\_public\_key); } return \_\_\_\_n\_e; } ``` **关键操作解析**: - **内存初始化**:先将工作数组 `v14` 和输出数组 `p_public_key` 全部置零,以此消除潜在的残留数据,保证后续操作的安全性。 - **公钥获取**:调用 `fgt_verifier_pub_key()` 函数,从固件中提取加密的公钥并进行解密。 - **公钥解析**:借助 `rsa_parse_pub_key()` 函数解析公钥数据,获取 RSA 所需的模数 `n` 和指数 `e`。 - **内存分配**:为 RSA 运算分配必要的临时内存空间,用于存储中间结果和其他辅助数据。 ### **2. `fgt_verifier_pub_key()` 函数解析** 该函数的主要作用是从固件中提取加密的公钥,并使用 ChaCha20 算法对其进行解密。 ```c unsigned \_\_int64 \_\_fastcall fgt\_verifier\_pub\_key(int \*initrd\_start) { \_DWORD \*v2; // rdi \_\_int64 n8; // rcx \_DWORD \*key\_1; // rsi \_DWORD v6\[8\]; // \[rsp+0h\] \[rbp-120h\] BYREF \_DWORD p\_KEY\[16\]; // \[rsp+20h\] \[rbp-100h\] BYREF \_QWORD buf\_\[13\]; // \[rsp+60h\] \[rbp-C0h\] BYREF \_BYTE key\[32\]; // \[rsp+C8h\] \[rbp-58h\] BYREF \_DWORD iv\[8\]; // \[rsp+E8h\] \[rbp-38h\] BYREF unsigned \_\_int64 v11; // \[rsp+108h\] \[rbp-18h\] v11 \= \_\_readgsqword(0x28u); 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); // 生成32字节密钥 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); // 生成8字节的iv v2 \= v6; n8 \= 8; key\_1 \= key; while ( n8 ) // 将 8byte key赋值给v2 { \*v2++ \= \*key\_1++; \--n8; } crypto\_chacha20\_init(p\_KEY, v6, iv); // 用生成的iv来进行初始化chacha20 p\_KEY 这里的v6不知道哪来的 猜测可能被ida优化了,其实就是sha256的密钥 chacha20\_docrypt((\_\_int64)p\_KEY, initrd\_start, initrd\_start\_, 0x10E);// 根据传入进来的p\_KEY、initrd\_start 动态申请获取的内存地址和initrd\_start\_固定的地址内容以及size 进行chacha20解密 return v11 \- \_\_readgsqword(0x28u); } ``` **加密设计解析**: - **密钥派生**:以固件中的常量数据和一个可能的嵌入式密钥标识符 `EKY` 作为输入,通过 SHA - 256 哈希算法生成 ChaCha20 加密所需的密钥和初始化向量(IV)。 - **双哈希机制**:使用不同的常量数据和 `EKY` 的不同长度,分别生成密钥和 IV,增强了加密的安全性。 - **ChaCha20 应用**:采用流密码 ChaCha20 对存储在固件中的公钥进行解密,这种算法在资源受限的环境中表现出高效的性能。 ### **3. `chacha20_docrypt()` 函数解析** 此函数实现了 ChaCha20 流密码的加密 / 解密操作,其采用的是分组处理的方式。 ```c unsigned \_\_int64 \_\_fastcall chacha20\_docrypt(\_\_int64 KEY, int \*initrd\_start, int \*initrd\_start\_2, \_\_int64 size) { int \*initrd\_start\_1; // r14 unsigned int size\_1; // r13d int \*initrd\_start\_3; // r12 int \*initrd\_start\_4; // rdx unsigned \_\_int64 \*\_\_\_\_\_\_\_\_\_; // rax \_QWORD enc\_block\[8\]; // \[rsp+0h\] \[rbp-70h\] BYREF unsigned \_\_int64 v11; // \[rsp+40h\] \[rbp-30h\] BYREF initrd\_start\_1 = initrd\_start; size\_1 = size; v11 = \_\_readgsqword(0x28u); if ( initrd\_start != initrd\_start\_2 ) // 首先判断动态加载的内存地址的内容和固定的常量是否一样 用来检测是否被篡改,如果被篡改,就恢复如初 memcpy(initrd\_start, initrd\_start\_2, (unsigned int)size); if ( size\_1 <= 63 ) // 判断大小是否小于等于63 { initrd\_start\_3 = initrd\_start; } else { initrd\_start\_3 = &initrd\_start\[0x10 \* (unsigned \_\_int64)(((size\_1 - 0x40) >> 6) + 1)\];// 这里提取固定地址的内容 方便后面进行判断 do { chacha20\_block((unsigned \_\_int64 \*)KEY, (\_\_int64)enc\_block);// 通过密钥加密块 initrd\_start\_4 = initrd\_start\_1; \_\_\_\_\_\_\_\_\_ = enc\_block; do { ++\_\_\_\_\_\_\_\_\_; // 将块的地址值自增1 initrd\_start\_4 += 2; // 在initrd\_start4向后读取2个地址的位置 \*((\_QWORD \*)initrd\_start\_4 - 1) ^= \*(\_\_\_\_\_\_\_\_\_ - 1);// 将第2个字节与加密块进行异或 } while ( \_\_\_\_\_\_\_\_\_ != &v11 ); // 直到加密块都异或完 initrd\_start\_1 += 16; // 每加密一论数据后进行init\_addr+16 再次进行加密,每64字节一块 } while ( initrd\_start\_1 != initrd\_start\_3 ); size\_1 &= 0x3Fu; } if ( size\_1 ) // 处理剩余数据 { chacha20\_block((unsigned \_\_int64 \*)KEY, (\_\_int64)enc\_block);// 生成最后一个密钥流块 \_crypto\_xor(initrd\_start\_3, initrd\_start\_3, enc\_block, size\_1);// \_crypto\_xor 将密钥流 enc\_block 与数据 initrd\_start\_3 异或,结果存入 initrd\_start\_3。 } return v11 - \_\_readgsqword(0x28u); } ``` **ChaCha20 核心机制**: - **分块处理**:将数据按每 64 字节分成一个块进行处理,每个块使用不同的计数器值生成唯一的密钥流。 - **密钥流生成**:通过 `chacha20_block()` 函数生成 64 字节的密钥流,该密钥流是由密钥、IV 和块计数器共同决定的。 - **异或操作**:明文与密钥流进行异或运算,得到密文;解密时执行相同的异或操作即可还原明文。 ### **`_crypto_xor` 函数解析:按字节异或加密 / 解密实现** ### **核心功能与逻辑架构** 该函数实现了 **内存块之间的按位异或操作**,常用于流密码的加解密(如 ChaCha20、Salsa20)。其核心逻辑是将三个内存块 `a1`、`a2`、`a3` 中对应位置的字节进行异或运算,并将结果存储到 `a1` 中。 ### 实现自解密算法 整体下来上面是解密公钥的算法,通过手动提取sha256的KEY和chacha250的block 解密公钥算法,来绕过下面的签名检测,这里我们根据这个算法来进行手动实现下,这里给出一个github博主争对这解密的算法实现(有时间再去分析这个算法的写法) [https://github.com/noways-io/fortigate-crypto/blob/main/decrypt\_rsapubkey.c](https://github.com/noways-io/fortigate-crypto/blob/main/decrypt_rsapubkey.c) **decrypt\_rsapubkey.c:** ```php /\* gcc decrypt\_rsapubkey.c chacha20.c -o decrypt\_rsapubkey -lssl -lcrypto \*/ #include <stdio.h> #include <openssl/evp.h> #include "chacha20.h" void printhex(unsigned char\* data, int len) { int i = 0; for(; i < len; i++) { printf("%02X", data\[i\]); } } int main(int argc, char \*\*argv) { if(argc < 3) { fprintf(stderr, "Usage: %s <KEY\_HEXA> <ENC\_RSAPUBKEY\_HEXA>\\n", argv\[0\]); return 1; } if (strlen(argv\[1\]) != 64) { fprintf(stderr, "Key must be 64 (32-bytes) hexa chars string\\n"); return 1; } if (strlen(argv\[2\]) != 540) { fprintf(stderr, "RSA pubkey must be 540 (270 bytes) hexa chars string\\n"); return 1; } char g\_FirmwareSeed\[32\] = {0}; char g\_RSA\_PubKey\[270\] = {0}; char \*pos = argv\[1\]; for (size\_t i = 0; i < sizeof g\_FirmwareSeed; i++) { sscanf(pos, "%2hhx", &g\_FirmwareSeed\[i\]); pos += 2; } pos = argv\[2\]; for (size\_t i = 0; i < sizeof g\_RSA\_PubKey; i++) { sscanf(pos, "%2hhx", &g\_RSA\_PubKey\[i\]); pos += 2; } EVP\_MD\_CTX \*mdctx; unsigned char \*md1 = NULL; unsigned char \*md2 = NULL; if((mdctx = EVP\_MD\_CTX\_new()) == NULL) return 1; if(EVP\_DigestInit\_ex(mdctx, EVP\_sha256(), NULL) != 1) return 1; if(EVP\_DigestUpdate(mdctx, (unsigned char\*)g\_FirmwareSeed + 3, 29) != 1) return 1; if(EVP\_DigestUpdate(mdctx, (unsigned char\*)g\_FirmwareSeed, 3) != 1) return 1; if((md1 = (unsigned char \*)OPENSSL\_malloc(EVP\_MD\_size(EVP\_sha256()))) == NULL) return 1; if(EVP\_DigestFinal\_ex(mdctx, md1, NULL) != 1) return 1; EVP\_MD\_CTX\_free(mdctx); printhex(md1, 32); printf("\\n"); if((mdctx = EVP\_MD\_CTX\_new()) == NULL) return 1; if(EVP\_DigestInit\_ex(mdctx, EVP\_sha256(), NULL) != 1) return 1; if(EVP\_DigestUpdate(mdctx, (unsigned char\*)g\_FirmwareSeed + 1, 31) != 1) return 1; if(EVP\_DigestUpdate(mdctx, (unsigned char\*)g\_FirmwareSeed, 1) != 1) return 1; if((md2 = (unsigned char \*)OPENSSL\_malloc(EVP\_MD\_size(EVP\_sha256()))) == NULL) return 1; if(EVP\_DigestFinal\_ex(mdctx, md2, NULL) != 1) return 1; EVP\_MD\_CTX\_free(mdctx); printhex(md2, 32); printf("\\n"); // // 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); printf("\\n"); return 0; } ``` ```php **chacha20.c:** #include "chacha20.h" static uint32\_t rotl32(uint32\_t x, int n) { return (x << n) | (x >> (32 - n)); } static uint32\_t pack4(const uint8\_t \*a) { uint32\_t res = 0; res |= (uint32\_t)a\[0\] << 0 \* 8; res |= (uint32\_t)a\[1\] << 1 \* 8; res |= (uint32\_t)a\[2\] << 2 \* 8; res |= (uint32\_t)a\[3\] << 3 \* 8; return res; } static void unpack4(uint32\_t src, uint8\_t \*dst) { dst\[0\] = (src >> 0 \* 8) & 0xff; dst\[1\] = (src >> 1 \* 8) & 0xff; dst\[2\] = (src >> 2 \* 8) & 0xff; dst\[3\] = (src >> 3 \* 8) & 0xff; } static void chacha20\_init\_block(struct chacha20\_context \*ctx, uint8\_t key\[\], uint8\_t nonce\[\]) { memcpy(ctx->key, key, sizeof(ctx->key)); memcpy(ctx->nonce, nonce, sizeof(ctx->nonce)); const uint8\_t \*magic\_constant = (uint8\_t\*)"expand 32-byte k"; ctx->state\[0\] = pack4(magic\_constant + 0 \* 4); ctx->state\[1\] = pack4(magic\_constant + 1 \* 4); ctx->state\[2\] = pack4(magic\_constant + 2 \* 4); ctx->state\[3\] = pack4(magic\_constant + 3 \* 4); ctx->state\[4\] = pack4(key + 0 \* 4); ctx->state\[5\] = pack4(key + 1 \* 4); ctx->state\[6\] = pack4(key + 2 \* 4); ctx->state\[7\] = pack4(key + 3 \* 4); ctx->state\[8\] = pack4(key + 4 \* 4); ctx->state\[9\] = pack4(key + 5 \* 4); ctx->state\[10\] = pack4(key + 6 \* 4); ctx->state\[11\] = pack4(key + 7 \* 4); // 64 bit counter initialized to zero by default. ctx->state\[12\] = pack4(nonce + 0 \* 4); ctx->state\[13\] = pack4(nonce + 1 \* 4); ctx->state\[14\] = pack4(nonce + 2 \* 4); ctx->state\[15\] = pack4(nonce + 3 \* 4); memcpy(ctx->nonce, nonce, sizeof(ctx->nonce)); } static void chacha20\_block\_set\_counter(struct chacha20\_context \*ctx, uint64\_t counter) { ctx->state\[12\] = (uint32\_t)counter; ctx->state\[13\] = pack4(ctx->nonce + 0 \* 4) + (uint32\_t)(counter >> 32); } static void chacha20\_block\_next(struct chacha20\_context \*ctx) { // This is where the crazy voodoo magic happens. // Mix the bytes a lot and hope that nobody finds out how to undo it. for (int i = 0; i < 16; i++) ctx->keystream32\[i\] = ctx->state\[i\]; #define CHACHA20\_QUARTERROUND(x, a, b, c, d) \\ x\[a\] += x\[b\]; x\[d\] = rotl32(x\[d\] ^ x\[a\], 16); \\ x\[c\] += x\[d\]; x\[b\] = rotl32(x\[b\] ^ x\[c\], 12); \\ x\[a\] += x\[b\]; x\[d\] = rotl32(x\[d\] ^ x\[a\], 8); \\ x\[c\] += x\[d\]; x\[b\] = rotl32(x\[b\] ^ x\[c\], 7); for (int i = 0; i < 10; i++) { CHACHA20\_QUARTERROUND(ctx->keystream32, 0, 4, 8, 12) CHACHA20\_QUARTERROUND(ctx->keystream32, 1, 5, 9, 13) CHACHA20\_QUARTERROUND(ctx->keystream32, 2, 6, 10, 14) CHACHA20\_QUARTERROUND(ctx->keystream32, 3, 7, 11, 15) CHACHA20\_QUARTERROUND(ctx->keystream32, 0, 5, 10, 15) CHACHA20\_QUARTERROUND(ctx->keystream32, 1, 6, 11, 12) CHACHA20\_QUARTERROUND(ctx->keystream32, 2, 7, 8, 13) CHACHA20\_QUARTERROUND(ctx->keystream32, 3, 4, 9, 14) } for (int i = 0; i < 16; i++) ctx->keystream32\[i\] += ctx->state\[i\]; uint32\_t \*counter = ctx->state + 12; // increment counter counter\[0\]++; if (0 == counter\[0\]) { // wrap around occured, increment higher 32 bits of counter counter\[1\]++; // Limited to 2^64 blocks of 64 bytes each. // If you want to process more than 1180591620717411303424 bytes // you have other problems. // We could keep counting with counter\[2\] and counter\[3\] (nonce), // but then we risk reusing the nonce which is very bad. assert(0 != counter\[1\]); } } void chacha20\_init\_context(struct chacha20\_context \*ctx, uint8\_t key\[\], uint8\_t nonce\[\])//, uint64\_t counter) { memset(ctx, 0, sizeof(struct chacha20\_context)); chacha20\_init\_block(ctx, key, nonce); //chacha20\_block\_set\_counter(ctx, counter); //ctx->counter = counter; ctx->position = 64; } void chacha20\_xor(struct chacha20\_context \*ctx, uint8\_t \*bytes, size\_t n\_bytes) { uint8\_t \*keystream8 = (uint8\_t\*)ctx->keystream32; for (size\_t i = 0; i < n\_bytes; i++) { if (ctx->position >= 64) { chacha20\_block\_next(ctx); ctx->position = 0; } bytes\[i\] ^= keystream8\[ctx->position\]; ctx->position++; } } ``` ```c **chacha20.h:** #pragma once #include <assert.h> #include <stddef.h> #include <stdint.h> #include <string.h> #ifdef \_\_cplusplus extern "C" { #endif struct chacha20\_context { uint32\_t keystream32\[16\]; size\_t position; uint8\_t key\[32\]; uint8\_t nonce\[12\]; uint64\_t counter; uint32\_t state\[16\]; }; void chacha20\_init\_context(struct chacha20\_context \*ctx, uint8\_t key\[\], uint8\_t nounc\[\]);//, uint64\_t counter); void chacha20\_xor(struct chacha20\_context \*ctx, uint8\_t \*bytes, size\_t n\_bytes); #ifdef \_\_cplusplus } #endif ``` 使用方法: ```php /decrypt\_rsapubkey \\ 4CF7A950B99CF29B0343E7BA6C609E49D9766F16C6D2F075F72AD400542F0765 \\ 97CE67A20E\[…\] … BER-encoded pub key: 3082010A02820101008D64BAC2CE5EBF82EDF58CA8C9E5B379D7C836E3F6ED0FEE2531A83286300F8A6…36BC071EAF6C7E3625E50203010001 ``` 最下面就是pub key,它使用EBR编码,最后0x10001(65537),密钥出来之后,下面进行绕过签名检测,之所以要绕过签名检测,是因为我们最后要进行对整体的固件包进行打包,在放到kernel里最后肯定要进行一个签名检测,如何在原有的固件包基础上加上其它的内容重新打包后而不损坏固定位置就很重要了,下面来进行过签名检测 签名检测 ---- 1.通过sha256计算initrd内容的哈希(不包括最后256字节) 2.读取initrd末尾的256字节签名数据 3.最后用mpi\_powm函数将signature\_data、public\_key\[1\] (n)、public\_key[0](e) 进行rsa签名检测,赋值给err\_code,如果返回值为0 说明有数据,随后将检验结果存放到缓冲区里tmp 4.然后就开始检测字节,第一个字节必须为0x1 第二个字节的后201字节必须为0xff,然后再检测后面前19个字节算法和预设的值匹配,对比19个字节的再后面与计算的hash结果匹配 0x00 0x01 padding 0x00 data 符合PKCS#1 v1.5 RSA签名方式 ### 验证上面的签名机制 手动取出 最后一个加密block,用xxd去分解或者winhex ```php xxd -p -u -s -256 -c 256 rootfs.gz.x64.v7.4.3 3B6207103D6FA98110868213C59544B5… ``` 然后再使用上面解密出来的公钥key(n,e)去用python做个rsa加密 ```php >>> e = 0x10001 >>> n = 0x008D64BAC2CE5EBF82EDF58CA8C9E5B3… >>> x = 0x3B6207103D6FA98110868213C59544B5… >>> res = pow(x, e, n) >>> print("%X" % res) 1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF … 003031300D060960864801650304020105000420E2738E2E2C73A55CC6DFE49A1E2E6C904A7948E 996E79D01EEECEDEA967DF914 #e2738e2e2c73a55cc6dfe49a1e2e6c904a7948e996e79d01eeecedea967df91 ``` 再手动提取下这个密文看下 ```php ls -l rootfs.gz.x64.v7.4.3 \-rw-r--r--1 user user 71395325 Feb 8 09:11 rootfs.gz.x64.v7.4.3 dd if=rootfs.gz.x64.v7.4.3 count=1 bs=$((71395325-256)) | sha256sum … e2738e2e2c73a55cc6dfe49a1e2e6c904a7948e996e79d01eeecedea967df914 - ``` 这个e2738e2e2c73a55cc6dfe49a1e2e6c904a7948e996e79d01eeecedea967df914 就是上面加密的密文,上面的用EBR编码了,可以看到一模一样,手动检测绕过了,后面只要保证PKCS#1 v1.5 RSA签名 方式格式就可以了 **Ramdisk decryption** ---------------------- 在主函数的最后还有个fgt\_verify\_decrypt(); ,手动逆向下这个函数的逻辑 ```c unsigned \_\_int64 fgt\_verify\_decrypt() { int \*initrd\_start; // r13 unsigned int size; // r14d \_DWORD \*v2; // rdi \_\_int64 n8; // rcx \_\_int64 \*key; // rsi \_DWORD v6\[8\]; // \[rsp+0h\] \[rbp-C0h\] BYREF \_DWORD KEY\[16\]; // \[rsp+20h\] \[rbp-A0h\] BYREF \_\_int64 v8\[4\]; // \[rsp+60h\] \[rbp-60h\] BYREF \_DWORD p\_iv\[8\]; // \[rsp+80h\] \[rbp-40h\] BYREF unsigned \_\_int64 v10; // \[rsp+A0h\] \[rbp-20h\] v10 = \_\_readgsqword(0x28u); initrd\_start = (int \*)::initrd\_start; size = initrd\_end - ::initrd\_start; fgt\_verifier\_key\_iv((\_\_int64)v8, (\_\_int64)p\_iv); # 内部派生主密钥以生成 ChaCha20 参数 v2 = v6; n8 = 8; key = v8; while ( n8 ) // 8 bytes的key转换,在大模型上的描述是将 8 字节的密钥数据从v8转换为v6数组格式 { \*v2 = \*(\_DWORD \*)key; key = (\_\_int64 \*)((char \*)key + 4); ++v2; --n8; } crypto\_chacha20\_init(KEY, v6, p\_iv); // 还是原来的chacha20加密算法初始化 chacha20\_docrypt((\_\_int64)KEY, initrd\_start, initrd\_start, size);// 继续解密 return v10 - \_\_readgsqword(0x28u); } ``` 将签名的initrd\_start 继续解密 还是魔改一下上面的脚本,看下 ```c /\* gcc decrypt\_rootfs.c chacha20.c -o decrypt\_rootfs -lssl -lcrypto \*/ #include <stdio.h> #include <stdlib.h> #include <openssl/evp.h> #include "chacha20.h" void printhex(unsigned char\* data, int len) { int i = 0; for(; i < len; i++) { printf("%02X", data\[i\]); } } int main(int argc, char \*\*argv) { if(argc < 4) { fprintf(stderr, "Usage: %s rootfs.tgz rootfs.tgz.decrypted <KEY\_HEXA>\\n", argv\[0\]); return 1; } if (strlen(argv\[3\]) != 64) { fprintf(stderr, "Key must be 64 (32-bytes) hexa chars string\\n"); return 1; } char g\_FirmwareSeed\[32\] = {0}; char \*pos = argv\[3\]; for (size\_t i = 0; i < sizeof g\_FirmwareSeed; i++) { sscanf(pos, "%2hhx", &g\_FirmwareSeed\[i\]); pos += 2; } EVP\_MD\_CTX \*mdctx; unsigned char \*md1 = NULL; unsigned char \*md2 = NULL; if((mdctx = EVP\_MD\_CTX\_new()) == NULL) return 1; if(EVP\_DigestInit\_ex(mdctx, EVP\_sha256(), NULL) != 1) return 1; if(EVP\_DigestUpdate(mdctx, (unsigned char\*)g\_FirmwareSeed + 4, 28) != 1) return 1; if(EVP\_DigestUpdate(mdctx, (unsigned char\*)g\_FirmwareSeed, 4) != 1) return 1; if((md1 = (unsigned char \*)OPENSSL\_malloc(EVP\_MD\_size(EVP\_sha256()))) == NULL) return 1; if(EVP\_DigestFinal\_ex(mdctx, md1, NULL) != 1) return 1; EVP\_MD\_CTX\_free(mdctx); printhex(md1, 32); printf("\\n"); if((mdctx = EVP\_MD\_CTX\_new()) == NULL) return 1; if(EVP\_DigestInit\_ex(mdctx, EVP\_sha256(), NULL) != 1) return 1; if(EVP\_DigestUpdate(mdctx, (unsigned char\*)g\_FirmwareSeed + 5, 27) != 1) return 1; if(EVP\_DigestUpdate(mdctx, (unsigned char\*)g\_FirmwareSeed, 5) != 1) return 1; if((md2 = (unsigned char \*)OPENSSL\_malloc(EVP\_MD\_size(EVP\_sha256()))) == NULL) return 1; if(EVP\_DigestFinal\_ex(mdctx, md2, NULL) != 1) return 1; EVP\_MD\_CTX\_free(mdctx); printhex(md2, 32); printf("\\n"); // // ChaCha20 (custom) // FILE \*f = fopen(argv\[1\], "rb"); fseek(f, 0, SEEK\_END); long fsize = ftell(f); fseek(f, 0, SEEK\_SET); // skip trailing signature fsize -= 256; printf("rootfs size: %u\\n", fsize); char \*data = malloc(fsize); fread(data, fsize, 1, f); fclose(f); printf("Decrypting rootfs...\\n"); struct chacha20\_context ctx; chacha20\_init\_context(&ctx, md1, md2); chacha20\_xor(&ctx, data, fsize); // Check if GZ uint16\_t magic = \*(int16\_t \*)data; if (magic != 0x8B1F) { fprintf(stderr, "Failed to decrypt (not a GZ, magic=%X)\\n", magic); return 1; } printf("Writing to %s...\\n", argv\[2\]); FILE \*f\_out = fopen(argv\[2\], "wb"); fwrite(data, fsize, 1, f\_out); fclose(f\_out); return 0; } ``` ```php ./decrypt\_rootfs rootfs.gz.x64.v7.4.3 rootfs.gz.x64.v7.4.3.decrypted \\ 4CF7A950B99CF29B0343E7BA6C609E49D9766F16C6D2F075F72AD400542F0765 … rootfs size: 71395069 Decrypting rootfs... Writing to rootfs.gz.x64.v7.4.3.decrypted... file rootfs.gz.x64.v7.4.3.decrypted rootfs.gz.x64.v7.4.3.decrypted: gzip compressed data, last modified: Thu Feb 1 17:37:142024, from Unix, original size modulo 2^32 116932640 ``` 这里的主密钥 需要每次手动提取比较麻烦,这里借鉴下网上的获取密钥方法 ```python from argparse import ArgumentParser import sys import binascii from miasm.analysis.binary import Container from miasm.analysis.machine import Machine from miasm.core.locationdb import LocationDB from miasm.ir.symbexec import SymbolicExecutionEngine from miasm.expression.expression import \* parser = ArgumentParser("Get FortiGate rootfs encryption seed") parser.add_argument("target_binary", help="Target binary path") options = parser.parse_args() fdesc = open(options.target_binary, 'rb') loc_db = LocationDB() cont = Container.from\_stream(fdesc, loc_db) machine = Machine(cont.arch) print(f"Architecture: {cont.arch}") ret_val_reg = None arg_val_reg = None match cont.arch: case "x86\_64": ret_val_reg = machine.mn.regs.RAX arg_val_reg = machine.mn.regs.RSI case "aarch64l": ret_val_reg = machine.mn.regs.X0 arg_val_reg = machine.mn.regs.X1 case _: sys.stderr.write("OS architecture not supported!") sys.exit(1) 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 = list() 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) print(f"Seed address: {hex(seed\_addr)}") seed_data = cont.executable.get_virt().get(seed_addr, seed_addr + 32) seed_data = binascii.hexlify(seed_data).upper() print(f"Extracted seed: {seed_data}") python getrootfskey.py ./flatkc.elf Architecture: x86_64 Seed address: 0xffffffff817932e0 Extracted seed: b'4CF7A950B99CF29B0343E7BA6C609E49D9766F16C6D2F075F72AD400542F0765' ``` 成功提取,然后再用解密进行解密即可 总结 -- 其实在很多固件加密当中,都可以上面的思路进行逆向分析去达到解密的操作,大佬勿喷。。希望对需要的人或者想研究固件加密的人有所帮助
发表于 2025-07-23 09:00:00
阅读 ( 533 )
分类:
硬件与物联网
0 推荐
收藏
0 条评论
请先
登录
后评论
Azd
5 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!