文章
中介绍了如何向 FortiGate 中植入后门并获取 SHELL 方便调试,在新版本中,开发者添加了多种系统完整性验证逻辑,并且加密了 rootfs.gz 文件,旧的方法失效,在本篇文章中,提供一种相对简单的方法,实现向新版本 FortiGate 中添加后门并获取 SHELL 权限。
在 2024 年 3 月 4 日,
optistream
的研究人员发布了一篇文章,详细描述了在新版 FortiGate 中添加的加密和校验逻辑,并且能够绕过这些校验,最终获取 root shell,感兴趣的朋友可以参考他们的分析文章,这里不再赘述,只做简要总结。
相对于旧版本来说,主要的变动有
在内核中添加了对 rootfs.gz 文件的完整性校验和解密算法
在用户态添加了
.db
文件的完整性校验
在系统中实现了 “
forticron
“ 自动任务,可能会在系统运行期间自动对文件系统执行完整性校验
后两点本质上对破解流程没有很大影响,只需要找到这些新添加的校验逻辑并将它们 Patch 掉即可。而影响较大的是 rootfs.gz 被加密,并且在内核中进行校验和解密。解密算法在 optistream 分析文章中已经给出,他们的思路为 Patch 掉用户空间完整性校验,植入后门并启动系统,在系统启动时调试内核,跳过内核中的校验算法,最终使得系统能够正常启动,这一点和本博客前篇文章类似。
本文将介绍一种更加简洁的方法,无需调试内核即可正常启动系统。
vmlinux-to-elf
项目直接转换为 ELF 文件,通过分析 ELF 文件定位到校验和解密内核的函数是 fgt_verify_initrd。理想情况下 Patch 内核的思路应该是
将 bzImage 解压
修改 vmlinux 中的代码
将 vmlinux 压缩回 bzImage,并确保系统仍能够正常启动
前两步可以容易的完成,但如何将 vmlinux 压缩回 bzImage 却没有想象中那样简单。
在分析过程中我查阅了网络上的多篇资料,最终找到一位作者 jamchamb 发布的
博客文章
,文中介绍了如何从逆向角度修改 ARM zImage 内核文件,最终成功完成重打包操作,修改了系统启动时输出的字符串信息。为了方便理解,我们先复现一下文中提到的方法,详细过程可以阅读原作者文章。
首先下载 zImage 文件并使用 QEMU 尝试启动
1 2
wget https://archive.openwrt.org/releases/17.01.0/targets/armvirt/generic/lede-17.01.0-r3205-59508e3-armvirt-zImage-initramfs -O zImage-initramfs qemu-system-arm -serial stdio -M virt -m 1024 -kernel zImage-initramfs
等待启动后按下回车可正常进入 shell,输出信息如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
BusyBox v1.25.1 () built-in shell (ash) _________ / /\ _ ___ ___ ___ / LE / \ | | | __| \| __| / DE / \ | |__| _|| |) | _| /________/ LE \ |____|___|___/|___| lede-project.org \ \ DE / \ LE \ / ----------------------------------------------------------- \ DE \ / Reboot (17.01.0, r3205-59508e3) \________\/ ----------------------------------------------------------- === WARNING! ===================================== There is no root password defined on this device! Use the "passwd" command to set up a new password in order to prevent unauthorized SSH logins. -------------------------------------------------- root@(none):/#
我们希望将
WARNING!
字符串修改为
NORMAL!!
。
通过查看 Linux
源码目录
,在 zImage 中被压缩的 vmlinux 叫做 Piggy,Piggy 可以由不同的算法压缩,我们下载的镜像使用的压缩算法为 xz
1 2 3 4 5
DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 Linux kernel ARM boot executable zImage (little-endian) 15400 0x3C28 xz compressed data 15632 0x3D10 xz compressed data
在 piggy.xzkern.S 汇编中看到 Piggy 在 zImage 文件中的位置由 input_data、input_data_end 界定,这些变量在 arch/arm/boot/compressed/misc.c 中的 decompress_kernel 函数被引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
void decompress_kernel (unsigned long output_start, unsigned long free_mem_ptr_p, unsigned long free_mem_ptr_end_p, int arch_id) { int ret; __stack_chk_guard_setup(); output_data = (unsigned char *)output_start; free_mem_ptr = free_mem_ptr_p; free_mem_end_ptr = free_mem_ptr_end_p; __machine_arch_type = arch_id; arch_decomp_setup(); putstr("Uncompressing Linux..." ); ret = do_decompress(input_data, input_data_end - input_data, output_data, error); if (ret) error("decompressor returned an error" ); else putstr(" done, booting the kernel.\n" ); }
do_decompress 函数负责对内核文件进行解压,对照源码查看 zImage 的反编译代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
int __fastcall decompress_kernel (unsigned int output_start, unsigned int free_mem_ptr_p, unsigned int free_mem_ptr_end_p, int arch_id) { const char *v5; int result; const char *v8; sub_9AC(); v5 = "Uncompressing Linux..." ; while ( *v5++ ) ; result = do_decompress(input_data, 0x2BB404 , output_start, sub_940); if ( result ) sub_940("decompressor returned an error" ); v8 = " done, booting the kernel.\n" ; while ( *v8++ ) ; return result; }
这样找到了 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 2
-rw-rw-r-- 1 admin admin 2863840 3月 14 18:04 vmlinux-mod-warntest.xz -rw-rw-r-- 1 admin admin 2864132 3月 14 18:05 vmlinux.xz
接下来要把修改之后的文档重新塞入 zImage 中,由于新的文档更小,所以不用考虑扩容的问题,缺失的部分使用 00 填充,不会影响 xz 正常解压。不过注意保留原来 xz 文档结尾的 4 个字节。
1 2 3
cp zImage-initramfs zImage-initramfs-warnmoddd if =/dev/zero of=zImage-initramfs-warnmod bs=1 seek=$[0x3d10] count=$[0x2bb400] conv=notruncdd if =vmlinux-mod-warntest.xz of=zImage-initramfs-warnmod bs=1 seek=$[0x3d10] conv=notrunc
修改之后启动新的镜像
1
qemu-system-arm -serial stdio -M virt -m 1024 -kernel zImage-initramfs-warnmod
可以在终端看到修改已经成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
BusyBox v1.25.1 () built-in shell (ash) _________ / /\ _ ___ ___ ___ / LE / \ | | | __| \| __| / DE / \ | |__| _|| |) | _| /________/ LE \ |____|___|___/|___| lede-project.org \ \ DE / \ LE \ / ----------------------------------------------------------- \ DE \ / Reboot (17.01.0, r3205-59508e3) \________\/ ----------------------------------------------------------- === NORMAL!! ===================================== There is no root password defined on this device! Use the "passwd" command to set up a new password in order to prevent unauthorized SSH logins. --------------------------------------------------
基于以上思路,猜测对 FortiGate 的 flatkc 也可以执行类似的操作,先将 Piggy 取出,解压后把校验和解密 rootfs.gz 的函数跳过,再将内核重新压缩并塞回 flatkc 中。
flatkc 是一个 x86 镜像,所以在 Linux 源码中找到 arch/x86/boot/compressed 目录,在 misc.c 中找到了叫做 extract_kernel 的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
asmlinkage __visible void *extract_kernel (void *rmode, memptr heap, unsigned char *input_data, unsigned long input_len, unsigned char *output, unsigned long output_len) { const unsigned long kernel_total_size = VO__end - VO__text; unsigned long virt_addr = LOAD_PHYSICAL_ADDR; boot_params = rmode; boot_params->hdr.loadflags &= ~KASLR_FLAG; sanitize_boot_params(boot_params); if (boot_params->screen_info.orig_video_mode == 7 ) { vidmem = (char *) 0xb0000 ; vidport = 0x3b4 ; } else { vidmem = (char *) 0xb8000 ; vidport = 0x3d4 ; } lines = boot_params->screen_info.orig_video_lines; cols = boot_params->screen_info.orig_video_cols; console_init(); debug_putstr("early console in extract_kernel\n" ); free_mem_ptr = heap; free_mem_end_ptr = heap + BOOT_HEAP_SIZE; debug_putaddr(input_data); debug_putaddr(input_len); debug_putaddr(output); debug_putaddr(output_len); debug_putaddr(kernel_total_size); #ifdef CONFIG_X86_64 debug_putaddr(trampoline_32bit); #endif choose_random_location((unsigned long )input_data, input_len, (unsigned long *)&output, max(output_len, kernel_total_size), &virt_addr); if ((unsigned long )output & (MIN_KERNEL_ALIGN - 1 )) error("Destination physical address inappropriately aligned" ); if (virt_addr & (MIN_KERNEL_ALIGN - 1 )) error("Destination virtual address inappropriately aligned" ); #ifdef CONFIG_X86_64 if (heap > 0x3fffffffffff UL) error("Destination address too large" ); if (virt_addr + max(output_len, kernel_total_size) > KERNEL_IMAGE_SIZE) error("Destination virtual address is beyond the kernel mapping area" ); #else if (heap > ((-__PAGE_OFFSET-(128 <<20 )-1 ) & 0x7fffffff )) error("Destination address too large" ); #endif #ifndef CONFIG_RELOCATABLE if ((unsigned long )output != LOAD_PHYSICAL_ADDR) error("Destination address does not match LOAD_PHYSICAL_ADDR" ); if (virt_addr != LOAD_PHYSICAL_ADDR) error("Destination virtual address changed when not relocatable" ); #endif debug_putstr("\nDecompressing Linux... " ); __decompress(input_data, input_len, NULL , NULL , output, output_len, NULL , error); parse_elf(output); handle_relocations(output, output_len, virt_addr); debug_putstr("done.\nBooting the kernel.\n" ); return output; }
通过搜索函数出现的一些字符串,在 flatkc 中找到了该函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
__int64 *__fastcall extract_kernel ( void *rmode, void *heap, unsigned __int8 *input_data, unsigned int input_len, unsigned __int8 *output, unsigned int output_len) { v8 = heap; v11 = rmode; *(rmode + 529 ) &= ~2u ; v12 = *(rmode + 495 ) == 0 ; qword_700690 = rmode; if ( !v12 ) { sub_6E6A00(rmode + 192 , 0 , 256LL ); sub_6E6A00(rmode + 491 , 0 , 6LL ); sub_6E6A00(rmode + 616 , 0 , 40LL ); sub_6E6A00(rmode + 3280 , 0 , 48LL ); heap = 0LL ; sub_6E6A00(rmode + 3820 , 0 , 276LL ); v11 = 0LL ; } if ( v11[6 ] == 7 ) { qword_7006B8 = 720896LL ; dword_7006B0 = 948 ; } else { qword_7006B8 = 753664LL ; dword_7006B0 = 980 ; } dword_7006AC = v11[14 ]; dword_7006A8 = v11[7 ]; sub_6E6F90(); qword_700688 = v8; qword_700680 = v8 + 0x10000 ; v13 = &unk_1826000; if ( *&output_len >= 0x1826000 uLL ) v13 = *&output_len; if ( (output & 0x1FFFFF ) != 0 ) error("Destination physical address inappropriately aligned" , heap); if ( v8 > 0x3FFFFFFFFFFF LL ) error("Destination address too large" , heap); if ( v13 + 0x200000 > 0x20000000 ) error("Destination virtual address is beyond the kernel mapping area" , heap); if ( output != LOAD_PHYSICAL_ADDR ) error("Destination address does not match LOAD_PHYSICAL_ADDR" , heap); v14 = input_data; if ( !input_data ) { v14 = malloc (0x4000 uLL); if ( !v14 ) error("Out of memory while allocating input buffer" , heap); *&input_len = 0LL ; } v15 = malloc (0x60 uLL); if ( !v15 ) error("Out of memory while allocating z_stream" , heap); v16 = malloc (0x2548 uLL); v15[8 ] = v16; v18 = v16; if ( !v16 ) error("Out of memory while allocating workspace" , heap); if ( !*&input_len ) { heap = sub_4000; *&input_len = fill(v14, 0x4000 LL); } if ( *&input_len <= 9 || *v14 != 31 || v14[1 ] != 0x8B || v14[2 ] != 8 ) error("Not a gzip file" , heap); v19 = *&input_len - 10LL ; *v15 = v14 + 10 ; v15[1 ] = v19; if ( (v14[3 ] & 8 ) != 0 ) { do { if ( !v19 ) error("header error" , heap); v20 = *v15; v15[1 ] = --v19; *v15 = v20 + 1 ; } while ( *v20 ); } v15[7 ] = v18; v15[3 ] = 0x200000 LL; v15[4 ] = v17; v15[6 ] = 0LL ; v18[2 ] = 0 ; v18[10 ] = 15 ; *(v18 + 7 ) = v15[8 ] + 9544LL ; v21 = sub_6E4440(v15); *(v15[8 ] + 44LL ) = 0 ; *(v15[8 ] + 56LL ) = 0LL ; if ( !v21 ) { while ( 1 ) { if ( !v15[1 ] ) { v22 = fill(v14, 0x4000 LL); if ( v22 < 0 ) error("read error" , 0x4000 LL); *v15 = v14; v15[1 ] = v22; } v23 = sub_6E4540(v15, 0LL ); if ( v23 == 1 ) break ; if ( v23 ) error("uncompression error" , 0LL ); } } dword_700698 = -2 ; if ( !input_data ) { dword_700698 = -3 ; qword_7006A0 = 0LL ; } sub_6E6A90(v29, LOAD_PHYSICAL_ADDR); if ( v29[0 ] != 1179403647 ) error("Kernel is not a valid ELF file" , LOAD_PHYSICAL_ADDR); v24 = malloc (56 * v31); v25 = v24; if ( !v24 ) error("Failed to allocate space for phdrs" , LOAD_PHYSICAL_ADDR); v26 = 0 ; v27 = v30 + 0x200000 ; sub_6E6A90(v24, v30 + 0x200000 ); if ( v31 ) { do { if ( *v25 == 1 ) { if ( (v25[12 ] & 0x1FFFFF ) != 0 ) error("Alignment of LOAD segment isn't multiple of 2MB" , v27); v27 = *(v25 + 1 ) + 0x200000 LL; sub_6E6A30(*(v25 + 3 ), v27); } ++v26; v25 += 14 ; } while ( v26 < v31 ); } dword_700698 = -1 ; return LOAD_PHYSICAL_ADDR; }
观察发现函数开头和源码大致相同,但后面出现了一些和 gzip 解压相关的代码,搜索字符串在 decompress_inflate.c 找到的相关定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
STATIC int INIT __gunzip(unsigned char *buf, long len, long (*fill)(void *, unsigned long ), long (*flush)(void *, unsigned long ), unsigned char *out_buf, long out_len, long *pos, void (*error)(char *x)) { u8 *zbuf; struct z_stream_s *strm ; int rc; rc = -1 ; if (flush) { out_len = 0x8000 ; out_buf = malloc (out_len); } else { if (!out_len) out_len = ((size_t )~0 ) - (size_t )out_buf; } if (!out_buf) { error("Out of memory while allocating output buffer" ); goto gunzip_nomem1; } if (buf) zbuf = buf; else { zbuf = malloc (GZIP_IOBUF_SIZE); len = 0 ; } if (!zbuf) { error("Out of memory while allocating input buffer" ); goto gunzip_nomem2; } strm = malloc (sizeof (*strm)); if (strm == NULL ) { error("Out of memory while allocating z_stream" ); goto gunzip_nomem3; } strm->workspace = malloc (flush ? zlib_inflate_workspacesize() : sizeof (struct inflate_state)); if (strm->workspace == NULL ) { error("Out of memory while allocating workspace" ); goto gunzip_nomem4; } if (!fill) fill = nofill; if (len == 0 ) len = fill(zbuf, GZIP_IOBUF_SIZE); if (len < 10 || zbuf[0 ] != 0x1f || zbuf[1 ] != 0x8b || zbuf[2 ] != 0x08 ) { if (pos) *pos = 0 ; error("Not a gzip file" ); goto gunzip_5; } strm->next_in = zbuf + 10 ; strm->avail_in = len - 10 ; if (zbuf[3 ] & 0x8 ) { do { if (strm->avail_in == 0 ) { error("header error" ); goto gunzip_5; } --strm->avail_in; } while (*strm->next_in++); } strm->next_out = out_buf; strm->avail_out = out_len; rc = zlib_inflateInit2(strm, -MAX_WBITS); if (!flush) { WS(strm)->inflate_state.wsize = 0 ; WS(strm)->inflate_state.window = NULL ; } while (rc == Z_OK) { if (strm->avail_in == 0 ) { len = fill(zbuf, GZIP_IOBUF_SIZE); if (len < 0 ) { rc = -1 ; error("read error" ); break ; } strm->next_in = zbuf; strm->avail_in = len; } rc = zlib_inflate(strm, 0 ); if (flush && strm->next_out > out_buf) { long l = strm->next_out - out_buf; if (l != flush(out_buf, l)) { rc = -1 ; error("write error" ); break ; } strm->next_out = out_buf; strm->avail_out = out_len; } if (rc == Z_STREAM_END) { rc = 0 ; break ; } else if (rc != Z_OK) { error("uncompression error" ); rc = -1 ; } } zlib_inflateEnd(strm); if (pos) *pos = strm->next_in - zbuf+8 ; gunzip_5: free (strm->workspace); gunzip_nomem4: free (strm); gunzip_nomem3: if (!buf) free (zbuf); gunzip_nomem2: if (flush) free (out_buf); gunzip_nomem1: return rc; }
这说明 vmlinux 使用 gzip 算法压缩,通过 binwalk 也可以验证这一点
1 2 3 4 5 6
DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 Microsoft executable, portable (PE) 16820 0x41B4 gzip compressed data, maximum compression, from Unix, last modified: 1970-01-01 00:00:00 (null date) 7381096 0x70A068 Object signature in DER format (PKCS header length: 4, sequence length: 3274 7381235 0x70A0F3 Certificate in DER format (x509 v3), header length: 4, sequence length: 2279
从源码看到 extract_kernel 函数在 head_64.S 中被调用
1 2 3 4 5 6 7 8 9 10 11 12
/* * Do the extraction, and jump to the new kernel.. */ pushq %rsi /* Save the real mode argument */ movq %rsi, %rdi /* real mode address */ leaq boot_heap(%rip), %rsi /* malloc area for uncompression */ leaq input_data(%rip), %rdx /* input_data */ movl $z_input_len, %ecx /* input_len */ movq %rbp, %r8 /* output target address */ movq $z_output_len, %r9 /* decompressed length, end of relocs */ call extract_kernel /* returns kernel location in %rax */ popq %rsi
在 flatkc 也可以找到对应代码
1 2 3 4 5 6 7 8 9 10
push rsi mov rdi, rsi lea rsi, qword_6EC680 lea rdx, gzip_start mov ecx, 6DF3A4h mov r8, rbp /* 0x20000 */ mov r9, 1A34918h call extract_kernel pop rsi jmp rax
根据参数位置,发现 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 2 3
gzip -d vmlinux.gz vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=c1b391e6e7366ccf79173bd1fd93aac1935cf9f6, stripped
接下来要对 vmlinux 进行修改,为了方便定位需要修改的地址,可以先使用 vmlinux-to-elf 将 vmlinux 转换为带有符号信息的 ELF 文件,通过逆向定位到 fgt_verify_initrd 函数地址是 0xFFFFFFFF81709689,考虑这个函数只操作了 initramfs_start 和 initramfs_stop 即 rootfs.gz 的数据,我们选择直接将函数第一条指令改为 ret
1 2 3 4 5
.init.text:FFFFFFFF81709689 retn .init.text:FFFFFFFF8170968A mov rbp, rsp .init.text:FFFFFFFF8170968D push r15 .init.text:FFFFFFFF8170968F push r14 .init.text:FFFFFFFF81709691 push r13
然后把 Patch 好的 vmlinux 重新压缩回 gzip 格式
1
cat vmlinux | gzip -9 > vmlinux.gz
查看新文档和原文档的大小差异
1 2
-rw-rw-r-- 1 admin admin 7205795 3月 15 09:07 vmlinux.gz -rw-rw-r-- 1 admin admin 7205796 3月 15 08:57 vmlinux.gz.ori
新的文档比原文档小一个字节,先将新文档覆盖回 flatkc
1 2
dd if =/dev/zero of=flatkc bs=1 seek=$[0x41B4] count=$[0x6DF3A4] conv=notruncdd if =vmlinux.gz of=flatkc bs=1 seek=$[0x41B4] 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 文件,所以这应该是正常现象。
小工具
。需要注意的是生成的 dec.gz 末尾 256 字节是校验数据,要手动将它们去除。
解压 rootfs.gz 得到文件系统,首先要把 /bin/init 中完整性校验逻辑 Patch 掉,通过逆向分析发现当完整性校验失败时,都会执行 do_halt 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
if ( sub_4557C0() ) do_halt(); if ( system_integrity_check(1LL , "%s()-%d: %s: run_initlevel(SYSINIT)\n\n" ) < 0 ) do_halt(); if ( sub_2BC8AF0(1LL , "%s()-%d: %s: run_initlevel(SYSINIT)\n\n" ) ){ sub_2CC6290(); if ( sub_4543F0("/bin/fips_self_test" ) ) do_halt(); } else { if ( sub_455770() ) do_halt(); }
所以我们直接将 do_halt 的第一条指令改为 ret,跳过这个函数,这样就算校验失败也不会导致系统重启。
然后向系统添加 busybox 和 sh,再将 smartctl 替换成启动 shell 的脚本
1 2 3
# !/bin/sh /bin/busybox sh -i
重新打包 rootfs.gz,替换掉原文件。
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 2 3 4
__int64 forti_security_module_init () { return security_add_hooks(&fortism_func_list, 5LL , aFortiSecurityM); }
这个函数通过
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
unsigned __int64 __fastcall fortism_file_open_part_0 (__int64 a1) { v8 = __readgsqword(0x28 u); result = d_path(a1 + 16 , v7, 511 ); v2 = result; v3 = paths; if ( result <= 0xFFFFFFFFFFFFF000 LL ) { while ( 1 ) { v4 = strlen (*v3); if ( !strncmp (v2, *v3, v4) ) break ; if ( ++v3 == &qword_FFFFFFFF8165F678 ) { result = 0LL ; goto LABEL_5; } } v5 = paths2; while ( 1 ) { v6 = strlen (*v5); result = strncmp (v2, *v5, v6); if ( !result ) break ; if ( ++v5 == paths ) return fortism_file_open_part_0_cold(); } } LABEL_5: if ( v8 != __readgsqword(0x28 u) ) JUMPOUT(0xFFFFFFFF8055054F LL); return result; }
此函数会遍历两个全局数组,数组中包含一些路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/migadmin/fortiguard_resources /node-scripts/report-runner /node-scripts/logs /node-scripts/http-config.json /bin /migadmin /node-scripts /sbin /tools /usr /lib /lib64 /data/rootfs.gz /data/datafs.tar.gz /data/flatkc
当传入的参数匹配以上路径时,函数打印失败信息
try to write readonly file(xxx)
并返回负数值。这一点可以在 SHELL 中验证,例如我们尝试在 /bin 目录下创建一个 testfile 文件,会出现错误信息
另外内核中又存在
ima_file_mmap
函数:
1 2 3 4 5 6 7 8 9 10 11
__int64 __fastcall ima_file_mmap (__int64 file, char prot) { char v3[4 ]; unsigned __int64 v4; v4 = __readgsqword(0x28 u); if ( !file || (prot & 4 ) == 0 ) return 0LL ; security_task_getsecid(__readgsqword(&off_14D80), v3); return fos_process_appraise_constprop_0(file); }
最终调用
fos_process_appraise_constprop_0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
__int64 __fastcall fos_process_appraise_constprop_0 (unsigned __int64 file) { v32 = __readgsqword(0x28 u); inode = *(file + 32 ); v2 = integrity_iint_find(inode); if ( need_ima_check != 2 ) { v3 = v2; v4 = d_path(file + 16 , v30, 511 ); v5 = v4; if ( v4 > 0xFFFFFFFFFFFFF000 LL ) return fos_process_appraise_constprop_0_cold(); if ( v3 ) { v24 = 3 ; v26 = dword_FFFFFFFF8165F6B4; for ( i = 0 ; ; i = 0 ) { do { v29[i] = 0LL ; v29[i + 1 ] = 0LL ; v29[i + 2 ] = 0LL ; v29[i + 3 ] = 0LL ; i += 4 ; } while ( i < 8 ); result = ima_calc_file_hash(file, &v26); if ( !result ) break ; if ( !--v24 ) { if ( result < 0 ) return result; break ; } v27 = 0 ; v28 = 0 ; v26 = dword_FFFFFFFF8165F6B4; } if ( !memcmp (*(v3 + 112 ) + 4LL , v29, *(*(v3 + 112 ) + 1LL )) ) return 0LL ; ima_pr_emerg(3LL , v5); fos_print_hash_isra_0(aNew, v29, *(*(v3 + 112 ) + 1LL ), v12, v13, v14); fos_print_hash_isra_0(aOld, (*(v3 + 112 ) + 4LL ), *(*(v3 + 112 ) + 1LL ), v15, v16, v17); printk(&unk_FFFFFFFF81401B64, *(inode + 80 ), v18, v19, v20, v21); time64_to_tm(*(inode + 104 ), 0LL , v25); v23 = v25[0 ]; printk(&unk_FFFFFFFF81401C48, aModifiedTime, v25[6 ] + 1900 , v25[4 ] + 1 , v25[3 ], v25[2 ]); logmsg = fos_ima_get_logmsg(3LL ); (_fgtlog_vf_text_0[0 ])(36864LL , 255LL , 255LL , 20234LL , 0LL , logmsg, v5, v23); failed: msleep(5000LL ); kernel_restart(0LL ); return 0xFFFFFFF3 LL; } v8 = off_FFFFFFFF8165F6A0[0 ]; if ( strcmp (off_FFFFFFFF8165F6A0[0 ], v4) ) { v8 = off_FFFFFFFF8165F6A8; if ( strcmp (off_FFFFFFFF8165F6A8, v5) ) { ima_pr_emerg(1LL , v5); v9 = 1LL ; failed2: v10 = fos_ima_get_logmsg(v9); (_fgtlog_vf_text_0[0 ])(36864LL , 255LL , 255LL , 20233LL , 0LL , v10, v5); goto failed; } } memset (v31, 0 , sizeof (v31)); snprintf (v31, 511 , aSX_0, v8); if ( fos_verify_pkcs7(file, v5, v31) < 0 ) { if ( sys_security_level != 2 ) { if ( sys_security_level == 1 ) { ima_pr_warning(0 ); v11 = fos_ima_get_logmsg(0LL ); (_fgtlog_vf_text_0[0 ])(36864LL , 255LL , 255LL , 20233LL , 0LL , v11, v5); } else if ( !sys_security_level ) { ima_pr_warning(0 ); return 0LL ; } return 0LL ; } ima_pr_emerg(0LL , v5); v9 = 0LL ; goto failed2; } } return 0LL ; }
简单来说,函数会先判断当前是否开启了 IMA 验证,IMA 可以通过修改
/sys/kernel/security/integrity/fos/fix_to_enforce
值来开启或关闭。在 init 程序中也能找到相关代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
__int64 enforce_fos_integrity () { FILE *v0; FILE *v1; v0 = fopen("/sys/kernel/security/integrity/fos/fix_to_enforce" , "w" ); if ( v0 ) { v1 = v0; fputc('1' , v0); fclose(v1); return 0LL ; } else { sub_23338D0("Failed to enforce FortiOS Security Enforce mode\n" ); return 0xFFFFFFFF LL; } }
不过实际测试发现修改此文件并不能关闭 IMA 验证,原因暂时未知。
继续观察
fos_process_appraise_constprop_0
,当在缓存中找不到待验证文件的 Hash 时,代码判断这个文件是否为
/data/lib/libav.so
或者
/data/lib/libips.so
,如果是二者之一,调用
fos_verify_pkcs7
检查文件签名。
如果检查失败,应该返回错误并重启系统,但这里在失败的情况下仅打印了 log 信息,函数继续向下执行并返回 0,表示验证通过。
/data/lib 目录不在访问控制的范围内,且代码对这两个文件缺乏校验,所以我们尝试自己编写程序覆盖其中之一,看看能否绕过可信执行。
虽然内核 log 中留下了加载非法文件的提示,但程序依然成功执行,验证了前面的分析。
Github
上获取。
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://elixir.bootlin.com
https://zhuanlan.zhihu.com/p/438209486
https://liwugang.github.io/2020/10/25/introduce_lsm.html
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia-plus根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent:
meta: false
pages: false
posts:
title: true
date: true
path: true
text: false
raw: false
content: false
slug: false
updated: false
comments: false
link: false
permalink: false
excerpt: false
categories: false
tags: true