Windows XP 驱动开发(一)
ʕ •ᴥ•ʔ ɔ:
1 环境安装
1.1 安装VS 2010
1.2 安装WDK 7600
1.3 VS 2010开发驱动环境配置
VS 2010 本身不支持创建驱动项目,所以我们的做法一般是创建一个空项目,然后修改项目配置。这种做法容易出错,我们可以事先准备好一个配置文件,以后创建项目直接导入即可。
步骤:
文件-新建-项目- Visual C++-空项目-名称(HelloDriver)。
生成-配置管理器-活动解决方案配置-新建-名称(Driver_1)、从此处复制设置:Debug。
视图-属性管理器-点开左框“HelloDriver”列表小三角▶️符号-选中步骤2新建的Driver_1右键-添加新项目属性表-名称(DriverProperty)。
到项目目录(C:\Users\alvin\Documents\Visual Studio 2010\Projects\HelloDriver)中找到“DriverProperty.props”文件-使用以下内容进行替换。
之后再新建驱动项目时仅需执行前2步,第3步选择添加现有属性表即可。
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
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<ExecutablePath>D:\WinDDK\7600.16385.1\bin\x86;$(ExecutablePath)</ExecutablePath>
</PropertyGroup>
<PropertyGroup>
<IncludePath>D:\WinDDK\7600.16385.1\inc\api;D:\WinDDK\7600.16385.1\inc\ddk;D:\WinDDK\7600.16385.1\inc\crt;$(IncludePath)</IncludePath>
</PropertyGroup>
<PropertyGroup>
<LibraryPath>D:\WinDDK\7600.16385.1\lib\wxp\i386;$(LibraryPath)</LibraryPath>
<TargetExt>.sys</TargetExt>
<LinkIncremental>false</LinkIncremental>
<GenerateManifest>false</GenerateManifest>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>_X86_;DBG</PreprocessorDefinitions>
<CallingConvention>StdCall</CallingConvention>
<ExceptionHandling>false</ExceptionHandling>
<BasicRuntimeChecks>Default</BasicRuntimeChecks>
<BufferSecurityCheck>false</BufferSecurityCheck>
<CompileAs>Default</CompileAs>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<AssemblerOutput>All</AssemblerOutput>
</ClCompile>
<Link>
<AdditionalDependencies>ntoskrnl.lib;wdm.lib;wdmsec.lib;wmilib.lib;ndis.lib;Hal.lib;MSVCRT.LIB;LIBCMT.LIB;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<Link>
<IgnoreAllDefaultLibraries>true</IgnoreAllDefaultLibraries>
<EnableUAC>false</EnableUAC>
<SubSystem>Native</SubSystem>
<EntryPointSymbol>DriverEntry</EntryPointSymbol>
<BaseAddress>0x10000</BaseAddress>
<RandomizedBaseAddress>
</RandomizedBaseAddress>
<DataExecutionPrevention>
</DataExecutionPrevention>
<GenerateDebugInformation>true</GenerateDebugInformation>
<Driver>Driver</Driver>
</Link>
</ItemDefinitionGroup>
<ItemGroup />
</Project>替换内容:
<LibraryPath>D:\WinDDK\7600.16385.1\lib\wxp\i386;$(LibraryPath)</LibraryPath>
是设置目标平台的:将D:\WinDDK
替换为本机WDK安装路径,替换为C:\WinDDK
。wxp
表示Windows XP系统。因为我们的学习平台就是XP,这里就不用改了。
重启VS 2010。
左下角解决方案管理器-源代码-右键添加-新建项-Visual C++-C++文件-名称(.c文件)。
在新建的.c文件文件中添加如下驱动函数入口代码:
1
2
3
4
5
6
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
return STATUS_UNSUCCESSFUL;
}F7调试生成,生成文件在
C:\Users\alvin\Documents\Visual Studio 2010\Projects\HelloDriver\Driver_1\HelloDriver.sys
。
可以把鼠标放到类型上面去看属性,也可以使用F12进去查看。
2 编译调试驱动程序
2.1 驱动开发流程
驱动的开发流程:
编写代码 ----> 生成.sys文件 ----> 部署 ----> 启动 ----> 停止 ----> 卸载。
2.2 .PDB文件
- PDB文件是在我们编译工程的时候产生的,它是和对应的模块(exe、dll或sys)一起生成出来的。
- 每个模块编译的时候都可以生成自己的PDB文件。比如.exe/.dll/.sys等等。
PDB文件是编译驱动的同时生成的调试信息文件,它可以帮助我们像调试应用程序一样调试驱动程序。其实之前我们已经使用过PDB,我们配置双机调试环境时,在物理机上安装了符号文件,并在Windbg中导入过。
有了PDB,我们就可以知道当前汇编语句属于哪个函数,程序定义的结构体等关键信息,说一句题外话,软件发布的时候,切记不要把PDB也发布出去,因为这会给别人破解你的软件提供巨大便利。
2.3 配置PDB路径
我们要调试一个驱动程序,就要将这个驱动程序的PDB文件的路径配置到Windbg的符号文件路径“Symbol FIle Path…”中,然后执行.reload
。
步骤:
在Win7下编译生成XP的驱动文件
sys
。找到该sys一起发布的
.pdb
文件。将所在目录路径复制到Windbg的符号文件路径下(记得加分号
;
),然后执行.reload
。(C:\Users\alvin\Documents\Visual Studio 2010\Projects\20220112_01_FirstDriver\First_20220112)
3 调试程序
1 |
|
可以看到,触发断点了,观察windbg窗口:多了一个源代码窗口,现在可以像调试应用程序一样调试驱动了。
4 内核编程基础
4.1 内核API的使用
在应用层编程我们可以使用WINDOWS提供的各种API函数,只要导入头文件<windows.h>
就可以了,但是在内核编程的时候,我们不能像在Ring3那样直接使用。微软为内核程序提供了专用的API,只要在程序中包含相应的头文件就可以使用了,如:#include <ntddk.h>
(假设你已经正确安装了WDK)。
2、 在应用层编程的时候,我们通过MSDN来了解函数的详细信息,在内核编程的时候,要使用WDK自己的帮助文档。
WDK查到的函数就一定在导出表里:
文档化函数:函数在导出表里,有文档说明,有头文件。
未文档化函数:导出表里有,没有文档说明,没有头文件,定义函数指针去使用(*pFN = GetProcessAddress("xyz")
)。
未导出函数:不在导出表,无文档说明,没有头文件,可以找到该函数的地址,然后使用函数指针去使用(*pFN = 0x89765786
)。
4.2 未导出函数的使用
WDK说明文档中只包含了内核模块导出的函数,对于未导出的函数,则不能直接使用。
如果要使用未导出的函数,只要定义一个函数指针,并且为函数指针提供正确的函数地址就可以使用了。有两种办法都可以获取为导出的函数地址:
- 特征码搜索。
- 解析内核PDB文件(使用
uf 函数名称
或u 地址
)。
4.3 内核函数前缀
表2.3列出了一些常用的标识性前缀。(《Windows内核原理与实现》2.3.1)
4.4 内核基本数据类型
WDK数据类型在ntdef.h
中定义,下面列举部分,注意,并没有UINT。
在内核编程的时候,强烈建议大家遵守WDK的编码习惯,不要这样写:unsigned long length;
。
习惯使用WDK自己的类型(列举部分):
1 |
|
4.5 NTSTATUS返回值
4字节大小,类型定义如下:
1 | typedef LONG NTSTATUS |
当你调用的内核函数,如果返回的结果不是STATUS_SUCCESS,就说明函数执行中遇到了问题,具体是什么问题,可以在ntstatus.h
文件中查看。
大部分内核函数的返回值都是NTSTATUS类型,如:
1 | NTSTATUS PsCreateSystemThread(); |
这个值能说明函数执行的结果,比如:
1 | STATUS_SUCCESS 0x00000000 成功 |
- NT_SUCCESS(Status)
Evaluates to TRUE if the return value specified by Status is a success type (0 − 0x3FFFFFFF) or an informational type (0x40000000 − 0x7FFFFFFF).
- NT_INFORMATION(Status)
Evaluates to TRUE if the return value specified by Status is an informational type (0x40000000 − 0x7FFFFFFF).
- NT_WARNING(Status)
Evaluates to TRUE if the return value specified by Status is a warning type (0x80000000 − 0xBFFFFFFF).
- NT_ERROR(Status)
Evaluates to TRUE if the return value specified by Status is an error type (0xC0000000 - 0xFFFFFFFF).
4.6 内核异常处理
在内核中,一个小小的错误就可能导致蓝屏,比如:读写一个无效的内存地址。为了让自己的内核程序更加健壮,强烈建议大家在编写内核程序时,使用异常处。
Windows提供了结构化异常处理机制,一般的编译器都是支持的,如下:
1 | __try{ |
出现异常时,可根据filter_value
的值来决定程序该如果执行,当filter_value
的值为:
1 | EXCEPTION_EXECUTE_HANDLER(1) 代码进入except块 |
4.7 常用的内核内存函数
C语言 | 内核 |
---|---|
malloc | ExAllocatePool |
memset | RtlFillMemory |
memcpy | RtlMoveMemory |
free | ExFreePool |
4.8 内核字符串及常用字符串函数
为了提高安全性,内核中的字符串不再是字符串首地址指针作为开始,0作为结尾,而是采用了以下两个结构体:
ANSI_STRING字符串:
1 | typedef struct _STRING |
UNICODE_STRING字符串:
1 | typedef struct _UNICODE_STRING |
下面的表格列出了常用的字符串函数:
功能 | ANSI_STRING字符串 | UNICODE_STRING字符串 |
---|---|---|
创建 | RtlInitAnsiString | RtlInitUnicodeString |
复制 | RtlCopyString | RtlCopyUnicodeString |
比较 | RtlCompareString | RtlCompareUnicoodeString |
转换 | RtlAnsiStringToUnicodeString | RtlUnicodeStringToAnsiString |
具体函数使用可参考:内核模式下的字符串操作
4.9 中断请求级别IRQL
尽管APIC(高级可编程中断控制器)中断控制器已经提供了中断优先级支持,不过,Windows自己还是定义了一套优先级方案,称为中断请求级别(IRQL,Interrupt Request Level)。在Intel x86系统中,Windows使用0~31来表示优先级,数值越大,优先级越高。
- Windowst运行在一个高并发的环境当中,任一时刻,每一个处理器,都运行在某个IRQL之上。
- 每个处理器的IRQL决定了它如何处理中断,以及允许接收哪些中断。优先级低的可以被优先级高的打断。
- IRQL也被用于实现对一些内核模式数据结构的同步访问。例如,与线程调度相关的数据结构只有在
DISPATCH_LEVEL
上才可以访问。
优先级如下图:
- 0,PASSIVE_LEVEL:
PASSIVE_LEVEL
(被动级别)代表了最低的IRQL,运行在PASSIVE_LEVEL
的线程可以被任何高IRQL的事情打断,所有的用户模式代码都运行在此IRQL上。 - 1,APC_LEVEL:
APC_LEVEL
(APC级别)仅仅比PASSIVE_LEVEL
高,这也正是在一个线程中插入一个APC可以打断该线程(如果它正在PASSIVE_LEVEL
上)运行的原因。 - 2,DISPATCH_LEVEL:该级别是一个重要的区分点,它代表了线程调度器正在运行。一个处理器运行在此IRQL上,说明它正在分配处理器的计算资源,有可能正在做两件事情之一:
- 正在进行线程调度,比如选择新的线程。
- 正在处理一个硬件中断的后半部分(不那么紧急的部分),在Windows中,这被称为DPC(Deferred Procedure Call),DPC与线程无关。
- 3~26,设备IRQL(DIRQL):3~26之间的RQL被分配给设备,称为设备IRQL(或DIRQL),HAL规定它们的分配方案。例如,在Intel x86多处理器系统中,HAL会循环地将中断向量号映射到这段IRQL范围中。从IRQL的角度而言,DIRQL范围中的IRQL并无优先级区别,不同的设备中断只是被映射到相同或不同的IRQL而已。
关于APC_LEVEL:
在这个级别上,只有APC级别的中断被屏蔽,可以访问分页内存。当有APC发生时,处理器提升到APC级别,这样就屏蔽掉其它APC,为了和APC执行一些同步,驱动程序可以手动提升到这个级别。比如,如果提升到这个级别,APC就不能调用。在这个级别,APC被禁止了,导致禁止一些I0完成APC,所以有一些API不能调用。
⚠️关于DISPATCH_LEVEL需要特别注意:
DISPATCH_LEVEL
是最高的软件中断IRQL,它低于所有硬件中断的IRQL。- 运行在
DISPATCH_LEVEL
上的线程,不会被其他的线程抢占,只有可能被更高级别的中断抢占。 - 在
DISPATCH_LEVEL
或更高的IRQL上,不能访问换页内存区,因为一旦发生换页动作,就需要执行操作,从磁盘上读入页面,期间至少有一个等待动作。 - 除了系统调度器以外的其他内核代码,一旦运行在这个级别或更高的I迟IRQL上,则不得切换到其他线程(比如,等待一个同步对象),因为线程切换是通过系统调度器来完成的,而系统调度器运行在
DISPATCH_LEVEL
上。
4.a 练习1:读取GDT、IDT
申请一块内存,并在内存中存储GDT、IDT的所有数据。然后在Debugview中显示出来,最后释放内存。
1 |
|
4.b 练习2:处理字符串
<1> 初始化一个字符串
<2> 拷贝一个字符串
<3> 比较两个字符串是否相等
<4> ANSI_STRING与UNICODE_STRING字符串相互转换
结果:拷贝函数RtlCopyString
无法成功执行拷贝,不知道为啥。
⚠️注意:要是用变量的话需要在函数开头就进行定义,最好进行声明,不能是用变量的时候才进行定义,否则会失败。
1 |
|