flare-emu 将受支持的二进制分析框架(如 IDA Pro 或 Radare2)与 Unicorn 的仿真框架相结合,为用户提供易于使用且灵活的接口来编写仿真任务脚本。它旨在处理设置灵活且健壮的仿真器所需的所有内务管理,使其支持目标架构,从而让您专注于解决代码分析问题。目前,flare-emu 支持 x86、x86_64、ARM 和 ARM64 架构。
它目前提供了五种不同的接口来满足您的仿真需求,以及一系列相关的辅助和实用函数。
emulateRange – 此 API 用于在用户指定的上下文内仿真一系列指令或一个函数。它提供了用户定义的钩子选项,用于单个指令以及遇到“call”指令时。用户可以选择仿真器是跳过函数调用还是调用函数。此接口提供了一种简单的方式,让用户为给定的寄存器和栈参数指定值。如果指定了字节串,它会被写入仿真器的内存,并且指针会被写入寄存器或栈变量。仿真结束后,用户可以使用 flare-emu 的实用函数从仿真内存或寄存器中读取数据,或者使用返回的 Unicorn 仿真对象进行直接探测。emulateRange 的一个小型包装函数名为 emulateSelection,可用于仿真当前在 IDA Pro 中高亮显示的指令范围。
iterate – 此 API 用于强制仿真沿着函数内的特定分支向下执行,以到达给定的目标。用户可以指定一个目标地址列表,或者一个函数的地址,该函数的交叉引用列表将作为目标,同时提供一个到达目标时的回调函数。无论仿真过程中可能导致不同分支的条件如何,目标都会被到达。与 emulateRange API 类似,它提供了用户定义的钩子选项,用于单个指令和遇到“call”指令时。iterate API 的一个示例用法是实现类似于我们的 argtracker 工具的功能。
iterateAllPaths – 此 API 与 iterate 非常相似,不同之处在于您不必提供目标地址,而是提供一个目标函数,它将尝试找到该函数的所有路径并进行仿真。当您进行代码分析希望到达函数的每个基本块时,这非常有用。
emulateBytes – 此 API 提供了一种简单的方法来仿真一段额外的 shellcode。提供的字节不会被添加到 IDB 中,而是直接按原样仿真。这对于准备仿真环境可能很有用。例如,flare-emu 本身使用此 API 来操纵 Unicorn 未暴露的 ARM64 CPU 的特定模型寄存器(MSR),以启用向量浮点(VFP)指令和寄存器访问。Unicorn 仿真对象会返回给用户进行进一步探测。
emulateFrom – 此 API 在函数边界不明确的情况下非常有用,这在混淆二进制文件或 shellcode 中经常出现。您提供一个起始地址,它将一直仿真直到没有剩余内容可仿真,或者您在某一个钩子中停止仿真。使用 IDA Pro 时,可以通过将 strict 参数设置为 False 来调用此函数,以启用动态代码发现;flare-emu 会让 IDA Pro 在仿真过程中遇到指令时创建指令。
要安装适用于 IDA Pro 的 flare-emu,只需将 flare_emu.py、flare_emu_ida.py 和 flare_emu_hooks.py 放入 IDA Pro 的 python 目录,并在您的 IDAPython 脚本中将其作为模块导入。
要安装适用于 Rizin 的 flare-emu,只需确保 flare_emu.py、flare_emu_rizin.py 和 flare_emu_hooks.py 位于 Python 的模块搜索路径中。当使用 Rizin 作为 flare-emu 的二进制分析组件时,需要 rzpipe。
要安装适用于 Radare2 的 flare-emu,只需确保 flare_emu.py、flare_emu_radare.py 和 flare_emu_hooks.py 位于 Python 的模块搜索路径中。当使用 Radare2 作为 flare-emu 的二进制分析组件时,需要 r2pipe。
无论如何,flare-emu 都依赖于 Unicorn 及其 Python 绑定。
重要说明
flare-emu 是使用新的 IDA Pro 7x API 编写的,它不向后兼容旧版本的 IDA Pro。
虽然 flare-emu 可用于解决许多不同的代码分析问题,但它最常见的用途之一是帮助解密恶意软件二进制文件中的字符串。FLOSS 是一个很棒的工具,它通常可以通过尝试识别字符串解密函数,并使用仿真来解密每个交叉引用处传入的字符串,从而自动完成此操作。然而,FLOSS 并不总能识别这些函数并使用其通用方法正确地对它们进行仿真。有时您需要做一些额外的工作,而一旦您熟悉了 flare-emu,它可以为您节省大量时间。让我们来演练一下恶意软件分析师在处理加密字符串时遇到的常见场景。
您已识别出用于解密 x86_64 二进制文件中所有字符串的函数。此函数在多个地方被调用,并解密了许多不同的字符串。在 IDA Pro 中,您将此函数命名为 decryptString。以下是在 flare-emu 脚本中解密所有这些字符串,并在每个函数调用处放置包含解密后字符串的注释,同时记录每个解密的字符串及其解密地址的方法。```
from future import print_function
import flare_emu
def decrypt(argv): myEH = flare_emu.EmuHelper() myEH.emulateRange(myEH.analysisHelper.getNameAddr("decryptString"), registers = {"arg1":argv[0], "arg2":argv[1], "arg3":argv[2], "arg4":argv[3]}) return myEH.getEmuString(argv[0])
def iterateCallback(eh, address, argv, userData): s = decrypt(argv) print("%s: %s" % (eh.hexString(address), s)) eh.analysisHelper.setComment(address, s, False)
if name == 'main':
eh = flare_emu.EmuHelper()
eh.iterate(eh.analysisHelper.getNameAddr("decryptString"), iterateCallback)
在 `__main__` 中,我们首先创建一个 `EmuHelper` 类的实例,该类来自 `flare-emu`。这是我们用 `flare-emu` 完成所有任务所使用的类。接着,我们使用 `iterate` API,传入 `decryptString` 函数的地址以及回调函数的名称,`EmuHelper` 将为每个模拟到的交叉引用调用该回调函数。
`iterateCallback` 函数接收 `EmuHelper` 实例(此处命名为 `eh`),以及交叉引用的地址、此次调用传递的参数,以及一个特殊字典(此处命名为 `userData`)。在这个简单示例中并未使用 `userData`,但可以将其视为模拟器的持久上下文,你可以在其中存储自定义数据。但要注意,因为 `flare-emu` 自身也使用这个字典来存储执行任务所需的关键信息。其中一个数据就是 `EmuHelper` 实例本身,存储在 `“EmuHelper”` 键中。如果你感兴趣,可以搜索源代码了解这个字典的更多信息。该回调函数简单地调用 `decrypt` 函数,打印解密后的字符串,并在调用 `decryptString` 的地址处为其添加注释。
`decrypt` 创建第二个 `EmuHelper` 实例,用于模拟 `decryptString` 函数本身,从而为我们解密字符串。该 `decryptString` 函数的原型如下:`char * decryptString(char *text, int textLength, char *key, int keyLength)`。它直接在原位置解密字符串。我们的 `decrypt` 函数将 `iterateCallback` 函数接收到的参数传递给 `EmuHelper` 的 `emulateRange` API。由于这是一个 `x86_64` 二进制文件,调用约定使用寄存器传递参数,而不是栈。`flare-emu` 根据 IDA Pro 分析的架构和文件格式自动确定哪些寄存器代表哪些参数,从而允许你编写至少在一定程度上与架构无关的代码。如果是 32 位 `x86`,则需要使用 `stack` 参数来传递参数,如下所示:`myEH.emulateRange(myEH.analysisHelper.getNameAddr("decryptString"), stack = [0, argv[0], argv[1], argv[2], argv[3]])`。第一个栈值是 `x86` 中的返回地址,所以我们这里用 `0` 作为占位值。模拟完成后,我们调用 `getEmuString` API 来检索存储在该函数第一个参数指向的内存位置中的以 null 结尾的字符串。
### flare-emu 和 idalib
* 安装 IDA Pro
* 根据 Hex-Rays 用户指南安装 idalib
* (激活虚拟环境)
* pip install /path/to/IDA/installation/idalib/python
* python /path/to/IDA/installation/idalib/python/py-activate-idalib.py [-d /path/to/active/IDA/installation]
* 导入 idapro 并编写脚本
* 参见 tests/test_flare_emu_idalib.py 示例
### 使用 Rizin 的简单字符串解密场景
使用上面的相同示例,使用 Rizin 而不是 IDA Pro 时变化不大。一个区别是,`flare-emu` 目前设计为在使用 Rizin 时作为命令行脚本或在 Python shell 中运行。Python shell 非常适合临时解决问题,而命令行脚本则非常适合批处理。上述脚本的 Rizin 版本如下所示(你也可以省略样本路径以在 rizin 中运行):```
from __future__ import print_function
import sys
import flare_emu
def decrypt(argv, eh):
myEH = flare_emu.EmuHelper(samplePath=sys.argv[1], emuHelper=eh, isRizin=True)
myEH.emulateRange(
myEH.analysisHelper.getNameAddr("decryptString"),
registers={
"arg1": argv[0],
"arg2": argv[1],
"arg3": argv[2],
"arg4": argv[3],
},
)
return myEH.getEmuString(argv[0])
def iterateCallback(eh, address, argv, userData):
s = decrypt(argv, eh)
print("%s: %s" % (eh.hexString(address), s))
eh.analysisHelper.setComment(address, s, False)
if __name__ == "__main__":
eh = flare_emu.EmuHelper(samplePath=sys.argv[1], isRizin=True)
rz = eh.analysisHelper.r
eh.analysisHelper.setName(0x100000D60, "decryptString")
eh.iterate(eh.analysisHelper.getNameAddr("decryptString"), iterateCallback)
沿用上述示例,当使用 Radare2 而非 IDA Pro 时,变化不大。一个区别是,flare-emu 目前设计为在使用 Radare2 时通过命令行脚本或 Python 终端运行。Python 终端非常适合临时解决问题,而命令行脚本则适合批量处理。上述脚本的 Radare2 版本如下所示:```
from future import print_function
import flare_emu
def decrypt(argv, eh): myEH = flare_emu.EmuHelper(samplePath=sys.argv[1], emuHelper=eh) myEH.emulateRange(myEH.analysisHelper.getNameAddr("decryptString"), registers = {"arg1":argv[0], "arg2":argv[1], "arg3":argv[2], "arg4":argv[3]}) return myEH.getEmuString(argv[0])
def iterateCallback(eh, address, argv, userData): s = decrypt(argv, eh) print("%s: %s" % (eh.hexString(address), s)) eh.analysisHelper.setComment(address, s, False)
if name == 'main':
eh = flare_emu.EmuHelper(samplePath=sys.argv[1])
eh.analysisHelper.setName(, "decryptString")
eh.iterate(eh.analysisHelper.getNameAddr("decryptString"), iterateCallback)
该脚本有两个不同之处。首先,`EmuHelper` 构造函数在此处接受一个参数:`samplePath=sys.argv[1]`。当提供 `samplePath` 参数时,`flare-emu` 将使用 Radare2 配合 `r2pipe` 作为其二进制分析引擎。你还可以看到,第二个参数传递给了在 `decrypt` 函数中创建的第二个 `EmuHelper` 实例。`emuHelper` 参数接受一个已有的 `EmuHelper` 对象,并在创建新对象时克隆其内存。此外,如果你使用 Radare2,新实例会重用现有的 Radare2 会话,而不是创建一个新会话,从而减少额外开销。其次,`flare-emu` 使用 `r2pipe.open` 创建 Radare2 的新实例,因此它很可能不会将我们感兴趣的函数命名为 `decryptString`。你可以通过 `EmuHelper` 的 `analysisHelper` 对象自行设置名称,例如:`eh.analysisHelper.setName(<some地址>, "decryptString")`,或者你可以直接为 `iterate` 和 `emulateRange` 的调用输入地址。
## [仿真函数](#emulationfuncs)
`emulateRange(startAddr, endAddr=None, registers=None, stack=None, instructionHook=None, callHook=None, memAccessHook=None, hookData=None, skipCalls=True, hookApis=True, strict=True, count=0)` - 模拟从 `startAddress` 开始到 `endAddress`(不包括 `endAddress` 处的指令)的指令范围。如果 `endAddress` 为 `None`,则当在同一函数(仿真起始的函数)内遇到“返回”类型指令时停止仿真。
* `registers` 是一个字典,键为寄存器名称,值为寄存器值。`flare-emu` 创建了一些特殊的寄存器名称,可用于此处,例如 `arg1`、`arg2` 等,以及 `ret` 和 `pc`。
* `stack` 是一个值数组,按逆序压入堆栈,类似于 `x86` 中函数的参数。在 `x86` 中,请记住此数组中的第一个值用作函数调用的返回地址,而不是函数的第一个参数。`flare-emu` 将根据 `registers` 和 `stack` 参数中指定的值初始化仿真线程的上下文和内存。如果这些值中的任何一个被指定为字符串,则该字符串将被写入内存中的某个位置,并将指向该内存的指针写入指定的寄存器或堆栈位置。
* `instructionHook` 可以是你在每条指令仿真之前定义的要调用的函数。其原型为:`instructionHook(unicornObject, address, instructionSize, userData)`。
* `callHook` 可以是在仿真期间遇到“调用”类型指令时定义的要调用的函数。其原型为:`callHook(address, arguments, functionName, userData)`。
* `hookData` 是一个字典,包含用户定义的数据,以供你的钩子函数使用。它是在整个仿真过程中持久保存数据的一种方式。`flare-emu` 也出于自身目的使用此字典,因此必须小心不要定义已存在的键。此变量在用户定义的钩子函数中通常命名为 `userData`,因其命名源自 Unicorn。
* `skipCalls` 将使仿真器跳过“调用”类型指令并相应地调整堆栈,默认为 `True`。
* `hookApis` 使 `flare-emu` 对仿真期间遇到的更常见的运行时和操作系统库函数执行朴素实现。这使你无需担心对 `memcpy`、`strcat`、`malloc` 等函数的调用,默认为 `True`。
* `memAccessHook` 可以是在内存被读取或写入时定义的要调用的函数。其原型为:`memAccessHook(unicornObject, accessType, memAccessAddress, memAccessSize, memValue, userData)`。
* `strict`,当设置为 `True`(默认)时,会检查分支目标以确保反汇编器期望存在指令。否则会跳过分支指令。如果在使用 IDA Pro 时设置为 `False`,`flare-emu` 将在仿真指令时在 IDA Pro 中创建指令 **(谨慎禁用)**。
* `count` 是要仿真的最大指令数,默认为 `0`,表示无限制。
`iterate(target, targetCallback, preEmuCallback=None, callHook=None, instructionHook=None, hookData=None, resetEmuMem=False, hookApis=True, memAccessHook=None)` - 对于 `target` 指定的每个目标,从包含该目标的函数开头到目标地址分别执行一次仿真。仿真将被迫沿着到达每个目标所需的分支进行。`target` 可以是一个函数的地址,此时目标列表将用对该指定函数的所有交叉引用填充。或者,`target` 可以是一个明确的目标列表。
* `targetCallback` 是你创建的函数,`flare-emu` 将在仿真到达每个目标时调用它。其原型为:`targetHook(emuHelper, address, arguments, userData)`。
* `preEmuCallback` 是你创建的函数,将在每个目标的仿真开始之前调用。如果需要,你可以在此处实现一些设置代码。
* `resetEmuMem` 将使 `flare-emu` 在开始每个目标的仿真之前重置仿真内存,默认为 `False`。
`iterateAllPaths(target, targetCallback, preEmuCallback=None, callHook=None, instructionHook=None, hookData=None, resetEmuMem=False, hookApis=True, memAccessHook=None, maxPaths=MAXCODEPATHS, maxNodes=MAXNODESEARCH)` - 对于包含地址 `target` 的函数,将为通过该函数发现的每条路径(最多 `maxPaths` 条)分别执行一次仿真。
* `maxPaths` - 将被搜索并仿真的通过函数的最大路径数。某些较复杂的函数可能导致图搜索函数花费很长时间或永远无法完成;调整此参数以满足你在合理时间内的需求。
* `maxNodes` - 在查找通过目标函数的路径时将搜索的最大基本块数。这是一种安全措施,以防止不合理的搜索时间和挂起,通常无需更改。
`emulateBytes(bytes, registers=None, stack=None, baseAddress=0x400000, instructionHook=None, hookData=None)` - 将 `bytes` 中包含的代码写入仿真内存(如可能,写入 `baseAddress`),并从 `bytes` 的开头到结尾仿真这些指令。
`emulateFrom(startAddr, registers=None, stack=None, instructionHook=None, callHook=None, memAccessHook=None, hookData=None, skipCalls=True, hookApis=True, strict=True, count=0)` - 此 API 在函数边界不明确的情况下(通常如混淆的二进制文件或 shellcode)非常有用。你提供一个起始地址 `startAddr`,它将仿真,直到没有内容可仿真,或者你通过一个钩子停止仿真。可以使用 `strict` 参数设置为 `False` 来调用此方法以启用动态代码发现;`flare-emu` 将让 IDA Pro 在仿真期间遇到指令时创建指令。
## [实用函数](#utility)
以下是由 `EmuHelper` 类提供的一些有用的实用函数的不完整列表。
* `hexString(value)` - 返回值的十六进制格式化字符串。适用于日志记录和打印语句。
* `skipInstruction(userData, useAnalysisHelper=False)` - 从仿真钩子中调用此函数以跳过当前指令,将程序计数器移至下一条指令。添加了 `useAnalysisHelper` 选项以处理二进制分析框架将多条指令折叠成一条伪指令的情况,且你希望跳过所有这些指令。此函数不能从单个指令钩子中多次调用以跳过多条指令。要跳过多条指令,如果你正在仿真 ARM 代码,建议不要直接写入程序计数器,因为这可能导致 Thumb 模式出现问题。相反,请尝试使用 `EmuHelper` 的 `changeProgramCounter` API(如下所述)。
* `changeProgramCounter(userData, newAddress)` - 从仿真钩子中调用此函数以更改程序计数器寄存器的值。此 API 负责 ARM 架构中 Thumb 模式的跟踪。
* `getRegVal(registerName)` - 检索指定寄存器的值,对子寄存器寻址敏感。例如,在 `x86` 中,“ax”将返回 EAX/RAX 寄存器的低 16 位。
* `stopEmulation(userData)` - 从仿真钩子中调用此函数以停止仿真。请使用此方法而不是调用 `emu_stop` Unicorn API,以便 `EmuHelper` 对象能够处理与 `iterate` 功能相关的簿记。
* `getEmuString(address)` - 返回仿真内存中位于某个地址的字符串字符,以 null 终止符结束。字符不一定可打印。
* `getEmuWideString(address)` - 返回仿真内存中位于某个地址的“宽字符”字符串,以 null 终止符结束。“宽字符”在此处宽泛地指代每两个字节包含一个空字节的字节序列,就像以 UTF-16 LE 编码的 ASCII 字符串那样。字符不一定可打印。
* `getEmuBytes(address, length)` - 返回仿真内存中位于某个地址的字节字符串。
* `getEmuPtr(address)` - 返回位于给定地址的指针值。
* `writeEmuPtr(address, value)` - 在仿真内存中的给定地址写入指针值。
* `loadBytes(bytes, address=None)` - 在仿真器中分配内存并将字节写入该内存。
* `isValidEmuPtr(address)` - 如果提供的地址指向有效的仿真内存,则返回 `True`。
* `getEmuMemRegion(address)` - 返回一个元组,包含包含给定地址的内存区域的起始和结束地址,如果地址无效则返回 `None`。
* `getArgv()` - 在“调用”类型指令处从仿真钩子中调用此函数,以接收函数参数的数组。
* `addApiHook(apiName, hook)` - 为此 `EmuHelper` 实例添加一个新的 API 钩子。每当仿真期间遇到对 `apiName` 的调用指令时,`EmuHelper` 将调用由 `hook` 指定的函数。如果 `hook` 是一个字符串,则期望其为 `EmuHelper` 已钩住的 API 名称,此时它将调用其现有的钩子函数。如果 `hook` 是一个函数,它将调用该函数。
* `allocEmuMem(size, addr=None)` - 分配足够的仿真器内存以容纳 `size` 字节。它会尝试满足请求的 `地址`,但如果与现有内存区域重叠,则会在未使用的内存区域中分配并返回新地址。如果 `地址` 未页对齐,它将返回一个在新区域内保持相同页对齐偏移量的地址。例如,请求地址 `0x1234`,而 `0x1000` 已分配,可能会分配在 `0x2000` 并返回 `0x2234`。
# [了解更多](#learn)
要了解更多关于 **flare-emu** 的信息,请阅读我们的介绍性博客:https://www.fireeye.com/blog/threat-research/2018/12/automating-objective-c-code-analysis-with-emulation.html。