驱动框架理解

  • 概述

API在某个头文件中定义,被封装在某个DLL中,而这个DLL会进一步被封装在ntdll.dll中(它里面的API叫native api),比如,ReadFile在ntdll.dll中就对应着ntReadFile;然后这个API会通过sysenter的方式进入内核层。

那么,比如对于CreateFile的执行参数,必须告知内核,而这些参数就被封装在IRP中。IRP是一个结构体,它封装了应用层传下的命令和数据。

驱动拿到IRP并进行处理,并把处理结果返回给应用层。

现在随意附加到一个进程当中,看看它的CreateFile调用。

先用windbg连上虚拟机:

然后break下来,执行命令。加载完符号表之后,使用!process 0 0 命令列出所有进程信息

我们附加到explorer.exe,使用命令:

命令!peb查看是否附加成功:

下面使用命令, bp Kernel32!CreateFileW或者bp Kernel32!CreateFileA对CreateFile下断点,如果报错:

我们只需要执行一遍.reload 或者 .reload /user命令即可。

然后F5运行,使虚拟机“复活”,不一会就会断下,此时就离ntdll!NtCreateFile不远了,这时再给ntdll!NtCreateFile下断点,复活虚拟机后不一会就能断下了:

但是,此时我们无法通过F11进入syscall,为了查看更底层的函数,我们只能给更底层的函数下断点:

然后再运行:

此时,我们再尝试往下跟踪,进入IopCreateFile,用kv命令查看一下堆栈调用:

这里,以nt!开头的就是Ring0级的API。

这里说明一下,64位下没有nt!KiFastCallEntry,而是nt!KiSystemCall64

操作系统的IO管理器分配一个IRP包,然后把应用层的数据封装到IRP包,驱动接收到IRP包怎么处理?

  • 驱动架构

任何一个API发到内核中的IRP包,都会有一个对应的Dispatch函数来处理,这组函数叫做分发/派遣函数。

因此,一个NT框架可以看做是由DriverEntry、DriverUnload以及若干分发函数组成。

 

驱动就是.sys文件,当它被加载到内存中时,就会由系统创建一个DriverObject对象,所以DriverEntry的两个参数中:

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,
                     PUNICODE_STRING pRegPath)

有了内核对象还不行,还要创建设备对象。因为,R3的IRP就是发给驱动的设备对象的:

而符号链接:

#define LINK_NAME L"\\dosdevices\\ntmodeldrv"

是设备对象创建的,R3可以查看的东西。有了符号链接,R3看驱动就像是在看普通的文件。否则,没有符号链接,R3就无法访问驱动,也就无法向这个驱动发送IRP。当然,创建设备对象和符号链接并不是必须的,如果你不需要从应用层访问,那就不需要创建它们。

  • R3和R0通信模式

第一种

这种方式就用代码描述就是:

pDeviceObject->Flags |= DO_BUFFERED_IO;

这种方式是安全的,但是由于来回拷贝,效率较低。

此外,还有一种方式叫直接IO:

这种访问方式就是直接把R3中的一块虚拟内存锁住,不让你切换出去,并把这块虚拟内存映射到物理内存某块去,直接利用这块区域进行数据交换:

还有第三种方式,DO_FORCE_NEITHER_IO,这种情况就是利用Ring0可以直接访问Ring3的权限,而直接读取Ring3中的数据,既没有锁定虚拟地址,也没有映射到物理地址,所以如果虚拟地址切换出去,那么访问的地址就是无效的。所以,在访问之前必须使用ProbeForRead和ProbeForWrite函数进行校验。

以上简单介绍了通信方式,R0和R3的通行方式还可以参考:

http://blog.csdn.net/rodney443220/article/details/30039979

我们写驱动框架的代码时,设置好通讯方式:

之后,就要创建符号链接

随后进行初始化分发函数:

这里我们只对几个重要的函数自己定义Dispatch函数,其它函数统一用DispatchCommon,其定义原型如下:

  • IRP结构

IRP包的整体结构如下所示:

其中主要分为头部和堆栈两个部分。头部中,IOStatus包括两部分:Status和Information。IOStatus中status用来记录这一次IO操作的状态,表示成功或失败;Information,表示读写操作的有效字节数,不同的操作意义不同。例如,对于ReadFile:

这里实际读的长度,就对应了上面所说的Information。对于IOStatus的设置再函数中对应着:

而后边的

表示这个IRP包在这一层就结束掉,不再往下传。因此,IRP包传给Dispatch函数时,就立即设置状态结果返回,并结束掉,什么都没有做。

而在实际中,一个IRP包往往会不断的往下发送,发送给更多的驱动设备:

头中除了IOStatus还包括SystemBuffer、MdlAddress、UserBuffer三部分:

这两个部分分别对应着不同的R3和R0通信方式。如果是Buffer_IO则驱动从SystemBuffer中拿数据,如果是Direct_IO则从MdlAddress中去拿数据。而UserBuffer则是另外一种通信方式,即直接从R3拿数据。

而IRP栈又是什么呢?其实大多数的驱动是分层的,IRP包会不断往下发,每一层都记录了不同的数据:

例如文件过滤驱动设备从第n层拿数据,而磁盘设备从第0层拿数据,比如对于ReadFile中的参数“要读的长度”,就保存在栈上。如果IRP是一个读操作,我们就用Read结构体解析,如果是写操作,就用Write结构体去解析它,如图:

那么怎么知道这次IRP是读还是写?就是通过上图中的MajorFunction。

  • DispatchClose和DispatchClean有什么区别

CloseHandle对应着DispatchClean,它和DispatchClose几乎是同时产生的。当Handle引用为0的时候,调用的是DispatchClean,而FileObject引用为0的,调用的是DispatchClose。当从磁盘上打开一个文件的时候,系统会先为它建立一个FCB(文件控制块),它和磁盘上的文件是一一对应的。此外,内核层有一个概念FileObject——文件内核对象,是多对一的,即一个FCB对应多个FileObject,它是跨进程的;应用层有一个概念,文件Handle,也是多对一的,每个进程打开都会得到一个句柄,Handle不是跨进程的,存放在句柄表中。所以当你打开一个文件时,就会对应着一个文件控制块、文件内核对象、文件句柄,有时候应用层Handle为0时,内核层的FileObject的引用并不为0,此时则只能Clean不能Close。

  • DispatchRead介绍

DispatchRead是从内核读取数据发送给应用层。DispatchRead要从IRP中拿数据,由于通信方式是Buffer_IO,所以要从SystemBuffer拿数据:

而SystemBuffer中的数据是通过ReadFile中的第二个参数传递给R0的:

读取数据的长度,是存放到IRP栈中的,栈是通过下面的方式获取的:

在从Parameter结构体中获得长度:

现在在内核中将要读取的数据传递给pReadBuffer:

最后设置返回值,并结束:

总体来说就是从IRP中拿数据、根据自己的逻辑写入数据、设置返回值结束操作三个步骤。

  • DispatchIoControl设备控制函数

这个分发函数对应着应用层的DeviceIoControl。应用层除了Read、Write我们还可以给内核发送其它的命令,甚至是自己扩展出来的命令,这种情况下使用DeviceIoControl来实现:

因此,使用这个API需要知道如何定义控制码、如何在内核中拿到控制码、InputBuffer和OutBuffer地址如何拿到。

首先要在Ring0和Ring3层应用中同时定义相同的控制码:

InputBuffer和OutputBuffer获取:

获取Buffer的长度:

而对于不同的控制码进行不同的操作,则很简单,直接使用switch函数来实现即可。

posted @ 2016-04-03 12:20  _No.47  阅读(750)  评论(0编辑  收藏  举报