Introduction

在 基本静态分析 房间中,我们研究了恶意软件的特征,如字符串、哈希、导入函数和标头中的其他关键信息,以了解给定恶意软件的用途。在 高级静态分析 中,我们将进一步将恶意软件逆向工程到反汇编代码中,并分析汇编指令,以更好地了解恶意软件的核心功能。

高级静态分析

高级静态分析是一种用于分析恶意软件代码和结构而不执行恶意软件的技术。这可以帮助我们识别恶意软件的行为和弱点,并为防病毒软件开发签名以检测它。通过分析恶意软件的代码和结构,研究人员还可以更好地了解它的工作原理并开发新的防御技术。

学习目标

Reverse Engineering Process simplified

这个房间旨在帮助您获得有效逆向恶意软件所需的知识。它将教您更系统地处理汇编指令,使您能够更轻松地识别重要功能,而不是被每条指令所吸引。

这个房间涵盖的一些主题包括:

  • 了解如何执行高级静态分析。
  • 探索 Ghidra 的反汇编程序功能。
  • 理解和识别汇编中的不同 C 构造。

先决条件

参与者应完成以下房间以更好地理解。

让我们开始学习吧。

Malware Analysis: Overview

恶意软件分析是检查恶意软件(恶意软件)以了解其工作原理并确定其功能、行为和潜在影响的过程。分析恶意软件有四个主要步骤:基本静态分析、基本动态分析、高级静态分析和高级动态分析。每个步骤都使用不同的工具和技术来收集有关恶意软件的信息。

基本静态分析

基本静态分析旨在在不执行恶意软件的情况下了解恶意软件的结构和行为。这涉及检查恶意软件的代码、文件头和其他静态属性。

基本动态分析

基本动态分析旨在在受控环境中观察恶意软件在执行期间的行为。这涉及在沙箱或虚拟机中执行恶意软件并监控其系统活动、网络流量和进程行为。

高级动态分析

高级动态分析旨在使用高级监控技术发现更复杂和更具规避性的恶意软件行为。这涉及使用更复杂的沙箱和监控工具来更详细地捕获恶意软件的行为。

高级静态分析

高级静态分析旨在发现恶意软件中隐藏或混淆的代码和功能。这涉及使用更高级的技术来分析恶意软件的代码,例如反混淆和代码模拟。

如何执行高级静态分析

恶意软件的高级静态分析是了解其行为和识别其潜在威胁的关键过程。高级静态分析的主要目标是发现恶意软件的功能,识别其攻击媒介并确定其规避技术。

要执行高级静态分析,通常使用 IDA Pro、Binary Ninja 和 radare2 等反汇编程序。这些反汇编程序允许分析师探索恶意软件的代码并识别其功能和数据结构。执行恶意软件的高级静态分析所涉及的步骤如下:

  • 识别恶意软件的入口点及其进行的系统调用。
  • 识别恶意软件的代码部分并使用可用工具(如调试器和十六进制编辑器)对其进行分析。
  • 分析恶意软件的控制流图以确定其执行路径。
  • 通过分析恶意软件在执行过程中进行的系统调用来追踪其动态行为。
  • 使用以上信息了解恶意软件的逃避技术及其可能造成的潜在损害。

Ghidra: A Quick Overview

许多反汇编程序(如 Cutter、radare2、Ghidra 和 IDA Pro)都可用于反汇编恶意软件。但是,我们将在此房间中探索 Ghidra,因为它是免费的、开源的,并且具有许多可用于熟练掌握逆向工程的功能。目标是熟悉反汇编程序的主要用途,并利用这些知识使用任何反汇编程序。

Ghidra 是一种软件逆向工程工具,允许用户分析编译后的代码以了解其功能。它旨在通过提供反编译、反汇编和调试二进制文件的平台来帮助分析师和开发人员了解软件的工作原理。

功能
Ghidra 包含许多功能,使其成为强大的逆向工程工具。其中一些功能包括:

  • 反编译:Ghidra 可以将二进制文件反编译为可读的 C 代码,使开发人员更容易理解软件的工作原理。
  • 反汇编:Ghidra 可以将二进制文件反汇编为汇编语言,让分析人员可以检查代码的低级操作。
  • 调试:Ghidra 有一个内置调试器,允许用户逐步执行代码并检查其行为。
  • 分析:Ghidra 可以自动识别函数、变量和其他代码,帮助用户了解代码的结构。

Ghidra Interface

如何使用 Ghidra 进行分析

我们将通过分析位于桌面上的简单“HelloWorld.exe”程序来探索 Ghidra 及其功能。以下是使用 Ghidra 执行代码分析的步骤:

  • 打开 Ghidra 并创建一个新项目。

Creating a New project in Ghidra

  • 选择非共享项目。选择共享项目**可以让我们与其他分析师分享我们的分析。

Creating a New Project in Ghidra

  • 命名项目并设置目录或保留默认路径。

 Steps to start  a New Poroject in Ghidra

  • 导入您要分析的恶意软件可执行文件。现在我们已经创建了一个空项目,让我们将位于桌面上的“HelloWorld.exe”拖放到该项目中,或者导航到桌面文件夹并选择该程序。

Load HelloWorld Program in Ghidra

  • 一旦导入,它会向我们显示该程序的摘要,如下所示:

 Shows summary of program in Ghidra

  • 双击 HelloWorld.exe 在代码浏览器中打开它。当系统要求分析可执行文件时,单击 是。

 Shows Analysis steps in Ghidra

  • 接下来出现的窗口向我们展示了各种分析选项。我们可以根据需要选中或取消选中它们。这些插件或附加组件在分析过程中为 Ghidra 提供帮助。

Shows Analysis steps in Ghidra

分析需要一些时间。右下角的栏显示进度。等到分析完成 100%。

探索 Ghidra 布局

  • Ghidra 有很多选项可以帮助我们进行分析。其默认布局如下所示并简要说明。

Layout of the Ghidra Code Browser

  1. 程序树:显示程序的各个部分。我们可以单击不同的部分来查看每个部分的内容。解剖 PE 头文件 房间深入解释了头文件和 PE 部分。
  2. 符号树:

包含导入、导出和函数等重要部分。每个部分都提供了有关我们正在分析的程序的大量信息。

  • 导入:此部分包含有关程序正在导入的库的信息。单击每个 API 调用将显示使用该 API 的汇编代码。
  • 导出:此部分包含程序正在导出的 API/函数调用。此部分在分析 DLL 时很有用,因为它将显示 dll 包含的所有函数。
  • 函数:此部分包含它在代码中找到的函数。单击每个函数将带我们到该函数的反汇编代码。它还包含入口函数。单击 entry 函数将带我们进入正在分析的程序的开头。通用名称以 FUN_VirtualAddress 开头的函数是 Ghidra 没有为其命名的函数。
  1. 数据类型管理器:此部分显示程序中发现的各种数据类型。

  2. 列表:

此窗口显示二进制文件的反汇编代码,其中按顺序包含以下值。

  • 虚拟地址
  • 操作码
  • 汇编指令(PUSH、POP、ADD、XOR 等)
  • 操作数
  • 注释
  1. 反编译:Ghidra 在这里将汇编代码转换为伪 C 代码。这是分析过程中需要查看的一个非常重要的部分,因为它可以更好地理解汇编代码。

  2. 工具栏:它有各种选项可在分析过程中使用。

  • 图形视图:工具栏中的图形视图是一个重要选项,它允许我们查看反汇编的图形视图。

 Shows Graph View of if-else.exe program

  • 内存映射选项显示程序的内存映射,如下所示:

Shows Memory map in Ghidra

  • 该导航工具栏显示了浏览代码的不同选项。

 Shows toolbar options in Ghidra

  • 探索字符串。转到“搜索 -> 字符串”,然后单击“搜索”将为我们提供 Ghidra 在程序中找到的字符串。此窗口可以包含非常有用的信息,以帮助我们进行分析。

 Shows Strings Search tab

用汇编语言分析 HelloWorld

有很多方法可以找到感兴趣的代码。要找到 HelloWorld.exe 的汇编代码,我们将双击 Program Trees 部分中的 .text;它将带我们进入反汇编代码部分。滚动反汇编代码,直到看到对将显示 Hello World 字符串的消息框的调用。在反编译部分,我们可以看到该函数的翻译伪 C 代码。

反汇编部分显示了参数是如何被推送的,然后是调用 MessageBoxA,负责消息框的显示。

 Shows Analysis steps in Ghidra
我们在本任务中通过检查一个简单的“HelloWorld”程序探索了 Ghidra 及其功能。在下一个任务中,我们将利用这些知识探索不同的 C 构造及其在汇编中的对应表示。

注意:值得注意的是,恶意软件的作者可能对其进行了打包或使用了混淆或反 VM/AV 检测技术,使分析更加困难。这些技术将在后面的讨论中讨论。

Identifying C Code Constructs in Assembly

分析编译二进制文件的汇编代码对于初学者来说可能是一项艰巨的任务。了解汇编指令以及各种编程组件如何翻译/反映到汇编中非常重要。在这里,我们将检查各种 C 构造及其相应的汇编代码。这将帮助我们在分析过程中识别并关注恶意软件的关键部分。

您可以加载 Ghidra 中 Code_Constructs 文件夹中的程序,如下所示:

 Add programs in Ghidra project

有多种方法可以开始分析反汇编代码:

  • 从 符号树 部分找到主要函数。
  • 从 程序树 部分检查 .text 代码以查看代码部分并找到入口点。
  • 搜索有趣的 字符串 并找到引用这些字符串的代码。

注意:不同的编译器在编译时会添加自己的代码进行各种检查。因此,可能会出现一些没有意义的垃圾汇编代码。

代码:Hello World

用 C 语言

Hello World 是我们在任何编程语言中尝试的第一个程序。下面是一个简单的 C 代码,它将在控制台上打印“Hello World!”消息。

#include <stdio.h>

int main() { printf("Hello, world!");
return 0;
}

有两个 HelloWorld 程序。桌面上的程序显示一个带有“Hello World”消息的消息框。Code_Constructs 文件夹中的程序在终端中显示“Hello_World”。

In Assembly

section .data 
message db 'HELLO WORLD!!', 0

section .text
global _start

_start:
; write the message to stdout
mov eax, 4 ; write system call
mov ebx, 1 ; file descriptor for stdout
mov ecx, message ; pointer to message
mov edx, 13 ; message length
int 0x80 ; call kernel

此程序在 .data 部分中定义一个字符串“HELLO WORLD!!”,然后使用 write 系统调用将该字符串打印到 stdout。

Ghidra 中的 HelloWorld

打开 Ghidra 中 Code_Constructs 文件夹中的 Hello_World.exe 程序。找到主函数并检查汇编和反编译的 C 代码。

 Shows Hello_World program disassembled in Ghidra

如果我们在Listings View中查看反汇编代码,我们可以看到在调用打印函数之前将“HELLO WORLD!!”推送到堆栈的指令。

代码:For 循环

For 循环是重复某些指令直到循环完成的重要编程组件。

在 C 语言中

以下代码显示了一个简单的 for 循环,显示一条消息十次。

int main() {
for (int i = 1; i <= 5; i++) {
std::cout << i << std::endl;
}
return 0;
}

汇编中的 For 循环

main:
; initialize loop counter to 1
mov ecx, 1

; loop 5 times
mov edx, 5
loop:
; print the loop counter
push ecx
push format
call printf
add esp, 8

; increment loop counter
inc ecx

; check if the loop is finished
cmp ecx, edx
jle loop

在此代码中,主函数将循环计数器 ecx 初始化为 1,将循环限制 edx 初始化为 5。循环标签用于标记循环的开始。在循环内部,使用标准 C 库中的 printf 函数将循环计数器打印到控制台。打印循环计数器后,循环计数器递增,并检查循环限制以查看循环是否应继续。如果计数器仍然小于或等于循环限制,则循环继续。如果循环计数器超过循环限制,则循环终止,并将控制权传递给程序末尾,程序在此处返回 0。

Ghidra 中的 For 循环

打开 Ghidra 中 Code_Constructs 文件夹中的 for-loop.exe 程序。找到入口函数并检查汇编和反编译的 C 代码。

Shows assembly and decompiled code of for_loop program in Ghidra

我们可以看到 for 循环 是如何被翻译成反汇编代码的。

代码:函数

函数是任何编程语言的关键组成部分。它是一个独立的代码块,执行特定的任务。

在 C 语言中

下面是 C 程序中的一个简单的 add 函数,用于演示函数的工作原理以及如何将它们翻译成汇编语言。

int add(int a, int b){
int result = a + b;
return result;
}

In Assembly

add:
push ebp ; save the current base pointer value
mov ebp, esp ; set base pointer to current stack pointer value
mov eax, dword ptr [ebp+8] ; move the value of 'a' into the eax register
add eax, dword ptr [ebp+12] ; add the value of 'b' to the eax register
mov dword ptr [ebp-4], eax ; move the sum into the 'result' variable
mov eax, dword ptr [ebp-4] ; move the value of 'result' into the eax register
pop ebp ; restore the previous base pointer value
ret ; return to calling function

add 函数首先将当前基指针值保存到堆栈上。然后,它将基指针设置为当前堆栈指针值。然后,该函数将 a 和 b 的值移动到 eax 寄存器中,将它们相加,并将结果存储在结果变量中。最后,该函数将结果值移动到 eax 寄存器中,恢复先前的基指针值,并返回到调用函数。

Code: While loop

int i = 0;
while (i < 10) {
printf("%d\\n", i);
i++;
}

While Loop in Assembly

mov ecx, 0     ; initialize i to 0
loop_start:
cmp ecx, 10 ; compare i to 10
jge loop_end ; jump to loop_end if i >= 10
push ecx ; save the value of i on the stack
push format ; push the format string for printf
push dword [ecx]; push the value of i for printf
call printf ; call printf to print the value of i
add esp, 12 ; clean up the stack
inc ecx ; increment i
jmp loop_start ; jump back to the start of the loop
loop_end:

在此示例中,mov 指令将寄存器 ecx 初始化为 0,代表变量 i。loop_start 标签标记循环的开始。cmp 指令将 ecx 的值与 10 进行比较。如果 ecx 大于或等于 10,则循环结束,程序跳转到 loop_end 标签。否则,ecx 的值将与格式字符串和 ecx 本身的值一起推送到堆栈上,以便使用 printf 打印。add 指令在 printf 调用后清理堆栈。最后,ecx 的值递增,程序跳回到 loop_start 标签以重复循环。

Ghidra 中的 While 循环

在 Ghidra 中打开 While-Loop.exe 程序。转到 Symbol Tree 部分中的 Functions 选项卡,然后找到主函数。

Shows assembly and decompiled code of while program in Ghidra

在这个程序中,打印了五次文本,直到计数器变量的值达到 5。我们可以观察关于如何设置计数器变量、循环如何工作以及程序如何使用跳转指令来满足条件的汇编指令。

需要注意的是,不同的编译器会以不同的方式编译程序,并添加与编译器相关的代码。为了演示,本房间中使用的程序是使用不同的编译器编译的。因此,您可能会发现汇编代码的解释存在差异。

An Overview of Windows API Calls

Windows API 是 Windows 操作系统提供的一组函数和服务,可帮助开发人员创建 Windows 应用程序。这些函数包括创建窗口、菜单、按钮和其他用户界面元素,以及执行文件输入/输出和网络通信等任务。让我们以一个非常常见的 API 函数为例:CreateProcess。

创建进程 API

CreateProcessA 函数创建一个新进程及其主线程。该函数采用多个参数,包括可执行文件的名称、命令行参数和安全属性。

Shows CreateProcess API function help

下面是使用“CreateProcessA”函数启动新进程的 C 代码示例:

#include 

int main()
{
STARTUPINFO si;
PROCESS_INFORMATION pi;

ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));

if (!CreateProcess(NULL, "C:\\\\Windows\\\\notepad.exe", NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
printf("CreateProcess failed (%d).\\n", GetLastError());
return 1;
}

WaitForSingleObject(pi.hProcess, INFINITE);

CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);

return 0;
}

当编译成汇编代码时,“CreateProcessA”函数调用如下所示:

push 0
lea eax, [esp+10h+StartupInfo]
push eax
lea eax, [esp+14h+ProcessInformation]
push eax
push 0
push 0
push 0
push 0
push 0
push 0
push dword ptr [hWnd]
call CreateProcessA

此汇编代码以相反的顺序将必要的参数推送到堆栈上,然后调用CreateProcessA函数。然后,CreateProcessA函数启动一个新进程并返回该进程及其主线程的句柄。

在恶意软件分析过程中,识别 API 调用并检查代码有助于了解恶意软件的用途。

Common APIs used by Malware

恶意软件作者严重依赖 Windows API 来实现其目标。了解不同恶意软件变体中使用的 Windows API 非常重要。检查“导入”函数是高级静态分析中的重要步骤,它可以揭示有关恶意软件的很多信息。

键盘记录器

恶意软件可以使用多个 Windows API 进行键盘记录,包括:

  • SetWindowsHookEx:此函数将应用程序定义的挂钩过程安装到挂钩链中。恶意软件可以使用此功能监视和拦截系统事件,例如击键或鼠标点击。SetWindowsHookEx
  • GetAsyncKeyState:此函数在调用时检索虚拟键的状态。恶意软件可以使用此函数来确定是否按下或释放了某个键。 GetAsyncKeyState
  • GetKeyboardState:此函数检索所有虚拟按键的状态。恶意软件可以使用此函数确定键盘上所有按键的状态。GetKeyboardState
  • GetKeyNameText:此函数检索按键的名称。恶意软件可以使用此函数确定所按下按键的名称。GetKeyNameText

利用这些 API,恶意软件可以拦截和记录按键,从而获取密码和信用卡号等敏感信息。

下载器

下载器是一种恶意软件,旨在将其他恶意软件下载到受害者的系统中。下载器可以伪装成合法软件或文件,并通过恶意电子邮件附件、软件下载或利用软件中的漏洞进行传播。下载器可以使用各种 Windows API 来执行恶意操作。下载器常用的一些 API 包括:

  • URLDownloadToFile:此函数从互联网下载文件并将其保存到本地文件。恶意软件可以使用此功能下载其他恶意代码或恶意软件更新。URLDownloadToFile
  • WinHttpOpen:此函数初始化 WinHTTP API。恶意软件可以使用此功能与远程服务器建立 HTTP 连接并下载其他恶意代码。 WinHttpOpen
  • WinHttpConnect:此函数使用 WinHTTP API 建立与远程服务器的连接。恶意软件可以使用此函数连接到远程服务器并下载其他恶意代码。WinHttpConnect
  • WinHttpOpenRequest:此函数使用 WinHTTP API 打开 HTTP 请求。恶意软件可以使用此函数将 HTTP 请求发送到远程服务器并下载其他恶意代码或窃取数据。WinHttpOpenRequest

C2 通信

命令和控制 (C2) 通信是恶意软件用来与远程服务器或攻击者通信的一种方法。此通信可用于接收来自攻击者的命令、向攻击者发送被盗数据或将其他恶意软件下载到受害者的系统上。

  • InternetOpen:此函数初始化用于连接互联网的会话。恶意软件可以使用此函数连接到远程服务器并与命令和控制 (C2) 服务器通信。InternetOpen
  • InternetOpenUrl:此函数打开一个 URL 进行下载。恶意软件可以使用此函数下载其他恶意代码或从 C2 服务器窃取数据。InternetOpenUrl
  • HttpOpenRequest:此函数打开 HTTP 请求。恶意软件可以使用此函数向 C2 服务器发送 HTTP 请求并接收命令或其他恶意代码。 HttpOpenRequest
  • HttpSendRequest:此函数向 C2 服务器发送 HTTP 请求。恶意软件可以使用此函数从 C2 服务器发送数据或接收命令。HttpSendRequest

数据泄露

数据泄露是指未经授权将数据从组织传输到外部目的地。恶意软件可以使用各种 Windows API 执行数据泄露,包括:

  • InternetReadFile:此函数从开放互联网资源的句柄读取数据。

恶意软件可以使用此功能从受感染的系统中窃取数据并将其传输到 C2 服务器。InternetReadFile

  • FtpPutFile:此功能将文件上传到 FTP 服务器。恶意软件可以使用此功能将被盗数据泄露到远程服务器。FtpPutFile
  • CreateFile:此功能创建或打开文件或设备。恶意软件可以使用此功能读取或修改包含敏感信息或系统配置数据的文件。 CreateFile
  • WriteFile:此函数将数据写入文件或设备。恶意软件可以使用此函数将窃取的数据写入文件,然后将其泄露到远程服务器。WriteFile API
  • GetClipboardData:此 API 用于从剪贴板检索数据。恶意软件可以使用此 API 检索复制到剪贴板的敏感数据。GetClipboardData

Dropper

Dropper 是一种旨在将其他恶意软件安装到受害者系统的恶意软件。 Dropper 可以伪装成合法软件或文件,并通过恶意电子邮件附件、软件下载或利用软件中的漏洞进行传播。

  • CreateProcess:此函数创建一个新进程及其主线程。恶意软件可以使用此函数在合法进程的上下文中执行其代码,从而更难以检测和分析。CreateProcess
  • VirtualAlloc:此函数在调用进程的虚拟地址空间内保留或提交内存区域。恶意软件可以使用此函数分配内存来存储其代码。 VirtualAlloc
  • WriteProcessMemory:此函数将数据写入指定进程地址空间内的内存区域。恶意软件可以使用此函数将其代码写入分配的内存。WriteProcessMemory

API 挂钩

API 挂钩是恶意软件用来拦截对 Windows API 的调用并修改其行为的一种方法。这允许恶意软件避免被安全软件检测到并执行恶意操作,例如窃取数据或修改系统设置。恶意软件可以使用各种 API 进行挂钩,包括:

  • GetProcAddress:此函数从指定的动态链接库 (DLL) 中检索导出函数或变量的地址。恶意软件可以使用此函数来定位和挂接其他进程发出的 API 调用。GetProcAddress
  • LoadLibrary:此函数将动态链接库 (DLL) 加载到进程的地址空间中。恶意软件可以使用此函数从 DLL 或其他模块加载并执行其他代码。LoadLibrary
  • SetWindowsHookEx API:此 API 用于安装挂接程序,用于监视发送到窗口或系统事件的消息。恶意软件可以使用此 API 拦截对其他 Windows API 的调用并修改其行为。 SetWindowsHookEx API

反调试和 VM 检测

反调试和虚拟机检测是恶意软件用来逃避安全研究人员检测和分析的技术。以下是用于这些目的的一些常见 Windows API:

IsDebuggerPresent:

此函数检查进程是否在调试器下运行。恶意软件可以使用此函数确定是否正在分析该进程,并采取适当的措施逃避检测。IsDebuggerPresent

CheckRemoteDebuggerPresent:

此函数检查远程调试器是否正在调试进程。恶意软件可以使用此函数确定是否正在分析该进程,并采取适当的措施逃避检测。CheckRemoteDebuggerPresent

NtQueryInformationProcess:

此函数检索有关指定进程的信息。恶意软件可以使用此函数确定是否正在调试该进程,并采取适当的措施逃避检测。NtQueryInformationProcess

GetTickCount:

此函数检索自系统启动以来经过的毫秒数。恶意软件可以使用此函数来确定它是否在虚拟化环境中运行,这可能表明它正在被分析。GetTickCount

GetModuleHandle:

此函数检索指定模块的句柄。恶意软件可以使用此函数来确定它是否在虚拟化环境中运行,这可能表明它正在被分析。GetModuleHandle

GetSystemMetrics:

此函数检索各种系统指标和配置设置。恶意软件可以使用此函数来确定它是否在虚拟化环境中运行,这可能表明它正在被分析。GetSystemMetrics
有关反调试/AV 检测的详细信息,请参阅此房间 反逆向工程。

Process Hollowing: Overview

现在我们已经了解了如何识别汇编中的代码构造,让我们利用之前获得的知识来理解和分析称为进程挖空的进程注入技术,恶意软件主要使用这种技术来逃避检测。

进程挖空

进程挖空是恶意软件用来将恶意代码注入受害者计算机上运行的合法进程的一种技术。恶意软件创建一个挂起的进程,并用自己的代码替换其内存空间。然后恶意软件恢复该进程,使其执行注入的代码。由于恶意代码是在合法进程的上下文中执行的,因此这种技术允许恶意软件绕过可能存在的安全措施。

如何实现进程挖空

进程挖空涉及几个步骤:

使用 CreateProcessA() API 创建一个新进程。此进程将充当合法进程并将被挖空。
然后使用 NtSuspendProcess() 暂停新进程。
使用 VirtualAllocEx() API 在挂起的进程中分配内存。此内存将用于保存恶意代码。
使用 WriteProcessMemory() API 将恶意代码写入分配的内存。
使用 SetThreadContext() 和 GetThreadContext() API 修改进程的入口点以指向恶意代码的地址。
使用 NtResumeProcess() API 恢复暂停的进程。这将导致进程执行恶意代码。
清理进程以及进程期间使用的任何资源。
为了更好地理解我们所介绍的技术,下面添加了一个示例 C++ 代码:

#include 
#include 
#include 
using namespace std;

bool HollowProcess(char *szSourceProcessName, char *szTargetProcessName)
{
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe;
    pe.dwSize = sizeof(PROCESSENTRY32);

    if (Process32First(hSnapshot, &pe))
    {
        do
        {
            if (_stricmp((const char*)pe.szExeFile, szTargetProcessName) == 0)
            {
                HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID);
                if (hProcess == NULL)
                {
                    return false;
                }

                IMAGE_DOS_HEADER idh;
                IMAGE_NT_HEADERS inth;
                IMAGE_SECTION_HEADER ish;

                DWORD dwRead = 0;

                ReadProcessMemory(hProcess, (LPVOID)pe.modBaseAddr, &idh, sizeof(idh), &dwRead);
                ReadProcessMemory(hProcess, (LPVOID)(pe.modBaseAddr + idh.e_lfanew), &inth, sizeof(inth), &dwRead);

                LPVOID lpBaseAddress = VirtualAllocEx(hProcess, NULL, inth.OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

                if (lpBaseAddress == NULL)
                {
                    return false;
                }

                if (!WriteProcessMemory(hProcess, lpBaseAddress, (LPVOID)pe.modBaseAddr, inth.OptionalHeader.SizeOfHeaders, &dwRead))
                {
                    return false;
                }

                for (int i = 0; i < inth.FileHeader.NumberOfSections; i++)
                {
                    ReadProcessMemory(hProcess, (LPVOID)(pe.modBaseAddr + idh.e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER))), &ish, sizeof(ish), &dwRead);
                    WriteProcessMemory(hProcess, (LPVOID)((DWORD)lpBaseAddress + ish.VirtualAddress), (LPVOID)((DWORD)pe.modBaseAddr + ish.PointerToRawData), ish.SizeOfRawData, &dwRead);
                }

                DWORD dwEntrypoint = (DWORD)pe.modBaseAddr + inth.OptionalHeader.AddressOfEntryPoint;
                DWORD dwOffset = (DWORD)lpBaseAddress - inth.OptionalHeader.ImageBase + dwEntrypoint;

                if (!WriteProcessMemory(hProcess, (LPVOID)(lpBaseAddress + dwEntrypoint - (DWORD)pe.modBaseAddr), &dwOffset, sizeof(DWORD), &dwRead))
                {
                    return false;
                }

                CloseHandle(hProcess);

                break;
            }
        } while (Process32Next(hSnapshot, &pe));
    }

    CloseHandle(hSnapshot);

    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    ZeroMemory(&si, sizeof(si));
    ZeroMemory(&pi, sizeof(pi));

    if (!CreateProcess(NULL, szSourceProcessName, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
    {
        return false;
    }

    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL;

    if (!GetThreadContext(pi.hThread, &ctx))
    {
        return false;
    }

    ctx.Eax = (DWORD)pi.lpBaseOfImage + ((IMAGE_DOS_HEADER*)pi.lpBaseOfImage)->e_lfanew + ((IMAGE_NT_HEADERS*)(((BYTE*)pi.lpBaseOfImage) + ((IMAGE_DOS_HEADER*)pi.lpBaseOfImage)->e_lfanew))->OptionalHeader.AddressOfEntryPoint;

    if (!SetThreadContext(pi.hThread, &ctx))
    {
        return false;
    }

    ResumeThread(pi.hThread);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

    return true;
}

int main()
{
    char* szSourceProcessName = "C:\\\\Windows\\\\System32\\\\calc.exe";
    char* szTargetProcessName = "notepad.exe";

    if (HollowProcess(szSourceProcessName, szTargetProcessName))
    {
        cout << "Process hollowing successful" << endl;
    }
    else
    {
        cout << "Process hollowing failed" << endl;
    }

    return 0;
}

现在我们已经了解了如何实现进程挖空,现在是时候探索 Ghidra 反汇编程序并在实验室中检查进程挖空样本 benign.exe 了。

Analyzing Process Hollowing

现在我们了解了什么是进程挖空以及如何使用 Ghidra 反汇编程序分析恶意软件以更好地了解其来龙去脉,让我们创建一个新项目并将位于桌面上的 Benign.exe 示例加载到 Ghidra 中。

需要注意的一点是,几乎所有恶意软件都带有已知或自定义的打包程序,并且还采用了不同的反调试/VM 检测技术来阻碍分析。这个主题将在下一个房间中介绍。示例未在此任务中打包,并且未应用任何反调试/VM 检测技术。

我们进行高级静态分析的目标是:

检查 API 调用以查找模式或可疑调用。
查看可疑字符串。
查找有趣或恶意的函数。
检查反汇编/反编译的代码以查找尽可能多的信息。
让我们开始分析。

加载示例:加载程序;它将显示摘要,如下所示:

分析:让 Ghidra 分析样本。

Ghidra 不会自动在程序启动时启动。由我们来选择首先要分析哪个函数。我们将开始研究用于完成进程挖空的 Windows API。

注意:需要指出的是,立即开始搜索 CreateProcessA 函数并不是分析师开始分析未知二进制文件的方式。

CreateProcess

我们在上一个任务中了解到,在进程挖空中,可疑进程会创建一个处于挂起状态的受害进程。为了确认,让我们在符号树部分中搜索 CreateProcessA API。然后,右键单击 Show References 选项以显示调用此函数的所有程序部分。

单击第一个引用将带我们到反汇编的代码并在反编译部分显示反编译的 C 代码。

它清楚地显示了在调用函数之前,堆栈上的参数是如何以相反的顺序被推送的。进程创建标志中的值 0x4 被推送到堆栈中,代表暂停状态。

Graph View

点击工具栏中的“显示函数图”将显示我们正在检查的反汇编代码的图形视图。

在上述情况下,如果程序:

无法在挂起状态下创建受害进程,它将移动到块 1。红色箭头表示未能满足上述条件。
成功创建受害进程,它将移动到块 2。绿色箭头表示跳转条件成功。

打开可疑文件

CreateFileA API 用于创建或打开现有文件。让我们在符号树部分中搜索此 API 调用,并转到它引用的代码。

Hollow the Process

恶意软件使用 ZwUnmapViewOfSection 或 NtUnmapViewOfSection API 调用来取消映射目标进程的内存。让我们搜索一下这两个 API,看看是否调用了其中一个 API。

NtUnmapViewOfSection 恰好接受两个参数,即要取消映射的基地址(虚拟地址)和需要挖空的进程的句柄。

分配内存

一旦进程被挖空,恶意软件必须使用 VirtualAllocEx 分配内存,然后再写入进程。让我们以相同的方式找到 VirtualAllocEx API 调用的实例。传递给该函数的参数包括进程的句柄、要分配的地址、大小、分配类型和内存保护标志。

Write Down the Memory

一旦分配了内存,恶意软件就会尝试将可疑的进程/代码写入被挖空的进程的内存中。 WriteProcessMemory API 就是用于此目的。让我们找到该函数并分析代码。

对 WriteProcessMemory 函数有三次调用。最后一次调用引用了 Kernel32 DLL 中的代码;因此,我们可以忽略它。从反编译的代码来看,该程序似乎正在逐个复制可疑进程的不同部分。

恢复线程

一旦一切都整理好,恶意软件将使用 SetThreadContext 控制线程,然后使用 ResumeThread API 恢复线程以执行代码。

在这里,我们可以看到程序如何设置线程上下文,然后恢复它来执行恶意代码。

Conclusion

呼!!

终于,我们走到了尽头。这个房间讲授了以下内容:

  • 使用 Ghidra 工具对恶意软件执行高级静态分析的基础知识
  • 恶意软件使用的常见 API
  • 进程 Hollowing 的工作原理

执行高级静态分析后的下一步是高级动态分析,接下来将介绍该分析。