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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM