介绍
本文是从Tinylnst的了解学习再到Jackalope的使用再到首次编写Fuzzer的初探话不多说进入正题。

Tinylnst

TinyInst是一个轻量级动态检测库(插桩工具用于执行时代码覆盖率分析)可用于检测流程中选定的模块同时让流程的其余部分在本机运行。
跟传统的DynamoRIO相比PJ0团队对于TinyInst的定义主打的就是轻量级,而且TinyInst支持很多种架构Windows (x86和x64)、macOS (x64和arm64)、Linux (x64和arm64) 和 Android (ARM64)。
通过直接安装vs2019然后使用官方提供的命令一把梭即可编译。这里官方还说64位编译的可以32位目标上工作因此可能没有必要编译32位

记得安装python3
mkdir build
cd build
cmake -G "Visual Studio 16 2019" -A x64 ..
cmake --build . --config Release

在TinyInst框架中其代码主要分为三个大类分别是debugger类,Tinylnst类还有LiteCov类。而其中三个类的关系分别是debugger作为最底层的调试处理,而Tinylnst是继承与debugger类而LiteCov类又继承于Tinylnst类。TinyInst也提供了测试用例,其在Windows运行命令如下
litecov.exe -instrument_module notepad.exe -coverage_file coverage.txt -- notepad.exe


对于TinyInst的设计理念此文不叙述太多因为github项目下pj0给了一些核心观念,我们还是从其中的函数出发看看是怎么进行代码处理的然后才能更好的通过TinyInst去使用Jackalope-fuzz工具。

Debugger

此处来介绍一下此类中几个关键逻辑的函数首先来看一下debugger.h文件中的定义在此文件中就是定义了Debugger的类
在下方它定义了命令行的选项这代表可以根据自己的需求定期自己的命令行选项,要运行和控制程序的话则通过函数

DebuggerStatus Run(int argc, char **argv, uint32_t timeout);

debugger类还提供了Kill和Continue功能,这是为了当run和attach在目标进程仍然存在的时候就可以通过此函数来进行终止或者继续调试处理。

在往下我们可以看到一些成员函数和成员变量,用于处理调试过程中的各种事件和操作


Debugger::OnModuleLoaded函数是当模块被加载的时候调用的,在下方还可以看见 AddBreakpoint(target_address, BREAKPOINT_TARGET)函数的调用
因为其内部有一处判断如果指定了target_module和target_method而且这个module就是target_module
就去通过GetTargetAddress函数获取目标方法的地址并通过AddBreakpoint函数在该地址添加BREAKPOINT_TARGET类型的断点。

Debugger::OnProcessCreated函数是当目标进程被创建或附加时调用其中还考虑包含了当附加到目标进程主模块的加载逻辑。当附加到进程下方则是对主模块调用并使用函数OnModuleLoaded。


Debugger::HandleExceptionInternal函数是在目标程序发现异常的时候调用,它会根据提供的 EXCEPTION_RECORD创建一个异常对象,然后检测异常是否为断点异常因为这可能是调试器触发的断点导致的。然后通过Start或Continue来判断调试器该继续执行,此函数还检测了特定的异常类型:对于访问冲突异常,它检查是否定义了特定的目标函数,以及异常地址是否与预定义的结束地址匹配。如果是,它处理目标结束并返回 DEBUGGER_TARGET_END。否则,将其视为崩溃(DEBUGGER_CRASHED)。
而对于其他特定异常,如非法指令栈溢出堆损坏等,它标记调试器已崩溃。
最后对于任何其他未处理的异常,如果设置了 trace_debug_events,它会打印一条消息,然后返回 DEBUGGER_CONTINUE。最后,它返回适当的调试器状态。


Debugger::HandleTargetEnded此函数作用恢复目标方法地址处添加的BREAKPOINT_TARGET类型的断点
它获取目标方法的返回值,保存在 target_return_value 中,如果处于循环模式(loop_mode)则进行参数和执行上下文的恢复;如果不处于循环模式则恢复目标函数的返回地址,并且添加目标入口断点。

Debugger::HandleDllLoadInternal此函数在Dll加载的时候调用的主要是通过OnModuleLoaded函数传递加载的dll基地址和基本信息。


Debugger::DebugLoop此函数通过存储调试器的返回状态还有判断目标进程是否存活等信息还进行循环处理,其主循环中的WaitForDebugEvent是用于等待调试事件的发生,循环会根据传入的 timeout 来判断是否超时。如果成功等待到了调试事件就设置 dbg_continue_needed,如果超时返回 DEBUGGER_HANGED 表示调试器挂起。在处理调试事件之前将dbg_continue_status设置为DBG_CONTINUE。
根据调试事件的类型进行处理,对于异常调试事件,调用 HandleExceptionInternal 处理异常,并根据返回值判断是否需要继续调试或者目标进程已崩溃。
对于创建线程、创建进程、线程退出、进程退出、加载 DLL、卸载 DLL 等调试事件,分别调用相应的处理函数。对于其他类型的调试事件不做处理。处理完调试事件后调用 ContinueDebugEvent继续执行调试事件并继续主循环。当目标进程退出时打印相应的信息然后退出循环,并返回 DEBUGGER_PROCESS_EXIT表示目标进程已退出。

还有一些Debugger相关的命令参数在运行的时候可以添加像-trace_debug_events 打印调试器事件(加载的模块,异常等)-trace_basic_blocks 在执行基本块时打印它们,-trace_module_entries 将所有列表打印到检测代码中。-full_address_map 维护一个指令级的地址从检测代码到原始代码的地址。

TinyInst

TinyInst类是涉及到了插桩的实现,其基于自定义调试器而调试器负责监视目标进程的事件如加载模块、命中断点、触发异常。通过指定目标的方法调试器还可以进行断点和持久化处理。
最初被检测的模块中的所有可执行区域都会被标记为不可执行但会保留其他读写权限,每当控制流到达被检测的模块时就会触发异常然后被调试器捕获和处理。
而TinyInst还会在原始模块地址范围的2GB内分配一个可执行内存区域这里就是模块的插桩/重写代码将被放置的地方,然后所有使用[rip+offset]形式寻址的指令都被[rip+fixed_offset]取代。
TinyInst这个类提供了一系列功能,包括初始化、启用/禁用插桩、处理调试事件、处理异常、模块加载和卸载、间接跳转插桩等。我们到大致看一下其中的一些函数
TinyInst::FixCrossModuleLinks该函数用于修复指定模块相关的所有跨模块链接。
它遍历所有跨模块链接,找到目标模块相关的链接,并调用FixCrossModuleLink函数来修复链接。
而TinyInst::FixCrossModuleLink函数用于修复跨模块链接。它通过指向CrossModuleLink结构的指针作为参数,并在相关模块上执行操作以修复链接。


TinyInst::ClearCrossModuleLinks该函数用于清除指定模块相关的所有跨模块链接。它遍历所有跨模块链接,找到目标模块相关的链接,并将其从列表中移除。

TinyInst::InitGlobalJumptable此函数用于初始化全局跳转表并获取当前指令地址,以便在代码插桩和跳转表的使用中进行有效的管理和调度。
通过记录插桩代码的大小,设置全局跳转表的偏移量,设置全局间接跳转新目标地址的偏移量,在全局间接跳转新目标地址的位置写入全局跳转表的地址,调用 Breakpoint 函数在指定模块上设置断点,提交新插桩代码的变化来实现。

而在上述的TinyInst::CommitCode函数和TinyInst::WritePointer函数作用分别是,首先是TinyInst::CommitCode该函数用于提交已写入的插桩代码,此函数首先检查插桩代码是否已被映射到远程进程中,如果没有,则直接返回。
接着它调用 RemoteWrite 函数,将本地的插桩代码数据从指定的起始偏移量开始写入到远程进程中。这个函数的目的是将在本地内存中修改的插桩代码数据提交到远程进程中。
TinyInst::WritePointer 该函数将一个指针值写入到指定模块的插桩代码中。函数检查是否有足够的空间来存储指针值。然后,根据指针的大小(child_ptr_size),将指定的值转换为相应的整数类型(uint64_t 或 uint32_t)并将其写入到插桩代码的当前位置。函数更新已分配的插桩代码大小以便下一次写入时不会覆盖已有的数据。

而在上图中还有TinyInst::WriteCode函数,它是用于写入插桩代码到指定模块中的指定偏移量。
它接受一个指向 ModuleInfo 结构的指针 module、一个指向插桩代码数据的指针 data 和一个大小 size 作为参数。函数首先检查是否有足够的空间来存储新写入的代码。然后,函数使用 memcpy 函数将指定大小的插桩代码数据从给定的数据指针复制到指定模块的插桩代码中,并更新已分配的插桩代码大小。
此时再来回过头看一下上述提供的命令参数
litecov.exe -instrument_module notepad.exe -coverage_file coverage.txt -- notepad.exe
这里调试进程就是指的litecov.exe,目标进程就是指的notepad.exe。插桩的代码是先写到litecov.exe的地址空间(TinyInst::WriteCode实现的)再写到notepad.exe的地址空间(TinyInst::CommitCode实现的)。
TinyInst::HandleBreakpoint此函数实现了处理各种类型断点的功能,像基本块追踪信息指示正在执行的基本块的原始地址和插桩后的地址;还会遍历钩子列表,对每个钩子调用其 HandleBreakpoint 方法,以处理特定类型的断点。函数调用 HandleIndirectJMPBreakpoint 来处理间接跳转的断点。

TinyInst::TranslateBasicBlockRecursive 函数递归地翻译指定地址处的基本块及其后续基本块,记录当前模块的插桩代码大小调用 TranslateBasicBlock 函数来翻译指定地址处的基本块。并调用汇编器的 FixOffset 方法来修正跳转指令的偏移量,计算翻译后的代码大小 code_size_after。
最后提交所有更改的代码调用 CommitCode 方法来提交从 code_size_before 开始的插桩代码

TinyInst::TryExecuteInstrumented函数负责尝试执行插桩后的代码,检查给定地址获取模块信息,并且使用转换后的地址更新程序计数器 (PC) 寄存器以便执行插桩后的代码。


TinyInst::InstrumentModule此函数是核心的插桩函数;它检查插桩是否被禁用;如果启用了持久化插桩数据,并且模块已经被插桩过,那么直接重用之前的插桩数据。
如果模块尚未被插桩过,首先通过调用 ExtractCodeRanges函数提取模块的代码范围和大小。
分配用于插桩代码的本地缓冲区。如果间接插桩模式为全局或自动则还需额外分配大小为 child_ptr_size * JUMPTABLE_SIZE的跳转表空间。
在本地分配的缓冲区附近或在指定模块的地址范围内分配远程内存用于存储插桩代码。如果是ARM64架构,使用 RemoteAllocate 函数分配可读可执行的内存,否则使用 RemoteAllocateNear 函数。
如果间接插桩模式为全局或自动,则初始化全局跳转表。这个跳转表是一组指针,用于存储间接跳转的目标地址。将模块的 instrumented标志设置为 true,表示该模块已被插桩。修复跨模块链接,确保所有相关的跳转地址被正确修正。调用 OnModuleInstrumented 函数,通知任何已注册的监听器该模块已被插桩。
如果启用了模块入口点的修正还会调用 PatchModuleEntries 函数对模块的入口点进行修正。

所以TinyLnst的插桩主要是进行指令替换和重写的操作,而且插桩的模块都会插桩被命中的基本块;并且通过实现全局跳转表来进行操作且对于直接调用/间接调用都有对应的控制流的处理。
最后的LiteCov类就主要是关于代码覆盖率的处理了:
通过OnModuleInstrumented函数在模块插桩后调用,初始化模块的覆盖率数据并分配远程覆盖率缓冲区。又使用InstrumentBasicBlock和InstrumentEdge函数用于在指定的基本块或边上插入覆盖率插桩。其EmitCoverageInstrumentation函数主要用于在指定的地址插入覆盖率插桩指令。随便使用CollectCoverage和GetCoverage进行收集覆盖率;然后通过IgnoreCoverage;HasNewCoverage;ClearCoverage函数进行覆盖率数据的处理。
通过CompareCoverage函数比较两个覆盖率数据集,并返回它们的差异。
LiteCov类主要就是采取轻量级的代码去实现了覆盖率检测包括插桩、收集、处理和管理覆盖率数据等操作。
大致了解TinyLnst其内部的一些设计理念然后就进入到正题Jackalope的操作了。

Jackalope

Jackalope是一个可定制的、分布式的、覆盖引导的模糊器,专用于 Windows/macOS 的黑盒 fuzz 开源工具,而且Jackalope默认附带了上述中介绍的插桩工具TinyLnst还有语法变异引擎。在windows平台的编译命令如下,可以看见其中也是先把TinyLnst加进来的。

cd Jackalope
git clone --recurse-submodules https://github.com/googleprojectzero/TinyInst.git
mkdir build
cd build
cmake -G "Visual Studio 16 2019" -A x64 ..
cmake --build . --config Release

编译完成之后它会生成fuzzer.exe还有一个test.exe;其中test.exe是Jackalope源码中提供的test.cpp测试代码会自动编译生成用来进行测试用例。

以test.exe目标程序为例在windows上运行的命令如下所示:
fuzzer.exe -in in -out out -t 1000 -delivery shmem -instrument_module test.exe -target_module test.exe -target_method fuzz -nargs 1 -iterations 10000 -persist -loop -cmp_coverage -- test.exe -m @@

-in代表着输入目录也就是样本集(Jackalope并没有提供优秀的变异种子生成算法,所以样本集的深度要依靠与用户自身的修改获取)-out就是输出目录了。
-t是超时的示例时间;-delivery 样品传送机制使用。如果是文件则每个示例都作为文件输出,目标参数中的“@@”将被替换为文件的路径。如果是shmem,则fuzzer将创建共享内存,并用共享内存的名称替换目标参数中的“@@”。在这种情况下默认为file。-nthreads 就是fuzz的线程数 -instrument_module 就是目标模块收集覆盖率;-cmp_coverage 代表使用比较覆盖
再来介绍一下Jackalope中主要功能类像:Fuzzer是用来跟踪语料库和覆盖范围处理高级任务的;像其中的Fuzzer::RunSampleAndGetCoverage函数就是负责运行单个样本并获取覆盖率信息;样本被传递给目标程序执行;而如果目标程序执行过程中发生了崩溃或会将样本保存到对应的文件中,并记录崩溃的信息。

Mutator主要工作是调用样本定义的上限处理突变信息的比如-SpliceMutator::Mutate函数称为Splice Mutator 的突变器(Mutator)用于对输入样本进行修改。这个突变器的主要功能是将一个样本的部分内容替换为另一个样本的相应部分内容生成新的样本。

Instrumentation-是用来处理目标的运行和收集覆盖fuzzer还附带了一个使用tinyist的Instrumentation

SampleDelivery类负责处理将样本传递到目标;它在其中定义了两个类用于样本交付的不同方法
FileSampleDelivery类用于将样本保存到文件中;而SHMSampleDelivery 类用于将样本传递给共享内存它的构造函数接受共享内存的名称和大小。

Jackalope还提供了自定义的fuzzer编写;通过BinaryFuzzer 的类它继承自Fuzzer 类;BinaryFuzzer 类重写了 Fuzzer 类中的两个成员函数并提供了自己的实现。再往下就是CreateMutator函数;此函数用于创建一个突变器对象,用于修改输入样本以生成新的样本。函数使用 override 关键字进行了重写,意味着它覆盖了基类 Fuzzer 中的同名函数。
TrackHotOffsets 函数:这个函数用于指示是否追踪热点偏移量;函数使用 override 关键字进行了重写,意味着它覆盖了基类 Fuzzer 中的同名函数。
除此之外还有CreateInstrumentation() - 可以被重写,以便fuzzer使用自定义的检测。CreatePRNG() -可以被重写以使用自定义PRNG。

然后我编写了一个简单的fuzzer-gdi32库的用例虽然还有些许bug但是也能跑了并且产生crash。


fuzzer.exe -in inGDI -out outGDI -t 1000 -delivery shmem -instrument_module gdiplus.dll -target_module ConsoleApplication9.exe -target_offset 0x1170 -target_method fuzz -nargs 1 -iterations 10000 -persist -loop -cmp_coverage -- ConsoleApplication9.exe -m @@\

从网上找了一些语法料库当语料库的种子


跑了一段时间就生成了一些crash剩下的就是要分析了。

Jackalope也可以用来编写去fuzz-mpengine.dll像下文中的作者也是通过Jackalope编写harness去fuzzdefender的模块,通过调用函数去触发此模块然后找一些语料库不断进行优化处理等等;网上关于Jackalope玩法的文章远没有winafl多但Jackalope在某些方便确实很便捷其效率也不算很低但还需继续多尝试多个模块的fuzz效果哇

参考
https://github.com/googleprojectzero/TinyInst
https://www.anquanke.com/post/id/234925
https://github.com/googleprojectzero/Jackalope

点击收藏 | 0 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖