Windows驱动编程基础教程.docx
《Windows驱动编程基础教程.docx》由会员分享,可在线阅读,更多相关《Windows驱动编程基础教程.docx(77页珍藏版)》请在冰豆网上搜索。
Windows驱动编程基础教程
版权声明
本书是免费电子书。
作者保留一切权利。
但在保证本书完整性(包括版权声明、前言、正文内容、后记、以及作者的信息),并不增删、改变其中任何文字内容的前提下,欢迎任何读者以任何形式(包括各种格式的文档)复制和转载本书。
同时不限制利用此书赢利的行为(如收费注册下载,或者出售光盘或打印版本)。
不满足此前提的任何转载、复制、赢利行为则是侵犯版权的行为。
发现本书的错漏之处,请联系作者。
请不要修改本文中任何内容,不经过作者的同意发布修改后的版本。
作者信息
作者网名楚狂人。
真名谭文。
在上海从事Windows驱动开发相关的工作。
对本书任何内容有任何疑问的读者,可以用下列方式和作者取得联系:
QQ:
16191935
MSN:
walled_river@
Email:
mfc_tan_wen@,walled_river@
前言
本书非常适合熟悉Windows应用编程的读者转向驱动开发。
所有的内容都从最基础的编程方法入手。
介绍相关的内核API,然后举出示范的例子。
这本书只有不到70页,是一本非常精简的小册子。
所以它并不直接指导读者开发某种特定类型的驱动程序。
而是起到一个入门指导的作用。
即使都是使用C/C++语言的代码,在不同的应用环境中,常常看起来还是大相径庭。
比如用TurboC++编写的DOS程序代码和用VC++编写的MFC应用程序的代码,看起来就几乎不像是同一种语言。
这是由于它们所依赖的开发包不相同的缘故。
在任何情况下都以写出避免依赖的代码为最佳。
这样可以避免重复劳动。
但是我们在学习一种开发包的使用时,必须习惯这个环境的编码方式,以便获得充分利用这个开发包的能力。
本书的代码几乎都依赖于WDK(WindowsDriverKit)。
但是不限WDK的版本。
WDK还在不断的升级中。
这个开发包是由微软公司免费提供的。
读者可以在微软的网站上下载。
当然读者必须把WDK安装的计算机上并配置好开发环境。
具体的安装和配置方法本书没有提供。
因为网上已经有非常多的中文文档介绍它们。
读完这本书之后,读者一定可以更轻松的阅读其他专门的驱动程序开发的文档和相关书籍。
而不至于看到大量无法理解的代码而中途放弃。
如果有任何关于本书的内容的问题,读者可以随时发邮件到mfc_tan_wen@或者walled_river@。
能够回答的问题我一般都会答复。
写本书的时候,我和wowocock合作的一本名为《天书夜读》(在网上有一个大约20%内容的缩减电子版本)正在电子工业出版社编辑。
预计还有不到一个月左右就会出版。
这也是我自己所见的唯一一本中文原创的从汇编和反汇编角度来学习Windows内核编程和信息安全软件开发的书。
希望读者多多支持。
有想购买的读者请发邮件给我。
我会在本书出版的第一时间,回复邮件告知购买的方法。
此外我正在写另一本关于Windows安全软件的驱动编程的书。
但是题目还没有拟好。
实际上,读者现在见到的免费版本的《Windows驱动编程基础教程》是从这本书的第一部分中节选出来的。
这本书篇幅比较大,大约有600-800页。
主要内容如下:
第一章驱动编程基础
第二章磁盘设备驱动
第三章磁盘还原与加密
第四章传统文件系统过滤
第五章小端口文件系统过滤
第六章文件系统保护与加密
第七章协议网络驱动
第八章物理网络驱动
第九章网络防火墙与安全连接
第十章打印机驱动与虚拟打印
第十一章视频驱动与过滤
附录AWDK的安装与驱动开发的环境配置
附录B用WinDbg调试Windows驱动程序
这本书还没有完成。
但是肯定要付出巨大的精力,所以请读者不要来邮件索取完整的免费的电子版本。
希望读者支持本书的纸版出版。
因为没有完成,所以还没有联系出版商。
有愿意合作出版本书的读者请发邮件与我联系。
凡是发送邮件给我的读者,我将会发送邮件提供本人作品最新的出版信息,以及最新发布的驱动开发相关的免费电子书。
如果不需要这些信息的,请在邮件里注明,或者回复邮件给我来取消订阅。
谭文
2008年6月9日
目录
版权声明1
作者信息1
前言2
目录4
第一章字符串6
1.1使用字符串结构6
1.2字符串的初始化7
1.3字符串的拷贝8
1.4字符串的连接8
1.5字符串的打印9
第二章内存与链表11
2.1内存的分配与释放11
2.2使用LIST_ENTRY12
2.3使用长长整型数据14
2.4使用自旋锁15
第三章文件操作18
3.1使用OBJECT_ATTRIBUTES18
3.2打开和关闭文件18
3.3文件的读写操作21
第四章操作注册表25
4.1注册键的打开操作25
4.2注册值的读26
4.3注册值的写29
第五章时间与定时器30
5.1获得当前滴答数30
5.2获得当前系统时间31
5.3使用定时器32
第六章内核线程35
6.1使用线程35
6.2在线程中睡眠36
6.3使用事件通知37
第七章驱动与设备41
7.1驱动入口与驱动对象41
7.2分发函数与卸载函数41
7.3设备与符号链接42
7.4设备的生成安全性限制44
7.5符号链接的用户相关性46
第八章处理请求47
8.1IRP与IO_STACK_LOCATION47
8.2打开与关闭的处理48
8.3应用层信息传入49
8.4驱动层信息传出51
后记:
我的闲言碎语54
第一章字符串
1.1使用字符串结构
常常使用传统C语言的程序员比较喜欢用如下的方法定义和使用字符串:
char*str={“myfirststring”};//ansi字符串
wchar_t*wstr={L”myfirststring”};//unicode字符串
size_tlen=strlen(str);//ansi字符串求长度
size_twlen=wcslen(wstr);//unicode字符串求长度
printf(“%s%ws%d%d”,str,wstr,len,wlen);//打印两种字符串
但是实际上这种字符串相当的不安全。
很容易导致缓冲溢出漏洞。
这是因为没有任何地方确切的表明一个字符串的长度。
仅仅用一个’\0’字符来标明这个字符串的结束。
一旦碰到根本就没有空结束的字符串(可能是攻击者恶意的输入、或者是编程错误导致的意外),程序就可能陷入崩溃。
使用高级C++特性的编码者则容易忽略这个问题。
因为常常使用std:
:
string和CString这样高级的类。
不用去担忧字符串的安全性了。
在驱动开发中,一般不再用空来表示一个字符串的结束。
而是定义了如下的一个结构:
typedefstruct_UNICODE_STRING{
USHORTLength;//字符串的长度(字节数)
USHORTMaximumLength;//字符串缓冲区的长度(字节数)
PWSTRBuffer;//字符串缓冲区
}UNICODE_STRING,*PUNICODE_STRING;
以上是Unicode字符串,一个字符为双字节。
与之对应的还有一个Ansi字符串。
Ansi字符串就是C语言中常用的单字节表示一个字符的窄字符串。
typedefstruct_STRING{
USHORTLength;
USHORTMaximumLength;
PSTRBuffer;
}ANSI_STRING,*PANSI_STRING;
在驱动开发中四处可见的是Unicode字符串。
因此可以说:
Windows的内核是使用Uincode编码的。
ANSI_STRING仅仅在某些碰到窄字符的场合使用。
而且这种场合非常罕见。
UNICODE_STRING并不保证Buffer中的字符串是以空结束的。
因此,类似下面的做法都是错误的,可能会会导致内核崩溃:
UNICODE_STRINGstr;
…
len=wcslen(str.Buffer);//试图求长度。
DbgPrint(“%ws”,str.Buffer);//试图打印str.Buffer。
如果要用以上的方法,必须在编码中保证Buffer始终是以空结束。
但这又是一个麻烦的问题。
所以,使用微软提供的Rtl系列函数来操作字符串,才是正确的方法。
下文逐步的讲述这个系列的函数的使用。
1.2字符串的初始化
请回顾之前的UNICODE_STRING结构。
读者应该可以注意到,这个结构中并不含有字符串缓冲的空间。
这是一个初学者常见的出问题的来源。
以下的代码是完全错误的,内核会立刻崩溃:
UNICODE_STRINGstr;
wcscpy(str.Buffer,L”myfirststring!
”);
str.Length=str.MaximumLength=wcslen(L”myfirststring!
”)*sizeof(WCHAR);
以上的代码定义了一个字符串并试图初始化它的值。
但是非常遗憾这样做是不对的。
因为str.Buffer只是一个未初始化的指针。
它并没有指向有意义的空间。
相反以下的方法是正确的:
//先定义后,再定义空间
UNICODE_STRINGstr;
str.Buffer=L”myfirststring!
”;
str.Length=str.MaximumLength=wcslen(L”myfirststring!
”)*sizeof(WCHAR);
……
上面代码的第二行手写的常数字符串在代码中形成了“常数”内存空间。
这个空间位于代码段。
将被分配于可执行页面上。
一般的情况下不可写。
为此,要注意的是这个字符串空间一旦初始化就不要再更改。
否则可能引发系统的保护异常。
实际上更好的写法如下:
//请分析一下为何这样写是对的:
UNICODE_STRINGstr={
sizeof(L”myfirststring!
”)–sizeof((L”myfirststring!
”)[0]),
sizeof(L”myfirststring!
”),
L”myfirst_string!
”};
但是这样定义一个字符串实在太繁琐了。
但是在头文件ntdef.h中有一个宏方便这种定义。
使用这个宏之后,我们就可以简单的定义一个常数字符串如下:
#include
UNICODE_STRINGstr=RTL_CONSTANT_STRING(L“myfirststring!
”);
这只能在定义这个字符串的时候使用。
为了随时初始化一个字符串,可以使用RtlInitUnicodeString。
示例如下:
UNICODE_STRINGstr;
RtlInitUnicodeString(&str,L”myfirststring!
”);
用本小节的方法初始化的字符串,不用担心内存释放方面的问题。
因为我们并没有分配任何内存。
1.3字符串的拷贝
因为字符串不再是空结束的,所以使用wcscpy来拷贝字符串是不行的。
UNICODE_STRING可以用RtlCopyUnicodeString来进行拷贝。
在进行这种拷贝的时候,最需要注意的一点是:
拷贝目的字符串的Buffer必须有足够的空间。
如果Buffer的空间不足,字符串会拷贝不完全。
这是一个比较隐蔽的错误。
下面举一个例子。
UNICODE_STRINGdst;//目标字符串
WCHARdst_buf[256];//我们现在还不会分配内存,所以先定义缓冲区
UNICODE_STRINGsrc=RTL_CONST_STRING(L”Mysourcestring!
”);
//把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串。
RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src);//字符串拷贝!
以上这个拷贝之所以可以成功,是因为256比L”Mysourcestring!
”的长度要大。
如果小,则拷贝也不会出现任何明示的错误。
但是拷贝结束之后,与使用者的目标不符,字符串实际上被截短了。
我曾经犯过的一个错误是没有调用RtlInitEmptyString。
结果dst字符串被初始化认为缓冲区长度为0。
虽然程序没有崩溃,却实际上没有拷贝任何内容。
在拷贝之前,最谨慎的方法是根据源字符串的长度动态分配空间。
在1.2节“内存与链表”中,读者会看到动态分配内存处理字符串的方法。
1.4字符串的连接
UNICODE_STRING不再是简单的字符串。
操作这个数据结构往往需要更多的耐心。
读者会常常碰到这样的需求:
要把两个字符串连接到一起。
简单的追加一个字符串并不困难。
重要的依然是保证目标字符串的空间大小。
下面是范例:
NTSTATUSstatus;
UNICODE_STRINGdst;//目标字符串
WCHARdst_buf[256];//我们现在还不会分配内存,所以先定义缓冲区
UNICODE_STRINGsrc=RTL_CONST_STRING(L”Mysourcestring!
”);
//把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串
RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src);//字符串拷贝!
status=RtlAppendUnicodeToString(
&dst,L”mysecondstring!
”);
if(status!
=STATUS_SUCCESS)
{
……
}
NTSTATUS是常见的返回值类型。
如果函数成功,返回STATUS_SUCCESS。
否则的话,是一个错误码。
RtlAppendUnicodeToString在目标字符串空间不足的时候依然可以连接字符串,但是会返回一个警告性的错误STATUS_BUFFER_TOO_SMALL。
另外一种情况是希望连接两个UNICODE_STRING,这种情况请调用RtlAppendUnicodeStringToString。
这个函数的第二个参数也是一个UNICODE_STRING的指针。
1.5字符串的打印
字符串的连接另一种常见的情况是字符串和数字的组合。
有时数字需要被转换为字符串。
有时需要把若干个数字和字符串混合组合起来。
这往往用于打印日志的时候。
日志中可能含有文件名、时间、和行号,以及其他的信息。
熟悉C语言的读者会使用sprintf。
这个函数的宽字符版本为swprintf。
该函数在驱动开发中依然可以使用,但是不安全。
微软建议使用RtlStringCbPrintfW来代替它。
RtlStringCbPrintfW需要包含头文件ntstrsafe.h。
在连接的时候,还需要连接库ntsafestr.lib。
下面的代码生成一个字符串,字符串中包含文件的路径,和这个文件的大小。
#include
//任何时候,假设文件路径的长度为有限的都是不对的。
应该动态的分配
//内存。
但是动态分配内存的方法还没有讲述,所以这里再次把内存空间
//定义在局部变量中,也就是所谓的“在栈中”
WCHARbuf[512]={0};
UNICODE_STRINGdst;
NTSTATUSstatus;
……
//字符串初始化为空串。
缓冲区长度为512*sizeof(WCHAR)
RtlInitEmptyString(dst,dst_buf,512*sizeof(WCHAR));
//调用RtlStringCbPrintfW来进行打印
status=RtlStringCbPrintfW(
dst->Buffer,L”filepath=%wZfilesize=%d\r\n”,
&file_path,file_size);
//这里调用wcslen没问题,这是因为RtlStringCbPrintfW打印的
//字符串是以空结束的。
dst->Length=wcslen(dst->Buffer)*sizeof(WCHAR);
RtlStringCbPrintfW在目标缓冲区内存不足的时候依然可以打印,但是多余的部分被截去了。
返回的status值为STATUS_BUFFER_OVERFLOW。
调用这个函数之前很难知道究竟需要多长的缓冲区。
一般都采取倍增尝试。
每次都传入一个为前次尝试长度为2倍长度的新缓冲区,直到这个函数返回STATUS_SUCCESS为止。
值得注意的是UNICODE_STRING类型的指针,用%wZ打印可以打印出字符串。
在不能保证字符串为空结束的时候,必须避免使用%ws或者%s。
其他的打印格式字符串与传统C语言中的printf函数完全相同。
可以尽情使用。
另外就是常见的输出打印。
printf函数只有在有控制台输出的情况下才有意义。
在驱动中没有控制台。
但是Windows内核中拥有调试信息输出机制。
可以使用特殊的工具查看打印的调试信息(请参阅附录1“WDK的安装与驱动开发的环境配置”)。
驱动中可以调用DbgPrint()函数来打印调试信息。
这个函数的使用和printf基本相同。
但是格式字符串要使用宽字符。
DbgPrint()的一个缺点在于,发行版本的驱动程序往往不希望附带任何输出信息,只有调试版本才需要调试信息。
但是DbgPrint()无论是发行版本还是调试版本编译都会有效。
为此可以自己定义一个宏:
#ifDBG
KdPrint(a)DbgPrint##a
#else
KdPrint(a)
#endif
不过这样的后果是,由于KdPrint(a)只支持1个参数,因此必须把DbgPrint的所有参数都括起来当作一个参数传入。
导致KdPrint看起来很奇特的用了双重括弧:
//调用KdPrint来进行输出调试信息
status=KdPrint((
L”filepath=%wZfilesize=%d\r\n”,
&file_path,file_size));
这个宏没有必要自己定义,WDK包中已有。
所以可以直接使用KdPrint来代替DbgPrint取得更方便的效果。
第二章内存与链表
2.1内存的分配与释放
内存泄漏是C语言中一个臭名昭著的问题。
但是作为内核开发者,读者将有必要自己来面对它。
在传统的C语言中,分配内存常常使用的函数是malloc。
这个函数的使用非常简单,传入长度参数就得到内存空间。
在驱动中使用内存分配,这个函数不再有效。
驱动中分配内存,最常用的是调用ExAllocatePoolWithTag。
其他的方法在本章范围内全部忽略。
回忆前一小节关于字符串的处理的情况。
一个字符串被复制到另一个字符串的时候,最好根据源字符串的空间长度来分配目标字符串的长度。
下面的举例,是把一个字符串src拷贝到字符串dst。
//定义一个内存分配标记
#defineMEM_TAG‘MyTt’
//目标字符串,接下来它需要分配空间。
UNICODE_STRINGdst={0};
//分配空间给目标字符串。
根据源字符串的长度。
dst.Buffer=
(PWCHAR)ExAllocatePoolWithTag(NonpagedPool,src->Length,MEM_TAG);
if(dst.Buffer==NULL)
{
//错误处理
status=STATUS_INSUFFICIENT_RESOUCRES;
……
}
dst.Length=dst.MaximumLength=src->Length;
status=RtlCopyUnicodeString(&dst,&src);
ASSERT(status==STATUS_SUCCESS);
ExAllocatePoolWithTag的第一个参数NonpagedPool表明分配的内存是锁定内存。
这些内存永远真实存在于物理内存上。
不会被分页交换到硬盘上去。
第二个参数是长度。
第三个参数是一个所谓的“内存分配标记”。
内存分配标记用于检测内存泄漏。
想象一下,我们根据占用越来越多的内存的分配标记,就能大概知道泄漏的来源。
一般每个驱动程序定义一个自己的内存标记。
也可以在每个模块中定义单独的内存标记。
内存标记是随意的32位数字。
即使冲突也不会有什么问题。
此外也可以分配可分页内存,使用PagedPool即可。
ExAllocatePoolWithTag分配的内存可以使用ExFreePool来释放。
如果不释放,则永远泄漏。
并不像用户进程关闭后自动释放所有分配的空间。
即使驱动程序动态卸载,也不能释放空间。
唯一的办法是重启计算机。
ExFreePool只需要提供需要释放的指针即可。
举例如下:
ExFreePool(dst.Buffer);
dst.Buffer=NULL;
dst.Length=dst.MaximumLength=0;
ExFreePool不能用来释放一个栈空间的指针。
否则系统立刻崩溃。
像以下的代码:
UNICODE_STRINGsrc=RTL_CONST_STRING(L”Mysourcestring!
”);
ExFreePool(src.Buffer);
会招来立刻蓝屏。
所以请务必保持ExAllocatePoolWithTag和ExFreePool的成对关系。
2.2使用LIST_ENTRY
Windows的内核开发者们自己开发了部分数据结构,比如说LIST_ENTRY。
LIST_ENTRY是一个双向链表结构。
它总是在使用的时候,被插入到已有的数据结构中。
下面举一个例子。
我构筑一个链表,这个链表的每个节点,是一个文件名和一个文件大小两个数据成员组成的结构。
此外有一个FILE_OBJECT的指针对象。
在驱动中,这代表一个文件对象。
本书后面的章节会详细解释。
这个链表的作用是:
保存了文件的文件名和长度。
只要传入FILE_OBJECT的指针,使用者就可以遍历链表找到文件名和文件长度。
typedefstruct{
PFILE_OBJECTfile_object;
UNICODE_STRINGfile_name;
LARGE_INTEGERfile_length;
}MY_FILE_INFOR,*PMY_FILE_INFOR;
一些读