第三章 串口过滤
在windows系统上与安全软件相关的驱动开发中,“过滤”(filter)是极其重要的一个概念。过滤是在不影响上层和下层接口,在内核中加入新的层,从而不修改上层软件或下层真实驱动,就加入新功能。
进行过滤的最主要的方法是对一个设备对象(DO)进行绑定。
我们可以首先认为:一个真实的设备对应一个设备对象(实际对应关系可能复杂的多)。通过编程生成一个虚拟的DO,并Attach在一个真实的设备上。一旦绑定,则本来OS发给真实设备的请求,就会首先发送到这个虚拟设备。
WDK有多个API实现绑定功能。
NTSTATUS
IoAttachDevice(
IN PDEVICE_OBJECT SourceDevice, //调用者生成的用来过滤的虚拟设备
IN PUNICODE_STRING TargetDevice, //被绑定的目标设备,这里不是指针,这个字符串是要被绑定的设备的名字
OUT PDEVICE_OBJECT *AttachedDevice //返回指针的指针,成功后,被绑定的设备指针被返回到这个地址。
);
windows中许多设备对象是有名字的,但是不是所有设备对象都有名字。必须是有名字的设备,才能用这个API进行绑定。
如果一个设备被其他设备绑定,他们在一起的一组设备,称为设备栈。(称为栈,是由于和请求的传递方式有关)。实际上,IoAttachDevice总是会绑定设备栈上最顶层的那个设备。
NTSTATUS
IoAttachDeviceToDeviceStackSafe(
IN PDEVICE_OBJECT SourceDevice,
IN PDEVICE_OBJECT TargetDevice,
IN OUT PDEVICE_OBJECT *AttachedToDeviceObject //与前面函数返回的值一样,实际上就是绑定之前设备栈上最顶端的那个设备
);
如果设备没有名字,显然用IoAttachDevice就无法绑定设备了。另外两个API,IoAttachDeviceToDeviceStack,IoAttachDeviceToDeviceStackSafe。这两个都是根据设备对象的指针来绑定的。区别是IoAttachDeviceToDeviceStackSafe更安全,在2000SP4和XP以上才有,2000低版本使用IoAttachDeviceToDeviceStack。
生成过滤设备并绑定
NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT DriverObject, //本驱动的驱动对象。DriverEntry传入
IN ULONG DeviceExtensionSize, //先简单传入0
IN PUNICODE_STRING DeviceName OPTIONAL, //设备名称,一个规则是:过滤设备一般不需要名字,传入NULL即可
IN DEVICE_TYPE DeviceType, //保持与被绑定的设备类型一致即可
IN ULONG DeviceCharacteristics, //作者总是凭经验填0或False
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);
这个函数比较复杂,目前无需了解太多。
值得注意的是,在绑定一个设备之前,应该把这个设备对象的多个子域设置成和要绑定的目标对象一致,包括标志和特征。
从名字获得设备对象
NTSTATUS
IoGetDeviceObjectPointer(
IN PUNICODE_STRING ObjectName, //设备名字
IN ACCESS_MASK DesiredAccess, //实际使用不要顾忌那么多,直接填FILE_ALL_ACCESS
OUT PFILE_OBJECT *FileObject, //返回参数,即获得这个设备对象的同时会得到的一个文件对象。对于打开串口设备,这个文件对象没有什么用处。但必须注意:在使用这个函数之后必须把这个文件对象“接触引用”,否则内存泄露
OUT PDEVICE_OBJECT *DeviceObject
);
实际上如何从虚拟设备得到的串口上流过的数据呢?
根据请求。
1. 每个驱动程序只有一个驱动对象。
2. 每个驱动程序可以生产若干个设备对象。这些设备对象从属于一个驱动对象。
3. 若干个设备依次绑定形成一个设备栈,总是最顶端的设备先接受到请求。
请注意IRP是上层设备之间传递请求的常见数据结构,但绝对不是唯一的数据结构。传递请求还有其他很多方法,不同的设备也可能使用不同的结构来传递请求。但在本书中,90%的情况下,请求与IRP是等价概念。
串口设备接收到的是IRP,因此只要对所有IRP进行过滤,就可以得到串口上流过的所有数据。串口过滤只需要关心两种请求:读,写。
请求可以通过IRP的主功能号进行区分。IRP的主功能号在保存在IRP栈空间中的一个字节,用来标示这个IRP的功能大类。相应,还有个次功能号。
读请求的主功能号为IRP_MJ_READ,写为IRP_MJ_WRITE。
请求的结局
对请求的过滤
1. 请求被允许通过了。过滤不做任何事情,或者简单的获取请求的一些信息。但请求不受干扰。
2. 请求直接被否决了。过滤禁止这个请求通过,这个请求被返回错误,下层驱动程序根本收不到这个请求。这样系统行为就变了,后果是常常看见上层应用程序弹出错误提示权限错误或者读取文件失败之类信息。
3. 过滤完成了这个请求。比如一个读请求,我们想记录读到了什么。如果读请求还没有完成,那么如何知道到底会读到什么呢?只有让这个请求先完成再记录。过滤完成这个请求时不一定要原封不动的完成,这个请求的参数可以被修改(如把数据加密)。
当过滤请求时,就必须把这个请求按照上面3中方法之一进行处理。
对于第一种情况,最简单。首先调用IoSkipCurrentIrpStackLocation跳过当前栈空间,然后调用IoCallDriver把这个请求发送给真实的设备。注意,因为真实的设备已经被过滤设备绑定,所以首先接收到IRP的是过滤设备对象。
写请求的数据
一个写请求(串口一次发送出的数据)保存在哪里呢?
IRP有三个地方描述缓冲区:
1. irp->MDLAddress
2. irp->UserBuffer
3. irp->AssociateIrp.SystemBuffer
不同的IO类型,IRP的缓冲区不同。
SystemBuffer是一般用于比较简单且不追求效率情况下的解决方案。把Ring3层中内存空间中缓冲数据拷贝到内核空间。
UserBuffer则是最追求效率的解决方案。应用层的缓冲区地址直接放到UserBuffer里,在内核空间中访问。在当前进程和发送请求进程一致的情况下,内核访问应用程的内存空间当然是没错的。但是一旦内核进程已经切换,这个访问就结束了,访问UserBuffer当然是跳到其他进程空间去了。因为在windows中,内核空间是所有进程共用的,而应用层空间则是各个进程隔离的。
更简单的解决方案是把应用层的地址空间映射到内核空间,这需要在页表中增加一个映射。当然不需要手工修改,通过构造MDL就能实现。MDL可以翻译为“内存描述符链”。IRP中的MDLAddress域是一个MDL的指针,从这个MDL中可以读出一个内核空间的虚拟地址。这就弥补了UserBuffer的不足,同时比SystemBuffer的完全拷贝方法要轻量,因为这个内存实际上还是老地方,没有拷贝。
对于串口,作者也不清楚到底是哪种方式。但是通过MDL都可以把数据正确读出。
MmGetSystemAddressForMdlSafe,从MDL得到系统空间虚拟地址。
缓冲区的长度
ULONG len = irpsp->Parameters.Write.Length;
动态卸载
IoDetachDevice
IoDeleteDevice
KeDelayExcutionThread
卸载过滤驱动有一个关键的问题:我们要终止这个过滤程序,但是一些IRP可能还在这个过滤程序的处理中,要取消这些请求非常的麻烦,而且不一定能成功。所以解决方案是等待5秒来保证安全的卸载掉。这对于串口而言是没问题的。但是并非所有的设备都如此。后面会有不同的处理方案。
示例代码:
#include <ntddk.h> #define NTSTRSAFE_LIB //为了使用静态的ntstrsafe静态库 #include <ntstrsafe.h> #ifndef SetFlag #define SetFlag(_F,_SF) ((_F) |= (_SF)) #endif #ifndef ClearFlag #define ClearFlag(_F,_SF) ((_F) &= ~(_SF)) #endif #define CCP_MAX_COM_ID 32 // 过滤设备和真实设备 static PDEVICE_OBJECT s_fltobj[CCP_MAX_COM_ID] = { 0 }; static PDEVICE_OBJECT s_nextobj[CCP_MAX_COM_ID] = { 0 }; // 打开一个端口设备 PDEVICE_OBJECT ccpOpenCom(ULONG id,NTSTATUS *status) { UNICODE_STRING name_str; static WCHAR name[32] = { 0 }; PFILE_OBJECT fileobj = NULL; PDEVICE_OBJECT devobj = NULL; // 输入字符串。 memset(name,0,sizeof(WCHAR)*32); RtlStringCchPrintfW( name,32, L"\\Device\\Serial%d",id); RtlInitUnicodeString(&name_str,name); // 打开设备对象 *status = IoGetDeviceObjectPointer(&name_str, FILE_ALL_ACCESS, &fileobj, &devobj); // 如果打开成功,一定要把文件对象解除引用 if (*status == STATUS_SUCCESS) ObDereferenceObject(fileobj); // 返回设备对象 return devobj; } NTSTATUS ccpAttachDevice( PDRIVER_OBJECT driver, PDEVICE_OBJECT oldobj, PDEVICE_OBJECT *fltobj, PDEVICE_OBJECT *next) { NTSTATUS status; PDEVICE_OBJECT topdev = NULL; // 生成设备,然后绑定之。 status = IoCreateDevice(driver, 0, NULL, oldobj->DeviceType, 0, FALSE, fltobj); if (status != STATUS_SUCCESS) return status; // 拷贝重要标志位。 if(oldobj->Flags & DO_BUFFERED_IO) (*fltobj)->Flags |= DO_BUFFERED_IO; if(oldobj->Flags & DO_DIRECT_IO) (*fltobj)->Flags |= DO_DIRECT_IO; if(oldobj->Flags & DO_BUFFERED_IO) (*fltobj)->Flags |= DO_BUFFERED_IO; if(oldobj->Characteristics & FILE_DEVICE_SECURE_OPEN) (*fltobj)->Characteristics |= FILE_DEVICE_SECURE_OPEN; (*fltobj)->Flags |= DO_POWER_PAGABLE; // 绑定一个设备到另一个设备上 topdev = IoAttachDeviceToDeviceStack(*fltobj,oldobj); if (topdev == NULL) { // 如果绑定失败了,销毁设备,重新来过。 IoDeleteDevice(*fltobj); *fltobj = NULL; status = STATUS_UNSUCCESSFUL; return status; } *next = topdev; // 设置这个设备已经启动。 (*fltobj)->Flags = (*fltobj)->Flags & ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS; } // 这个函数绑定所有的串口。 void ccpAttachAllComs(PDRIVER_OBJECT driver) { ULONG i; PDEVICE_OBJECT com_ob; NTSTATUS status; for(i = 0;i<CCP_MAX_COM_ID;i++) { // 获得object引用。 com_ob = ccpOpenCom(i,&status); if(com_ob == NULL) continue; // 在这里绑定。并不管绑定是否成功。看一下s_fltobj即可,不为NULL的成员表示已绑定 ccpAttachDevice(driver,com_ob,&s_fltobj[i],&s_nextobj[i]); // 取消object引用。 } } #define DELAY_ONE_MICROSECOND (-10) #define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000) #define DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000) void ccpUnload(PDRIVER_OBJECT drv) { ULONG i; LARGE_INTEGER interval; // 首先解除绑定 for(i=0;i<CCP_MAX_COM_ID;i++) { if(s_nextobj[i] != NULL) IoDetachDevice(s_nextobj[i]); } // 睡眠5秒。等待所有irp处理结束 interval.QuadPart = (5*1000 * DELAY_ONE_MILLISECOND); KeDelayExecutionThread(KernelMode,FALSE,&interval); // 删除这些设备 for(i=0;i<CCP_MAX_COM_ID;i++) { if(s_fltobj[i] != NULL) IoDeleteDevice(s_fltobj[i]); } } NTSTATUS ccpDispatch(PDEVICE_OBJECT device,PIRP irp) { PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp); NTSTATUS status; ULONG i,j; // 首先得知道发送给了哪个设备。设备一共最多CCP_MAX_COM_ID // 个,是前面的代码保存好的,都在s_fltobj中。 for(i=0;i<CCP_MAX_COM_ID;i++) { if(s_fltobj[i] == device) { // 所有电源操作,全部直接放过。 if(irpsp->MajorFunction == IRP_MJ_POWER) { // 直接发送,然后返回说已经被处理了。 PoStartNextPowerIrp(irp); IoSkipCurrentIrpStackLocation(irp); return PoCallDriver(s_nextobj[i],irp); } // 此外我们只过滤写请求。写请求的话,获得缓冲区以及其长度。 // 然后打印一下。 if(irpsp->MajorFunction == IRP_MJ_WRITE) { // 如果是写,先获得长度 ULONG len = irpsp->Parameters.Write.Length; // 然后获得缓冲区 PUCHAR buf = NULL; if(irp->MdlAddress != NULL) buf = (PUCHAR) MmGetSystemAddressForMdlSafe(irp->MdlAddress,NormalPagePriority); else buf = (PUCHAR)irp->UserBuffer; if(buf == NULL) buf = (PUCHAR)irp->AssociatedIrp.SystemBuffer; // 打印内容 for(j=0;j<len;++j) { DbgPrint("comcap: Send Data: %2x\r\n", buf[j]); } } // 这些请求直接下发执行即可。我们并不禁止或者改变它。 IoSkipCurrentIrpStackLocation(irp); return IoCallDriver(s_nextobj[i],irp); } } // 如果根本就不在被绑定的设备中,那是有问题的,直接返回参数错误。 irp->IoStatus.Information = 0; irp->IoStatus.Status = STATUS_INVALID_PARAMETER; IoCompleteRequest(irp,IO_NO_INCREMENT); return STATUS_SUCCESS; } NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path) { size_t i; // 所有的分发函数都设置成一样的。 for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++) { driver->MajorFunction[i] = ccpDispatch; } // 支持动态卸载。 driver->DriverUnload = ccpUnload; // 绑定所有的串口。 ccpAttachAllComs(driver); // 直接返回成功即可。 return STATUS_SUCCESS; }