<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    APC機制初探

    VSole2022-05-31 16:10:02

    本質

    線程是不能被“殺掉”、“掛起”、“恢復”的,線程在執行的時候自己占據著CPU,別人怎么可能控制它呢?

    舉個極端的例子:如果不調用API,屏蔽中斷,并保證代碼不出現異常,線程將永久占用CPU,何談控制呢?所以說線程如果想“死”,一定是自己執行代碼把自己殺死,不存在“他殺”這種情況!

    那如果想改變一個線程的行為該怎么辦呢?

    可以給他提供一個函數,讓它自己去調用,這個函數就是APC(Asyncroneus Procedure Call),即異步過程調用。

    APC隊列

    kd> dt _KTHREAD
    nt!_KTHREAD
     ...
       +0x034 ApcState     : _KAPC_STATE
     ...
    kd> dt _KAPC_STATE
    nt!_KAPC_STATE
      +0x000 ApcListHead //2個APC隊列 用戶APC和內核APC
      +0x010 Process //線程所屬或者所掛靠的進程
      +0x014 KernelApcInProgress //內核APC是否正在執行
      +0x015 KernelApcPending //是否有正在等待執行的內核APC
      +0x016 UserApcPending //是否有正在等待執行的用戶APC
           
    用戶APC:APC函數地址位于用戶空間,在用戶空間執行
    內核APC:APC函數地址位于內核空間,在內核空間執行
    

    NormalRoutine會找到你提供的APC函數,并不完全等于APC函數的地址。

    APC函數何時被執行?

    KiServiceExit函數:

    這個函數是系統調用、異常或中斷返回用戶空間的必經之路。

    KiDeliverApc函數:

    負責執行APC函數

    逆向TerminateThread/ResumeThread

    自己實現APC隊列的插入,在3環調用QueueUserAPC

    // APC1.cpp : 此文件包含 "main" 函數。程序執行將在此處開始并結束。
    //
    #include 
    #include 
    DWORD WINAPI MyThread(LPVOID)
    {
     int i = 0;
     while (true)
     {
      SleepEx(300, TRUE);
      printf("%d", i++);
     }
    }
    void __stdcall MyApcFunction(LPVOID)
    {
     printf("Run APCFuntion");
     printf("APCFunction done");
    }
    int main(int argc, char* argv[])
    {
     HANDLE hThread = CreateThread(0, 0, MyThread, 0, 0, 0);
     Sleep(1000);
     if (!QueueUserAPC((PAPCFUNC)MyApcFunction, hThread, NULL))
     {
      printf("QueueUserAPC error : %d", GetLastError());
     }
     getchar();
     return 0;
    }
    

    QueueUserApc

    通過3環的QueueUserApc函數可以完成將APC插入到隊列的操作,首先調用了ntdll.dllNtQueueApcThread

    然后通過0xB4的調用號進入ring0

    在windbg里面對應的內核函數為NtQueueApcThread

    然后在ntosknl.exe里面定位到NtQueueApcThread

    最后是調用KeInitializeApcKeInsertQueueApc這兩個函數來實現APC的效果

    備用APC

    備用APC里面有幾個重要的成員

    kd> dt _KTHREAD
    nt!_KTHREAD
       ...
       +0x034 ApcState         : _KAPC_STATE
       ...
       +0x138 ApcStatePointer  : [2] Ptr32 _KAPC_STATE
      ...
       +0x14c SavedApcState    : _KAPC_STATE
      ...
       +0x165 ApcStateIndex    : UChar
       +0x166 ApcQueueable     : UChar
    

    SavedApcState

    線程APC隊列中的APC函數都是與進程相關聯的,具體點說:A進程的T線程中的所有APC函數,要訪問的內存地址都是A進程的。

    但線程是可以掛靠到其他的進程:比如A進程的線程T,通過修改Cr3(改為B進程的頁目錄基址),就可以訪問B進程地址空間,即所謂“進程掛靠”。

    當T線程掛靠B進程后,APC隊列中存儲的卻仍然是原來的APC,具體點說,比如某個APC函數要讀取一個地址為0x12345678的數據,如果此時進行讀取,讀到的將是B進程的地址空間,這樣邏輯就錯誤了

    為了避免混亂,在T線程掛靠B進程時,會將ApcState中的值暫時存儲到SavedApcState中,等回到原進程A時,再將APC隊列恢復。

    所以,SavedApcState又稱為備用APC隊列。

    掛靠環境下ApcState的意義

    在掛靠的環境下,也是可以向線程APC隊列插入APC的,那這種情況下,使用的是哪個APC隊列呢?

    A進程的T線程掛靠B進程,A是T的所屬進程,B是T的掛靠進程

    ApcState B進程相關的APC函數
    SavedApcState A進程相關的APC函數

    在正常情況下,當前進程就是所屬進程A,如果是掛靠情況下,當前進程就是掛靠進程B。

    ApcStatePointer 

    為了操作方便,_KTHREAD結構體中定義了一個指針數組ApcStatePointer ,長度為2。

    正常情況下:

    ApcStatePointer[0] 指向 ApcState

    ApcStatePointer[1] 指向 SavedApcState

    掛靠情況下:

    ApcStatePointer[0] 指向 SavedApcState

    ApcStatePointer[1] 指向 ApcState

    ApcStateIndex

     用來標識當前線程處于什么狀態

     0 正常狀態 1 掛靠狀態

    ApcStatePointer 與 ApcStateIndex組合尋址

     正常情況下,向ApcState隊列中插入APC時:

    ApcStatePointer[0] 指向 ApcState 此時 ApcStateIndex 的值為0

    ApcStatePointer[ApcStateIndex] 指向 ApcState

     掛靠情況下,向ApcState隊列中插入APC時:

     ApcStatePointer[1] 指向 ApcState 此時 ApcStateIndex 的值為1

     ApcStatePointer[ApcStateIndex] 指向 ApcState

     總結:

     無論什么環境下,ApcStatePointer[ApcStateIndex] 指向的都是ApcState,ApcState則總是表示線程當前使用的apc狀態

    APC掛入

    無論是正常狀態還是掛靠狀態,都有兩個APC隊列,一個內核隊列,一個用戶隊列。

    每當要掛入一個APC函數時,不管是內核APC還是用戶APC,內核都要準備一個KAPC的數據結構,并且將這個KAPC結構掛到相應的APC隊列中。

    KAPC

    kd> dt _KAPC
    nt!_KAPC
       +0x000 Type  //類型  APC類型為0x12
       +0x002 Size  //本結構體的大小  0x30
       +0x004 Spare0     //未使用                             
       +0x008 Thread   //目標線程                                  
       +0x00c ApcListEntry //APC隊列掛的位置
       +0x014 KernelRoutine //指向一個函數(調用ExFreePoolWithTag 釋放APC)
       +0x018 RundownRoutine//略 
       +0x01c NormalRoutine //用戶APC總入口  或者 真正的內核apc函數
       +0x020 NormalContext //內核APC:NULL  用戶APC:真正的APC函數
       +0x024 SystemArgument1//APC函數的參數 
       +0x028 SystemArgument2//APC函數的參數
       +0x02c ApcStateIndex //掛哪個隊列,有四個值:0 1 2 3
       +0x02d ApcMode //內核APC 用戶APC
       +0x02e Inserted //表示本apc是否已掛入隊列 掛入前:0  掛入后  1
    

    掛入流程

    KeInitializeApc(APC初始化)

    VOID KeInitializeApc
    (
    IN PKAPC Apc,//KAPC指針
    IN PKTHREAD Thread,//目標線程
    IN KAPC_ENVIRONMENT TargetEnvironment,//0 1 2 3四種狀態
    IN PKKERNEL_ROUTINE KernelRoutine,//銷毀KAPC的函數地址
    IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
    IN PKNORMAL_ROUTINE NormalRoutine,//用戶APC總入口或者內核apc函數
    IN KPROCESSOR_MODE Mode,//要插入用戶apc隊列還是內核apc隊列
    IN PVOID Context//內核APC:NULL  用戶APC:真正的APC函數
    ) 
    

    主要看TargetEnvironment這個參數,對應的是ApcStateIndex,與KTHREAD(+0x165)的屬性同名,但含義不一樣

    ApcStateIndex 有四個值:
    0 原始環境 1 掛靠環境 2 當前環境 3 插入APC時的當前環境
    正常情況下:
     ApcStatePointer[0]  指向 ApcState    
     ApcStatePointer[1]  指向 SavedApcState
    掛靠情況下:
     ApcStatePointer[0]  指向 SavedApcState
     ApcStatePointer[1]  指向 ApcState    
    2 初始化的時候,當前進程的ApcState
    3 插入的時候,當前進程的ApcState    
    

    當傳入值為2時,會直接使用當前進程的ApcState

    當傳入值為3時,使用的是當前進程的APC,那么這里跟2有什么區別呢?

    當初始化的時候可能處于原始環境,也可能處于掛靠環境,在即將插入的那個時候可能環境發生了改變,所以傳入值設置為3

    偽代碼分析

    KiInsertQueueApc(插入APC隊列)

    1) 根據KAPC結構中的ApcStateIndex找到對應的APC隊列
    2) 再根據KAPC結構中的ApcMode確定是用戶隊列還是內核隊列
    3) 將KAPC掛到對應的隊列中(掛到KAPC的ApcListEntry處)
    4) 再根據KAPC結構中的Inserted置1,標識當前的KAPC為已插入狀態
    5) 修改KAPC_STATE結構中的KernelApcPending/UserApcPending
    

    1、Alertable=0 當前插入的APC函數未必有機會執行:UserApcPending = 0

    2、Alertable=1 UserApcPending = 1 將目標線程喚醒(從等待鏈表中摘出來,并掛到調度鏈表)

    KeInsertQueueApc源碼

    NTKERNELAPI
    BOOLEAN
    KeInsertQueueApc (
        __inout PRKAPC Apc,
        __in_opt PVOID SystemArgument1,
        __in_opt PVOID SystemArgument2,
        __in KPRIORITY Increment    //優先級,3環添加用戶APC時默認為0.
    );
    /*++
     
    Routine Description:
     
        This function inserts an APC object into the APC queue specifed by the
        thread and processor mode fields of the APC object. If the APC object
        is already in an APC queue or APC queuing is disabled, then no operation
        is performed. Otherwise the APC object is inserted in the specified queue
        and appropriate scheduling decisions are made.
     
    Arguments:
     
        Apc - Supplies a pointer to a control object of type APC.
     
        SystemArgument1, SystemArgument2 - Supply a set of two arguments that
            contain untyped data provided by the executive.
     
        Increment - Supplies the priority increment that is to be applied if
            queuing the APC causes a thread wait to be satisfied.
     
    Return Value:
     
        If the APC object is already in an APC queue or APC queuing is disabled,
        then a value of FALSE is returned. Otherwise a value of TRUE is returned.
     
    --*/
    

    首先根據之前傳入的Enviroment來判斷要取哪個_KPAC_STATE成員

    選擇完_KPAC_STATE后,判斷ApcModeNormalRoutine決定插入到哪個鏈表中。經分析得知,用戶APC回調存在NormalRoutine中,但KernelRoutine會存一個名為PsExitSpecialApc的特殊APC回調(用于釋放當前APC內存空間)。

    如果當前APC插入到了備用APC隊列(SavedApcState)中就返回。如果插入的是ApcState隊列中就繼續判斷這個APC是自身插入還是其他線程插入的

    如果是插入到其他線程的APC并且是個用戶APC

    如果這個APC是內核APC并且是插入到其他線程的

    APC的插入位置與傳入的Enviroment函數相關。如果是插入到了備用APC隊列中則執行返回。若是普通APC隊列中則繼續進行多個判斷。

    • 當這個APC是自身線程插入給自身的,并且是個特殊內核APC,則會立馬觸發軟中斷執行。
    • 如果這個APC是當前線程插入給其他線程的,且是個用戶APC。當APC所屬線程處于等待時,會嘗試喚醒線程來執行APC。如果不是等待狀態,則UserOrNormalKernel默認為0,插入后不執行APC。
    • 如果這個APC是當前線程插入給其他線程的,且是個內核APC。當APC所屬線程處于運行時,會直接觸發軟中斷執行APC或通知其他核觸發軟中斷執行。當APC所屬線程處于等待時,會嘗試喚醒線程來執行APC。其他狀態則不會立馬執行APC。
    線程apc
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    APC機制初探
    2022-05-31 16:10:02
    本質 線程是不能被“殺掉”、“掛起”、“恢復”的,線程在執行的時候自己占據著CPU,別人怎么可能控制它呢?舉個極端的例子:如果不調用API,屏蔽中斷,并保證代碼不出現異常,線程將永久占用CPU,何談控制呢?所以說線程如果想“死”,一定是自己執行代碼把自己殺死,不存在“他殺”這種情況!那如果想改變一個線程的行為該怎么辦呢?可以給他提供一個函數,讓它自己去調用,這個函數就是APC,即異步過程調用。
    線程從等待狀態蘇醒后,會自動檢測自己得APC隊列中是否存在APC過程。所以只需要將目標進程的線程APC隊列里面添加APC過程,當然為了提高命中率可以向進程的所有線程中添加APC過程。然后促使線程從休眠中恢復就可以實現APC注入。往線程APC隊列添加APC,系統會產生一個軟中斷。第二個參數表示插入APC線程句柄,要求線程句柄必須包含THREAD_SET_CONTEXT訪問權限。第三個參數表示傳遞給執行函數的參數。如果直接傳入shellcode不設置第三個函數,可以直接執行shellcode。
    在Windows大部分應用都是基于消息機制,他們都擁有一個消息過程函數,根據不同消息完成不同功能,windows通過鉤子機制來截獲和監視系統中的這些消息。一般鉤子分局部鉤子與全局鉤子,局部鉤子一般用于某個線程,而全局鉤子一般通過dll文件實現相應的鉤子函數。
    全局鉤子注入在Windows大部分應用都是基于消息機制,他們都擁有一個消息過程函數,根據不同消息完成不同功能,windows通過鉤子機制來截獲和監視系統中的這些消息。一般鉤子分局部鉤子與全局鉤子,局部鉤子一般用于某個線程,而全局鉤子一般通過dll文件實現相應的鉤子函數。
    之后想到了更完美的辦法
    Dll注入
    2021-11-08 14:57:41
    最近太忙啦XDM,又在做一些列的分析復現工作量有點大,更新要慢一點了。一致,也不會覆蓋其他的進程信息。
    Windows注入的一些方式
    BEServer - BattlEye服務器,收集上傳的信息,并判定作弊行為。本次分析的是BEDaisy,也就是BE內核驅動中的各種檢測。上傳的內容主要是一些黑名單特征,這些特征應該是從服務器下發的,因此可以在不重新編譯驅動的情況下,動態調整檢測的特征。沒有給定偏移量的特征,BE在檢測時會按子串匹配的方式嘗試所有位置進行匹配。
    shellcode loader的編寫
    2023-04-17 11:15:39
    改變加載方式指針執行#include?參數1:分配的內存的起始地址,如果為NULL則由系統決定。參數2:分配的內存大小,以字節為單位。參數3:分配的內存類型,MEM_COMMIT表示將分配的內存立即提交給物理內存,MEM_RESERVE表示保留內存但不提交。參數4:分配的內存保護屬性,PAGE_READWRITE可讀可寫,PAGE_EXECUTE_READ可執行可讀。結構體的指針,用于指定新線程的安全屬性,NULL表示默認安全屬性
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类