googleprojectzero/TinyInst
GitHub: googleprojectzero/TinyInst
Google Project Zero出品的轻量级动态二进制插桩库,支持选择性模块插桩和覆盖率收集,专为模糊测试与安全研究优化。
Stars: 1307 | Forks: 130
# TinyInst
```
Copyright 2020 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
## 什么是 TinyInst?
TinyInst 是一个轻量级的动态插桩库,可以用于仅插桩进程中选定的模块,而让进程的其余部分原生运行。它的设计初衷是易于理解、易于修改(hack on)且易于利用(hack with)。它并非设计为与所有目标兼容(稍后详述)。
### 它与 [DynamoRIO](https://dynamorio.org/) 和 [PIN](https://software.intel.com/en-us/articles/pintool) 相比如何?
TinyInst 并非旨在替代 DynamoRIO 和 PIN 等复杂的插桩框架,而是作为一种替代方案,适用于那些更轻量级解决方案就足够的场景。TinyInst 假定目标行为良好(含义如下文所述),而更复杂的框架则不以此为前提。因此,您可能无法像[之前使用 DynamoRIO 那样](https://www.slideshare.net/MaximShudrak/fuzzing-malware-for-fun-profit-applying-coverageguided-fuzzing-to-find-bugs-in-modern-malware) 成功地对恶意软件运行 TinyInst。另一方面,如果某个目标由于不需要插桩的模块而无法与其他框架配合使用,且被插桩的模块行为良好,那么它可能适用于 TinyInst。因为在 TinyInst 下,进程的大部分内容将原生运行,因此进程启动时间更短,并且在目标进程在不需插桩的模块中花费大量时间的情况下,其性能可能会优于其他解决方案。
### 它与 [Mesos](https://github.com/gamozolabs/mesos) 和 [TrapFuzz](https://github.com/googleprojectzero/p0tools/tree/master/TrapFuzz) 相比如何?
TinyInst 是一个完整的二进制重写解决方案,因此可以在目标模块中更改任意行为。例如,这使其能够提取边缘覆盖(edge coverage)而不仅仅是基本块。此外,TinyInst 不依赖于其他软件(如 IDA Pro)来识别基本块。
### TinyInst 支持哪些操作系统?
TinyInst 可在 Windows(x86 和 x64)、macOS(x64 和 ARM64)、Linux(x64 和 ARM64)以及 Android(ARM64)上运行。有关其他说明和限制,请参阅相应目录中针对每个操作系统的 README。
### 哪些目标与 TinyInst 兼容?
TinyInst 假定所有被插桩的模块在以下意义上是行为良好的:
- 不存在自修改代码
- 程序永远不会直接访问栈上的返回地址
或者/并且(取决于设置)
- 永远不会在栈顶之前(地址低于 ESP/RSP 指向的地址)存储数据。可以使用 `-stack_offset` 标志将此条件放宽为“在 (ESP/RSP - arbitrary_offset) 之前没有数据”。
TinyInst 还要求为目标进程启用 DEP/NX。如果尚未启用,可以使用 `-force_dep` 标志强制开启。但是,在极少数情况下,如果目标确实需要关闭 DEP 才能正常运行,强制开启可能会导致其行为异常。
### 性能开销是多少?
根据早期的图像解码测量,在具有默认 TinyInst 设置的行为良好的 64 位目标上,在没有客户端的情况下性能开销约为 15%,在使用示例覆盖率收集客户端的情况下约为 20%。请注意,这不包括初始插桩模块引入的超时。有关更多详细信息,请参阅下面的性能提示。
## 构建 TinyInst
1. 打开终端并设置您的构建环境(例如在 Windows 上,运行 vcvars64.bat / vcvars32.bat)
2. 导航到包含源代码的目录
3. 运行以下命令(根据您要构建的 IDE 版本和平台更改生成器):
#### Windows
```
mkdir build
cd build
cmake -G "Visual Studio 16 2019" -A x64 ..
cmake --build . --config Release
```
#### macOS
```
mkdir build
cd build
cmake -G Xcode ..
cmake --build . --config Release
```
#### Linux
```
mkdir build
cd build
cmake ..
cmake --build . --config Release
```
#### 交叉编译 Android 版本
```
mkdir build
cd build
cmake -DCMAKE_TOOLCHAIN_FILE=build/cmake/android.toolchain.cmake -DANDROID_NDK= -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM= ..
cmake --build . --config Release
```
注意 #1:64 位版本也可以在 Windows 和 Linux 操作系统上针对 32 位目标运行。
注意 #2:由于环境未正确设置且缺少库,在 64 位 Windows 上创建 32 位版本时遇到问题?在 Visual Studio 中打开生成的 .sln 文件并从那里构建,而不是运行 cmake --build。另请注意,64 位版本将适用于 32 位目标,因此可能不需要创建 32 位版本。
## 使用 TinyInst
TinyInst 主要旨在作为库在其他程序内部使用。
TinyInst 客户端被编写为 TinyInst 类的子类。然后,客户端可以覆盖其所需的 API 方法。API 方法定义如下。
创建客户端后,必须通过调用以下方法使用命令行选项对其进行初始化:
`void init(int argc, char **argv);`
命令行选项定义如下,客户端也可以定义自己的选项。之后,要运行和控制插桩程序,可以使用以下函数。
`DebuggerStatus Run(int argc, char **argv, uint32_t timeout);`
`DebuggerStatus Attach(unsigned int pid, uint32_t timeout);`
这些函数要么运行程序(使用指定的命令行),要么附加到已经运行的程序。如果未指定目标方法,目标将继续运行,直到程序退出、程序崩溃或超时(以毫秒为单位)到期。如果定义了目标方法,TinyInst 将在进入目标方法和目标方法返回时返回,从而允许调用者执行其他任务。
当 `Run` 和 `Attach` 在目标进程仍然存活时返回,可以使用以下函数终止进程或继续执行。
`DebuggerStatus Kill();`
`DebuggerStatus Continue(uint32_t timeout);`
TinyInst 附带了一个示例覆盖率二进制文件,可以使用以下命令调用:
` -- `
Windows 上的示例:
`litecov.exe -instrument_module notepad.exe -coverage_file coverage.txt -- notepad.exe`
## 插桩 API
### 调试器事件回调
这些回调仅用于提供信息,客户端不应在其中发出任何插桩代码。客户端必须在处理这些事件之前调用超类中定义的相同处理程序。
`OnProcessCreated`
在创建或附加目标进程时调用。
`OnProcessExit`
在目标进程退出时调用。
`OnProcessEntrypoint`
当到达进程(主二进制文件)入口点时调用。
`OnTargetMethodReached`
如果定义了目标方法,则在首次到达目标方法时调用。
`OnModuleLoaded`
在加载模块时调用。针对每个模块调用,而不仅仅是被插桩的模块。
`OnModuleUnloaded`
在卸载模块时调用。针对每个模块调用,而不仅仅是被插桩的模块。
`OnException`
遇到异常时调用。客户端必须返回 true(如果异常已处理)或父类上相同方法的结果。
### 插桩回调
在这些回调期间,客户端可以通过调用 `WriteCode()` 将代码添加到目标中。请注意,客户端负责保存和恢复任何上下文(例如在插入的代码中被修改的寄存器和标志)。
`InstrumentBasicBlock`
可用于插入将在特定基本块上运行的代码。
`InstrumentEdge`
可用于插入将在特定边上运行的代码。注意:出于性能原因,此回调仅在非确定性边(即条件跳转)和间接跳转/调用(例如 `call rax`)上发出。对于给定前一个基本块就知道下一个基本块的边(例如 `jmp offset`,`call offset`),不会发出回调。
`InstrumentInstruction`
可用于修改指令或在其之前插入代码。根据返回代码,原始指令将在回调之后发出或不发出。
### 其他回调
`OnModuleEntered`
当控制流从另一个模块转移到被插桩模块时调用。
`OnModuleInstrumented`
当模块被插桩时调用。这通常发生在到达进程入口点时(如果未定义目标方法)或到达目标方法时(如果已定义)。客户端可以在此处初始化其与插桩相关的数据。
`OnModuleUninstrumented`
当插桩数据不再有效并需要清除时调用。请注意,这与模块被卸载不同,因为默认情况下,插桩会在模块卸载/重新加载期间持续存在。此回调可用于清除客户端中任何与插桩相关的数据。
### Hook API
除了上面记录的通用 API 外,TinyInst 还实现了一个 Hook API,该 API 更适合检查和修改单个函数的行为。该 API 在[单独的页面](https://github.com/googleprojectzero/TinyInst/blob/master/hook.md)上进行了记录。
## 命令行选项
### 插桩相关
`-instrument_module [module name]` 指定要插桩的模块,可以指定多个 `-instrument_module` 选项以插桩多个模块。
`-instrument_transitive [module name]` 类似于 `-instrument_module`,不同之处在于只有从其他被插桩模块进入的代码才会运行插桩。主要用于优化 module1->module2->module1 这样的调用,其中插桩整个 module2 模块并不重要,但 module2->module1 的条目会导致速度变慢。
`-indirect_instrumentation [none|local|global|auto]` 用于间接跳转/调用的插桩方式。
`-patch_return_addresses` - 用原始值替换返回地址,导致使用任何指定的 `-indirect_instrumentation` 方法对返回进行插桩。
`-generate_unwind` - 为插桩代码生成栈展开数据(用于更快的 C++ 异常处理)。请注意,它可能无法在某些较旧的 Windows 版本上正常工作。
`-persist_instrumentation_data` (默认 = true) 不在模块卸载/重新加载时重新插桩模块。仅在模块加载到与之前加载的地址相同的地址时才有效。
`-instrument_cross_module_calls` (默认=true) 如果指定了多个 `-instrument_module` 模块并且一个调用另一个,则跳转到另一个模块的插桩代码而不会引起异常(这会导致速度变慢)。
`-stack_offset` (默认=0) 在栈上保存上下文时,保持栈顶(栈指针之前)的这么多字节不变。
`-patch_module_entries [off|data|code|all]` 尝试通过搜索指向先前检测到的入口点的指针并将其替换为其插桩对应项,来解决由于模块条目过多而导致的速度变慢问题。标志的值控制在哪里搜索这些指针。警告:启用此功能可能会给目标带来不稳定性。
### 调试相关
`-trace_debug_events` - 打印调试器事件(加载的模块、异常等)。
`-trace_basic_blocks` - 打印已执行的基本块。
`-trace_module_entries` - 打印进入插桩代码的所有条目。
`-trace_syscalls` - [仅限 Linux/Android] 使客户端能够通过 `OnSyscall()` / `OnSyscallEnd()` 回调接收系统调用开始/结束事件。
`-full_address_map` - 维护插桩代码中的地址到原始代码中的地址的指令级映射。占用内存较多,但对调试很有用。
### 目标方法和持久性
TinyInst 允许用户定义目标方法。如果定义了目标方法,则在首次到达目标方法之前不会插桩任何代码(所有内容都将原生运行)。此外,TinyInst 将在目标方法进入和退出时中断执行。
`-target_module` - 包含目标方法的模块。
`-target_method` - 目标方法的名称。仅当目标方法被导出或者您拥有目标模块的符号时,此选项才有效。
`-target_offset` - 在无法通过名称指定目标方法时使用。目标方法相对于模块基址的相对地址。
`-loop` - 如果指定了此标志,TinyInst 将在无限循环中运行目标方法(或直到调用 Kill() 或进程因其他原因终止)。函数参数将在迭代之间保存和恢复。这主要用于强制模糊测试的持久性。
`-nargs` - 在迭代之间保存的目标方法参数的数量。与 `-loop` 一起使用。
`-callcon [ms64|stdcall|fastcall|thiscall]` - 目标方法使用的调用约定。与 `-loop` 一起使用。
### 其他
`-target_env key=value` - [目前仅限 macOS 和 Linux/Android] 指定要传递给目标进程的附加环境变量。可以指定多个 `-target_env` 选项以传递多个环境变量。
`-force_dep` - [仅限 Windows] 为目标进程强制启用 DEP。
## 覆盖率模块
TinyInst 附带了一个(示例)覆盖率模块 `LiteCov`。覆盖率模块可以收集基本块或边缘覆盖(edge coverage)(使用 `-covtype` 标志控制)。除此之外,该模块还可以通过指定 `-cmp_coverage` 标志来提取“比较”覆盖率(计算 cmp/sub 指令中匹配的字节数)。
覆盖率模块的一个特殊功能是,目标进程中的覆盖率缓冲区最初被分配为只读,从而在首次遇到新覆盖率时导致异常。结合忽略特定覆盖率子集的选项,这使得能够快速查询使用给定输入运行目标是否产生了新的覆盖率。
## TinyInst 如何工作?
TinyInst 构建在自定义调试器之上。调试器监视目标进程的事件,例如加载模块、命中断点、触发异常等。如果指定了目标方法,调试器还会实现断点和持久性。
当要插桩的模块被加载时,它最初以以下方式被“插桩”:
- 模块中的所有可执行区域都标记为不可执行,同时保留其他权限(读/写)与原来相同。每当控制流到达被插桩模块时,这都会导致异常,该异常由调试器捕获并处理。
- 在原始模块地址范围的 2GB 范围内分配一个可执行的内存区域。这是放置模块的插桩/重写代码的地方。2GB 很重要,因为它允许所有使用 [rip+offset] 形式寻址的指令被替换为 [rip+fixed_offset]。
每当进入被插桩模块时(无论是第一次还是其他任何时间),命中的基本块都会被插桩,同时被插桩的还有所有可以通过递归跟随条件分支以及直接调用和跳转(例如 jmp offset, call offset)可靠发现的基本块。
这足以运行插桩代码,因为:
-直接跳转/调用都将在正确的位置落在插桩代码中
- 所有间接跳转/调用(例如 call rax)都将落在其原始代码位置,这会导致异常,调试器通过将指令指针替换为插桩代码中的相应位置来解决该异常。
但是,虽然这样做有效,但请注意,它会在目标位于被插桩模块中的每个间接调用/跳转上导致异常。由于异常处理速度很慢,如果没有额外的插桩,对具有大量间接性(例如 C++ 中的虚方法、函数指针)的目标进行插桩将会很慢。
### 插桩间接调用和跳转
TinyInst 可以插桩间接调用和跳转,以避免(已见过的)间接目标上的异常。插桩后的调用/跳转不会跳转到原始目标,而是跳转到桩(stub)链表的头部。每个桩包含一对 (original_target, translated_target)。它测试跳转/调用目标是否匹配 original_target,如果匹配,则控制流指向 translated_target。否则,它跳转到下一个桩。如果到达列表末尾,则意味着该跳转/调用目标之前未出现过。这将导致一个被调试器捕获的断点,调试器将通过创建另一个桩并将其插入列表来解决该断点。
此机制可以通过 2 种方式实现:
- 每个调用点(局部)列表
- 所有间接跳转/调用使用的全局哈希表
全局哈希表可带来更好的性能。局部(每个调用点列表)允许在间接调用/跳转上获取正确的边(具有正确的源地址)。
请注意,在现代 Windows 上,由于 CFG,所有间接跳转/调用都发生在同一位置,因此对于 CFG 编译的二进制文件,(没有某种特殊处理)无论如何都不可能获得准确的边。这与性能优势一起,就是全局哈希列表是 TinyInst 中处理间接调用/跳转的默认方法的原因。
### 返回地址修补
默认情况下,当在插桩代码中发生调用时,写入的返回地址将是 *插桩代码* 中的下一条指令。这在大多数情况下都能正常工作,但是如果目标进程出于返回以外的目的访问返回地址,则会导致问题。一个值得注意的例子是 64 位操作系统上异常处理期间的栈展开。因此,需要捕获异常的目标在默认情况下无法在 TinyInst 下正常工作。
在大多数情况下,可以通过添加 `-generate_unwind` 标志来解决此问题,该标志使 TinyInst 为目标进程生成并注册栈展开/异常处理元数据。请注意,由于需要 UNWIND_INFO 版本 2,`-generate_unwind` 可能无法在某些较旧的 Windows 版本上正常工作。
TinyInst 还有一个选项(通过 `-patch_return_addresses` 标志公开),可以在发生调用时将返回地址重写为它们在非插桩代码中的相应值。但是请注意,此选项引入了相当大的开销,因为它会导致每次从未插桩模块返回(后向边缘)到插桩模块时都会发生上下文切换。
## 性能提示
TinyInst 中最大的开销来自于每当从非插桩模块进入被插桩模块时抛出的异常。您可以使用 `-trace_module_entries` 标志查看触发的这些异常。应尽可能使用间接跳转/调用插桩,且应尽可能不使用返回插桩。TinyInst 在(模块或模块组)合理自包含的模块上表现最佳。例如,如果您有两个模块 A 和 B,其中 A 经常调用 B 但只有 B 被插桩,这将导致大量速度下降。通过同时插桩 A 和 B 可以实现更好的性能。
## 调试提示
使用 `-trace_basic_blocks` 查看正在执行的基本块。您将看到插桩代码中的地址和非插桩代码中的相应地址。
使用 OnException() 回调检查崩溃发生时的程序状态。
## 免责声明
这不是一个官方的 Google 产品。
标签:ARM, Bash脚本, Bing搜索, C++, DBI, Google, Instrumentation, LangChain, Nuclei, 二进制分析, 云安全运维, 云资产清单, 代码覆盖率, 动态二进制插桩, 开源, 性能分析, 指令级跟踪, 数据擦除, 测试框架, 端点可见性, 轻量级, 逆向工程