同步篇——事件等待與喚醒

寂靜的羽夏發表於2022-02-11

寫在前面

  此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。

  看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。


? 華麗的分割線 ?


臨界區的實現

  實現臨界區的方式有很多,只要保證在多執行緒多核的情況只有一個執行緒進入在臨界區即可。如下是我實現臨界區的效果圖:

同步篇——事件等待與喚醒

  如下是我的實現程式碼:

#include "stdafx.h"
#include <stdlib.h>
#include <windows.h>

int flag=0;
int content=0;

void __declspec(naked) MyCriticalEnter()
{
    _asm
    {
startproc:
        mov eax, 1;
        lock xchg [flag],eax;
        test eax,eax;
        jz endproc;
    }
    Sleep(100);
    _asm
    {
        jmp startproc;
endproc:
        ret;
    }
}


void __declspec(naked) MyCriticalExit()
{
    _asm
    {
        mov eax,0;
        lock xchg [flag],eax;
        ret;
    }
}

DWORD WINAPI ThreadProc(LPWORD param)
{
    MyCriticalEnter();
    content++;
    printf("content : %d\n",content);
    MyCriticalExit();
    return 0;
}

int main(int argc, char* argv[])
{
    for (int i = 0 ;i<100;i++)
    {
        CloseHandle(CreateThread(NULL,NULL,(LPTHREAD_START_ROUTINE)ThreadProc,NULL,NULL,NULL));
    }

    system("pause");
    return 0;
}

高併發的 Hook

  首先我們需要一個實驗的小白鼠,之前我們學過執行緒切換,正好這個函式就是非常高併發的函式:SwapContext。我們實現的功能就是Hook它實現統計計數,當然你可以給其他的高併發函式掛鉤,實現其他的功能。
  我們給這樣的函式進行掛鉤的時候,會有一個併發的問題,稍有不慎就會導致藍屏。周所周知,每一行彙編程式碼都是由一些規則編制出的硬編碼,它們有長有短。我們給出如下情況:

0x463891 | 6A 00  | push 0 
0x463893 | 6A 00  | push 0 
0x463895 | 6A 00  | push 0 
0x463897 | 6A 00  | push 0 
0x463899 | 6A 00  | push 0 
0x46389B | 6A 00  | push 0 
0x46389D | 6A 00  | push 0 

  這是某一塊函式反彙編的引數結果,如果我必須Hook這裡,否則我就無法實現我的功能,我們常見的Hook無非是一個跳轉,但是如果是長跳轉會帶來問題,因為它的硬編碼是5個位元組,它已修改會影響3個2位元組的硬編碼彙編,這是十分不好的結果。不過我們的短跳是2個位元組,我們可以通過幾個短跳加長跳的方式解決。
  如果我們使用長跳進行Hook,這就會有個問題,因為對於32位的CPU一般的指令一條執行最多改4個位元組,也就是說,我們正好執行到我們修改的區域當中時,然後執行緒切換,你把它給改了,然後執行緒回去後的EIP還是指向那裡,導致出錯,所以直接Hook是不可以的。
  還有種情況,比如你修改跳轉的地址,你兩個位元組的改也會出現問題。比如我執行到你改Jmp跳轉地址的Hook,然而你只改了兩個位元組,還有兩個位元組沒改,而我已經執行完畢了,這就會導致Hook地址錯誤,也是不可以的。
  可以看出Hook是不能亂Hook的,有很多我們需要考慮的情況,雖然正好這麼巧的概率挺低的,但時間一長,執行緒一多,遲早會出問題的。
  下面我們來實現一下這個功能:

#include <ntifs.h>
#include <ntddk.h>

int HookAddr = 0;
unsigned int OldThread = 0;
char shellcode[8] = { 0xE9,0,0,0,0 ,0x9c,0x8b,0x0b };
char yshellcode[8] = { 0x26, 0xC6, 0x46, 0x2D,0x02,0x9c,0x8b,0x0b };

KDPC dpc = { 0 };
KTIMER timer = { 0 };
LARGE_INTEGER duringtime = { 0 };

VOID DPCRoutine(_In_ struct _KDPC* Dpc, _In_opt_ PVOID DeferredContext,
 _In_opt_ PVOID SystemArgument1, _In_opt_ PVOID SystemArgument2)
{
    if (OldThread)
        DbgPrint("Report Per 2s : Calls Old Thread %x \n", OldThread);
    OldThread = 0;
    KeSetTimer(&timer, duringtime, &dpc);
}

void __declspec(naked) HookSwapContext()
{
    __asm
    {
        mov byte ptr es : [esi + 2Dh] , 2;
        mov [OldThread], edi;
        mov eax, [HookAddr];
        add eax, 5;
        push eax;
        ret;
    }
}   

unsigned int  __declspec(naked) GetKernelBase()
{
    __asm
    {
        mov eax, fs: [34h] ;
        mov eax, [eax + 18h];
        mov eax, [eax];
        mov eax, [eax + 18h];
        ret;
    }
}

void __declspec(naked) HookProc()
{
    _asm
    {
        pushad;
        pushfd;
        xor edx, edx;
        lea esi, shellcode;
        mov ebx, [esi];
        mov ecx, [esi + 4];
        
        mov edi, [HookAddr];
        mov eax, [edi];
        mov edx, [edi + 4];
        lock cmpxchg8b qword ptr[edi];
        popfd;
        popad;
        ret;
    }
}

void __declspec(naked) UnHookProc()
{
    _asm
    {
        pushad;
        pushfd;
        xor edx, edx;
        lea esi, yshellcode;
        mov ebx, [esi];
        mov ecx, [esi + 4];

        mov edi, [HookAddr];
        mov eax, [edi];
        mov edx, [edi + 4];
        lock cmpxchg8b qword ptr[edi];
        popfd;
        popad;
        ret;
        
}

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
    UnHookProc();
    while (1)
    {
        KeDelayExecutionThread(KernelMode, TRUE, &  duringtime);
        if (!OldThread)
        {
            break;
        }
    }

    KeCancelTimer(&timer);
    DbgPrint("Unloaded Successfully!");
    return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    DriverObject->DriverUnload = UnloadDriver;
    
    unsigned int base = GetKernelBase();
    
    HookAddr = base + 0x6A8E2;
    
    //初始化 shellcode
    unsigned int jmpdes = (int)HookSwapContext -    HookAddr - 5;
    RtlCopyMemory(&shellcode[1], &jmpdes, 4);
    
    
    KeInitializeTimer(&timer);
    KeInitializeDpc(&dpc, DPCRoutine, NULL);
    duringtime.QuadPart = -20 * 1000 * 1000;
    
    HookProc();
    
    KeSetTimer(&timer, duringtime, &dpc);
    
    DbgPrint("Loaded Successfully!");
    
    return STATUS_SUCCESS;
}

  效果圖如下:

同步篇——事件等待與喚醒

  當然,這個程式碼並不完美,主要是在退出解除安裝驅動上。因為我要解除安裝Hook的話,必須讓所有的執行緒必須都從我的Hook流程處理中出來,否則我硬撤掉的話,就會導致藍屏。目前沒有很好的辦法來處理,就算使用引用計數的方式也不太行。

等待與喚醒概要

  我們之前講解了如何自己實現臨界區以及什麼是Windows自旋鎖,這兩種同步方案線上程無法進入臨界區時都會讓當前執行緒進入等待狀態:一種是通過Sleep函式實現的,一種是通過讓當前的CPU空轉實現的,但這兩種等待方式都有侷限性。
  如果通過臨界區進行,我們的執行緒通過Sleep函式進行等待,等待時間是不確定的。我們可以舉個例子,一個執行緒已經進入臨界區還未出來,另一個執行緒發現不行開始睡大覺,睡一會發現還不行,繼續睡,即使這次期間那個執行緒走了,必須等到設定的時間到了才能醒來進入臨界區,如果執行緒一多更沒有頭了。通過自旋鎖的方式,只有等待時間很短的情況下才有意義,否則對CPU資源是種浪費,而且自旋鎖只能在多核的環境下才有意義。所以有沒有更加合理的等待方式呢?只有在條件成熟的時候才將當前執行緒喚醒?
  在Windows中,一個執行緒可以通過等待一個或者多個可等待物件,從而進入等待狀態,另一個執行緒可以在某些時刻喚醒等待這些物件的其他執行緒。我們在3環程式設計的時候經常會用到WaitForSingleObject來等待核心物件,比如互斥體、事件、程式或者執行緒等。為什麼那些核心物件可以等待呢?我們之前提到過它們頭部都有一個結構體:

kd> dt _DISPATCHER_HEADER
ntdll!_DISPATCHER_HEADER
   +0x000 Type             : UChar
   +0x001 Absolute         : UChar
   +0x002 Size             : UChar
   +0x003 Inserted         : UChar
   +0x004 SignalState      : Int4B
   +0x008 WaitListHead     : _LIST_ENTRY

  上面的結構體的研究將是我們以後介紹的重點。可以被等待的核心物件有:KPROCESSKTHREADKTIMERKSEMAPHORE(訊號量)、KEVENTKMUTANT(互斥體)、FILE_OBJECT。但是最後一個比較特殊,它沒有直接包含上面所述的結構體,我們來看看它的結構:

kd> dt _FILE_OBJECT
ntdll!_FILE_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x004 DeviceObject     : Ptr32 _DEVICE_OBJECT
   +0x008 Vpb              : Ptr32 _VPB
   +0x00c FsContext        : Ptr32 Void
   +0x010 FsContext2       : Ptr32 Void
   +0x014 SectionObjectPointer : Ptr32 _SECTION_OBJECT_POINTERS
   +0x018 PrivateCacheMap  : Ptr32 Void
   +0x01c FinalStatus      : Int4B
   +0x020 RelatedFileObject : Ptr32 _FILE_OBJECT
   +0x024 LockOperation    : UChar
   +0x025 DeletePending    : UChar
   +0x026 ReadAccess       : UChar
   +0x027 WriteAccess      : UChar
   +0x028 DeleteAccess     : UChar
   +0x029 SharedRead       : UChar
   +0x02a SharedWrite      : UChar
   +0x02b SharedDelete     : UChar
   +0x02c Flags            : Uint4B
   +0x030 FileName         : _UNICODE_STRING
   +0x038 CurrentByteOffset : _LARGE_INTEGER
   +0x040 Waiters          : Uint4B
   +0x044 Busy             : Uint4B
   +0x048 LastLock         : Ptr32 Void
   +0x04c Lock             : _KEVENT
   +0x05c Event            : _KEVENT
   +0x06c CompletionContext : Ptr32 _IO_COMPLETION_CONTEXT

  為什麼檔案核心物件可以被等待,是因為它有LockEvent成員,這兩個都是事件,都是可被等待的物件,所以它可以被等待。

等待鏈與等待網

  學習相關知識之前,我們得看一張圖:

同步篇——事件等待與喚醒

  如上就是一個執行緒等待一個核心物件的示意圖,為了更好的理解這東西,我們做一個實驗加以講解。如下是實驗程式碼:

DWORD WINAPI ThreadProc(LPVOID param)
{
    WaitForSingleObject((HANDLE)param,INFINITE);
    puts("等待執行緒執行完畢!");
    return 0;
}

int main(int argc, char* argv[])
{
    HANDLE hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
    HANDLE hthread = CreateThread(NULL,NULL,(LPTHREAD_START_ROUTINE)ThreadProc,hEvent,NULL,NULL);

    system("pause");
    CloseHandle(hthread);
    CloseHandle(hEvent);
    return 0;
}

  編譯執行之後,然後就是僅有顯示“按任意鍵繼續”的控制檯,這就是想要的效果,由於是無盡等待,所以會一直停在這裡。我們在WinDbg看看等待鏈結構也就是上面的結構:

kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
……

Failed to get VadRoot
PROCESS 89b4f020  SessionId: 0  Cid: 03fc    Peb: 7ffdf000  ParentCid: 06a0
    DirBase: 12dc0220  ObjectTable: e10641a0  HandleCount:  23.
    Image: WaitTest.exe
……

kd> !process 89b4f020
Failed to get VadRoot
PROCESS 89b4f020  SessionId: 0  Cid: 03fc    Peb: 7ffdf000  ParentCid: 06a0
    ……
        THREAD 89d9a8a8  Cid 03fc.04dc  Teb: 7ffde000 Win32Thread: 00000000 WAIT: (UserRequest) UserMode Non-Alertable
            89aeb488  ProcessObject
        Not impersonating
        DeviceMap                 e18d1c80
        Owning Process            00000000       Image:         <Invalid process>
        Attached Process          89b4f020       Image:         WaitTest.exe
        Wait Start TickCount      75638          Ticks: 9940 (0:00:01:39.543)
        Context Switch Count      59             IdealProcessor: 0             
        UserTime                  00:00:00.000
        KernelTime                00:00:00.000
        Win32 Start Address 0x00401380
        Stack Init b83a6000 Current b83a5ca0 Base b83a6000 Limit b83a3000 Call 00000000
        Priority 12 BasePriority 8 PriorityDecrement 2 IoPriority 0 PagePriority 0
        Kernel stack not resident.
        ChildEBP RetAddr      
        b83a5cb8 80501cd6     nt!KiSwapContext+0x2e (FPO: [Uses EBP] [0,0,4])
        b83a5cc4 804fad62     nt!KiSwapThread+0x46 (FPO: [0,0,0])
        b83a5cec 805b7126     nt!KeWaitForSingleObject+0x1c2 (FPO: [Non-Fpo])
        b83a5d50 8053e638     nt!NtWaitForSingleObject+0x9a (FPO: [Non-Fpo])
    <Intermediate frames may have been skipped due to lack of complete unwind>
        b83a5d50 7c92e4f4 (T) nt!KiFastCallEntry+0xf8 (FPO: [0,0] TrapFrame @ b83a5d64)
    <Intermediate frames may have been skipped due to lack of complete unwind>
        0012fdd8 00000000 (T) ntdll!KiFastSystemCallRet (FPO: [0,0,0])

        THREAD 89d14da8  Cid 03fc.068c  Teb: 7ffdd000 Win32Thread: 00000000 WAIT: (UserRequest) UserMode Non-Alertable
            89af1cb0  NotificationEvent
        Not impersonating
        DeviceMap                 e18d1c80
        Owning Process            00000000       Image:         <Invalid process>
        Attached Process          89b4f020       Image:         WaitTest.exe
        Wait Start TickCount      75638          Ticks: 9940 (0:00:01:39.543)
        Context Switch Count      10             IdealProcessor: 0             
        UserTime                  00:00:00.000
        KernelTime                00:00:00.000
        Win32 Start Address 0x00401005
        Stack Init b8226000 Current b8225ca0 Base b8226000 Limit b8223000 Call 00000000
        Priority 10 BasePriority 8 PriorityDecrement 2 IoPriority 0 PagePriority 0
        Kernel stack not resident.
        ChildEBP RetAddr      
        b8225cb8 80501cd6     nt!KiSwapContext+0x2e (FPO: [Uses EBP] [0,0,4])
        b8225cc4 804fad62     nt!KiSwapThread+0x46 (FPO: [0,0,0])
        b8225cec 805b7126     nt!KeWaitForSingleObject+0x1c2 (FPO: [Non-Fpo])
        b8225d50 8053e638     nt!NtWaitForSingleObject+0x9a (FPO: [Non-Fpo])
    <Intermediate frames may have been skipped due to lack of complete unwind>
        b8225d50 7c92e4f4 (T) nt!KiFastCallEntry+0xf8 (FPO: [0,0] TrapFrame @ b8225d64)
    <Intermediate frames may have been skipped due to lack of complete unwind>
        0052ff44 00000000 (T) ntdll!KiFastSystemCallRet (FPO: [0,0,0])

  由於篇幅限制,我只展示了部分,一般來說,最後一個就是我們想要找的執行緒,我們檢視一下:

kd> dt _KTHREAD 89d14da8
ntdll!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   ……
   +0x05c WaitBlockList    : 0x89d14e18 _KWAIT_BLOCK
   +0x060 WaitListEntry    : _LIST_ENTRY [ 0x0 - 0x80553d88 ]
   +0x060 SwapListEntry    : _SINGLE_LIST_ENTRY
   ……
kd> dx -id 0,0,805539a0 -r1 ((ntdll!_KWAIT_BLOCK *)0x89d14e18)
((ntdll!_KWAIT_BLOCK *)0x89d14e18)                 : 0x89d14e18 [Type: _KWAIT_BLOCK *]
    [+0x000] WaitListEntry    [Type: _LIST_ENTRY]
    [+0x008] Thread           : 0x89d14da8 [Type: _KTHREAD *]
    [+0x00c] Object           : 0x89af1cb0 [Type: void *]
    [+0x010] NextWaitBlock    : 0x89d14e18 [Type: _KWAIT_BLOCK *]
    [+0x014] WaitKey          : 0x0 [Type: unsigned short]
    [+0x016] WaitType         : 0x1 [Type: unsigned short]

  這就是我們要找的等待塊。由於執行緒只等待一個事件物件,所以只有一個可用的等待塊,這裡為什麼說是可用的。因為雖然有4個等待塊,最後一個也就是第四個等待塊已經被佔坑了,也就是我們呼叫WaitForSingleObject函式後的等待超時時間,如果是某一個值,第四個等待塊就會啟用,也就是計時器。我們來看看第四個等待塊:

kd> dt _KWAIT_BLOCK 0x89d14e18 + 18 * 3
ntdll!_KWAIT_BLOCK
   +0x000 WaitListEntry    : _LIST_ENTRY [ 0x89d14ea0 - 0x89d14ea0 ]
   +0x008 Thread           : 0x89d14da8 _KTHREAD
   +0x00c Object           : 0x89d14e98 Void
   +0x010 NextWaitBlock    : (null) 
   +0x014 WaitKey          : 0x102
   +0x016 WaitType         : 1

  我們再會過來看看第一個等待塊的內容。WaitKey是指等待塊的索引,但是對於第四個等待塊,它是特殊的,被賦予了比較大的值0x102Thread是指向的是當前執行緒,在這裡也就是被等待的執行緒。Object指的就是被等待的物件的地址,我們可以看一下我們等待的到底是不是事件:

kd> dt _OBJECT_HEADER 0x89af1cb0-18
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n2
   +0x004 HandleCount      : 0n1
   +0x004 NextToFree       : 0x00000001 Void
   +0x008 Type             : 0x89fabbf8 _OBJECT_TYPE
   +0x00c NameInfoOffset   : 0 ''
   +0x00d HandleInfoOffset : 0 ''
   +0x00e QuotaInfoOffset  : 0 ''
   +0x00f Flags            : 0 ''
   +0x010 ObjectCreateInfo : 0x89b32660 _OBJECT_CREATE_INFORMATION
   +0x010 QuotaBlockCharged : 0x89b32660 Void
   +0x014 SecurityDescriptor : (null) 
   +0x018 Body             : _QUAD
kd> dx -id 0,0,805539a0 -r1 ((ntkrnlpa!_OBJECT_TYPE *)0x89fabbf8)
((ntkrnlpa!_OBJECT_TYPE *)0x89fabbf8)                 : 0x89fabbf8 [Type: _OBJECT_TYPE *]
    [+0x000] Mutex            : Unowned Resource [Type: _ERESOURCE]
    [+0x038] TypeList         [Type: _LIST_ENTRY]
    [+0x040] Name             : "Event" [Type: _UNICODE_STRING]
    [+0x048] DefaultObject    : 0x0 [Type: void *]
    [+0x04c] Index            : 0x9 [Type: unsigned long]
    [+0x050] TotalNumberOfObjects : 0x631 [Type: unsigned long]
    [+0x054] TotalNumberOfHandles : 0x677 [Type: unsigned long]
    [+0x058] HighWaterNumberOfObjects : 0x684 [Type: unsigned long]
    [+0x05c] HighWaterNumberOfHandles : 0x6cd [Type: unsigned long]
    [+0x060] TypeInfo         [Type: _OBJECT_TYPE_INITIALIZER]
    [+0x0ac] Key              : 0x6e657645 [Type: unsigned long]
    [+0x0b0] ObjectLocks      [Type: _ERESOURCE [4]]

  通過我們的Name屬性我們就能判斷出就是事件核心物件了。
  繼續下面的講NextWaitBlock指向的就是下一個等待塊的地址,由於我們只等待第一個,所以就是指向自己的地址。WaitType指的是等待型別,如果執行緒等待多個核心物件,只要有一個等待物件符合條件就被啟用,那麼這個值就是1;如果必須全部的等待物件符合條件才啟用,該值就是0。
  對於WaitListEntry這個成員,我們需要看一下被等待物件的Head成員:

kd> dt _KEVENT 0x89af1cb0
ntdll!_KEVENT
   +0x000 Header           : _DISPATCHER_HEADER
kd> dx -id 0,0,805539a0 -r1 (*((ntdll!_DISPATCHER_HEADER *)0x89af1cb0))
(*((ntdll!_DISPATCHER_HEADER *)0x89af1cb0))                 [Type: _DISPATCHER_HEADER]
    [+0x000] Type             : 0x0 [Type: unsigned char]
    [+0x001] Absolute         : 0x1c [Type: unsigned char]
    [+0x002] Size             : 0x4 [Type: unsigned char]
    [+0x003] Inserted         : 0x89 [Type: unsigned char]
    [+0x004] SignalState      : 0 [Type: long]
    [+0x008] WaitListHead     [Type: _LIST_ENTRY]
kd> dx -id 0,0,805539a0 -r1 (*((ntdll!_LIST_ENTRY *)0x89af1cb8))
(*((ntdll!_LIST_ENTRY *)0x89af1cb8))                 [Type: _LIST_ENTRY]
    [+0x000] Flink            : 0x89d14e18 [Type: _LIST_ENTRY *]
    [+0x004] Blink            : 0x89d14e18 [Type: _LIST_ENTRY *]

  被等待物件的Header成員中的WaitListHead會把用來等待它的等待塊通過WaitListEntry串起來,這個就是該成員的作用。對於執行緒擁有的等待塊會通過KThread結構體的WaitListEntry成員串起來,這個圖的結構也就是這樣了。
  如果是一個執行緒等待兩個核心物件,它的結構示意圖如下:

同步篇——事件等待與喚醒

  實驗程式碼如下:

#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>

HANDLE hEvent[2];

DWORD WINAPI ThreadProc(LPVOID param)
{
    WaitForMultipleObjects(2,hEvent,TRUE,INFINITE);
    puts("等待執行緒執行完畢!");
    return 0;
}

int main(int argc, char* argv[])
{
    hEvent[0] = CreateEvent(NULL,TRUE,FALSE,NULL);
    hEvent[1]=CreateEvent(NULL,TRUE,FALSE,NULL);
    HANDLE hthread = CreateThread(NULL,NULL,(LPTHREAD_START_ROUTINE)ThreadProc,NULL,NULL,NULL);

    system("pause");
    CloseHandle(hthread);
    CloseHandle(hEvent[0]);
    CloseHandle(hEvent[1]);
    return 0;
}

  具體步驟我就不詳細展示了,我就給一下結果:

kd> dt _KTHREAD 89d14da8 
ntdll!_KTHREAD
   ……
   +0x05c WaitBlockList    : 0x89d14e18 _KWAIT_BLOCK
   +0x060 WaitListEntry    : _LIST_ENTRY [ 0x89b15e08 - 0x89d99908 ]
   ……
kd> dx -id 0,0,805539a0 -r1 (*((ntdll!_KWAIT_BLOCK (*)[4])0x89d14e18))
(*((ntdll!_KWAIT_BLOCK (*)[4])0x89d14e18))                 [Type: _KWAIT_BLOCK [4]]
    [0]              [Type: _KWAIT_BLOCK]
    [1]              [Type: _KWAIT_BLOCK]
    [2]              [Type: _KWAIT_BLOCK]
    [3]              [Type: _KWAIT_BLOCK]
kd> dx -id 0,0,805539a0 -r1 (*((ntdll!_KWAIT_BLOCK *)0x89d14e18))
(*((ntdll!_KWAIT_BLOCK *)0x89d14e18))                 [Type: _KWAIT_BLOCK]
    [+0x000] WaitListEntry    [Type: _LIST_ENTRY]
    [+0x008] Thread           : 0x89d14da8 [Type: _KTHREAD *]
    [+0x00c] Object           : 0x89a6ee58 [Type: void *]
    [+0x010] NextWaitBlock    : 0x89d14e30 [Type: _KWAIT_BLOCK *]
    [+0x014] WaitKey          : 0x0 [Type: unsigned short]
    [+0x016] WaitType         : 0x0 [Type: unsigned short]
kd> dx -id 0,0,805539a0 -r1 (*((ntdll!_KWAIT_BLOCK *)0x89d14e30))
(*((ntdll!_KWAIT_BLOCK *)0x89d14e30))                 [Type: _KWAIT_BLOCK]
    [+0x000] WaitListEntry    [Type: _LIST_ENTRY]
    [+0x008] Thread           : 0x89d14da8 [Type: _KTHREAD *]
    [+0x00c] Object           : 0x89d93598 [Type: void *]
    [+0x010] NextWaitBlock    : 0x89d14e18 [Type: _KWAIT_BLOCK *]
    [+0x014] WaitKey          : 0x1 [Type: unsigned short]
    [+0x016] WaitType         : 0x0 [Type: unsigned short]
kd> dx -id 0,0,805539a0 -r1 (*((ntdll!_KWAIT_BLOCK *)0x89d14e48))
(*((ntdll!_KWAIT_BLOCK *)0x89d14e48))                 [Type: _KWAIT_BLOCK]
    [+0x000] WaitListEntry    [Type: _LIST_ENTRY]
    [+0x008] Thread           : 0x89d14da8 [Type: _KTHREAD *]
    [+0x00c] Object           : 0x0 [Type: void *]
    [+0x010] NextWaitBlock    : 0x0 [Type: _KWAIT_BLOCK *]
    [+0x014] WaitKey          : 0x0 [Type: unsigned short]
    [+0x016] WaitType         : 0x0 [Type: unsigned short]
kd> dx -id 0,0,805539a0 -r1 (*((ntdll!_KWAIT_BLOCK *)0x89d14e60))
(*((ntdll!_KWAIT_BLOCK *)0x89d14e60))                 [Type: _KWAIT_BLOCK]
    [+0x000] WaitListEntry    [Type: _LIST_ENTRY]
    [+0x008] Thread           : 0x89d14da8 [Type: _KTHREAD *]
    [+0x00c] Object           : 0x89d14e98 [Type: void *]
    [+0x010] NextWaitBlock    : 0x0 [Type: _KWAIT_BLOCK *]
    [+0x014] WaitKey          : 0x102 [Type: unsigned short]
    [+0x016] WaitType         : 0x1 [Type: unsigned short]

  上面介紹的都是一個執行緒等待一個或者多個核心物件。如果一個一個執行緒交錯起來,有的被等待物件同時被多個執行緒等待,就會形成錯綜複雜的網路,也就是所謂的等待網:

同步篇——事件等待與喚醒

WaitForSingleObject 分析概要

  對於不同的可等待的核心物件,WaitForSingleObject的處理方式都是不同的。我們把大概的流程說一下,學完可以被等待的核心物件結構體之後,我們將詳細分析一下它的具體流程,將會在本篇章的總結與提升中進行講解。
  WaitForSingleObject經過分析呼叫流程如下:

graph TD WaitForSingleObject --> WaitForSingleObjectEx -.進入核心.-> NtWaitForSingleObject --> KeWaitForSingleObject

  我們先看一下NtWaitForSingleObject函式原型:

NTSTATUS __stdcall NtWaitForSingleObject(
    HANDLE Handle, 
    BOOLEAN Alertable, 
    PLARGE_INTEGER Timeout)

  其中,Handle是使用者層傳遞的等待物件的控制程式碼;Alertable對應的是KTHREAD結構體的Alertable屬性。如果為1,則在插入使用者APC時,該執行緒將被喚醒。注意這裡的喚醒只是喚醒該執行緒執行APC,而不是真正的喚醒。因為如果當前的執行緒在等待網上,執行完使用者APC後,仍然要進入等待狀態。
Timeout就是超時時間,就算沒等待到符合條件到了指定事件也會被喚醒。
  無論可等待物件是何種型別,執行緒都是通過WaitForSingleObject或者WaitForMultipleObjects進入等待狀態的,這兩個函式是理解執行緒等待與喚醒進位制的核心。在繼續深入之前我們講解一下DISPATCHER_HEADER這個結構:

kd> dt _DISPATCHER_HEADER
ntdll!_DISPATCHER_HEADER
   +0x000 Type             : UChar
   +0x001 Absolute         : UChar
   +0x002 Size             : UChar
   +0x003 Inserted         : UChar
   +0x004 SignalState      : Int4B
   +0x008 WaitListHead     : _LIST_ENTRY

  Type是型別,每一個可以被等待的物件的型別是不一樣的。具體值需要通過核心初始化程式碼可以逆向出。不同的型別,WaitForSingleObject處理方式是不一樣的。SignalState指示有沒有訊號,如果有訊號值大於0;WaitListHead我們前面講過就不贅述了。
  NtWaitForSingleObject的大體流程是呼叫ObReferenceObjectByHandle函式,通過物件控制程式碼找到等待物件結構體地址。然後呼叫KeWaitForSingleObject函式,進入關鍵迴圈。
  KeWaitForSingleObject開頭會做如下操作:

  1. KTHREAD(+70)位置的等待塊賦值。
  2. 如果超時時間不為0,KTHREAD(+70)第四個等待塊與第一個等待塊關聯起來。第一個等待塊指向第四個等待塊,第四個等待塊指向第一個等待塊。
  3. KTHREAD(+5C)指向第一個_KWAIT_BLOCK
  4. 進入關鍵迴圈

  這個關鍵迴圈就是處理等待的核心,在看該迴圈的流程之前我們要看一下這個圖:

同步篇——事件等待與喚醒

  這個比我們之前最後一個圖多了一個等待連結串列頭,至於為什麼看之後的講解。由於十分複雜我們用虛擬碼(非 IDA)來展示一下大體流程:

while(true)//每次執行緒被其他執行緒喚醒,都要進入這個迴圈
{
    if(/*符合啟用條件*/)//1、超時   2、等待物件SignalState>0 
        
        //1) 修改SignalState
        //2) 退出迴圈
    }
    else
    {
        if(/*第一次執行*/)
              //將當前執行緒的等待塊掛到等待物件的連結串列(WaitListHead)中;

        //將自己掛入等待佇列(KiWaitListHead)
        //切換執行緒...再次獲得CPU時,從這裡開始執行
    }
}
//1) 執行緒將自己+5C位置清0
//2) 釋放 _KWAIT_BLOCK 所佔記憶體

  看到“將自己掛入等待佇列”這行註釋了嗎?也就是說,如果呼叫KeWaitForSingleObject這個函式,執行緒就會把自己掛到等待連結串列中。
  不同的等待物件,用不同的方法來修改DISPATCHER_HEADER中的SignalState。比如:如果可等待物件是EVENT,其他執行緒通常使用SetEvent來設定SignalState = 1;並且,將正在等待該物件的其他執行緒喚醒,也就是從等待連結串列(KiWaitListHead)中摘出來。但是,SetEvent函式並不會將執行緒從等待網上摘下來,是否要下來,由當前執行緒自己來決定。
  我們考慮為什麼SetEvent不會把執行緒摘下來。如果執行緒不僅僅等待該物件,而且等待其他物件,其他物件不符合啟用條件,於是還得掛到等待連結串列上繼續等待。如果把它摘下來,不就出錯了?
  至此,本篇的內容就這麼多,如果要真正的學會,請自行逆向分析搞懂WaitForSingleObject的所有流程,而該流程講解將會在總結與提升進行。

下一篇

  同步篇——核心物件

相關文章