0707:更新驱动遍历、隐藏。更换图床
0712:更新驱动通信
0922:更新内存加载
内存加载驱动的代码放在评论区了
WDK和VS安装参考文章:X86内核笔记0配置双机调试环境
打开VS2017,新建项目选择Visual C++ -> Windwos Drivers -> Legacy -> Empty WDM Driver

右键SourceFiles目录,新建项。创建一个扩展名为C的C++文件。(不要用cpp扩展名)。文件名随意起,不是非要和项目名一样。

在.c文件中先引入头文件 ntifs.h
。
删除INF文件

如果引入头文件出现找不到头文件,就如下图设置一下SDK版本。

然后设置一些项目属性:
驱动大体分为三种,分别是:NT式驱动、WDM式驱动、WDF式驱动(KWDF内核驱动,UWDF用户驱动)。
NT虚拟驱动,老式驱动,从WIN95开始使用NT式驱动。 若所开发的驱动不与硬件打交道,建议使用NT式驱动或WDM式驱动。如果NT式驱动出现了绑定设备的情况,该驱动将无法卸载。只能通过重启系统进行卸载。对于服务器来说重启很伤。
相对于NT式驱动来讲,WDM式驱动支持卸载(热拔插)。无需重启即可卸载。并且WDM式驱动对于NT式驱动进行了一些封装和优化。本质区别不大。
WDF式驱动相较前两种,其最大的意义是简化开发。不像NT与WDM驱动那么底层化。WDF式驱动将WDM式驱动进行了封装,做成了一套架构,使得开发驱动变得更简单。同时带来的弊端就是无法掌控底层。
由于开发简便,不容易蓝屏,所以公司开发驱动一般选用WDF式驱动。
想要学习WDF式驱动,需要了解COM相关知识。
只有系统中存在WDFLDR.sys驱动,我们编写的WDF驱动才可以跑起来。并且项目中需要一个inf文件,NT/WDM式驱动则不需要这个inf文件。

1
2
3
4
5
|
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject,PUNICODE_STRING RegistryPath){
/ / 代码
return STATUS_UNSUCCESSFUL;
}
|
DriverEntry是我们写代码时的入口函数。其编译生成的sys文件真正的入口点并不是DriverEntry。在IDA中可以看到驱动真正的入口点函数是GsDriverEntry。其内部调用了我们的DriverEntry函数。

如果不想让编译器生成GsDriverEntry而是直接将入口函数设置为DriverEntry,可以按照下图设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void UnloadDriver(PDRIVER_OBJECT driver);
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
DbgBreakPoint(); / / 相当于 __asm{ int 3 }
DbgPrint( "驱动加载了。\r\n" ); / / 驱动的打印函数,相当于 3 环的printf
DriverObject - >DriverUnload = UnloadDriver; / / 为驱动指定卸载函数
return STATUS_SUCCESS;
}
/ / 驱动卸载函数
void UnloadDriver(PDRIVER_OBJECT driver) {
DbgPrint( "驱动停止了。\r\n" );
}
|
如果想要打印字符串对象中的字符串,可以使用如下格式:
1
2
3
4
|
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING pReg) {
DbgPrint( "-------%wZ--------" ,pReg); / / 传入字符串对象指针。
return STATUS_SUCCESS;
}
|
点击生成解决方案即可。若报一些格式错误,就删除一些特殊符号之类的东西。
使用InstDrv.exe加载驱动。使用DbgView.exe查看输出(必须选中监视核心,否则无法监视驱动层输出)。
通过调用函数DbgBreakPoint为驱动增加一个断点。这个函数相当于int 3指令。这样我们就可以在windbg中断下。并且Windbg会自动识别PE结构中的PDB路径,自动加载PDB文件识别出我们的源码。

如果windbg调试过程中,出现了刷屏的情况,执行以下命令可以关闭刷屏。
1 |
kd> ed nt!Kd_SXS_Mask 0 ;ed nt!Kd_FUSION_Mask 0
|
在成功断在我们的代码中后,在Windbg中查看驱动对象结构。

-
Type:驱动对象类型。
-
Size:驱动对象大小
-
DeviceObject:设备对象,我们这里没添加设备,因此是null
-
DriverStart:驱动文件基址,也就是PE格式中的ImageBase。通过db命令可以看到4D 5A。
-
DriverSize:驱动模块大小,也就是PE格式中的SizeOfImage。
-
DriverExtension:驱动扩展对象。使用dt命令查看该对象
1
2
3
4
5
6
7
8
|
kd> dt _DRIVER_EXTENSION 0x8831b790
ntdll!_DRIVER_EXTENSION
+ 0x000 DriverObject : 0x8831b6e8 _DRIVER_OBJECT
+ 0x004 AddDevice : (null)
+ 0x008 Count : 0
+ 0x00c ServiceKeyName : _UNICODE_STRING "hellodriver"
+ 0x014 ClientDriverExtension : (null)
+ 0x018 FsFilterCallbacks : (null)
|
- DriverObject:指向当前驱动对象首地址。
- ServiceKeyName:驱动服务注册表文件夹名。
-
DriverName:驱动名,也就是驱动的文件名前面加个\Driver\。这个名字是个字符串结构体。
查看该字符串结构:
1
2
3
4
5
6
|
kd> dt _UNICODE_STRING 8831b6e8 + 1c
ntdll!_UNICODE_STRING
"\Driver\hellodriver"
+ 0x000 Length : 0x26 / / 字符串长度
+ 0x002 MaximumLength : 0x26 / / 字符串最大长度
+ 0x004 Buffer : 0x87fc36c8 "\Driver\hellodriver" / / 字符串内容
|
-
HardwareDatabase:驱动服务注册表路径。前往注册表查看该路径,可以发现一个名为“hellodriver”的文件夹,这就是我们的驱动。

- DisplayName:驱动名
- ErrorControl:当驱动加载失败时会设置这个值。
- ImagePath:驱动文件路径。\??\是设备路径,我们平时访问各种文件夹其实都带这个\??\,只是windows底层帮我们补充了。
- Start:驱动加载类型。手动启动为3,开机自启为2,BIOS自启为1。
- Type:服务类型。1为驱动。
-
DriverInit :驱动入口点,也就是PE文件的AddressOfEntryPoint。
-
DriverUnload:驱动卸载函数地址。
加载驱动大体分为两种:服务加载和直接加载。实际应用中可以将两种方法都利用上。
- 调用OpenSCManager打开服务控制。
- 调用CreateService创建服务。实际上就是创建注册表相关键值。在执行完该API后,驱动已经被注册为服务了。这时我们通过CMD执行net start XXXX也可以加载我们的驱动。
- 调用OpenService打开现有服务。
- 调用StartService启动服务
这种方式实际上加载该驱动的进程,并不是调用API的进程。而是通过API向系统通知我要加载一个驱动。系统进程接收到通知后加入到系统中的一个队列。并由系统进程在某时某刻加载该驱动。
也就是这种方式是通知系统进程来进行加载。
调用ZwLoadDriver或NtLoadDriver加载一个已被正确注册的驱动。
这种方法需要我们自己手动去注册表内注册该驱动的相关信息。这样该驱动才可以被加载。
直接加载的方式在调用API后就会直接加载该驱动,所以该驱动的加载者就是调用该API的进程。相比于服务加载会留下痕迹。
编写两个驱动A和B,在A中定义全局变量值为100,打印A的地址pA。在B中打印pA的数据,观察是否与A中定义的相同。
A代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void UnloadDriver(PDRIVER_OBJECT driver);
UINT32 i = 100 ;
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
DbgPrint( "i addr = %08x\r\n" , &i);
DriverObject - >DriverUnload = UnloadDriver;
return STATUS_SUCCESS;
}
/ / 驱动卸载函数
void UnloadDriver(PDRIVER_OBJECT driver) {
DbgPrint( "驱动停止了。\r\n" );
}
|
B代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void UnloadDriver(PDRIVER_OBJECT driver);
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
PUINT32 p = (PUINT32) 0x8e315000 ;
DbgPrint( "i value = %d\r\n" , * p);
DriverObject - >DriverUnload = UnloadDriver;
return STATUS_SUCCESS;
}
/ / 驱动卸载函数
void UnloadDriver(PDRIVER_OBJECT driver) {
DbgPrint( "驱动停止了。\r\n" );
}
|
在驱动中写代码与3环不同,一些数据类型及常用API也最好使用驱动开发专用的版本。这算是一种代码规范。
在驱动中,原数据类型int char等均被封装、重定义。在驱动开发中应使用如下数据类型:
1
2
3
4
5
6
7
8
9
10
11
|
UINT8,PUINT8 - > unsigned char
UINT16,PUINT16 - > unsigned short
UINT32,PUINT32 - > unsigned int
UINT64,PUINT64 - > unsigned __int64
INT8,PINT8 - > char
INT16,PINT16 - > short
INT32,PINT32 - > int
INT64,PINT64 - > __int64
LONG32,PLONG32 - > int
ULONG32,PULONG32 - > unsigned int
DWORD32,PDWRD32 - > int
|
绝大多数内核函数都会有一个返回值,类型为NTSTATUS
。该类型本质就是一个LONG。
如GetLastError这种取错误码的函数,取到的值其实就是NTSTATUS转化后的错误码。
常用的NTSTATUS宏如下,负数(大于0X80000000)的返回值为错误,大于等于0为成功
1
2
3
4
5
6
|
STATUS_SEVERITY_SUCCESS 0x0
STATUS_SEVERITY_INFORMATIONAL 0x1
STATUS_SEVERITY_WARNING 0x2
STATUS_SEVERITY_ERROR 0x3
STATUS_UNSUCCESSFUL 0xC0000001
STATUS_ACCESS_VIOLATION 0xC0000005
|
同时有一个宏用于判断返回值是成功还是失败:
1 |
NT_SUCCESS(NTSATUS类型参数) / /
|
在内核开发中,字符串不要定义为char* x = "xx",WDK为我们准备了一些字符串相关的API。
1
2
3
|
UNICODE_STRING uStr = { 0 }; / / 定义一个 unicode 字符串,类型为UNICODE_STRING
STRING aStr = { 0 }; / / 定义一个ascii字符串,类型为STRING
ANSI_STRING aStr = { 0 }; / / 所有ANSI与直接STRING 作用相同
|
1
2
3
|
RtlInitUnicodeString(&uStr,L "unicode string" ); / / 初始化 unicode 字符串,为其赋值。不会申请内存。
RtlInitString(&aStr, "ascii string" ); / / 初始化ascii字符串,为其赋值,不会申请内存。
RtlInitAnsiString(&aStr, "ascii string" ); / / 初始化ascii字符串,为其赋值,不会申请内存。
|
1
2
|
RtlAnsiStringToUnicodeString(&uStr,&aStr,true); / / 将ascii字符串转为 unicode 字符串,无需为 unicode 字符串做初始化,第三个参数为true则自动申请内存。为false则不申请,仅修改 unicode 现有空间。若为true,则需要手动释放字符串内存。
RtlUnicodeStringToAnsiString(); / / 将 unicode 字符串转为ascii字符串,用法与上面相同。
|
1
2
|
RtlFreeUnicodeString(); / / 释放 unicode 字符串内存,当字符串初始化中为其分配了内存时,需要释放内存。
RtlFreeAnsiString(); / / 释放ascii字符串内存,当字符串初始化中为其分配了内存时,需要释放内存。
|
1
2
3
4
5
|
char aStr[ 0x1000 ] = { 0 };
RtlStringCbPrintfA(aStr, 0x1000 , "%d---%s" , 123 , "test" ); / / 参数 1 : Ascii字符串指针
wchar uStr[ 0x1000 ] = { 0 };
RtlStringCbPrintfW(uStr, 0x1000 , L "%d---%s" , 123 , L "test" ); / / 参数 1 : Unicode 字符串指针
|
1
2
|
RtlCompareUnicodeString(&uStr1,&uStr2,TRUE); / / 比较两个 unicode 字符串是否相等,true忽略大小写
RtlCompareString / / 比较两个ascii字符串是否相等
|
1
2
3
4
|
ExAllocatePool( type ,size); / / type :内存类型,PagePool和NonPagePool,分别为分页内存和非分页内存。
/ / 分页内存:后面章节会详细说,暂时理解为不可执行的内存
/ / 非分页内存:后面章节会详细说,暂时理解为可执行的内存 通常填NonPagePool,对应属性为PTE的XD / NX位。
ExAllocatePoolWithTag( type ,size,tag); / / tag:内存标志,四个字节最多,如 'test' ,为申请的内存起个名字。用单引号包含,内部最终转为 16 进制数据。
|
1
2
3
4
5
|
RtlFillMemory(pointer,length,value); / / 相当于memset
RtlEqualMemory(pointer,Source,Length) / / 相当于memcmp结果取反
RtlMoveMemory(pointer,Source,Length) / / 相当于memmove
RtlCopyMemory(pointer,Source,Length) / / 相当于memcpy
RtlZeroMemory(pointer,Length) / / 相当于memset第二参数为 0.
|
1 |
ExFreePool(pointer); / / 释放内存
|
1
2
3
4
5
6
7
|
/ / 驱动代码中的延迟不可以使用Sleep,而是KeDelayExecutionThread
LARGE_INTEGER li = { 0 }; / / 时长结构。
li.QuadPart = - 10000 * 5000 ; / / 时间单位 负数代表相对时间 正数代表绝对时间。 5000 代表 5 秒。
KeDelayExecutionThread(KernelMode,FALSE,&li);
/ / 第一个参数:延迟模式,我们这里选内核模式
/ / 第二个参数:强制唤醒。如果为FALSE,那么休眠时间未结束前,不会被唤醒。
/ / 第三个参数:延迟时长。
|
1
2
3
4
5
6
7
8
9
10
11
|
/ / 线程函数
VOID myThreadFun(_In_ PVOID StartContext) {
/ / 线程函数代码
}
HANDLE tHandle = NULL;
NTSTATUS tRet = PsCreateSystemThread(&tHandle,THREAD_ALL_ACCESS,NULL,NULL,NULL, myThreadFun,NULL);
/ / 最后一个参数是线程函数启动参数。
if (NT_SUCCESS(tRet)){
ZwClose(tHandle); / / 相当于CloseHandle
}
|
[注意]看雪招聘,专注安全领域的专业人才平台!
最后于 2021-9-22 17:11
被SSH山水画编辑
,原因: 更新驱动通信