|
DRIDEX 是最著名和最流行的银行木马之一,其历史可以追溯到 2014 年末。在改进和变体过程中,DRIDEX 成功地针对金融服务部门进行窃取信息和关键用户凭据。DRIDEX 通常以包含恶意 VBA 宏的 Word 和 Excel 文档的形式通过网络钓鱼传送。
在本篇文章中,我们将深入探讨 DRIDEX 使用字符串哈希和 VEH 处理混淆 Windows API 调用的反分析方法背后的理论。
要继续,可以在 MalwareBazaar 上获取样本:ad86dbdd7edd170f44aac99eebf972193535a9989b06dccc613d065a931222e7
样本也可以在本文末下载!
一、API 解析函数
通过对样本进行一些基本的静态分析,可以很快看到 DRIDEX DLL 在其导入表中有两个函数,OutputDebugStringA 和 Sleep。 考虑到 DRIDEX 是一个具有许多复杂功能的大型恶意软件,缺少 import hits,恶意软件动态解析其大部分 API。
当进入 DLL 的入口点时,可以立即看到一个以两个哈希值作为参数调用的函数。入口点函数调用此函数两次,两次都使用相同的第一个参数值。对于第二次调用,返回值作为函数调用,因此我们知道 sub_6815C0 必须通过其参数的哈希值来动态解析 API。此外,由于两个调用的第一个参数共享相同的值,但第二个参数的值不同,可以假设第一个哈希对应于库名称,第二个对应于该库中目标 API 的名称。
我们可以进一步检查 sub_6815C0 以确认这一点。该子例程首先将 DLL 哈希传递给函数 sub_686C50 和 sub_687564。然后将返回值和 API 哈希值作为参数传递给 sub_6867C8。由此,可以假设前两个函数检索与 DLL 哈希对应的 DLL 的基址,并将该基址传递给带有 API 哈希的最后一个函数以解析 API。
在深入研究 sub_687564 时,可以看到 DRIDEX 从 PEB (进程环境块) 访问加载程序数据表,其中包含加载程序数据表条目的双向链表。这些条目中的每一个都包含有关内存中已加载库的信息,因此,通过遍历表,恶意软件提取每个库的名称,将其转换为小写,最后使用 sub_69D620 对其进行哈希并与 0x38BA5C7B 进行异或。每个库的哈希与目标哈希进行比较,如果能找到则返回目标库的基地址。这证实了 sub_687564 检索了与给定 DLL 哈希对应的 DLL 的基址。
类似地,在 sub_6867C8 中,DRIDEX 使用目标库的基地址访问其导出表并遍历包含导出名称地址的列表。由于 API 名称在导出表中存储为 UNICODE 字符串,因此恶意软件将每个 API 的名称转换为 ASCII 并使用相同的哈希函数 sub_69D620 对其进行哈希。在与每个 API 名称的哈希进行比较之前,目标 API 哈希与 0x38BA5C7B 进行异或运算。这向我们证实了 sub_6867C8 使用给定的哈希从目标库动态检索 API。
二、识别 API 哈希算法
此时,我们知道 sub_69D620 是哈希算法,最终的哈希是通过将函数的返回值与 0x38BA5C7B 进行异或来产生的。该函数的核心功能包含处理 XMM 寄存器的 SSE 数据传输指令。
通常,不值得花时间分析这些加密函数中的汇编指令。在大多数情况下,可以依靠程序中加载或使用的常量值来选择正确的算法,而 Mandiant 的 capa 等工具在帮助我们自动化这个过程方面非常棒。不幸的是,capa 无法识别这个特定的算法,所以我们必须自己分析常量。
幸运的是,在此函数中使用的三个常量中,一个突出的是值 0x0EDB8832,该值通常用于 CRC32 哈希算法。因此,可以假设 sub_69D620 是一个从给定字符串生成 CRC32 哈希的函数,而 DRIDEX 的 API 哈希算法归结为对 API/DLL 名称的 CRC32 哈希与 0x38BA5C7B 进行异或运算。
为了快速检查这个哈希算法是否正确,可以使用 OALabs 的 hashdb 插件来测试解析在 DLL 的入口点函数中解析的 API。首先,由于 DRIDEX 的哈希有一个额外的 XOR 层,我们必须在使用 CRC32 查找哈希之前将 0x38BA5C7B 设置为 hashdb 的 XOR 键。
最后,可以使用 hashdb 来查找样本中的哈希值。在这里,可以看到哈希 0x1DAACBB7 与 ExitProcess API 正确对应,这向我们证实了我们对哈希算法的假设是正确的。
三、VEH - 向量异常处理程序
与大多数恶意软件不同,DRIDEX 不使用 call 指令来调用 API。相反,恶意软件使用 int3 和 retn 指令的组合在动态解析它们后调用其 Windows API。
这个特性是一个很好的反分析技巧,因为它使静态和动态分析都变得更加困难。由于 retn 指令,IDA 将每个 API 调用视为父函数的结束。这使得它背后的所有指令都无法访问,并破坏了函数反编译代码的控制流。
int3 指令也会减慢动态分析的速度,因为像 x64dbg 这样的调试器将中断注册为异常,而不是将其吞入正常断点以避免调试器检测。这需要分析人员在调试时手动跳过 int3 指令或将其传递给系统的异常处理程序。
为了正确调用 API,DRIDEX 从哈希动态解析它,将 API 的地址存储在 eax 中,将 API 的参数压入堆栈,并执行如上所示的 int3。然而,恶意软件并没有使用系统的异常处理程序来处理这个中断,而是通过在 DLL 入口点函数的开头调用 sub_687980 来注册自己的自定义处理程序。
函数 sub_687980 动态解析 RtlAddVectoredExceptionHandler 并调用它以将 sub_687D40 注册为 VEH,这意味着当程序遇到 int3 指令时,内核会调用 sub_687D40 来处理中断并将控制权转移到存储在 eax 中的 API。
处理程序代码首先检查异常信息结构以查看它正在处理的异常类型,如果类型为 EXCEPTION_ACCESS_VIOLATION、EXCEPTION_STACK_OVERFLOW、STATUS_HEAP_CORRUPTION,DRIDEX 解析 TerminateProcess API 并调用它通过 int3 中断来终止自身。
对于向量异常处理,系统处理程序和用户注册的处理程序放置在向量或链中。异常通过此链上的处理程序传递,直到正确处理它并将控制返回到它发生的点。在 DRIDEX 的处理程序中,如果类型不是 EXCEPTION_BREAKPOINT (由 int3 调用),则处理程序返回 0 (EXCEPTION_CONTINUE_SEARCH) 以将异常传递给另一个处理程序。
最后,如果异常类型为 EXCEPTION_BREAKPOINT,则处理程序在 eax 中设置要调用的 API。当异常发生时,系统将控制权从触发异常的用户线程转移到内核线程来执行异常处理程序。当上下文切换发生时,系统在执行内核线程之前将来自用户线程的所有寄存器保存在内存中,以便在处理程序完成时正确恢复它们。对于向量异常处理,包含其寄存器的用户线程的上下文记录存储在作为参数传递给处理程序的 EXCEPTION_POINTERS 结构中。
使用此结构,DRIDEX 的处理程序访问上下文记录并增加 eip 值,使其指向 int3 之后的 retn 指令。因为 eip 在处理程序完成后从上下文记录中恢复,这将设置用户线程在异常处理后在 retn 指令处开始执行。接下来,将 retn 之后的指令地址和来自 eax 的 API 地址连续压入栈中。
下面是当前用户堆栈在此处理程序末尾的样子:
在处理程序返回 EXCEPTION_CONTINUE_EXECUTION 代码后,用户线程的上下文被恢复,恶意软件开始在 retn 指令处执行。因为 retn 指令弹出栈顶的值并跳转到它,恶意软件会跳转到解析的 API 的地址。这将成为函数调用的正常堆栈帧,其中 esp 指向返回地址和在堆栈上正确设置的参数。当 API 返回时,DRIDEX 在 retn 指令之后的地址继续执行。
四、编写脚本来给 DLL 打补丁
在了解了 DRIDEX 如何使用 VEH 调用 API 之后,我们可以通过修改所有 int3,retn 序列 (0xCCC3) 来调用样本中的 eax 指令 (0xFFD0),从而以编程方式 patch 样本以绕过 IDA 和调试器中的此反分析功能 .text 部分。这应该使 IDA 的反编译工作良好,同时防止执行在我们的调试器中被中断!
- import pefile
-
- file_path = '<DRIDEX SAMPLE PATH>'
-
- file = open(file_path, 'rb')
- file_buffer = file.read()
- file.close()
-
- dridex_pe = pefile.PE(data=file_buffer)
-
- text_sect_start = 0
- text_sect_size = 0
- for section in dridex_pe.sections:
- if section.Name.decode('utf-8').startswith('.text'):
- text_sect_start = dridex_pe.get_offset_from_rva(section.VirtualAddress)
- text_sect_size = section.SizeOfRawData
-
- patched_text_sect = file_buffer[text_sect_start:text_sect_start+text_sect_size].replace(b'\xCC\xC3', b'\xFF\xD0')
-
- file_buffer = file_buffer[0:text_sect_start] + patched_text_sect + file_buffer[text_sect_start+text_sect_size:]
-
- out_path = '<OUTPUT PATH>'
- out_file = open(out_path, 'wb')
- out_file.write(file_buffer)
- out_file.close()
复制代码
文中涉及到的样本 (ZIP password: infected):
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|