翻开小Win的菜单,APC赫然在目...
做工讲究,味道不错,是小Win的热门菜,我们点一来尝尝!
吃了可以做很多事情...
细节来自于ReactOS
源码分析。
如果对这个发神经的文风有任何不适,请谅解,因为我确实神经了
点APC的正确姿势是使用QueueUserApc
,不走寻常路的也可以使用NtQueueApcThread
也就是QueueUserApc内部是NtQueueApcThread做的,两者区别不大,当然,使用后者可以字节加点调料(不使用IntCallUserApc、换成自己的函数,函数参数也可以有三个了,而PARCFUNC只有一个参数)。
小Win默认是通过统一的接口IntCallUserApc来调用的顾客指定的Apc函数。
NtQueueApcThread经过系统调用进入到ring0,一般人是看不到了...,我也是一般人来着,下面努力变成二班的...。
进了NtQueueApcThread,先通过KeInitializeApc初始化一个Apc对象
APC对象结构定义如下:
根据KeInitializeApc传入参数,Apc被赋值如下:
其中关于ApcStateIndex有4中值,如下:
Apc->KernelRoutine总是有值的,被赋值为PspQueueApcSpecialApc,用于Apc结束时候释放Apc对象内存
通过KeInsertQueueApc
插入队列,在队列中等待被上菜...
先不管Apc是怎么得到执行的,来看看KAPC_STATE
其中ApcListHead保存了线程的两个Apc链表,分别对应UserMode和KernelMode。
Thread->ApcState表示当前需要执行的ApcState,可能是挂靠进程的
Thread->SavedApcState表示挂靠后保存的当前线程的ApcState,
KTHREAD的ApcStatePointer[2]字段保存了两个ApcState的指针
具体看下面的代码
来一个结构图
Apc已经点了,什么时候才能端上来呢?我们接着看...
线程wait、线程切换到应用层、线程被挂起等,一旦线程有空隙了,windows就会把apc队列顺便执行一遍
搜索NormalRoutine
和KernelRoutine
字段,找到KiDeliverApc
,这个函数是具体分发Apc的函数
那在哪里调用的KiDeliverApc的呢,找到多处
根据《windows内核情景分析》介绍, 执行用户APC的时机在从内核返回用户空间的途中(可能是系统调用、中断、异常处理之后需要返回用户空间)
也就是肯定会经过_KiServiceExit
,那就跟着来看看吧。
// This macro creates an epilogue for leaving any system trap. // It is used for exiting system calls, exceptions, interrupts and generic // traps.
继续看一下调用KiDeliverApc
内部究竟是怎么处理的
根据注释应该很清楚deliver的逻辑了,还是在看张图
CHECK_FOR_APC_DELIVER
用户态Apc的delvier有个重点,Thread->ApcState.UserApcPending必须是TRUE,那什么时候才会是TRUE,我蛮来看看
两种情况都需要Alertable = TRUE,这个字段表示线程是唤醒的,也就是说只有可唤醒的线程,才能拿投递他的用态APC,否则不会
SleepEx, WaitForSingleObject,WaitForMultipleObjects都可以设置线程为Alertable
接着继续看看KiInitializeUserApc
是怎么切换到用户空间执行的用户态函数
执行流程根据注释应该很清楚了,这里要解释一下TrapFrame。
CPU进入内核之后,内核堆栈就会有个TrapFrame,保存的是用户空间的线程(因进入内核原因不同,可能是自陷、中断、异常框架,都是一样的结构)。CPU返回用户空间时会使用这个TrapFrame,才能正确返回原来的断点,并回复寄存器的状态 这里为了让Apc返回到用户空间执行,就会修改这个TrapFrame,原来的TrapFrame就需要保存,这里保存在了用户空间堆栈中(CONTEXT) 执行完Apc函数之后,执行一个NtContinue,将这个CONTEXT作为参数,这样保存的TrapFrame就会还原到原来的状态,然后CPU又能正常回之前的用户空间了。
KiDeliverApc完了之后,回到_KiServiceExit,会使用被修改过的TrapFrame回到用户空间,执行指定的KiUserApcDispatcher
(ntdll提供)
KiUserApcDispatcher
其实挺简单的,通过esp弹出APc函数,然后调用,就进入了IntCallUserApc,
执行完成后,调用_ZwContinue(Context, 1),回到内核回复之前修改TrapFrame,也会重新检查是否有Apc需要投递,有则继续投递, 重复上面的步骤,直到没有了则可以回到之前被中断的用户态的断点处。
下面将_KiServiceExit到IntCallUserApc的流程总结一下:
到这里,终于执行到了用户的Apc函数。
到这,APC流程基本弄清楚了。
下一篇将结合APC机制分析一下最近比较新的AtomBombing注入技术的详细实现和各个细节。
参考
如果大家觉得还不错,欢迎关注我的博客:http://anhkgg.github.io/win-apc-analyze1/
DWORD WINAPI QueueUserApc(PARCFUNC pfnApc, HANDLE hThread, ULONG_PTR dwData);
{
NtQueueApcThread(hThread, IntCallUserApc, pfnApc, dwData, NULL);
}
NTSTATUS NTAPI NtQueueApcThread(IN HANDLE ThreadHandle,
IN PKNORMAL_ROUTINUE ApcRoutine, IN PVOID NormalContext, //pfnApc
IN PVOID SystemArgument1, //dwData
IN PVOID SystemArgument2
);
static void CALLBACK
IntCallUserApc(PVOID Function, PVOID dwData, PVOID Arg3){
((PAPCFUNC)Function)(dwData);
}
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)