[原创]QueueUserAPC执行成功条件-逆向总结
发表于:
2022-4-29 14:12
24075
[原创]QueueUserAPC执行成功条件-逆向总结
使用Win32 API QueueUserAPC做注入时总是遇到明明插入成功了,但就是得不到执行的情况。为了得到原因,在Win7 x86 PAE模式下跟踪了一下QueueUserAPC的整个处理过程(初始化->插入->执行),详细分析过程我会用附件的形式给出,下面是得到的一些结论,有错误欢迎指出,一起交流。
在系统调用、中断、异常等操作发生后,会从r3进入内核来处理,在内核处理完准备返回r3时,会调用这个函数,在函数内部会做出一些判断来决定是否要交付用户态APC,判断条件就是当前线程的ApcState.UserApcPending,这个域为1时,表示有要处理的用户态APC存在,才会继续调用KiDeliverApc,这一点很关键。
这个函数进去第一件事就是判断当前线程的SpecialApcDisable域是否为1,这个域相当于一个总开关,置1表示,内核和用户态APC都不能交付,函数直接返回。如果为0,判断内核APC队列是否有内容,没有就去交付用户APC,如果有内核APC,一个循环全部交付完,但在交付内核APC过程中,一旦检查发现KernelApcDiable域为1,KiDeliverApc函数直接返回,不会向下处理用户APC了。这里可以得出结论,用户态APC必须在内核APC交付完才有机会交付。但通过QueueUserAPC插入的APC是不存在内核APC的,直接相当于内核APC交付完了。 而内核APC交付完,会开始交付用户APC,这里需要注意一下,使用QueueUserApc插入的APC,真正的回调函数是保存在NormalContext里,而NormalRoutine只是保存了一个r3执行时候的派发入口(ntdll.dll中的8号导出函数),而要执行回调函数,需要从内核返回到r3由派发函数统一派发。Windows这里采用的是劫持TrapFrame实现的回r3。
从上面的执行过程分析得出,要想让由QueueUserApc插入的用户态APC交付成功,需要满足以下条件:
第三点暂时不用考虑,一般都是0,但UserApcPending什么时候才能是1以及队列什么时候被插入APC?这就需要去分析QueueUserApc的初始化和插入过程了。
这个函数是系统调用进来先执行的内核函数,函数内部首先判断目标线程是否是系统线程,即CrossThreadFlags.SystemThread位是否为1,如果为1函数直接返回,为0才分配KAPC结构内存并初始化。
这个函数中判断插入的目标线程的ThreadFlags.ApcQueue位,如果这个位为0,不插入队列,直接返回。不为1,继续调用KiInsertQueueApc。
将APC插入ApcList[1]队列。这个函里面有个点,因为调用QueueUserApc环境固定为0(始终插入原始环境),也就是KAPC.ApcStateIndex始终为0,而这个函数里会把当前线程的ApcStateIndex与KAPC.ApcStateIndex做对比,因为线程在挂靠的时候ApcStateIndex为1,这样就说明无法插入一个正在挂靠的线程。接下来会去判断目标线程是不是当前线程,如果是当前线程并且是用户态APC,函数也返回了。前面条件都满足了,会判断目标线程是否处于可以被唤醒的等待状态,如果不是,函数在这里也返回了。如果是,将目标线程唤醒(从等待链表->延时就绪链表),唤醒成功后将UserApcPending置1。到这里,条件全部满足。
从检查顺序排列,要想QueueUserApc执行成功,目标线程需要满足以下条件:
CurrentThread.ApcState.UserApcPending值为1
ApcList[1]队列不为空
CurrentThread.SpecialApcDisable值为0
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
上传的附件: