当前位置:编程学习 > 网站相关 >>

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

补充:综合编程 , 安全编程 ,
CopyRight © 2012 站长网 编程知识问答 www.zzzyk.com All Rights Reserved
部份技术文章来自网络,