-
-
[翻译]Ghidra基于脚本的恶意软件分析
-
发表于: 2025-3-7 18:57 1955
-
(译者注:本文是对国外逆向工程师David Álvarez Pérez 所著《Ghidra Software Reverse Engineering for Beginners》第六章《Scripting Malware Analysis》主要内容的翻译,版权归原作者所有,翻译如有错误,还请各位读者指正。为熟悉Ghidra操作,文中部分截图是译者自己截图,与原文有些许差异,如需查看原文可点击文末链接)
1. 使用 Ghidra 脚本 API
Ghidra 脚本 API 分为 Flat API(ghidra.app.decompiler.flatapi)和其他更复杂的函数(http://ghidra.re/ghidra_docs/api/overview-tree.html)。
Flat API 是 Ghidra API 的简化版本,它允许你执行以下操作:
- 处理内存地址:addEntryPoint、addInstructionXref、createAddressSet、getAddressFactory 和 removeEntryPoint。
- 执行代码分析:analyze、analyzeAll、analyzeChanges、analyzeAll 和 analyzeChanges。
- 清除代码列表:clearListing。
- 声明数据:createAsciiString、createAsciiString、createBookmark、createByte、createChar、createData、createDouble、createDWord、createDwords、createEquate、createUnicodeString、removeData、removeDataAt、removeEquate、removeEquate 和 removeEquates。
- 从内存地址获取数据:getInt、getByte、getBytes、getShort、getLong、getFloat、getDouble、getDataAfter、getDataAt、getDataBefore、getLastData、getDataContaining、getUndefinedDataAfter、getUndefinedDataAt、getUndefinedDataBefore、getMemoryBlock、getMemoryBlocks 和 getFirstData。
- 处理引用:createExternalReference、createStackReference、getReference、getReferencesFrom、getReferencesTo 和 setReferencePrimary。
- 处理数据类型:createFloat、createQWord、createWord、getDataTypes 和 openDataTypeArchive。
- 为某些内存地址设置值:setByte、setBytes、setDouble、setFloat、setInt、setLong 和 setShort。
- 创建片段:getFragment、createFragment、createFunction、createLabel、createMemoryBlock、createMemoryReference、createSymbol、getSymbol、getSymbols、getSymbolAfter、getSymbolAt、getSymbolBefore、getSymbols 和 getBookmarks。
- 反汇编字节:disassemble。
- 处理事务:end 和 start。
- 查找值:find、findBytes、findPascalStrings 和 findStrings。
- 在函数级别操作:getGlobalFunctions、getFirstFunction、getFunction、getFunctionAfter、getFunctionAt、getFunctionBefore、getFunctionContaining 和 getLastFunction。
- 在程序级别操作:getCurrentProgram、saveProgram、set 和 getProgramFile。
- 在指令级别操作:getFirstInstruction、getInstructionAfter、getInstructionAt、getInstructionBefore、getInstructionContaining 和 getLastInstruction。
- 处理等价值:getEquate 和 getEquates。
- 删除某些内容:removeBookmark、removeFunction、removeFunctionAt、removeInstruction、removeInstructionAt、removeMemoryBlock、removeReference 和 removeSymbol。
- 处理注释:setEOLComment、setPlateComment、setPostComment、setPreComment、getPlateComment、getPostComment、getPreComment、getEOLComment 和 toAddr。
- 反编译字节:FlatDecompilerAPI、decompile 和 getDecompiler。
- 其他杂项函数:getMonitor、getNamespace 和 getProjectRootFolder。
这个参考可以帮助你在开始使用 Ghidra 脚本时识别你需要的函数,并在文档中查找其原型。
2. 使用 Java 编写脚本
正如你在前一章中所了解的,Alina 恶意软件包含注入到 explorer.exe 进程中的 shellcode。如果你想对 shellcode 中的 Kernel32 API 函数调用进行去混淆,那么你需要识别 call 指令。你还需要过滤函数,以筛选你需要的内容,最后,当然,你需要执行去混淆操作:
1
2
3
4
5
6
7
8
9
10
11
12
|
Function fn = getFunctionAt(currentAddress); Instruction i = getInstructionAt(currentAddress); while (getFunctionContaining(i.getAddress()) == fn) {
String nem = i.getMnemonicString();
if (nem.equals( "CALL" )) {
Object[] target_address = i.getOpObjects(0);
if (target_address[0].toString().equals( "EBP" )) {
// Do your deobfuscation here.
}
}
i = i.getNext();
} |
让我逐行解释这段代码的工作原理:
- 它获取包含当前地址(选中地址)的函数(第 01 行)。
- 还获取当前地址的指令(第 02 行)。
- 执行一个从当前指令到函数末尾的循环(第 03 行)。
- 获取指令的助记符(第 04 行)。
- 检查助记符是否对应于 CALL 指令,这是我们感兴趣的指令类型(第 05 行)。
- 还检索指令的操作数(第 06 行)。
- 由于混淆的调用是相对于存在哈希表的 EBP 地址的,我们检查 EBP 是否是一个操作数(第 07 行)。
- 去混淆例程必须在这一行实现(第 08 行)。
- 检索下一条指令(第 11 行)。
在本节中,你学习了如何使用 Ghidra API 以 Java 语言实现脚本。在下一节中,你将学习如何使用 Python 做同样的事情,我们将在 Ghidra 脚本的背景下比较这两种语言
3. 使用 Python 编写脚本
如果我们使用 Python 重写去混淆代码框架,它看起来如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
fn = getFunctionAt(currentAddress) i = getInstructionAt(currentAddress) while getFunctionContaining(i.getAddress()) == fn:
nem = i.getMnemonicString()
if nem == "CALL" :
target_address = i.getOpObjects(0)
if target_address[0].toString() == 'EBP' :
# Do your deobfuscation here.
i = i.getNext()
|
正如你所看到的,它与 Java 类似,因此不需要额外的解释。
要开发 Ghidra 脚本,不需要记住所有函数。唯一重要的是要清楚你想做什么,并找到必要的资源,例如文档,以定位正确的 API 函数。
Python 是一种非常棒的语言,拥有一个开发库和工具的出色社区。如果你想快速编写代码,Python 是一个很好的选择。不幸的是,Ghidra 并没有集成纯 Python 实现。Ghidra 主要用 Java 实现,然后通过 Jython 移植到 Python。
理论上,你可以随意选择使用 Python 或 Java,但实际上,Jython 存在一些问题:
- Jython 依赖于 Python 2.x,而 Python 2.x 已被弃用。
- 有时,某些功能在 Java 中按预期工作,但在 Jython 中不起作用。以下是一些示例:
https://github.com/NationalSecurityAgency/ghidra/issues/1890
https://github.com/NationalSecurityAgency/ghidra/issues/1608
鉴于上述提到的种种因素,选择哪种语言来编写脚本取决于你:是选择更稳定的 Java,还是选择更快速但可能不太稳定的 Python。 不妨亲自评估一下这两种方案,然后做出最适合你的选择!
4. 使用脚本对恶意软件样本进行去混淆
在上一章中,我们展示了 Alina 如何将 shellcode 注入到 explorer.exe 进程中。我们通过简单地读取字符串来分析这一点,这是一种快速、实用的方法,但我们可以更准确地进行分析。让我们看看一些 shellcode 的细节。
4.1 Delta Offset
在注入代码时,代码被放置在一个在开发时未知的位置。因此,无法使用绝对地址访问数据;相反,必须通过相对位置访问。shellcode 在运行时检索当前地址。换句话说,它试图获取 EIP 寄存器的值。
在 x86 架构(32 位)中,EIP 寄存器的目的是指向要执行的下一条指令;因此,它控制程序的执行流程。它决定了要执行的下一条指令。
但是,由于 EIP 寄存器是隐式控制的(通过控制转移指令、中断和异常),它不能直接访问,因此恶意软件通过执行以下技术来检索它:
执行一个指向 5 字节外地址的 CALL 指令。因此,CALL 指令会执行两个更改:
- 它将返回地址(下一条指令的地址)压入堆栈,即 0x004f6105:
- 它将控制权转移到目标地址:
然后,它通过 POP EBP 恢复存储在堆栈中的地址。该指令执行以下操作:
- 它移除最后压入堆栈的值:
- 它将值存储在目标寄存器中,本例中为 EBP:
最后,它从 EBP 寄存器中减去 0x5,以获取存储在 EBP 中的 EIP 值(这是我们在执行 CALL 指令时的 EIP 值,而不是当前的 EIP 值):
通过使用这种技巧,恶意软件开发者可以利用 EBP 寄存器(shellcode 的起始位置)加上一个偏移量来引用数据值。 使用这种技术,生成的代码与位置无关;无论你将 shellcode 放在哪个位置,它都能正常工作。
你可以在下面的代码片段中验证这一点:
这种技巧通常被称为 Delta Offset。在本例中,它用于计算 API 哈希码表的位置,该表位于相对于 shellcode 起始地址的 0x5e2 偏移处:
之后,一个函数负责将 Kernel32 API 函数的哈希值替换为函数地址,从而允许你从程序中调用它。
一旦替换完成,许多调用都是通过这个哈希表的偏移量完成的,该哈希表现在已转换为 API 地址表:
正如你所看到的,反汇编显示了指向 EBP 相对偏移量的 CALL 指令。更希望看到的是被调用函数的名称。我们的目标是改进反汇编以显示函数名称,但作为第一步,在下一节中,你将学习如何将 API 哈希值替换为相应的 API 函数地址。
赞赏
- [翻译]Ghidra基于脚本的恶意软件分析 1956
- [翻译]基于 Ghidra 脚本的逆向工程任务自动化 1776
- [翻译]使用Ghidra对恶意软件进行逆向分析 1362
- [原创]APC与Early Bird注入 3070
- [原创]Windows格式化字符串漏洞利用简单示例 8257