深入探索Windows用户/内核异常分发器机制

本文详细分析了Windows用户态与内核态异常分发机制,通过逆向工程探索ntdll!KiUserExceptionDispatcher的工作原理,并利用Detours库实现用户态异常监控工具,涵盖x86架构下的异常处理流程和实战代码示例。

深入探索Windows用户/内核异常分发器机制

引言

这篇短文旨在创建一段能够监控进程中引发异常的代码(类似于gynvael的ExcpHook,但在用户态),并生成包含异常相关信息的报告。另一个目的当然是深入了解内部机制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
--Exception detected--
ExceptionRecord: 0x0028fa2c Context: 0x0028fa7c
Image Path: D:\Codes\The Sentinel\tests\divzero.exe
Command Line: ..\tests\divzero.exe divzero.exe
PID: 0x00000aac
Exception Code: 0xc0000094 (EXCEPTION_INT_DIVIDE_BY_ZERO)
Exception Address: 0x00401359
EAX: 0x0000000a EDX: 0x00000000 ECX: 0x00000001 EBX: 0x7ffde000
ESI: 0x00000000 EDI: 0x00000000 ESP: 0x0028fee0 EBP: 0x0028ff18
EIP: 0x00401359
EFLAGS: 0x00010246

Stack:
0x767bc265 0x54f3620f 0xfffffffe 0x767a0f5a 
0x767ffc59 0x004018b0 0x0028ff90 0x00000000

Disassembly:
00401359 (04) f77c241c                 IDIV DWORD [ESP+0x1c]
0040135d (04) 89442404                 MOV [ESP+0x4], EAX
00401361 (07) c7042424304000           MOV DWORD [ESP], 0x403024
00401368 (05) e833080000               CALL 0x401ba0
0040136d (05) b800000000               MOV EAX, 0x0

因此,我将本文分为两个主要部分:

  • 第一部分将讨论理解底层工作原理所需的Windows内部背景知识,
  • 最后一部分将讨论Detours以及如何挂钩ntdll!KiUserExceptionDispatcher以实现我们的目的。基本上,该库为程序员提供了一组API来轻松挂钩过程。它还有清晰易读的文档,所以你应该使用它!它通常用于以下情况:
    • 热修补错误(无需重启),
    • 跟踪API调用(类似API Monitor),
    • 监控(有点像我们的示例),
    • 伪沙盒(阻止API调用),
    • 等等。

目录

  • 引言
  • 聚焦ntdll!KiUserExceptionDispatcher
  • nt!KiTrap*
  • 序列Detourer
  • 未讲述的故事:Win8和nt!KiFastFailDispatch
    • 引言
    • Win7上的情况
    • Win8上的情况
  • 结论

聚焦ntdll!KiUserExceptionDispatcher

本部分的目的是确保理解异常如何返回用户态,以便由SEH/UEF机制处理(或不处理);尽管我将专注于Windows 7 x86,因为那是我在VM中运行的操作系统。本部分的另一个目标是给你一个大致的了解,我的意思是我们不会深入太多细节,只够后来编写一个工作的异常哨兵PoC。

nt!KiTrap*

当你的用户态应用程序做错了什么时,CPU会引发异常:假设你试图除以零(nt!KiTrap00将处理这种情况),或者你试图获取一个不存在的内存页(nt!KiTrap0E)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kd> !idt -a

Dumping IDT: 80b95400

00:   8464d200 nt!KiTrap00
01:   8464d390 nt!KiTrap01
02:   Task Selector = 0x0058
03:   8464d800 nt!KiTrap03
04:   8464d988 nt!KiTrap04
05:   8464dae8 nt!KiTrap05
06:   8464dc5c nt!KiTrap06
07:   8464e258 nt!KiTrap07
08:   Task Selector = 0x0050
09:   8464e6b8 nt!KiTrap09
0a:   8464e7dc nt!KiTrap0A
0b:   8464e91c nt!KiTrap0B
0c:   8464eb7c nt!KiTrap0C
0d:   8464ee6c nt!KiTrap0D
0e:   8464f51c nt!KiTrap0E
0f:   8464f8d0 nt!KiTrap0F
10:   8464f9f4 nt!KiTrap10
11:   8464fb34 nt!KiTrap11
[...]

我相信你已经知道,但在x86 Intel处理器中,有一个称为IDT的表,存储了处理异常的不同例程。该表的虚拟地址存储在一个特殊的x86寄存器IDTR中,该寄存器只能通过使用指令sidt(存储中断描述符表寄存器)和lidt(加载中断描述符表寄存器)来访问。

基本上,IDT条目中有两个重要的事情:ISR的地址和CPU应使用的段选择器(记住它只是GDT中的一个简单索引)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
kd> !pcr
KPCR for Processor 0 at 84732c00:
    [...]
                    IDT: 80b95400
                    GDT: 80b95000

kd> dt nt!_KIDTENTRY 80b95400
    +0x000 Offset           : 0xd200
    +0x002 Selector         : 8
    +0x004 Access           : 0x8e00
    +0x006 ExtendedOffset   : 0x8464

kd> ln (0x8464 << 10) + (0xd200)
Exact matches:
    nt!KiTrap00 (<no parameter info>)

kd> !@display_gdt 80b95000

#################################
# Global Descriptor Table (GDT) #
#################################

Processor 00
Base : 80B95000    Limit : 03FF

Off.  Sel.  Type    Sel.:Base  Limit   Present  DPL  AVL  Informations
----  ----  ------  ---------  ------- -------  ---  ---  ------------
[...]
0008  0008  Code32  00000000  FFFFFFFF  YES     0    0    Execute/Read, accessed  (Ring 0)CS=0008
[...]

上面的条目告诉我们,对于处理器0,如果引发除零异常,内核模式例程nt!KiTrap00将被调用,使用平面模型code32 ring0段(参见GDT转储)。

一旦CPU进入nt!KiTrap00的代码,它基本上做了很多事情,所有其他nt!KiTrap例程也是如此,但不知何故,它们(或多或少)最终进入内核模式异常分发器:nt!KiDispatchException(记得gynvael的工具吗?他正在挂钩那个方法!)一旦它们创建了与故障关联的nt!_KTRAP_FRAME结构。

现在,你可能已经问过自己,内核如何回到用户态以便通过SEH机制处理异常?

这其实很简单。Windows内核使用的技巧是检查异常发生的位置:如果来自用户模式,内核模式异常分发器将设置陷阱帧结构(传入参数)的eip字段为符号nt!KeUserExceptionDispatcher。然后,nt!KeEloiHelper将使用相同的陷阱帧恢复执行(在我们的例子中是nt!KeUserExceptionDispatcher)。

但猜猜怎么着?该符号保存了ntdll!KiUserExceptionDispatcher的地址,所以这完全合理!

1
2
kd> dps nt!KeUserExceptionDispatcher L1
847a49a0  77476448 ntdll!KiUserExceptionDispatcher

如果像我一样你喜欢插图,我已经制作了一个WinDbg会话,我将展示我们刚刚讨论的内容。首先,让我们触发我们的除零异常:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
kd> bp nt!KiTrap00

kd> g
Breakpoint 0 hit
nt!KiTrap00:
8464c200 6a00            push    0

kd> k
ChildEBP RetAddr  
8ec9bd98 01141269 nt!KiTrap00
8ec9bd9c 00000000 divzero+0x1269

kd> u divzero+0x1269 l1
divzero+0x1269:
01141269 f7f0            div     eax,eax

现在让我们进一步进入ISR,更精确地说,当nt!_KTRAP_FRAME被构建时:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
kd> bp nt!KiTrap00+0x36

kd> g
Breakpoint 1 hit
nt!KiTrap00+0x36:
8464c236 8bec            mov     ebp,esp

kd> dt nt!_KTRAP_FRAME @esp
    +0x000 DbgEbp           : 0x1141267
    +0x004 DbgEip           : 0x1141267
    +0x008 DbgArgMark       : 0
    +0x00c DbgArgPointer    : 0
    +0x010 TempSegCs        : 0
    +0x012 Logging          : 0 ''
    +0x013 Reserved         : 0 ''
    +0x014 TempEsp          : 0
    +0x018 Dr0              : 0
    +0x01c Dr1              : 0
    +0x020 Dr2              : 0
    +0x024 Dr3              : 0x23
    +0x028 Dr6              : 0x23
    +0x02c Dr7              : 0x1141267
    +0x030 SegGs            : 0
    +0x034 SegEs            : 0x23
    +0x038 SegDs            : 0x23
    +0x03c Edx              : 0x1141267
    +0x040 Ecx              : 0
    +0x044 Eax              : 0
    +0x048 PreviousPreviousMode : 0
    +0x04c ExceptionList    : 0xffffffff _EXCEPTION_REGISTRATION_RECORD
    +0x050 SegFs            : 0x270030
    +0x054 Edi              : 0
    +0x058 Esi              : 0
    +0x05c Ebx              : 0x7ffd3000
    +0x060 Ebp              : 0x27fd58
    +0x064 ErrCode          : 0
    +0x068 Eip              : 0x1141269
    +0x06c SegCs            : 0x1b
    +0x070 EFlags           : 0x10246
    +0x074 HardwareEsp      : 0x27fd50
    +0x078 HardwareSegSs    : 0x23
    +0x07c V86Es            : 0
    +0x080 V86Ds            : 0
    +0x084 V86Fs            : 0
    +0x088 V86Gs            : 0

kd> .trap @esp
ErrCode = 00000000
eax=00000000 ebx=7ffd3000 ecx=00000000 edx=01141267 esi=00000000 edi=00000000
eip=01141269 esp=0027fd50 ebp=0027fd58 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0030  gs=0000             efl=00010246
divzero+0x1269:
001b:01141269 f7f0            div     eax,eax

kd> .trap
Resetting default scope

现在的想法是通过硬件断点跟踪nt!_KTRAP_FRAME.Eip字段的修改,正如我们之前讨论的(顺便说一句,不要尝试直接在VMware上对nt!KiDispatchException设置断点,它只会炸毁我的客户虚拟机):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
kd> ba w4 esp+68

kd> g
Breakpoint 2 hit
nt!KiDispatchException+0x3d6:
846c559e c745fcfeffffff  mov     dword ptr [ebp-4],0FFFFFFFEh

kd> dt nt!_KTRAP_FRAME Eip @esi
    +0x068 Eip : 0x77b36448

kd> ln 0x77b36448
Exact matches:
    ntdll!KiUserExceptionDispatcher (<no parameter info>)

好的,所以这里我们可以清楚地看到陷阱帧已被修改(请记住WinDbg在实际写入后给你控制权)。这基本上意味着当内核通过nt!KiExceptionExit(或nt!Kei386EoiHelper,两个符号对应同一个地址)恢复执行时,CPU将直接执行用户模式异常分发器。

很好,我认为我们现在有足够的理解来继续文章的第二部分。

序列Detourer

在这一部分,我们将讨论Detours,API的样子,以及如何使用它来构建一个用户态异常哨兵,而不需要太多代码行。以下是我们想要的功能列表:

  • 挂钩ntdll!KiUserExceptionDispatcher:我们将使用Detours来实现,
  • 生成一个小的可读异常报告:对于反汇编部分,我们将使用Distorm(另一个易于使用的酷库),
  • 专注于x86架构:因为不幸的是,express版本不适用于x86_64。

Detours将修改你想要挂钩的API的前几个字节,以便将其执行重定向到你的代码中:这称为内联挂钩。

Detours可以在两种模式下工作:

  • 第一种模式是你不动你将要挂钩的二进制文件,你将需要一个DLL模块,你将注入到你的二进制文件的内存中。然后,Detours将在内存中修改你将挂钩的API的代码。这就是我们将要使用的。
  • 第二种模式是你修改二进制文件本身,更精确地说是IAT。在这种模式下,你将不需要DLL注入器。如果你对这些技巧的细节感兴趣,它们在安装目录的Detours.chm文件中描述了,请阅读它!

所以我们的哨兵将分为两个主要部分:

  • 一个程序,将启动目标二进制文件并注入我们的DLL模块(所有重要的事情都在那里),
  • 哨兵DLL模块,将挂钩用户态异常分发器并写入异常报告。

第一个使用DetourCreateProcessWithDll非常容易实现:它将创建进程并注入我们想要的DLL。

用法:./ProcessSpawner <完整路径dll> <可执行文件路径> <可执行文件名> [参数..]

要成功挂钩一个函数,你当然必须知道它的地址,并且你必须实现挂钩函数。然后,你必须调用DetourTransactionBegin、DetourUpdateThread、DetourTransactionCommit,你就完成了,很棒不是吗?

在我们的情况下,唯一棘手的事情是我们想要挂钩ntdll!KiUserExceptionDispatcher,并且该函数有它自己的自定义调用约定。幸运的是,在Detours的示例目录中,你可以找到如何处理这种特定情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
VOID __declspec(naked) NTAPI KiUserExceptionDispatcher(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT Context)
{
    /* 取自Excep的detours示例 */
    __asm
    {
        xor     eax, eax                ; // 在堆栈上创建假返回地址。
        push    eax                     ; // (通常,我们由内核调用。)

        push    ebp                     ; // 序言
        mov     ebp, esp                ;
        sub     esp, __LOCAL_SIZE       ;
    }

    EnterCriticalSection(&critical_section);
    log_exception(ExceptionRecord, Context);
    LeaveCriticalSection(&critical_section);

    __asm
    {
        mov     ebx, ExceptionRecord    ;
        mov     ecx, Context            ;
        push    ecx                     ;
        push    ebx                     ;
        mov     eax, [TrueKiUserExceptionDispatcher];
        jmp     eax                     ;
        //
        // 上面的代码应该永远不会返回。
        //
        int     3                       ; // 中断!
        mov     esp, ebp                ; // 尾声
        pop     ebp                     ;
        ret                             ;
    }
}

以下是挂钩后ntdll!KiUserExceptionDispatcher在内存中的样子:

使用distorm_decode反汇编由CONTEXT.Eip字段指向的一些指令也非常简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if(IsBadReadPtr((const void*)Context->Eip, SIZE_BIGGEST_X86_INSTR * MAX_INSTRUCTIONS) == 0)
{
    _DecodeResult res;
    _OffsetType offset = Context->Eip;
    _DecodedInst decodedInstructions[MAX_INSTRUCTIONS] = {0};
    unsigned int decodedInstructionsCount = 0;

    res = distorm_decode(
        offset,
        (const unsigned char*)Context->Eip,
        MAX_INSTRUCTIONS * SIZE_BIGGEST_X86_INSTR,
        Decode32Bits,
        decodedInstructions,
        MAX_INSTRUCTIONS,
        &decodedInstructionsCount
    );

    if(res == DECRES_SUCCESS || res == DECRES_MEMORYERR)
    {
    fprintf(f, "\nDisassembly:\n");
    for(unsigned int i = 0; i < decodedInstructionsCount; ++i)
    {
        fprintf(
        f,
        "%.8I64x (%.2d) %-24s %s%s%s\n",
        decodedInstructions[i].offset,
        decodedInstructions[i].size,
        (char*)decodedInstructions[i].instructionHex.p,
        (char*)decodedInstructions[i].mnemonic.p,
       
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计