最近在看ROOTKITS方面的书,有一章直接操纵硬件,是关于键盘上的LED指示器的,感觉对我这样的菜鸟很有帮助,就按照着文章的例程,写下了这篇文章。由于我在ROOTKITS这方面还只能算个菜鸟中的菜鸟,所以文章中的所谓还希望各位看官们,不吝赐教嗷。
说实话不知道应该从哪里讲起,我这方面的知识很零碎……硬着头皮上吧,一边写一边百度,额。
其实光明正大的控制一个键盘很简单。一般的键盘控制器在0X60和0X64上是可寻址的,也就是说这两个端口(PORT)是进入键盘芯片的端口。如果像我一样使用WDK,则可以使用:
READ_PORT_UCHAR(PORT);
WRITE_PORT_UCHAR(PORT,COMMAND/DATA);
这两个函数对这两个端口进行读写操作。
通过这两个端口我们可以做很多事情,比如键盘窃听什么的^^当然还有我们今天的主角,控制键盘上LED指示器。
键盘和鼠标是共享0X60端口的,所以在使用之前,系统要先读取0X64端口,判断是鼠标还是键盘。话说回来。笔记本这方面也应该一样的吧。


设置LED的命令是 0XED。而 OXED 使用的数据格式是一个八位的数据是:
  ■■■■■■■■        (话说一个■表示一位,即0或者1,最后三位控制键盘上 SCROLL LOCK ,NUM LOCK,CAPS LOCK3个LED灯的亮暗) 
            ↑↑↑
            ↑↑CAPS LOCK
            ↑↑
            ↑NUM LOCK
            ↑
            ↑
            SCROLL LOCK
而我们所要做的就是使用上面提到的2个函数:
WRITE_PORT_UCHAR(0X60,0XED);
WRITE_PORT_UCHAR(0X60,00000111b); 比如这样表示3个灯全亮。
上面两个就是传说中的核心代码……要是文章能这样结束那该多好呀~~~不过硬件可不是那么好对付的。
如果键盘忙于处理击键动作,那么这种方式很可能无法成功。对话硬件一个很重要的地方就是要等到硬件芯片就绪,如果芯片未就绪就试图向它发送命令、数据,比较理想的情况是什么都不会发生。偶尔么就崩溃了。
程序中出现了一个叫做Dpc的东西,看不懂可以先放放,而且我也不怎么懂,等下我们一起学吧。
前置知识介绍完了,开始动手吧。 对了 这个针对8042键盘,也就是一般是普通台式的键盘,话说笔记本上的3个LED灯就不一样了,比如我这台上面最左边的是表示硬盘读写状态的LED。
写完用WDK编译一下就可以用 InstDrv 加载测试了,呵呵,来一次与硬件的对话吧。
===========================================
#include "ntddk.h"  //不解释
#include <stdio.h>  //同上

PUCHAR KEYBOARD_PORT_60 = (PUCHAR)0x60;
PUCHAR KEYBOARD_PORT_64 = (PUCHAR)0x64; //定义两个端口

#define IBUFFER_FULL 0X02  //寄存器标志位,等下会与READ_PORT_UCHAR()的返回值进行&运算,来判断是否没有输入,至于为什么是00000010b,我不知道
#define OBUFFER_FULL 0X01  //估计是与键盘输的数据有关吧,因为WDK文档上讲READ_PORT_UCHAR()的返回值是从指定端口的读取的字节。
                           //键盘输出的字符毕竟有限。
PKTIMER gTimer;  //计时器
PKDPC gDPCP;     //Dpc,后面细说
UCHAR g_key_bits=0;    //这个是控制LED灯循环的

BOOLEAN WaitForKeyboard()
{
   INT i=100;     //循环的次数
   UCHAR mychar;    
   do
     {
       mychar=READ_PORT_UCHAR(KEYBOARD_PORT_64);      
       KeStallExecutionProcessor(666);             //这个函数据说能使CPU暂停特定微秒数。于是键盘可以完成它先前的工作。至于为什么
                                                   //是666毫秒。。因为书上这么写的。
       if(!(mychar&IBUFFER_FULL)) break;
     }
   while(i--);
   if(i) return TRUE;
   return FALSE;
}
VOID DrainOutputBuffer()                                 //这个函数的用途是如果键盘缓冲区含有击键动作,它会将缓冲区排干,防止干                                                          // 扰LED灯
{                                                        
  int i=100;      //循环次数
  UCHAR c;
   do
   {
       c=READ_PORT_UCHAR(KEYBOARD_PORT_64);              
       KeStallExecutionProcessor(666);                   //依然666。
       if(!(c&OBUFFER_FULL)) break;    
   }
   while(i--);
}
BOOLEAN SendKeyboardCommand(                              //等下会被下一个函数SetLEDS调用,开始传给0X60端口命令和数据了 ,文章开
                             IN UCHAR theCommand          //始有说过怎么传的 还记得吧。
                           )
{
   if(TRUE == WaitForKeyboard())                          //如果键盘就绪 就开始传递。
    {
      WRITE_PORT_UCHAR(KEYBOARD_PORT_60,theCommand);
      return TRUE;
    }
   return FALSE;
   
}
VOID SetLEDS(                                   //这个函数调用上面一个函数。
              UCHAR theLEDS
            )
{
  SendKeyboardCommand(0XED);
  SendKeyboardCommand(theLEDS);
}

VOID OnUnload(                                      //卸载驱动的函数。取消该取消的,释放该释放的。
               IN PDRIVER_OBJECT DriverObject
             )
{
      KeCancelTimer(gTimer); //取消计时器
      ExFreePool(gTimer);    //释放内存池
      ExFreePool(gDPCP);     //同上
}


VOID timerDPC(                                           //这个函数是KeInitializeDpc的一个参数,等下看KeInitializeDpc的时候一起                                                                //讲吧。
               IN PKDPC Dpc,
               IN PVOID DeferredContext,
               IN PVOID sys1,
               IN PVOID sys2
             )
{
  SetLEDS(g_key_bits++);
  if(g_key_bits>0X07) g_key_bits=0;
}                                                       

NTSTATUS DriverEntry(
                     IN PDRIVER_OBJECT theDriverObject,
                     IN PUNICODE_STRING theRegistryPath
                    )
{   
    LARGE_INTEGER timeout;                          
    theDriverObject->DriverUnload=OnUnload;              //申明驱动卸载例程
    gTimer=ExAllocatePool(NonPagedPool,sizeof(KTIMER));  //分配内存,由于只是个测试,就用NonPagedPool参数了。
    gDPCP=ExAllocatePool(NonPagedPool,sizeof(KDPC));    //一样的。
    timeout.QuadPart=-10;                           //为什么是负数,后面讲。
    KeInitializeTimer(gTimer);                      //安装计时器。
    KeInitializeDpc(gDPCP,timerDPC,NULL);           //安装Dpc。Dpc这方面我也不懂。等下在后面我们一起学吧。
    KeSetTimerEx(gTimer,timeout,300,gDPCP);         //我的理解是配置计时器。
    
      return STATUS_SUCCESS;
    
    
}

==============================================================================
程序就是上面这样。
我们倒着看吧。先看看KeSetTimerEx(gTimer,timeout,300,gDPCP)这个函数。
WDK上的帮助文件如是说:
The KeSetTimerEx routine sets the absolute or relative interval at which a timer object is to be set to a signaled state, optionally supplies a CustomTimerDpc routine to be executed when that interval expires, and optionally supplies a recurring interval for the timer.

BOOLEAN 
  KeSetTimerEx(
    IN PKTIMER  Timer,
    IN LARGE_INTEGER  DueTime,
    IN LONG  Period OPTIONAL,
    IN PKDPC  Dpc OPTIONAL
    );


我的理解是对计时器的配置。 第一个参数就我们之前用KeInitializeTimer安装的计时器。第二个参数是之前提到的类型是LARGE_INTEGER的timeout,表示计时器的循环响应(“响应”这个词我想了好久,额)时间,如果timeout.QuadPart是正数,就表示绝对响应时间,这里不说开去。这里我们用了timeout.QuadPart=-10,表示相对于系统当前时间的响应时间,也就是10微秒后响应。后面两个参数都是可选的。Period申明一个周期多少微秒,第四个Dpc么依然放到后面讲吧。


再上面一个函数就是安装我提到好几次的Dpc了------KeInitializeDpc(gDPCP,timerDPC,NULL) WDK文档里面说了等于没说,额。

首先谈谈DPC(Deffered Procedure Call)延迟过程调用。
当一段内核代码运行于一个被提升的IRQL上时,其他任何拥有相同或较低IRQL值的中断都不能执行的(在相同CUP上)。然而,如果在很高的IRQL上执行的代码太多,系统总体性能就会下降。时间关键的时间处理会被延迟、并导致更加灾难性的结果。
为了避免这些问题发生,就需要对内核模式代码进行设计,使其尽可能在最低级的IRQL上执行尽量多的代码。延迟过程调用就是这样一个重要策略。
DPC结构允许一个任务从一个高级的IRQL上被触发,但是不被执行。当服务硬件在一个驱动程序上中断时,这种延迟执行是必要的,因为在一个给定任务能被延迟的情况下,没有必要中断低级IRQL代码的执行。


然后是KeInitializeDpc(gDPCP,timerDPC,NULL)这边有3个参数,第一个就是个DPC,第二个是一个PKDEFERRED_ROUTINE结构,可以理解成被延迟的程序,这里就是timerDPC。这个PKDEFERRED_ROUTINE有4个参数……不讲开去了,本来好好的控制键盘LED,浅显易懂,往深了讲还是再开一帖的好。第三个参数为NULL


另外在写这篇文章的时候,偶然查到了liudanking的【讨论】简单的键盘sniff,为何无法正常工作? 
http://bbs.pediy.com/showthread.php?t=113648恩,应该看的和我是同一本书吧,哈,那是个sniff键盘的程序,的确运行不起来,不过当时没在意~~这个问题值得思考,我也再想想吧

上传的附件 ROOTKIT_KEYBOARD.rar