一个轻量级的动态插装库

2020-06-06 19:42:28

版权所有2020 Google LLCL根据阿帕奇许可证2.0版(该许可证);除非遵守该许可证,否则您不能使用本文件。您可以从https://www.apache.org/licenses/LICENSE-2.0Unless获取适用法律要求或书面同意的许可证副本,根据该许可证分发的软件按原样分发,没有任何形式的担保或条件,无论是明示的还是默示的。请参阅许可证中管辖权限和限制的特定语言的许可证。

TinyInst是一个轻量级动态插装库,可用于仅检测流程中选定的模块,而让流程的其余部分本机运行。它应该是易于理解、易于破解和易于破解的。它并不是设计成与所有目标兼容的(稍后将详细介绍)。

TinyInst并不是作为DynamoRIO和PIN等复杂检测框架的替代品,而是作为更轻量级解决方案的替代方案。TinyInst假设目标行为良好(在下面解释的意义上),而对于更复杂的框架则不是这样。因此,您可能无法像以前使用DynamoRIO那样针对恶意软件成功运行TinyInst。另一方面,如果目标由于不需要检测的模块而不能与其他框架一起工作,并且检测到的模块行为良好,那么它可能会与TinyInst一起工作。因为使用TinyInst,大多数进程将在本地运行,因此它的进程启动时间更短,并且在目标进程在不需要检测的模块中花费大量时间的情况下,其性能可能优于其他解决方案。

TinyInst是一个完全的二进制重写解决方案,因此可以在目标模块中更改任意行为。例如,这使得它能够提取边缘覆盖,而不仅仅是基本块。此外,TinyInst不依赖其他软件(如IDA Pro)来识别基本块。

目前仅限Windows(32位和64位)。将来可能会考虑支持Mac OS。

程序永远不会直接访问堆栈上的返回地址OR/AND(取决于设置)。

在堆栈顶部之前不会存储任何数据(在低于ESP/RSP指向的地址上)。可以使用-STACK_OFFSET标志将此条件放宽为在(ESP/RSP-ARBitrary_OFFSET)";之前没有数据。

根据对图像解码的早期测量,在具有默认TinyInst设置的行为良好的64位目标上,没有客户端的性能开销约为15%,使用示例覆盖收集客户端的性能开销约为20%。请注意,这不包括最初插入指令的模块引入的超时。有关更多详细信息,请参阅下面的性能提示。

打开命令提示符并设置构建环境,例如运行vcvars64.bat/vcvars32.bat。

运行以下命令(根据要生成的Visual Studio和平台的版本更改生成器):

注2:由于环境设置不正确和库丢失,在64位Windows上创建32位版本时遇到问题?在Visual Studio中打开生成的.sln文件并从那里进行构建,而不是运行cmake--build。还要注意,64位构建将在32位目标上工作,因此可能没有必要创建32位构建。

TinyInst客户端被编写为TinyInst类的子类。然后,客户端可以覆盖它需要的API方法。API方法定义如下。

创建客户端后,必须使用命令行选项通过调用。

下面定义了命令行选项,客户端也可以定义它们自己的选项。之后,要运行和控制插入指令的程序,可以使用以下函数。

这些函数或者运行程序(使用指定的命令行),或者附加到已经运行的程序。如果未指定目标方法,则目标将继续运行,直到程序退出、程序崩溃或超时(以毫秒为单位)到期。如果定义了目标方法,则每当输入目标方法和返回目标方法时,TinyInst都将返回,从而允许调用者执行其他任务。

当目标进程仍处于活动状态时运行并附加返回时,可以使用以下函数来终止进程或继续执行。

这些回调仅供参考,客户端在回调期间不应发出任何检测代码。在处理这些事件本身之前,客户端必须调用超类中定义的相同处理程序。

OnTargetMethodReached如果定义了目标方法,则在第一次到达目标方法时调用。

遇到异常时调用OnExceptionCalled。客户端必须返回TRUE(如果异常已处理)或父类上相同方法的结果。

OnBasicBlock可用于插入将在特定基本块上运行的代码。

OnEdge可用于插入将在特定边缘上运行的代码。注意:出于性能原因,此回调仅在不确定的边缘(即条件跳转)和间接跳转/调用(例如调用rax)上发出。对于在给定前一个基本块的情况下,下一个基本块总是已知的边(例如,JMP偏移、调用偏移),不会发出回调。

OnInstructionOn可用于修改指令或在其之前插入代码。根据返回代码的不同,原始指令要么在回调后发出,要么不发出。

检测模块时调用OnModuleInstrumented.。这通常发生在到达进程入口点时(如果未定义目标方法)或到达目标方法时(如果定义了目标方法)。客户端可以在此处初始化与检测相关的数据。

在检测数据不再有效且需要清除时调用OnModuleUnInstrumentedCalled。请注意,这与卸载模块不同,因为默认情况下,指令插入会在模块卸载/重新加载过程中持续存在。此回调可用于清除客户端中与检测相关的任何数据。

-Instrument_MODULE[模块名称]指定要检测的模块,可以指定多个Instrument_MODULE选项来检测多个模块。

-PATCH_RETURN_ADDRESS-将返回地址替换为原始值,导致使用指定的任何-INDIRECT_INTERFORMENT方法检测返回。

-Persistent_Instrumentation_Data(默认值=TRUE)不会在模块卸载/重新加载时重新检测模块。仅当模块加载到与之前加载的地址相同的地址时才起作用。

-Instrument_Cross_MODULE_CALLES(默认值=TRUE)如果指定了多个Instrument_MODULE模块,并且一个调用进入另一个模块,则跳转到另一个模块的指令插入代码,而不会导致异常(这会导致速度减慢)。

-STACK_OFFSET(默认值=0)在堆栈上保存上下文时,保持堆栈顶部(堆栈指针之前)的字节数不变。

TinyInst允许用户定义目标方法。如果定义了目标方法,则在第一次到达目标方法之前,不会检测任何代码(所有代码都将在本地运行)。此外,TinyInst将中断目标方法入口和出口的执行。

-target_method-目标方法的名称。这仅在导出目标方法或您具有目标模块的符号时才有效。

-target_offset-当无法按名称指定目标方法时使用。目标方法在模块库中的相对地址

-loop-如果指定了此标志,TinyInst将在无限循环中运行目标方法(或直到调用Kill()或进程因其他原因终止)。函数参数将在迭代之间保存和恢复。这主要用于强制模糊的持久性。

-nargs-要在迭代之间保存的目标方法参数的数量。与-loop一起使用。

TinyInst附带了一个(示例)覆盖模块LiteCov。覆盖模块可以收集基本的块或边缘覆盖(使用-covtype标志进行控制)。

Coverage模块的特殊功能是,目标进程中的Coverage缓冲区最初被分配为只读,在第一次遇到新的Coverage时会导致异常。与忽略某个覆盖率子集的选项相结合,可以快速查询使用给定输入运行目标是否会产生新的覆盖率。

TinyInst构建在自定义调试器之上。调试器监视目标进程中的事件,如加载模块、命中断点、激发异常等。如果指定了目标方法,调试器还实现断点和持久性。

当加载要检测的模块时,最初通过以下方式检测该模块。

模块中的所有可执行区域都标记为不可执行,同时保留原来的其他权限(读/写)。每当控制流到达检测到的模块时,这都会导致异常,该模块由调试器捕获和处理。

在原始模块地址范围的2 GB内分配可执行内存区域。这里将放置模块的插入指令/重写的代码。2 GB非常重要,因为它允许使用[RIP+OFFSET]形式的所有寻址指令替换为[RIP+FIXED_OFFSET]。

无论何时进入插入指令的模块(无论是第一次还是任何其他时间),都会检测命中的基本块,以及通过递归跟随条件分支以及直接调用和跳转(例如,JMP偏移量、调用偏移量)可以可靠地发现的所有基本块。

所有间接跳转/调用(例如调用rax)都将落在它们的原始代码位置,这会导致异常,调试器通过将指令指针替换为插入指令的代码中的相应位置来解决该异常。

但是,尽管这是可行的,但请注意,它将在目标位于插入指令的模块中的每个间接调用/跳转上导致异常。由于异常处理很慢,如果没有额外的检测,检测具有大量间接性(例如,C++中的虚方法、函数指针)的目标将会很慢。

TinyInst可以检测间接调用和跳转,以避免在(已经看到的)间接目标上出现异常。插入指令的调用/跳转不是跳到原始目标,而是跳到存根链接列表的头部。每个存根包含一对(Original_target,Translated_target)。它测试跳转/调用目标是否与Original_TARGET匹配,如果匹配,则将控制流定向到TRANSPECTED_TARGET。否则,它将跳转到下一个存根。如果到达列表的末尾,这意味着以前没有看到过跳转/调用目标。这将导致调试器捕获断点,并通过创建另一个存根并将其插入列表来解决此问题。

全局哈希表带来更好的性能。本地(每个调用点列表)允许在间接调用/跳转时获得正确的边缘(具有正确的源地址)。

请注意,在现代Windows上,由于使用了CFG,所有间接跳转/调用都发生在同一位置,因此,使用CFG编译的二进制文件,无论如何都不可能(如果没有某种特殊处理)获得准确的边缘。这就是为什么全局哈希列表是TinyInst中处理间接调用/跳转的默认方法的原因,还有性能优势。

默认情况下,当调用发生在插入指令的代码中时,正在写入的返回地址将是插入指令的代码中的下一条指令。这在大多数情况下都可以正常工作,但是,如果目标进程出于返回以外的目的访问返回地址,则会导致问题。一个值得注意的例子是64位Windows上异常处理(SEH)中的堆栈展开。因此,默认情况下,需要捕获异常的目标不能与TinyInst一起正常工作。

此时,TinyInst还有一个选项(通过-patchReturnAddresses标志公开),可以在发生调用时将返回地址重写为它们在未检测代码中的相应值。请注意,在没有附加检测的情况下,这会在每次返回时导致异常(导致大量减慢)。但是,使用-patchReturnAddresses时,返回指令也会以类似于间接跳转/调用的方式进行检测。虽然这解决了模块内发生的返回问题,但请注意,从未检测到的模块到检测模块的所有返回仍将导致异常。

由于这是一种相当常见的情况,将来将研究更多用于支持Windows中的异常处理的性能选项。这可以使用RtlAddFunctionTable/RtlAddGrowableFunctionTable/RtlInstallFunctionTableCallback接口来实现。

TinyInst中最大的开销来自每当从非检测模块进入检测模块时引发的异常。您可以看到这些异常是使用-TRACE_MODULE_ENTRIES标志触发的。只要有可能,就应该使用间接跳转/调用指令插入,而在任何可能的情况下,都不应该使用返回指令插入。TinyInst在合理自包含的模块(或模块组)上执行得最好。例如,如果您有两个模块,A和B,其中A经常调用B,但只有B被检测,这将导致很大的减慢。通过检测A和B可以获得更好的性能。

使用-TRACE_BASIC_BLOCKS查看正在执行的基本块。您将在插入指令的代码中看到地址,在未插入指令的代码中看到相应的地址。