[内核编程] 寒假学习内核编程小结

The road to success was trial and error development, recompilation, and lots of crashes.

寒假过去一个月,计划很多时候也没法跟上。不过总体上来说,还是学习和收获了一些东西的。过去的事情不能改变,所以也不必过于纠结和懊悔。

误解minifilter与sfilter的关系

    对minifilter和sfilter关系的误解,导致后续工作的方向出现了一些偏差。我原以为这是两个不一样的东西,总以为minifilter的开发需要重新搭建环境,sfilter的那一套都不再适用于minifilter。这使得我迟迟不敢开展minifilter的相关工作,就是认为为sfilter搭建好的环境在minifilter中不再适用。

    直到3月中旬,我才恍然大悟。其实minifilter只是将sfilter中文件系统开发的部分封装了起来,用的工具以及开发包其实都是一样的。

    此外,一直认为sfilter能做的事情minifilter不能做,其实不然,偶然看到一篇文章,说minifilter一样可以实现sfilter的功能,只是会降低一些兼容性,这就和在C中嵌入汇编是一样的。在后续的学习过程中,发现这样的看法是很正确的。综合开发时间,以及工程量,以及兼容性等多方面考虑,最终十分坚信的选择minifilter框架开发透明过滤系统。

    弯路是无法避免的,但往往只要坚持,一切都将会明朗起来。

开发环境和调试环境

    Windows7+WDK+VS2010+VisualDDK、Windows7+WDK+VS2010+DDKWizard,这两种都是在VS中配置调试环境,有人说速度较快而且方便,不过这不是我最先采用的方案,也不知道情况,就不多作评论。还有人在VC6.0中配置开发环境,不过据说调试不是很方便。

    现在采用的方案是,开发环境:Windows7+WDK_VS2010,调试环境Windbg + VM虚拟机双机调试,采用DbgView查看内核输出,srvinstw用来安装sfilter服务,对于minifilter则采用inf文件。工具的使用也整理在了博客上。

    用VS的时候,即使能通过WDK编译的程序,也会显示错误,有错误当然就无法编译出.sys文件,但是如果将编译类型.exe改成文件类型,不管怎么样都是可以变移除.sys文件的了。在网上看了一些资料和建议,包括OSR上也建议使用WDK提供的编译工具进行编译,这样方便发现一些VS中无法发现的错误。为了编写方便,可以使用VS进行代码的编写,然后使用WDK编译。

    Windbg使用的不是很熟练,不过确实还是挺强大的,也有网友支持使用Windbg加虚拟机进行双机调试,而不太认同采用在VS中配置调试环境,或者是觉得没这个必要。总之目前我自己使用的话觉得这确实也是个不错的选择。

    有时候可采用的方案不少,但是应该尽早的确定,以免把事件浪费在犹豫不定之中。

Sfilter的学习是基础

    即使是采用minifilter进行开发,还是有必要对sfilter进行系统性的学习的。毕竟minifilter是sfilter之上的接口。再者,了解真正的底层处理过程,消息传递机制,内存访问控制等等知识对于理解minifilter是很有帮助的。此外,有时候为了实现特殊的功能,比如自己填写并发送IRP,还是会涉及到sfilter相关的概念的。

    学习,主要是围绕《汉江独钓windows内核安全编程》一书开展的。其中对Microsoft提供的sfilter实例代码进行了认真的研读。详细的内容在《汉江独钓》的学习笔记中。学习了以下基础知识:

内核编程环境与应用层环境的区别:主要就是内核层不受操作系统管制,而用户层是受到操作系统的保护的和约束的,所以内核层开发难度大。

Windows开发模型:

NT(KDM)、WDM、WDF(WDM的升级版)

WDM:基于分层,物理设备对象(PDO)和功能设备对象(FDO)。FDO附加到PDO上,之前还会存在过滤驱动,AttachedDevice指向PDO,StackSize表示还有多少层到达底层物理设备。

[图] 设备对象栈

设备层次结构:

垂直层次结构:层次结构主要是通过一个设备附加到另一个设备上实现的。设备栈的顺序是,先创建底层的PDO,在创建上层的FDO,及从底层设备到高层设备。在PDO和FDO之间夹杂着各种过滤驱动。下层通过AttachedDevice域找到上层设备,上层通过设备扩展找到下层设备。

水平层次结构:描述了同一驱动对象所创建出来的设备对象之间的关系。

[图] 设备的水平结构

 

重要数据结构:

驱动对象(DRIVER_OBJECT):每一个驱动程序都对应着一个唯一的驱动对象。

[图] 驱动对象的结构

设备对象(DEVICE_OBJECT):一个驱动对象会创建一个或多个设备对象,每个设备对象都有一个指向驱动对象的指针,还有一个指向下一个设备对象的指针,从而形成设备链。在内核编程中,大多"消息"都是通过IRP进行传递的,而设备是唯一的接收者。

设备扩展(DEVICE_EXTENSION):由编程者定义,I/O管理器创建,用于存储必要的全局信息,从而避免使用全局变量,缓解不可重入的问题。

输入输出请求包(IRP,I/O request packet):上层应用层会通过I/O请求与底层驱动进行通信,操作系统会将I/O请求转换成相应的IRP数据,由对应的分发函数进行处理。

    [图] IRP包结构示意图

MdlAddress(PMDL)域指向一个内存描述符表(MemoryDeseriptionList,MDL),该表描述了一个与请求关联的用户模式缓冲区。

AsociateIrp域是一个三指针联合,该指针指向一个数据缓冲区,该缓冲区位于内核模式的非分页内存中。

IoStatus(IO_STATUS_BLOCK):是一个仅包含两个域的结构,驱动在最终完成请求时填写这个结构。

UserBuffer对于读写方式为"皆不是方式"的IRP_MJ_DEVICE_CONTROL请求,该域包含输出缓冲区的用户模式虚拟地址。

IRP的处理主要有:根据IRP直接操作具体硬件,根据IRP生产新的IRP发到其他驱动中,直接将IRP转发到更底层驱动中去。

重入性:重入性是指,在多线程的程序中,多个函数并行运行,函数的运行结果不会根据函数的调用先后顺序而导致不同。

符号连接:驱动程序的设备名称只能在内核态可见,而用户模式下应用程序识别设备必须通过符号链接或设备接口找到设备

内核编程的特殊性:

内核编程的调用源:调用源是指编程者能够看到的条用某段代码的最初始函数。一般单线程只有一个main调用源。但是在内核编程中,有可能存在多个调用源:DriverEntry、DriverUnload、回调函数、分发函数、完成函数。

函数多线程安全:多个线程调用该函数时要保证可重入性。

代码中断级:主要有PASIVE_LEVEL和DISPACH_LEVEL,并且前者的中断级低于后者的中断级,运行于低级中断级中的程序可以调用高级中断级中的程序,反之则不行,并且不能通过强制降低中断级来实现,可以另外产生一个运行于低级中断中的线程来完成任务。此外,在使用分页内存的时候,可能会产生缺页中断。缺页中断允许在PASIVE_LEVEL级的程序中,但是不能出现在DISPACH_LEVEL以及更高级的中断级中,否则系统将会崩溃,所以对于DISPACH_LEVEL以及更高级的中断级,必须使用非分页内存。

过滤的概念:过滤是在不影响上层和下层接口的情况下,在windows系统内核中加入新的层,从而不需要修改上层的软件或下层的真实驱动程序,就加入了新的功能。

中断:IRQ(硬件中断,某个IRQ来自什么硬件,很大程度上有规定,少数由用户进行定义)、NT(软中断),使得CPU暂停,跳到中断处理函数中,中断处理函数的首地址在IDT(中断描述符表)中。

文件系统:

控制设备(CDO):CDO主要的任务是修改整个驱动的内部配置,所以一般来说一个驱动只对应一个CDO。

卷设备:一般来说一个卷对应一个逻辑盘(如:C、D、E、F)。

普通分发函数:对特定的主功能号的IRP进行处理,在DriverEntry中指定。一般需要特殊处理的则给予单独的分发函数,最简单的处理是直接下发。

快速分发函数:处理Fast I/O,因为文件系统除了普通的IRP请求之外,还有Fast I/O请求,若不作处理会出错。

加载函数(DriverEntry):一般要完成一些初始化工作,例如,设备的创建与绑定,创建符号链接,指派分发函数等。

卸载函数(DriverUnload):完成清理工作,例如设备的解绑,用户开辟内存的销毁等工作。

    驱动创建的设备一般有三种读写方式:

缓冲区方式:操作系统会分配一段内核模式下的内存,将应用程序提供缓冲区的数据复制到所分配的内核模式下的内存中;

直接方式:操作系统会将用户模式下的系统锁住。然后操作系统将这段这段缓冲区在内核模式地址再映射一次。(MDL)

皆不是方式:分发函数直接读写应用程序提供的缓冲区地址。

 

Minifilter的学习

相关基础知识:

miniflter一些解释如下:

例程(Routine):我不懂得例程和函数有什么不同。我认为例程就是函数。称为Routine而不是Function可能是为了避免其他c程序员理解得太容易。

接口(Api):编程开发接口,一个提供给你调用的函数。

流(Stream):NTFS文件系统独有.用来保存一个文件的额外信息.似乎可以当作文件被打开。

域(Field):一个数据结构中的一个数据成员。喜欢数据库的人可能称为字段。喜欢面向对象的称为数据成员。

透明(transparence):看不见,意味着也不需要管。不过请注意透明的反意词绝对不是不透明(opacity)。

不透明(opacity):不知道的。比如空指针。因为空指针指向的是什么,从空指针本身是了解不到的。所以称为不透明的指针。

回调(Callback)函数:一个由系统调用而且原则上你不能自己调的函数。

预操作(pre-operation)回调:如果打算过滤一个操作,那么这个回调出现在操作完成之前。

后操作(post-operation)回调:如果打算过滤一个操作,那么这个回调出现在操作完成之后。

过滤管理器(filter manager / fltmgr):微软的windows内核开发者开发的一个新的驱动。这个驱动恰恰就是传统型的文件系统过滤驱动,但是这个驱动提供了一个接口,接收一些注册过的内核模块。微软把基本的IRP处理都交给了fltmgr。用户若按照此规范进行开发,则相对简单了很多

Miniflter用到的内核函数定义都在fltKernel.h

Minifilter的编程框架:

    微文件系统过滤的注册与开启(FltRegisterFilter、FltStartFiltering)

微过滤器的数据结构(FLT_REGISTRATION、FLT_OPERATION_REGISTRATION)

卸载回调函数(FltUnregisterFilter)

预操作回调函数:代表minifilter处理I/O操作完成之前

    FLT_PREOP_CALLBACK_STATUS

NPPreCreate (

__inout PFLT_CALLBACK_DATA Data,

__in PCFLT_RELATED_OBJECTS FltObjects,

__deref_out_opt PVOID *CompletionContext

)

后操作回调函数:代表minifilter已经完成对I/O的所有处理,并返回控制给过滤管理器。

    FLT_POSTOP_CALLBACK_STATUS

NPPostCreate (

__inout PFLT_CALLBACK_DATA Data,

__in PCFLT_RELATED_OBJECTS FltObjects,

__in_opt PVOID CompletionContext,// 用户可自定义,从预操作中传入的上下文

__in FLT_POST_OPERATION_FLAGS Flags

)

其他回调函数(以下回调函数不是必须的):

InstanceSetupCallback():这个回调函数的存在意义主要是让,内核程序开发者来决定哪些卷需要绑定,哪些卷不需要绑定。

InstanceQueryTeardownCallback():控制实例销毁函数,这个函数只有在一个手工解绑定的请求时被调用。

InstanceTeardownStartCallback():为实例解绑定控制函数,当调用的时候表示已经决定要解除绑定。

InstanceTeardownCompleteCallback():为实例解绑定的完成函数。

    

minifilter与应用程序通信模块(可作成DLL),相关的用户层API定义在#include <FltUser.h>中。

minifilter的安装(使用INF文件)。

示例代码——《汉江独钓》:

    minifilter    :内核代码,用WDK编译环境编译,得到.sys文件。

    minifilter_app    :    与用户交互界面,编译获得exe,【VS编译通过】

    minifilter_dll    : 与内核程序交互的接口,供minifilter_app编译得到的exe动态加载调用,以实现和内核程序的通信,编译得到.dll文件。【VS编译通过】

    将exe和dll放在同一个目录下,然后使用INF安装.sys文件,启动服务后,运行exe即可与内核层的.sys服务进行通信。

————————————————————————————————

其它:

ULONG gTraceFlags = 0;

#define PT_DBG_PRINT( _dbgLevel, _string )

(FlagOn(gTraceFlags,(_dbgLevel)) ?

DbgPrint _string :

((void)0))

PT_DBG_PRINT( PTDBG_TRACE_ROUTINES,

("NPminifilter!NPInstanceSetup: Entered ") );

FlagOn在当前文档中没找到,我想应该在sfilter源码中可以找到,因为minifilter使用的过滤管理器是基于sfilter实现的。稍后参看sfilter源码找答案。

————————————————————————————————

已经在ntifs.h头文件中找到 FlagOn的定义:

//

// These macros are used to test, set and clear flags respectivly

//

#ifndef FlagOn

#define FlagOn(_F,_SF) ((_F) & (_SF))

#endif

————————————————————————————————

回调数据包结构代表了一个I/O操作。过滤管理器与微过滤驱动都使用这个结构来初始化与处理I/O操作,内含许多嵌套结构定义,这些可以在WDK标头档fltkernel.h中找到更多数据。

————————————————————————————————

Like legacy filter drivers, minifilter drivers attach in a particular order. However, the order of attachment is determined by a unique identifier called an altitude. The attachment of a minifilter driver at a particular altitude on a particular volume is called an instance of the minifilter driver.

The filter manager provides the following support routines for explicit load and unload requests, which can be issued from user mode or kernel mode:

FilterLoad

FilterUnload

FltLoadFilter

FltUnloadFilter

The following routines are used to register and unregister callback routines for instance setup and teardown:

FltRegisterFilter

FltStartFiltering

FltUnregisterFilter

——由此想到,可以通过内核或者用户层调用这些API对当前的minifilter进程(也就是透明加密进程)进行保护,对进程监控过程中,如果发现进程被关闭则另一个进程就调用相关的API对其进行重启。此外服务器端可以通过这些API对客户端的miniflter进程进行控制。

安全字符串的问题

调试输出设置技巧

全局变量结构:Global

在阅读微软所给的示例代码中,全局变量都使用了一个自定义的Global结构存起来。这样一来便于对全部变量进行一个统一的管理,一看就知道有多少个全局变量,感觉还不错,可以效仿。

Windbg调试

今天搞了一下午,windbg调试,老以为是虚拟机死机。实际上,是因为windbg自动跳到了停止的状态,也就是说让虚拟机停了(就是ctrl+break)的效果。应该用GO使得虚拟机跑起来,虚拟机就可以动了,一直以为是虚拟机死机,现在才晃过神来。。。明天继续加油!!

一些资料

看雪:枚举注册的minifilter驱动及其信息:

http://bbs.pediy.com/showthread.php?p=1237318

msdn:

http://blogs.msdn.com/b/alexcarp/

函数指针:

http://baike.baidu.com/link?url=Ais5Inih-jhN6vLludf-x_gbzEf1mdWpWjVPpp_St8iuN9ikQIHnph-WPZF_4q4DbNYkcKgkMI1p1lKniNzIMK

详细的相关概念介绍

栈空间:

WDK:

ms-help://MS.WDK.v10.7600.091201/Kernel_d/hh/Kernel_d/IRPs_0966c1a7-2239-4be3-a545-e358173f29e8.xml.htm

I/O Stack Locations

The I/O manager gives each driver in a chain of layered drivers an I/O stack location for every IRP that it sets up. Each I/O stack location consists of an IO_STACK_LOCATION structure.

The I/O manager creates an array of I/O stack locations for each IRP, with an array element corresponding to each driver in a chain of layered drivers. Each driver owns one of the stack locations in the packet and calls IoGetCurrentIrpStackLocation to obtain driver-specific information about the I/O operation.

Each driver in such a chain is responsible for calling IoGetNextIrpStackLocation, then setting up the next-lower driver's I/O stack location. Any higher-level driver's I/O stack location can also be used to store context about an operation so that the driver's IoCompletion routine can perform its cleanup operations.

The Processing IRPs in Layered Drivers figure shows two I/O stack locations in the original IRP because it shows two drivers, a file system driver and a mass-storage device driver. The driver-allocated IRPs in the Processing IRPs in Layered Drivers figure do not have a stack location for the FSD that created them. Any higher-level driver that allocates IRPs for lower-level drivers also determines how many I/O stack locations the new IRPs should have, according to the StackSize value of the next-lower driver's device object.

The following figure shows the contents of the IRP in more detail.

Contents of I/O Stack Location in an IRP

As shown in the figure, each driver-specific I/O stack location in an IRP contains the following general information:

  • The major function code (IRP_MJ_XXX), indicating the basic operation the driver should carry out
  • For some major function codes handled by FSDs, higher-level SCSI drivers, and all PnP drivers, a minor function code (IRP_MN_XXX), indicating which subcase of the basic operation the driver should carry out
  • A set of operation-specific arguments, such as the length and starting location of a buffer into which or from which the driver transfers data
  • A pointer to the driver-created device object, representing the target (physical, logical, or virtual) device for the requested operation
  • A pointer to the file object, representing an open file, device, directory, or volume

A file system driver accesses the file object through its I/O stack location in IRPs. Other drivers usually ignore the file object.

The set of IRP major and minor function codes that a particular driver handles can be device-type-specific. However, lowest-level drivers and intermediate drivers (including PnP function and filter drivers) usually handle the following set of basic requests:

  • IRP_MJ_CREATE — open the target device object, indicating that it is present and available for I/O operations
  • IRP_MJ_READ — transfer data from the device
  • IRP_MJ_WRITE — transfer data to the device
  • IRP_MJ_DEVICE_CONTROL — set up (or reset) the device, according to a system-defined, device-type-specific I/O control code (IOCTL)
  • IRP_MJ_CLOSE — close the target device object
  • IRP_MJ_PNP — perform a Plug and Play operation on the device. An IRP_MJ_PNP request is sent by the PnP manager through the I/O manager.
  • IRP_MJ_POWER — perform a power operation on the device. An IRP_MJ_POWER request is sent by the power manager through the I/O manager.

For more information about the major IRP function codes that drivers are required to handle, see IRP Major Function Codes.

In general, the I/O manager sends IRPs with at least two I/O stack locations to mass-storage device drivers because a file system is layered over other drivers for mass-storage devices. The I/O manager sends IRPs with a single stack location to any driver that has no other driver layered above it.

However, the I/O manager provides support for adding a new driver to any chain of existing drivers in the system. For example, an intermediate mirror driver that backs up data on a given disk partition might be inserted between a pair of drivers, such as the file system driver and lowest-level driver shown in the Processing IRPs in Layered Drivers figure. When this new driver attaches itself to the device stack, the I/O manager adjusts the number of I/O stack locations in all IRPs it sends to the file system, mirror, and lowest-level drivers. Every IRP that the file system in the Processing IRPs in Layered Drivers figure allocated would also contain another I/O stack location for such a new mirror driver.

Note that this support for adding new drivers to an existing chain implies certain restrictions on any particular driver's access to the I/O stack locations in IRPs:

  • A higher-level driver in a chain of layered drivers can safely access only its own and the next-lower-level driver's I/O stack locations in any IRP. Such a driver must set up the I/O stack location for the next-lower-level driver in IRPs. However, when designing such a higher-level driver, you cannot predict when (or whether) a new driver will be added to the existing chain just below your driver.

Therefore, you should assume that any subsequently added driver will handle the same IRP major function codes (IRP_MJ_XXX) as the displaced next-lower-level driver did.

  • The lowest-level driver in a chain of layered drivers can safely access only its own I/O stack location in any IRP. When designing such a driver, you cannot predict when (or whether) a new driver will be added to the existing chain above your device driver.

In designing a lowest-level driver, assume that the driver can continue to process IRPs using the information passed in its own I/O stack location, whatever the originating source of a given IRP and however many drivers are layered above it.

一些资源网站

Everything Windows Driver Development,OSR Online:http://osronline.com/

看雪:

http://bbs.pediy.com/

msdn:

http://blogs.msdn.com/b/alexcarp/

微软官网:

https://msdn.microsoft.com/en-us/library/windows/hardware/ff540402(v=vs.85).aspx

CODE PROJECT:

http://www.codeproject.com/

驱网:

http://bbs3.driverdevelop.com/thread.php?fid-39.html

CSDN、博客园等也有一些文章。会百度,会google也能找到的。

原文地址:https://www.cnblogs.com/fanling999/p/4509785.html