• WIN64内核编程-的基础知识


      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
    ");
    
      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
    ");
    
      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
    ");
    
      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
    ");
    
      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
    ");
    
      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
    ");
    
      return STATUS_SUCCESS;
    
      }
    View Code

      如果你懒得认真看完上面的代码,也没问题,我总结几句: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_MACHINESYSTEMCurrentControlSetControlSession ManagerDebug

      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)

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

  • 相关阅读:
    可复用的WPF或者Silverlight应用程序和组件设计(1)——应用程序级别
    优化网站设计(一):减少请求数
    可复用的WPF或者Silverlight应用程序和组件设计(5)——布局重用
    演讲时经常用到的几个小工具介绍
    谈一谈职业素养
    优化网站设计(三):对资源添加缓存控制
    可复用的WPF或者Silverlight应用程序和组件设计(4)——外观级别
    如何在ViewModel中正确地使用Timer(定时器)
    一把锋利的匕首:利用数据绑定语法糖为Flash应用提供富JS接口
    一句话总结.Net下struct和class内存分配方面的区别
  • 原文地址:https://www.cnblogs.com/csnd/p/11601317.html
Copyright © 2020-2023  润新知