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)
内核编程的基本规矩不是一篇能讲完的,下面几篇会细化讲解,这篇只是个引子。