Windows 内核是否可以避免在用户模式下发生的损坏

Is Windows Kernel safe from corruptions that could happen in User mode

我目前正在使用 liveKD 执行内核调试。

在我发生阻塞的所有情况下(一个 ::CloseHandle() 函数调用,从来没有 returns )我碰巧有一个堆栈跟踪,它在 synchronisationEvent 上的内核中阻塞。

但是当我做 !object 12345678 如果 123456789 是我的 synchronisationEvent 正如进程的线程信息中所报告的那样,它说 Not a valid object (ObjectType invalid).

我担心在用户模式下我们应用程序级别的损坏是否会损坏内核? windows 是否保证像内存空间分离这样的事情可以防止类似的事情发生?

应用程序的代码大量使用了C++、COM/DCOM和Win32。


一条评论要求提供有关堆栈跟踪和句柄类型的更多信息。 在这种情况下,它与串行 com 端口有关。但我想我也有它用于文件句柄(尚未调试这些情况) 这是我拥有的那种堆栈跟踪:

        THREAD 856a2d48  Cid 0660.0350  Teb: 7ff25000 Win32Thread: ffaaedd8 WAIT: (Executive) KernelMode Non-Alertable
            860c6f9c  SynchronizationEvent
        IRP List:
            babea5d8: (0006,01d8) Flags: 00000404  Mdl: 00000000
        Not impersonating
        DeviceMap                 89809fc8
        Owning Process            86212d40       Image:         DataCaptorIS.exe
        Attached Process          N/A            Image:         N/A
        Wait Start TickCount      27315407       Ticks: 6067021 (1:02:17:26.134)
        Context Switch Count      2259           IdealProcessor: 0
        UserTime                  00:00:04.976
        KernelTime                00:00:02.184
        Win32 Start Address 0x775c03e9
        Stack Init 8aa0dfd0 Current 8aa0da98 Base 8aa0e000 Limit 8aa0b000 Call 0
        Priority 9 BasePriority 8 UnusualBoost 0 ForegroundBoost 0 IoPriority 2 PagePriority 5
        ChildEBP RetAddr  Args to Child
        8aa0dab0 824bfced 856a2d48 00000000 8ab00120 nt!KiSwapContext+0x26 (FPO: [Uses EBP] [0,0,4])
        8aa0dae8 824beb4b 856a2e08 856a2d48 860c6f9c nt!KiSwapThread+0x266
        8aa0db10 824b856f 856a2d48 856a2e08 00000000 nt!KiCommitThreadWait+0x1df
        8aa0db88 914539fb 860c6f9c 00000000 00000000 nt!KeWaitForSingleObject+0x393
        8aa0dbcc 82478c1e 860c6f98 babea5d8 babea73c serial!SerialClose+0x332 (FPO: [Non-Fpo])
        8aa0dbe4 886206b9 babea5d8 861986d0 bc859308 nt!IofCallDriver+0x63
        8aa0dc08 82478c1e 861986d0 860c6890 00000800 serenum!Serenum_CreateClose+0x77 (FPO: [Non-Fpo])
        8aa0dc20 82673be6 84aa7a00 bc8592f0 00000000 nt!IofCallDriver+0x63
        8aa0dc64 826647c9 bc859308 bc859308 bc8592f0 nt!IopDeleteFile+0x10c
        8aa0dc7c 824ba1e0 00000000 856a2d48 bc8592f0 nt!ObpRemoveObjectRoutine+0x59
        8aa0dc90 824ba150 bc859308 82687556 960a5578 nt!ObfDereferenceObjectWithTag+0x88 (FPO: [0,0,3])
        8aa0dc98 82687556 960a5578 856a2d48 00000c80 nt!ObfDereferenceObject+0xd (FPO: [0,1,0])
        8aa0dcdc 8268727c 960a5578 a7d21900 86212d40 nt!ObpCloseHandleTableEntry+0x21d
        8aa0dd0c 82687616 86212d40 856a2d01 0763f3b4 nt!ObpCloseHandle+0x7f
        8aa0dd28 8247f8a6 00000c80 0763f3b8 775d7094 nt!NtClose+0x4e
        8aa0dd28 775d7094 00000c80 0763f3b8 775d7094 nt!KiSystemServicePostCall (FPO: [0,3] TrapFrame @ 8aa0dd34)
WARNING: Frame IP not in any known module. Following frames may be wrong.
        0763f3b8 00000000 00000000 00000000 00000000 0x775d7094

此堆栈跟踪表明线程正在等待 synchronisationEvent 860c6f9c。

命令kd> !object 860c6f9creturnsNot a valid object (ObjectType invalid)。 我不知道这是否意味着内核中的 synchronisationEvent 已损坏。当我将命令应用于进程的其他 synchronisationEvent 时,我得到的输出类似于:

0: kd> !object 95369c68
Object: 95369c68  Type: (84aa6378) Event
    ObjectHeader: 95369c50 (new version)
    HandleCount: 1  PointerCount: 2

在应用程序级别,在用户模式下,这种情况发生在本应取消并清除任何 IRP 的代码之后:

::CancelIoEx(m_handle_to_serial_port_com);
WaitForRequestToComplete(); // our function calls ::GetOverlappedResult(..., bWait) for any OVERLAPPED that was pending, with bWait == TRUE

::PurgeComm(m_handle_to_serial_port_com);
WaitForRequestToComplete(); // our function calls ::GetOverlappedResult(..., bWait) for any OVERLAPPED that was pending, with bWait == TRUE

::CloseHandle(m_handle_to_serial_port_com); // the closeHandle which never returns

这些问题确实是随机出现的。有时需要几天才能重现。


显示 synchronisationEvent object 地址 86184f9c 的内存(在另一台有相同错误的机器上):

0: kd> dp 86184f9c - @@c++(sizeof(nt!_object_header) - #RTL_FIELD_SIZE(nt!_object_header, Body)) 
86184f84  00000006 00000000 00000000 00000000
86184f94  00000000 00000001 00040001 00000000
86184fa4  85917420 85917420 00000000 00000000
86184fb4  00000000 00000000 00000000 0000000d
86184fc4  86184890 00000040 00000000 00000800
86184fd4  00000000 85747a68 00000000 00000000
86184fe4  00000000 00000000 86184fec 86184fec
86184ff4  86184ff4 86184ff4 96d4c000 040d0000

并尝试显示 object header:

0: kd> dt nt!_object_header 86184f9c - @@c++(sizeof(nt!_object_header) - #RTL_FIELD_SIZE(nt!_object_header, Body))
Cannot find specified field members.

这绝对是应该的。如果内核在用户模式下是可破坏的,那么你就会有一个安全漏洞,可能会影响机器上的所有用户。您可以通过使内核崩溃来拒绝服务。您可以利用内核缓冲区溢出来提升权限。您可以利用内核信息泄露漏洞窃取他人的数据。

仅仅因为你向内核提供了错误的数据并不意味着它有这样的漏洞。内核可能足够聪明,可以检测和防止此类问题,或者它可以执行任何可能遭受用户空间中损坏的用户空间输入的代码。

如果您确实发现了导致内核崩溃、读取传递给内核的其他人的数据或类似问题的错误,则应将其报告给 Microsoft。如果您怀疑自己找到了什么,请尝试联系 MS 支持,看看他们是否可以提供帮助。他们是 OS 方面的专家,最有可能确定您怀疑的缺陷是否是真正的缺陷。

假设 ring 0 中的内核 运行 不能从 ring 3 中的进程 运行 更改。Windows 依靠微处理器的硬件支持来实现内存和 I/O 隔离,因此任何进程都不能访问内核 space 中的内存,也不能访问其他用户内存中的内存 space.

访问内核的唯一方法是通过系统调用。 Windows 中的系统调用采用 API 的形式,称为 "native API"。一个例子是 NtCreateFile,它是代表调用 CreateFile 函数而调用的函数。 NtCreateFile 必须检查所有参数的有效性。这个非常相同的功能可以从内核本身作为 ZwCreateFile 访问。当从内核调用时,它不进行检查,因为内核信任在内核模式下运行的任何代码。

CloseHandle() 只会在句柄的所有未决 IO 完成后完成。根据您发布的线程信息,该线程至少有一个挂起的 IRP - 我会看看它是否与您试图关闭的同一对象有关。

FWIW,I/O Cancellation is not always supported - 您的串行设备支持取消吗?

这只是关于为什么 !object 在给定地址上不起作用的跟进。

TL;DR:您的 SynchronizationEvent 没有损坏。传递给 !object 命令的地址不是内核 object,它只是一个内核结构。

kd !object 命令只是寻找一个特殊的结构(即 nt!_OBJECT_HEADER),它被添加到所有内核 object 中。更准确地说,只要内核结构前面有一个 nt!_OBJECT_HEADER,它就会变成一个 object。一旦一个结构被这个 _OBJECT_HEADER 前缀,它就变成了一个内核 object 然后由内核 object 管理器处理(特别是所有那些以 Ob 开头的内核函数前缀,但内核中object管理还涉及其他函数。

如果内核想要创建一个事件,但特别是如果此 object 不必跨越 user-land / kernel-land 边界(或者如果不需要引用计数)那么内核可能只是创建一个没有 nt!_OBJECT_HEADER.

nt!_KEVENT 结构

检查地址是否是内核object(或不是)

查看您的堆栈跟踪,我们有这两行:

    8aa0db88 914539fb 860c6f9c 00000000 00000000 nt!KeWaitForSingleObject+0x393
    8aa0dbcc 82478c1e 860c6f98 babea5d8 babea73c serial!SerialClose+0x332 (FPO: [Non-Fpo])

幸好serial.sys是微软的driver,所以我们有符号信息。查看 serial!SerialClose 大约偏移量 0x332 内的代码,我们有以下代码:

PAGESER:0001EEFC                 lea     eax, [esi+654h]
PAGESER:0001EF02                 push    ebx             ; Timeout
PAGESER:0001EF03                 push    ebx             ; Alertable
PAGESER:0001EF04                 push    ebx             ; WaitMode
PAGESER:0001EF05                 push    ebx             ; WaitReason
PAGESER:0001EF06                 push    eax             ; Object
PAGESER:0001EF07                 call    ds:__imp__KeWaitForSingleObject@20 ; KeWaitForSingleObject(x,x,x,x,x)

代码等待的事件(KEVENT类型)来自[esi+0x654]... 在函数开始时回溯,我们有:

PAGESER:0001EBD5                 mov     esi, [ebp+DeviceObject]
PAGESER:0001EBD8                 push    edi
PAGESER:0001EBD9                 mov     esi, [esi+_DEVICE_OBJECT.DeviceExtension]

因此 esi(在 [esi+0x654] 中)是设备 object 的设备扩展名。

在整个序列 driver 代码中搜索此偏移量 returns 几次。事件初始化在serial!SerialCreateDevObj:

中完成
PAGESRP0:000194AF                 push    esi             ; State
PAGESRP0:000194B0                 push    1               ; Type
PAGESRP0:000194B2                 lea     eax, [ebx+654h]
PAGESRP0:000194B8                 push    eax             ; Event
PAGESRP0:000194B9                 call    edi ; KeInitializeEvent(x,x,x) ; KeInitializeEvent(x,x,x)

这告诉我们 KEVENT 是内核结构,而不是内核 object,因为它使用 KeInitializeEvent.

关于 KEVENT 的更多信息

假设我有一个带有 SynchronizationEvent 的线程:

kd> !thread ffffe001ef7a5400
THREAD ffffe001ef7a5400  Cid 0538.054c  Teb: 00007ff7a9869000 Win32Thread: fffff901406825d0 WAIT: (Executive) KernelMode Non-Alertable
    ffffd0006dba6278  SynchronizationEvent
...

内核(和 kd)知道线程正在等待,因为这个线程有一个 waitblocklist 并且它不是空的:

kd> dt _kthread ffffe001ef7a5400 waitblocklist
ntdll!_KTHREAD
   +0x0d0 WaitBlockList : 0xffffe001`ef7a5540 _KWAIT_BLOCK

等待时间为 non-alertable 因为 alertable 字段为 0:

kd> dt _kthread ffffe001ef7a5400 alertable
ntdll!_KTHREAD
   +0x074 Alertable : 0y0

是内核模式(!=用户模式)等待,因为线程waitmode是0:

kd> dt _kthread ffffe001ef7a5400 waitmode
ntdll!_KTHREAD
   +0x187 WaitMode : 0 ''

线程WaitBlockList是一个_KWAIT_BLOCK类型的结构:

kd> dt _kwait_block 0xffffe001`ef7a5540
ntdll!_KWAIT_BLOCK
   +0x000 WaitListEntry    : _LIST_ENTRY [ 0xffffd000`6dba6280 - 0xffffd000`6dba6280 ]
   +0x010 WaitType         : 0x1 ''
   +0x011 BlockState       : 0x4 ''
   +0x012 WaitKey          : 0
   +0x014 SpareLong        : 0n1089
   +0x018 Thread           : 0xffffe001`ef7a5400 _KTHREAD
   +0x018 NotificationQueue : 0xffffe001`ef7a5400 _KQUEUE
   +0x020 Object           : 0xffffd000`6dba6278 Void
   +0x028 SparePtr         : (null)

如果你看上面的 _KWAIT_BLOCK,你可以看到有一个 Object 字段指示线程正在等待的 object。

我们知道这是一个事件,但所有可调度的 object 都有一个调度 header,因此我们可以 dt Object 指针借助 nt!_DISPATCHER_HEADER结构。

KEVENTs 可以作为由 KeInitializeEvent() 初始化的独立数据结构存在,也可以作为使用 NtCreateEvent() 创建的内核(事件)objects 存在:如果事件初始化为KeInitializeEvent()那么它不是内核object,而如果用NtCreateEvent()初始化,那么它是内核object。

nt!_KEVENT 结构只是 nt!_DISPATCHER_HEADER 结构的包装器。

0: kd> dt _KEVENT
nt!_KEVENT
   +0x000 Header           : _DISPATCHER_HEADER 

kd> dt _dispatcher_header 0xffffd000`6dba6278
ntdll!_DISPATCHER_HEADER
   +0x000 Lock             : 0n1594228737
   +0x000 LockNV           : 0n1594228737
   +0x000 Type             : 0x1 ''
   +0x001 Signalling       : 0 ''
   +0x002 Size             : 0x6 ''
   +0x003 Reserved1        : 0x5f '_'
   +0x000 TimerType        : 0x1 ''
   +0x001 TimerControlFlags : 0 ''
   +0x001 Absolute         : 0y0
   +0x001 Wake             : 0y0
   +0x001 EncodedTolerableDelay : 0y000000 (0)
   +0x002 Hand             : 0x6 ''
   +0x003 TimerMiscFlags   : 0x5f '_'
   +0x003 Index            : 0y011111 (0x1f)
   +0x003 Inserted         : 0y1
   +0x003 Expired          : 0y0
   +0x000 Timer2Type       : 0x1 ''
   +0x001 Timer2Flags      : 0 ''
   +0x001 Timer2Inserted   : 0y0
   +0x001 Timer2Expiring   : 0y0
   +0x001 Timer2CancelPending : 0y0
   +0x001 Timer2SetPending : 0y0
   +0x001 Timer2Running    : 0y0
   +0x001 Timer2Disabled   : 0y0
   +0x001 Timer2ReservedFlags : 0y00
   +0x002 Timer2Reserved1  : 0x6 ''
   +0x003 Timer2Reserved2  : 0x5f '_'
   +0x000 QueueType        : 0x1 ''
   +0x001 QueueControlFlags : 0 ''
   +0x001 Abandoned        : 0y0
   +0x001 DisableIncrement : 0y0
   +0x001 QueueReservedControlFlags : 0y000000 (0)
   +0x002 QueueSize        : 0x6 ''
   +0x003 QueueReserved    : 0x5f '_'
   +0x000 ThreadType       : 0x1 ''
   +0x001 ThreadReserved   : 0 ''
   +0x002 ThreadControlFlags : 0x6 ''
   +0x002 CycleProfiling   : 0y0
   +0x002 CounterProfiling : 0y1
   +0x002 GroupScheduling  : 0y1
   +0x002 AffinitySet      : 0y0
   +0x002 ThreadReservedControlFlags : 0y0000
   +0x003 DebugActive      : 0x5f '_'
   +0x003 ActiveDR7        : 0y1
   +0x003 Instrumented     : 0y1
   +0x003 Minimal          : 0y1
   +0x003 Reserved4        : 0y011
   +0x003 UmsScheduled     : 0y1
   +0x003 UmsPrimary       : 0y0
   +0x000 MutantType       : 0x1 ''
   +0x001 MutantSize       : 0 ''
   +0x002 DpcActive        : 0x6 ''
   +0x003 MutantReserved   : 0x5f '_'
   +0x004 SignalState      : 0n0
   +0x008 WaitListHead     : _LIST_ENTRY [ 0xffffe001`ef7a5540 - 0xffffe001`ef7a5540 ]

只要给定一个内核事件,您就可以知道哪些线程正在等待事件变为信号状态(事件正在设置),但最大的问题是您不知道应该由谁来设置这个特定事件.

在您的情况下,只有三个函数将设置序列 driver 中的事件:serial!SerialSetPendingDpcEventserial!SerialDpcEpilogueserial!SerialInsertQueueDpc。获得这些功能肯定是另一个问题...