一. 从驱动文件走进开发第一步:
一般,编译一个驱动需要三个主要文件:
.c文件(.cpp亦可) sources文件 makefile文件
其中.c文件为驱动实现文件,sources文件和makefile文件可以理解为辅助文件,用于编译之用。
二. 根据框架图来逐步细化:
1. 串口过滤驱动入口:
所有Windows驱动均是以DriverEntry函数作为入口,好比C语言中的main函数或MFC中的WinMain函数;据驱动程序的框架,我们可以在这个函数里面定义处理IRP的派遣函数,卸载函数,及绑定串口函数。
另外,我们设定过滤的串口个数设为:CCP_MAX_COM_ID;
现在我们来用代码来见证这种构思:
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;
}
2.派遣函数:
2.1电源操作:
对于每个驱动都会对应一些设备的开关,比如现在面对的电源,所以我们在驱动加载时打开即可:
// 所有电源操作,全部直接放过。
if(irpsp->MajorFunction == IRP_MJ_POWER)
{
// 直接发送,然后返回说已经被处理了。
PoStartNextPowerIrp(irp);
IoSkipCurrentIrpStackLocation(irp);
return PoCallDriver(s_nextobj[i],irp);
}
2.2过滤写请求:
a.写请求的数据:
首先我们来看一下IRP数据结构:
typedef struct _IRP {
PMDL MdlAddress;
ULONG Flags;
union {
struct _IRP *MasterIrp;
PVOID SystemBuffer;
} AssociatedIrp;
IO_STATUS_BLOCK IoStatus;
KPROCESSOR_MODE RequestorMode;
BOOLEAN PendingReturned;
BOOLEAN Cancel;
KIRQL CancelIrql;
PDRIVER_CANCEL CancelRoutine;
PVOID UserBuffer;
union {
struct {
union {
KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
struct {
PVOID DriverContext[4];
};
};
PETHREAD Thread;
LIST_ENTRY ListEntry;
} Overlay;
} Tail;
} IRP, *PIRP;
其中有3个地方可以描述缓冲区:
IRP->MdlAddress :把应用层的地址空间映射到内核空间(以虚拟地址实现);
IRP->UserBuffer :应用层的缓冲区地址直接放在UserBuffer里,在内核空间中访问;
IRP-> SystemBuffer :把应用层中内存空间中的缓冲数据拷贝到内核空间。
不同的IO类别,IRP的缓冲区不同。因为我们要获取的是所有的数据,所以3个地方我们都去就是了。另外,在此之前,我们得申请一块缓冲区供存放读取的书据,这又涉及到此块缓冲区应该申请多大的问题,幸好WDK文档中,对于IRP_MJ_WRITE有如下说明:
The buffer supplies data for the device or driver. The buffer's length is specified by Parameters.Write.Length in the driver's IO_STACK_LOCATION structure.
所以对于写操作而言,缓冲区长度可以如下获得:
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;
b.打印数据:
// 打印内容
for(j=0;j<len;++j)
{
DbgPrint("comcap: Send Data: %2x\r\n",
buf[j]);
}
C.请求数据后的善后处理:
对于请求数据后的处理,我们不得不谈谈过滤的概念:
过滤是在不影响上层和下层接口的情况下,在Windows系统内核中加入新的层,从而不需要修改上层的软件或者下层的真实驱动程序,就加入了新的功能。简单点说,就是从中间层”偷看信息”,而不让别人知道。所以作为”偷窥者”,我们最好让这个信息回到它该去的地方,以下代码可以帮我们顺利”逃过凶险”:
// 这些请求直接下发执行即可。我们并不禁止或者改变它。
IoSkipCurrentIrpStackLocation(irp);
return IoCallDriver(s_nextobj[i],irp);
3.动态卸载函数:
作为一名合格的程序员,我们应考虑到加载后的驱动程序如何卸载,这里的动态卸载函数有点像Windows应用程序里的回调函数,在入口函数中声明,等待事件发生就触发此函数。在没有介绍绑定的前提下,我们先做一个约定:
在卸载函数中先要完成解除过滤驱动绑定的功能,否则会出现蓝屏。
同时我们要补充的一点就是,在卸载过滤驱动时,必须要考虑到一个时间差的问题,即一些IRP的请求是否已经处理完。作为保险我们最好在删除设备前能在线程中睡眠一小段时间,以确保发送过来的请求处理完成,也避免因此而产生蓝屏现象。
好啦,停下来把我们的思路再整理整理,首先记得我们的约定,解除过滤驱动绑定的功能;然后因为请求处理时间差的问题,我们得”睡眠”一小段时间;然后呢,就是删除设备了。下面用代码来实现:
// 1.首先解除绑定
for(i=0;i<CCP_MAX_COM_ID;i++)
{
if(s_nextobj[i] != NULL)
IoDetachDevice(s_nextobj[i]);
}
// 2.睡眠 5 秒。等待所有irp处理结束
interval.QuadPart = (5*1000 * DELAY_ONE_MILLISECOND);
KeDelayExecutionThread(KernelMode,FALSE,&interval);
//3. 删除这些设备
for(i=0;i<CCP_MAX_COM_ID;i++)
{
if(s_fltobj[i] != NULL)
IoDeleteDevice(s_fltobj[i]);
}
4.绑定串口
4.1打开一个端口设备
在入口函数DriverEntry中已经传进一个设备的名字,通过这个名字我们就可以调用IoGetDeviceObjectPointer来获得这个设备对象的指针。以下是这个函数的原型:
NTSTATUS
IoGetDeviceObjectPointer(
IN PUNICODE_STRING ObjectName, //设备名字
IN ACCESS_MASK DesiredAccess, //期望访问的权限
OUT PFILE_OBJECT *FileObject, //输出一个文件对象
OUT PDEVICE_OBJECT *DeviceObject //输出这个设备对象的指针
);
这里有一点需要注意,这个函数返回的参数中有一个FileObject文件对象,在使用这个函数之后必须把它”解除应用”,否则会引起内存泄漏。
另外,在驱动开发中字符串一般都由一个叫做 UNICODE_STRING 的结构体来存储,下面是它的定义:
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
结构体内的参数根据其名称就可以断定,就不多说了。
以下是实现打开一个端口的代码:
// 打开一个端口设备
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;
}
4.2 绑定设备
在绑定一个设备之前,我们最好先生成一个过滤设备,用它来绑定现有设备。如此,我们先用 IoCreateDevice 函数生成一个设备,其原型如下:
NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT DriverObject, //本驱动的驱动对象
IN ULONG DeviceExtensionSize, //设备扩展
IN PUNICODE_STRING DeviceName OPTIONAL, //设备名称
IN DEVICE_TYPE DeviceType, //设备类型
IN ULONG DeviceCharacteristics, //设备特征
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject //产生一个设备对象指针
);
值得注意的是,在绑定一个设备之前,应该把这个设备对象的多个子域设置成要和绑定的目标对象一致,包括标志和特征。下面给个小示例:
// 拷贝重要标志位。
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;
然后就是把这个过滤设备绑定到现有设备上了,在此,我们必须要有一个根据设备对象指针(而不是名字)来进行绑定,在WDK中 IoAttachDeviceToDeviceStack 这个函数课用来实现此功能,其原型如下:
PDEVICE_OBJECT
IoAttachDeviceToDeviceStack(
IN PDEVICE_OBJECT SourceDevice,
IN PDEVICE_OBJECT TargetDevice
);
绑定好设备之后,最好能将这个设备设置成已经启动:
// 设置这个设备已经启动。
(*fltobj)->Flags = (*fltobj)->Flags & ~DO_DEVICE_INITIALIZING;
4.3 绑定所有串口:
如果要实现所有的串口,就必须能够实现串口过滤设备的一一对应,因此用数组实现最好,如果笔者没记错的话,串口个数在计算机上好像最大只能设置成256,在此我们设定为32作为实验之用吧。以下用代码来巩固我们的设想:
//假定要检测的串口个数为32个
#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 };
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;
// 在这里绑定。并不管绑定是否成功
// 生成一个设备,然后绑定在领了一个设备上
ccpAttachDevice(driver,com_ob,&s_fltobj[i],&s_nextobj[i]);
}
三. 生成驱动并测试
用Windows Driver Kits中的x86 Checked Build Environment生成驱动;
1. 用DriverMonitor控制驱动的加载和卸载,软件图标如下:
用DriverMonitor加载和卸载串口驱动comcap.sys会有相应信息显示如下:
2.用Dbgview来观察加载驱动后串口的输出消息:
四. 对comcap.c进行修改,使之对所有的串口输出都禁止
对于禁止串口的输出,我们可以回忆一下当我们的过滤设备接收消息后是如何处理的,根据当时的需求,我们只是做了一个监视的功能,现在要做一个控制的功能,所以回到处理时的代码看看:
// 这些请求直接下发执行即可。我们并不禁止或者改变它。
IoSkipCurrentIrpStackLocation(irp);
return IoCallDriver(s_nextobj[i],irp);
对于禁止输出无非就是我们拿到消息之后,不要往下传,”就当什么事情也没发生”。想想,我们要不往下传,可以取消调用上面的函数即可。(即将源文件中的这两行代码注释即可)
[课程]Linux pwn 探索篇!