关于如何获取 FortiGate shell 权限,可信执行和新版本 License 授权分析。(二)
新的验证逻辑
我们在之前的文章中介绍了如何向 FortiGate 中植入后门并获取 SHELL 方便调试,在新版本中,开发者添加了多种系统完整性验证逻辑,并且加密了 rootfs.gz 文件,旧的方法失效,在本篇文章中,提供一种相对简单的方法,实现向新版本 FortiGate 中添加后门并获取 SHELL 权限。
在 2024 年 3 月 4 日,optistream 的研究人员发布了一篇文章,详细描述了在新版 FortiGate 中添加的加密和校验逻辑,并且能够绕过这些校验,最终获取 root shell,感兴趣的朋友可以参考他们的分析文章,这里不再赘述,只做简要总结。
相对于旧版本来说,主要的变动有
- 在内核中添加了对 rootfs.gz 文件的完整性校验和解密算法
- 在用户态添加了
.db
文件的完整性校验 - 在系统中实现了 “forticron“ 自动任务,可能会在系统运行期间自动对文件系统执行完整性校验
后两点本质上对破解流程没有很大影响,只需要找到这些新添加的校验逻辑并将它们 Patch 掉即可。而影响较大的是 rootfs.gz 被加密,并且在内核中进行校验和解密。解密算法在 optistream 分析文章中已经给出,他们的思路为 Patch 掉用户空间完整性校验,植入后门并启动系统,在系统启动时调试内核,跳过内核中的校验算法,最终使得系统能够正常启动,这一点和本博客前篇文章类似。
本文将介绍一种更加简洁的方法,无需调试内核即可正常启动系统。
修改内核
FortiGate 的内核文件是 flatkc,通过 file 命令可以看到它的格式为:
1 | flatkc: Linux kernel x86 boot executable bzImage, version 4.19.13 (root@build) #1 SMP Thu Feb 1 17:10:41 UTC 2024, RO-rootFS, swap_dev 0X7, Normal VGA |
bzImage 是 Linux 内核的一种引导映像格式,主要用于基于 x86/x64 架构的计算机,bzImage 中包含被压缩的内核文件(vmlinux)以及一段用于解压内核的代码,另外它还负责处理内核命令行参数等辅助数据。
bzImage 等格式的镜像可以使用 vmlinux-to-elf 项目直接转换为 ELF 文件,通过分析 ELF 文件定位到校验和解密内核的函数是 fgt_verify_initrd。理想情况下 Patch 内核的思路应该是
- 将 bzImage 解压
- 修改 vmlinux 中的代码
- 将 vmlinux 压缩回 bzImage,并确保系统仍能够正常启动
前两步可以容易的完成,但如何将 vmlinux 压缩回 bzImage 却没有想象中那样简单。
在分析过程中我查阅了网络上的多篇资料,最终找到一位作者 jamchamb 发布的博客文章,文中介绍了如何从逆向角度修改 ARM zImage 内核文件,最终成功完成重打包操作,修改了系统启动时输出的字符串信息。为了方便理解,我们先复现一下文中提到的方法,详细过程可以阅读原作者文章。
首先下载 zImage 文件并使用 QEMU 尝试启动
1 | wget https://archive.openwrt.org/releases/17.01.0/targets/armvirt/generic/lede-17.01.0-r3205-59508e3-armvirt-zImage-initramfs -O zImage-initramfs |
等待启动后按下回车可正常进入 shell,输出信息如下
1 | BusyBox v1.25.1 () built-in shell (ash) |
我们希望将 WARNING!
字符串修改为 NORMAL!!
。
通过查看 Linux 源码目录,在 zImage 中被压缩的 vmlinux 叫做 Piggy,Piggy 可以由不同的算法压缩,我们下载的镜像使用的压缩算法为 xz
1 | DECIMAL HEXADECIMAL DESCRIPTION |
在 piggy.xzkern.S 汇编中看到 Piggy 在 zImage 文件中的位置由 input_data、input_data_end 界定,这些变量在 arch/arm/boot/compressed/misc.c 中的 decompress_kernel 函数被引用
1 | void |
do_decompress 函数负责对内核文件进行解压,对照源码查看 zImage 的反编译代码
1 | int __fastcall decompress_kernel(unsigned int output_start, unsigned int free_mem_ptr_p, unsigned int free_mem_ptr_end_p, int arch_id) |
这样找到了 input_data 和 input_data_end 两个变量的值,也就可以定位到 Piggy 的位置。
将 Piggy 拆出
1 | dd if=zImage-initramfs of=vmlinux.xz ibs=1 skip=$[0x3d10] count=$[0x2BB404] |
注意拆分出来的文件是一个正常的 xz 压缩包,但是在末尾多出了 4 个字节,用来存放原始 vmlinux 的大小,这一点可以在源码中看到。因此解压时使用参数 --single-stream
避免出现解压失败的提示。
1 | unxz --verbose --single-stream vmlinux.xz |
打开解压得到的 vmlinux,在其中找到想要修改的字符串进行修改,完成之后把 vmlinux 重新压缩回 Piggy (这里使用了 xz 的 nice 参数,以便于让重打包的文档尽可能小于原始文档)
1 | xz --check=crc32 --arm --lzma2=,dict=32MiB,nice=128 < vmlinux > vmlinux-mod-warntest.xz |
得到的 xz 文档相比源文档更小
1 | -rw-rw-r-- 1 admin admin 2863840 3月 14 18:04 vmlinux-mod-warntest.xz |
接下来要把修改之后的文档重新塞入 zImage 中,由于新的文档更小,所以不用考虑扩容的问题,缺失的部分使用 00 填充,不会影响 xz 正常解压。不过注意保留原来 xz 文档结尾的 4 个字节。
1 | cp zImage-initramfs zImage-initramfs-warnmod |
修改之后启动新的镜像
1 | qemu-system-arm -serial stdio -M virt -m 1024 -kernel zImage-initramfs-warnmod |
可以在终端看到修改已经成功
1 | BusyBox v1.25.1 () built-in shell (ash) |
基于以上思路,猜测对 FortiGate 的 flatkc 也可以执行类似的操作,先将 Piggy 取出,解压后把校验和解密 rootfs.gz 的函数跳过,再将内核重新压缩并塞回 flatkc 中。
flatkc 是一个 x86 镜像,所以在 Linux 源码中找到 arch/x86/boot/compressed 目录,在 misc.c 中找到了叫做 extract_kernel 的函数
1 | asmlinkage __visible void *extract_kernel(void *rmode, memptr heap, |
通过搜索函数出现的一些字符串,在 flatkc 中找到了该函数
1 | __int64 *__fastcall extract_kernel( |
观察发现函数开头和源码大致相同,但后面出现了一些和 gzip 解压相关的代码,搜索字符串在 decompress_inflate.c 找到的相关定义
1 | STATIC int INIT __gunzip(unsigned char *buf, long len, |
这说明 vmlinux 使用 gzip 算法压缩,通过 binwalk 也可以验证这一点
1 | DECIMAL HEXADECIMAL DESCRIPTION |
从源码看到 extract_kernel 函数在 head_64.S 中被调用
1 | /* |
在 flatkc 也可以找到对应代码
1 | push rsi |
根据参数位置,发现 Piggy 的起始地址为 0x41B4,长度为 0x6DF3A4,最终解压得到的 vmlinux 大小应该是 0x1A34918。
所以先将 Piggy 拆出
1 | dd if=flatkc of=vmlinux.gz ibs=1 skip=$[0x41B4] count=$[0x6DF3A4] |
查看 Piggy 信息
1 | vmlinux.gz: gzip compressed data, max compression, from Unix, original size modulo 2^32 27478296 |
使用 gzip 解压得到 vmlinux
1 | gzip -d vmlinux.gz |
接下来要对 vmlinux 进行修改,为了方便定位需要修改的地址,可以先使用 vmlinux-to-elf 将 vmlinux 转换为带有符号信息的 ELF 文件,通过逆向定位到 fgt_verify_initrd 函数地址是 0xFFFFFFFF81709689,考虑这个函数只操作了 initramfs_start 和 initramfs_stop 即 rootfs.gz 的数据,我们选择直接将函数第一条指令改为 ret
1 | .init.text:FFFFFFFF81709689 retn |
然后把 Patch 好的 vmlinux 重新压缩回 gzip 格式
1 | cat vmlinux | gzip -9 > vmlinux.gz |
查看新文档和原文档的大小差异
1 | -rw-rw-r-- 1 admin admin 7205795 3月 15 09:07 vmlinux.gz |
新的文档比原文档小一个字节,先将新文档覆盖回 flatkc
1 | dd if=/dev/zero of=flatkc bs=1 seek=$[0x41B4] count=$[0x6DF3A4] conv=notrunc |
由于在 gzip 文件结尾添加多余字符时可能会导致解压失败,所以修改调用 extract_kernel 的汇编代码,将 input_len 修改为实际长度 7205795。
使用 qemu 在本地启动测试
1 | qemu-system-x86_64 -serial stdio -M q35 -m 1024 -kernel flatkc |
启动之后内核 panic 在 mount_block_root 位置,因为本地模拟不存在 rootfs.gz 文件,所以这应该是正常现象。
植入后门
内核修改完毕,下面要向系统植入后门。
修改之后的内核理论上已经不会再对 rootfs.gz 执行校验和加密,所以要将虚拟磁盘中被加密的 rootfs.gz 替换为明文版本。解密算法可参考之前的文章, 也可以使用我编写的小工具。需要注意的是生成的 dec.gz 末尾 256 字节是校验数据,要手动将它们去除。
解压 rootfs.gz 得到文件系统,首先要把 /bin/init 中完整性校验逻辑 Patch 掉,通过逆向分析发现当完整性校验失败时,都会执行 do_halt 函数
1 | // ... |
所以我们直接将 do_halt 的第一条指令改为 ret,跳过这个函数,这样就算校验失败也不会导致系统重启。
然后向系统添加 busybox 和 sh,再将 smartctl 替换成启动 shell 的脚本
1 | !/bin/sh |
重新打包 rootfs.gz,替换掉原文件。
可信执行
至此我们已经将内核和文件系统中部分完整性校验绕过,并且无需关心 rootfs.gz 加解密的问题,将内核和 rootfs.gz 替换之后尝试启动
从 log 中看到由于完整性校验失败输出错误信息,但是系统仍然成功启动并进入登录界面。
登录后执行 diagnose hardware smartctl
即可得到 SHELL 权限
当尝试使用 wget 下载新的文件到系统并执行时,系统会重新启动。在官方文档找到了导致这一问题的原因:https://docs.fortinet.com/document/fortigate/7.4.0/new-features/226732/real-time-file-system-integrity-checking
新版本添加了实时完整性检查,即可信执行,在系统启动时内核会统计关键文件的 hash 值并存放到内存,当执行程序时内核验证这个程序的 hash 是否和原始值相同,如果不同或不存在,则说明该程序非法,会导致系统直接重启。
不过我们离线添加的 busybox 等程序能够正常执行,因此只需要将想要运行的程序提前添加到磁盘中(bin.tar.xz),进入系统就能正常使用。例如再向系统添加一个 gdbserver 程序,重新启动后可正常运行。
通过阅读官方文档得知,新版本添加的可信执行校验位于内核中,具体来说,开发者利用 Linux 的 IMA(完整性子系统) 实现了一套运行时文件完整性校验。此外还通过 LSM(Linux Security Module) 框架实现了访问控制系统。
逆向分析内核,定位到 forti_security_module_init
函数:
1 | __int64 forti_security_module_init() |
这个函数通过 security_add_hooks
注册 LSM 的 handler,在 fortism_func_list
函数列表中包含 fortism_file_open
、fortism_path_link
、fortism_path_rename
、fortism_kernel_load_data
、fortism_sb_mount
五个函数,实现了对文件、符号链接、内核模块、磁盘挂载等操作的访问控制。我们以 fortism_file_open
函数为例简要分析。fortism_file_open
最终会调用 fortism_file_open_part_0
:
1 | unsigned __int64 __fastcall fortism_file_open_part_0(__int64 a1) |
此函数会遍历两个全局数组,数组中包含一些路径
1 | /migadmin/fortiguard_resources |
当传入的参数匹配以上路径时,函数打印失败信息 try to write readonly file(xxx)
并返回负数值。这一点可以在 SHELL 中验证,例如我们尝试在 /bin 目录下创建一个 testfile 文件,会出现错误信息
另外内核中又存在 ima_file_mmap
函数:
1 | __int64 __fastcall ima_file_mmap(__int64 file, char prot) |
最终调用 fos_process_appraise_constprop_0
1 | __int64 __fastcall fos_process_appraise_constprop_0(unsigned __int64 file) |
简单来说,函数会先判断当前是否开启了 IMA 验证,IMA 可以通过修改 /sys/kernel/security/integrity/fos/fix_to_enforce
值来开启或关闭。在 init 程序中也能找到相关代码
1 | __int64 enforce_fos_integrity() |
不过实际测试发现修改此文件并不能关闭 IMA 验证,原因暂时未知。
继续观察 fos_process_appraise_constprop_0
,当在缓存中找不到待验证文件的 Hash 时,代码判断这个文件是否为 /data/lib/libav.so
或者 /data/lib/libips.so
,如果是二者之一,调用 fos_verify_pkcs7
检查文件签名。
如果检查失败,应该返回错误并重启系统,但这里在失败的情况下仅打印了 log 信息,函数继续向下执行并返回 0,表示验证通过。
/data/lib 目录不在访问控制的范围内,且代码对这两个文件缺乏校验,所以我们尝试自己编写程序覆盖其中之一,看看能否绕过可信执行。
虽然内核 log 中留下了加载非法文件的提示,但程序依然成功执行,验证了前面的分析。
破解 License
在之前的文章中介绍了如何生成测试版 License,新版代码修改了验证逻辑,导致以前生成的 License 无法正常使用。
新的验证函数:
1 | __int64 __fastcall do_process_license(char *license_data, __int64 a2, char *lic_struct) |
对比旧版验证逻辑,总结变动如下
1 | 1. 去除了单独使用 AES 算法解密 License 的代码 |
对于第一点,代码在处理 License 时会先读取开头 4 个字节,这 4 字节表示 Header 长度,默认为 64。然后获取后续 64 字节,再用内存中解压得到的公钥进行解密,将解密结果作为 AES KEY,使用 AES 算法继续解密剩余内容。所以我们只需要获取一个合法的 License Header,并使用它对应的 AES KEY 加密 License 数据即可通过验证。
对于第二、三点,由于在获取 SHELL 权限时就需要对 init 程序进行修改,所以顺便把两个判断也绕过。是否包含 CMS 对 License 验证基本无影响,所以 CMS 验证暂时忽略。
按照上面的思路我编写了针对新版的 License 生成工具,你可以在 Github 上获取。
参考资料
本文介绍了一种通过修改 FortiGate 内核与文件系统实现获取 SHELL 权限的方法,修改后无需调试内核或者关心 rootfs.gz 加解密即可启动系统。分析了新版添加的可信执行策略,并找到一种方法来绕过。分析了新版的 License 校验逻辑,并更新工具实现对新版本的破解。
https://jamchamb.net/2022/01/02/modify-vmlinuz-arm.html
https://www.optistream.io/blogs/tech/fortigate-firmware-analysis
https://stackoverflow.com/questions/76571876/how-to-repack-vmlinux-elf-back-to-bzimage-file
https://reverseengineering.stackexchange.com/questions/27803/repacking-vmlinux-into-zimage-bzimage
https://github.com/kiler129/recreate-zImage
https://zhuanlan.zhihu.com/p/438209486
https://liwugang.github.io/2020/10/25/introduce_lsm.html
- 本文作者: CataLpa
- 本文链接: https://wzt.ac.cn/2024/04/02/fortigate_debug_env2/
-
版权声明:
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。