1. 环境准备
开发环境
使用Win10系统为例
- Visiual Studio 2019
- Windows SDK 10
- Windows Driver Kit (WDK)
都是那种直接安装的
调试环境
驱动是内核模块,所以需要整个Windows系统作为调试环境。由于没有双机调试,只能使用虚拟机网络调试或管道调试。
调试环境找到了这篇文章 https://blog.csdn.net/qq_40277608/article/details/109765965
- 基于网络的调试
被调试机器:
1 2
| bcdedit /debug on bcdedit /dbgsettings net hostip:192.168.0.110 port:50010
|
这两条指令执行后会显示一个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 2
| bcdedit /debug on bcdedit /dbgsettings serial baudrate:115200 debugport:2
|
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
- 其它
在被调试机器上面安装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);
|
重要的数据结构
驱动对象:C语言对面向对象的模拟,把设备、驱动、文件看成一个对象
DRIVER_OBJECT
用于描述程序提供的功能,填写一组回调函数让Windows函数
设备对象:可能代表硬盘、管道等,可以接受请求给出响应
DEVICE_OBJECT
驱动对象生成多个设备对象,而Windows向设备对象发出请求,被驱动对象的分发函数捕获。
请求:给设备对象发的数据、指令等IRP
IRP还有几种衍生说法
函数调用
注意编程时查看WDK API,WDK自带的help也可以
后续将会陆续用到常用的函数
驱动开发模型
https://docs.microsoft.com/zh-cn/windows-hardware/drivers/gettingstarted/choosing-a-driver-model
Windows有官方文档,基本上囊括了所有类型的驱动程序
WDK编程特点
调用源
- 入口函数
- 各种分发函数
- 处理请求完成时的回调函数
- 其它回调函数
多线程安全性
如果总是考虑多线程安全,则会降低效率,所以有以下原则
- 可能运行于多线程环境的函数必须多线程安全
- 函数A的所有调用源都是单线程环境,则A也是单线程环境
- 函数A的其中一个调用源位于多线程环境或多个调用源可并发,则A时多线程
- 函数A的所有可能运行于多线程环境下的调用路径上,都有序列化的强制措施,则A运行在单线程下
- 只使用函数内部资源,则是多线程安全
- 如果对于全局变量的访问强制序列化,则也是多线程安全
代码中断级
中断级有两种,Dispatch和Passive,Dispatch级别高,复杂功能要求在Passive级运行
- 调用路径上无中断级变化,则函数和它的调用源中断级相同
- 获取自旋锁中断级升高,释放自旋锁中断级下降
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)); 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); 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"); 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);
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);
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) { 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; } DbgPrint(dst); } 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: "); printLinkedList(&head); linkedListToString(&head, &str); DbgPrint(&str); DbgPrint("Freeing memory"); ExFreePool(str.Buffer); return STATUS_SUCCESS; }
|
参考资料