驱动源码如下(每个函数及重要部份都标上了注释)

代码:
;@echo  off
;goto  make
;功能:驱动实现模拟键盘按键
;说明:模拟键盘按键就需要访问IO端口 但是系统默认是不允许访问的,所以我们利用驱动修改TSS中的IOPM(IO许可位图),允许我们的主程序访问IO端口(直接用in、out访问).
;by: 看雪--ericzw  QQ--巴丫丫 37261550
    .586
    .model  flat,stdcall
    option  casemap:none
include  \masm32\include\w2k\ntddk.inc;驱动开发必备的头文件
include  \masm32\include\w2k\ntstatus.inc;一些状态标识符
include  \masm32\include\w2k\ntoskrnl.inc;将用到ntoskrnl很多未公开的函数
includelib  \masm32\lib\w2k\ntoskrnl.lib
include  \masm32\macros\strings.mac
;字符宏 声明字符串
$s MACRO txt:REQ
  local d,sn
  sn TEXTEQU @CurSeg
   .const
    d db txt,0
  @CurSeg ENDS
  sn SEGMENT
  EXITM <offset d>
ENDM
    .data
szUnicode  WORD   'P','r','o','c','e','s','s','I','d',0
szUs  UNICODE_STRING  <sizeof szUnicode,sizeof  szUnicode,offset szUnicode>;这2句可以使用$COUNTED_UNICODE_STRING('ProcessId',4)宏来代替 
    .code
;驱动程序入口函数 每次驱动加截都将调用此函数
DriverEntry  proc  _lpDeriverObject:PDRIVER_OBJECT,_lpRegisterString:PUNICODE_STRING;1参:当前驱动对象指针,由系统初始化这个驱动程序时分配的DRIVER_OBJECT结构 2参:指向定长的Unicode字符串,记录该驱动在注册表中的键路径
  local  @oa:OBJECT_ATTRIBUTES
  local  @hKey;注册表句柄
  local  @kvpi:KEY_VALUE_PARTIAL_INFORMATION
  local  @lp;分配的虚拟内存指针
  local  @kProcess:PVOID;KPROCESS对象指针
  ;int  3;需要调试此驱动的时候 把注释去掉,在softice里面开启捕捉int 3指令(使用“I3Here on“即可)
  
  ;初始化OBJECT_ATTRIBUTES供ZwOpenKey使用(使用这几个注册表函数就是为了得到调用程序的进程ID 我们保存在注册表里面)
  lea  esi,@oa
  assume  esi:ptr OBJECT_ATTRIBUTES
  mov  [esi]._Length,sizeof OBJECT_ATTRIBUTES;结构大小 结构字段原来是Length,但汇编Length是保留字,所以定义的时候在它前面加了'_'符号
  mov  [esi].RootDirectory,NULL;根目录路径 为NULL则结构的ObjectName必须指向一个完整路径对象名称 不为NULL则ObjectName指定对象名称即可
  push  _lpRegisterString;驱动在注册表中的键路径
  pop  [esi].ObjectName;指向Unicode字符串 指示对象是由哪个句柄打开
  mov  [esi].Attributes,NULL;属性
  mov  [esi].SecurityDescriptor,NULL;指示对象安全描述 为NULL则为默认设置
  mov  [esi].SecurityQualityOfService,NULL;一般为NULL
  assume  esi:nothing
  invoke  ZwOpenKey,addr @hKey,KEY_READ,esi;打开现有注册表 参1:函数在此返回取得的注册表句柄 参2:操作权限 参3:指向已初始化的OBJECT_ATTRIBUTES
  .if  eax==STATUS_SUCCESS
    push  eax
    invoke  ZwQueryValueKey,@hKey,addr szUs,KeyValuePartialInformation,addr @kvpi,sizeof @kvpi,esp;取注册表中的键值 参1:打开的句柄 参2:定长的UNICODE_STRING字符串注册表中键的名字 参3:需要取得注册表键的值使用KeyValuePartialInformation 参4:指针指向结构KEY_VALUE_PARTIAL_INFORMATON(最终取得的值返回在结构中字段Data里面) 参5:结构大小 参6:在此返回取得的数据大小
    pop  ecx
    .if  eax==STATUS_SUCCESS && ecx!=0;函数成功 同时取得的数据不为0(ecx就是上个函数堆栈上的值pop弹出的)
      invoke  MmAllocateNonCachedMemory,8192;我们总共有8192个许可位图 所以分配1个不被cache的虚拟内存
      .if  eax
        mov  @lp,eax
        lea  ecx,@kvpi
        invoke  PsLookupProcessByProcessId,DWORD ptr (KEY_VALUE_PARTIAL_INFORMATION PTR [ecx]).Data,addr @kProcess;利用进程ID取得进程对象  参1:进程id 参2:在此处返回取得的进程对象  使用此函数会使进程对象的引用计数加1
        .if  eax==STATUS_SUCCESS
          invoke  Ke386QueryIoAccessMap,1,@lp;从TSS中取得许可位图 参1:为0将所有位置1 为1普通的拷贝  参2:虚拟内存指针 许可位图的数据返回到此指针指向的虚拟内存
          .if  al;成功al非0
            ;把60 64端口都置位 注:由于IO许可位图使用的是bit位 我们正常表示都是字节 所以他们需要除8
            mov  esi,@lp
            add  esi,60h / 8 
            mov  eax,[esi]
            btr  eax, 60h mod 8;btr把指定的位清0 也就是把60h端口位设置成0 允许访问60h端口
            mov  [esi],eax
            
            mov  esi,@lp
            add  esi,64h / 8
            mov  eax,[esi]
            btr  eax,64h mod 8
            mov  [esi],eax
            invoke  Ke386SetIoAccessMap,1,@lp;拷贝新的IOPM(IO许可位图)到TSS中 参1:必须为1 参2:指向不低于2000h的虚拟内存
            .if  al;成功al非0
              invoke  Ke386IoSetAccessProcess,@kProcess,1;上面的数据拷贝进TSS后 必须使TSS中的偏移定位到新的IOPM  参1:KPROCESS进程对象  参2:为0表示禁址对I/O端口进行存取将IOPM的偏移指到TSS段外面 为1表示允许存取I/O端口
              .if  al;成功al非0
                invoke  DbgPrint,$s('driver ok...')
              .endif
            .endif
          .endif
          invoke  ObDereferenceObject,@kProcess;将对象引用计数减1
        .endif
        invoke  MmFreeNonCachedMemory,@lp,8192;释放内存
      .endif
      invoke  ZwClose,@hKey;释放句柄
    .endif
  .endif
  mov  eax,STATUS_DEVICE_CONFIGURATION_ERROR;入口函数返回值,这里将使系统返回设备配置错误,系统将直接把驱动从内存中清除
  ret
DriverEntry  endp
end    DriverEntry

:make

set drv=myDriver

\masm32\bin\ml /nologo /c /coff %drv%.bat
\masm32\bin\link /nologo /driver /base:0x10000 /align:32 /out:%drv%.sys /subsystem:native %drv%.obj

del %drv%.obj

echo.
pause
主程序源码
代码:
;@echo  off
;goto  make
;功能:驱动实现模拟键盘按键
;说明:模拟键盘按键就需要访问IO端口 但是系统默认是不允许访问的,所以我们利用驱动修改TSS中的IOPM(IO许可位图),允许我们的主程序访问IO端口(直接用in、out访问).
;by: 看雪--ericzw  QQ--巴丫丫 37261550
    .586
    .model  flat,stdcall
    option  casemap:none
include  \masm32\include\windows.inc
include  \masm32\include\user32.inc
include  \masm32\include\kernel32.inc
includelib  \masm32\lib\user32.lib
includelib  \masm32\lib\kernel32.lib

include  \masm32\include\advapi32.inc;操作注册表使用
includelib  \masm32\lib\advapi32.lib
;字符宏 声明字符串
$s MACRO txt:REQ
  local d,sn
  sn TEXTEQU @CurSeg
   .const
    d db txt,0
  @CurSeg ENDS
  sn SEGMENT
  EXITM <offset d>
ENDM
    .data
hScm  dword  ?;SCM句柄
hService  dword  ?;服务句柄
szFullName  byte  MAX_PATH  DUP  (?);驱动全路径
hKey  dword  ?;注册表句柄
dwProcessId  dword  ?;当前进程ID
szProcessId  byte  'ProcessId',0
f_flag  dword  0;标志 成功启动了驱动程序后为非0 
    .code
;访问端口实现模拟键盘按键 
SetKey    proc    _Lparamkey;
        @Lp1:;无论向0x60,还是0x64写东西前都要等状态寄存器OBF变0
        in    al,64h
        and  al,10b
        jnz  @Lp1
        ;向$64端口写命令
        mov  al,0D2h;写键盘输出缓存命令
        out  64h,al
        
        @Lp2:;无论向0x60,还是0x64写东西前都要等状态寄存器OBF变0
        in    al,64h
        and   al,10b
        jnz  @Lp2
        ;向$60端口写参数
        mov  ebx,_Lparamkey
        mov  al,bl
        out  60h,al
        ret
SetKey    endp
;程序入口
Start  proc
  invoke  OpenSCManager,NULL,NULL,SC_MANAGER_ALL_ACCESS;建立到SCM的连接
  .if  eax
    mov  hScm,eax
    push  eax
    invoke  GetFullPathName,$s('myDriver.sys'),MAX_PATH,addr szFullName,esp;取得当前文件夹下的驱动完整路径
    pop  eax
    invoke  CreateService,hScm,$s('myDriverReg'),$s('myDriverNameReg'),SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,\
        SERVICE_DEMAND_START,SERVICE_ERROR_IGNORE,addr szFullName,NULL,NULL,NULL,NULL,NULL;将驱动添加到服务数据库中并创建对应的注册表键
    .if  eax
      mov  hService,eax
      invoke  RegOpenKeyEx,HKEY_LOCAL_MACHINE,$s("SYSTEM\\CurrentControlSet\\Services\\myDriverReg"),0,KEY_ALL_ACCESS,addr hKey;打开注册表 注意:每次把驱动添加到服务数据中时,程序会同时在注册表"SYSTEM\\CurrentControlSet\\Services\\"下面创建一个以CreateService第二个参数为键名的新项.
      ;我们本次的工作就是往上面的注册表路径下面添加一个新的名为"ProcessId"的键值,里面的数据是当前进程的进程ID,供我们的驱动读取,来实现我们的操作。
      .if  eax==ERROR_SUCCESS
        invoke  GetCurrentProcessId;取得当前进程ID
        mov  dwProcessId,eax
        invoke  RegSetValueEx,hKey,addr szProcessId,NULL,REG_DWORD,addr dwProcessId,sizeof DWORD;向注册表中写入键值 写入我们的进程ID
        .if  eax==ERROR_SUCCESS
          invoke  StartService,hService,NULL,NULL;启动驱动程序,这个函数运行后直到驱动程序入口函数运行完后才返回
          inc  f_flag;设置为非0 下面将访问端口
          invoke  RegDeleteKey,hKey,addr szProcessId;清除我们刚才清加的进程ID项 (可有可无 运行DeleteService后整个驱动键都会被清除
        .else
          invoke  MessageBox,NULL,$s('写入进程ID失败'),NULL,MB_OK  
        .endif
        invoke  RegCloseKey,hKey;释放句柄
      .else
        invoke  MessageBox,NULL,$s('打开注册表失败'),NULL,MB_OK  
      .endif
      invoke  DeleteService,hService;将驱动从SCM数据库中删除 严格的说这个函数并不真正将服务删除,它将服务做了一个删除标志,只有服务已经停止,并且服务句柄被关闭后SCM才真正将服务删除    
      invoke  CloseServiceHandle,hService;关闭句柄
    .else
      invoke  MessageBox,NULL,$s('添加驱动到服务数据库失败'),NULL,MB_OK
    .endif
    invoke  CloseServiceHandle,hScm;关闭句柄
  .else
    invoke  MessageBox,NULL,$s('打开scm失败'),NULL,MB_OK
  .endif
  ;如果驱动正常加载,则我们连续按5次'A'键
  .if  f_flag
    invoke  MessageBox,NULL,$s("安装驱动成功,点击'确定'将模拟键盘按5次'A'键"),$s("成功"),MB_OK
    mov  ecx,5
    @@:  
    push  ecx
    invoke  MapVirtualKey,VK_A,0;把A键的虚拟键码翻译为扫描码
    invoke  SetKey,eax;访问IO实现模拟键盘按A键
    invoke  Sleep,1000
    pop  ecx
    loop  @B 
  .endif
  invoke  ExitProcess,-1
  ret
Start  endp
end  Start
:make
set  exe=StartD
\masm32\bin\ml /nologo /c /coff %exe%.bat
\masm32\bin\link /nologo /subsystem:windows %exe%.obj
del  %exe%.obj
echo.
pause
解释已经尽量写的很清楚,主程序的解释少一些,不懂的函数可以查msdn.
上传的附件 模拟按键.rar