Windows内核安全编程__传统键盘过滤程序
技术原理
1.预备知识
何为符号链接?符号链接其实就是设备的一个“别名”。在应用程序中想要访问设备一般要通过符号链接来完成,而不是设备名本身。
ZwCreateFile是很重要的函数。同名的函数有两个:一个在内核中(ntknos.exe),一个在应用层(ntdll.dll)。在应用程序中调用CreateFile就可以引发对这个函数的调用。
它不但可以打开文件,还可以打开设备(返回一个类似于文件句柄的句柄)。这个函数最终调用NtCreateFile。
何为PDO?PDO是Phsiycal DeviceObject的简称,字面上的意义是物理设备,可以暂时这样理解:
PDO是设备栈最下面的那个设备。
这个理解并不精确,但是很实用。
2.Windows中从击键到内核
在任务管理器中有一个进程叫做csrss.exe。这个进程很关键,他有一个线程win32!RawInputThread,这个线程通过一个GUID(GUID_CLASS_KEYBOARD)获得键盘设备栈中PDO的符号链接。
win32k!RawInputThread执行到函数win32k!OpenDevice,它的一个参数可以找到键盘设备栈的PDO符号连接名。win32k!OpenDevice有一个OBJECT_ATTRIBUTES结构的局部变量,它自己初始化这个局部变量,用传入参数中的键盘设备栈的PDO赋值给OBJECT_ATTRIBUTES中的PUNICODE_STRING ObjectName。
然后调用ZwCreateFile,ZwCreateFile完成打开设备的工作,最后通过传入参数返回得到句柄。win32k!RawInputThread把得到的句柄保存起来,供后面的ReadFile,DeviceIOControl等使用。
ZwCreateFile通过系统服务,调用内核中的NtCreateFile,NtCreateFile执行到nt!IoParseDevice中,调用nt!IoGetAttachDevice,通过PDO获得键盘设备栈最顶端的设备对象。用这个设备对象中的char StackSize作为参数来调用函数IoAllocateIrp,创建IRP。调用nt!ObOpenObjectByName中继续执行,调用nt!ObpCreateHandle在进程(csrss.exe)的句柄表中创建一个新的句柄,这个句柄对应的对象是刚才创建初始化的那个文件对象,文件对象中的DeviceObject指向键盘设备栈的PDO。
win32k!RawInputThread在获得了句柄之后,会以这个句柄为参数,调用nt!ZwReadFile,向键盘驱动要求读入数据。nt!ZwReadFile中会创建一个IRP_MJ_READ的IRP发给键盘驱动,告诉键盘驱动要求读入数据。键盘驱动通常会使这个IRP Pending,即IRP_MJ_READ不会被满足,它一直被放在那里,等待来自键盘的数据。而发出这个读请求的线程win32k!RawInputThread也会等待,等待这个读操作完成。
当键盘上有键被按下时,将触发键盘的那个中断,引起中断服务例程的执行,键盘中断的中断服务例程由键盘驱动提供。键盘驱动从端口读取扫描码,进过一些列处理之后,把键盘得到的数据交给IRP,最后结束这个IRP。
这个IRP的结束,将导致win32k!RawInputThread线程对这个操作的等待结束。win32k!RawInputThread线程将会对得到的数据作出处理,分发给合适的进程。一旦把输入数据处理完之后,win32k!RawInputThread线程会立刻再调用一个nt!ZwReadFile,想键盘驱动要求读入数据。于是又开始一个等待,等待键盘上的键被按下。
简单的说,win32k!RawInputThread线程总是nt!ZwCreateFile要求读入数据,然后等待键盘上的按键被按下。当键盘上的键被按下时,win32k!RawInputThread处理nt!ZwReadFile得到的数据。然后nt!ReadFile要求读入数据,再等待键盘上的键被按下。
(记住,这么一长串描述都是在瞬间完成的,不要被迷惑)
我们一般看到的PS/2键盘设备栈,如果自己没有另外安装其他键盘过滤程序,那么设备栈的情况是这样的:
*最顶层的设备是驱动Kbdclass生成的设备对象
*中间层的设备对象是驱动i8042prt生成的设备对象
*最底层设备对象是驱动ACPI生成的对象
现在,我们只需要知道要去绑定的那个设备驱动就是KbdClass的设备对象就可以。
3.键盘硬件原理
从键盘被敲击到计算机屏幕上出现一个字符,中间有很多复杂的变换。一个字符显然并不代表一个键,因为大写小写的字母是同一个键,只根据Shift键来决定是大写还是小写。此外还有许多复杂的功能键,如Ctrl,Alt。所以键不是用字符来代表,而是给每个键规定了一个扫描码。
键盘和CPU的交互方式是中断和读取端口,这个操作是串行的。一次中断发生,就等于键盘给了CPU一次通知。这个通知只能通知一个事件:某个键被按下了,某个键被弹起了。为此,一个键实际需要两个扫描码,如果按下的扫描码为X,则同一个键弹起的扫描码为X+0x80;
键盘过滤的框架
1.找到所有的键盘设备
要过滤一种设备,首先要绑定它。现在需要找到所有代表键盘的设备。从前面的原理来看,可以认定的是,如果绑定了驱动KbdClass的所有设备对象,则代表键盘的设备一定在其中。如何找到一个驱动下的所有对象。一个DRIVER_OBJECT下有一个域叫做DeviceObject,这个看似是一个设备对象的指针,但是由于DeviceObject之中又有一个域叫做NextDevice,指向同一个驱动中的下一个设备,所以这里是一个设备链。
除了用上面所说的直接读取驱动对象下面的DeviceObject域之外,另一种获得驱动下所有设备对象的方法是调用IoEnumerateDeviceObjectList,这个函数也可以枚举出一个驱动下所有的设备。
现在来看代码:
// 这个函数是事实存在的,只是文档中没有公开。声明一下
// 就可以直接使用了。
extern "C" NTSTATUS ObReferenceObjectByName(
PUNICODE_STRING ObjectName,
ULONG Attributes,
PACCESS_STATE AccessState,
ACCESS_MASK DesiredAccess,
POBJECT_TYPE ObjectType,
KPROCESSOR_MODE AccessMode,
PVOID ParseContext,
PVOID *Object
);
extern "C" POBJECT_TYPE IoDriverObjectType;
// 这个函数经过改造。能打开驱动对象Kbdclass,然后绑定
// 它下面的所有的设备:
NTSTATUS
c2pAttachDevices(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
NTSTATUS status = 0;
UNICODE_STRING uniNtNameString;
PC2P_DEV_EXT devExt;
PDEVICE_OBJECT pFilterDeviceObject = NULL;
PDEVICE_OBJECT pTargetDeviceObject = NULL;
PDEVICE_OBJECT pLowerDeviceObject = NULL;
PDRIVER_OBJECT KbdDriverObject = NULL;
KdPrint(("MyAttach\n"));
// 初始化一个字符串,就是Kdbclass驱动的名字。
RtlInitUnicodeString(&uniNtNameString, KBD_DRIVER_NAME);
// 请参照前面打开设备对象的例子。只是这里打开的是驱动对象。
status = ObReferenceObjectByName (
&uniNtNameString,
OBJ_CASE_INSENSITIVE,
NULL,
0,
IoDriverObjectType,
KernelMode,
NULL,
(PVOID*)&KbdDriverObject
);
// 如果失败了就直接返回
if(!NT_SUCCESS(status))
{
KdPrint(("MyAttach: Couldn't get the MyTest Device Object\n"));
return( status );
}
else
{
// 这个打开需要解应用。早点解除了免得之后忘记。
ObDereferenceObject(DriverObject);
}
// 这是设备链中的第一个设备
pTargetDeviceObject = KbdDriverObject->DeviceObject;
// 现在开始遍历这个设备链
while (pTargetDeviceObject)
{
// 生成一个过滤设备,这是前面读者学习过的。这里的IN宏和OUT宏都是
// 空宏,只有标志性意义,表明这个参数是一个输入或者输出参数。
status = IoCreateDevice(
IN DriverObject,
IN sizeof(C2P_DEV_EXT),
IN NULL,
IN pTargetDeviceObject->DeviceType,
IN pTargetDeviceObject->Characteristics,
IN FALSE,
OUT &pFilterDeviceObject
);
// 如果失败了就直接退出。
if (!NT_SUCCESS(status))
{
KdPrint(("MyAttach: Couldn't create the MyFilter Filter Device Object\n"));
return (status);
}
// 绑定。pLowerDeviceObject是绑定之后得到的下一个设备。也就是
// 前面常常说的所谓真实设备。
pLowerDeviceObject =
IoAttachDeviceToDeviceStack(pFilterDeviceObject, pTargetDeviceObject);
// 如果绑定失败了,放弃之前的操作,退出。
if(!pLowerDeviceObject)
{
KdPr
补充:综合编程 , 安全编程 ,