使用 Radare2 和 x86dbg 分析 LoadLibraryA 堆栈字符串混淆技术
英文原文:https://www.archcloudlabs.com/projects/loadlibrary-analysis/今天,我们将分析 Arch Cloud Labs 恶意软件收集系统 "Archie" 最近发现的恶意二进制文件,此二进制文件利用 LoadLibraryA 函数在运行时解析 DLL 以获得附加功能。恶意软件样本通常这样做是为了确保导入表中的信息有限,以避免触发静态规则检测或逃避 EDR 产品。这个特殊的样本让我觉得很有趣,因为 Ghidra 没有正确反汇编使用的堆栈字符串混淆方法。快速查看 radare2 和它 "破坏" (未被 Ghidra 的 AutoAnalysis 识别) 之间的反汇编差异以引起你的兴趣可以在下图中看到。
这不是对二进制文件的完整分析,相反,这是对恶意软件作者如何实施调用 LoadLibraryA 的孤立观察,以及在工具损坏时理解程序集的重要性,如上图所示。
如果你在家里跟着,这个二进制文件可以通过 Malshare 下载。
让我们开始吧!
使用 Radare2 对二进制文件进行分类
在查看来自 malshare.com 的每日恶意软件样本转储时,我通常首先使用 radare2 对二进制文件进行分类。这样做是因为在将所有内容加载到 Ghidra 或 IDA 之前,很容易快速查看函数、转储字符串和反汇编有趣的部分。此外,由于 radare2 是一个命令行实用程序,它可以让我快速对样本进行分类,以便找到一个有趣的样本。毕竟,这是一个将恶意软件分析作为业余爱好的爱好者网站。一个人如何度过他们日益减少的空闲时间与你把它花在什么上一样重要,在上一篇文章中有更多内容。 顺便说一句,让我们看一下通过以下方式打开和分析二进制文件:r2 malware.exe。
接下来,我们将执行分析命令来识别函数、外部参照、符号等 ……
> aaa
现在分析已经完成,让我们执行一些初始分类,例如检查文件部分。
通常对于商用 Linux 恶意软件,如果某个部分名为 "UPX0",你会在这里看到使用了 UPX 指示符。虽然我们没有看到任何打包的迹象,但我们确实看到了一个相对较小的 .text 部分,一个较大的资源部分。在正常情况下,这可能只是一个小的 hello-world 程序,其中嵌入了一些 PNG,导致部分大小不一致。然而,VirusTotal 表明该文件确实是恶意的,75 家供应商中有 57 家认为它是恶意的。让我们暂时停止查看 VirusTotal,看看我们是否可以做一些进一步的分析来确定核心功能。
字符串
Radare2 可以通过 iz 命令显示字符串。将此输出通过管道传输到 more 允许滚动浏览大量数据,就像在 Linux 上浏览任何其他文件一样。我们在文件中看到的第一个字符串是对 deflate 的引用。通过谷歌快速搜索下面的版权字符串,可以找到 zlib 源代码。这个字符串工件告诉我们,二进制文件中可能有要解压缩的数据。已压缩的数据在二进制文件中具有更高的熵率。p== 的 radare2 选项将以图形形式打印整个二进制文件的熵。其输出可以在下面的第二张图片中看到。这个二进制文件中显然有一些压缩数据,让我们在分析时记住这一点。
nthpaddr vaddr len size section type string
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x000090b8 0x004090b8 5253 .rdataascii deflate 1.2.2 Copyright 1995-2004 Jean-loup Gailly
1 0x00009168 0x00409168 5 6 .rdataascii 1.2.2
> p==
█
█
█ █
█ █
█ █
█ █ █
█ █ █ █
█ ██ █ █ █ █
█ ██ █ █ █ █
██ ██ █ █ █ █
██ ██ █ █ █ █
██ ██ █ █ █ █
██████████████████████████████████████████████████████████████████████████████
查看符号
符号告诉我们二进制文件导入了哪些函数,这些功能是该给定恶意软件样本可以执行的基础功能的关键指标。通过 > 列出二进制文件中的符号显示了从 Kernel32 导入的一些有趣的函数。这些函数可以在下面的 radare2 输出中看到:
> is
nth paddr vaddr bind type size lib name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1 0x00009000 0x00409000 NONE FUNC 0 KERNEL32.dll imp.CreateDirectoryA
2 0x00009004 0x00409004 NONE FUNC 0 KERNEL32.dll imp.CloseHandle
3 0x00009008 0x00409008 NONE FUNC 0 KERNEL32.dll imp.WriteFile
4 0x0000900c 0x0040900c NONE FUNC 0 KERNEL32.dll imp.CreateFileA
5 0x00009010 0x00409010 NONE FUNC 0 KERNEL32.dll imp.GetTempPathA
6 0x00009014 0x00409014 NONE FUNC 0 KERNEL32.dll imp.GetModuleFileNameA
7 0x00009018 0x00409018 NONE FUNC 0 KERNEL32.dll imp.ReadFile
8 0x0000901c 0x0040901c NONE FUNC 0 KERNEL32.dll imp.GetFileSize
9 0x00009020 0x00409020 NONE FUNC 0 KERNEL32.dll imp.GetProcAddress
10 0x00009024 0x00409024 NONE FUNC 0 KERNEL32.dll imp.LoadLibraryA
11 0x00009028 0x00409028 NONE FUNC 0 KERNEL32.dll imp.GetModuleHandleA
12 0x0000902c 0x0040902c NONE FUNC 0 KERNEL32.dll imp.GetStartupInfoA
看到什么有趣的东西了吗?我们可以根据这个二进制文件中确实存在的导入信息开始形成什么假设?也许创建了一个临时目录,也许将内容写入文件,然后我们从该文件加载数据?嗯,那个资源部分很大,也许有些东西正在从那里转储到文件中?谁知道!让我们进一步检查。
XRefs w/ Radare2
现在有了我们感兴趣的函数,分析它们在二进制文件中的调用位置,看看周围代码块中是否有任何内容揭示了有关该恶意软件功能的更多信息。
首先,跳转到 Kernel32 的 WriteFile 查看写入磁盘的内容。
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile
MSDN 文档显示 section 参数是要写入磁盘的数据缓冲区。 这使我们能够回溯到所述数据并确定内容是什么。 这里的整体分析流程是:
[*]找到有趣的功能;
[*]在代码中找到函数的 XREF;
[*]了解函数的参数,以及如何将参数传递给函数 (x86 与 x64 调用约定);
下面的 radare2 输出 "寻找" (s) 到 WriteFile 偏移量 (请注意,可以使用 tab 键自动完成,例如:sym.imp.<tab>)。接下来,通过 axt 命令打印对该符号的函数调用。在这里我们看到 WriteFile 在这个应用程序中发生在四个不同的地方。
> s sym.imp.KERNEL32.dll_WriteFile
> axt
(nofunc) 0x402467 call dword
(nofunc) 0x4024ce call dword
(nofunc) 0x402a79 call dword
(nofunc) 0x402ac9 call dword
//radare2 tip
// If you're ever curious about a given radare2 function or command line flag,
// you can always use ? after a command to get more information.
现在去寻找这些地址,然后切换到可视化显示模式以查看反汇编。
> s0x402467
> v!
Neat-o burrito,我们在进行 WriteFile 调用的一些反汇编过程中轻拍了一下。Radare2 甚至足以对从反汇编传递的这些文件函数参数的写入进行注释。作为对 fastcall 的 x86 调用约定的快速复习,参数从右向左推送。所以第一个参数被推到最后。Radare2 将为我们注释其中的一些参数。
包含要写入的数据的参数是寄存器 ebp 的偏移量,在没有动态运行程序的情况下跳转到那里并没有向我们显示任何有用的数据,因为这是在引用堆栈上的局部变量。因此,为了确定写入磁盘的内容,必须运行这个恶意软件样本。但是,我们仍处于此示例的静态分析部分,因此在在隔离的 VM 环境中执行此二进制文件之前,让我们进行更多探索。
XRefs to LoadLibraryA
如前所述,LoadLibrary 方法启用了在运行时加载 DLL 的能力,根据我们目前看到的数据,了解以下内容:
[*]二进制调用 WriteFile 四次;
[*]有对压缩库的字符串引用;
[*]二进制文件中的高熵部分表示压缩;
[*]有一个非常大的 .rscr 部分;
在这一点上,可以开始对正在发生的事情提出一些可能的假设。但是,证据在 pudding 中,或者在本例中是实际运行二进制文件的输出。现在,让我们继续使用 LoadLibraryA 进行外部参照分析。
> s sym.imp.KERNEL32.dll_LoadLibraryA
> axt
fcn.0040343b 0x40346e mov ebx, dword
在地址 0x0040343b 定义的函数中调用一个加载库的函数。可以看到符号被移动到寄存器 ebx 中,这一点很重要。 让我们寻找这个调用并分析反汇编。
> s fcn.0040343b
; CALL XREF from main @ +0x406
┌ 534: fcn.0040343b ();
│ ; var int32_t var_144h @ esp+0x90
│ ; var int32_t var_140h @ esp+0x94
│ ; var int32_t var_138h @ esp+0x9c
│ ; var int32_t var_12ch @ esp+0xa8
│ ; var int32_t var_fch @ esp+0xd8
│ ; var int32_t var_f4h @ esp+0xe0
│ ; var int32_t var_ech @ esp+0xe8
│ ; var int32_t var_e4h @ esp+0xf0
│ ; var int32_t var_d8h @ esp+0xfc
│ ; var int32_t var_cch @ esp+0x108
│ 0x0040343b 81ec30010000 sub esp, 0x130
│ 0x00403441 53 push ebx
│ 0x00403442 55 push ebp
│ 0x00403443 56 push esi
│ 0x00403444 57 push edi
│ 0x00403445 6a00 push 0
......................................abbreviated output ...............................................................
现在事情变得有趣了!在下面的反汇编中,会看到许多 ASCII 字符被压入堆栈。向上看地址 0x004034a1 你会看到这种看起来像 ShellExecuteA,但有些字符丢失。如果我们仔细观察 0x00403486,堆栈顶部的值将被弹出 (pop ebp) 到 ebp 寄存器中。该值为十六进制 65,即 ASCII "e"。在我们看到 PUSH ebp 的任何地方,我们实际上是在将 "e" 的十六进制值压入堆栈以构建 "堆栈字符串"。
0x0040346e 8b1d24904000 mov ebx, dword ; =0xc7b2 reloc.KERNEL32.dll_LoadLibraryA
0x00403474 83c434 add esp, 0x34
0x00403477 8d442414 lea eax, dword
0x0040347b 50 push eax
0x0040347c ffd3 call ebx
0x0040347e 6a00 push 0
0x00403480 6a41 push 0x41 ; 'A' ; 65
0x00403482 6a65 push 0x65 ; 'e' ; 101
0x00403484 8bf8 mov edi, eax
0x00403486 5d pop ebp
0x00403487 8d8424800000.lea eax, dword
0x0040348e 55 push ebp
0x0040348f 6a74 push 0x74 ; 't' ; 116
0x00403491 6a75 push 0x75 ; 'u' ; 117
0x00403493 6a63 push 0x63 ; 'c' ; 99
0x00403495 55 push ebp
0x00403496 6a78 push 0x78 ; 'x' ; 120
0x00403498 6a45 push 0x45 ; 'E' ; 69
0x0040349a 6a6c push 0x6c ; 'l' ; 108
0x0040349c 6a6c push 0x6c ; 'l' ; 108
0x0040349e 55 push ebp
0x0040349f 6a68 push 0x68 ; 'h' ; 104
0x004034a1 6a53 push 0x53 ; 'S' ; 83 ; int32_t arg_8h
0x004034a3 50 push eax ; int32_t arg_4h
0x004034a4 e8ecdfffff call fcn.00401495
下面的输出与上面的输出相同,只是为了便于阅读而做了注释。
0x0040346e 8b1d24904000 mov ebx, dword ; =0xc7b2 reloc.KERNEL32.dll_LoadLibraryA
0x00403474 83c434 add esp, 0x34
0x00403477 8d442414 lea eax, dword
0x0040347b 50 push eax
0x0040347c ffd3 call ebx
0x0040347e 6a00 push 0
0x00403480 6a41 push 0x41 ; 'A' ; 65
0x00403482 6a65 push 0x65 ; 'e' ; 101
0x00403484 8bf8 mov edi, eax
0x00403486 5d pop ebp ; // put 'e' into EBP
0x00403487 8d8424800000.lea eax, dword
0x0040348e 55 push ebp ; e
0x0040348f 6a74 push 0x74 ; 't' ; 116
0x00403491 6a75 push 0x75 ; 'u' ; 117
0x00403493 6a63 push 0x63 ; 'c' ; 99
0x00403495 55 push ebp ; e
0x00403496 6a78 push 0x78 ; 'x' ; 120
0x00403498 6a45 push 0x45 ; 'E' ; 69
0x0040349a 6a6c push 0x6c ; 'l' ; 108
0x0040349c 6a6c push 0x6c ; 'l' ; 108
0x0040349e 55 push ebp ; e
0x0040349f 6a68 push 0x68 ; 'h' ; 104
0x004034a1 6a53 push 0x53 ; 'S' ; 83 ; int32_t arg_8h
万岁!我们现在已经发现恶意软件作者如何使用汇编实现一个巧妙的技巧,以便能够构建字符串 LoadLibraryA 作为参数加载到恶意进程中。将 LoadLibraryA 放入 ebx 的那个 movinstruction 怎么样?如果我们继续查看这个代码块,我们会看到这个堆栈字符串技巧在调用地址 0x0401945 的另一个函数之前实现了几次。下图显示了通过调用 ebx 加载的 USER32.DLL,但没有任何堆栈字符串混淆。
查看 Ghidra 中的反汇编,我们发现 Ghidra 无法识别此功能所在的位置。
在定义函数时,Ghidra 的反编译也无法识别从堆栈字符串传递的值。要解决此问题,我们必须检查并修改 Ghidra 自动识别的数据类型。
我认为这突出了逆向工程的一个重要部分,没有工具是完美的,了解每个工具的左右边界以及它们如何失败将使您能够有效地进行故障排除,你不能总是依赖反编译是 100% 准确的。既然我们对这里玩的一些有趣的组装技巧感到模糊,那么对函数 00401945 的调用是什么?让我们探索一下。
分析未知函数
除了 LoadLibraryA 调用之外,反汇编程序还多次调用了地址为 0x401495 的函数让我们寻找这个未知函数,看看反汇编程序是什么样子的。
> s fcn.00401495
; XREFS(21)
┌ 23: fcn.00401495 (int32_t arg_4h, int32_t arg_8h);
│ ; arg int32_t arg_4h @ esp+0x4
│ ; arg int32_t arg_8h @ esp+0x8
│ 0x00401495 8b4c2404 mov ecx, dword ;// counter variable
│ 0x00401499 8d542408 lea edx, dword ;/
│ ; CODE XREF from fcn.00401495 @ 0x4014a9
│ ┌─> 0x0040149d 8a02 mov al, byte
│ ╎ 0x0040149f 84c0 test al, al
│ ╎ 0x004014a1 8801 mov byte , al
│ ┌──< 0x004014a3 7406 je 0x4014ab
│ │╎ 0x004014a5 41 inc ecx
│ │╎ 0x004014a6 83c204 add edx, 4
│ │└─< 0x004014a9 ebf2 jmp 0x40149d
│ │ ; CODE XREF from fcn.00401495 @ 0x4014a3
└ └──> 0x004014ab c3 ret
radare2 可以生成程序集的伪代码来帮助我们进行分析,让我们用 pdcto 生成这个帮助注释我们的分析
> pdc
function fcn.00401495 () {
//4 basic blocks
loc_0x401495:
//XREFS(21)
ecx = dword
edx = dword
do
{
loc_0x40149d:
//CODE XREF from fcn.00401495 @ 0x4014a9
al = byte // moving lower 8 bits of 32bit address into al register
var = al & al //this will set eax to 0
byte = al // overwrite value in ecx
if (!var) goto 0x4014ab//unlikely
} while (?);
return;
loc_0x4014a5:
ecx++
edx += 4
goto 0x40149d
}
好吧,这个功能有点用处,但不是什么超级有用的东西,堆栈上的值似乎正在发生一些字节交换,如果测试调用失败,则增加字节值。我们将跳到此处并逐步执行 x86dbg 中的此函数,以更好地了解发生了什么。
下面的第一张图片显示了多次通过未知函数将 Shell32.dll 的字符串值压入堆栈后的输出。
第二张图片显示 LoadLibraryA 被移动到 ebx,通过向其添加值 32 来 "增长" 堆栈,然后将 "Shell32.DLL" 从引用未知函数结果的偏移量移动到 eax。最后,eax 被压入堆栈,这是调用 ebx (LoadLibraryA) 之前 Shell32.DLL 的值。
至此,我们已经成功地对该二进制文件及其相关的混淆机制中的 LoadLibraryA 功能进行了逆向工程。现在,让我们继续我们的动态分析来识别正在写入磁盘的文件。
动态分析 - 通过解压工作
将感兴趣的样本作为 "malware.exe" 复制到 Windows-10 VM,我们看到资源部分确实包含可执行文件的 PNG。启动的程序可以在下面的第二张图片中看到。
使用 x86dbg 启动 malware.exe 使分析师能够指定放置断点的位置。鉴于我们对 LoadLibraryA 的兴趣,我们将在此函数上放置一个断点。继续执行二进制文件后,可以看到堆栈字符串技巧确实如前所述解析为特定的 DLL。下面的两个图像显示了其中几个正在加载的 DLL。
Spawning Sysinternals 的 ProcessExplorer 和 ProcessMonitor 揭示了两个二进制文件被写入磁盘并由此进程生成。因此,原来关于将附加文件写入磁盘的假设似乎被证明是正确的。
在 WriteFile 函数调用上放置断点并参考函数参数的 MSDN 文档,我们可以识别正在写入磁盘的内容,如下图所示。
现在的问题是,这些文件是从哪里来的呢?让我们在二进制文件中搜索一些压缩引用,下面的命令将我们带到一个字符串 "unknown_compression_method",并识别对这些字符串的引用。
> iz | grep 'compress'
17 0x0000d1bc 0x0040d1bc 2627 .data ascii unknown compression method
> s str.unknown_compression_method
> axt
(nofunc) 0x404a17 mov dword , str.unknown_compression_method
(nofunc) 0x404aee mov dword , str.unknown_compression_method
让我们寻找这些抵消并进一步探索。
s 0x404a17
v!
可以在偏移量 0x004048b0 处的函数中识别出更多表明正在使用某种类型的压缩例程的字符串工件。
在函数 0x00408b0 处设置断点,一直运行直到我们遇到函数的 ret 指令,我们看到现在奇迹般地出现了一个 MZ 头文件!一些使用压缩字符串工件的快速谷歌搜索显示压缩库是 zlib。
嵌入的文件
这篇文章越来越长,所以让我们执行嵌入的文件并将出站连接交叉引用到 VirusTotal 以识别恶意活动的任何线索,执行这些嵌入式文件会导致发现域 "dns3-domain[.]com"。
该域与许多恶意文件相关联,这些文件表明该恶意软件样本是木马,并且恶意负载将自身嵌入到其他应用程序中。根据我们在这里看到的情况,这是有道理的。根据这份微软报告,它的底层功能允许二进制文件执行远程命令。上面的 Virustotal 关系表显示了过去几年的大量通信文件。值得注意的是,大多数子域对恶意活动的检测为零。虽然嵌入式有效载荷并不是世界上最有趣的东西,但逆向工程之旅还是值得的。考虑一下在此过程中学到的技能,而旅程是构建技能组合的一部分。
为了结束这篇文章,我将把哈希值留在 IOC 部分!
IOCs
[*]585197476537724e04a6ba334f68458e malware.exe (original file)
[*]dc24611ea25fda4c3f1e6b1cfde2f319<unicode name>.exe (upx unpacked)
[*]d2f69c1ade1686668ec85ecb19c7384asmss.exe
[*]dns3[-]domain[.]com (https://www.virustotal.com/gui/domain/dns3-domain.com/relations)
页:
[1]