前言
多年来,我持续在信息安全领域开展工作,为客户和SOC提供支持,在此过程中我持续接触着大量的告警和事件。其中,就存在一些值得关注的事件。近期,我们偶然发现了一个特定的Smoke Loader恶意软件样本。自2013年左右,Smoke Loader就开始流行,该恶意软件经常用于分发其他恶意组件或人工编写的代码。尽管这样的样本并不陌生,但这确实是一个很好的机会,可以让我们来重新审视这种威胁,并逐步了解一些内部的原理。
这一次告警是由于被判定为木马的可疑文件引起的,这个文件已经被清除并隔离。引起我好奇的是,在几个小时之间,总是同一个工作站的同一个用户产生告警。
我们已经了解到,该威胁并没有产生任何实际影响,并且已经被清除,因此我们可以直接对其进行研究。通过查看SentinelOne控制台,我们可以看到:
(1)检测到威胁的完整路径;
(2)关联的风险等级为“高”,这表明该威胁的准确度也比较高;
(3)根据文件独特的哈希值,可以结合任何公开的威胁指标(IoC)进行测试和判断。
我获得了这一恶意样本,并准备逐步开始对这个恶意软件进行逆向工程。
第一层:加壳的VB Win32程序
在获得了恶意样本之后,我迅速启动了装有Flare工具的隔离分析主机,并开始进行研究。初步看来,这个样本似乎是利用Win32 API的Visual Basic程序。
我们看看从标头还能得到什么。看起来很像是标准信息,我们根据其导入表确认了这是一个Visual Basic程序。
我们可以在二进制字符串中发现一些具有独特特征、方言化的单词,可以让我们联想到意大利南部的一种地方性方言。
在积累了一些经验之后,我们可以初步假定这个文件是Visual Basic的最外一层,这一层的作用是防止或尽可能减慢静态分析的过程。那么,这个文件在运行时展现出的行为是什么样的呢?
在运行时观察恶意样本,我们可以看到进程注入的过程,这种行为对于VB加壳工具来说是非常常见的,我觉得我们也不难搞定这一层。
击败Visual Basic加壳工具
我们不会在此花费太多的时间,关于如何对这类加壳工具进行脱壳的教程资源非常多,我在这里强烈建议大家阅读OALabs Youtube视频教程。我们必须在调试器内部的CreateProcessInternalW API上放置一个断点,从而在适当的时间停止执行。
此时,在内存中的某个位置,存在一个准备运行的PE文件,我们的任务就是找到这个文件。为此,我们可以搜索整个内存映射来寻找线索,我决定在PE内部搜索“DOS”子字符串,这个子字符串通常会包含在“This program cannot be run in DOS mode”(此程序无法在DOS模式下运行)的提示之中。
这个字符串的十六进制值为44 4F 53,我们搜索到了很多结果。
但是,我们只关注其中的一少部分结果。通常,可执行文件的加载地址为0x00400000,因此我们在0x0040006C处获得的结果,看起来非常像是我们想寻找的可执行文件本身。
然而,在0x002F0094位置出现了一些值得关注的情况,我们可以在内存转储中进行操作。
其中包含PE文件的内存区域,被影射为可执行、可读取、可写入,这绝对是我们的注入文件。
我们可以轻松地将该内存区域转储到文件中,清理MZ标头之前的垃圾内容并分析其标头。
这看起来像是合法的可执行文件,但问题在于——根本没有导入!这非常有趣。
第二层:静态分析
当我们在IDA Pro中加载这个新的可执行文件时,我们发现这是被反汇编的唯一代码块。
我们意识到,这里有一个XOR循环,将从地址0x00401567解码大小为0xCD字节的代码块,其XOR的密钥为0xCB。在循环结束的位置,将相同的起始地址0x00401567压入栈中,并使用RET指令将程序流分支到该栈中。
解码缓冲区
借助一些IDA脚本,我们可以对加密的缓冲区进行XOR运算,然后继续进行分析。
在对缓冲区进行XOR解密之后,我们发现这里结合使用了反汇编的防范技术和反调试技术。现在,可以映射代码块的目标。
在这段代码中,我们可以观察到许多防止反汇编过程的技巧。我们举其中的一些例子,这些技巧包括:
1. 滥用CALL和RET指令混淆函数边界。CALL指令会将返回地址压入栈。随后,RET指令会将这个地址弹出到EIp寄存器中,这实际上导致这两个指令没有任何作用。但是,上述的操作码会使得IDA认为函数在此结束,而下一条指令是另一个函数的结束。
2. 滥用无效的分支指令:CALL <address>并在<address>处POP <reg>。这是在EIP寄存器中获取地址并控制程序流的最简单方法。
3. 滥用JMP指令:只需要放置大量的JMP指令,这些指令会来回跳转,使分析人员的分析工作陷入困境。
经过上述技术的混淆之后,恶意软件可以检查其自身是否正在被调试。实现这一检查的代码并不复杂,是通过查询PEB的某些标志(IsDebuggerPresent)以确认调试器是否存在。
mov eax, fs:[30h] ; Process Environment Block cmp b [eax+2], 0 ; check BeingDebugged jne being_debugged
如前文所述,这段代码被大量跳转指令和垃圾指令所混淆,其唯一作用是增加安全人员进行分析的复杂度。例如,下面的一小段代码是截取的最后十几行,其作用是将值0x30放入EAX寄存器中以查找PEB。
在这个函数的结尾,我们发现了另一个XOR stub解码的例程,该例程将解码另一个代码块,然后重定向执行流。解码过程从地址0x004014E8开始,缓冲区大小为0x7F,使用相同的XOR密钥0xCB。
和之前一样,我们可以进行静态分析,并使用相同的脚本对这个缓冲区进行手动解码。
但是,在这里,作者使用了另外一个反调试技巧——NtGlobalFlag检查:
mov eax, fs:[30h] ; Process Environment Block mov al, [eax+68h] ; NtGlobalFlag and al, 70h cmp al, 70h je being_debugged
这个代码块会检查进程是否已经附加到调试器,如果运行顺利,则另一个XOR解码stub从地址0x00401000开始,缓冲区大小为0x4E8,XOR密钥为0xCB。
解码新缓冲区后,我们需要面对另一个反汇编防范技巧——带有常量的JMP指令。这是恶意软件防范静态分析的最常用技巧。基本上,该过程中会创建一个到新位置加上一个或几个字节的跳转,这会导致反汇编程序对操作码的错误解析。要想解决这个问题,我们需要花费大量的时间。
运行时的IAT解析
在地址0x00401000处,可以简单地调用另一个地址0x00401049,随着恶意软件逐渐能够动态解析其导入,这个地址也开始变得有趣起来。如前所述,对二进制标头进行分析,发现完全没有导入。但根据这段代码,恶意软件是从较早发现的PEB位置找到ntdll.dll的基址的。
但是,这是为什么呢?原来,在所有最新版本的Windows中,GS寄存器都指向一个名为线程环境块(Thread Enviroment Block,TEB)的数据结构。在TEB的0x30偏移量处,还有另外一个数据结构,也就是我们之前看到过的进程环境块(Process Enviroment Block)。
我们可以在Microsoft公共符号和WinDBG的帮助下,对这些数据结构进行检查。
使用这些工具,我们也可以对PEB进行检查:
对于第三条指令,我们关注到偏移量0x0C(_PEB_LDR_DATA结构)。这个结构非常重要,因为它包含一个指向双链表头部的指针InInitializationOrderModuleList,该链表包含用于已加载模块的NTDLL加载器数据结构。
其中的每个条目,都是指向LDR_DATA_TABLE_ENTRY结构的指针。对这个结构进行检查,我们将得到DLLBase。
在调试器中查看这些内容,会有助于我们理解:
我们将模块ntdll.dll的基址放入EDX寄存器中,因为这是Windows环境中加载到每个进程中的第一个模块。我们添加了注释,并重命名了select函数以使这部分内容更容易阅读。
恶意软件在获取ntdll.dll基址后,将循环两次调用名为DecryptionFunction的函数。该函数接收一个dword作为输入,也就是一个哈希值。接下来,它将在模块的导出地址表中查找名称与传递的哈希值相匹配的特定函数。在第一个循环中,恶意软件发现了两个函数:strstr和LdrGetDllHandle。
例如,在这种特定情况下,正如我们之前对ntdll.dll(kernel32.dll模块)所解释的那样,DecryptionFunction正在运行,它会检索放置在EAX寄存器中的VirtualAlloc地址,将其作为返回值。
DecryptionFunction
在完全对函数进行反汇编之后,我们将得到以下函数:
已解析函数和已导入函数的哈希值显示如下:
使用调试器进入到DecryptionFunction之后,我们便可以找到恶意软件接下来使用的函数。
可执行文件的这一部分,借助库和函数,以与之前几乎相同的方式来工作。我强烈建议逐行阅读反汇编代码,以了解Windows内部子系统和API调用的工作原理。
另外一个更加隐秘的有趣技巧,是使用栈字符串来构建对LoadLibraryA的调用。这里的秘诀在于,根据定义,CALL指令会将下一个地址作为返回地址推入栈中。但是,这个地址是一个ASCII空终止字符串,将作为下一个LoadLibraryA调用的参数。在这里,我们可以了解如何加载advapi32和user32这两个库。
在解决导入的问题之后,恶意软件会立即休眠10秒钟,然后通过GetModuleFileNameA检索文件名。
值得关注的是,上图还展示了代码检查其自身名称是否包含字符串“sample”的过程,如果发现存在则会终止运行。我们可以了解如何构建对strstr函数的调用,以及上一次传入是如何检查“sample”字符串的。
这是一种简单的反分析技术,但可能非常有效。作为安全研究人员也需要关注,请不要直接把恶意样本命名为“sample”。
接下来,恶意软件通过GetVolumeInformationA执行另一项检查,这项检查已经在MSDN中有了详细的记录。我们来看一下其中的调用,以进一步了解其目的。
从上面的反汇编代码中,我们可以看到这一过程中检索了卷序列号,并检查它是否与两个特定的序列号相同。随后,它使用RegOpenKeyExA打开注册表项,并使用相同的调用技术传入其中一个参数。然后,获取注册表项的值,关闭句柄,然后将该值转换为小写,接下来再继续这一过程。
在调试器中查看,我们可以非常清晰地看到其工作原理。
将这个字符串保存在内存中的某个位置之后,代码将继续执行其他一些检查,以尝试确认是否是在虚拟环境中运行。
作为反虚拟机检查的一部分,该过程将初始化一个包含四个步骤的循环,在循环中,会执行对strstr函数的调用,以在检索到的注册表值中搜索是否存在包括“qemu”、“virtual”、“vmware”、“xen”在内的字符串。如果大家关注了上一个调试器的截图,会发现我正在VMWare虚拟机上运行这个样本,因此要继续下去,我必须要修补strstr函数调用的返回值,使其返回0。
其他需要进行的检查:
如我们所见,恶意软件通过尝试获取模块sbiedll和dbghelp的句柄来确认是否正在沙箱中调试或执行。如果检测到这两个库中的任意一个存在,那么就会终止这个过程,并退出。
发现Payload
经过了各种反分析和反调试检查,我们终于到达了Payload!现在,就可以在内存中揭开这个恶意软件的秘密。
我们可以清楚地看到,这是一个PE文件,但该文件以某种方式被打乱了。这段代码会通过复杂的例程在内存中进行解码和管理。
要深入研究这段代码,所花费的时间和精力比预期的要更多一些。相反,我们也可以在隔离的换几个闹钟功能运行该恶意软件,并观察其执行情况。我们在下一篇文章中,将展现svchost.exe的新实例是如何加载到内存中的,并探讨关于进程注入的原理。
威胁指标
样本哈希值:07e81dfc0a01356fd96f5b75efe3d1b1bc86ade4
MITRE ATT&CK
Smoke Loader {S0226}
虚拟化/沙箱逃避 {T1497}