环境配置
开发环境
在开发驱动程序之前,我们需要配置好开发环境, 首先安装好VS IDE(这里自己选择版本),其次因为我们需要开发驱动程序所以需要安装WDK(WDK下载地址:以前的 WDK 版本和其他下载 - Windows drivers | Microsoft Learn),在我们安装WDK时候需要注意其版本应与SDK的版本一致。我们可以通过控制面板-程序卸载,找到当前VS IDE安装的SDK版本,如下所示,我的系统上SDK的版本是17763:
接着你需要在WDK的下载地址中选择对应系统的安装包,由于我当前是Windows 10 21H2版本,但是页面中并没有对应的版本选择,所以我把所有Windows10的WDK安装包都下载下来了:
安装包依次打开,就找到了与SDK对应版本的WDK安装包,接着按步骤安装即可:
项目配置
安装好开发环境之后我们打开VS2017,新建项目并选择VC++下的Wimdows Drivers,创建Empty WDM Driver项目:
创建完项目之后,进入到项目属性,按如下图所示进行配置:
至此,我们所有的配置工作就搞定了。
Hello Driver
驱动程序代码
当我们配置好驱动开发环境及项目之后,可以创建代码文件,但需要注意的是我们在学习阶段时候建议代码文件使用C语言,这样编译器就不会进行太多的优化,也便于我们调试:
接着我们将项目代码的警告配置修改一下:
然后我们开始写自己的第一个驱动程序代码,最基本的格式就是包含ntddk.h头文件,以及写好驱动程序入口函数DriverEntry:
#include <ntddk.h>
// 必须要包含的头文件
// 自定义的驱动程序卸载函数
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
DbgPrint(
"Bye. \n"
);
}
// 驱动程序入口函数
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgPrint(
"Hello Driver. \n"
);
// 设置一个卸载函数,当驱动停止时触发
DriverObject->DriverUnload = DriverUnload;
return
STATUS_SUCCESS;
}
最后我们编译代码,在项目的Debug目录下找到.sys文件,这就是我们编译出来的驱动程序:
使用驱动程序
将编译好的驱动程序、KdmManager、DebugView拖到XP虚拟机中(在虚拟机中去调试驱动比实体机中方便),使用KdmManager加载驱动,按下图所示步骤依次进行,于此同时也要打开DebugView,同时开启监听内核的选项:
当我们依次进行注册、运行、停止、卸载,可以清晰的在DebugView中观察到我们驱动程序所输出的内容,并且我们也知道了函数执行的流程:当点击Run按钮时进入驱动程序入口函数,当点击Stop按钮时进入自定义的驱动程序卸载函数:
调试驱动程序
调试环境
调式驱动程序不像应用程序一样简单(直接在OD之类的调试工具中下断点),要想调试驱动程序就需要使用双机调试,在虚拟机中运行驱动程序,在实体机上使用0环调试器(也就是WinDbg)进行调试。
之前我们进行双机调试配置时,需要手动修改文件然后重启,这个步骤相对来说比较繁琐,我们可以通过VirtualKD(开源项目地址:GitHub - 4d61726b/VirtualKD-Redux: VirtualKD-Redux - A revival and modernization of VirtualKD)程序来简化整个步骤。
如下图所示就整个程序的目录结构,target32/64目录下的文件就是根据虚拟机操作系统的类型选择并复制进虚拟机的,vmmon64.exe就是主程序,直接实体机打开:
我的虚拟机系统是Windows XP 32位,所以我将target32目录放入虚拟机中,并且按如下步骤操作:
双击运行vminstall.exe-点击Install按钮-点击“是”-系统重启-选择VKD-Redux启动
于此同时打开vmmon64.exe,选择好对应的Windbg程序路径(一般情况下程序会寻找默认Windbg程序路径):
最后进入调试模式,管道连接上会自动打开Windbg程序:
PDB文件
无论是OD还是Windbg,都需要符号文件的加持才能在逆向时获得到更多的信息,例如你用OD去调试程序时经常可以看到一些Windows API是以函数名称的形式存在于反汇编里的,这就是符号文件起的作用。
PDB文件也就是符号文件,我们在编译程序时候,只要不取消调试信息的输出,默认情况下是可以在编译输出的目录中找到所编译程序的PDB文件的,例如我们上文中编译的驱动程序的输出目录下就有对应的PDB文件。
我们要想在Windbg中调试驱动程序就需要这个PDB文件,但是在这之前我们需要先在驱动代码内写上内联汇编,用于断点:
编译之后将文件放入虚拟机,以及在Windbg重载符号文件,按如下图操作填入PDB文件所在路径即可:
接着在虚拟机中用KdmManager加载并运行驱动程序,运行时(KdmManager软件中点击Run按钮)就会在Windbg中断下来,如下图在Windbg中新建了一个窗口(左边的窗口)展示当前驱动程序断点的位置,并且都是以源代码形式展示给我们的,这种效果与我们在调试应用程序时用VS是一样的:
基础内容
内核API的使用
在应用层编程时我们可以通过包含Windows.h这个头文件来使用Windows提供的API,但是在内核编程时我们不可以使用应用层的API,而要使用内核专用的API,所以我们需要包含的头文件就变成了ntddk.h(需要安装好WDK)。
与应用程序开发一样,我们想要去了解某个内核API的信息,也需要查阅文档,老版本的WDK安装之后会自带文档信息,较新一点的WDK不会自带,我们需要去官网查看:Windows 驱动程序工具包 (WDK) 的 API 参考文档 - Windows drivers | Microsoft Learn
未导出函数的使用
WDK文档里只包含了导出的函数信息,对于未导出的函数是查阅不到相关资料的(我们也可以将其称之为未文档化函数)。如果我们想使用未导出的函数,需要定义一个函数指针,并且为函数指针提供正确的函数地址就可以使用了。未导出函数的地址可以通过特征码搜索、解析内核的PDB文件来找到。
基本数据类型的使用
在内核编程的时候,必须要遵守WDK的编码习惯,例如无符号类型不要在类型前加上关键词unsigned,而是要遵循WDK自己的类型:
WDK的写法 | 表达的意思 |
ULONG | unsigned long |
UCHAR | unsigned char |
UINT | unsigned int |
VOID | void |
PULONG | unsigned long * |
PUCHAR | unsigned char * |
PUNIT | unsigned int * |
PVOID | void * |
如果你按后者去写,在不同的平台上移植代码可能会导致数据宽度不同,要修改到相同宽度就需要修改代码,而按WDK的写法就可以减少这种不必要的情况。
返回值
大部分内核函数的返回值都是NTSTATUS类型,它本质是一个宏,里面包含的类型有很多,如下三个就是常见的返回值:
宏名称 | 实际值 | 含义 |
STATUS_SUCCESS | 0x00000000 | 成功 |
STATUS_INVALID_PARAMETER | 0xC000000D | 参数无效 |
STATUS_BUFFER_OVERFLOW | 0x80000005 | 缓冲区长度不够 |
当你调用的内核函数返回结果不是STATUS_SUCCESS,那就说明函数在执行时遇到了问题,具体的问题可以根据返回值在ntstatus.h头文件中去寻找:
通过宏的名称也能知道个大概,如果仍然不知道问题的含义,可以在微软的WDK文档中去搜索相关宏名称。
内核中的异常处理
在内核中一个小小的错误就可能导致蓝屏,例如我们去读写一个无效的内存地址。为了让自己的内核程序更加健壮,在编写内核程序时要使用到异常处理。
在Windows下提供了结构化异常处理机制,编译器普遍都支持,如下就是代码使用方法:
__try
{
// 填入可能要出错的代码
}
__except (filter_value)
{
// 填入出错后要执行的代码
}
如上示例中的filter_value,就是当内核程序出现异常时决定程序如何执行的,一般有这三种情况:
宏名称 | 实际值 | 含义 |
EXCEPTION_EXECUTE_HANDLER | 1 | 进 |