前言
这个系列的目的是希望你能系统的理解APC的内部原理(不再是零散的理解)
我重构了用户模式和内核模式下APC相关的函数,希望更好的理解APC机制,在文章的最后会分享源码
在这个系列中,我将探讨下面的主题
1、用户模式下APC的使用
2、内核模式下APC的使用
3、用户模式下APC内部原理
4、内核模式下APC内部原理
5、“Alerts”的概念以及它和APC的关系
6、APC在Wow64中的应用
7、如何在用户模式和内核模式下干扰微软对APC的ETW监控
8、如何使用APC安全的卸载一个驱动程序
9、像Procmon和Process Hacker这类安全工具如何利用APC
10、CET(用于检测ROP的CPU Shadow Stack)如何影响APC
11、关于APC机制有文档的源代码
12、关于APC的逆向工程练习
13、接下来文章中的惊喜
本文是系列的第一篇文章,讲述用户模式下APC,是这个系列中相对简单的部分,文中我将一边讲解,一边分享代码
APC介绍
APC全称Asynchronous Procedure Call,译为“异步过程调用”,是Windows中使用的一种机制,这种机制的核心是一个队列,通常称为APC队列,每个线程有2个APC队列:一个是用户模式APC队列、一个是内核模式APC队列,可以利用这种机制将函数放到指定线程的队列中,安全人员了解APC主要因为它被用在恶意软件中,用于执行代码或将代码注入到远程进程中,本质上是对APC机制的滥用。
好多初学者对异步很模糊,异步简单来说就是:当前执行的任务A不用等待任务B完成,就可以继续执行,相对应的,同步指的是当前执行的任务A需要任务B完成,才能继续
最好不要在内核模式使用APC,因为内核模式下APC的使用是没有官方文档的,那意味着一点差错都可能导致系统崩溃等问题,但是安全人员往往从内核模式使用APC,用来注入代码到用户模式进程中(比如反病毒软件、Rookits、等),如果你不是完全清楚你在做什么的话(即使你是一个超级专家,认为Windows不会影响你的代码),我不推荐你在内核模式下使用APC以及任何无官方文档的机制,然而如果你知道你在做什么,在某些情况下使用APC是相对安全的,关于内核模式下APC会在后续的文章中讲述
APC的应用例如,你调用一个异步的RPC方法,当RPC方法完成时,你指定的APC例程将被执行,再比如NtWriteFile/NtReadFile、Get/SetThreadContext、SuspendThread、TerminateThread、等等都用到了APC,甚至Windows调度程序也使用了APC,这也是为什么我觉得理解APC对理解Windows内部原理至关重要(尽管微软宣称这是一个无官方文档的功能,人们应该忽视它)
用户模式下APC有2种:
• 普通用户模式APC:仅当目标线程处于警报状态时,才会执行我们通过APC插入的函数(或者特定情况下,后面会提到) • 特殊用户模式APC:在Windows 10 RS5中添加的相对新的API(也是无官方文档的)
用户模式下APC的API如下
DWORD QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData
)
Alertable
Alertable译为警报状态,恶意软件使用用户模式APC的主要问题之一是,目标线程必须处于警报状态,才能执行恶意软件插入到目标线程中的APC函数,一个线程在调用带有"等候"特性的函数时,将进入警报状态,例如WaitForSingleObjectEx、SleepEx、等等,并且bAlertable的值为TRUE,如下
DWORD WaitForSingleObjectEx(
[in] HANDLE hHandle,
[in] DWORD dwMilliseconds,
[in] BOOL bAlertable
);
DWORD SleepEx(
[in] DWORD dwMilliseconds,
[in] BOOL bAlertable
);
APC具体的执行时机是,当调用SleepEx时,SleepEx执行期间会有一个等待状态,这个等待状态期间,会检查APC队列,如果有函数,就会执行,另外一个能让APC队列中函数执行的方式是NtTestAlert,我们会在后面讲述
微软不想强制用户模式下的线程执行APC,因为那可能会导致微妙的条件竞争和死锁(我猜测的),假设你写的程序中有一个被特定锁保护的列表,你的程序中使用了RPC和APC,当线程获取了锁,想要对列表进行操作,这时一个APC被强制运行在这个线程中,然后APC中的代码尝试获取锁,于是死锁产生了,但如果被插入的APC仅在线程处于警报状态时执行,这会大大改善这种情况发生的概率
在APC内部获取锁不是一个好想法,但在内核态,微软解决了一部分问题,你可以阻止一个线程的APC获取某个锁通过将IRQL提升到APC_LEVEL,或者通过KeEnterCriticalRegion/KeEnterGuardedRegion关闭APC,这在某些情况下是需要的,例如调用ExAcquireResourceSharedLite时,我将在内核模式APC中详细讨论
此外,微软对用户模式APC有一些建议:
• 不要在APC内部进入警报状态 • 不要在远程进程的APC队列中添加函数,因为不同进程中的函数地址、重定位表均不同,还涉及Wow64
我既不同意也不反对这些建议,但是想要用好APC机制,就需要深刻理解它并知道如何使用它
APC用于注入
如果你想在用户模式下使用APC进行注入,你必须找到一个可警报的线程或者希望线程自己进入警报状态,但是在Windows 10 RS5中,微软实现了一个有趣的机制:特殊用户APC,这种机制允许目标线程强制执行我们在APC队列中插入的函数(即使目标线程不是警报状态),原理是通过内核模式APC队列发出信号,触发用户模式下线程的APC函数执行
我们从API视角探讨一下这些APC的不同,假如有2个进程,合法的和恶意的,恶意进程想通过APC将代码注入到合法进程,合法进程只有一个线程,代码如下
int main() {
while (1) {
Sleep(500);
}
return 0;
}
如果注入器使用普通用户APC函数插入函数到目标线程,插入的代码永远不会执行,无需惊讶,因为线程不会进入警报状态,插入的代码位于目标线程的APC队列,但不会执行,直到线程终止,APC队列被释放
特殊用户APC这个机制在Windows 10 RS5中被添加(Native API是NtQueueApcThreadEx,位于ntdll.dll中,后面改为NtQueueApcThreadEx2,这是一个新的syscall),如果这类APC被调用,则在线程执行过程中对其发出信号,让它执行特殊用户APC
这对攻击者是很有吸引力的,但实际上用起来很危险,假设一个线程调用LoadLibrary期间,攻击者调用特殊用户APC想要将自己的代码加入到目标线程APC队列中,已知LoadLibrary会修改PEB中加载器的结构以及获取一些锁,攻击者自己的代码也是LoadLibrary,这会造成问题,因为目标线程中已经有了LoadLibrary(这也是为什么微软不想你在线程没有处于警报状态时运行APC队列中的函数),这种情况下线程会卡住或者PEB中加载器的数据结构被异常修改,这个问题听起来很少见,但实际上很危险,因为不只是LoadLibrary使用锁,其他好多函数都会用到锁,不过,特殊用户APC对于攻击者还是很有用的
通常,想要正确使用APC需要你对目标线程有一定了解
探索API
让我们从底层开始,内核提供了3个和APC相关的API:NtQueueApcThread、NtQueueApcThreadEx、NtQueueApcThreadEx2,其中QueueUserAPC是NtQueueApcThread的上层函数,QueueUserAPC2是NtQueueApcThreadEx和NtQueueApcThreadEx2的上层函数,均位于KernelBase.dll中,我们看一下
// ThreadHandle - 线程句柄,必须拥有THREAD_SET_CONTEXT权限,可以是不同进程下线程的句柄(尽管微软不推荐使用不同进程下线程句柄)
//
// ApcRoutine - 要执行的函数
//
// SystemArgument1-3 - ApcRoutine的前三个参数
//
//
NTSTATUS
NtQueueApcThread(
IN HANDLE ThreadHandle,
IN PPS_APC_ROUTINE ApcRoutine,
IN PVOID SystemArgument1 OPTIONAL,
IN PVOID SystemArgument2 OPTIONAL,
IN PVOID SystemArgument3 OPTIONAL
);
typedef VOID (*PPS_APC_ROUTINE)(
PVOID SystemArgument1,
PVOID SystemArgument2,
PVOID SystemArgument3,
PCONTEXT ContextRecord
);
这个API的用法很简单,我们看一下使用示例(出于简化考虑,移除了错误处理)
VOID QueueLoadLibrary(ULONG ProcessId, PSTR LibraryName) {
PVOID RemoteLibraryAddress;
HANDLE ProcessHandle;
HANDLE ThreadHandle;
NTSTATUS Status;
//
// 使用需要的权限打开进程,用来分配和写入库名
//
ProcessHandle = OpenProcess(
PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE,
FALSE,
ProcessId
);
RemoteLibraryAddress = WriteLibraryNameToRemote(ProcessHandle, LibraryName);
//
// 在进程中获取第一个线程的句柄
//
NtGetNextThread(
ProcessHandle,
NULL,
THREAD_SET_CONTEXT,
0,
0,
&ThreadHandle
);
NtQueueApcThread(
ThreadHandle,
GetProcAddress(GetModuleHandle("kernel32"), "LoadLibraryA"),
RemoteLibraryAddress,
NULL,
NULL
);
}
用法很简单,查看注释就能理解,可以看到LoadLibraryA的函数原型不是PPS_APC_ROUTINE,但在x64系统中没关系
QueueUserAPC:KernelBase.dll层
微软喜欢为系统调用创建“包装器”,这个“包装器”就是指上层函数,以至于他们可以改变内部实现而不影响上层函数,微软也喜欢COM和DLL的载入和重定向机制,上述的结合造就了QueueUserAPC
//
// This is the wrapper function implemented in kernelbase.dll to queue APCs. This function is documented.
// This function has 3 arguments:
//
// pfnAPC - the pointer to the apc routine in the target process context.
// Note that the signature of this function is different from the signature in NtQueueApcThread.
//
// hThread - the handle to the target thread. Requires THREAD_SET_CONTEXT.
//
// dwData - the context argument passed to pfnAPC - This is the only argument passed to pfnAPC.
//
DWORD
QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData
);
//
// This is the signature of the APC Routine if QueueUserAPC is used.
// The only parameter here is the dwData argument from QueueUserAPC.
// You may ask why the signature is different than the signature of PPS_APC_ROUTINE, we'll see below why.
//
typedef
VOID
(NTAPI *PAPCFUNC)(
IN ULONG_PTR Parameter
);
//
// This is the reverse engineered implementation of QueueUserAPC in windows 10
// (In was changed a bit in the latest insider, you'll see below)
//
// This function captures the activation context of the current thread
// and saves it, so it can be inherited by the APC routine.
//
// Activation Contexts are data structures that save configuration for DLL redirection, SxS and COM.
// To read more about activation context: https://docs.microsoft.com/en-us/windows/win32/sbscs/activation-contexts.
//
DWORD
QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData
)
{
ACTIVATION_CONTEXT_BASIC_INFORMATION Info;
NTSTATUS Status;
//
// 捕获激活上下文,激活上下文指DLL重定向、SxS、COM等配置,并传递给APC队列中的函数
//
Status = RtlQueryInformationActivationContext(
1,
NULL,
NULL,
ActivationContextBasicInformation,
&Info,
sizeof(Info),
NULL
);
if (!NT_SUCCESS(Status)) {
DbgPrint("SXS: %s failing because RtlQueryInformationActivationContext() returned status %08lx",
"QueueUserAPC", Status);
BaseSetLastNTError(Status);
return 0;
}
//
// Forward the call to the actual system call.
// The ApcRoutine that is used is actually a wrapper function in ntdll, called "RtlDispatchApc"
// The purpose of this wrapper function is to use the activation context passed as a parameter.
//
Status = NtQueueApcThread(
hThread, // ThreadHandle
RtlDispatchAPC, // ApcRoutine
(PPS_APC_ROUTINE)pfnAPC, // SystemArgument1
(PVOID)dwData, // SystemArgument2
Info.hActCtx // SystemArgument3
);
if (!NT_SUCCESS(Status)) {
BaseSetLastNTError(Status);
return 0;
}
return 1;
}
//
// This is used as SystemArgument3 if QueueUserAPC
// was used to queue the APC.
//
typedef union _APC_ACTIVATION_CTX {
ULONG_PTR Value;
HANDLE hActCtx;
} APC_ACTIVATION_CTX;
//
// This is the actual APC routine.
// It enables the activation context, calls the user provided routine, and deactivates the context.
//
VOID
RtlDispatchAPC( // ntdll
PAPCFUNC pfnAPC,
ULONG_PTR dwData,
APC_ACTIVATION_CTX ApcActivationContext
)
{
RTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME_EXTENDED StackFrame;
//
// Initialize the StackFrame data structure.
//
StackFrame.Size = sizeof(RTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME_EXTENDED);
StackFrame.Format = 1;
StackFrame.Extra1 = 0;
StackFrame.Extra2 = 0;
StackFrame.Extra3 = 0;
StackFrame.Extra4 = 0;
if (ApcActivationContext.Value == -1) {
pfnAPC(dwData);
return;
}
//
// Use the activation context of the queuing thread.
//
RtlActivateActivationContextUnsafeFast(&StackFrame, ApcActivationContext.hActCtx);
//
// Call the user provided routine.
//
pfnAPC(dwData);
//
// Pop the activation context from the "activation context stack"
//
RtlDeactivateActivationContextUnsafeFast(&StackFrame);
//
// Free the handle to the activation context.
//
RtlReleaseActivationContext(ApcActivationContext.hActCtx);
正如代码和注释中展示的,攻击者插入的APC函数位于SystemArgument1中,这对于反病毒开发中想要hook的人来说,需要注意
NtQueueApcThreadEx:内核内存的重复使用
每次NtQueueApcThread被调用,内核模式中会有一个新的KAPC对象被创建(从内核池中)来存储关于APC对象的数据,如果有一个组件它的APC队列中有很多APC函数,这会影响性能,因为大量非分页内存被使用,并且内存分配也需要时间
在Windows 7中,微软在内核模式添加了一个非常简单的对象,叫做内存保留对象,它允许在内核模式为特定对象保留内存,在释放对象时,用相同区域存储另一个对象,这样大大减少了ExAllocatePool/ExFreePoll的调用,NtQueueApcThreadEx就是接收这样一个对象的句柄,因此允许调用者重复使用相同的内存
下述代码用于创建“保留内存”
//
// 内存保留对象当前支持分配2种类型的对象
// - User APC
// - Io Completion
//
typedef enum _MEMORY_RESERVE_OBJECT_TYPE {
MemoryReserveObjectTypeUserApc,
MemoryReserveObjectTypeIoCompletion
} MEMORY_RESERVE_OBJECT_TYPE, *PMEMORY_RESERVE_OBJECT_TYPE;
//
// 这个系统调用分配一个内存保留对象
//
NTSTATUS
NtAllocateReserveObject(
__out PHANDLE MemoryReserveHandle,
__in_opt POBJECT_ATTRIBUTES ObjectAttributes,
__in MEMORY_RESERVE_OBJECT_TYPE ObjectType
);
//
// 这是Windows 7中新添加的系统调用
// 这个系统调用功能和NtQueueApcThread类似,但是允许指定一个MemoryReserveHandle
// 使用NtAllocateReserveObject创建的对象的句柄
//
// 如果内存在使用中(例如,APC对象没有被释放),你可以重新使用内存
//
NTSTATUS
NtQueueApcThreadEx(
IN HANDLE ThreadHandle,
IN HANDLE MemoryReserveHandle,
IN PPS_APC_ROUTINE ApcRoutine,
IN PVOID SystemArgument1 OPTIONAL,
IN PVOID SystemArgument2 OPTIONAL,
IN PVOID SystemArgument3 OPTIONAL
);
使用这个新对象,你可以节省KAPC对象分配的开销,示例代码如下
int main(int argc, const char** argv)
{
NTSTATUS Status;
HANDLE MemoryReserveHandle;
Status = NtAllocateReserveObject(&MemoryReserveHandle, NULL, MemoryReserveObjectTypeUserApc);
if (!NT_SUCCESS(Status)) {
printf("NtAllocateReserveObject Failed! 0x%08Xn", Status);
return -1;
}
while (TRUE) {
Status = NtQueueApcThreadEx(
GetCurrentThread(),
MemoryReserveHandle,
ExampleApcRoutine,
NULL,
NULL,
NULL
);
if (!NT_SUCCESS(Status)) {
printf("NtQueueApcThreadEx Failed! 0x%08Xn", Status);
return -1;
}
// 警报状态的
SleepEx(0, TRUE);
}
return 0;
}
VOID ExampleApcRoutine(PVOID SystemArgument1, PVOID SystemArgument2, PVOID SystemArgument3) {
// 非警报状态的
Sleep(500);
printf("This is the weird loop!n");
}
此机制被RPC服务器用来重复使用完成例程的APC对象,如果你对此感兴趣,可以进一步查看rpcrt4!CALL::QueueAPC
NtQueueApcThreadEx:特殊用户APC
Windows 10 RS5开始,加入了特殊用户APC功能,正如我上面提到的,特殊用户APC可以强制一个线程执行APC队列中的函数,无需线程处于警报状态
Windows 10 RS5中,微软不想添加新的系统调用,所以修改了NtQueueApcThreadEx来支持特殊用户APC,通过调整MemoryReserveHandle为一个联合体
// 定义一个枚举类型,后面只会用到QueueUserApcFlagsSpecialUserApc
typedef enum _QUEUE_USER_APC_FLAGS {
QueueUserApcFlagsNone,
QueueUserApcFlagsSpecialUserApc,
QueueUserApcFlagsMaxValue
} QUEUE_USER_APC_FLAGS;
// 之前的MemoryReserveHandle被替换为联合体USER_APC_OPTION,出于兼容性考虑,包含了之前的MemoryReserveHandle
typedef union _USER_APC_OPTION {
ULONG_PTR UserApcFlags;
HANDLE MemoryReserveHandle;
} USER_APC_OPTION, *PUSER_APC_OPTION;
// 除了MemoryReserveHandle被替换为UserApcOption,其他和上面一样,这允许调用者使用UserApcOption中的MemoryReserveHandle或UserApcFlags
NTSTATUS
NtQueueApcThreadEx(
IN HANDLE ThreadHandle,
IN USER_APC_OPTION UserApcOption,
IN PPS_APC_ROUTINE ApcRoutine,
IN PVOID SystemArgument1 OPTIONAL,
IN PVOID SystemArgument2 OPTIONAL,
IN PVOID SystemArgument3 OPTIONAL
);
下面是特殊用户APC的示例代码
int main(
int argc,
const char** argv
)
{
PNT_QUEUE_APC_THREAD_EX NtQueueApcThreadEx;
USER_APC_OPTION UserApcOption;
NTSTATUS Status;
NtQueueApcThreadEx = (PNT_QUEUE_APC_THREAD_EX)(GetProcAddress(GetModuleHandle("ntdll.dll"), "NtQueueApcThreadEx"));
if (!NtQueueApcThreadEx) {
printf("wtf, before win7n");
return -1;
}
//
// This is a special flag that tells NtQueueApcThreadEx this APC is a special user APC.
//
UserApcOption.UserApcFlags = QueueUserApcFlagsSpecialUserApc;
while (TRUE) {
//
// This will force the current thread to execute the special user APC,
// Although the current thread does not enter alertable state.
// The APC will execute before the thread returns from kernel mode.
//
Status = NtQueueApcThreadEx(
GetCurrentThread(),
UserApcOption,
ApcRoutine,
NULL,
NULL,
NULL
);
if (!NT_SUCCESS(Status)) {
printf("NtQueueApcThreadEx Failed! 0x%08Xn", Status);
return -1;
}
//
// This sleep does not enter alertable state.
//
Sleep(500);
}
return 0;
}
VOID
ApcRoutine(
PVOID SystemArgument1,
PVOID SystemArgument2,
PVOID SystemArgument3
)
{
printf("yo wtf?? I was not alertable!n");
}
需要注意:特殊用户APC可以打断一个不同线程的执行,在之后的文章会提到
NtQueueApcThreadEx2:一些新东西
大约在Windows 10 19603,微软新添加了2个函数
• NtQueueApcThreadEx2:这是一个新的系统调用,允许同时传递UserApcFlags和MemoryReserveHandle(由于冲突检查,这并不会生效) • QueueUserAPC2:这是位于kernelbase.dll中的新的Win32 API,允许用户访问特殊用户APC
这个Win32 API表明微软希望用户使用这个API,这可以被用来在线程执行期间对其发送信号,这会很有用,类似于Linux中的信号机制
DWORD WaitForSingleObjectEx(
[in] HANDLE hHandle,
[in] DWORD dwMilliseconds,
[in] BOOL bAlertable
);
DWORD SleepEx(
[in] DWORD dwMilliseconds,
[in] BOOL bAlertable
);
0
NtTestAlert
NtTestAlert是Windows中警报机制相关的系统调用,它会让线程APC队列中函数执行,即使这个线程本身是不能警报的,我们会在后面探索这个机制的内部原理,通过调用NtTestAlert可以执行任何待处理的APC函数
总结
在下一篇文章中,我将继续探索APC内部原理,这个是存储APC相关代码的地址
参考
https://repnz.github.io/posts/apc/user-apc/
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...