Windows驱动开发入门

开始

到微软官网上下载并安装SDK和WDK,这俩内部版本号必须得对上,且安装时必须保持默认安装路径不变,新建Empty WDM Driver项目。当编译时出现Spectre相关错误,在项目设置中把这东西关掉。如果还是不能编译通过,把项目目录下的.inf文件从工程中移除。当某些警告被当作错误处理时,降低警告等级并关闭警告视为错误。

如果你的项目设置中没有链接器等选项(像我这样),恭喜你需要重装系统,或搞个虚拟机装吧!

WinDBG调试

Windows 7

别真机搞,弄个虚拟机,先在虚拟机设置中把打印机删除,再添加串行端口,使用名字“\\\.\\pipe\com_1”,并把轮询时中断CPU勾选上。当虚拟机为Windows 7时运行msconfig,引导中选择高级选项并勾选调试。Windows 10中要在设置的安全和更新选项卡的针对开发人员选项中开启开发人员模式。最后运行命令:

1
bcdedit /set testsigning on

打开WinDBG Preview并Attach to Kernel,选项选COM,勾选Pipe和Reconnect,Port输入“\\\.\\pipe\\com_1”。

Windows 10

关机然后删掉打印机,新建串口,方法同上。Windows 10中要在设置的安全和更新选项卡的针对开发人员选项中开启开发人员模式。运行命令:

1
2
3
4
5
6
bcdedit /set testsigning on
bcdedit /set “{current}” bootmenupolicy Legacy
bcdedit /dbgsettings SERIAL DEBUGPORT:1 BAUDRATE:115200
bcdedit /copy “{current}” /d “Debug”
bcdedit /debug “{<新建的启动配置的标识符>}” on
bcdedit /enum ;看看Windows启动加载器的debug选项是不是Yes

重启自动进入Windows启动管理器,进入Debug,即可被WinDBG附加。

加载驱动

Windows 7 x64

兄弟,别用。这个系统的驱动加载与签名校验都有大病。

Windows 10

写个驱动并编译试试:

1
2
3
4
5
6
7
8
9
10
11
12
#include<ntddk.h>
VOID DriverUnload(PDRIVER_OBJECT pDriver) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[%d] Unloading...\r\n", __LINE__);
return;
};
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING RegistryPath) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "RegistryPath = %S\r\n", RegistryPath->Buffer);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[%d] Loading...\r\n", __LINE__);
//...
pDriverObject->DriverUnload = DriverUnload; //设置驱动卸载入口函数
return STATUS_SUCCESS;
};

驱动想装载需要签名校验,有两种方法,一是尝试签个能用的证书,二是直接关掉Win10的签名校验机制。

第一种方法用软件Windows 64Signer,勾选上开启Test Signing启动选项然后重启,用它签名.sys即可。

第二种方法尝试在管理员下执行:

1
2
bcdedit /set testsigning on
bcdedit -set loadoptions DDISABLE_INTEGRITY_CHECKS

然后按着Shift点击开始菜单的重启,选择疑难解答->高级启动选项,重启,启动时选择倒数第二个选项(虚拟机可能汉字显示框框)。重启后桌面壁纸右下角应该显示测试模式即可。

然后以后每次都得在启动时选择带“DEBUG”的启动选项。

打开DebugView,菜单勾选上“Capture”的“Capture Win32”、“Capture Global Win32”、“Capture Kernel”等,再利用DriverMonitor进行加载即可,可以看到DebugView中的调试信息。

WinDBG初探

Attach to Kernel后,无论任何情况按一或两次F5或命令g即可继续被调试机的运行。例如调试驱动程序MyDriver1.sys,用DriverMonitor进行加载后,Alt+Delete进行中断,并命令bp MyDriver1+0x5000,其中0x5000是该.sys的入口点地址,随便找个PE分析工具都能看。重新运行几次驱动后可断在入口点。F11为步进,F10为步过,Shift+F11为运行到返回。

基础速成

字符串

双字节字符串定义:

1
2
3
4
5
typedef struct _UNICODE_STRING{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
}UNICODE_STRING *PUNICODE_STRING;

打印方法:

1
2
UNICODE_STRING str=RTL_CONSTANT_STRING(TEXT("xxx"));
DbgPrint("%wZ",&str); //估计DbgPrint不好使且必须为Passive中断级 可改用DbgPrintEx KdPrint只有在Debug版下输出

也有ANSI版的结构但极少使用。

UNICODE_STRING类型不能随便初始化,有两种方法,注意清零时Buffer一定要赋值为缓冲区指针,否则空指针访问异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
//法一
UNICODE_STRING str = { 0 };
WCHAR strBuf[128] = { 0 };
str.Buffer = strBuf;
wcscpy(str.Buffer, TEXT("xxx"));
str.Length = str.MaximumLength = wcslen(TEXT("xxx")) * sizeof(WCHAR);
//法二
UNICODE_STRING str;
str.Buffer = TEXT("xxx");
str.Length = str.MaximumLength = wcslen(TEXT("xxx")) * sizeof(WCHAR);
//法三
UNICODE_STRING str = { 0 };
RtlInitUnicodeString(&str, TEXT("xxx"));

字符串src拷贝到dst中,dst的缓冲区为dst_buf

1
2
3
4
5
UNICODE_STRING dst;
WCHAR dst_buf[256] = { 0 }; //需要用到内存分配 但还没学 先用定义缓冲区的方法
UNICODE_STRING src = RTL_CONSTANT_STRING(TEXT("xxx"));
RtlInitEmptyUnicodeString(&dst, dst_buf, 256 * sizeof(WCHAR));
RtlCopyUnicodeString(&dst, &src);

字符串连接,有两个API能用,这俩具体啥区别我也不知道:

1
2
3
NTSTATUS status = STATUS_SUCCESS;
status = RtlAppendUnicodeToString(&dst, &src); //缓冲区不够返回STATUS_BUFFER_TOO_SMALL
status = RtlAppendUnicodeStringToString(&dst, &src);

字符串打印,缓冲区不够直接截断并返回STATUS_BUFFER_OVERFLOW,最好是缓冲区多次扩展长度直到STATUS_SUCCESS为止:

1
2
3
4
5
6
7
8
NTSTATUS status=STATUS_SUCCESS;
UNICODE_STRING dst = { 0 };
WCHAR dst_buf[256] = { 0 };
UNICODE_STRING file_path = RTL_CONSTANT_STRING(TEXT("\\??\\C:\\xxx.xxx"));
RtlInitEmptyUnicodeString(&dst, dst_buf, 256 * sizeof(WCHAR));
USHORT file_size = 1024;
status = RtlStringCbPrintfW(dst.Buffer, 512 * sizeof(WCHAR), TEXT("%wZ -> %d\r\n"), &file_path, file_size);
dst.Length = wcslen(dst.Buffer) * sizeof(WCHAR); //通过RtlStringCbPrintfW打印的Buffer是以零结尾的 用wcslen问题不大

多线程安全性

一个函数的多线程安全性指的是一个函数在被调用过程中,还未返回时,又再次被调用情况下执行结果可靠性。

可能运行于多线程环境函数必须多线程安全,只运行于单线程环境的函数不需要。函数所有调用源只运行于同一单线程环境下则该函数也只运行在单线程环境下。函数其中一个调用源可能运行在多线程环境下或多个调用源可能运行于不同可并发多个线程环境下,且调用路径上没有采取多线程序列化强制措施时该函数可能运行在多线程环境下。若函数所有可能运行于多线程环境下调用路径上都有序列化强制措施,则运行在单线程环境下。只使用函数内部资源,完全不使用全局变量、静态变量或其他全局性资源时多线程安全的。对某个全局/静态变量所有访问被强制同步手段限制为同一时刻只有一个线程访问,即使使用全局/静态变量则不影响多线程安全性。

例如DriverEntryDriverUnload需要单线程,各种分发/完成/NDIS回调函数运行环境为多线程。

代码中断级(IRQL)

有Dispatch和Passive两种中断级,Dispatch比Passive级高。很多复杂功能API需要Passive级执行。

调用路径上没有导致中断级改变的操作时,则与调用源中断级相同。调用路径上有获取自旋锁时中断级升高,有释放自旋锁时中断级下降。

例如DriverEntryDriverUnload还有各种分发函数需要Passive级,完成函数、各种NDIS回调函数需要Dispatch级。

这玩意儿没法强制升降,需要些编程技巧。

参数说明宏

_In__Out_一样:

1
2
3
4
5
6
VOID NdisProtStatus(
IN NDIS_HANDLE ProtocolBindingContext,
IN NDIS_STATUS GeneralStatus,
__in_bcount(StatusBufferSize) IN PVOID StatusBuffer,//代表该参数字节长度由StatusBufferSize参数决定
IN UINT StatusBufferSize
)

预编译指令

默认代码在text段的PAGELK节,但一般可以把DriverEntry放到INIT节,INIT节初始化完毕后就被释放,不占内存。NDIS相关也可放到PAGE节,这个节在内存紧张时可被交换到磁盘上。但PAGE节函数不可在Dispatch级调用,否则可诱发缺页中断,且缺页中断处理不能在Dispatch级完成,可用PAGED_CODE宏测试,如果发现在Dispatch级程序直接报异常:

1
2
3
4
5
6
7
8
9
10
#pragma alloc_text(INIT,DriverEntry)
#pragma alloc_text(PAGE,SfAttachToMountedDevice)
NTSTATUS SfAttachToMountedDevice(IN PDEVICE_OBJECT DeviceObject, IN PDEVICE_OBJECT SFilterDeviceObject) {
PSFILTER_DEVICE_EXTENSION newDevExt = SFilterDeviceObject->DeviceExtension;
NTSTATUS status = STATUS_SUCCESS;
ULONG i = 0;

PAGED_CODE();
//...
};

内存

内存分配与释放,非分页内存永久在物理地址上而不会被交换到磁盘上,ExFreePool只能释放用ExAllocatePoolWithTag申请的内存,如果不释放则重启计算机前该内存将永久泄露。

1
2
3
4
5
6
7
8
9
10
11
NTSTATUS status=STATUS_SUCCESS;
UNICODE_STRING dst = { 0 };
UNICODE_STRING src = RTL_CONSTANT_STRING(TEXT("xxx"));
dst.Buffer = (PWCHAR)ExAllocatePoolWithTag(NonPagedPool, src.Length, 'MyTt'); //随便起名字
if (dst.Buffer == NULL)
status = STATUS_INSUFFICIENT_RESOURCES;
dst.Length = dst.MaximumLength = src.Length;
RtlCopyUnicodeString(&dst, &src);
ExFreePool(dst.Buffer);
dst.Buffer = NULL;
dst.Length = dst.MaximumLength = 0;

LIST_ENTRY结构

定义如下:

1
2
3
4
typedef struct _LIST_ENTRY{
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
}LIST_ENTRY,*PLIST_ENTRY;

初始化与增加一个链表节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LIST_ENTRY my_list_head;
VOID MyFileInforInit(VOID) { //程序入口初始化调用
InitializeListHead(&my_list_head);
return;
};
typedef struct {
LIST_ENTRY list_entry; //最好LIST_ENTRY是第一个成员
PFILE_OBJECT file_object;
PUNICODE_STRING file_name;
LARGE_INTEGER file_length;
}MY_FILE_INFOR,*PMY_FILE_INFOR;
NTSTATUS MyFileInforAppendNode(PFILE_OBJECT file_object, PUNICODE_STRING file_name, PLARGE_INTEGER file_length) { //非线程安全的写法
PMY_FILE_INFOR my_file_infor = (PMY_FILE_INFOR)ExAllocatePoolWithTag(PagedPool, sizeof(MY_FILE_INFOR), 'MyTt');
if (my_file_infor == NULL)
return STATUS_INSUFFICIENT_RESOURCES;
my_file_infor->file_object = file_object;
my_file_infor->file_name = file_name;
my_file_infor->file_length = file_length;
InsertHeadList(&my_list_head, (PLIST_ENTRY)&my_file_infor);
return STATUS_SUCCESS;
};

遍历链表:

1
2
3
4
for (PLIST_ENTRY p = my_list_head.Flink; p != &my_list_head.Flink; p = p->Flink) { //最后一个指向头节点
PMY_FILE_INFOR elem = CONTAINING_RECORD(p, MY_FILE_INFOR, list_entry);
//...
};

长长整型

LARGE_INTEGER类似于__int64

高32位为HighPart,低32位为LowPart,整个64位为QuadPart

1
2
3
4
5
6
LARGE_INTEGER a = { 0 }, b = { 0 };
a.QuadPart = 100;
a.QuadPart *= 100;
b.QuadPart = a.QuadPart;
if(b.QuadPart>1000)
//...

自旋锁

基本操作:

1
2
3
4
5
6
7
KSPIN_LOCK my_spin_lock; //只能是静态/全局变量或在堆上申请 要不每个函数都不一样

KeInitializeSpinLock(&my_spin_lock);
KIRQL irql = 0;
KeAcquireSpinLock(&my_spin_lock,&irql); //获得自旋锁 中断级别提高 其他线程被中断在这里
//需要单线程化的操作...
KeReleaseSpinLock(&my_spin_lock,&irql); //释放自旋锁

给LIST_ENTRY结构加锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
LIST_ENTRY my_list_head;
KSPIN_LOCK my_list_lock;
VOID MyFileInforInit(VOID) { //程序入口初始化调用
InitializeListHead(&my_list_head);
KeInitializeSpinLock(&my_list_head);
return;
};
typedef struct {
LIST_ENTRY list_entry; //最好LIST_ENTRY是第一个成员
PFILE_OBJECT file_object;
PUNICODE_STRING file_name;
LARGE_INTEGER file_length;
}MY_FILE_INFOR,*PMY_FILE_INFOR;
NTSTATUS MyFileInforAppendNode(PFILE_OBJECT file_object, PUNICODE_STRING file_name, PLARGE_INTEGER file_length) { //非线程安全的写法
PMY_FILE_INFOR my_file_infor = (PMY_FILE_INFOR)ExAllocatePoolWithTag(PagedPool, sizeof(MY_FILE_INFOR), 'MyTt');
if (my_file_infor == NULL)
return STATUS_INSUFFICIENT_RESOURCES;
my_file_infor->file_object = file_object;
my_file_infor->file_name = file_name;
my_file_infor->file_length = file_length;
ExInterlockedInsertHeadList(&my_list_head, (PLIST_ENTRY)&my_file_infor, &my_list_lock);
//移除一个节点并返回到my_file_infor中
my_file_infor=ExInterlockedRemoveHeadList(&my_list_head, &my_list_lock);
return STATUS_SUCCESS;
};

队列自旋锁使用KLOCK_QUEUE_HANDLE结构定义,除了获取和释放外,其他操作跟原子自旋锁一模一样,这是队列自旋锁的区别点:

1
2
3
4
5
6
KSPIN_LOCK my_Queue_SpinLock = { 0 };
KeinitializeSpinLock(&my_Queue_SpinLock);
KLOCK_QUEUE_HANDLE my_lock_queue_handle = { 0 };
KeAcquireInStackQueuedSpinLock(&my_Queue_SpinLock, &my_lock_queue_handle); //函数名意思不是自旋锁要放到栈上 而只是暗示中断级别要Dispatch级
//...
KeReleaseInStackQueuedSpinLock(&my_lock_queue_handle);