同步篇——事件等待与唤醒

写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

你如果是从中间插过来看的,请仔细阅读 羽夏看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的所有流程,而该流程讲解将会在总结与提升进行。

下一篇

  同步篇——内核对象

posted @ 2022-02-11 18:12  寂静的羽夏  阅读(673)  评论(2编辑  收藏  举报