Ghidra基于脚本的恶意软件分析

admin 2025年4月6日00:44:39评论11 views字数 9059阅读30分11秒阅读模式

本文是对国外逆向工程师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 指令。你还需要过滤函数,以筛选你需要的内容,最后,当然,你需要执行去混淆操作:

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();
}

让我逐行解释这段代码的工作原理:

1.它获取包含当前地址(选中地址)的函数(第 01 行)。

2.还获取当前地址的指令(第 02 行)。

3.执行一个从当前指令到函数末尾的循环(第 03 行)。

4.获取指令的助记符(第 04 行)。

5.检查助记符是否对应于 CALL 指令,这是我们感兴趣的指令类型(第 05 行)。

6.还检索指令的操作数(第 06 行)。

7.由于混淆的调用是相对于存在哈希表的 EBP 地址的,我们检查 EBP 是否是一个操作数(第 07 行)。

8.去混淆例程必须在这一行实现(第 08 行)。

9.检索下一条指令(第 11 行)。

在本节中,你学习了如何使用 Ghidra API 以 Java 语言实现脚本。在下一节中,你将学习如何使用 Python 做同样的事情,我们将在 Ghidra 脚本的背景下比较这两种语言。

3

使用 Python 编写脚本

如果我们使用 Python 重写去混淆代码框架,它看起来如下:

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:

Ghidra基于脚本的恶意软件分析

◆它将控制权转移到目标地址:

Ghidra基于脚本的恶意软件分析

然后,它通过 POP EBP 恢复存储在堆栈中的地址。该指令执行以下操作:

◆它移除最后压入堆栈的值:

Ghidra基于脚本的恶意软件分析

◆它将值存储在目标寄存器中,本例中为 EBP:

Ghidra基于脚本的恶意软件分析

最后,它从 EBP 寄存器中减去 0x5,以获取存储在 EBP 中的 EIP 值(这是我们在执行 CALL 指令时的 EIP 值,而不是当前的 EIP 值):

Ghidra基于脚本的恶意软件分析

通过使用这种技巧,恶意软件开发者可以利用 EBP 寄存器(shellcode 的起始位置)加上一个偏移量来引用数据值。使用这种技术,生成的代码与位置无关;无论你将 shellcode 放在哪个位置,它都能正常工作。

你可以在下面的代码片段中验证这一点:

Ghidra基于脚本的恶意软件分析

这种技巧通常被称为 Delta Offset。在本例中,它用于计算 API 哈希码表的位置,该表位于相对于 shellcode 起始地址的 0x5e2 偏移处:

Ghidra基于脚本的恶意软件分析

之后,一个函数负责将 Kernel32 API 函数的哈希值替换为函数地址,从而允许你从程序中调用它。

一旦替换完成,许多调用都是通过这个哈希表的偏移量完成的,该哈希表现在已转换为 API 地址表:

Ghidra基于脚本的恶意软件分析

正如你所看到的,反汇编显示了指向 EBP 相对偏移量的 CALL 指令。更希望看到的是被调用函数的名称。我们的目标是改进反汇编以显示函数名称,但作为第一步,在下一节中,你将学习如何将 API 哈希值替换为相应的 API 函数地址。

4.2 将 API 哈希值转换为地址

以下函数负责将函数的哈希值替换为相应的函数地址:

Ghidra基于脚本的恶意软件分析

上述代码遍历从 kernel32.dll 库的导出表的 AddressOfNames 部分提取的每个 API 名称。

如果你有一些分析 PE 文件的背景,就很容易识别上述功能,因为代码中的一些偏移量非常引人注目。让我们看看之前的 apiHashesToApiAdresses 反汇编中显示的偏移量与可移植可执行文件格式字段之间的对应关系:

◆0x3c 对应于 e_lfanew 字段,表示可移植可执行文件头的相对虚拟地址(RVA)。

◆0x78 是导出表的 RVA。

◆0x20 是导出表中名称指针表的 RVA。

◆0x1c 是导出表中地址表的 RVA。

◆0x24 是导出表中序数表的 RVA。

◆0x18 是名称数量的 RVA,这是循环迭代的最大次数。

图 6.9 中的第 21 和 22 行是用于去混淆目的的代码的关键部分。在这些提到的行中,对 API 的每个字符应用了一系列逻辑操作。这一系列操作可以很容易地翻译成 Python,如下面的 Python shell 命令列表所示:

>>> apiname = "lstrlenW"

>>> hash = 0

>>> for c in apiname:

... hash = hash << 7 & 0xffffff00 | ( (0xFF&(hash << 7)) | (0xFF&(hash >> 0x19)) ^ ord(c))

...

>>> print(hex(hash))

0x2d40b8f0L

让我解释一下这四个 Python 命令:

1.我们将 lstrlenW 字符串存储在 apiname 变量中,因为我们想计算它的哈希值。通过这种方式,我们正在测试我们的 Python 代码在真实的 kernel32.dll API 名称上的表现。

2.我们将哈希值初始化为 0。这是此哈希算法的第一步。

3.我们遍历 lstrlenW 字符串的每个字符(变量 c),同时根据哈希算法更新 hash 变量值。

4.我们最终使用十六进制表示法打印哈希值。请注意,哈希值末尾的 L 字符表示长整型数据类型,它不属于哈希值。

当然,上述代码也可以翻译成 Java:

class AlinaAPIHash {
public static void main(String args[]) {
int hash = 0;
String apiName = "lstrlenW";

for (int i = 0; i < apiName.length(); i++) {
hash = ((hash << 7) & 0xFFFFFF00) | ((hash << 7) & 0xFF) | ((hash >> 0x19) & 0xFF) ^ apiName.charAt(i);
System.out.println(String.format("0x%08X", hash));
}

System.out.println(String.format("0x%08X", hash));
}
}

在本节中,你了解了 API 哈希的工作原理,以及如何将算法从汇编语言翻译成 Python 和 Java。在下一节中,我们将使用上述代码来解析被调用函数的名称,并将其放入反汇编列表中。

4.3 使用 Ghidra 脚本对哈希表进行去混淆

在自动对程序进行去混淆之前,我们需要 Kernel32.dll 导出的 API 函数名称的完整列表。你可以在专门的 GitHub 中找到以下脚本(get_kernel32_exports.py),它使用 Python 的 pefile 模块来实现这一目的:

import pefile

pe = pefile.PE("c:\windows\system32\kernel32.dll")
exports = set()

for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
exports.add(exp.name.encode('ascii'))

上述代码执行以下操作:

1.导入 pefile 模块,使其能够解析 PE 文件格式,这是 32 位和 64 位 Microsoft Windows 操作系统中用于可执行文件、目标代码、DLL 等的文件格式。

2.将解析后的 Kernel32.dll 可移植可执行文件实例存储在 pe 中。

3.创建一个空的导出集,用于存储 Kernel32.dll 导出的函数。

4.遍历 Kernel32.dll 导出的函数。
检索导出函数的名称(使用 ASCII 字符编码)并将其添加到导出集中。

上述脚本生成的结果是一个包含 Kernel32 导出的集合,如下面的部分输出所示:

exports = set(['GetThreadPreferredUILanguages', 'ReleaseMutex', 
'InterlockedPopEntrySList', 'AddVectoredContinueHandler',
'ClosePrivateNamespace', … ])

Finally, we can put all the pieces together in order to automate the task of resolving hashed Kernel32 API addresses:

最后,我们可以将所有部分组合在一起,以自动化解析哈希的 Kernel32 API 地址的任务:

from ghidra.program.model.symbol import SourceType
from ghidra.program.model.address.Address import *
from struct import pack

exports = set(['GetThreadPreferredUILanguages', 'ReleaseMutex', 'InterlockedPopEntrySList', 'AddVectoredContinueHandler', 'ClosePrivateNamespace', 'SignalObjectAndWait', …])
def getHash(provided_hash):
for apiname in exports:
hash = 0
for c in apiname:
hash = (hash << 7 & 0xffffff00) | ((0xFF & (hash << 7)) | (0xFF & (hash >> 0x19))) ^ ord(c)
if provided_hash == pack('<L', hash):
return apiname
return ""

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':
current_hash = bytes(pack('<L', getInt(currentAddress.add(int(target_address[1].toString(), 16))))
current_function_from_hash = getHash(current_hash)
setEOLComment(i.getAddress(), current_function_from_hash)
print(i.getAddress().toString() + " " + nem + "[EBP + " + target_address[1].toString() + "]" + " -> " + current_function_from_hash)
i = i.getNext()

总结一下,我们正在执行以下操作:

1.声明 Kernel32 API 名称的集合。

2.查找与提供的哈希值匹配的 API 名称。

3.遍历函数,寻找混淆的调用。

4.分别设置注释并打印函数名称。

脚本的执行会在反汇编列表中产生以下更改(关于被调用函数的注释):

Ghidra基于脚本的恶意软件分析

显示函数名称比什么都不显示要好,但显示符号更好,因为它们不仅显示名称,还引用函数。在下一节中,你将看到如何添加这一改进。

4.4 改进脚本结果

你还可以通过添加必要的 Kernel32 符号来改进结果。例如,你可以在 Symbol Tree 窗口中查找 CreateFileA 符号:

Ghidra基于脚本的恶意软件分析

将此符号附加到当前程序,并通过双击它来访问函数地址:

Ghidra基于脚本的恶意软件分析

然后,使用 Ctrl + Shift + G 键组合修补 CALL 指令:

Ghidra基于脚本的恶意软件分析

用之前查到的 CreateFileA 地址修补它:

Ghidra基于脚本的恶意软件分析

按 R 键并将此引用设置为 INDIRECTION:

Ghidra基于脚本的恶意软件分析

经过此修改后,代码被修改,允许 Ghidra 在分析代码时识别函数参数、识别对函数的引用等,这总是比添加注释更好。在下图中,你可以看到生成的反汇编列表:

Ghidra基于脚本的恶意软件分析

正如你所看到的,脚本在分析恶意软件时非常有用,因为字符串去混淆、解析 API 地址、代码去混淆等重复性任务可以通过编写几行简单的代码完全自动化。

此外,你编写的脚本越多,你的效率就会越高,并且你可以为未来的脚本和项目重用更多的代码。

5

总结

在本章中,你学习了如何使用脚本在 Ghidra 中更高效地分析恶意软件。我们使用脚本来超越静态分析的限制,并解析一些在运行时计算的 API 函数哈希值。

你还了解了在开发脚本时使用 Python 或 Java 的优缺点。

你学习了如何将汇编语言算法翻译成 Java 和 Python,并在开发第一个非常有用的脚本时学习了脚本编写技能。通过使用提供的 Ghidra Flat API 函数分类,你现在能够快速识别自己脚本所需的 Ghidra API 函数,而无需记住或在文档中浪费时间查找函数。

在本书的下一章中,我们将介绍 Ghidra 的无头模式(headless mode),这在某些情况下非常有用,例如分析大量二进制文件或单独使用 Ghidra 将其与其他工具集成。

资源链接:https://bbs.kanxue.com/thread-264947-1.htm

Ghidra基于脚本的恶意软件分析

看雪ID:ZyOrca

https://bbs.kanxue.com/user-home-944427.htm

*本文为看雪论坛优秀文章,由 ZyOrca 原创,转载请注明来自看雪社区

原文始发于微信公众号(看雪学苑):Ghidra基于脚本的恶意软件分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月6日00:44:39
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Ghidra基于脚本的恶意软件分析https://cn-sec.com/archives/3919582.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息