热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

TinyInst动态插桩工具原理分析

 作者:houjingyi前言这篇文章主要是分析一下project zero大佬开源的一个插桩的库TinyInst:https://github.com/googleprojectzero/TinyI

 

作者:houjingyi

前言

这篇文章主要是分析一下project zero大佬开源的一个插桩的库TinyInst:
https://github.com/googleprojectzero/TinyInst
同时大佬也开源了基于该库的fuzzer:
https://github.com/googleprojectzero/Jackalope
TinyInst和Jackalope都支持Windows和macOS。不过只支持Intel平台,因为具体实现依赖于Intel的xed解码/编码器。
也有人把这个和WinAFL结合的:
https://github.com/linhlhq/TinyAFL

看他们twitter都用基于TinyInst的fuzzer挖到了一些Windows和macOS上的漏洞,看来还是值得学习这里面插桩的原理的。

TinyInst中litecov类继承自TinyInst类,而TinyInst类继承自Debugger类。这就是最关键的三个类了。下面我们以Windows环境为例进行分析。
在下面这张我画的流程图中,黑色的是Debugger类中实现的函数,黄色的是TinyInst类中实现的函数,红色的是litecov类中实现的函数。看上去很复杂,实际上这里面的代码逻辑还是比较清楚的。比如OnProcessExit在Debugger类中是一个虚方法,在TinyInst类和litecov类中才有具体的实现,对于这样的情况为了简化在图中就没有黑色的OnProcessExit了。

我们接下来分析的顺序也是debugger.cpp-tinyinst.cpp-litecov.cpp,每分析一个新类之前都会先简单介绍一下整体的功能,然后会详细解释流程图中涉及到的函数。当然并不是每个函数都画在流程图里面了,只挑了一些关键的函数。所以阅读文章时最好还是自己调试阅读源代码。

 

debugger.cpp

我们先看流程图中黑色的函数。熟悉Windows系统调试相关知识的话应该很快能明白,就是实现了一个简单的调试器。创建进程之后在DebugLoop中根据不同的调试事件进行不同的处理。在目标方法地址处设置一个断点,程序运行到目标方法时触发这个断点产生异常被调试器捕获,此时保存参数和返回地址,修改返回地址使得目标方法返回时产生一个异常又被调试器捕获,此时恢复参数和返回地址,恢复之前设置的断点,以此循环。
下面是图中函数的注释。

Debugger::OnModuleLoaded
当模块被加载时会调用该函数,如果指定了target_module和target_method/target_offset并且这个module就是target_module那么获取目标方法的地址并在该地址添加BREAKPOINT_TARGET类型的断点
Debugger::HandleTargetReachedInternal
当到达目标方法时会调用该函数,保存参数和返回地址,并将返回地址修改为PERSIST_END_EXCEPTION,当目标方法结束时会产生一个异常
Debugger::HandleTargetEnded
当目标方法返回时会调用该函数,恢复参数和返回地址,恢复在目标方法的地址处添加的BREAKPOINT_TARGET类型的断点
Debugger::OnEntrypoint
当到达程序入口点时会调用该函数,对所有加载的模块都调用Debugger::OnModuleLoaded,并将child_entrypoint_reached标记为true
Debugger::HandleDebuggerBreakpoint
当遇到断点的时候会调用该函数,首先从断点列表breakpoints中删除该断点,然后恢复断点处原来的值和指令地址寄存器,根据断点的类型BREAKPOINT_ENTRYPOINT或BREAKPOINT_TARGET调用OnEntrypoint或HandleTargetReachedInternal,返回断点类型
Debugger::HandleDllLoadInternal
当模块被加载时会调用该函数,如果child_entrypoint_reached为true调用Debugger::OnModuleLoad
Debugger::OnProcessCreated
如果是附加到一个进程的情况那么直接对主模块调用Debugger::OnModuleLoaded,否则在模块的入口点添加一个BREAKPOINT_ENTRYPOINT类型的断点
Debugger::HandleExceptionInternal
处理EXCEPTION_DEBUG_EVENT调试事件。
对于断点的情况调用Debugger:: HandleDebuggerBreakpoint函数,返回DEBUGGER_TARGET_START或者DEBUGGER_CONTINUE;
对于 ACCESS_VIOLATION的情况如果指定了target_module和target_method/target_offset并且ExceptionAddress是PERSIST_END_EXCEPTION说明这是目标方法返回产生的异常,调用Debugger::HandleTargetEnded函数,返回DEBUGGER_TARGET_END;
对于其它情况返回DEBUGGER_CRASHED
Debugger::DebugLoop
循环,根据不同的调试事件进行不同的处理

 

tinyinst.cpp

接下来我们来看流程图中黄色的函数,这里涉及到的就是插桩具体的一些实现。
总的来说,加载要插桩的模块后:
1.模块中的所有可执行区域都被标记为不可执行,同时保留了其他权限(读/写)。这会导致每当控制流到达模块时都会产生异常并被调试器捕获并处理。
2.在原始模块地址范围的2GB之内分配了一个可执行的内存区域放置已插桩的模块代码。将所有以[rip+offset]形式寻址的指令替换为[rip+fixed_offset]。
无论何时进入被插桩的模块都会插桩被命中的基本块(TinyInst::TranslateBasicBlock),以及可以通过递归遵循条件分支以及直接调用和跳转到达的所有基本块(TinyInst::TranslateBasicBlockRecursive)。
对于直接调用/跳转:都会访问已插桩代码中的正确位置
对于间接调用/跳转:都会访问其原始代码位置,这将导致异常,调试器会rip替换为插桩代码中的相应位置(TinyInst::TryExecuteInstrumented)
目标位于已插桩模块中的每个间接调用/跳转上都会引起异常。由于异常处理的速度很慢,因此具有很多间接调用/跳转的目标(例如C++中的虚方法,函数指针)将很慢。
TinyInst中支持两种转换间接调用/跳转的方法:
1.本地列表
(TinyInst::InstrumentLocalIndirect)
2.全局hash表(默认)
(TinyInst::InitGlobalJumptable、TinyInst::InstrumentGlobalIndirect)
不管是本地列表还是全局hash表,原理都是让间接调用/跳转去跳转到一个列表的开头。列表每一项都包含一对(original_target,translation_target)。测试跳转/调用目标是否与original_target相匹配,如果匹配,控制流将转到translation_target。否则跳到下一项。如果到达列表的末尾,则意味着之前没有看到调用/跳转的目标。这将导致调试器捕获到一个断点(TinyInst::HandleBreakpoint),此时会创建一个新项并将其插入列表中(TinyInst::AddTranslatedJump)。
使用本地列表的情况:
插桩间接调用/跳转指令之后:

调试器捕获到一个断点向列表中加入一个新项:

使用全局hash表的情况:
插桩间接调用/跳转指令之后:

调试器捕获到一个断点向列表中加入一个新项:

下面几个变量单独说一下:

size_t instrumented_code_size;
//插桩区域总大小
size_t instrumented_code_allocated;
//已经占用的大小
char *instrumented_code_local;
//指向调试进程插桩区域起始位置的指针
char *instrumented_code_remote;
//指向目标进程插桩区域起始位置的指针

比如我们用github上给出的示例程序对notepad.exe进行插桩:
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::InitGlobalJumptable
大小为JUMPTABLE_SIZE的数组,其中每项最初都指向一个断点。当检测到新的间接调用/跳转时将触发断点,然后会在此哈希表中添加新项
TinyInst::HandleBreakpoint
调用TinyInst::HandleIndirectJMPBreakpoint
TinyInst::HandleIndirectJMPBreakpoint
该地址如果指向TinyInst::InitGlobalJumptable中添加的断点说明是全局跳转;如果能在br_indirect_newtarget_list中找到说明是本地跳转。调用TinyInst::AddTranslatedJump并从 TinyInst::AddTranslatedJump创建的代码处开始执行
TinyInst::AddTranslatedJump
向列表中插入一对新((original_target,translation_target)
TinyInst::InstrumentRet
对ret指令插桩,最后rax中保存返回地址

mov [rsp + rax_offset], rax
//保存rax
mov rax, [rsp]
mov [rsp + ret_offset], rax
//保存返回地址
lea rsp, [rsp + ret_pop]
//栈对齐
push f
mov rax, [rsp + rax_offset]
push rax
mov rax, [rsp + ret_offset]
调用TinyInst::InstrumentIndirect

TinyInst::InstrumentIndirect
调用TinyInst::InstrumentGlobalIndirect或者TinyInst::InstrumentLocalIndirect
TinyInst::InstrumentGlobalIndirect
使用全局跳转表转换间接jump/call xxx
TinyInst::InstrumentLocalIndirect
使用本地跳转表转换间接jump/call xxx
TinyInst::TranslateBasicBlock
首先保存原始偏移和插桩后的偏移,调用LiteCov::InstrumentBasicBlock进行基本块插桩,然后调用LiteCov::InstrumentInstruction进行指令级插桩。
1.如果基本块的最后一条指令是ret指令则调用InstrumentRet插桩;
2.如果基本块的最后一条指令是条件跳转指令,则进行如下所示的插桩:
插桩前:

// j* target_address

插桩后:

// j* label
//
// jmp continue_address
// label:
//
// jmp target_address

3.如果基本块的最后一条指令是非条件跳转指令并且是jmp address(不是jmp [address]这样的指令),则改成jmp fixed_address;如果基本块的最后一条指令是非条件跳转指令并且不是jmp address,则调用InstrumentIndirect插桩;
4.如果基本块的最后一条指令是call指令并且是call address(不是call [address]这样的指令),则进行如下所示的插桩:
插桩前:

// call target_address

插桩后:

// call label
// jmp return_address
// label:
// jmp target_address

如果基本块的最后一条指令是call指令并且不是call address,则调用InstrumentIndirect插桩。
TinyInst::TranslateBasicBlockRecursive
从起始地址开始任何插桩过程中遇到的基本块都加入到队列循环调用TranslateBasicBlock进行插桩
TinyInst::OnCrashed
打印出crash时的信息,前后的代码,所在的模块等等
TinyInst::GetTranslatedAddress
返回给定地址对应的插桩模块中的地址
TinyInst::TryExecuteInstrumented
检查给定地址是否能在插桩模块中找到,如果是则调用LiteCov::OnModuleEntered,将rip设为其在插桩模块中的地址
TinyInst::InstrumentModule/TinyInst::InstrumentAllLoadedModules
对模块进行插桩,首先将模块中所有可执行的区域标记为不可执行并拷贝这些代码,然后为插桩的代码分配地址空间,调用TinyInst::InitGlobalJumptable初始化全局跳转表,最后调用LiteCov::OnModuleInstrumented
TinyInst::OnInstrumentModuleLoaded
调用TinyInst::InstrumentModule
TinyInst::OnModuleLoaded
如果需要插桩该模块则调用TinyInst::OnInstrumentModuleLoaded
TinyInst::OnModuleUnloaded
清除插桩信息
TinyInst::OnTargetMethodReached
调用TinyInst::InstrumentAllLoadedModules
TinyInst::OnEntrypoint
调用TinyInst::InstrumentAllLoadedModules
TinyInst::OnException
如果是断点导致的异常调用TinyInst::HandleBreakpoint;如果是ACCESS_VIOLATION这可能是因为要执行的代码在插桩的代码区域,调用TinyInst::TryExecuteInstrumented
TinyInst::OnProcessExit
清理并调用LiteCov::OnModuleUninstrumented

 

litecov.cpp

最后我们来看流程图中红色的函数,这里终于涉及到了关于代码覆盖率处理。我们先快速过一下用到的x86_helpers.c中的函数以便之后更好理解litecov.cpp中的代码。

GetUnusedRegister
返回AX/EAX/RAX
Get8BitRegister
返回寄存器的低8位,例如对于AX/EAX/RAX都返回AL
GetFullSizeRegister
和Get8BitRegister相反,RAX/EAX/AX/AH/AL都返回RAX(64位),EAX/AX/AH/AL都返回EAX(32位)
Push
生成push指令
Pop
生成pop指令
CopyOperandFromInstruction
进行指令级插桩时如果cmp指令的第一个操作数不是寄存器(cmp DWORD PTR [ebp-0x14], eax)那么需要一条mov指令将第一个操作数移到寄存器中(mov ecx, DWORD PTR [ebp-0x14]),该函数将cmp指令的第一个操作数拷贝到mov指令的第二个操作数
Mov
生成mov指令
Lzcnt
生成lzcnt指令
CmpImm8
生成cmp指令
GetInstructionLength
获取指令长度
FixRipDisplacement
修复[rip+displacement]这样的指令的偏移

下面几个变量单独说一下:

unsigned char *coverage_buffer_remote;
//指向coverage buffer起始位置的指针
size_t coverage_buffer_size;
//coverage buffer总大小
size_t coverage_buffer_next;
//coverage buffer已经占用的大小
std::set collected_coverage;
//收集的coverage的集合
std::set ignore_coverage;
//忽略的coverage的集合
std::unordered_map buf_to_coverage;
//key是coverage_buffer的偏移,valve是对应的basic block/edge code
std::unordered_map coverage_to_inst;
//key是basic block/edge code,valve是对应的插桩区域中的位置
std::unordered_map buf_to_cmp;
//key是cmp code,value是对应的CmpCoverageRecord
std::unordered_map coverage_to_cmp;
//key是coverage_buffer的偏移,valve是对应的CmpCoverageRecord

下面是图中函数的注释。

LiteCov:: OnModuleInstrumented
分配coverage_buffer
LiteCov:: OnModuleUninstrumented
调用LiteCov::CollectCoverage,释放coverage_buffer
LiteCov::EmitCoverageInstrumentation
插入mov [coverage_buffer_remote + coverage_buffer_next], 1
将信息记录到buf_to_coverage和coverage_to_inst
LiteCov::InstrumentBasicBlock
基本块插桩,调用LiteCov::EmitCoverageInstrumentation
LiteCov::InstrumentEdge
边插桩,调用LiteCov::EmitCoverageInstrumentation
LiteCov::GetBBCode
basic block code是模块起始地址到基本块的偏移
LiteCov::GetEdgeCode
edge code的低32位和高32位分别表示源地址和目的地址
LiteCov::InstrumentInstruction
实现指令级插桩,当指定了compare_coverage时可以通过指令级插桩记录cmp/sub指令中匹配的字节数。对于sub指令调用LiteCov::ShouldInstrumentSub判断是否应该插桩。
插桩前:

cmp DWORD PTR [ebp-0x14],eax

插桩后:

push ecx
mov ecx,DWORD PTR [ebp-0x14]
xor ecx,eax
lzcnt ecx,ecx
cmp ecx, match_width
jb end
mov BYTE PTR [data->coverage_buffer_remote + data->coverage_buffer_next],cl
end:
pop ecx
将信息记录到buf_to_cmp和coverage_to_cmp

LiteCov::OnModuleEntered
如果插桩边,因为源地址来自另一个模块,所以用0表示源地址,加入到collected_coverage
LiteCov::CollectCoverage
读取coverage_buffer_remote中的数据,通过buf_to_coverage找到对应的basic block/edge code, 如果没有找到并且设置了-cmp_coverage说明此处是cmp的coverage信息,调用CollectCmpCoverage获取cmp code并加入collected_coverage;如果找到了则将basic block/edge code加入collected_coverage
LiteCov::OnProcessExit
调用CollectCoverage
LiteCov::GetCmpCode
cmp code高32位表示basic block偏移,接下来的24位表示cmp指令在basic block内的偏移,最后8位表示匹配的bit数。最高位设为1
LiteCov::ShouldInstrumentSub
是否应该插桩sub指令,如果后面有call/ret/jmp这样的指令就不插桩,如果后面有cmov/jz/jnz这样的指令就插桩

 

coverage.cpp

coverage.cpp实现了对coverage的管理,Coverage列表中每个成员是一个ModuleCoverage,两个成员分别是模块名和该模块中的basic block/edge/cmp code。代码很简单就不再赘述了。

 

总结

如前所述,作者也给出了一个示例程序tinyinst-coverage.cpp。基本上主要的代码就是这样。希望这篇文章能对动态插桩和tinyInst感兴趣的同学有所帮助。


推荐阅读
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • 解决Only fullscreen opaque activities can request orientation错误的方法
    本文介绍了在使用PictureSelectorLight第三方框架时遇到的Only fullscreen opaque activities can request orientation错误,并提供了一种有效的解决方案。 ... [详细]
  • 开机自启动的几种方式
    0x01快速自启动目录快速启动目录自启动方式源于Windows中的一个目录,这个目录一般叫启动或者Startup。位于该目录下的PE文件会在开机后进行自启动 ... [详细]
  • 本文详细解析了 MySQL 5.7.20 版本中二进制日志(binlog)崩溃恢复机制的工作流程。假设使用 InnoDB 存储引擎,并且启用了 `sync_binlog=1` 配置,文章深入探讨了在系统崩溃后如何通过 binlog 进行数据恢复,确保数据的一致性和完整性。 ... [详细]
  • WinMain 函数详解及示例
    本文详细介绍了 WinMain 函数的参数及其用途,并提供了一个具体的示例代码来解析 WinMain 函数的实现。 ... [详细]
  • [转]doc,ppt,xls文件格式转PDF格式http:blog.csdn.netlee353086articledetails7920355确实好用。需要注意的是#import ... [详细]
  • javascript分页类支持页码格式
    前端时间因为项目需要,要对一个产品下所有的附属图片进行分页显示,没考虑ajax一张张请求,所以干脆一次性全部把图片out,然 ... [详细]
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • 本文将详细介绍如何在Webpack项目中安装和使用ECharts,包括全量引入和按需引入的方法,并提供一个柱状图的示例。 ... [详细]
  • 本文探讨了如何利用Java代码获取当前本地操作系统中正在运行的进程列表及其详细信息。通过引入必要的包和类,开发者可以轻松地实现这一功能,为系统监控和管理提供有力支持。示例代码展示了具体实现方法,适用于需要了解系统进程状态的开发人员。 ... [详细]
  • QT框架中事件循环机制及事件分发类详解
    在QT框架中,QCoreApplication类作为事件循环的核心组件,为应用程序提供了基础的事件处理机制。该类继承自QObject,负责管理和调度各种事件,确保程序能够响应用户操作和其他系统事件。通过事件循环,QCoreApplication实现了高效的事件分发和处理,使得应用程序能够保持流畅的运行状态。此外,QCoreApplication还提供了多种方法和信号槽机制,方便开发者进行事件的定制和扩展。 ... [详细]
  • 本文介绍了如何利用 Delphi 中的 IdTCPServer 和 IdTCPClient 控件实现高效的文件传输。这些控件在默认情况下采用阻塞模式,并且服务器端已经集成了多线程处理,能够支持任意大小的文件传输,无需担心数据包大小的限制。与传统的 ClientSocket 相比,Indy 控件提供了更为简洁和可靠的解决方案,特别适用于开发高性能的网络文件传输应用程序。 ... [详细]
  • 在尝试为 Unity 编译一个简单的 Java 库时,运行 `ant jar` 命令后遇到了 Java I/O 异常。具体错误信息为“无法启动程序 ${aAPT},错误代码 2”,这通常表示指定的文件或目录不存在。此问题可能是由于环境配置不正确或路径设置有误导致的。建议检查相关路径和环境变量,确保所有依赖项都已正确安装和配置。 ... [详细]
  • 在CentOS 7上部署WebRTC网关Janus
    在CentOS 7上部署WebRTC网关Janus ... [详细]
  • 在开发Xamarin.Forms应用程序时,遇到了使用Entity Framework Core 3.0访问SQLite数据库时 `Database.MigrateAsync` 方法调用的问题。本文详细探讨了该问题的根源,并提供了一种有效的解决方案,确保数据库迁移能够顺利执行。此外,还介绍了如何配置和优化EF Core以提高应用性能和稳定性。 ... [详细]
author-avatar
花亜_277
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有