应用层 APC 注入 Dll
常用的线程注入方法有:远程线程注入、全局消息钩子注入、应用层 APC 注入 和 ZwCreateThreadEx 强力注入等。
今天我们讲一下应用层 APC 注入的这种方法,首先我们来看一下官方的定义:
异步过程调用(APC)是在特定线程的上下文中异步执行的函数。当 APC 排队到线程时,系统会发出软件中断。下次调度线程时,它将运行 APC 函数。
系统生成的 APC 称为内核模式 APC,应用程序生成的 APC 称为用户模式 APC。线程必须处于可警报状态才能运行用户模式 APC。
线程在调用 SleepEx、SignalObjectAndWait、MsgWaitForMultipleObjectsEx、WaitForMultipleObjectsEx 或 WaitForSingleObjectEx 函数时进入可警报状态。
从上面的官方定义中我们可以看到,每个线程其实都有一个自己的 APC 队列,那么我们只要将 LoadLibrary
函数作为一个异步过程调用插入到目标进程中所有线程的 APC 队列,每当目标进程中的任意线程在调用能进入可警报状态的特定函数时就会执行该线程 APC 队列中的函数,从而就可以把我们的 Dll 文件注入到目标线程。
step 1
和远程线程注入的前面步骤类似,要想跨进程调用 LoadLibaray
函数,首先我们必须把需要加载的 Dll 路径写入到目标进程地址空间,然后获取 LoadLibrary
函数在目标进程空间的地址:
HANDLE hProcess = NULL, hThread = NULL;
PSTR fileName = NULL;
FARPROC pfnThreadRtn = NULL;
// 打开注入进程,获取进程句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
if (NULL == hProcess)
{
printf("打开进程失败\r\n");
return FALSE;
}
// 计算将注入 Dll 文件的完整路径长度
SIZE_T fileNameLen = strlen(DllName) + 1;
// 在目标空间申请一块长度为 fileNameLen 大小的内存空间
fileName = (PSTR)VirtualAllocEx(hProcess, NULL, fileNameLen, MEM_COMMIT, PAGE_READWRITE);
if (NULL == fileName)
{
CloseHandle(hProcess);
printf("在目标空间申请内存空间失败\r\n");
return FALSE;
}
// 将 Dll 文件的完整路径写入目标进程申请的空间内
if (FALSE == WriteProcessMemory(hProcess, fileName, (PVOID)DllName, fileNameLen, NULL))
{
VirtualFreeEx(hProcess, fileName, 0, MEM_RELEASE);
CloseHandle(hProcess);
printf("将完整路径写入目标进程空间失败\r\n");
return FALSE;
}
// 在我们进程空间内获得 LoadLibrary 函数的地址
pfnThreadRtn = GetProcAddress(GetModuleHandle(L"Kernel32"), "LoadLibraryA");
if (NULL == pfnThreadRtn)
{
VirtualFreeEx(hProcess, fileName, 0, MEM_RELEASE);
CloseHandle(hProcess);
printf("获取 LoadLibrary 函数地址失败\r\n");
return FALSE;
}
step 2
准备工作做好之后,我们可以通过 CreateToolhelp32Snapshot
函数来得到系统中所有进程以及进程中使用的堆、模块和线程的快照:
THREADENTRY32 te = { 0 };
te.dwSize = sizeof(THREADENTRY32);
// 得到特定进程以及进程中使用的堆、模块和线程的快照(TH32CS_SNAPTHREAD 表示快照包括系统所有线程)
HANDLE handleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (INVALID_HANDLE_VALUE == handleSnap)
{
VirtualFreeEx(hProcess, fileName, 0, MEM_RELEASE);
CloseHandle(hProcess);
printf("拍摄进程快照失败\r\n");
return FALSE;
}
step 3
得到所有的线程快照后,我们就可以通过遍历所有线程,找出我们想要注入进程中的所有线程,然后将 Loadlibrary
函数插入所有线程的 APC 队列中:
DWORD dwRet = 0;
// 遍历进程快照中的线程,并向每一个线程插入 APC
if (Thread32First(handleSnap, &te))
{
do
{
// 只对特定进程的线程插入 APC
if (te.th32OwnerProcessID == Pid)
{
// 得到线程句柄
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (hThread)
{
// 向线程插入 APC
dwRet = QueueUserAPC((PAPCFUNC)pfnThreadRtn, hThread, (ULONG_PTR)fileName);
CloseHandle(hThread);
}
}
} while (Thread32Next(handleSnap, &te));
}
step 4
在进行 APC 注入之前,我们还需要新建一个命令行程序,里面调用 SleepEx
函数用来做实验,不然可能很难看到注入效果(毕竟没哪个程序会闲着没事总是调用等待函数...):
#include <stdio.h>
#include <Windows.h>
int main()
{
int i = 0;
while (i < 1000)
{
SleepEx(1000, TRUE);
printf("秒数:%d\r\n", ++i);
}
return 0;
}
step 5
最后我们就可以进行注入了:
int main()
{
if (ApcInjectDll(53588, "C:\\Users\\Administrator\\Desktop\\console_Dll.dll"))
{
printf("APC 注入成功\r\n");
}
system("pause");
return 0;
}
注入的效果如下: