1394 is back !

从XP时代开始就一直在用1394 Kernel Debugging,虽然Windows相继增加支持了USB2、USB3或者网络调试,但1394因其方便及更好的适用性一直是我的最爱,这也是为什么没有将我的工作笔记本升级到超级本的原因。

但从Windows 10发布以后,微软不再提供Kernel Debugging over 1394,但好在有相应的Workaround延长了1394的寿命,参见 KD 1394 Work-Around。但此办法无法解决boot debugging的问题,好在我目前都是系统级的内核调试,用不着boot debugging。

只是好景不长,随着Win10版本升级,1394又不行了。曾经为了测试新版Win10的兼容性,装了Windows 10 Pro Insider Preview 16226,结果发现Kernel Debugging over 1394彻底罢工,试过几个版本的Kd1394.dll均不行。

本以为1394就此死翘翘的时候,看到M$发布的一则消息:New WinDbg available in preview!。安装只能通过Windows Store,安装后习惯性的检查了一下文件目录,发现了新版的Kd1394.dll,随即在Win10 Pro 16226系统上做替换测试,然后 bing bing bing ! Windbg host端不断吐出字符来!

后来在Win10 Pro 15063上做了验证,发现此版Kd1394.dll并不适用,但老版的Kd1394.dll还是可以正常适配Win10 15063系统的。作为一个重度1394使用者,又可以欢呼了,但此好景能延续到什么时候呢?

Continue reading » · Rating: · Written on: 09-10-17 · 1 Comment »

Win10升级导致的休眠故障

一切都是因为手贱,看到Win10的升级引入了不少新的特性,心痒就赶了个时髦。升级过程花了不短的时间,但总体顺利,使用上也没有发现异常,也没有发现改进,比如Hyper-V照常和Vmware Workstation冲突。

下班后将电脑休眠从公司带回家,但等到再开机却发现系统重启了,日志中没有看到明显异常。之后就发现休眠时不时就会出问题,不是休眠中当机,就是休眠后无法正常开机。

用驱动大师升级了所有能升级的驱动,问但题还是没有解决,只得回退系统,但回退之后发现还是不能正常休眠。看来只能上Windbg来动动手术了:

开始调试时休眠正常:过程中曾中断系统枚举了所有的进程、IRP及ERESOURCE锁的,只发现dxgkrnl.sys驱动中有明显的锁等待,不少线程都因尝试获取一个已经Exclusively Owned ERESOURCE锁。dxgkrnl是DirectX显示驱动,与此相关的只有intel显卡,等系统恢复后卸载了intel显卡驱动,用Windows来自动安装。

重试几次后windbg端忽然显示出熟悉的输出:

*** Fatal System Error: 0x0000009f
           (0x0000000000000003,0xFFFF980487D7B060,0xFFFFD3810AFB68F0,0xFFFF980490101010)

果然有坑!分析下来发现是蓝牙驱动的问题:

2: kd> !devobj ffff980487d7b060; !irp  ffff980490101010 1
Device object (ffff980487d7b060) is for:
   USBPDO-8 \Driver\usbhub DriverObject ffff980487f6b300
   ...

2: kd> !devnode ffff98048901c010
DevNode 0xffff98048901c010 for PDO 0xffff980487d7b060
  Parent 0xffff980488186d30   Sibling 0xffff98048940f790   Child 0xffff98048929dd30   
  InstancePath is "USB\VID_0A5C&PID_21E6\7CE9D3E86CA0"
  ServiceName is "BTHUSB"
  ...

2: kd> !dh -f bthport
File Type: DLL
FILE HEADER VALUES
    8664 machine (X64)
       C number of sections
CED98754 time date stamp Wed Dec 20 22:34:12 2079
...

bthport的timestamp竟然是2079 !? 什么鬼!先不管这些,重启系统后将蓝牙禁掉后,再测试休眠,试了几次均正常。然后开启蓝牙,什么都不做再次尝试休眠,果然又中招。

问题解决起来倒也简单:将所有的蓝牙设备都删除,重新安装Thinkpad Bluetooth,之后又测试了几次休眠,再没有出问题,世界终于又大同了!

在检查系统加载驱动过程中还有个新发现,国内的公司还有各大银行简直是驱动开发专业户,每家都有几个驱动,就连迅雷,你个下载软件,竟然有两个内核驱动。不由分说,直接咔嚓了此类驱动。从此和谐社会!比新闻联播还鸡血!

Continue reading » · Rating: · Written on: 09-01-17 · 1 Comment »

解析TopLevelIrp

何为TopLevelIrp

TopLevelIrp是当前内核线程结构中的一个指针,可以是一个数值标识(ULONG_PTR),也可以是指向一个内存块的指针(内核IRP结构或用户自定义结构),其目的就是给文件系统驱动记录当前线程其内核栈最早传入且正在处理的I/O请求。因为处理一个I/O请求的过程中可能引入新的I/O,这样就会导致文件系统的重入(嵌套调用),重入问题很有可能会带来对资源依赖的死锁,所以文件系统驱动需要在重入的情况也能甄别出当前正处理的请求是不是最早的,即当前线程内核栈及资源的占用情况,从而采取不同的策略以避免死锁的发生。在时间顺序上最早的请求,从调用栈上来看就是处于最高地址(最高位置)的请求。

TopLevelIrp在内核线程结构中的定义如下:

typedef struct _ETHREAD {
    KTHREAD Tcb;
    ...
    //
    //  File Systems
    //

    ULONG_PTR TopLevelIrp;  // either NULL, an Irp or a flag defined in FsRtl.h
    struct _DEVICE_OBJECT *DeviceToVerify;
    ...
} ETHREAD, *PETHREAD;

TopLevelIrp及DevcieToVerify等域均是留给文件系统驱动专用的,可以作为一个当前线程的环境变量或各层调用栈间的共享变量。

系统操作函数

ETHREAD是内核私有结构,系统内核提供给外部组件(主要是文件系统驱动)两个接口:

函数 功能
VOID IoSetTopLevelIrp(IN PIRP Irp); 设置(改变)当前线程的TopLevelIrp值
PIRP IoGetTopLevelIrp(VOID); 读取当前线程的TopLevelIrp值

TopLevelIrp的操作过程

文件系统驱动与操作系统内核组件:Cache Manager(缓存子系统)、Virtual Memory Manager(虚拟内存管理子系统)及I/O Manager (I/O子系统)间交互相非常紧密且调用关系也非常复杂,调用嵌套及重入的情形非常多,分辨并正确处理不同的重入情景正是文件系统驱动不得不面对的复杂难题。

TopLevelIrp就是为了解决这个问题而设计的。其最直接的操纵及访问者也是文件系统驱动,因为用户层I/O请求最初的响应都是由I/O Manager直接传给文件系统的。文件系统驱动在响应过程中会调用操作系统的其它子系统,在调用前会针对TopLevelIrp做不同的设置。另外还有几种常见情况,如Cache Manager及Virtual Memory Manager会根据内存压力来发起I/O操作,所以内核子系统也需要修改、设置TopLevelIrp域以标识出本次I/O操作的发起者。

IoGetTopLevelIrp()返回值 (当前线程的TopLevelIrp域) 含义
FSRTL_CACHE_TOP_LEVEL_IRP 由缓存管理器触发的i/o请求:延迟写(Lazy-write)或预读(Readahead)
FSRTL_MOD_WRITE_TOP_LEVEL_IRP 由系统脏页回写线程触发的写请求(在MiMappedPageWriter回写映射文件的脏页时)
FSRTL_FAST_IO_TOP_LEVEL_IRP 由I/O Manager及文件系统自身处理Fast i/o请求时设置
FSRTL_FSP_TOP_LEVEL_IRP 通常由文件系统本身触发,用以标识文件系统驱动自身的i/o处理线程(Workitem线程池)。由于此情况下TopLevelIrp完全由文件系统自已管理,IRP或私有结构,通常由文件系统自已构造并使用

操作场景

  1. 用户态切换到内核态 这是最通常的情形,即请求直接来自于用户,用户请求经过syscall之后到达I/O Manager,而后I/O Manager会构造Irp并直接传给文件系统。 文件系统最通常的做法就是将当前的Irp设为TopLevelIrp。文件系统当然也可以用自己私有的结构,因为此线程在Irp没有完成之前将完全由文件系统所控制,TopLevelIrp也只有文件系统驱动会查询。
  2. Fast I/O 用户态的请求通过syscall到达I/O Manager后,I/O Manager发现文件已经被打开过且可以通过文件系统驱动所提供的callback直接来处理这个请求,不必从内存池中分配Irp等结构。 文件系统被调用后又会调用内核所提供的文件系统支持函数(FsRtl...),此操作将有可能涉及Cache Manager、VM,或重入文件系统驱动等,所以文件系统支持函数会设置TopLevelIrp为FSRTL_FAST_IO_TOP_LEVEL_IRP以标示出最初的请求响应是通过Fast I/O来做的。
  3. 文件系统自身的Workitem线程 类似第一种情形(用户态切换到内核态),Workitem线程本身是由文件系统发起并且是文件系统所私有的内核线程,但Workitem中的操作却是有可能重入文件系统的(这个由文件系统的设计决定)。 TopLevelIrp的值可以由文件系统驱动自行决定,如可以设为FSRTL_FSP_TOP_LEVEL_IRP,也可以设为此Workitem当前要处理的请求Irp,也可以设成是文件系统驱动的私有结构。
  4. 缓存管理器:延迟写 Cache Manager的Lazy-write操作(延迟写)是在Cache Manager私有的内核线程中执行的。在调用文件系统进行I/O操作之前会设置TopLevelIrp为FSRTL_CACHE_TOP_LEVEL_IRP,因为Lazy-write操作会通过Cache Manager callback来获取相应的锁资源,并且最终的I/O操作总要由文件系统来完成。
  5. 虚拟内存管理器:MiMappedPageWriter 内存映射文件的I/O不会经过Cache Manager,而是由Virtual Memory Manager直接处理的,并且是在Virtual Memory Manager私有的内核线程中做的,此线程的处理函数就是MiMappedPageWriter。同Cache Manager延迟写的流程类似,最终的I/O操作总要由文件系统来完成,所以移交控制权之前,VM会将当前线程的TopLevelIrp设置为FSRTL_MOD_WRITE_TOP_LEVEL_IRP,以通知文件系统驱动当前请求是由VM发起的。

不同文件系统驱动的处理差异

前面说过TopLevelIrp的读操作及检测只有文件系统驱支在做,虽然其设置可以是多方的。对于文件系统驱动来说,TopLevelIrp的具体值是由文件系统自己来解释的,所以针对 FSRTL_FSP_TOP_LEVEL_IRP,不同的文件驱动对TopLevelIrp的处理也不尽相同:

文件系统驱动 TopLevelIrp所指向的结构
FastFat nt!_IRP
Cdfs cdfs!_THREAD_CONTEXT
Ntfs ntfs!_TOP_LEVEL_CONTEXT

案例分析

TopLevelIrp的设置是一项精巧的手艺活,稍有不慎就会阴沟里翻船,下面就介绍两个开发中曾遇到的案例:

NTFS:Valid Data Length的设置

Ntfs文件系统驱动在其内部维护着每个数据流的Valid Data Length(VDL,有效数据长度),且VDL是管理于Ntfs的私有结构中的。VDL的更新一般是在cached i/o过程中做的。但实际操作中我们有个特殊的需求:要截获所有的cached i/o请求,只会通过paging i/o请求和Ntfs文件系统驱动交与,这样操作的后果就导致VDL不能被正常更新,虽然数据已经写到了磁盘上但是当试图读回数据的时候,Ntfs会根据VDL对数据进行清零化处理。

最后的解决办法也是一个不得已的投巧的办法:将我们的paging writing 请求模拟成mmap i/o (memory mapping i/o),即映射文件的数据刷写方式。mmap i/o操作本身就是个top level操作,并且也是仅有的top level paging i/o的情形。所以针对mmap i/o,Ntfs会更新VDL,这也是唯一的可以更新VDL的机会。

CDFS: 换了光盘但不能正确加载

这个问题比较妖怪,本来以为是文件系统过滤驱动的问题,但仔细分析过滤驱动又没有发现可疑点,最后经过代码对比及对WDK中cdfs的动态调试才定位到问题症结:原来是top level irp的问题。

我们都知道文件系统对于可移动设备及可移除介质设备都会有个检验(即verify)操作,但cdfs的verify操作会检测当前的TopLevelIrp状态,并根据这个状态来决定是否继续verify操作。当我们设置了TopLevelIrp之后,cdfs会认为它不在最高的调用栈上,故直接跳过了verify操作,最终就导致了换光盘后新光盘无法被正常识别的问题。

参考链接

  1. TopLevelIrp是如何设置的 - blog: Oops Kernel
  2. More on Maintaining Valid Data Length - OSR

<本文用时约5小时>

Continue reading » · Rating: · Written on: 02-28-17 · 1 Comment »

Windows内核开发陷阱 – NTSTATUS

STATUS_REPARSE不是“失败”

曾经在过滤驱动中想针对REPARSE的情况做特殊处理,实现代码如下:

if (NT_SUCCESS(status)) {
    /* file created */
} else if (STATUS_REPARSE == status) {
    /* to be repared  */
} else {
    ...
}

结果代码运行没达有预期效果,手工调试时才发现代码进入了NT_SUCCESS(status)分支。STATUS_REPARSE竟然表示是“成功”的!

这件事后才真正注意到status的问题,虽然在学习内核及驱动编程的时候书上都有相关介绍,但这些基础章节并没有引起重视就直接跳过了。但是像STATS_REPARSE这样的特例并不是少数,如STATUS_PENDING、STATUS_TIMEOUT等。

NTSTATUS的定义

再回顾一下NTSTATUS的定义: NTSTATUS是一个32位长的有符号整型数,即LONG型。其格式如下:

  3                   2                   1                   0
1 0|9|8|7 6 5 4 3 2 1 0 9 8 7 6|5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
---+-+-+-----------------------+-------------------------------
S  |C|N|    Facility           |               Code              
---------------------------------------------------------------

各部分的解释如下:

位数 注解
S 2 Severity: 分4个等级(Success, Informational, Warning, Error)
C 1 1:用户自定义的值 0:默认为系统定义(Microsoft)
N 1 保留位,始终为0值
Facility 12 错误类别或子系统的划分
Code 16 具体错误代码

Severity的4个等级的定义如下:

00 - Success
01 - Informational
10 - Warning
11 - Error

NT_XXXX宏分析

引用WDK代码(ntdef.h):

//
// Generic test for success on any status value (non-negative numbers
// indicate success).
//

#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)

//
// Generic test for information on any status value.
//

#define NT_INFORMATION(Status) ((((ULONG)(Status)) >> 30) == 1)

//
// Generic test for warning on any status value.
//

#define NT_WARNING(Status) ((((ULONG)(Status)) >> 30) == 2)

//
// Generic test for error on any status value.
//

#define NT_ERROR(Status) ((((ULONG)(Status)) >> 30) == 3)

由定义可以看出至少两点:

  1. NT_SUCCESS的定义和其它三者是有区别的,并不是根据Severity的值来判断的。NT_SUCCESS(status)其实包含了Severity == 0及Severity == 1的情形。

  2. NT_SUCCESS与NT_ERROR不是单纯的非黑即白的关系。新手常犯的错误就是主观上认为NT_SUCCESS()与!NT_ERROR(),或者NT_ERROR()与!NT_SUCCESS()是等价。

如果想确保一个irp确实是“成功”完成了,就要严格用STATUS_SUCCESS来判断,而不是NT_SUCCESS()。类似地,用NT_ERROR()可以保证代码的执行确实出现了错误,但是NT_WARNING()的情况即被漏掉了。

总之,必要的基本功始终是不可少的,像熟悉开发场景相关的返回值并弄懂这些返回值的确切含义等此类的苦活累活是必须要做且要做好的。

参考链接

  1. [MS-ERREF]: Windows Error Codes
  2. Windows Driver Kit (WDK)

<本文用时2小时,字体及表格调整花时较多。>

Continue reading » · Rating: · Written on: 01-19-17 · No Comments »

Windows内核开发陷阱: Nt vs Zw

在Windows平台上做过开发的人对Native API都不会陌生。但以Nt开头与以Zw开头的Native API到底有没有差别以及都有什么使用场景等一直以来都是让人感觉比较困扰的问题。另外内核态也有同样的一组Nt及Zw开头的内核支持函数,这两者间又是怎么一回事呢,这其中会有会有开发上有陷阱呢?!

我们先来解析用户层,然后研究下内核层其各自的实现:

用户层的Nt vs Zw

对用户层程序来说Nt与Zw开头函数实际上是一样的,只是同一个函数的两个名称而已。比如下面是对ntdll!NtCreateFile及ntdll!ZwCreateFile(Win7 X64)的反汇编:

1: kd> u ntdll!ZwCreateFile
ntdll!NtCreateFile:
00000000`77480400 4c8bd1          mov     r10,rcx
00000000`77480403 b852000000      mov     eax,52h
00000000`77480408 0f05            syscall
00000000`7748040a c3              ret

1: kd> u ntdll!ZwClose
ntdll!NtClose:
00000000`7747ffa0 4c8bd1          mov     r10,rcx
00000000`7747ffa3 b80c000000      mov     eax,0Ch
00000000`7747ffa8 0f05            syscall
00000000`7747ffaa c3              ret

syscall

用户层ntdll!ZwCreateFile或NtCreateFile会通过syscall切换到内核,最早的Windows系统是通过软中断0x2e来完成模式切换的。如何切换以及切换至何处是由CPU的MSR寄存器所决定的,由操作系统在启动时完成的初始化设定。

针对Win7 X64测试系统来说,此处的syscall指令将跳转由MSR_LSTAR所指向的固定地址:

0: kd> rdmsr 0xc0000082
msr[c0000082] = fffff800`03082ec0
0: kd> u fffff800`03082ec0
nt!KiSystemCall64:
fffff800`03082ec0 0f01f8          swapgs
fffff800`03082ec3 654889242...000 mov   qword ptr gs:[10h],rsp
fffff800`03082ecc 65488b242...000 mov   rsp,qword ptr  gs:[1A8h]
fffff800`03082ed5 6a2b            push    2Bh
fffff800`03082ed7 65ff34251...000 push    qword ptr gs:[10h]
fffff800`03082edf 4153            push    r11
fffff800`03082ee1 6a33            push    33h
fffff800`03082ee3 51              push    rcx
......
fffff800`02c7bfce 6690            xchg    ax,ax
fffff800`02c7bfd0 fb              sti
fffff800`02c7bfd1 48898be0010000  mov     qword ptr [rbx+1E0h],rcx
fffff800`02c7bfd8 8983f8010000    mov     dword ptr [rbx+1F8h],eax
nt!KiSystemServiceStart:
fffff800`02c7bfde 4889a3d8010000  mov     qword ptr [rbx+1D8h],rsp
......

nt!KiSystemCall64主要的工作是:

  1. 参数及用户栈的保存(内核态处理完打好返回用户层)
  2. 内核栈切换、内核线程的初始化设定等
  3. 准备工作做好后,最重要的是调用相应的内核服务(比如nt!NtXXX)

内核层的Nt vs Zw

对内核层来说,Zw与Nt函数虽然参数及功能上都是一致的,但二者对参数的处理及对调用环境的认同是不一样的。以nt!NtClose及nt!ZwClose的代码为例(Win7 X64):

1: kd> u nt!NtClose
nt!NtClose [d:\w7rtm\minkernel\ntos\ob\obclose.c @ 430]:
fffff800`0338e2a0 48895c2408      mov     qword ptr [rsp+8],rbx
fffff800`0338e2a5 57              push    rdi
fffff800`0338e2a6 4883ec20        sub     rsp,20h
fffff800`0338e2aa 65488....010000 mov   rax,qword ptr gs:[188h]
fffff800`0338e2b3 48833....e9ff00 cmp     qword ptr [fffff800`03227450],0
fffff800`0338e2bb 488bd9          mov     rbx,rcx
fffff800`0338e2be 0fb6b8f6010000  movzx   edi,byte ptr [rax+1F6h]
fffff800`0338e2c5 0f8520a70200    jne     nt!NtClose+0x2a74b (fffff800`033b89eb)
fffff800`0338e2cb 400fb6d7        movzx   edx,dil
fffff800`0338e2cf 488bcb          mov     rcx,rbx
fffff800`0338e2d2 488b5c2430      mov     rbx,qword ptr [rsp+30h]
fffff800`0338e2d7 4883c420        add     rsp,20h
fffff800`0338e2db 5f              pop     rdi
fffff800`0338e2dc e91ffdffff      jmp     nt!ObpCloseHandle (fffff800`0338e000)

看nt!NtClose没有针对栈做任何准备工作而是直接进入了任务的处理:即调用nt!ObpCloseHandel。类似NtCreateFile也是类似的流程。

下面再来看nt!ZwClose:

1: kd> u nt!ZwClose
nt!ZwClose [o:\w7rtm.obj.amd64fre\minkernel\ntos\ke\mp\objfre\amd64\sysstubs.asm @ 267]:
fffff800`03073640 488bc4          mov     rax,rsp
fffff800`03073643 fa              cli
fffff800`03073644 4883ec10        sub     rsp,10h
fffff800`03073648 50              push    rax
fffff800`03073649 9c              pushfq
fffff800`0307364a 6a10            push    10h
fffff800`0307364c 488d059d300000  lea     rax,[nt!KiServiceLinkage (fffff800`030766f0)]
fffff800`03073653 50              push    rax
fffff800`03073654 b80c000000      mov     eax,0Ch
fffff800`03073659 e9e2670000      jmp     nt!KiServiceInternal (fffff800`03079e40)

1: kd> u nt!KiServiceInternal
nt!KiServiceInternal:
fffff800`02c7be40 4883ec08        sub     rsp,8
fffff800`02c7be44 55              push    rbp
fffff800`02c7be45 4881ec58010000  sub     rsp,158h
fffff800`02c7be4c 488da...000000  lea     rbp,[rsp+80h]
fffff800`02c7be54 48899dc0000000  mov     qword ptr [rbp+0C0h],rbx
fffff800`02c7be5b 4889bdc8000000  mov     qword ptr [rbp+0C8h],rdi
fffff800`02c7be62 4889b5d0000000  mov     qword ptr [rbp+0D0h],rsi
fffff800`02c7be69 fb              sti
fffff800`02c7be6a 65488b1c2588010000 mov   rbx,qword ptr gs:[188h]
fffff800`02c7be73 0f0d8bd8010000  prefetchw [rbx+1D8h]
fffff800`02c7be7a 0fb6bbf6010000  movzx   edi,byte ptr [rbx+1F6h]
fffff800`02c7be81 40887da8        mov     byte ptr [rbp-58h],dil
fffff800`02c7be85 c683f601000000  mov     byte ptr [rbx+1F6h],0
fffff800`02c7be8c 4c8b93d8010000  mov     r10,qword ptr [rbx+1D8h]
fffff800`02c7be93 4c8995b8000000  mov     qword ptr [rbp+0B8h],r10
fffff800`02c7be9a 4c8d1d3d010000  lea     r11,[nt!KiSystemServiceStart (fffff800`02c7bfde)]
fffff800`02c7bea1 41ffe3          jmp     r11

从代码上来nt!ZwClose前面做的工作则有些类似syscall,如果将syscall称作是硬模式切换的话,那nt!ZwCreate所做的可以称算是软模式切换。前者 是从用户态切换到内核态,涉及到栈及context的切换,转换成本很高。而后者则是从内核态到内核态的切换,其实仅是调用关系,均在同一个内核栈上。

nt!ZwXXX函数最终都会切换到相应的nt!NtXXX函数上,因为系统设定所有的任务都是由系统服务nt!NtXXX来实际处理。

从上面的对比不难看出,nt!ZwXXX函数做了一些看似没必要的切换工作,针对 内核驱动来说貌似完全没有必要。比如打开一个文件用nt!NtCreateFile或nt!IoCreateFile等函数更能节省稀缺的内核栈资源的使用,从这点上来看确实如此。

但二者细微的差别却可能导致严重的资源泄露问题。比如一个内核句柄,即在打开文件时指定了OBJ_KERNEL_HANDLE标志,在使用完毕要释放的时候,如果正巧是在用户context(进程上下文),比如IRP_MJ_CREATE或IRP_MJ_CLEANUP等irp的处理程序中,如果调用nt!NtClose的话,则会导致此句柄的泄露,然后此文件会一直处于打开(被占用)的状态。

其原因是nt!NtClose在处理句柄关闭时默认此句柄属于前一个模式空间(PreviousMode)。相反nt!ZwClose正因为多做了“软模式切换”,其切换过程中会修改PreviousMode为KernelMode,因为nt!ZwClose被调用时的线程已经处于内核线程模式(由用户线程通过syscall切换过来的),此时内核句柄用nt!ZwClose就能够正确关闭。

那么我们这里又有了一个新问题:如果反过来用nt!ZwClose来关闭一个本属于用户进程空间的句柄,会不会出错?

答案是不会的。因为nt!ObpCloseHandle在释放句柄前会针对内核句柄做检查,只有在满足全部条件的情况才会当作内核句柄来处理:

#define KERNEL_HANDLE_MASK ((ULONG_PTR)((LONG)0x80000000))

#define IsKernelHandle(H,M)                           \
    (((KERNEL_HANDLE_MASK & (ULONG_PTR)(H)) ==        \
       KERNEL_HANDLE_MASK) &&                         \
     ((M) == KernelMode) &&                           \
     ((H) != NtCurrentThread()) &&                    \
     ((H) != NtCurrentProcess()))

Object Manager通过句柄的第31位来区分是不是内核句柄,所以即使PreviousMode是KernelMode的情形也只会操作当前用户进程的句柄表(CurrentProcess->ObjectTable)而不是内核句柄表(nt!ObpKernelHandleTable)。

小结

不妨简单一点,用这样几个“简单粗暴”的公式来理解它们之间的关系,以NtClose/ZwClose为例:

  • ntdll!ZwClose(用户层) = ntdll!NtClose (用户层)
  • ntdll!ZwClose = syscall(硬切换) + nt!ZwClose(内核层)
  • nt!ZwClose = 软切换 + nt!NtClose(内核层)

魔鬼总在细节之中 (Devils in the details) !

<一直认为对这部分掌握得比较熟,但真正动笔的时候才发现很多不确定的地方。理解只有够深度够准确够熟练才是真正的理解!本文用时2.5小时。>

参考链接

  1. Nt vs. Zw - Clearing Confusion On The Native API
  2. MSDN: PreviousMode
  3. MSDN: Using Nt and Zw Versions of the Native System Services Routines
  4. MSDN: What Does the Zw Prefix Mean?
  5. Windows 7 x64 下的 System Call
  6. 看雪学院:windows 7 x64 KiSystemCall64 笔记
Continue reading » · Rating: · Written on: 01-13-17 · No Comments »

再谈_chkstk

WDK针对XP系统的build环境,在有_chkstk产生的情况下,link阶段会失败,因为无法解析引入符号__chkstk。

可我的驱动程序却能成功创建,只是在XP系统上加载时失败。检测并证实winxp的ntoskrnl.lib并没有__chkstk的输出。但是,wdmsec.lib的输出表中含有__chkstk,而驱动在SOURCE文件中指定了要加载wdmsec.lib,所以针对XP系统的build能够成功。

针对Win7系统,ntoskrnl.lib中已包含__chkstk符号,所以针对Win7所做的build不会报警。

为了让你的驱动正强壮,有意的针对XP系统编译一下你的驱动,将更容易检查出此类堆栈使用上的问题。

Continue reading » · Rating: · Written on: 12-09-12 · No Comments »