查看原文
其他

基于Wrk和ReactOS源码分析APC机制的记录

cat喵 看雪学院 2019-05-26

一、首先在用户层的APC(asynchronous procedure call)相关函数如下:



这个函数在Kernel32.dll导出,我们常见的APC注入方式,在ring3下就可以使用该函数,查看ReactOS源码可以发现该函数内部调用如下:


我们都知道Nt*和Zw*函数在ring3下大多在ntdll.dll中,通过系统调用(int 0x2e或者sysenter...)进入ring0,我们也可以在3环直接使用NtQueneApcThread插入APC异步过程。

NtQueueApcThread分析:


  • hThread 是线程句柄,可以是当前线程本身,也可以是其它线程(同一进程的其它线程和不同进程的其它线程)。


  • pfnAPC 就是我们要进行异步过程调用的函数指针,需要注意如果hThread是其它线程的情况,pfnAPC的地址是hThread所在线程空间的地址。

  • BaseDispatchApc 是内部的一个函数,我们可以看到其实我们传入的pfnAPC是作为该函数的一个参数在内部APC调用的时候进行调用我们自己的函数,其实使用NtQueneApcThread函数的时候在这里我们可以直接使用pfnAPC代替BaseDispatchApc,然后我们的回调函数可以多出来两个参数传递的位置。

经过NtQueueApcThread的系统调用后从3环进入0环,下面分析在0环下的一些情况。


­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­

二、在内核层的APC分析



首先看下 NtQueueApcThread 在 ring0 下的具体实现逻辑:


首先根据我们传入的线程句柄,通过 ObReferenceObjectByHandle 得到线程的ETHREAD对象,其实这一步在源码中很多函数的开始作为一些基本信息的获取是经常出现的。


接下来如果顺利得到ETHREAD对象,会判断该线程是否是系统线程,
(Thread)­>CrossThreadFlags&PS_CROSS_THREAD_FLAGS_SYSTEM) != 0 ­­­> 如果 true就说明是系统线程。


这时候 STATUS_INVALID_HANDLE 作为返回值返回,说明你想这样对系统线程插入APC?不可能的,直接拒绝。


如果不是系统线程,分配一个_KAPC对象,然后初始话APC对象:

我们先看一下_KAPC对象的具体结构体:

下面来分析 KeInitializeApc 中具体填充了_KAPC的哪些字段:


Apc­>Type = ApcObject;­­>这个ApcObject也是枚举变量,属于_KOBJECTS
Apc­>Size = sizeof(KAPC);­­>这种用法在3环的windows编程中经常出现
填充ApcStateIndex:


如果 Environment 是 CurrentApcEnvironment,那么就直接取线程对象的ApcStateIndex;
否则 Apc­>ApcStateIndex = (CCHAR)Environment;­­­> 在我们分析的这个流程的上下文就是代表的 OriginalApcEnvironment(0)

} else {
     /*Environment必须小于等于线程的ApcStateIndex或者Environment是
InsertApcEnvironment插入的情况*/

       ASSERT((Environment <= Thread‐>ApcStateIndex) || (Environment ==
InsertApcEnvironment));
       Apc‐>ApcStateIndex = (CCHAR)Environment;
   }


Apc­>Thread = Thread; ­­­> 指向插入APC的线程

Apc­>KernelRoutine = KernelRoutine; ­­­> 当前流程是固定的PspQueueApcSpecialApc,该函数只是单纯的释放_KAPC对象内存
Apc­>RundownRoutine = RundownRoutine;


-->这里是NULL,这个函数例程在PspExitThread退出线程的时候会被调用,具体分析进程和线程的时候可以看到,也就是说
RundownRoutine 是线程退出前的回调。


Apc­>NormalRoutine = NormalRoutine;­­­>BaseDispatchApc,我们在3环或者0环下可以直接使用NtQueueApcThread指定其它的函数。

接下来判断NormalRoutine是不是NULL:

如果是NULL,那么


Apc‐>ApcMode = KernelMode;/*内核模式*/
Apc‐>NormalContext = NIL;/*置为NULL*/


如果不是NULL,那么


Apc‐>ApcMode = ApcMode;//当前上下文是UserMode
Apc‐>NormalContext = NormalContext;/*这就是我们真正自己实现的APC回调函数*/


最后插入状态是FALSE:
Apc­>Inserted = FALSE;


在KeInitializeApc后调用KeInsertQueueApc插入APC链表中,


下面分析KeInsertQueueApc具体是怎么实现:


否则插入APC


而真正执行插入APC链表的是KiInsertQueueApc。
先看一下_KAPC_STATE的结构体:


分析KiInsertQueueApc:


下面正式插入又分为Apc­>NormalRoutine 是否为NULL,对照上面的分析,结合前后文理解:


如果是NULL:


/*
Thread­>ApcState表示当前要执行的ApcState,可能是挂靠进程的;
Thread­>SavedApcState表示挂靠后保存的当前线程的ApcState;
Thread­>ApcStatePointer就是保存的两个ApcState的指针;
关于进程挂靠可以去分析KiAttachProcess
*/


如果不是NULL,又分为 ApcMode 是 KernelMode 和 UserMode:
如果是 UserMode 并且 Apc­>KernelRoutine == PsExitSpecialApc:


如果是内核模式或者 Apc­>KernelRoutine 不等于 PsExitSpecialApc:



插入完成后,在把当前APC插入链表后要判断Apc­>ApcStateIndex 和Thread­
>ApcStateIndex是否相等,如果相等的话要经过一些检查判断


决定APC是不是要中断当前线程的运行或者将线程从等待状态排序:



如果APC线程是当前线程:



如果APC线程不是当前线程:


ApcMode是内核模式:



ApcMode是用户模式:


那么至此APC已经插入线程就等待触发执行了。


总结一下:


1. APC插入线程是Running状态且Apc­>ApcMode是KernelMode就会触发软中断先去执行APC??


2. APC插入线程是Waiting状态且满足一些条件就会把线程设为准备执行的状态(KiUnwaitThread)。


3. APC插入线程是GateWait状态且满足一些条件会插入当前CPU的延迟执行列表。


三、APC的执行时机



那么APC是在那个函数中进行的派发呢?答案是KiDeliverApc。
最初并不知道在哪里派发APC,毕竟异步啊,但是通过查找Apc­>KernelRoutine和Apc­
>NormalRoutine的引用,定位到该函数,
那么下面来看一下该函数内部具体做了什么事情把:



先看上面函数原型,有异常帧和陷阱帧,其实这个和APC触发的时机有关,也就是说和调用KiDeliverApc函数的函数有关系,下面分析流程的时候对TrapFrame等参数的处理暂且忽略,只关注核心的分发逻辑。



分析一下KiInitializeUserApc:



APC派发流程总结如下:
内核APC的KernelRouting > 内核APC的NormalRouting > 用户APC的KernelRouting > 用户APC的NormalRouting(不断的在3环和0环切换中)


应用:
1.APC注入
2.windows异步IO
3.在KernelRouting插入PspExitThread结束宿主线程

TODO:
1.需要分析0环和3环的栈帧切换和构造





本文由看雪论坛 cat喵  原创

转载请注明来自看雪社区



往期热门阅读:



扫描二维码关注我们,更多干货等你来拿!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存