• 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,这之后就很好理解了。

  • 相关阅读:
    UVA 10827 Maximum sum on a torus 最大矩阵和
    UVA 11100 The Trip, 2007 水题一枚
    编程之美2015测试赛 题目1 : 同构
    UVA 10801 Lift Hopping Floyd
    UVA 11389 The Bus Driver Problem 贪心水题
    UVA 11039 Building designing 贪心
    UVA 11636 Hello World! 水题
    poj 3070 Fibonacci 矩阵快速幂
    hdu 1757 A Simple Math Problem 矩阵快速幂
    了解常见的 Azure 灾难
  • 原文地址:https://www.cnblogs.com/onetrainee/p/12600409.html
Copyright © 2020-2023  润新知