驱动开发入门-1

1. 环境准备

开发环境

使用Win10系统为例

  1. Visiual Studio 2019
  2. Windows SDK 10
  3. Windows Driver Kit (WDK)

都是那种直接安装的

调试环境

驱动是内核模块,所以需要整个Windows系统作为调试环境。由于没有双机调试,只能使用虚拟机网络调试或管道调试。

调试环境找到了这篇文章 https://blog.csdn.net/qq_40277608/article/details/109765965

  1. 基于网络的调试

被调试机器:

1
2
bcdedit /debug on # 设置为调试模式
bcdedit /dbgsettings net hostip:192.168.0.110 port:50010 # 网络调试方式,指定调试机器ip和本机端口,建议49152~65535

这两条指令执行后会显示一个key,保持这个key,下面用到

调试机器:

VS->扩展->Driver->Test->Configure Drivers->Add New Driver

Display Name为设备名字,Network host name为被调试机器IP,下面选择手动配置调试选项以及手动分发驱动文件

下一步,在Kernel Mode下设置key以及ip,Bus Parameters是多块网卡时才会使用,要根据PCI规范填入总线号、设备号、功能号

在驱动入口函数添加一个KdBreakPoint(),这个断点只有在Debug版本有用,DbgBreakPoint()同时在Release版本也有用

生成sys文件,调试->附加进程->链接类型=Windows Kernel Mode Debugger,目标选择配置好的,可用进程=kernel

接下来就会出现调试输出界面(实际上就是gdb的调试界面)

重启被调试机器,就可以看到调试信息了

最后把sys文件放在被调试机器中,使用sc start/sc create运行

sc命令文档 https://docs.microsoft.com/zh-cn/windows-server/administration/windows-commands/sc-create

https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/cc742126(v=ws.11)

1
2
sc create HelloDriver binpath="C:\Users\kali\Desktop\HelloDriver.sys" type=kernel start=demand
sc start HelloDriver
  1. 基于管道的调试
1
2
bcdedit /debug on
bcdedit /dbgsettings serial baudrate:115200 debugport:2 # 串口2为调试介质,波特率为115200

XP及更低版本需要更改C:\boot.ini

1
2
3
4
5
[boot loader]
timeout=30
default=multi(0)disk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDWOS="WINDWOS XP Debug" /fastdetect /debug /debugport=com2 /baudrate=11520

关闭虚拟机,在Vmware中新增串口设备,设置命名管道,名字为 \.\pipe\com_2

修改windbg的快捷方式,改为

1
"PATH_TO_WINDBG" -b -k com:pipe,port=\.\pipe\com_2,resets=0

windbg会连接被调试机器

生成驱动,sc create/sc start

  1. 其它

在被调试机器上面安装debugview,并以管理员身份运行,可以查看调试输出

测试示例

VS2019 创建新项目-> Kernel Mode Driver, Empty(KMDF)

新建hello.c,填入以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <ntddk.h>

VOID DriverUnload(PDRIVER_OBJECT driver) {
DbgPrint("hello: driver unloading...");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path) {
DbgPrint("hello: driver loaded!");
DbgPrint("hello:This is a breakpoint");
KdBreakPoint();
DbgPrint("hello:You step over breakpoint");
driver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
  • 生成时会有一个漏洞缓解措施,在单个组件中安装就行
  • 以下警告视为错误 项目->自己的项目->配置属性->c/c++->常规->将警告视为错误=否
  • SignTask任务失败 属性->配置属性->Driver Signing-> Sign mode=off
  • 运行时选择调试->附加到进程->类型=Windows kernel mode debugger,目标=sc创建的驱动名,进程=kernel,如果没有kernel就勾选显示所有用户进程

注意:编译位数要和系统位数相同

2. 驱动开发和应用层应用开发的不同

  • 共享的内核空间vs隔离的应用程序
  • 操作系统内核位于高2GB,有接口可以供硬件驱动编程人员调用
  • Windows一般使用系统进程加载内核模块,但是内核代码不一定始终运行在system进程里

数据类型

使用重新定义的数据类型,万一出现问题,重新定义一下就行

  • unsigned long-> ULONG
  • unsigned char-> UCHAR
  • unsigned int-> UINT
  • void->VOID
  • unsigned long*->PULONG
  • unsigned char*->PUCHAR
  • unsigned int*->PUINT
  • void*->PVOID

返回状态

绝大多数内核API都是返回一个状态,也就是一个错误码NTSTATUS,可以使用下面的代码判断

1
2
3
4
5
if(!NT_SUCCESS(status)){ // 处理失败的情况
//...
}else{
//...
}

错误值的意思不总是很明确,可以通过WDK头文件寻找

字符串

由于共享的内核空间,使得很多应用层可以用的API都无法使用

驱动中的字符串用一个结构来容纳

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(L"hello driver!");
DbgPrint("%wZ",&str);

重要的数据结构

  1. 驱动对象:C语言对面向对象的模拟,把设备、驱动、文件看成一个对象
    DRIVER_OBJECT

    用于描述程序提供的功能,填写一组回调函数让Windows函数

  2. 设备对象:可能代表硬盘、管道等,可以接受请求给出响应
    DEVICE_OBJECT

驱动对象生成多个设备对象,而Windows向设备对象发出请求,被驱动对象的分发函数捕获。

  1. 请求:给设备对象发的数据、指令等IRP

    IRP还有几种衍生说法

函数调用

注意编程时查看WDK API,WDK自带的help也可以

后续将会陆续用到常用的函数

驱动开发模型

https://docs.microsoft.com/zh-cn/windows-hardware/drivers/gettingstarted/choosing-a-driver-model

Windows有官方文档,基本上囊括了所有类型的驱动程序

WDK编程特点

  1. 调用源

    • 入口函数
    • 各种分发函数
    • 处理请求完成时的回调函数
    • 其它回调函数
  2. 多线程安全性

    如果总是考虑多线程安全,则会降低效率,所以有以下原则

    • 可能运行于多线程环境的函数必须多线程安全
    • 函数A的所有调用源都是单线程环境,则A也是单线程环境
    • 函数A的其中一个调用源位于多线程环境或多个调用源可并发,则A时多线程
    • 函数A的所有可能运行于多线程环境下的调用路径上,都有序列化的强制措施,则A运行在单线程下
    • 只使用函数内部资源,则是多线程安全
    • 如果对于全局变量的访问强制序列化,则也是多线程安全
  3. 代码中断级

    中断级有两种,Dispatch和Passive,Dispatch级别高,复杂功能要求在Passive级运行

    • 调用路径上无中断级变化,则函数和它的调用源中断级相同
    • 获取自旋锁中断级升高,释放自旋锁中断级下降
  4. PAGE节中的函数不能时Dispatch级,因为可能引发缺页中断,可以使用PAGED_CODE()测试

3. 驱动开发中的字符串和链表

字符串

1
2
3
4
5
typedef struct _UNICODE_STRING{
USHORT Length;
USHORT MAximumLength;
PWSTR Buffer;
} UNICODE_STRING,*PUNICODE_STRING;
1
2
3
4
5
6
7
8
9
10
11
#include <ntstrsafe.h>
NTSTATUS status;
UNICODE_STRING str;
RtlInitUnicodeString(&str,L"My first String"); // 初始化字符串
UNICODE_STRING src=RTL_CONSTANT_STRING(L"My source String"),dst;
WCHAR dst_buf[256]; // 定义缓冲区
RtlInitEmptyUnicodeString(&dst,dst_buf,256*sizeof(WCHAR));// 让dst使用dst_buf作为缓冲区
RtlCopyUnicodeString(&dst,&src); // 字符串拷贝
status=RtlAppendUnicodeString(&dst,L"My second String");// 字符串连接
status=RtlStringCbPrintfW(dst.Buffer,512*sizeof(WCHAR),L"filepath=%wZ,size=%d\r\n",&str,1024); // 相当于swprintf
KdPrint((L"filepath=%wZ,size=%d\r\n",&str,1024));//直接输出,使用双重括号

链表

1
2
3
4
5
6
NTSTATUS status;
UNICODE_STRING dst={0};
dst.Buffer=(PWCHAR)ExAllocPoolWithTag(NonPagedPool,1024,"MyTt");//分配内存空间,NonPagedPool表示不会被交换到硬盘中的内存
dst.Length=0;
dst.MaximumLength=1024;
ExFreePool(dst.Buffer); // 释放内存

ExAllocPoolWithTag和ExFreePool必须成对出现,如果不释放,则永远造成内存泄漏,直到重启计算机

内核开发者定义的数据结构:

1
2
3
4
5
typedef struct _LIST_ENTRY{
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY,*PLIST_ENTRY;
InsertHeadList(&list_Head,(PLIST_ENTRY)&my_struct);//插入到链表末尾,非多线程安全

自定义的链表可以把这个结构放在开头

长长整形 LARGE_INTEGER

自旋锁

自旋锁用于保证多线程的安全性

1
2
3
4
5
6
KSPIN_LOCK my_spin_lock;
KIRQL irql;//中断级别
KeInitializeSpinLock(&my_spin_lock);//初始化一个自旋锁
KeAcquireSpinLock(&my_spin_lock,&irql);//提高了中断级别
// your code, single thread executing
KeReleaseSpinLock(&my_spin_lock,&irql);

自旋锁应该是各个线程共享的变量

队列自旋锁可以提高性能,遵守先来先服务原则

1
2
3
4
5
6
KSPIN_LOCK my_queue_spinlock=0;
KeInitializeSpinLock(&my_queue_spinlock);
KLOCK_QUEUE_HANDLE my_lock_queue_handle;
KeAcquireInStackQueuedSpinLock(&my_queue_spinlock,&my_lock_queue_handle);
// your code
KeReleaseInStackQueuedSpinLock(&my_lock_queue_handle);

实例:链表和字符串转换程序

(虽然转换还是不成功,待后面开发填补)

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <ntddk.h>
#include <ntstrsafe.h>
#include <windef.h>

typedef struct {
LIST_ENTRY list;
WCHAR c;
UINT len;
}String,*PString;
KSPIN_LOCK spinLock;
KIRQL irql;
VOID printLinkedList(PString head) {
//ExAcquireSpinLock(&spinLock, &irql);
WCHAR dst[1024];
RtlStringCbPrintfW(dst, sizeof(dst), L"hello: LinkedList:");
PString p = head->list.Blink;
UINT len = head->len;
for(UINT i=0;i<len;i++) {
RtlStringCbPrintfW(dst, sizeof(dst), L"%wZ%d,",dst, (UINT)(p->c));
p = p->list.Blink;
}
//KdBreakPoint();
DbgPrint(dst);
//ExReleaseSpinLock(&spinLock, &irql);
}
VOID addToLinkedList(PString head,PString node) {
PString p = ExAllocatePool(NonPagedPool, sizeof(String));
InsertHeadList(head, node);
head->len++;
}
VOID freeLinkedList(PString head) {
PString p = head->list.Blink,pp=head;
UINT len = head->len;
for (UINT i = 0; i < len; i++) {
ExFreePool(pp);
pp = p;
p = pp->list.Blink;
}
}

VOID linkedListToString(PString head,UNICODE_STRING *str) {
WCHAR *buf = ExAllocatePool(NonPagedPool, sizeof(WCHAR)*(head->len));
if (!buf) {
DbgPrint(L"Memory Alloc Failed");
return;
}
PString p = head->list.Blink;
for (UINT i = 0; i < head->len; i++) {
buf[i] = (WCHAR)p->c;
p = p->list.Blink;
}
str->Buffer = buf;
str->Length = head->len;
str->MaximumLength = head->len;
}
VOID stringToLinkedLIst(UNICODE_STRING str,PString head) {

for (UINT i = 0; i < str.Length; i++) {
PString p = ExAllocatePool(NonPagedPool, sizeof(String));
if (!p) {
DbgPrint(L"Memory Alloc Failed");
return;
}
addToLinkedList(head, p);
p->c = str.Buffer[i];
head->len++;
}

}

VOID DriverUnload(PDRIVER_OBJECT driver) {
DbgPrint("hello: driver unloading...");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path) {
KeInitializeSpinLock(&spinLock);
UNICODE_STRING str;
String head;
InitializeListHead(&head);
head.len = 0;
RtlInitUnicodeString(&str, L"You are the best!");
DbgPrint("Transfering UNICODE_STRING to LinkList: %wZ", &str);
stringToLinkedLIst(str, &head);
DbgPrint("Transfering LinkList to UNICODE_STRING: ");
//KdBreakPoint();
printLinkedList(&head);
linkedListToString(&head, &str);
DbgPrint(&str);
DbgPrint("Freeing memory");
//freeLinkedList(&head); // 暂时不释放了,似乎链表有检查机制,需要别的释放方法
ExFreePool(str.Buffer);
return STATUS_SUCCESS;
}

参考资料

  • 《Windows内核安全与驱动开发》