Windows内核是否安全,不会在用户模式下发生损坏

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

本文关键字:模式 用户 损坏 是否 内核 安全 Windows      更新时间:2023-10-16

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

在所有发生阻塞的情况下(一个永远不会返回的::CloseHandle()函数调用),我碰巧有一个堆栈跟踪,它在synchronisationEvent上的内核中进行阻塞。

但是,当我执行!object 12345678时,如果123456789是进程的线程信息中报告的synchronisationEvent,它会显示Not a valid object (ObjectType invalid).

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

应用程序代码集中使用C++、COM/DCOM和Win32。


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

THREAD 856a2d48 Cid 0660.0350 Teb:7ff25000 Win32线程:ffaaedd8 WAIT:(Executive)KernelMode不可报警860c6f9c同步事件IRP列表:babea5d8:(0006,01d8)标志:00000404 Mdl:000000000不模拟DeviceMap 89809fc8拥有进程86212d40图像:DataCaptorIS.exe附件进程不适用图像:不适用等待开始TickCount 27315407 Ticks:6067021(1:02:17:26.134)上下文切换计数2259 IdealProcessor:0用户时间00:00:976内核时间00:00:02.184Win32起始地址0x775c03e9堆栈初始化8aa0dfd0当前8aa0da98基本8aa0e000限制8aa0b000调用0优先级9基本优先级8异常提升0前景提升0 IoPriority 2页面优先级5ChildEBP RetAddr Args到Child8aa0dab0 824bfed 856a2d48 00000000 8ab00120 nt!KiSwapContext+0x26(FPO:[使用EBP][0,0,4])8aa0dae8 824beb4b 856a2e08 856a2d48 860c6f9c nt!KiSwapThread+0x2668aa0db10 824b856f 856a2d48 856a2e08 00000000 nt!KiCommitThreadWait+0x1df8aa0db88 914539fb 860c6f9c 0000000000000000nt!KeWaitForSingleObject+0x3938aa0dbcc 82478c1e 860c6f98 babea5d8 babea73c系列!串行关闭+0x332(FPO:[非FPO])在公元前859308年8月8日发生的一起事件中!IofCallDriver+0x638aa0dc08 82478c1e 861986d0 860c6890 00000800小夜曲!Serum_CreateClose+0x77(FPO:[非FPO])8aa0dc20 82673be6 84aa7a00 bc8592f0 00000000nt!IofCallDriver+0x638aa0dc64 826647c9 bc859308 bc859308bc8592f0 nt!IopDeleteFile+0x10c8aa0dc7c 824ba1e 00000000 856a2d48 bc8592f0 nt!ObpRemoveObjectRoute+0x598aa0dc90 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 a7d2900 86212d40 nt!ObpCloseHandleTableEntry+0x21d8aa0dd0c 82687616 86212d40 856a2d01 0763f3b4 nt!ObpCloseHandle+0x7f8aa0dd28 8247f8a6 00000c80 0763f3b8 775d7094 nt!NtClose+0x4e8aa0dd28 775d7094 00000c80 0763f3b8 775d7094nt!KiSystemServicePostCall(FPO:[0,3]TrapFrame@8aa0dd34)警告:机架IP不在任何已知模块中。以下框架可能是错误的。0763f3B8000000000000000000000000000000000x775d7094

此堆栈跟踪声明线程正在等待synchronisationEvent860c6f9c。

命令kd> !object 860c6f9c返回Not a valid object (ObjectType invalid)。我不知道这是否意味着synchronisationEvent在内核中已损坏。当我在进程的其他同步事件上应用该命令时,我会得到一个类似于的输出

0:kd>!对象95369c68对象:95369c68类型:(84aa6378)事件ObjectHeader:95369c50(新版本)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

这些问题真的是随机发生的。有时它需要几天才能再生。


显示同步事件对象的内存地址86184f9c(在另一台有相同错误的机器上):

0:kd>dp 86184f9c-@@c++(sizeof(nt!_object_header)-#RTL_FIELD_SIZE(nt!_object_headers,Body))86184f84 0000000 6 00000000 0000000086184f94 000000000000000 100040001 0000000086184fa4 85917420 85917420 000000000000000086184fb4 0000000000000000000000000000000天86184fc4 861848900000000004000000000080086184fd4 0000000085747a68 00000000000000000000000086184fe4 000000000000000086184fec86184fec86184ff4 86184ff6 86184ff8 96d4c000 040d0000

并尝试显示对象标题:

0:kd>dt-nt_object_header 86184f9c-@@c++(sizeof(nt!_object_header)-#RTL_FIELD_SIZE(nt!_object_headers,Body))找不到指定的字段成员

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

仅仅因为你给内核提供了坏数据并不意味着它有这样的漏洞。内核可能足够聪明,可以检测并防止此类问题,也可能执行任何可能因用户输入到用户中而损坏的代码。

如果你真的发现了一个错误,让你崩溃内核,读取别人传递给内核的数据,或者类似的东西,那么你应该向微软报告。如果您怀疑自己发现了什么,请尝试联系MS支持,看看他们是否能提供帮助。他们是操作系统的专家,最有可能确定您的可疑缺陷是否是真正的缺陷。

这只是!object无法处理给定地址的原因的后续说明。

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

kd!object命令只是查找一个为所有内核对象准备的特殊结构(即nt!_OBJECT_HEADER)。更确切地说,一个内核结构在其前面有一个nt!_OBJECT_HEADER时就变成了一个对象。一旦一个结构用这个_OBJECT_HEADER做了准备,它就变成了内核对象,然后由内核对象管理器处理(特别是所有以Ob前缀开头的内核函数,但内核中还有其他涉及对象管理的函数)。

如果内核想要创建一个事件,但特别是如果这个对象不必跨越用户陆地/内核陆地边界(或者如果不需要引用计数),那么内核可能只创建一个没有nt!_OBJECT_HEADERnt!_KEVENT结构。

检查地址是否是内核对象

看看你的堆栈轨迹,我们有这两行:

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

幸运的是,serial.sys是一个Microsoft驱动程序,所以我们有符号信息。看看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]中)是来自设备对象的设备扩展。

在整个串行驱动程序代码中搜索此偏移会返回一些实例。事件初始化在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是一个内核结构,而不是内核对象,因为它使用KeInitializeEvent

关于KEVENTs的更多信息

假设我有一个带有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

由于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字段,它指示线程正在等待的对象。

我们知道这是一个事件,但所有可调度的对象都有一个调度头,所以我们可以在nt!_DISPATCHER_HEADER结构的帮助下dtObject指针。

KEVENT可以作为由KeInitializeEvent()初始化的独立数据结构存在,也可以作为用NtCreateEvent()创建的内核(事件)对象存在:如果事件是用KeInitializeEvent()初始化的,则它不是内核对象,而如果用NtCreateEvent()初始化,则它是内核对象。

CCD_ 41结构只是围绕CCD_ 42结构的包装。

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 ]

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

在您的情况下,只有三个函数会在串行驱动程序中设置事件:serial!SerialSetPendingDpcEventserial!SerialDpcEpilogueserial!SerialInsertQueueDpc。使用这些函数肯定是另一个问题。。。

CloseHandle()只有在句柄的所有挂起IO完成后才会完成。根据你发布的线程信息,这个线程至少有一个挂起的IRP——我会看看它是否适用于你试图关闭的同一个对象。

FWIW,并不总是支持I/O取消-您的串行设备支持取消吗?

很遗憾,在环0中运行的内核不能从在环3中运行的进程更改。Windows中继微处理器的硬件支持,以实现内存和I/O隔离,因此无论是在内核空间还是在其他用户的内存空间,任何进程都无法访问内存。

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