WIN64内核编程-的基础知识
阅读原文时间:2023年07月10日阅读:2

  WIN64内核编程基础班(作者:胡文亮)   https://www.dbgpro.com/x64driver

  我们先从一份“简历”说起:

  姓名:X86或80x86

  性别:?

  出生年月:1978

  出生地点:美国

  所属公司:主要是INTEL和AMD

  主要历史(摘自维基百科):x86架构于1978年推出的Intel 8086中央处理器中首度出

  现,它是从Intel 8008处理器中发展而来的,而8008则是发展自Intel 4004的。8086

  在三年后为IBM PC所选用,之后x86便成为了个人电脑的标准平台,成为了历来最成功

  的CPU架构。其他公司也有制造x86架构的处理器,计有Cyrix(现为威盛电子所收购)、

  恩益禧集团、IBM、IDT以及Transmeta。Intel以外最成功的制造商为AMD,其早先产品

  Athlon系列处理器的市场份额仅次于Intel Pentium。8086是16位处理器;直到1985

  年32位的80386的开发,这个架构都维持是16位。接着一系列的处理器表示了32位架

  构的细微改进,推出了数种的扩充,直到2003年AMD对于这个架构发展了64位的扩充,

  并命名为AMD64。后来英特尔也推出了与之兼容的处理器,并命名为Intel 64。两者一般

  被统称为x86-64或x64,开创了x86的64位时代。

  在X86的“简历”里,我们摘出一段重要的话:2003年AMD对于这个架构发展了64位

  的扩充,并命名为AMD64。后来英特尔也推出了与之兼容的处理器,并命名为Intel 64。两

  者一般被统称为x86-64或x64,开创了x86的64位时代。也就是说,如果要学习WIN64内

  核编程,就必须拥有2003年以后的CPU!不像学习WIN32内核编程一样,随便一台运行XP

  的奔腾3笔记本也行!但实际情况是,基本上只有2005年以后的CPU才支持X64指令集;

  到2008年之后,CPU才普遍含有X64指令集;到2010年之后,CPU才普遍含有X64指令集

  和支持VT-X技术(没有VT-X技术就无法运行WIN64虚拟机)。

  鉴于中国的实际情况,应该很多人手里还有酷睿2的笔记本。一般来说,T5XXX以下的

  CPU是没有X64指令集的;T7XXX以下的CPU是不支持VT-X的。只有T7XXX以上的CPU,才

  有X64指令集和支持VT-X。而2010年之后的Core i系列的CPU,都有X64指令集和支持

  VT-X了。台式机方面也差不多,CORE 2似乎只有比较高端的E8000或者Q8000以上才有X64

  指令集和支持VT-X技术。AMD则比较厚道,Athlon X2 245之类的低端CPU都有X64指令

  集和支持AMD-V技术(等于是AMD的VT-X技术)。

  总结来说,如果你用的CPU是CORE I系列的,就可以了,如果不是的话,可以用CPUZ检测一下,看看是否支持X64和VT-X。如果发现不支持VT-X的话,看看是不是BIOS里没

  有打开,一般主板的默认设置里,VT-X都是关闭的。

  说完CPU,说说内存。内存经历过两次大跌大涨,现在(2013年11月)又在价格的顶

  峰,真是让人心碎。不过再让人心碎的价格,为了学习技术,大家也只能忍受了。一句话,

  学习WIN64内核编程至少需要8GB的内存,如果要多开虚拟机,推荐16GB。否则在双机调

  试时卡死(鼠标移动变成了“飘动”的),会让人非常愤怒。

  配置好驱动测试环境后,就可以正式编写驱动了。市面上讲解驱动开发的书籍汗牛充栋,

  但讲得较为太复杂,让初学者不好理解。本文从一个简单的hello,world驱动(驱动模板)

  讲起,力求讲解得简单明了,让大家好理解。

  本文主角:

  1.DbgView。DbgView是查看程序调试输出的工具,由美国高富帅Mark Russinovich编写

  (不得不说,此人长得帅,编程技术又牛,让多少男人羡慕妒忌,让多少女人一见倾心)。

  下载地址:http://technet.microsoft.com/en-us/sysinternals/bb896647.aspx

  2.KmdMgr。KmdMgr是一个由俄国人编写的驱动加载工具。比起国内那些乱七八糟的驱动加

  载工具,它的特点是可以与驱动进行通信(虽然无法设置I/O缓冲区)。下载地址:

  https://www.assembla.com/code/L2h/subversion/nodes/LowLevel/KmdManager.exe?_for

  mat=raw&rev=1

  3.WIN64AST。作者自行开发的64位ARK类工具。在本章中用来查看驱动是否加载成功。在

  后续章节还有其他的用途。下载地址:www.win64ast.com。

  4.WIN64UDL。作者自行开发的驱动加载工具,能在正常模式下加载没有签名的驱动。因为这

  个工具,被人举报滥用签名,最终导致价值15000人民币的数字签名被吊销。下载地址:

  http://www.m5home.com/bbs/thread-7845-1-1.html

  编写驱动:

  以下是一个我写的WIN64驱动模板(代码中已经加了详细的注释,完整工程文件可以在

  论坛上下载):

  //【0】包含的头文件,可以加入系统或自己定义的头文件

  #include<ntddk.h>

  #include<windef.h>

  #include<stdlib.h>

  //【1】定义符号链接,一般来说修改为驱动的名字即可

  #define DEVICE_NAME L"\\Device\\KrnlHW64"

  #define LINK_NAME L"\\DosDevices\\KrnlHW64"

  #define LINK_GLOBAL_NAME L"\\DosDevices\\Global\\KrnlHW64"

  //【2】定义驱动功能号和名字,提供接口给应用程序调用

  #define IOCTL_IO_TEST CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,

  METHOD_BUFFERED,FILE_ANY_ACCESS)

  #define IOCTL_SAY_HELLO CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,

  METHOD_BUFFERED,FILE_ANY_ACCESS)

  //【3】驱动卸载的处理例程

  VOID DriverUnload(PDRIVER_OBJECT pDriverObj)

  {

  UNICODE_STRING strLink;

  DbgPrint("[KrnlHW64]DriverUnload\n");

  RtlInitUnicodeString(&strLink,LINK_NAME);

  IoDeleteSymbolicLink(&strLink);

  WIN64内核编程基础班(作者:胡文亮;QQ:1923208126)

  IoDeleteDevice(pDriverObj->DeviceObject);

  }

  //【4】IRP_MJ_CREATE对应的处理例程,一般不用管它

  NTSTATUS DispatchCreate(PDEVICE_OBJECT pDevObj,PIRP pIrp)

  {

  DbgPrint("[KrnlHW64]DispatchCreate\n");

  pIrp->IoStatus.Status=STATUS_SUCCESS;

  pIrp->IoStatus.Information=0;

  IoCompleteRequest(pIrp,IO_NO_INCREMENT);

  return STATUS_SUCCESS;

  }

  //【5】IRP_MJ_CLOSE对应的处理例程,一般不用管它

  NTSTATUS DispatchClose(PDEVICE_OBJECT pDevObj,PIRP pIrp)

  {

  DbgPrint("[KrnlHW64]DispatchClose\n");

  pIrp->IoStatus.Status=STATUS_SUCCESS;

  pIrp->IoStatus.Information=0;

  IoCompleteRequest(pIrp,IO_NO_INCREMENT);

  return STATUS_SUCCESS;

  }

  //【6】IRP_MJ_DEVICE_CONTROL对应的处理例程,驱动最重要的函数之一,一般走正常途径调

  用驱动功能的程序,都会经过这个函数

  NTSTATUS DispatchIoctl(PDEVICE_OBJECT pDevObj,PIRP pIrp)

  {

  NTSTATUS status=STATUS_INVALID_DEVICE_REQUEST;

  PIO_STACK_LOCATION pIrpStack;

  ULONG uIoControlCode;

  PVOID pIoBuffer;

  ULONG uInSize;

  ULONG uOutSize;

  DbgPrint("[KrnlHW64]DispatchIoctl\n");

  pIrpStack=IoGetCurrentIrpStackLocation(pIrp);

  //控制码

  uIoControlCode=pIrpStack->Parameters.DeviceIoControl.IoControlCode;

  //输入输出缓冲区

  pIoBuffer=pIrp->AssociatedIrp.SystemBuffer;

  //输入区域大小

  uInSize=pIrpStack->Parameters.DeviceIoControl.InputBufferLength;

  //输出区域大小

  uOutSize=pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;

  switch(uIoControlCode)

  WIN64内核编程基础班(作者:胡文亮;QQ:1923208126)

  {

  //在这里加入接口

  case IOCTL_IO_TEST:

  {

  DWORD dw=0;

  //获得输入的内容

  memcpy(&dw,pIoBuffer,sizeof(DWORD));

  //使用输入的内容

  dw++;

  //输出处理的结果

  memcpy(pIoBuffer,&dw,sizeof(DWORD));

  //处理成功,返回非STATUS_SUCCESS会让DeviveIoControl返回失败

  status=STATUS_SUCCESS;

  break;

  }

  case IOCTL_SAY_HELLO:

  {

  DbgPrint("[KrnlHW64]IOCTL_SAY_HELLO\n");

  status=STATUS_SUCCESS;

  break;

  }

  }

  if(status==STATUS_SUCCESS)

  pIrp->IoStatus.Information=uOutSize;

  else

  pIrp->IoStatus.Information=0;

  pIrp->IoStatus.Status=status;

  IoCompleteRequest(pIrp,IO_NO_INCREMENT);

  return status;

  }

  //【7】驱动加载的处理例程,里面进行了驱动的初始化工作

  NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj,PUNICODE_STRING

  pRegistryString)

  {

  NTSTATUS status=STATUS_SUCCESS;

  UNICODE_STRING ustrLinkName;

  UNICODE_STRING ustrDevName;

  PDEVICE_OBJECT pDevObj;

  //初始化驱动例程

  pDriverObj->MajorFunction[IRP_MJ_CREATE]=DispatchCreate;

  pDriverObj->MajorFunction[IRP_MJ_CLOSE]=DispatchClose;

  pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL]=DispatchIoctl;

  pDriverObj->DriverUnload=DriverUnload;

  WIN64内核编程基础班(作者:胡文亮;QQ:1923208126)

  //创建驱动设备

  RtlInitUnicodeString(&ustrDevName,DEVICE_NAME);

  status=IoCreateDevice(pDriverObj,0,&ustrDevName,FILE_DEVICE_UNKNOWN,

  0,FALSE,&pDevObj);

  if(!NT_SUCCESS(status))return status;

  if(IoIsWdmVersionAvailable(1,0x10))

  RtlInitUnicodeString(&ustrLinkName,LINK_GLOBAL_NAME);

  else

  RtlInitUnicodeString(&ustrLinkName,LINK_NAME);

  //创建符号链接

  status=IoCreateSymbolicLink(&ustrLinkName,&ustrDevName);

  if(!NT_SUCCESS(status))

  {

  IoDeleteDevice(pDevObj);

  return status;

  }

  //走到这里驱动实际上已经初始化完成,下面添加的是功能初始化的代码

  DbgPrint("[KrnlHW64]DriverEntry\n");

  return STATUS_SUCCESS;

  }

  如果你懒得认真看完上面的代码,也没问题,我总结几句:1.DriverEntry就是驱动的

  main函数,驱动加载后会从DriverEntry开始执行。2.驱动类似DLL,可以提供接口给应用

  程序调用,不过以导出函数的方式,而是用一套专门的通信函数DeviceIoControl。关于应

  用程序与驱动程序通信,后面会讲。

  编译驱动:

  1.打开『x64 Free Build Environment』:

  2.切换到源码目录(假设源码目录是:z:\sys),并输入BUILD编译:

  WIN64内核编程基础班(作者:胡文亮;QQ:1923208126)

  3.如果看到『1 executable built』字眼,则证明编译成功。

  4.驱动的编译跟目录下的source文件有关系,比如本例中,它的内容如下(注意不要手贱

  把空行去掉了,否则可能会导致无法编译):

  TARGETNAME=KrnlHW64<-驱动的文件的名称,一般来说修改这个就行了

  TARGETTYPE=DRIVER<-编译的类型

  TARGETPATH=obj

  INCLUDES=.\

  SOURCES=MyDriver.c<-多个C文件时,把所有C文件的名称分成多行写

  测试驱动前的准备:

  1.以管理员权限运行DBGVIEW。

  2.把HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug

  Print Filter的Default值修改为ffffffff

  3.打开DBGVIEW并把以下选项全部勾上:

  标准的驱动测试方法:

  1.打开虚拟机,进入双机调试的环境(忘记了就参考上节课的内容)。

  WIN64内核编程基础班(作者:胡文亮;QQ:1923208126)

  2.运行KmdMgr.exe,把SYS拖动到文本框里。

  3.点击“Register”和“Run”按钮,看看输出是否提示成功。如果成功会有类似的输出:

  4.运行WIN64AST,点击内核模块,查看驱动是否已经存在于内核里了:

  WIN64内核编程基础班(作者:胡文亮;QQ:1923208126)

  5.在CODE处输入222004(为什么是222004而不是801?这个后面会讲到,这里先卖一个关

  子。但这个数值可以使用calc_ctl_code.exe算出来,既输入801,可以输出222004),点

  击“I/O Control”按钮,如果成功会有类似的输出:

  6.点击“Unregister”和“Stop”按钮,如果成功会有类似的输出:

  WIN64内核编程基础班(作者:胡文亮;QQ:1923208126)

  很显然,用标准方法测试一个驱动是很麻烦且很耗时的。双机调试非常占用系统资源,

  虽然我的电脑配置较好(2600K+16GB内存),但是在操作虚拟机时,仍然感到了明显的卡顿。

  下面介绍一种用特殊工具测试驱动的方法,无需双机调试,甚至无需使用虚拟机。

  用WIN64UDL测试驱动:

  1.运行WIN64UDL。

  2.把驱动文件拖进WIN64UDL里,然后按下ENTER加载。

  3.再按一次ENTER卸载驱动。

  最后,再补充一种非常麻烦的方法,此方法也算是标准方法之一,适用于没有虚拟机或

  无法进行双机调试的时候。由于非常麻烦,所以不推荐使用。说实话,谁用此方法测试驱动,

  绝对是脑门被驴踢了。

  1.开启测试模式。管理员权限运行CMD,输入:bcdedit-set testsigning on。

  WIN64内核编程基础班(作者:胡文亮;QQ:1923208126)

  2.重启计算机

  3.用dseo13b(下载地址:http://files.ngohq.com/ngo/dseo/dseo13b.exe)给驱动程序添

  加测试签名。方法很简单,运行deso13b,一路NEXT,当出现这个对话框时,选择“Sign a

  System File”再点NEXT:

  4.用任意工具加载驱动。

----------------------------

  说完基本环境配置,这篇就稍微轻松点了,可以听听我吹牛侃大山,谈谈WIN64内核编

  程的基本规则。在WIN32环境下,大家都能乱来,随便加载各种驱动,进行各种挂钩和DKOM,

  各种穷凶极恶的手段粉墨登场。把好端端的WINDOWS弄得连七八糟,严重影响了系统安全。

  虽说为编程爱好者提供了表演的舞台,但是苦了那些把电脑当工具的人。于是那段时间,

  “LINUX和MAC OS比WINDOWS安全可靠”的谣言四起(似乎也不是谣言),大有把WINDOWS

  和不安全划上等号之势。

  为此,微软很生气,后果很严重。从WINDOWS 2003 X64开始,微软开始对WIN64系统

  增加限制,增强系统安全。总体来说有两条,一是KPP(Kernel Patch Protection,内核补

  丁保护),二是DSE(Driver Signature Enforcement,驱动签名强制)。WINDOWS 2003 X64

  只有KPP,从VISTA开始有了DSE。KPP利用PatchGuard技术检查内核有没有被“打补丁”

  (不仅检查内核函数有没有被HOOK,也包括一些关键的内核结构体有没有被修改,比如进

  程链表PsActiveProcessLinks有没有被摘链),如果发现被“打补丁”,则直接引发0x109

  蓝屏(CRITICAL_STRUCTURE_CORRUPTION,直译为关键结构损毁)。DSE则是拒绝加载不包含

  正确签名的驱动(包括伪造签名和测试签名)。多说一句,总有人把KPP把PatchGuard划等

  号,其实二者是不等的。KPP是机制,PatchGuard是实现。就好比CIA是机构,CIA的特工

  才是一系列“黑色行动”的执行者。

  说完正儿八经的,说点通地气的话。实际上KPP和DSE并非铁板一块,二是各有漏洞可

  钻的。KPP保护不了内核所有的部分,只保护了几个驱动:NTOSKRNL.EXE、HAL.DLL、CI.DLL、

  NDIS.SYS等(当然,NTOS部分包括了IDT、GDT、MSR等)。对一些较为上层的驱动,比如

  FAT32和NTFS作IRP HOOK,PG是不管的。而DSE则在某些条件下不启动,比如在PE环境

  下;或者说有些时候管得不严格,比如在测试模式下,允许含有测试签名的驱动加载。总结

  一句:进行WIN64内核编程的时候,别想用API HOOK解决问题;当发布含有WIN64驱动的

  时候,记得给“证书签发机构”交保护费(购买正规数字签名)。不过,这两项限制让很多

  黑客乃至安全公司大为不满,各种过KPP和DSE的方法层出不穷。目前,VISTA、WIN7、WIN8、

  WIN8.1的KPP和DSE已全部被攻破。

  编程的时候,大家基本都是需要使用API的。在RING3下使用WINAPI,在RING0下则

  使用内核API。特别注意的是,内核编程是无法使用WINAPI的(当然,也有特殊的方法调

  用,不过非标准方法,这里略过不提)。什么叫做内核API呢,就是虚拟地址位于内核空间

  的API。不管是不是微软的内核模块,也不管导出没导出,只要知道地址和每个参数的含义,

  就能调用。不过,我们写程序大多时候都是使用微软模块(NTOSKRNL、HAL等)导出的API。

  例如:ZwOpenProcess,IoCreateFile等。

  内核编程用内核API,而自然也有内核结构体。其实“内核结构体”这个说法不太妥当,

  因为结构体是不分场合甚至不分系统的。但这么说大家也能理解是什么意思,就是内核编程

  中常用的结构体。这种结构体又分为两种,一种是“万年不变”的,一种是每个系统都不同

  的。“万年不变”的结构体通常能在MSDN上查到,比如CLIENT_ID、IO_STATUS_BLOCK;每个

  系统都不同的结构体通常在MSDN上查不到,但是存在于符号文件里,比如EPROCESS、ETHREAD。

  我们编程的时候,尽量只使用“万年不变”的结构体,不使用每个系统都不同的结构体。当

  非要使用不可的时候,必须根据系统版本定义制定成员的偏移量。如果发现未知的系统版

  本,则提示并退出。如果不这样做,等着BSOD吧。

  WIN64内核编程基础班(作者:胡文亮;QQ:1923208126)

  内核编程的基本规矩不是一篇能讲完的,下面几篇会细化讲解,这篇只是个引子。