Windows XP 驱动开发(一)

ʕ •ᴥ•ʔ ɔ:

1 环境安装

1.1 安装VS 2010

点击下载安装镜像

1.2 安装WDK 7600

点击下载安装镜像

1.3 VS 2010开发驱动环境配置

VS 2010 本身不支持创建驱动项目,所以我们的做法一般是创建一个空项目,然后修改项目配置。这种做法容易出错,我们可以事先准备好一个配置文件,以后创建项目直接导入即可。

步骤:

  1. 文件-新建-项目- Visual C++-空项目-名称(HelloDriver)。

  2. 生成-配置管理器-活动解决方案配置-新建-名称(Driver_1)、从此处复制设置:Debug。

  3. 视图-属性管理器-点开左框“HelloDriver”列表小三角▶️符号-选中步骤2新建的Driver_1右键-添加新项目属性表-名称(DriverProperty)。

  4. 到项目目录(C:\Users\alvin\Documents\Visual Studio 2010\Projects\HelloDriver)中找到“DriverProperty.props”文件-使用以下内容进行替换。

  5. 之后再新建驱动项目时仅需执行前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
    <?xml version="1.0" encoding="utf-8"?>
    <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>
  6. 替换内容:

    • <LibraryPath>D:\WinDDK\7600.16385.1\lib\wxp\i386;$(LibraryPath)</LibraryPath>是设置目标平台的:将D:\WinDDK替换为本机WDK安装路径,替换为C:\WinDDK
    • wxp表示Windows XP系统。因为我们的学习平台就是XP,这里就不用改了。
  7. 重启VS 2010。

  8. 左下角解决方案管理器-源代码-右键添加-新建项-Visual C++-C++文件-名称(.c文件)。

  9. 在新建的.c文件文件中添加如下驱动函数入口代码:

    1
    2
    3
    4
    5
    6
    #include "ntddk.h"

    NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
    {
    return STATUS_UNSUCCESSFUL;
    }
  10. F7调试生成,生成文件在C:\Users\alvin\Documents\Visual Studio 2010\Projects\HelloDriver\Driver_1\HelloDriver.sys

可以把鼠标放到类型上面去看属性,也可以使用F12进去查看。

2 编译调试驱动程序

2.1 驱动开发流程

驱动的开发流程:
编写代码 ----> 生成.sys文件 ----> 部署 ----> 启动 ----> 停止 ----> 卸载。

2.2 .PDB文件

  1. PDB文件是在我们编译工程的时候产生的,它是和对应的模块(exe、dll或sys)一起生成出来的。
  2. 每个模块编译的时候都可以生成自己的PDB文件。比如.exe/.dll/.sys等等。

PDB文件是编译驱动的同时生成的调试信息文件,它可以帮助我们像调试应用程序一样调试驱动程序。其实之前我们已经使用过PDB,我们配置双机调试环境时,在物理机上安装了符号文件,并在Windbg中导入过。

有了PDB,我们就可以知道当前汇编语句属于哪个函数,程序定义的结构体等关键信息,说一句题外话,软件发布的时候,切记不要把PDB也发布出去,因为这会给别人破解你的软件提供巨大便利。

2.3 配置PDB路径

我们要调试一个驱动程序,就要将这个驱动程序的PDB文件的路径配置到Windbg的符号文件路径“Symbol FIle Path…”中,然后执行.reload

步骤:

  1. 在Win7下编译生成XP的驱动文件sys

  2. 找到该sys一起发布的.pdb文件。

  3. 将所在目录路径复制到Windbg的符号文件路径下(记得加分号;),然后执行.reload。(C:\Users\alvin\Documents\Visual Studio 2010\Projects\20220112_01_FirstDriver\First_20220112)

    1.png

    2.png

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
#include <ntddk.h>

// 卸载函数
VOID DriverUnload(PDRIVER_OBJECT driver)
{
DbgPrint("驱动程序停止运行了.\r\n");
}

// 入口函数,相当于main
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
{
// 下断点
__asm
{
int 3
mov eax,eax
mov ecx,ecx
}
// 驱动程序入口
DbgPrint("A1v1n的第一个驱动程序.\r\n");

// 设置一个卸载函数,便于退出
driver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

可以看到,触发断点了,观察windbg窗口:多了一个源代码窗口,现在可以像调试应用程序一样调试驱动了。

3.png

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说明文档中只包含了内核模块导出的函数,对于未导出的函数,则不能直接使用。

如果要使用未导出的函数,只要定义一个函数指针,并且为函数指针提供正确的函数地址就可以使用了。有两种办法都可以获取为导出的函数地址:

  1. 特征码搜索。
  2. 解析内核PDB文件(使用uf 函数名称u 地址)。

4.3 内核函数前缀

表2.3列出了一些常用的标识性前缀。(《Windows内核原理与实现》2.3.1)

12.png

4.4 内核基本数据类型

WDK数据类型在ntdef.h中定义,下面列举部分,注意,并没有UINT

在内核编程的时候,强烈建议大家遵守WDK的编码习惯,不要这样写:unsigned long length;

习惯使用WDK自己的类型(列举部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define VOID void
typedef char CHAR;
typedef short SHORT;
typedef long LONG;

typedef unsigned char UCHAR;
typedef unsigned short USHORT;
typedef unsigned long ULONG;
typedef QUAD UQUAD;

typedef void *PVOID;
typedef UCHAR *PUCHAR;
typedef USHORT *PUSHORT;
typedef ULONG *PULONG;
typedef UQUAD *PUQUAD;

4.5 NTSTATUS返回值

4字节大小,类型定义如下:

1
typedef LONG NTSTATUS

当你调用的内核函数,如果返回的结果不是STATUS_SUCCESS,就说明函数执行中遇到了问题,具体是什么问题,可以在ntstatus.h文件中查看。

大部分内核函数的返回值都是NTSTATUS类型,如:

1
2
3
NTSTATUS PsCreateSystemThread();    
NTSTATUS ZwOpenProcess();
NTSTATUS ZwOpenEvent();

这个值能说明函数执行的结果,比如:

1
2
3
STATUS_SUCCESS			0x00000000	成功		
STATUS_INVALID_PARAMETER 0xC000000D 参数无效
STATUS_BUFFER_OVERFLOW 0x80000005 缓冲区长度不够
  • 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
2
3
4
5
6
__try{
//可能出错的代码
}
__except(filter_value) {
//出错时要执行的代码
}

出现异常时,可根据filter_value的值来决定程序该如果执行,当filter_value的值为:

1
2
3
EXCEPTION_EXECUTE_HANDLER(1)		代码进入except块
EXCEPTION_CONTINUE_SEARCH(0) 不处理异常,由上一层调用函数处理
EXCEPTION_CONTINUE_EXECUTION(-1) 回去继续执行错误处的代码

4.7 常用的内核内存函数

C语言 内核
malloc ExAllocatePool
memset RtlFillMemory
memcpy RtlMoveMemory
free ExFreePool

4.8 内核字符串及常用字符串函数

为了提高安全性,内核中的字符串不再是字符串首地址指针作为开始,0作为结尾,而是采用了以下两个结构体:

ANSI_STRING字符串:

1
2
3
4
5
6
typedef struct _STRING
{
USHORT Length; //字符串长度(单位:字节)
USHORT MaximumLength; //字符串最大长度
PCHAR Buffer; //字符串首地址
}STRING;

UNICODE_STRING字符串:

1
2
3
4
5
6
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaxmumLength;
PWSTR Buffer;
} 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来表示优先级,数值越大,优先级越高。

  1. Windowst运行在一个高并发的环境当中,任一时刻,每一个处理器,都运行在某个IRQL之上
  2. 每个处理器的IRQL决定了它如何处理中断,以及允许接收哪些中断。优先级低的可以被优先级高的打断
  3. IRQL也被用于实现对一些内核模式数据结构的同步访问。例如,与线程调度相关的数据结构只有在DISPATCH_LEVEL上才可以访问。

优先级如下图:

4.png

  1. 0,PASSIVE_LEVELPASSIVE_LEVEL(被动级别)代表了最低的IRQL,运行在PASSIVE_LEVEL的线程可以被任何高IRQL的事情打断,所有的用户模式代码都运行在此IRQL上
  2. 1,APC_LEVELAPC_LEVEL(APC级别)仅仅比PASSIVE_LEVEL高,这也正是在一个线程中插入一个APC可以打断该线程(如果它正在PASSIVE_LEVEL上)运行的原因。
  3. 2,DISPATCH_LEVEL:该级别是一个重要的区分点,它代表了线程调度器正在运行。一个处理器运行在此IRQL上,说明它正在分配处理器的计算资源,有可能正在做两件事情之一:
    • 正在进行线程调度,比如选择新的线程。
    • 正在处理一个硬件中断的后半部分(不那么紧急的部分),在Windows中,这被称为DPC(Deferred Procedure Call),DPC与线程无关。
  4. 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需要特别注意:

  1. DISPATCH_LEVEL是最高的软件中断IRQL,它低于所有硬件中断的IRQL。
  2. 运行在DISPATCH_LEVEL上的线程,不会被其他的线程抢占,只有可能被更高级别的中断抢占
  3. DISPATCH_LEVEL或更高的IRQL上,不能访问换页内存区,因为一旦发生换页动作,就需要执行操作,从磁盘上读入页面,期间至少有一个等待动作。
  4. 除了系统调度器以外的其他内核代码,一旦运行在这个级别或更高的I迟IRQL上,则不得切换到其他线程(比如,等待一个同步对象),因为线程切换是通过系统调度器来完成的,而系统调度器运行在DISPATCH_LEVEL上。

4.a 练习1:读取GDT、IDT

申请一块内存,并在内存中存储GDT、IDT的所有数据。然后在Debugview中显示出来,最后释放内存。

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
#include <ntddk.h>
#include <ntdef.h>

// 卸载函数
VOID DriverUnload(PDRIVER_OBJECT driver)
{
DbgPrint("驱动程序停止运行了.\r\n");
}

// 入口函数,相当于main
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
{
UCHAR GDT[6] = {0};
UCHAR IDT[6] = {0};
ULONG GDTAddr = 0; ULONG IDTAddr = 0;
USHORT GDTLen = 0; USHORT IDTLen = 0;
PUCHAR pBuffer = NULL;
USHORT i = 0;
DbgPrint("主函数开始运行...\r\n");
// 读取GDT、IDT寄存器的值
__asm
{
sgdt fword ptr ds:[GDT];
sidt fword ptr ds:[IDT];
}
GDTLen = *(PUSHORT)GDT;
IDTLen = *(PUSHORT)IDT;
GDTAddr = *(PULONG)(GDT+2);
IDTAddr = *(PULONG)(IDT+2);

/*
PVOID
ExAllocatePoolWithTag(
IN POOL_TYPE PoolType,
IN SIZE_T NumberOfBytes,
IN ULONG Tag
);
*/
//VirtualAlloc
pBuffer = (PUCHAR)ExAllocatePoolWithTag(PagedPool,GDTLen + IDTLen,20220114);
if(!pBuffer)
{
DbgPrint("内存申请失败!\n");
driver->DriverUnload = DriverUnload;
return 0x2;
}
//memset
RtlFillMemory(pBuffer,GDTLen + IDTLen,0x0);
//memcpy
RtlMoveMemory(pBuffer,(PUCHAR)GDTAddr,GDTLen);
RtlMoveMemory(pBuffer+GDTLen,(PUCHAR)IDTAddr,IDTLen);

DbgPrint("打印GDT\r\n");
for(i = 0;i < GDTLen;i+=16)
{
DbgPrint("%08X %08X`%08X %08X`%08X\r\n",GDTAddr+i,((PULONG)pBuffer+i)[1],((PULONG)pBuffer+i)[0],((PULONG)pBuffer+i)[3],((PULONG)pBuffer+i)[2]);
}

DbgPrint("打印IDT\r\n");

//pBuffer = pBuffer + GDTLen; //这行执行会蓝屏,不知道为什么

for(i = 0;i < IDTLen;i+=16)
{
DbgPrint("%08X %08X`%08X %08X`%08X\r\n",IDTAddr+i,((PULONG)(pBuffer+GDTLen)+i)[1],((PULONG)(pBuffer+GDTLen)+i)[0],((PULONG)(pBuffer+GDTLen)+i)[3],((PULONG)(pBuffer+GDTLen)+i)[2]);
}

//释放内存
ExFreePool(pBuffer);

// 设置一个卸载函数,便于退出
driver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

5.png

4.b 练习2:处理字符串

<1> 初始化一个字符串

<2> 拷贝一个字符串

<3> 比较两个字符串是否相等

<4> ANSI_STRING与UNICODE_STRING字符串相互转换

结果:拷贝函数RtlCopyString无法成功执行拷贝,不知道为啥。

⚠️注意:要是用变量的话需要在函数开头就进行定义,最好进行声明,不能是用变量的时候才进行定义,否则会失败

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
#include <ntddk.h>
#include <ntdef.h>

// 卸载函数
VOID DriverUnload(PDRIVER_OBJECT driver)
{
DbgPrint("驱动程序停止运行了.\r\n");
}

// 入口函数,相当于main
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
{
STRING StrSour = {0};
STRING StrDest = {0};
UNICODE_STRING UnicodeStr = {0};
UNICODE_STRING UnicodeStrDest = {0};
LONG lRet = 0;
NTSTATUS NtRet;

//初始化一个ANSI字符串
RtlInitAnsiString(&StrSour,"This is ANSI String!");
DbgPrint("StrSour内容为:%s,长度为:%d,最大长度为:%d\n",StrSour.Buffer,StrSour.Length,StrSour.MaximumLength);

//字符串拷贝
RtlCopyString(&StrDest,&StrSour);
DbgPrint("StrDest内容为:%s,长度为:%d,最大长度为:%d\n",StrDest.Buffer,StrDest.Length,StrDest.MaximumLength);

//比较两个字符串
lRet = RtlCompareString(&StrDest,&StrSour,TRUE); //TRUE:区分大小写
if(lRet == 0)
{
DbgPrint("两个字符串一样!\n");
}
if(lRet > 0)
{
DbgPrint("第一个字符串大于第二个字符串!\n");
}else
{
DbgPrint("第二个字符串大于第二个字符串!\n");
}

//ANSI_STRING与UNICODE_STRING字符串相互转换
NtRet = RtlAnsiStringToUnicodeString(&UnicodeStr,&StrSour,TRUE);//TRUE:是否需要对被转换的字符串分配内存
DbgPrint("转换返回值:0x%X\n",NtRet);
if(NtRet == STATUS_SUCCESS)
{
DbgPrint("转换后内容为:%ws,长度为:%d,最大长度为:%d\n",UnicodeStr.Buffer,UnicodeStr.Length,UnicodeStr.MaximumLength);
}

//字符串拷贝
RtlCopyUnicodeString(&UnicodeStrDest,&UnicodeStr);
DbgPrint("转换后内容为:%ws,长度为:%d,最大长度为:%d\n",UnicodeStrDest.Buffer,UnicodeStrDest.Length,UnicodeStrDest.MaximumLength);

// 设置一个卸载函数,便于退出
driver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

6.png