PDO
:Physical Device Object,字面意义是物理设备,暂时可以这么理解:PDO为设备栈中最下面的那个设备对象。
键盘工作原理:P121页;简述:当键盘有按键按下/松开时,将会触发中断处理函数,中断服务例程由键盘驱动提供,键盘的驱动会从端口读取扫描码,经过处理后,将获取到的数据提交给IRP,将会有对应线程对该IRP进行读取获取键盘信息。
一般来说一个PS/2的键盘的设备栈(没有安装其它键盘过滤驱动的话)为:顶层设备对象为KbdClass
驱动生成的设备对象,中间层的设备对象是驱动8042prt
生成的设备对象,底层设备对象是驱动ACPI
生成的设备对象,
无须关心上面的细节,只需要知道一个正规的键盘过滤驱动一般是去绑定KbdClass
驱动生成的设备对象。
找到并绑定所有的键盘设备。
KbdClass
驱动名为\Driver\Kbdclass
,通过ObReferenceObjectByName
,可以打开该驱动。KbdClass
驱动的设备对象,利用IoCreateDevice
创建过滤设备对象,使用IoAttachDeviceToDeviceStack
绑定每一个设备即可绑定到所有的键盘设备,将该API返回的参数以设备扩展的方式保存,方便直接下发IRP给下层设备对象。设置相应的IRP回调,需要关心的有IRP_MJ_READ
(因为要过滤的就是读取按键信息)、IRP_MJ_POWER
(因为这类请求需要使用PoCallDriver和一个PoStartNextPowerIrp来向下层设备发送IRP)、IRP_MJ_PNP
(当设备被插拔时的分发函数)、以及相应的卸载函数。
IRP_MJ_READ
:
当截到read的irp请求时,应该有一个全局变量计数器对其进行+1操作,目的是为了在卸载驱动的时候,等待该全局变量计数器归0再去卸载,不然会造成irp完成例程还没结束时就卸载的情况,此时就会蓝屏。
因为读取的IRP还没下发,所以并不知道读取了什么内容,只能通过设置完成例程来获取IRP读取的内容。通过如下代码拷贝栈空间,设置IRP完成例程来读取键盘的内容。
IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine( Irp, c2pReadComplete,
DeviceObject, TRUE, TRUE, TRUE );
在完成例程中可以通过如下代码来获取所有读取的内容,也可以修改读取的内容来讲LOCK键改成CTRL键,当完成例程结束时还需要将全局变量-1.
if( NT_SUCCESS( Irp->IoStatus.Status ) )
{
// 获得读请求完成后输出的缓冲区
buf = Irp->AssociatedIrp.SystemBuffer;
KeyData = (PKEYBOARD_INPUT_DATA)buf;
// 获得这个缓冲区的长度。一般的说返回值有多长都保存在
// Information中。
buf_len = Irp->IoStatus.Information;
numKeys = buf_len / sizeof(KEYBOARD_INPUT_DATA);
//… 这里可以做进一步的处理。我这里很简单的打印出所有的扫
// 描码。
//for(i=0;i
for(i=0;i<numKeys;++i)
{
//DbgPrint("ctrl2cap: %2x\r\n", buf[i]);
DbgPrint("\n");
DbgPrint("numKeys : %d",numKeys);
DbgPrint("ScanCode: %x ", KeyData->MakeCode );
DbgPrint("%s\n", KeyData->Flags ?"Up" : "Down" );
print_keystroke((UCHAR)KeyData->MakeCode);
if( KeyData->MakeCode == CAPS_LOCK)
{
KeyData->MakeCode = LCONTROL;
}
}
}
//别忘记全局变量还要-1
gC2pKeyCount--;
IRP_MJ_PNP
:
可以看到,和自行卸载的区别在于不用担心是否有IRP没有完成。Windows系统会自行处理这些IRP。
NTSTATUS c2pPnP(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
PC2P_DEV_EXT devExt;
PIO_STACK_LOCATION irpStack;
NTSTATUS status = STATUS_SUCCESS;
KIRQL oldIrql;
KEVENT event;
// 获得真实设备。
devExt = (PC2P_DEV_EXT)(DeviceObject->DeviceExtension);
irpStack = IoGetCurrentIrpStackLocation(Irp);
switch (irpStack->MinorFunction)
{
case IRP_MN_REMOVE_DEVICE:
KdPrint(("IRP_MN_REMOVE_DEVICE\n"));
// 首先把请求发下去
IoSkipCurrentIrpStackLocation(Irp);
IoCallDriver(devExt->LowerDeviceObject, Irp);
// 然后解除绑定。
IoDetachDevice(devExt->LowerDeviceObject);
// 删除我们自己生成的虚拟设备。
IoDeleteDevice(DeviceObject);
status = STATUS_SUCCESS;
break;
default:
// 对于其他类型的IRP,全部都直接下发即可。
IoSkipCurrentIrpStackLocation(Irp);
status = IoCallDriver(devExt->LowerDeviceObject, Irp);
}
return status;
}
unload
等待几秒钟,将所有的设备解除绑定,删除所有的过滤设备,等待全局变量归零(所有irp完成例程结束),再返回。
VOID
c2pUnload(IN PDRIVER_OBJECT DriverObject)
{
PDEVICE_OBJECT DeviceObject;
PDEVICE_OBJECT OldDeviceObject;
PC2P_DEV_EXT devExt;
LARGE_INTEGER lDelay;
PRKTHREAD CurrentThread;
//delay some time
lDelay = RtlConvertLongToLargeInteger(100 * DELAY_ONE_MILLISECOND);
CurrentThread = KeGetCurrentThread();
// 把当前线程设置为低实时模式,以便让它的运行尽量少影响其他程序。
KeSetPriorityThread(CurrentThread, LOW_REALTIME_PRIORITY);
UNREFERENCED_PARAMETER(DriverObject);
KdPrint(("DriverEntry unLoading...\n"));
// 遍历所有设备并一律解除绑定
DeviceObject = DriverObject->DeviceObject;
while (DeviceObject)
{
// 解除绑定并删除所有的设备
c2pDetach(DeviceObject);
DeviceObject = DeviceObject->NextDevice;
}
ASSERT(NULL == DriverObject->DeviceObject);
while (gC2pKeyCount)
{
KeDelayExecutionThread(KernelMode, FALSE, &lDelay);
}
KdPrint(("DriverEntry unLoad OK!\n"));
return;
}
获取类驱动对象,修改类驱动对象的分发函数
需要HOOK的是键盘类驱动对象的分发函数,这个类驱动对象的名字是KbdClass
,所以还是用ObReferenceObjectByName
来获取该驱动对象,使用InterlockedExchangePointer
这个原子操作的宏来替换原来的分发函数。
但是这么做呢有几个缺点,一是你要确保在hook的这一刻没有相关irp要处理,不然的话就会关联不上;二个是HOOK这个驱动的分发函数这么做特别容易被杀毒软件发现。
类驱动之下的端口驱动
KbdClass
称为键盘类驱动,类驱动通常指的是统管一类设备的驱动程序。端口驱动和类驱动之间的协作机制
找到回调函数的条件
从上面来看,该回调函数地址是在类驱动之中的,由端口驱动调用,根据前辈的经验指出:
根据以上规律即可找到这个回调函数了。
该回调函数的原型
typedef VOID(_stdcall *KEYBOARDCLASSSERVICECALLBACK)
(IN PDEVICE_OBJECT DeviceObject,
IN PKEYBOARD_INPUT_DATA InputDataStart,
IN PKEYBOARD_INPUT_DATA InputDataEnd,
IN OUT PULONG InputDataConsumed
);
打开键盘端口驱动寻找设备。
KbdClass
类驱动,获取该驱动模块的基址和大小。遍历类驱动的设备对象,同时以一个指针大小遍历端口驱动的设备对象的设备扩展。判断是否有指针与类驱动的设备对象的指针相同,同时判断是否有指针指向的地址空间在类驱动范围之类(该指针即是我们hook需要的函数指针)。
修改这个得到的函数指针进行hook。
获取IDT的基址
// 从sidt指令获得一个如下的结构。从这里可以得到IDT的开始地址
#pragma pack(push,1)
typedef struct P2C_IDTR_ {
P2C_U16 limit; // 范围
P2C_U32 base; // 基地址(就是开始地址)
} P2C_IDTR, *PP2C_IDTR;
#pragma pack(pop)
// 下面这个函数用sidt指令读出一个P2C_IDTR结构,并返回IDT的地址。
void *p2cGetIdt()
{
P2C_IDTR idtr;
// 一句汇编读取到IDT的位置。
_asm sidt idtr
return (void *)idtr.base;
}
IDT数组的内存空间表示
#pragma pack(push,1)
typedef struct P2C_IDT_ENTRY_ {
P2C_U16 offset_low;
P2C_U16 selector;
P2C_U8 reserved;
P2C_U8 type:4;
P2C_U8 always0:1;
P2C_U8 dpl:2;
P2C_U8 present:1;
P2C_U16 offset_high;
} P2C_IDTENTRY, *PP2C_IDTENTRY;
#pragma pack(pop)
找到第IDT的第93项,修改为我们HOOK的函数,同时保存原来的中断函数地址。
// 这个函数修改IDT表中的第0x93项,修改为p2cInterruptProc。
// 在修改之前要保存到g_p2c_old中。
void p2cHookInt93(BOOLEAN hook_or_unhook)
{
PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
idt_addr += 0x93;
KdPrint(("p2c: the current address = %x.\r\n",
(void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high)));
if(hook_or_unhook)
{
KdPrint(("p2c: try to hook interrupt.\r\n"));
// 如果g_p2c_old是NULL,那么进行hook
g_p2c_old = (void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high);
idt_addr->offset_low = P2C_LOW16_OF_32(p2cInterruptProc);
idt_addr->offset_high = P2C_HIGH16_OF_32(p2cInterruptProc);
}
else
{
KdPrint(("p2c: try to recovery interrupt.\r\n"));
// 如果g_p2c_old不是NULL,那么取消hook.
idt_addr->offset_low = P2C_LOW16_OF_32(g_p2c_old);
idt_addr->offset_high = P2C_HIGH16_OF_32(g_p2c_old);
}
KdPrint(("p2c: the current address = %x.\r\n",
(void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high)));
}
修改为自己的函数为裸函数,这个裸函数为中间跳板,跳转到真正希望执行的函数之中去。
__declspec(naked) p2cInterruptProc()
{
__asm
{
pushad // 保存所有的通用寄存器
pushfd // 保存标志寄存器
call p2cUserFilter // 调一个我们自己的函数。 这个函数将实现
// 一些我们自己的功能
popfd // 恢复标志寄存器
popad // 恢复通用寄存器
jmp g_p2c_old // 跳到原来的中断服务程序
}
}
自己实现的函数
// 首先读端口获得按键扫描码打印出来。然后将这个扫
// 描码写回端口,以便别的应用程序能正确接收到按键。
// 如果不想让别的程序截获按键,可以写回一个任意的
// 数据。
void p2cUserFilter()
{
static P2C_U8 sch_pre = 0;
P2C_U8 sch;
p2cWaitForKbRead();
_asm in al,0x60
_asm mov sch,al
KdPrint(("p2c: scan code = %2x\r\n",sch));
// 把数据写回端口,以便让别的程序可以正确读取。
if(sch_pre != sch)
{
sch_pre = sch;
_asm mov al,0xd2
_asm out 0x64,al
p2cWaitForKbWrite();
_asm mov al,sch
_asm out 0x60,al
}
键盘不是随时都可以读写数据的,以下的函数可以等到键盘可以读写的时机。
ULONG p2cWaitForKbRead()
{
int i = 100;
P2C_U8 mychar;
do
{
_asm in al,0x64
_asm mov mychar,al
KeStallExecutionProcessor(50);
if(!(mychar & OBUFFER_FULL)) break;
} while (i--);
if(i) return TRUE;
return FALSE;
}
ULONG p2cWaitForKbWrite()
{
int i = 100;
P2C_U8 mychar;
do
{
_asm in al,0x64
_asm mov mychar,al
KeStallExecutionProcessor(50);
if(!(mychar & IBUFFER_FULL)) break;
} while (i--);
if(i) return TRUE;
return FALSE;
}
还有一种HOOK的版本是设置IOAPIC的。这个后面再写。。。
当然这样的IDT hook有一种缺陷,因为多核CPU有多个IDT表。。。