常见的内核数据结构
1。驱动对象结构(DRIVER_OBJECT)
每个驱动对象代表一个已经加载的内核驱动程序,指向驱动对象结构的指针常常作为DriverEntry,AddDevice,Unload等函数的参数。
驱动对象结构是半透明的(即结构中只有部分域是公开的),其中公开的域包括DeviceObiect,DriverExtension,HardwareDatabase,FastIoDispatch,DriverInit,DriverStartIo,DriverUnload以及MajorFunction
驱动对象的结构如下
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
//
// The following links all of the devices created by a single driver
// together on a list, and the Flags word provides an extensible flag
// location for driver objects.
//
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
//
// The following section describes where the driver is loaded. The count
// field is used to count the number of times the driver has had its
// registered reinitialization routine invoked.
//
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
//
// The driver name field is used by the error log thread
// determine the name of the driver that an I/O request is/was bound.
//
UNICODE_STRING DriverName;
//
// The following section is for registry support. Thise is a pointer
// to the path to the hardware information in the registry
//
PUNICODE_STRING HardwareDatabase;
//
// The following section contains the optional pointer to an array of
// alternate entry points to a driver for "fast I/O" support. Fast I/O
// is performed by invoking the driver routine directly with separate
// parameters, rather than using the standard IRP call mechanism. Note
// that these functions may only be used for synchronous I/O, and when
// the file is cached.
//
PFAST_IO_DISPATCH FastIoDispatch;
//
// The following section describes the entry points to this particular
// driver. Note that the major function dispatch table must be the last
// field in the object so that it remains extensible.
//
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT;
typedef struct _DRIVER_OBJECT *PDRIVER_OBJECT;
这样和编写一个应用程序,windows直接从main函数开始执行来生成一个进程就不同了,内核模块并不是生成一个进程,只是填写一组回调函数让windows来调用,而且这组回调函数必须符合windows内核规定。
这一组回调函数包括上面的“普通分发函数PFAST_IO_DISPATCH FastIoDispatch”和“快速IO分发函数PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
”
一个内核模块的所有功能都由他们提供给Windows
Windows 的很多组件都拥有自己的DRIVER_OBJECT,比如所有的硬件驱动程序,文件系统(NTFS和FASTFat,有各自的DRIVER_OBJECT),以及其他许多的内核组件,
如果编写内核程序,能找到这些关键的驱动结构(比如NTFS文件系统)然后修改下面的分发函数,替换成我么的函数,可能就能捕获WIndows的文件操作,让我们的内核程序处理完毕后再交给NTFS文件系统处理,这样就可以加入我们自己的功能,比如扫描病毒,文件加密等,这就是所谓的分发函数HOOK技术
2.0设备对象
设备对象是内核中的重要对象,其重要性比不亚于WindowsGUI中的窗口,窗口是唯一可以接收消息的东西,任何消息都是发送给一个窗口的,而在内核中,大部分的消息都是以请求包IRP的方式传递的,而设备对象(DEVICE_OBJECT)是唯一可以接受请求的实体,任何一个请求IRP都是发送给某个设备对象的。
举一个例子,一个DEVICE_OBJECT可以代表一个实际的硬盘,很明显,硬盘可以被读写,所以这个DEVICE_OBJECT将接收读和写这两个请求,(或者更多,比如删除),但是一个DEVICE_OBJECT也可能代表一个和硬件毫无关系的东西,比如内核中可能有一个设备,实现类似于“管道”的功能,一个进程打开这个设备对象进行读,另一个进程打开这个设备对象进行写,就把数据从一个进程传递到另一个进程
因为我们总是在内核程序中生成一个设备对象,而一个内核程序是用一个驱动对象表示的,所以一个设备对象总是属于一个驱动对象。
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _DEVICE_OBJECT {
CSHORT Type;
USHORT Size;
LONG ReferenceCount;
struct _DRIVER_OBJECT *DriverObject;
struct _DEVICE_OBJECT *NextDevice;
struct _DEVICE_OBJECT *AttachedDevice;
struct _IRP *CurrentIrp;
PIO_TIMER Timer;
ULONG Flags; // See above: DO_...
ULONG Characteristics; // See ntioapi: FILE_...
__volatile PVPB Vpb;
PVOID DeviceExtension;
DEVICE_TYPE DeviceType;
CCHAR StackSize;
union {
LIST_ENTRY ListEntry;
WAIT_CONTEXT_BLOCK Wcb;
} Queue;
ULONG AlignmentRequirement;
KDEVICE_QUEUE DeviceQueue;
KDPC Dpc;
//
// The following field is for exclusive use by the filesystem to keep
// track of the number of Fsp threads currently using the device
//
ULONG ActiveThreadCount;
PSECURITY_DESCRIPTOR SecurityDescriptor;
KEVENT DeviceLock;
USHORT SectorSize;
USHORT Spare1;
struct _DEVOBJ_EXTENSION *DeviceObjectExtension;
PVOID Reserved;
} DEVICE_OBJECT;
3.IRP请求包
I/O请求包(I/O Request Packets)是I/O管理器与驱动程序通信基础的数据结构,同时,通过使用IRP也允许驱动程序之间进行通信,一般由应用程序发起I/O请求,I/O管理器创建并根据请求填充IRP,然后发送给目标驱动程序
IRP包含两个部分,固定大小的IRP头以及非固定大小的I/O栈单元,IRP头用I/O管理器用来存储与原始请求相关的信息,如调用者所传入与设备无关的参数,打开的文件所在设备的地址,同时它被驱动程序用来存储最终的请求结果,紧随IRP头之后就是一系列的I/O栈单元,驱动设备中的每一个驱动程序对应一个I/O栈单元,每个栈单元包括参数,功能码等
举一个例子,如果要求网卡发送一个数据包,或者向网卡请求把已经存在缓冲区里接收到的包读出来,这就是一个请求:如果读取一个文件从0开始到512个字节,这也是一个请求,应用程序的开发者当然是看不到这些请求的对一个应用层的开发者而言,只要调用API函数WriteFile就可以写入文件数据了,但是这些操作最终在内核中那个被IO管理器翻译成请求IRP或者与之等效的其他形式比如快速IO调用,发送往某个设备对象。
IRP的结构非常复杂
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP {
CSHORT Type;
USHORT Size;
//
// Define the common fields used to control the IRP.
//
//
// Define a pointer to the Memory Descriptor List (MDL) for this I/O
// request. This field is only used if the I/O is "direct I/O".
//
PMDL MdlAddress;
//
// Flags word - used to remember various flags.
//
ULONG Flags;
//
// The following union is used for one of three purposes:
//
// 1. This IRP is an associated IRP. The field is a pointer to a master
// IRP.
//
// 2. This is the master IRP. The field is the count of the number of
// IRPs which must complete (associated IRPs) before the master can
// complete.
//
// 3. This operation is being buffered and the field is the address of
// the system space buffer.
//
union {
struct _IRP *MasterIrp;
__volatile LONG IrpCount;
PVOID SystemBuffer;
} AssociatedIrp;
//
// Thread list entry - allows queueing the IRP to the thread pending I/O
// request packet list.
//
LIST_ENTRY ThreadListEntry;
//
// I/O status - final status of operation.
//
IO_STATUS_BLOCK IoStatus;
//
// Requestor mode - mode of the original requestor of this operation.
//
KPROCESSOR_MODE RequestorMode;
//
// Pending returned - TRUE if pending was initially returned as the
// status for this packet.
//
BOOLEAN PendingReturned;
//
// Stack state information.
//
CHAR StackCount;
CHAR CurrentLocation;
......
//
// CancelRoutine - Used to contain the address of a cancel routine supplied
// by a device driver when the IRP is in a cancelable state.
//
__volatile PDRIVER_CANCEL CancelRoutine;
//
// Note that the UserBuffer parameter is outside of the stack so that I/O
// completion can copy data back into the user's address space without
// having to know exactly which service was being invoked. The length
// of the copy is stored in the second half of the I/O status block. If
// the UserBuffer field is NULL, then no copy is performed.
//
PVOID UserBuffer;
//
// Kernel structures
//
// The following section contains kernel structures which the IRP needs
// in order to place various work information in kernel controller system
// queues. Because the size and alignment cannot be controlled, they are
// placed here at the end so they just hang off and do not affect the
// alignment of other fields in the IRP.
//
union {
.......
//
// Thread - pointer to caller's Thread Control Block.
//
PETHREAD Thread;
.....
struct {
//
// List entry - used to queue the packet to completion queue, among
// others.
//
LIST_ENTRY ListEntry;
union {
//
// Current stack location - contains a pointer to the current
// IO_STACK_LOCATION structure in the IRP stack. This field
// should never be directly accessed by drivers. They should
// use the standard functions.
//
struct _IO_STACK_LOCATION *CurrentStackLocation;
//
// Minipacket type.
//
ULONG PacketType;
};
};
//
// Original file object - pointer to the original file object
// that was used to open the file. This field is owned by the
// I/O system and should not be used by any other drivers.
//
PFILE_OBJECT OriginalFileObject;
} Overlay;
.....
} Tail;
} IRP;
typedef IRP *PIRP;
生成请求 :主功能号为IRP_MJ_CREATE的IRP
查询请求:主功能号为IRP_MJ_QUERY_INFOMATION的IRP
设置请求:主功能号为IRP_MJ_SET_INFOMATION的IRP
控制请求:主功能号为IRP_MJ_DEVICR_CONTROL
关闭请求:主功能号为IRP_MJ_CLOSE的IRP
请求指针:IRP的指针,PIRP
第三章 串口的过滤
在windows系统上与安全软件相关的驱动开发过程中,过滤是极其重要的一个概念,过滤是在不影响上层和下层的接口的情况下,在windows 系统内核中加入新的层,从而不需要修改上层的软件或者下层的真实驱动程序,就加入了新的功能
设备绑定的内核API,一个是IoAttachDeviceToDeviceStackSafe,另一个是IoAttachDeviceToDeviceStack,这两个函数功能一样,都是根据设备对象的指针(而不是名称)进行绑定,区别是IoAttachDeviceToDeviceStackSafe更加安全,而且只有XP以上的系统才有,一般都是IoAttachDeviceToDeviceStackSafe。
NTSTATUS
IoAttachDeviceToDeviceStackSafe(
_In_ PDEVICE_OBJECT SourceDevice,
_In_ PDEVICE_OBJECT TargetDevice,
_Outptr_ PDEVICE_OBJECT *AttachedToDeviceObject
);
生成过滤设备并绑定
在绑定一个设备之前,先要知道如何生成一个用于过滤的过滤设备,函数IoCreateDevice被用于生成设备,在WDM.H中
NTSTATUS
IoCreateDevice(
_In_ PDRIVER_OBJECT DriverObject,
_In_ ULONG DeviceExtensionSize,
_In_opt_ PUNICODE_STRING DeviceName,
_In_ DEVICE_TYPE DeviceType,
_In_ ULONG DeviceCharacteristics,
_In_ BOOLEAN Exclusive,
_Outptr_result_nullonfailure_
_At_(*DeviceObject,
__drv_allocatesMem(Mem)
_When_((((_In_function_class_(DRIVER_INITIALIZE))
||(_In_function_class_(DRIVER_DISPATCH)))),
__drv_aliasesMem))
PDEVICE_OBJECT *DeviceObject
);
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->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;
}
在知道一个设备名字的情况下,使用函数IoGetDeviceObjectPointer可以获得这个设备对象的指针,这个函数的原型如下
NTSTATUS
IoGetDeviceObjectPointer(
_In_ PUNICODE_STRING ObjectName,
_In_ ACCESS_MASK DesiredAccess,
_Out_ PFILE_OBJECT *FileObject,
_Out_ PDEVICE_OBJECT *DeviceObject
);
其中ObjectName就是设备名字,DesiredAccess是期望访问的权限。FileObject是一个返回参数,即获得这个设备对象的同时会得到一个文件对象(FileObject),就打开串口设备这件事而言,这个文件对象并没有多大的用处,但是必须注意,在使用这个函数之后必须把这个文件对象“解除引用”,否则会引起内存泄露
当虚拟设备已经绑定了真正的串口设备,那么实际上如何从虚拟设备得到串口上流过的数据呢?答案是根据请求,操作系统请求发送给串口设备,请求中就包含了要发送的数据,请求的回答中则含有要接受的数据。
请求的区分
Windows的内核开发者们确定了很多的数据结构,DEVICE_OBJECT,FILE_OBJECT,DRIVER_OBJECT
每个驱动程序只有一个驱动对象
每个驱动程序可以生成若干个设备对象。
若干个设备一次绑定形成一个设备栈,总是最顶端的设备先接收到请求
串口设备接收到的请求都是IRP,因此只要对所有IRP进行过滤,就可以得到串口上流过的所有数据,串口过滤时只要关心有两种请求,读请求和写请求,对串口而言,读指接收数据,而写指发出数据,串口还有其他请求比如打开和关闭,设置波特率等
请求通过IRP的主功能号区分,IRP的主功能号是保存在IRP栈空间中的一个字节,用来标致这个IRP的功能大类,相应的还有一个次功能号来标致这个IRP的功能细分
读请求的主功能号为IRP_MJ_READ,而写请求的主功能号为IRP_MJ_WRITE,下面的方法用于从一个IRP指针得到一个主功能号
这里的irpsp称为IRP的栈空间,IoGetCurrentIrpStackLocation获得当前栈空间
PIRP irp;
PIO_STACK_LOCATION irpsp=IoGetCurrentIrpStackLocation(irp);
if(irpsp->MajorFunction==IRP_MJ_READ)
{
}
else if(irpsp->MajorFunction==IRP_MJ_WRITE)
{
}
请求的结局
对请求的过滤,最终的结局有三种
1当请求被允许通过了,过滤不做任何事情,或者简单的获取一些请求的信息,但是请求本身不受干扰
2.请求直接被否决,过滤禁止这个请求通过,这个请求被返回了错误,下层驱动程序根本收不到这个请求,这样系统行为就变了,然后常常看到上层应用程序弹出错误框提示无权限错误或者读取文件失败之类的信息
3.过滤完成了请求,有时有这样的需求,比如一个读请求,我们想记录读到了什么,如果读请求还没有完成,那么如何知道到底读到了什么呢,只有让这个请求先完成再去记录,过滤完成这个请求时不一定要原封不动的完成,这个请求的参数可以被修改。(比如把数据都加密一番)
串口过滤要捕获两种数据:一种是发送出的数据,另一种是接收到的数据,为了简单起见,我们只捕获发送出的数据,这样,只需要采取第一种处理方法即可
这种处理最简单,首先调用IoSkipCurrentIrpStackLocation跳过当前栈空间,然后调用IoCallDriver把这个请求发送给真实的设备,注意,因为真实的设备已经被过滤设备绑定,所有首先接收到IRP的事过滤设备的对象
写请求的数据
那么一个写请求(也就是串口一次发送出的数据)保存在哪里呢?回忆前面关于IRP结构的描述,里面一共有3个地方可以描述缓冲区,一个是irp->MDLAddress
一个是irp->UserBuffer,一个是irp->AssociateIrp.SystemBuffer,不同的I/O类别,IRP缓冲区不同,SystemBuffer是一般用于比较简单且不追求效率情况下的解决方案,把应用层R3中内存空间中的缓冲数据拷贝到内核空间。
UserBuffer则是最追求效率的解决方案,应用层的缓冲区地址直接放在UserBuffer里,在内核空间中访问,在当前进程和发送请求进程一致的情况下,内核访问应用层的内存空间当然没错,但是一旦内核进程已经切换,这个访问就结束了,访问UserBuffer当然是跳到其他进程空间中去了,因为在Windows中,内核空间是所有进程共用的,而应用层空间则是各个进程隔离的