本文由Bruno Oliveira于2024年4月25日发表于IncludeSec的官方网站上。作为IncludeSec的安全研究人员,在他们日常的安全审计和渗透测试工作中,有时需要为客户开发一些模糊测试工具。在安全评估方法中使用模糊测试技术,可以有效地在复杂的现代化软件产品中发现和识别安全漏洞,并为应用程序快速提供高度结构化的输入数据。
当我们的客户要求在手动和传统自动化测试之外进行更全面的工作,以提供额外的分析来发现更复杂的漏洞时,通常会应用此技术。在这篇文章中,我们将跟大家介绍于模糊测试相关的内容,并详细阐述如何通过扩展指令增强基于覆盖引导的模糊测试。
介绍
基于覆盖引导的模糊测试是很多高级模糊测试工具所使用的一种十分有用的功能,例如AFL、libFuzzer和Fuzzilli等。这种功能允许模糊测试工具确认一个输入是否能够在源程序路径中发现新的边或执行分支。在控制流图(CFG)中,一个边连接两个分支。比如说,如果一个辑条件涉及if-else语句,则会有两条边,一条用于if,另一条用于else语句。它是模糊测试过程中的重要组成部分,有助于确定模糊测试工具是否有效地覆盖了目标程序的可执行代码。
引导模糊测试通常会使用基于覆盖引导的模糊测试(CGF)技术,这种技术会使用非常基本的指令来收集所需数据,以识别在模糊测试用力的执行过程中是否命中了新的边或代码块。
这种指令指的是在程序编译过程中添加的代码,这些代码的功能非常丰富,包括软件调试在内。
本文我们将以JerryScript(包含了一个已知且公开的漏洞)为例,介绍如何使用相关技术来扩展Fuzzili的检测以提升漏洞识别效率,并为模糊测试工具提供更有价值的数据以进行进一步的测试。
模糊测试
模糊测试指的是向目标应用程序提供一系列随机输入以尝试触发应用程序非预期行为的过程。根据最新的模糊测试方法,很多模糊测试工具会考虑目标应用程序的多个方面以生成更合适测试场景的输入数据。其中一个考虑因素就是种子,即生成输入数据的源。某些现代软件的结构比较复杂,我们无法通过简单的输入来获取期望的结果。换言之,也就是无法通过简单的输入对目标应用程序产生足够的影响,这样也就难以发现潜在的安全漏洞。
下图中显示的是带有变异策略和代码覆盖功能的模糊测试程序的通用基本结构:
1、选择种子;
2、变异过程需要获取种子作为初始的执行输入;
3、程序执行;
4、触发漏洞,或者...;
5、输入命中了目标程序中的一个新的边,模糊测试工具继续对种子执行变异操作,或者...;
6、输入没有命中新的边,模糊测试工具选择一个新的种子执行变异;
代码覆盖率可以让模糊测试工具在目标应用程序执行过程中发现新的边或代码块,有助于识别输入是否能够抵达目标应用程序的各个部分。
下图所示为Fuzzilli在对样本进行处理和变异时所使用的算法:
Clang
Clang是一款针对C、C++、Objective-C和Objective-C++编程语言的编译器,该编译器属于LLVM项目的一个部分,可以提供比GCC这种传统编译器更强大的功能。
Clang编译器中很重要的一个工具就是数据清洗器(Sanitizer),Sanitizer可以被视作一种安全库或工具,可以通过检查目标代码来自动检测安全漏洞。启用了Sanitizer之后,编译器会自动检查编译后的代码是否存在安全问题。
常见的Sanitizer包括:
1、AddressSanitizer (ASAN);
2、UndefinedBehaviorSanitizer (UBSAN);
3、MemorySanitizer (MSAN);
4、ThreadSanitizer (TSAN);
5、LeakSanitizer (LSAN);
下面给出的Shell代码段显示了如何使用ASAN选项在代码编译过程中跟踪程序计数器:
$ clang -o targetprogram -g -fsanitize=address -fsanitize-coverage=trace-pc-guard targetprogram.c
根据Clang文档的描述,LLVM内置了一个简单的代码覆盖指令,可以向用户定义的函数插入函数调用,并提供了回调的默认实现,从而实现了简单的覆盖率报告和可视化。
比如说,Fuzzilli(Google的JavaScript引擎模糊测试工具)就使用了简单的指令来响应Fuzzilli的进程,具体如下代码段所示:
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { uint32_t index = *guard; __shmem->edges[index / 8] |= 1 << (index % 8); *guard = 0; }
当找到一个新的边时,__sanitizer_cov_trace_pc_guard()函数会持续执行,因此无需任何条件来处理新的边被发现时要做什么。接下来,函数会将共享位图中的__shmem->edges设置为1,Fuzzilli会在执行后对位图进行分析。
其他工具,比如说LLVM-COV,能够静态地捕获代码覆盖率信息,在执行之后提供人类可读的文档。但是,需要高效读取磁盘中文档的模糊测试工具,可能会影响性能。
获取更多的信息
我们可以修改Fuzzilli的指令,并观察__sanitizer_cov_trace_pc_guard()能够给代码覆盖率带来什么其他的东西。下列代码段演示了我们对Fuzzilli指令的部分修改:
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { uint32_t index = *guard; void *PC = __builtin_return_address(0); char PcDescr[1024]; __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr)); printf("guard: %p %x PC %s\n", guard, *guard, PcDescr); __shmem->edges[index / 8] |= 1 << (index % 8); *guard = 0; }
我们已经知道的是,__sanitizer_cov_trace_pc_guard()函数在每次程序命中新的边时便会执行。此时,我们可以利用__builtin_return_address()函数来收集每一个命中的新的边所返回的地址。现在,PC指针已经获取到了返回的地址信息。我们可以利用__sanitizer_symbolize_pc()函数来将地址与符号相关联,从而提供有关执行过程中所使用的源代码文件的更多信息。
大多数模糊测试工具只会使用边的信息来引导模糊测试。然而,正如我们接下来会给大家演示的那样,我们可以使用Sanitizer接口来为安全评估提供更多有价值的信息。
动手实操
在我们的演示过程中,我们将利用一个旧版本的JerryScript JavaScript引擎来创建一个环境,环境信息如下:
1、操作系统(OS):Ubuntu 22.04;
2、目标程序:JerryScript;
3、漏洞:CVE-2023-36109;
环境搭建
我们可以使用下列命令构建JerryScript,首先克隆项目代码库:
$ git clone https://github.com/jerryscript-project/jerryscript.git
切换到JerryScript目录,并校验8ba0d1b6ee5a065a42f3b306771ad8e3c0d819bc commit:
$ git checkout 8ba0d1b6ee5a065a42f3b306771ad8e3c0d819bc
然后应用Fuzziilli库提供的修补程序:
$ cd jerry-main $ wget https://github.com/googleprojectzero/fuzzilli/raw/main/Targets/Jerryscript/Patches/jerryscript.patch $ patch < jerryscript.patch patching file CMakeLists.txt patching file main-fuzzilli.c patching file main-fuzzilli.h patching file main-options.c patching file main-options.h patching file main-unix.c
Fuzziilli修补程序提供的指令文件为jerry-main/main-fuzzilli.c,其中也包含了简单的代码覆盖功能,但这还远远不够。因此 ,我们还需要像之前一样在编译代码之前更新__sanitizer_cov_trace_pc_guard()函数。除此之外,还需要将下列Header添加到jerry-main/main-fuzzilli.c文件中:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { uint32_t index = *guard; if(!index) return; index--; void *PC = __builtin_return_address(0); char PcDescr[1024]; __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr)); printf("guard: %p %x PC %s\n", (void *)guard, *guard, PcDescr); __shmem->edges[index / 8] |= 1 << (index % 8); *guard = 0; }
我们现在更改编译配置并禁用strip,这些符号仅用于识别我们演示中可能存在的易受攻击功能。修改根目录中的CMakeLists.txt文件:
# Strip binary if(ENABLE_STRIP AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug") jerry_add_link_flags(-g) endif()
确保jerry-main/CMakeLists.txt包含了main-fuzzilli.c文件之后,我们就可以准备编译代码并使用Fuzzilli指令完成构建了:
$ python jerryscript/tools/build.py --compile-flag=-fsanitize-coverage=trace-pc-guard --profile=es2015-subset --lto=off --compile-flag=-D_POSIX_C_SOURCE=200809 --compile-flag=-Wno-strict-prototypes --stack-limit=15
如果你安装了Clang,但CMAKE_C_COMPILER_ID却显示 GNU或其他内容的话,你可能构建过程出错了:
$ python tools/build.py --compile-flag=-fsanitize-coverage=trace-pc-guard --profile=es2015-subset --lto=off --compile-flag=-D_POSIX_C_SOURCE=200809 --compile-flag=-Wno-strict-prototypes --stack-limit=15 -- CMAKE_BUILD_TYPE MinSizeRel -- CMAKE_C_COMPILER_ID GNU -- CMAKE_SYSTEM_NAME Linux -- CMAKE_SYSTEM_PROCESSOR x86_64
你可以直接修改CMakeLists.txt文件中的28-42行,通过将USING_GCC 1修改为USING_CLANG 1来强制使用Clang:
# Determining compiler if(CMAKE_C_COMPILER_ID MATCHES "GNU") set(USING_CLANG 1) endif() if(CMAKE_C_COMPILER_ID MATCHES "Clang") set(USING_CLANG 1) endif()
构建出的代码路径为“build/bin/jerry”。
测试执行
首先,我们先禁用掉ASLR:
$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
测试完成后,我们可以通过将值设置为2来重新启用ASLR:
$ echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
现在,我们可以先尝试跟踪源码文件的地址,禁用ASLR将有助于我们在分析过程中不受干扰,且不会影响我们的结果。
现在,我们使用针对CVE-2023-36109的PoC文件来执行JerryScript,并尝试触发漏洞。根据漏洞描述,该漏洞位于jerry-core/ecma/base/ecma-helpers-string.c文件的ecma_stringbuilder_append_raw函数中,具体如下所示:
$ ./build/bin/jerry ./poc.js [...] guard: 0x55e17d12ac88 7bb PC 0x55e17d07ac6b in ecma_string_get_ascii_size ecma-helpers-string.c guard: 0x55e17d12ac84 7ba PC 0x55e17d07acfe in ecma_string_get_ascii_size ecma-helpers-string.c guard: 0x55e17d12ac94 7be PC 0x55e17d07ad46 in ecma_string_get_size (/jerryscript/build/bin/jerry+0x44d46) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) guard: 0x55e17d12e87c 16b8 PC 0x55e17d09dfe1 in ecma_regexp_replace_helper (/jerryscript/build/bin/jerry+0x67fe1) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) guard: 0x55e17d12ae04 81a PC 0x55e17d07bb64 in ecma_stringbuilder_append_raw (/jerryscript/build/bin/jerry+0x45b64) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) guard: 0x55e17d12e890 16bd PC 0x55e17d09e053 in ecma_regexp_replace_helper (/jerryscript/build/bin/jerry+0x68053) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) guard: 0x55e17d12e8b8 16c7 PC 0x55e17d09e0f1 in ecma_regexp_replace_helper (/jerryscript/build/bin/jerry+0x680f1) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) guard: 0x55e17d133508 29db PC 0x55e17d0cc292 in ecma_builtin_replace_substitute (/jerryscript/build/bin/jerry+0x96292) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) guard: 0x55e17d133528 29e3 PC 0x55e17d0cc5bd in ecma_builtin_replace_substitute (/jerryscript/build/bin/jerry+0x965bd) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) guard: 0x55e17d12f078 18b7 PC 0x55e17d040a78 in jmem_heap_realloc_block (/jerryscript/build/bin/jerry+0xaa78) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) guard: 0x55e17d12f088 18bb PC 0x55e17d040ab4 in jmem_heap_realloc_block (/jerryscript/build/bin/jerry+0xaab4) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) guard: 0x55e17d12f08c 18bc PC 0x55e17d040c26 in jmem_heap_realloc_block (/jerryscript/build/bin/jerry+0xac26) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) guard: 0x55e17d12f094 18be PC 0x55e17d040ca3 in jmem_heap_realloc_block (/jerryscript/build/bin/jerry+0xaca3) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) UndefinedBehaviorSanitizer:DEADLYSIGNAL ==27636==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x55e27da7950c (pc 0x7fe341fa092b bp 0x000000000000 sp 0x7ffc77634f18 T27636) ==27636==The signal is caused by a READ memory access. #0 0x7fe341fa092b string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:513 #1 0x55e17d0cc3bb in ecma_builtin_replace_substitute (/jerryscript/build/bin/jerry+0x963bb) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) #2 0x55e17d09e103 in ecma_regexp_replace_helper (/jerryscript/build/bin/jerry+0x68103) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) #3 0x55e17d084a23 in ecma_builtin_dispatch_call (/jerryscript/build/bin/jerry+0x4ea23) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) #4 0x55e17d090ddc in ecma_op_function_call_native ecma-function-object.c #5 0x55e17d0909c1 in ecma_op_function_call (/jerryscript/build/bin/jerry+0x5a9c1) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) #6 0x55e17d0d4743 in ecma_builtin_string_prototype_object_replace_helper ecma-builtin-string-prototype.c #7 0x55e17d084a23 in ecma_builtin_dispatch_call (/jerryscript/build/bin/jerry+0x4ea23) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) #8 0x55e17d090ddc in ecma_op_function_call_native ecma-function-object.c #9 0x55e17d0909c1 in ecma_op_function_call (/jerryscript/build/bin/jerry+0x5a9c1) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) #10 0x55e17d0b929f in vm_execute (/jerryscript/build/bin/jerry+0x8329f) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) #11 0x55e17d0b8d4a in vm_run (/jerryscript/build/bin/jerry+0x82d4a) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) #12 0x55e17d0b8dd0 in vm_run_global (/jerryscript/build/bin/jerry+0x82dd0) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) #13 0x55e17d06d4a5 in jerry_run (/jerryscript/build/bin/jerry+0x374a5) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) #14 0x55e17d069e32 in main (/jerryscript/build/bin/jerry+0x33e32) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) #15 0x7fe341e29d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16 #16 0x7fe341e29e3f in __libc_start_main csu/../csu/libc-start.c:392:3 #17 0x55e17d0412d4 in _start (/jerryscript/build/bin/jerry+0xb2d4) (BuildId: 9588e1efabff4190fd492d05d3710c7810323407) UndefinedBehaviorSanitizer can not provide additional info. SUMMARY: UndefinedBehaviorSanitizer: SEGV string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:513 ==27636==ABORTING
通过使用这种技术,我们可以识别出ecma_stringbuilder_append_raw()中漏洞存在根本原因的栈地址。
如果我们仅仅依赖于Sanitizer来检测堆栈记录的话,我们将无法在输出中看到存在漏洞的函数名称:
$ ./build/bin/jerry ./poc.js [COV] no shared memory bitmap available, skipping [COV] edge counters initialized. Shared memory: (null) with 14587 edges UndefinedBehaviorSanitizer:DEADLYSIGNAL ==54331==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x5622ae01350c (pc 0x7fc1925a092b bp 0x000000000000 sp 0x7ffed516b838 T54331) ==54331==The signal is caused by a READ memory access. #0 0x7fc1925a092b string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:513 #1 0x5621ad66636b in ecma_builtin_replace_substitute (/jerryscript/build/bin/jerry+0x9636b) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) #2 0x5621ad6380b3 in ecma_regexp_replace_helper (/jerryscript/build/bin/jerry+0x680b3) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) #3 0x5621ad61e9d3 in ecma_builtin_dispatch_call (/jerryscript/build/bin/jerry+0x4e9d3) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) #4 0x5621ad62ad8c in ecma_op_function_call_native ecma-function-object.c #5 0x5621ad62a971 in ecma_op_function_call (/jerryscript/build/bin/jerry+0x5a971) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) #6 0x5621ad66e6f3 in ecma_builtin_string_prototype_object_replace_helper ecma-builtin-string-prototype.c #7 0x5621ad61e9d3 in ecma_builtin_dispatch_call (/jerryscript/build/bin/jerry+0x4e9d3) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) #8 0x5621ad62ad8c in ecma_op_function_call_native ecma-function-object.c #9 0x5621ad62a971 in ecma_op_function_call (/jerryscript/build/bin/jerry+0x5a971) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) #10 0x5621ad65324f in vm_execute (/jerryscript/build/bin/jerry+0x8324f) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) #11 0x5621ad652cfa in vm_run (/jerryscript/build/bin/jerry+0x82cfa) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) #12 0x5621ad652d80 in vm_run_global (/jerryscript/build/bin/jerry+0x82d80) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) #13 0x5621ad607455 in jerry_run (/jerryscript/build/bin/jerry+0x37455) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) #14 0x5621ad603e32 in main (/jerryscript/build/bin/jerry+0x33e32) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) #15 0x7fc192429d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16 #16 0x7fc192429e3f in __libc_start_main csu/../csu/libc-start.c:392:3 #17 0x5621ad5db2d4 in _start (/jerryscript/build/bin/jerry+0xb2d4) (BuildId: 15a3c1cd9721e9f1b4e15fade2028ddca6dc542a) UndefinedBehaviorSanitizer can not provide additional info. SUMMARY: UndefinedBehaviorSanitizer: SEGV string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:513 ==54331==ABORTING
总结
在这篇文章中,我们演示了如何通过扩展Fuzzilli的指令来对目标应用程序的栈进行实施跟踪,以更好地了解返回地址信息以及相关的源代码文件信息,从而给模糊测试工具提供更多的路径,最终产生更多有价值的测试结果。
参考资料
https://clang.llvm.org/docs/SanitizerCoverage.html
参考链接
https://blog.includesecurity.com/2024/04/coverage-guided-fuzzing-extending-instrumentation/