APC

Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html

APC

1. APC的本质

  一个线程,其一直占用着CPU,对CPU拥有所有权,不可能从外部改变行为。

  APC的本质就是:通过一个函数,让线程执行,从而可以从外部改变该线程的行为。

  1)APC的挂入位置 _KAPC_STATE

    _ETHREAD+0x034 ApcState,存在一个_KAPC_STATE结构体:

    其存在两个双向链表,称为APC队列,一个挂内核APC队列,一个挂用户APC队列。

    //0x18 bytes (sizeof) struct _KAPC_STATE {

      struct _LIST_ENTRY ApcListHead[2]; //0x0 用户/内核 APC队列

      struct _KPROCESS* Process; //0x10 所挂靠的进程

      UCHAR KernelApcInProgress; //0x14 当前内核APC函数是否执行 0/1

      UCHAR KernelApcPending; //0x15 是否存在内核APC 0/1

      UCHAR UserApcPending; //0x16 是否存在用户APC 0/1

    };

  2)APC存储的单元 _KAPC

    前面我们介绍过 _KAPC_STATE,其中一个Entry_List,里面一个个结构就是 _KAPC结构,如下:

    //0x30 bytes (sizeof)

    struct _KAPC {

      SHORT Type; //0x0 类型

      SHORT Size; //0x2 大小

      ULONG Spare0; //0x4 未发现使用

      struct _KTHREAD* Thread; //0x8 目标线程

      struct _LIST_ENTRY ApcListEntry; //0xc APC队列挂的位置(双向链表)

      VOID (*KernelRoutine)(struct _KAPC* arg1, VOID (**arg2)(VOID* arg1, VOID* arg2, VOID* arg3), VOID** arg3, VOID** arg4, VOID** arg5); //0x14 APC完成释放内存

      VOID (*RundownRoutine)(struct _KAPC* arg1); //0x18

      VOID (*NormalRoutine)(VOID* arg1, VOID* arg2, VOID* arg3); //0x1c APC函数所在的位置:如果是内核APC,其是函数地址;如果是用户APC,则是三环总入口

      VOID* NormalContext; //0x20 VOID* SystemArgument1; //0x24 内核APC:略;用户APC:当前函数的总入口。

      VOID* SystemArgument1; //0x24  APC函数的参数 

      VOID* SystemArgument2; //0x28  APC函数的参数

      CHAR ApcStateIndex; //0x2c 挂哪个队列,有四个值 0,1,2,3

      CHAR ApcMode; //0x2d UCHAR 用户APC 内核APC

      Inserted; //0x2e 表示当前APC是否已经挂入

    };

  3)APC函数何时执行

    关注一下KiServiceExit,从零环返回三环就通过这个函数,该函数是系统调用、异常和中断的必经之路。

    APC处理函数通过  _KiDeliverApc 函数来执行,而 KiServiceExit 上来就先检查是否存在用户APC,如果有就调用该函数来执行。

    该函数先判断是否存在内核APC,如果内核APC存在就先执行内核APC,然后再执行用户APC。

    

 

 

2. 备用APC队列

  _Kthread+0x14c SavedApcState存在一个备用APC队列,其与 +0x034 ApcState位置结构体完全一样。

  其和进程挂靠相关,如果不了解,可以去看《进程与线程》一节,该节后面介绍了进程挂靠相关细节。

  1)线程APC中的函数都是与进程相关联的

    线程APC中的函数要执行,执行的是当前CR3的内存地址,但是线程可以挂靠,当线程A挂靠到其他进程的CR3时,

    如果此时线程A的APC函数要进行内存读写,其就会读写挂靠进程的内存地址,显然会发生错误。

  2)SavedApc作用:

    SavedApc函数就是为了避免当出现线程挂靠时内存读取错误,当线程挂靠时,其将该线程的APC存储到SavedApc中。

    等到解除挂靠,再还原回来,这样就避免了内存执行错误。

  3)SavedApc真实运行策略:

    在挂靠环境下,也是可以向当前线程插入APC的,比如X进程中A线程挂靠T进程,此时也可以插入APC函数,只不过针对B进程的。

    ApcState:B进程相关的APC函数。

    SavedApcState:A进程相关的APC函数。

  4)_KTHREAD+0x138 ApcStatePointer[2]:

    Windows为了方便操作这两个_APC_STATE,设置了一组指针,在_KTHREAD+0x138处 ApcStatePointer[2],其操作情况如下。

    因此,如果找原线程的APC,直接ApcStatePointer[0]就好,找Saved就找ApcStatePointer[1],很好理解。

    

  5)_KTHREAD+0x165  ApcStateIndex 实现组合寻址

    0 正常状态 / 1 挂靠状态

    其经常会结合ApcStatePointer来进行寻址

    A进程的线程挂靠B进程,如果在非挂靠的情况下,此时插入的是A进程的APC,因此为ApcStatePointer[ApcState];

    如果此时在挂靠情况下,插入的进程就是关于B进程的APC,此时A进程的APC被备份到SavedApcState,B进程的也为ApcStatePointer[ApcState]。

  6)_KTHREAD+0x166 ApcQueueAble

    表示当前线程是否可以插入APC,比如线程退出时,不允许插入APC。

    此时会将ApcQueueAble置为0,则进制APC挂入。

 

3. APC的插入

  

   1)KeInitalizeApc函数分析

    该函数声明如下,简单来说就是对应KAPC中的各个成员(可在文章开头查看)

    

   2)KAPC.ApcStateIndex 作用

    注意,其与KTHREAD.ApcStateIndex同名,但其值只有0/1,我们在之前的进程挂靠讲过,配合ApcStatePointer来指向有关地址。

    0 原始环境 ;1 挂靠环境 ;2 当前环境 ;3 插入APC时的当前环境。

    结合挂靠那一节,我们来分析下面的各种情况,以A进程的线程挂靠B进程为例(可能有点乱,一定结合上面挂靠来看)

    0 原始环境:ApcStatePointer[0] 正常:ApcState;挂靠:SavedApcState,其都是写入A进程的ApcState。

    1 挂靠环境:ApcStatePointer[1] 正常:SavedApcState;挂靠:ApcState,都是写入B进程的ApcState。

    2 当前环境:其在初始化时修改为当前线程的Kthread.ApcStateIndex,Pointer[ApcStateIndex],挂靠哪个插入哪个。

    3 插入Apc时当前环境: 真正指向插入时(KiInserQueueApc),再做判断,插入当前进程的Apc中。(初始化到插入时,可能APC又被修改)

   3)KiInsertQueueApc函数分析

    该函数虽然长,但结构体比较单一,很好分析其对应的操作步骤。

    

   4)Kthread+0x164 Alterable属性

    Kthread+0x164 Alterable,其表示是否可以被用户APC唤醒。

    我们在挂起线程调用SleepEx或WaitForSingleObjectEx,其最后一个参数就是修改这个值(注意,必须是Ex结尾的函数)。

    当在KiInsertQueueApc插入用户KAPC之后,其会判断是否需要唤醒当前线程,如果此时值为1,则唤醒线程执行用户APC。

    

 

4. 内核APC执行过程

  1)APC函数的执行与插入不是一个线程

    A线程向B线程插入一个APC,插入的动作在A线程中完成的,但什么时候执行则由B线程决定!所以叫“异步过程调用”。

    内核APC函数与用户APC函数的执行时间和执行方式也有区别。

  2)内核APC的时机

    ①SwapContext

      我们在线程切换时,会判断是否要有用户APC执行,注意,此时作为SwapContext的返回值返回,其一直返回到KiSwapThread中。

      此时如果返回值为1,其会调用KiDeliverApc函数来处理当前线程的Apc。

    ②KiServiceExit

      KiServiceExit中也会判断是否存在用户APC,调用KiDeliverApc函数来执行。

  3)KiDeliverApc函数分析

    内核APC如下(注意,其_LIST_ENTRY偏移在中间,故看起来很不美观),其直接从_KAPC中取出kernel

     

 

 

 

5. 用户APC执行过程

  1) 用户APC函数的执行时机

  当程序在零环执行完成返回三环时,其调用_KiServiceExit,此时其调用_KiDeliverApc来检查是否有派发的APC函数,然后执行。

  

  2) 用户APC执行流程

  当发现有用户APC要执行时,其处于零环,要执行必须返回三环。

  执行流程为:零环->三环(执行用户APC)->零环->三环(正常退出)。

  之前我们在系统调用这中提到过如何从三环进到零环,其三环现场保存在_KTRAP_FRAME(_ETHREAD+0x124)这个结构体中。

  此时回去肯定不能从_KTRPA_FRAME中返回三环。

  3)构建_CONTEXT结构体返回三环

  返回三环时根据_KTRAP_FRAME.Eip来返回三环,因此我们想要处理用户APC,其必须修改KTrapFrame.EIP。

  1> KiDiverApc函数中调用KiInitalUserApc来初始化用户APC环境

    

   2> KiInitalUserApc函数中调用KiContextFromKframes将TrapFrame转换为CONTEXT结构体

    这一步的目的是为了备份原来的TrapFrame,因为返回三环必然修改TrapFrame,因此将旧的转换为CONTEXT预先放到三环的堆栈。

    

  3> KiInitalUserApc函数中将_APC_RECORD和_CONTEXT保存到三环的堆栈中

    虽然此时处于零环,但是可以从TrapFrame.esp来获取三环的堆栈地址,然后将两者保存进去。

    因为APC执行完之后还必须从三环进入零环,此时直接在堆栈进行操作进行复原即可。

    1* 获取esp并计算提升堆栈大小

      

    2* 将 _Context 写入三环地址

      

      3* 将ApcRecord写入三环地址

      

       4* 保存之后三环的堆栈空间

      

 

  4> KiInitalUserApc函数中修改TrapFrame为返回三环做准备

    其修改很多TrapFrame的值,但对于我们最重要的就是回到三环后的落脚点。

    其回到KeUserApcDispatch来执行用户的APC函数,至于函数地址,ApcRecord.NormalContext存放的是真正的APC函数。

    

  4)总结

  理解上面过程,此时,我们就可以通过KiServiceExit函数利用KTrapFrame来返回用户层,其返回的就是KiInitalizeUserApc函数,然后执行用户APC。

  当用户APC执行完成之后,返回零环,此时就是Context。我们直接在三环把Context再转换为KtrapFrame,这之后就很好理解了。

posted @ 2020-03-30 18:48  OneTrainee  阅读(1997)  评论(0编辑  收藏  举报