内核编程
作者:互联网
内核编程
一. 驱动程序概述
1. 介绍
- 每一个进程都有一个4GB的进程空间,储存了进程需要的所有代码和数据,并分为用户空间和内核空间
- 不同进程的用户空间相互隔离,互不影响,共享内核空间,操作系统通过内核层代码给应用程序提供支持
- 用户空间代码不能直接访问内核空间,执行的命令也有限制,内核空间代码可以执行特权指令,用户层访问内核层也需要通过特定的接口
- 我们编写的驱动程序,并非是平时我们看到的一个能够运行的程序,而是加载到内核空间中,成为操作系统的一部分,作为应用程序与硬件的桥梁,为应用程序提供支持的一个模块
2. 分类
- 仅仅作为Windows操作系统内核的扩展,而并非是使得一个硬件工作起来,我们将它称之为内核程序
- 针对于某种硬件,使得其能够很好的工作起来
- NT
- WDM驱动模型(Windows Driver Model)
- WDF驱动模型(Windows Driver Foudation)
1.首先,先从基础的东西说起,开发WINDOWS下的驱动程序,需要一个专门的开发包,如:开发JAVA程序,我们可能需要一个JDK,开发WINDOWS应用程序,我们需要WINDOWS的SDK,现在开发WINDOWS下的驱动程序,我们需要一个DDK/WDK。
2.DDK(Driver Developer Kit)和WDK(Windows Driver Kit)的区别:
这个要说说驱动相关的一些历史:
1).95/98/ME下,驱动模型为:Vxd,相关资料可以看《编程高手箴言》的前几个章节,里面有很详细的介绍,虽然这个东西已经过时,但大概看看还是会增长见识的。
2).2000/XP/2003下,Windows采用WDM驱动模型(Windows Driver Model),开发2000/XP/2003的驱动开发包为:DDK。
WDM驱动无非是微软在NT式驱动之上进行了扩充,过滤驱动也不例外 。
3).Vista及以后版本,采用了WDF驱动模型(Windows Driver Foudation),对应的开发包:WDK。
其实WDK可以看做是DDK的升级版本,现在一般的WDK是包含以前DDK相关的功能,现在XP下也可以用WDK开发驱动,WDK能编译出2000-2008的各种驱动。
3.Vxd驱动文件扩展名为:.vxd。
WDM和WDF驱动文件扩展名为:.sys。
4、WDM 是 Win32设备驱动程序体系结构。
Windows设备驱动程序,过去是WDM(Windows Driver Model)框架,编程复杂,初学者难以掌握其编程要领。为了解决这一问题,微软对WDM驱动程序的架构做了改进,形成了全新的WDF(Windows Driver Foundation)框架结构。它提供了面向对象和事件驱动的驱动程序开发框架,大大降低了开发难度。从现在开始,掌握Windows设备驱动程序的开发人员,由过去的“专业”人士,将变为“普通”大众。
WDF驱动程序包括两个类型,一个是内核级的,称为KMDF(Kernel-Mode Driver Framework),为SYS文件;另一个是用户级的,称为UMDF(User-Mode Driver Framework),为DLL文件。
5、
ddk 和wdk
ddk是基于wdm驱动模型的,而wdk是基于WDF驱动模型的,wdm驱动模型和wdf驱动模型的最大的区别是:
1)wdf驱动框架对WDM进行了一次封装,WDF框架就好像C++中的基类一样,且这个基类中的model,IO model ,pnp和电源管理模型;且提供了一些与操作系统相关的处理函数,这些函数好像C++中的虚函数一样,WDF驱动中能够对这些函数进行override;特别是Pnp管理和电源管理!基本上都由WDF框架做了,而WDF的功能驱动几乎不要对它进行特殊的处理;
2)WDF驱动模型 与WDM驱动模型的另外一个主要区别是:
WDF 驱动采用队列进行IO处理,而WDM中将所有的IO操作都用默认的队列进行处理,如果要进行IRp同步,必须使用StartIO;
3)WDF是面向对象的,而WDM是面向过程的,WDF提供对象的封装,如将IRP封装成WDFREQUEST,对象提供方法和Event。
5)usb设备的读写;
当应用程序使用ReadFile或WriteFile进行读写时,首先将
UsbBuildInterruptOrBulkTransferRequest将构建urb请求,然后通过IoCallDriver发送给底层usb 总线驱动;
对于WDF来说,WdfUsbTargetPipeFormatRequestForRead 格式化读请求,然后使用WdfRequestSend 发送给底层Usb总线驱动;
对WDM和WDF的usb的读写都可以设置完成例程;
3. 安装SDK
-
VS中工具:工具与功能查看SDK版本
-
MSDN下载对应WDK版本
-
新建项目WDM(扩展版NT)
- 驱动程序
1. 最简单的驱动程序
#include <ntddk.h>
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
return STATUS_SUCCESS;
}
2. 简单驱动程序
- 包含ntddk.h(wdm.h)头文件
- DriverEntry入口函数
- DriverUnload驱动卸载函数(资源回收,清理)
- 返回值STATUS_SUCCESS(加载成功条件)
// 0. 由于驱动程序是和操作系统相关的,所以并不是一个驱动程序能够被
// 所有的系统加载,我们需要在项目属性中配置目标系统的版本。如果漏
// 掉了这一步,在加载驱动的瞬间,会蓝屏 !!!!!!
// 1. 需要包含提供内核结构体和基本函数的头文件,也可以使用 wdm.h
#include <ntddk.h>
// 提供驱动程序的卸载函数,在卸载驱动的时候会被自动的调用
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
KdPrint(("driver unloading...\n"));
UNREFERENCED_PARAMETER(DriverObject);
}
// 2. 定义驱动程序的入口函数,该函数必须没有名称粉碎机制
NTSTATUS DriverEntry(
PDRIVER_OBJECT DriverObject, // 驱动对象,类似实例句柄,
PUNICODE_STRING RegistryPath) // 字符串指针,指向驱动在注册表的配置信息
{
// 由于驱动程序的编写要求十分的严格,任何可能产生问题的语句都会被视
// 为错误,所以我们对于为引用的对象应该使用以下的宏进行说明。
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
// Windows 提供了函数 DbgBreakPoint 用于设置断点,当处于双机调试
// 状态,该异常会被内核调试器 windbg 接收到,可以用于继续调试
DbgBreakPoint();
// 通过驱动对象中的一个字段设置所属驱动的卸载函数,如果没有提供这个
// 函数驱动就无法被卸载,可以故意不提供卸载函数来防止被恶意卸载
DriverObject->DriverUnload = DriverUnload;
// 在驱动程序中不能直接使用输出函数,但是可以用调试信息,KdPrint 函
// 数只会在当前驱动为 Debug 版本时被调用
DbgPrint
KdPrint(("driver loading...\n"));
// 3. 想让当前的驱动程序加载成功,必须返回 STATUS_SUCCESS,如果返
// 回了其它值,对应的驱动会加载失败
return STATUS_SUCCESS;
}
显示 kdPrint reg文件
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter]
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter]
"DEFAULT"=dword:0000000f
重启生效
关闭安全警告 (将警告视为错误)
1. C++
2. 链接器->常规
3. 加载驱动对象
驱动编译完成之后,不同于exe文件,sys不能够随便的运行,只能通过windows提供的接口加载驱动程序,在windows内核程序的加载是通过服务来实现的
4. 驱动调试
- 驱动程序不像应用程序,可以将文字输入到窗口DC和打印到控制台,需要通过调试信息(DebugView和Windbg接收),在内核程序编写使用KdPrint(Dbgprint)输出调试信息,在Debug版本会输出,由于Win7在对调试信息做出了过滤处理,需要设置
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter]
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter]
"DEFAULT"=dword:0000000f
- 驱动程序并不会自己断下,为了能够中断自己的驱动程序,得在需要调试的地方插入一个 __asm int 3 断点或者DbgBreakPoint()
调试工具:
VirtualKD是一个开源的调式辅助软件,能帮助WinDbg与VmWare建立快速通讯http://virtualkd.sysprogs.org
VirtualKD分两部分,在target子文件夹中的所有文件需要拷贝到虚拟机中,并在虚拟机中运行vminstall.exe点击Install安装虚拟机部分,安装完成之后会在目标机器中添加一个新启动项,打开vmmon64配置调试器路径即可
- 蓝屏处理
- 内核编程通常是指在操作系统内核中进行程序编写的一种编写行为,其代码均运行在内核层
- 集反汇编,逆向,PE文件结构到系统结构,硬件层X86结构都需要掌握+
- Bule Screen Of Death(蓝屏处理)
可以使用windbg调式转储文件,分析崩溃原因
windbg使用命令 !analyze -v,将分析dump文件,并显示程序崩溃处于的代码行。
二. 驱动对象
- 在windows系统内核中,一样需要以面向对象的观点看待整体结构,对象之间通过消息相互作用
- windows内核编程中有三个重要的概念
- 驱动对象(程序)
- 设备对象(窗口)
- IRP(消息)
1. 驱动对象(程序)
与容器类似,代表着你编写的这个驱动程序,就如同windows编程时的实例句柄一样,代表了整个应用程序一样
当驱动加载时,对象管理器便会生成一个驱动对象,并调用DriverEntry,将驱动对象传入,用户可以在DriverEntry中为驱动对象中未初始化的各个字段赋值,为整体的驱动程序定下总基调,如:设定卸载驱动的函数,设定各个消息的处理函数
// 0. 由于驱动程序是和操作系统相关的,所以并不是一个驱动程序能够被
// 所有的系统加载,我们需要在项目属性中配置目标系统的版本。如果漏
// 掉了这一步,在加载驱动的瞬间,会蓝屏 !!!!!!
/*
struct _DRIVER_OBJECT
{
驱动对象: 类似于 GUI 程序中的应用程序本身,不能直接和 R3 进行交互,需要依赖设备对象传递 IRP
SHORT Type; //0x0 // 表示结构体的类型,所以的驱动对象都为 4
SHORT Size; //0x2 // 驱动对象的大小,所占用的字节数
struct _DEVICE_OBJECT* DeviceObject; //0x4 // * 当前驱动对象下所有设备对象组合成的单向链表,每个驱动对象都会有一个或多个设备对象,所有的对象是链表的形式串联起来,这里指第一个设备对象,可以遍历所有设备对象
ULONG Flags; //0x8 // 标志位
VOID* DriverStart; //0xc // 当前对象在内存中的起始位置
ULONG DriverSize; //0x10 // 驱动对象所占用的总空间
VOID* DriverSection; //0x14 // * 指向 LDR 链表,可以用于遍历系统下的所有驱动对象
struct _DRIVER_EXTENSION* DriverExtension; //0x18 // 指向扩展空间的指针
struct _UNICODE_STRING DriverName; //0x1c // * 驱动对象的名称,是一个字符串结构体,采用UNICODE编码字符串,一般形式为\Drivers\[驱动程序名称]
struct _UNICODE_STRING* HardwareDatabase; //0x24 // 在注册表硬件配置表中的一个名称
struct _FAST_IO_DISPATCH* FastIoDispatch; //0x28 // 特殊的派遣函数,在文件过滤驱动中会被使用
LONG(*DriverInit)(struct _DRIVER_OBJECT* arg1, struct _UNICODE_STRING* arg2);
//0x2c // 驱动程序最先执行的代码
VOID(*DriverStartIo)(struct _DEVICE_OBJECT* arg1, struct _IRP* arg2);
//0x30 // 用于驱动 IRP 的串行化处理
VOID(*DriverUnload)(struct _DRIVER_OBJECT* arg1);
//0x34 // * 驱动程序的卸载函数地址,不提供就无法卸载
LONG(*MajorFunction[28])(struct _DEVICE_OBJECT* arg1, struct _IRP* arg2);
//0x38 // * 消息派遣函数组成的列表(类似于消息处理函数)每个指针成员指向一个响应的处理IRP的派遣函数
};
*/
// 1. 需要包含提供内核结构体和基本函数的头文件,也可以使用 wdm.h
#include <ntddk.h>
// LDR 链表中的每一项都是这个结构体,保存了驱动的基本信息
typedef struct _LDR_DATA_TABLE_ENTRY
{
struct _LIST_ENTRY InLoadOrderLinks; //0x0
struct _LIST_ENTRY InMemoryOrderLinks; //0x8
struct _LIST_ENTRY InInitializationOrderLinks; //0x10
VOID* DllBase; //0x18
VOID* EntryPoint; //0x1c
ULONG SizeOfImage; //0x20
struct _UNICODE_STRING FullDllName; //0x24
struct _UNICODE_STRING BaseDllName; //0x2c
// ... 后面还有一些字段,由于用不到,为了节省代码量,直接不考虑
} *PLDR_DATA_TABLE_ENTRY;
/* _LDR_DATA_TABLE_ENTRY结构体
//0x78 bytes (sizeof)
struct _LDR_DATA_TABLE_ENTRY
{
struct _LIST_ENTRY InLoadOrderLinks; 双向链表 //0x0
struct _LIST_ENTRY InMemoryOrderLinks; //0x8
struct _LIST_ENTRY InInitializationOrderLinks; //0x10
VOID* DllBase; //0x18
VOID* EntryPoint; //0x1c
ULONG SizeOfImage; //0x20
struct _UNICODE_STRING FullDllName; //0x24
struct _UNICODE_STRING BaseDllName; //0x2c
ULONG Flags; //0x34
USHORT LoadCount; //0x38
USHORT TlsIndex; //0x3a
union
{
struct _LIST_ENTRY HashLinks; //0x3c
struct
{
VOID* SectionPointer; //0x3c
ULONG CheckSum; //0x40
};
};
union
{
ULONG TimeDateStamp; //0x44
VOID* LoadedImports; //0x44
};
struct _ACTIVATION_CONTEXT* EntryPointActivationContext; //0x48
VOID* PatchInformation; //0x4c
struct _LIST_ENTRY ForwarderLinks; //0x50
struct _LIST_ENTRY ServiceTagLinks; //0x58
struct _LIST_ENTRY StaticLinks; //0x60
VOID* ContextInformation; //0x68
ULONG OriginalBase; //0x6c
union _LARGE_INTEGER LoadTime; //0x70
};
*/
//0x8 bytes (sizeof)
struct _LIST_ENTRY
{
struct _LIST_ENTRY* Flink; //0x0
struct _LIST_ENTRY* Blink; //0x4
};
// 提供驱动程序的卸载函数,在卸载驱动的时候会被自动的调用
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
KdPrint(("driver unloading...\n"));
UNREFERENCED_PARAMETER(DriverObject);
}
// 2. 定义驱动程序的入口函数,该函数必须没有名称粉碎机制
NTSTATUS DriverEntry(
PDRIVER_OBJECT DriverObject, // 驱动对象,类似实例句柄,
PUNICODE_STRING RegistryPath) // 字符串指针,指向驱动在注册表的配置信息
{
// 由于驱动程序的编写要求十分的严格,任何可能产生问题的语句都会被视
// 为错误,所以我们对于为引用的对象应该使用以下的宏进行说明。
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
// Windows 提供了函数 DbgBreakPoint 用于设置断点,当处于双机调试
// 状态,该异常会被内核调试器 windbg 接收到,可以用于继续调试
DbgBreakPoint();
// 通过驱动对象中的一个字段设置所属驱动的卸载函数,如果没有提供这个
// 函数驱动就无法被卸载,可以故意不提供卸载函数来防止被恶意卸载
DriverObject->DriverUnload = DriverUnload;
// 在驱动程序中不能直接使用输出函数,但是可以用调试信息,KdPrint 函
// 数只会在当前驱动为 Debug 版本时被调用
KdPrint(("driver loading...\n"));
// LDR 是一个双向链表,遍历结束的条件是,遍历到的是自己
PLDR_DATA_TABLE_ENTRY current = DriverObject->DriverSection;
PLDR_DATA_TABLE_ENTRY item = DriverObject->DriverSection;
// 创建一个索引,表示当前遍历到的是第几个
int index = 1;
do {
// 输出元素的基本信息 %wZ UNICODE_STRING字符串
KdPrint(("%d: %wZ %wZ\n", index++, &item->BaseDllName, &item->FullDllName));
// 获取遍历到的元素的下一个元素
item = (PLDR_DATA_TABLE_ENTRY)item->InLoadOrderLinks.Flink;
} while (current != item);
// 摘链 断链 front前 back后
current->InLoadOrderLinks.Flink->Blink = current->InLoadOrderLinks.Blink;
current->InLoadOrderLinks.Blink->Flink = current->InLoadOrderLinks.Flink;
// 3. 想让当前的驱动程序加载成功,必须返回 STATUS_SUCCESS,如果返
// 回了其它值,对应的驱动会加载失败
return STATUS_SUCCESS;
}
// 枚举驱动对象
void EnumDriver(PDRIVER_OBJECT pDriver)
{
PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)pDriver->DriverSection;
LIST_ENTRY* pTemp = &pLdr->InLoadOrderLinks;
do
{
PLDR_DATA_TABLE_ENTRY pDriverInfo = (PLDR_DATA_TABLE_ENTRY)pTemp;
KdPrint(("%wZ\n", &pDriverInfo->FullDllName));
pTemp = pTemp->Blink;
} while (pTemp != &pLdr->InLoadOrderLinks);
}
2. 设备对象(窗口)
- 内核程序中,负责与外部进行交流的是设备对象,就如同大部分的应用程序的窗口,在内核编程中,消息都是以IRP的方式传递,能够接收IRP的只有设备对象,一个驱动程序可以有多个设备对象,创建对象的时候需要指明其所属的设备对象是谁。
- 设备对象创建好之后,设备名称是不暴露给用户层的,需要创建一个符号链接
- 创建好一个符号链接后,用户层可以通过CreateFile打开设备,得到句柄
- 之后便可以通过ReadFile和WriteFile与驱动程序进行通讯
1. UNICODE_STRING对象
#include <ntddk.h>
// WINDOWS 中内存的分类(重点)
// 1. 分页文件: 能够被交换到页交换文件(磁盘)的内存[访问可能产生缺页异常]
// 2. 非分页文件: 只能存在于物理内存(内存条)中的内存[访问绝对不会产生异常]
// IRQL(中断请求级别)
// Dispatch(DPC): 软件层面的最高优先级,同一时刻只会运行一个,不能访问分页内存
// APC: 比Dispatch低的一个级别,可以访问分页内存
// Passive: 最低的优先级,大多数代码所运行的级别
// 创建全局变量,用于接受申请到的非分页空间
PVOID Buffer = NULL;
// 驱动对象的卸载函数,如果不设置,就无法卸载
#pragma code_seg("PAGE") // 表示当前的函数被放置在分页内存中,如果
// 短时间内不会用到,可以放置到页交换文件中
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
// 内核程序和普通应用程序不同,即使驱动程序被卸载了,使用的内存
// 也不会被自动的释放,所以需要在卸载的时候释放掉所有的内存,否
// 则会导致内存泄露,直至操作系统重新启动
ExFreePoolWithTag(Buffer, 'nim.');
UNREFERENCED_PARAMETER(DriverObject);
}
// 用于测试字符串结构体的函数
VOID StringTest()
{
// DbgBreakPoint();
// 内核中一般不再采用 C 语言风格的字符串,原因是可能会出现溢出问题
// 下面的字符串结构体就是内核中所采用的表示字符串的方式,没有使用
// 空字符结尾,而是通过其中的一些字段进行描述。
// typedef struct _UNICODE_STRING {
// USHORT Length; // 字符串长度
// USHORT MaximumLength; // 最大长度
// PWSTR Buffer; // 字符串缓存区
// } UNICODE_STRING, * PUNICODE_STRING;
// 初始化的方法之一:在创建的时候直接使用常量进行初始化操作
UNICODE_STRING String = RTL_CONSTANT_STRING(L"CONSTANT_STRING_1");
// 初始化的方法之二:在创建完成之后,再手动的进行初始化
RtlInitUnicodeString(&String, L"CONSTANT_STRING_2");
// 如果想要对整个结构体内的字符串进行修改,需要手动的分配空间(常见写法)
Buffer = ExAllocatePoolWithTag(NonPagedPool, 0x100, 'nim.');
if (Buffer)
{
RtlFillMemory(Buffer, 0x100, 0xAA);
RtlCopyMemory(Buffer, L"UNICODE_STRING_1", 34);
}
// 初始化的方法之三:使用自定义的空间关联到字符串结构体中
String.Buffer = Buffer; // 如果手动赋值,尽量都使用堆空间
String.Length = 17 * 2; // 当前字符串的字节数
String.MaximumLength = 0x100; // 能够存储的最大的字节数
// 可以使用 KdPrint 配合 wZ 输出一个 UNICODE_STRING对象,注意取地址
KdPrint(("%wZ\n", &String));
}
// 对于驱动程序,入口函数的名字是 DriverEntry,必须是没有名称粉碎的
#pragma code_seg("INIT") // 表示当前的函数放置在 INIT 区段中,其中的
// 所有内容都会在驱动初始化的时候能够使用,一
// 旦 DriverEntry 执行结束,就会被释放掉
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;
StringTest();
return STATUS_SUCCESS;
}
2. MDL
#include <ntddk.h>
// 驱动对象的卸载函数,如果不设置,就无法卸载
#pragma code_seg("PAGE") // 表示当前的函数被放置在分页内存中,如果
// 短时间内不会用到,可以放置到页交换文件中
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
}
// 用于测试MDL结构体的函数
VOID MDLTest()
{
DbgBreakPoint();
// 创建一个常量指针,指向不可修改的常量字符串
LPWSTR Buffer = L"ABCDEFGHIJKLMN";
// 创建一个 MDL 用于描述指定的一个虚拟地址
PMDL Mdl = IoAllocateMdl(Buffer, 30, FALSE, FALSE, NULL);
// 对于分页内存,需要使用下面的这个函数进行锁住,暂时性的不允许交换
// 对于非分页内存,应该使用 MmBuildMdlForNonPagedPool 函数进行操作
MmProbeAndLockPages(Mdl, KernelMode, IoModifyAccess);
// 设置 MDL 结构体中的 MdlFlags 字段,添加新的属性,能够允许新的映射可读写
Mdl->MdlFlags |= MDL_MAPPED_TO_SYSTEM_VA;
内存描述页表
// 1. 需要进行重新映射的地址对应的 MDL 结构,必须是锁死在内存的
// 2. 重新映射的虚拟内存位于什么空间,内核程序通通写 KernelMode
// 3. 表示允许该数据被 CPU 缓存,加快读写的速度
// 4. 请求将物理地址映射到哪一个新的虚拟地址上,如果是 NULL,表示随机分配
// 5. 如果分配出现了问题,是否需要蓝屏
// 5. 内存级别,使用默认的级别
WCHAR* Buffer2 = MmMapLockedPagesSpecifyCache(Mdl, KernelMode, MmCached, NULL, FALSE, NormalPagePriority);
// 修改本身不可写的内容,并进行输出
Buffer2[0] = L'0';
KdPrint(("%wZ %wZ\n", &Buffer, &Buffer2));
// 进行清理操作,和上面的操作是相反的
MmUnmapLockedPages(Buffer2, Mdl); // 取消映射
MmUnlockPages(Mdl); // 取消锁定
IoFreeMDL(Mdl); // 释放MDL
// 重新的设置不可写的数组,会导致蓝屏
Buffer[0] = L'A';
}
// 对于驱动程序,入口函数的名字是 DriverEntry,必须是没有名称粉碎的
#pragma code_seg("INIT") // 表示当前的函数放置在 INIT 区段中,其中的
// 所有内容都会在驱动初始化的时候能够使用,一
// 旦 DriverEntry 执行结束,就会被释放掉
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;
MDLTest();
return STATUS_SUCCESS;
}
3. DeViceObject
#include <ntddk.h>
// 创建一个设备对象,接受的参数是 DriverEntry 传入的驱动对象
NTSTATUS CreateDevice(PDRIVER_OBJECT DriverObject)
{
// 1. 初始化设备对象的内部名称(只能被 R0 识别的名称)
// 2. 创建设备对象,并挂在到指定的驱动对象中
// 3. 由于内部名称只能在内核层使用,需要创建一个外部链接名
// 创建该函数需要使用到的局部变量
NTSTATUS Status = STATUS_SUCCESS;
PDEVICE_OBJECT DeviceObject = NULL;
// 初始化设备对象的名称,要求格式必须是 \\Device\\xxx 的形式
UNICODE_STRING DeviceName = { 0 };
RtlInitUnicodeString(&DeviceName, L"\\Device\\.min");
// 创建设备对象使用 IoCreateDevice,如果成功返回 STATUS_SUCCESS
Status = IoCreateDevice(
DriverObject, // 所属的驱动对象,设备对象创建后会添加到其 DeviceObjict 成员中
0, // 设备的扩展空间大小,分配的空间会由 DeviceExtension 字段指向
&DeviceName, // 设备对象的名称,必须符合格式 \\Device\\DeviceName
FILE_DEVICE_UNKNOWN, // 设备对象的类型,特指硬件无关的虚拟设备对象
0, // 设备对象的属性
FALSE, // 是否启用独占模式,同一时刻能被打开几次
&DeviceObject); // 创建出的设备对象,由哪一个指针指向
NTSTATUS
IoCreateDeviceSecure(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceName OPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
IN PCUNICODE_STRING DefaultSDDLString, // 安全描述符定义语言
IN LPCGUID DeviceClassGuid, // GUID
OUT PDEVICE_OBJECT *DeviceObject)
// 通过 NT_SUCCESS 判断函数的调用是否成功
if (!NT_SUCCESS(Status))
{
KdPrint(("设备对象创建失败,检查原因,错误码(%08X)\n", Status));
return Status;
}
// 设备对象的名称只能在内核中被直接的解析,为了 R3 能够识别并操作设备对象,需要
// 创建与设备名称直接关联的符号链接名,必须写作: \\DosDevices\\xxx 或 \\??\\xxx
UNICODE_STRING SymLinkName = { 0 };
RtlInitUnicodeString(&SymLinkName, L"\\??\\.min");
Status = IoCreateSymbolicLink(&SymLinkName, &DeviceName);
// 通过 NT_SUCCESS 判断函数的调用是否成功
if (!NT_SUCCESS(Status))
{
IoDeleteDevice(DeviceObject);
KdPrint(("符号链接创建失败,检查原因,错误码(%08X)\n", Status));
return Status;
}
return Status;
}
// 用于处理所有非特殊操作的 IRP 请求
NTSTATUS DefaultDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
UNREFERENCED_PARAMETER(DeviceOBject);
// 设置当前消息的处理状态,如果成功必须返回 STATUS_SUCCESS,它的返回值影响 R3 的 GetLastError
Irp->IoStatus.Status = STATUS_SUCCESS;
// 设置当前的消息处理了多少个字节的数据,影响 ReadFile 中返回的实际读写字节数
Irp->IoStatus.Information = 0;
// 通知 IO 管理器当前的 IRP 已经处理成功,需要返回给 R3
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// 当前函数的返回结构,输出是否成功
return STATUS_SUCCESS;
}
// 处理 CreateFile 产生的消息,注意如果没有设置这个函数,CreateFile 必然失败
NTSTATUS CreateDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
KdPrint(("create device\n"));
UNREFERENCED_PARAMETER(DeviceOBject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
// 处理 CloseFile 产生的消息
NTSTATUS CloseDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
KdPrint(("close device\n"));
UNREFERENCED_PARAMETER(DeviceOBject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
// 处理 ReadFile 产生的消息
NTSTATUS ReadDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
KdPrint(("read device\n"));
UNREFERENCED_PARAMETER(DeviceOBject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
// 处理 WriteFile 产生的消息
NTSTATUS WriteDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
KdPrint(("write device\n"));
UNREFERENCED_PARAMETER(DeviceOBject);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
#include <ntddk.h>
#include "header.h"
// 驱动对象的卸载函数,如果不设置,就无法卸载
#pragma code_seg("PAGE") // 表示当前的函数被放置在分页内存中,如果
// 短时间内不会用到,可以放置到页交换文件中
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
// 如果设备对象创建成功,那么需要在卸载函数中删除设备对象和符号链接名称
// 必须要先删除符号链接名,再删除设备对象,否则会出现不可描述的问题
// 删除符号链接名
UNICODE_STRING SymLinkName = { 0 };
RtlInitUnicodeString(&SymLinkName, L"\\??\\.min");
IoDeleteSymbolicLink(&SymLinkName);
// 删除设备对象,如果有多个,需要遍历设备对象表
IoDeleteDevice(DriverObject->DeviceObject);
}
// 对于驱动程序,入口函数的名字是 DriverEntry,必须是没有名称粉碎的
#pragma code_seg("INIT") // 表示当前的函数放置在 INIT 区段中,其中的
// 所有内容都会在驱动初始化的时候能够使用,一
// 旦 DriverEntry 执行结束,就会被释放掉
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;
// 为当前驱动下的所有设备对象,设置消息响应函数
for (int i = 0; i < 28; ++i)
DriverObject->MajorFunction[i] = DefaultDispath;
// 对于一些特殊的函数,需要额外进行处理
DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateDispath; // CreateFile
DriverObject->MajorFunction[IRP_MJ_CLOSE] = CloseDispath; // CloseFile
DriverObject->MajorFunction[IRP_MJ_READ] = ReadDispath; // ReadFile
DriverObject->MajorFunction[IRP_MJ_WRITE] = WriteDispath; // WriteFile
// 创建一个设备对象,如果失败则返回错误码
NTSTATUS Status = CreateDevice(DriverObject);
// 如果返回的不是 STATUS_SUCESS,驱动会安装失败,意味着 DriverUnload 不会调用
return Status;
}
//0xb8 bytes (sizeof)
struct _DEVICE_OBJECT
{
SHORT Type; //0x0
USHORT Size; //0x2
LONG ReferenceCount; //0x4
struct _DRIVER_OBJECT* DriverObject; //0x8
struct _DEVICE_OBJECT* NextDevice; //0xc
struct _DEVICE_OBJECT* AttachedDevice; //0x10
struct _IRP* CurrentIrp; //0x14
struct _IO_TIMER* Timer; //0x18
ULONG Flags; //0x1c
ULONG Characteristics; //0x20
struct _VPB* Vpb; //0x24
VOID* DeviceExtension; //0x28
ULONG DeviceType; //0x2c
CHAR StackSize; //0x30
union
{
struct _LIST_ENTRY ListEntry; //0x34
struct _WAIT_CONTEXT_BLOCK Wcb; //0x34
} Queue; //0x34
ULONG AlignmentRequirement; //0x5c
struct _KDEVICE_QUEUE DeviceQueue; //0x60
struct _KDPC Dpc; //0x74
ULONG ActiveThreadCount; //0x94
VOID* SecurityDescriptor; //0x98
struct _KEVENT DeviceLock; //0x9c
USHORT SectorSize; //0xac
USHORT Spare1; //0xae
struct _DEVOBJ_EXTENSION* DeviceObjectExtension; //0xb0
VOID* Reserved; //0xb4
};
3. R3 Basic
#include <stdio.h>
#include <windows.h>
int main()
{
// 在驱动程序中创建了设备对象后,可以使用 CreateFile 连接到设备对象,并进行通信
// 设备对象必须以 \\\\.\\ 开头,当前程序必须使用管理员权限打开
HANDLE Device = CreateFile(L"\\\\.\\.min", GENERIC_ALL, NULL,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// 判断句柄是否打开,输出错误信息
if (Device == NULL || Device == INVALID_HANDLE_VALUE)
{
printf("设备对象打开失败,错误原因(%08X)\n", GetLastError());
system("pause"); exit(0);
}
// 读写函数
DWORD OperBytes = 0;
ReadFile(Device, NULL, NULL, &OperBytes, NULL);
WriteFile(Device, NULL, NULL, &OperBytes, NULL);
// 在进行一系列操作后,需要关闭设备对象句柄,使用 CloseHandle
CloseHandle(Device);
system("pause");
return 0;
}
4. do_bufferd_io
#include <ntddk.h>
// 创建一个设备对象,接受的参数是 DriverEntry 传入的驱动对象
NTSTATUS CreateDevice(PDRIVER_OBJECT DriverObject)
{
// 1. 初始化设备对象的内部名称(只能被 R0 识别的名称)
// 2. 创建设备对象,并挂在到指定的驱动对象中
// 3. 由于内部名称只能在内核层使用,需要创建一个外部链接名
// 创建该函数需要使用到的局部变量
NTSTATUS Status = STATUS_SUCCESS;
PDEVICE_OBJECT DeviceObject = NULL;
// 初始化设备对象的名称,要求格式必须是 \\Device\\xxx 的形式
UNICODE_STRING DeviceName = { 0 };
RtlInitUnicodeString(&DeviceName, L"\\Device\\.min");
// 创建设备对象使用 IoCreateDevice,如果成功返回 STATUS_SUCCESS
Status = IoCreateDevice(
DriverObject, // 所属的驱动对象,设备对象创建后会添加到其 DeviceObjict 成员中
0, // 设备的扩展空间大小,分配的空间会由 DeviceExtension 字段指向
&DeviceName, // 设备对象的名称,必须符合格式 \\Device\\DeviceName
FILE_DEVICE_UNKNOWN, // 设备对象的类型,特指硬件无关的虚拟设备对象
0, // 设备对象的属性
FALSE, // 是否启用独占模式,同一时刻能被打开几次
&DeviceObject); // 创建出的设备对象,由哪一个指针指向
// 通过 NT_SUCCESS 判断函数的调用是否成功
if (!NT_SUCCESS(Status))
{
KdPrint(("设备对象创建失败,检查原因,错误码(%08X)\n", Status));
return Status;
}
// 设备对象的名称只能在内核中被直接的解析,为了 R3 能够识别并操作设备对象,需要
// 创建与设备名称直接关联的符号链接名,必须写作: \\DosDevices\\xxx 或 \\??\\xxx
UNICODE_STRING SymLinkName = { 0 };
RtlInitUnicodeString(&SymLinkName, L"\\??\\.min");
Status = IoCreateSymbolicLink(&SymLinkName, &DeviceName);
// 通过 NT_SUCCESS 判断函数的调用是否成功
if (!NT_SUCCESS(Status))
{
IoDeleteDevice(DeviceObject);
KdPrint(("符号链接创建失败,检查原因,错误码(%08X)\n", Status));
return Status;
}
// 设置设备对象的读写方式为缓冲区方式
DeviceObject->Flags |= DO_BUFFERED_IO;
return Status;
}
// 用于处理所有非特殊操作的 IRP 请求
NTSTATUS DefaultDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
UNREFERENCED_PARAMETER(DeviceOBject);
// 设置当前消息的处理状态,如果成功必须返回 STATUS_SUCCESS,它的返回值影响 R3 的 GetLastError
Irp->IoStatus.Status = STATUS_SUCCESS;
// 设置当前的消息处理了多少个字节的数据,影响 ReadFile 中返回的实际读写字节数
Irp->IoStatus.Information = 0;
// 通知 IO 管理器当前的 IRP 已经处理成功,需要返回给 R3
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// 当前函数的返回结构,输出是否成功
return STATUS_SUCCESS;
}
// 处理 ReadFile 产生的消息
NTSTATUS ReadDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
// 除了 IRP 结构体之外,IRP 栈保存了当前层 IRP 的附加参数
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
// Irp 栈中保存了从用户层传递进来的一些信息,这里是 R3 想读取的长度
KdPrint(("Length: %d\n", IrpStack->Parameters.Read.Length));
// 当用户层调用 ReadFile 的时候,需要提供一个缓冲区用于接受内容,内存管理器会将
// Irp->AssociatedIrp.SystemBuffer 字段中的内容,拷贝到 R3 缓冲区中
RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, "hello", 6);
// 将实际的操作数量,返回给 R3,由 ReadFile 的第 4 个参数接受
Irp->IoStatus.Information = 20;
UNREFERENCED_PARAMETER(DeviceOBject);
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
// 处理 WriteFile 产生的消息
NTSTATUS WriteDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
// 除了 IRP 结构体之外,IRP 栈保存了当前层 IRP 的附加参数
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
// Irp 栈中保存了从用户层传递进来的一些信息,这里是 R3 想写入的长度
KdPrint(("Length: %d\n", IrpStack->Parameters.Write.Length));
// 当用户层调用 ReadFile 的时候,需要提供一个缓冲区用于接受内容,内存管理器会将
// Irp->AssociatedIrp.SystemBuffer 字段中的内容,拷贝到 R3 缓冲区中
KdPrint(("R3写入了: %s\n", Irp->AssociatedIrp.SystemBuffer));
// 将实际的操作数量,返回给 R3,由 ReadFile 的第 4 个参数接受
Irp->IoStatus.Information = 66;
UNREFERENCED_PARAMETER(DeviceOBject);
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
#include <ntddk.h>
#include "header.h"
// 驱动对象的卸载函数,如果不设置,就无法卸载
#pragma code_seg("PAGE") // 表示当前的函数被放置在分页内存中,如果
// 短时间内不会用到,可以放置到页交换文件中
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
// 如果设备对象创建成功,那么需要在卸载函数中删除设备对象和符号链接名称
// 必须要先删除符号链接名,再删除设备对象,否则会出现不可描述的问题
// 删除符号链接名
UNICODE_STRING SymLinkName = { 0 };
RtlInitUnicodeString(&SymLinkName, L"\\??\\.min");
IoDeleteSymbolicLink(&SymLinkName);
// 删除设备对象,如果有多个,需要遍历设备对象表
IoDeleteDevice(DriverObject->DeviceObject);
}
// 对于驱动程序,入口函数的名字是 DriverEntry,必须是没有名称粉碎的
#pragma code_seg("INIT") // 表示当前的函数放置在 INIT 区段中,其中的
// 所有内容都会在驱动初始化的时候能够使用,一
// 旦 DriverEntry 执行结束,就会被释放掉
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;
// 为当前驱动下的所有设备对象,设置消息响应函数
for (int i = 0; i < 28; ++i)
DriverObject->MajorFunction[i] = DefaultDispath;
// 对于一些特殊的函数,需要额外进行处理
DriverObject->MajorFunction[IRP_MJ_READ] = ReadDispath; // ReadFile
DriverObject->MajorFunction[IRP_MJ_WRITE] = WriteDispath; // WriteFile
// 创建一个设备对象,如果失败则返回错误码
NTSTATUS Status = CreateDevice(DriverObject);
// 如果返回的不是 STATUS_SUCESS,驱动会安装失败,意味着 DriverUnload 不会调用
return Status;
}
4. R3
#include <stdio.h>
#include <windows.h>
int main()
{
DWORD Bytes = 0;
// 可以使用 CreateFile 去打开一个设备对象,要求管理员权限
HANDLE DeviceHandle = CreateFile(L"\\\\.\\.min", GENERIC_ALL,
NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// 向设备对象写入数据
WriteFile(DeviceHandle, "123456 r3", 15, &Bytes, NULL);
printf("WriteFile(15) > Bytes[%d]\n", Bytes);
// 从设备对象读取内容
CHAR Buffer[0x100] = { 0 };
ReadFile(DeviceHandle, Buffer, 0x100, &Bytes, NULL);
printf("ReadFile > %s Bytes[%d]\n", Buffer, Bytes);
// 关闭句柄,会相应到 IRP 的 CLOSE 消息
CloseHandle(DeviceHandle);
system("pause");
return 0;
}
5. do_direct_io
#include <ntddk.h>
// 创建一个设备对象,接受的参数是 DriverEntry 传入的驱动对象
NTSTATUS CreateDevice(PDRIVER_OBJECT DriverObject)
{
// 1. 初始化设备对象的内部名称(只能被 R0 识别的名称)
// 2. 创建设备对象,并挂在到指定的驱动对象中
// 3. 由于内部名称只能在内核层使用,需要创建一个外部链接名
// 创建该函数需要使用到的局部变量
NTSTATUS Status = STATUS_SUCCESS;
PDEVICE_OBJECT DeviceObject = NULL;
// 初始化设备对象的名称,要求格式必须是 \\Device\\xxx 的形式
UNICODE_STRING DeviceName = { 0 };
RtlInitUnicodeString(&DeviceName, L"\\Device\\.min");
// 创建设备对象使用 IoCreateDevice,如果成功返回 STATUS_SUCCESS
Status = IoCreateDevice(
DriverObject, // 所属的驱动对象,设备对象创建后会添加到其 DeviceObjict 成员中
0, // 设备的扩展空间大小,分配的空间会由 DeviceExtension 字段指向
&DeviceName, // 设备对象的名称,必须符合格式 \\Device\\DeviceName
FILE_DEVICE_UNKNOWN, // 设备对象的类型,特指硬件无关的虚拟设备对象
0, // 设备对象的属性
FALSE, // 是否启用独占模式,同一时刻能被打开几次
&DeviceObject); // 创建出的设备对象,由哪一个指针指向
// 通过 NT_SUCCESS 判断函数的调用是否成功
if (!NT_SUCCESS(Status))
{
KdPrint(("设备对象创建失败,检查原因,错误码(%08X)\n", Status));
return Status;
}
// 设备对象的名称只能在内核中被直接的解析,为了 R3 能够识别并操作设备对象,需要
// 创建与设备名称直接关联的符号链接名,必须写作: \\DosDevices\\xxx 或 \\??\\xxx
UNICODE_STRING SymLinkName = { 0 };
RtlInitUnicodeString(&SymLinkName, L"\\??\\.min");
Status = IoCreateSymbolicLink(&SymLinkName, &DeviceName);
// 通过 NT_SUCCESS 判断函数的调用是否成功
if (!NT_SUCCESS(Status))
{
IoDeleteDevice(DeviceObject);
KdPrint(("符号链接创建失败,检查原因,错误码(%08X)\n", Status));
return Status;
}
// 设置设备对象的读写方式为缓冲区方式
DeviceObject->Flags |= DO_DIRECT_IO;
return Status;
}
// 用于处理所有非特殊操作的 IRP 请求
NTSTATUS DefaultDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
UNREFERENCED_PARAMETER(DeviceOBject);
// 设置当前消息的处理状态,如果成功必须返回 STATUS_SUCCESS,它的返回值影响 R3 的 GetLastError
Irp->IoStatus.Status = STATUS_SUCCESS;
// 设置当前的消息处理了多少个字节的数据,影响 ReadFile 中返回的实际读写字节数
Irp->IoStatus.Information = 0;
// 通知 IO 管理器当前的 IRP 已经处理成功,需要返回给 R3
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// 当前函数的返回结构,输出是否成功
return STATUS_SUCCESS;
}
// 处理 ReadFile 产生的消息
NTSTATUS ReadDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
// 除了 IRP 结构体之外,IRP 栈保存了当前层 IRP 的附加参数
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
// Irp 栈中保存了从用户层传递进来的一些信息,这里是 R3 想读取的长度
KdPrint(("Length: %d\n", IrpStack->Parameters.Read.Length));
// 如果使用的是 DIRECT 方式,那么系统会给我们提供一个绑定到用户缓冲区的
// MDL,通过 MmGetSystemAddressForMdlSafe 进行映射
PVOID Buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
RtlCopyMemory(Buffer, "hello", 6);
// 将实际的操作数量,返回给 R3,由 ReadFile 的第 4 个参数接受
Irp->IoStatus.Information = 20;
UNREFERENCED_PARAMETER(DeviceOBject);
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
// 处理 WriteFile 产生的消息
NTSTATUS WriteDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
// 除了 IRP 结构体之外,IRP 栈保存了当前层 IRP 的附加参数
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
// Irp 栈中保存了从用户层传递进来的一些信息,这里是 R3 想写入的长度
KdPrint(("Length: %d\n", IrpStack->Parameters.Write.Length));
// 如果使用的是 DIRECT 方式,那么系统会给我们提供一个绑定到用户缓冲区的
// MDL,通过 MmGetSystemAddressForMdlSafe 进行映射
PVOID Buffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
KdPrint(("R3写入了: %s\n", Buffer));
// 将实际的操作数量,返回给 R3,由 ReadFile 的第 4 个参数接受
Irp->IoStatus.Information = 66;
UNREFERENCED_PARAMETER(DeviceOBject);
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
#include <ntddk.h>
#include "header.h"
// 驱动对象的卸载函数,如果不设置,就无法卸载
#pragma code_seg("PAGE") // 表示当前的函数被放置在分页内存中,如果
// 短时间内不会用到,可以放置到页交换文件中
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
// 如果设备对象创建成功,那么需要在卸载函数中删除设备对象和符号链接名称
// 必须要先删除符号链接名,再删除设备对象,否则会出现不可描述的问题
// 删除符号链接名
UNICODE_STRING SymLinkName = { 0 };
RtlInitUnicodeString(&SymLinkName, L"\\??\\.min");
IoDeleteSymbolicLink(&SymLinkName);
// 删除设备对象,如果有多个,需要遍历设备对象表
IoDeleteDevice(DriverObject->DeviceObject);
}
// 对于驱动程序,入口函数的名字是 DriverEntry,必须是没有名称粉碎的
#pragma code_seg("INIT") // 表示当前的函数放置在 INIT 区段中,其中的
// 所有内容都会在驱动初始化的时候能够使用,一
// 旦 DriverEntry 执行结束,就会被释放掉
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;
// 为当前驱动下的所有设备对象,设置消息响应函数
for (int i = 0; i < 28; ++i)
DriverObject->MajorFunction[i] = DefaultDispath;
// 对于一些特殊的函数,需要额外进行处理
DriverObject->MajorFunction[IRP_MJ_READ] = ReadDispath; // ReadFile
DriverObject->MajorFunction[IRP_MJ_WRITE] = WriteDispath; // WriteFile
// 创建一个设备对象,如果失败则返回错误码
NTSTATUS Status = CreateDevice(DriverObject);
// 如果返回的不是 STATUS_SUCESS,驱动会安装失败,意味着 DriverUnload 不会调用
return Status;
}
6. neither
#include <ntddk.h>
// 创建一个设备对象,接受的参数是 DriverEntry 传入的驱动对象
NTSTATUS CreateDevice(PDRIVER_OBJECT DriverObject)
{
// 1. 初始化设备对象的内部名称(只能被 R0 识别的名称)
// 2. 创建设备对象,并挂在到指定的驱动对象中
// 3. 由于内部名称只能在内核层使用,需要创建一个外部链接名
// 创建该函数需要使用到的局部变量
NTSTATUS Status = STATUS_SUCCESS;
PDEVICE_OBJECT DeviceObject = NULL;
// 初始化设备对象的名称,要求格式必须是 \\Device\\xxx 的形式
UNICODE_STRING DeviceName = { 0 };
RtlInitUnicodeString(&DeviceName, L"\\Device\\.min");
// 创建设备对象使用 IoCreateDevice,如果成功返回 STATUS_SUCCESS
Status = IoCreateDevice(
DriverObject, // 所属的驱动对象,设备对象创建后会添加到其 DeviceObjict 成员中
0, // 设备的扩展空间大小,分配的空间会由 DeviceExtension 字段指向
&DeviceName, // 设备对象的名称,必须符合格式 \\Device\\DeviceName
FILE_DEVICE_UNKNOWN, // 设备对象的类型,特指硬件无关的虚拟设备对象
0, // 设备对象的属性
FALSE, // 是否启用独占模式,同一时刻能被打开几次
&DeviceObject); // 创建出的设备对象,由哪一个指针指向
// 通过 NT_SUCCESS 判断函数的调用是否成功
if (!NT_SUCCESS(Status))
{
KdPrint(("设备对象创建失败,检查原因,错误码(%08X)\n", Status));
return Status;
}
// 设备对象的名称只能在内核中被直接的解析,为了 R3 能够识别并操作设备对象,需要
// 创建与设备名称直接关联的符号链接名,必须写作: \\DosDevices\\xxx 或 \\??\\xxx
UNICODE_STRING SymLinkName = { 0 };
RtlInitUnicodeString(&SymLinkName, L"\\??\\.min");
Status = IoCreateSymbolicLink(&SymLinkName, &DeviceName);
// 通过 NT_SUCCESS 判断函数的调用是否成功
if (!NT_SUCCESS(Status))
{
IoDeleteDevice(DeviceObject);
KdPrint(("符号链接创建失败,检查原因,错误码(%08X)\n", Status));
return Status;
}
// 设置设备对象的读写方式为缓冲区方式
DeviceObject->Flags;
return Status;
}
// 用于处理所有非特殊操作的 IRP 请求
NTSTATUS DefaultDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
UNREFERENCED_PARAMETER(DeviceOBject);
// 设置当前消息的处理状态,如果成功必须返回 STATUS_SUCCESS,它的返回值影响 R3 的 GetLastError
Irp->IoStatus.Status = STATUS_SUCCESS;
// 设置当前的消息处理了多少个字节的数据,影响 ReadFile 中返回的实际读写字节数
Irp->IoStatus.Information = 0;
// 通知 IO 管理器当前的 IRP 已经处理成功,需要返回给 R3
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// 当前函数的返回结构,输出是否成功
return STATUS_SUCCESS;
}
// 处理 ReadFile 产生的消息
NTSTATUS ReadDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
// 除了 IRP 结构体之外,IRP 栈保存了当前层 IRP 的附加参数
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
// Irp 栈中保存了从用户层传递进来的一些信息,这里是 R3 想读取的长度
KdPrint(("Length: %d\n", IrpStack->Parameters.Read.Length));
// 如果使用两者都不的方式,那么内核代码会直接尝试操作用户数据,此时由于
// 不能保证数据和进程是对应的,所以可能出现问题
try {
RtlCopyMemory(Irp->UserBuffer, "hello", 6);
} except(EXCEPTION_EXECUTE_HANDLER) {
KdPrint(("地址无法访问,请尝试其它方式\n"));
}
// 将实际的操作数量,返回给 R3,由 ReadFile 的第 4 个参数接受
Irp->IoStatus.Information = 20;
UNREFERENCED_PARAMETER(DeviceOBject);
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
// 处理 WriteFile 产生的消息
NTSTATUS WriteDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
// 除了 IRP 结构体之外,IRP 栈保存了当前层 IRP 的附加参数
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
// Irp 栈中保存了从用户层传递进来的一些信息,这里是 R3 想写入的长度
KdPrint(("Length: %d\n", IrpStack->Parameters.Write.Length));
// 如果使用两者都不的方式,那么内核代码会直接尝试操作用户数据,此时由于
// 不能保证数据和进程是对应的,所以可能出现问题
try {
KdPrint(("R3: %s\n", Irp->UserBuffer));
} except(EXCEPTION_EXECUTE_HANDLER) {
KdPrint(("地址无法访问,请尝试其它方式\n"));
}
// 将实际的操作数量,返回给 R3,由 ReadFile 的第 4 个参数接受
Irp->IoStatus.Information = 66;
UNREFERENCED_PARAMETER(DeviceOBject);
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
#include <ntddk.h>
#include "header.h"
// 驱动对象的卸载函数,如果不设置,就无法卸载
#pragma code_seg("PAGE") // 表示当前的函数被放置在分页内存中,如果
// 短时间内不会用到,可以放置到页交换文件中
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
// 如果设备对象创建成功,那么需要在卸载函数中删除设备对象和符号链接名称
// 必须要先删除符号链接名,再删除设备对象,否则会出现不可描述的问题
// 删除符号链接名
UNICODE_STRING SymLinkName = { 0 };
RtlInitUnicodeString(&SymLinkName, L"\\??\\.min");
IoDeleteSymbolicLink(&SymLinkName);
// 删除设备对象,如果有多个,需要遍历设备对象表
IoDeleteDevice(DriverObject->DeviceObject);
}
// 对于驱动程序,入口函数的名字是 DriverEntry,必须是没有名称粉碎的
#pragma code_seg("INIT") // 表示当前的函数放置在 INIT 区段中,其中的
// 所有内容都会在驱动初始化的时候能够使用,一
// 旦 DriverEntry 执行结束,就会被释放掉
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = DriverUnload;
// 为当前驱动下的所有设备对象,设置消息响应函数
for (int i = 0; i < 28; ++i)
DriverObject->MajorFunction[i] = DefaultDispath;
// 对于一些特殊的函数,需要额外进行处理
DriverObject->MajorFunction[IRP_MJ_READ] = ReadDispath; // ReadFile
DriverObject->MajorFunction[IRP_MJ_WRITE] = WriteDispath; // WriteFile
// 创建一个设备对象,如果失败则返回错误码
NTSTATUS Status = CreateDevice(DriverObject);
// 如果返回的不是 STATUS_SUCESS,驱动会安装失败,意味着 DriverUnload 不会调用
return Status;
}
7. control
#include <ntddk.h>
// 设备类型,IOCTL码,读写方式,权限
#define BufferWay CTL_CODE( FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS )
#define DirectWay CTL_CODE( FILE_DEVICE_UNKNOWN, 0x802, METHOD_IN_DIRECT, FILE_ANY_ACCESS )
#define NeitherWay CTL_CODE( FILE_DEVICE_UNKNOWN, 0x803, METHOD_NEITHER, FILE_ANY_ACCESS )
// 创建设备对象,接收的参数是所属的驱动对象
NTSTATUS CreateDevice(PDRIVER_OBJECT DriverObject)
{
DbgBreakPoint();
// 创建需要用到的一些变量
NTSTATUS Status = STATUS_SUCCESS;
PDEVICE_OBJECT DeviceObject = NULL;
// 初始化设备对象的名称,名称必须是 \\Device\\xxx 的形式
UNICODE_STRING DeviceName = { 0 };
RtlInitUnicodeString(&DeviceName, L"\\Device\\mmm");
// 创建设备对象的函数,如果成功,返回 STATUS_SUCCESS
Status = IoCreateDevice(
DriverObject, // 所属驱动对象,创建后会被添加到它的 DeviceObject 链表中
0, // 设备的扩展空间大小,分配的空间会被 DeviceExtension 指向
&DeviceName, // 设备对象的名称
FILE_DEVICE_UNKNOWN, // 一般说明它是一个和硬件无关的虚拟设备
0, // 设备的属性信息
TRUE, // 是否独占 \ R3是否可访问
&DeviceObject); // 创建出的设备对象被保存到的地方
// 通过 NT_SUCCESS 判断创建是否成功
if (!NT_SUCCESS(Status))
{
KdPrint(("设备对象创建失败"));
return Status;
}
// 设备对象的名称只能被内核程序解析,为了让R3应用识别
// 需要设置符号链接名: \\DosDevice\\xxx 或 \\??\\xxx
UNICODE_STRING SymLinkName = { 0 };
RtlInitUnicodeString(&SymLinkName, L"\\??\\mmms");
Status = IoCreateSymbolicLink(&SymLinkName, &DeviceName);
// 通过 NT_SUCCESS 判断创建是否成功
if (!NT_SUCCESS(Status))
{
IoDeleteDevice(DeviceObject);
KdPrint(("设备对象创建失败"));
return Status;
}
// 创建成功后,可以通过设置 Flags 指定读写的方式,如果没有指定
// 读写方式,就会采用两者都不的形式,此时访问的直接是用户空间
return Status;
}
// 用于实现默认的消息派遣函数
NTSTATUS DefaultDispatch(
PDEVICE_OBJECT DeviceObject, // 表示当前的消息是那个设备对象产生的
PIRP Irp) // IRP,对应的是三环程序的消息,保存了一些附加参数
{
UNREFERENCED_PARAMETER(DeviceObject);
// 设置消息的处理状态: 成功或失败 -> GetLastError
Irp->IoStatus.Status = STATUS_SUCCESS;
// 设置消息处理的内容长度: ReadFile 或者 WriteFIle 的实际操作长度
Irp->IoStatus.Information = 0;
// 通知操作已经完成,完成后不提高当前的 IRQL
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// 返回当前处理的整体结果是成功还是失败
return STATUS_SUCCESS;
}
// 用于实现默认的消息派遣函数
NTSTATUS DeviceIoControlDispatch(
PDEVICE_OBJECT DeviceObject, // 表示当前的消息是那个设备对象产生的
PIRP Irp) // IRP,对应的是三环程序的消息,保存了一些附加参数
{
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation(Irp);
// 可以通过 irp 栈获取到 DeviceIoControl 的通知码
switch (Stack->Parameters.DeviceIoControl.IoControlCode)
{
case BufferWay:
Irp->AssociatedIrp.SystemBuffer;
Irp->MdlAddress;
break;
case DirectWay:
Irp->MdlAddress;
break;
case NeitherWay:
Irp->UserBuffer;
Stack->Parameters.DeviceIoControl.Type3InputBuffer;
break;
}
// 设置消息的处理状态: 成功或失败 -> GetLastError
Irp->IoStatus.Status = STATUS_SUCCESS;
// 设置消息处理的内容长度: ReadFile 或者 WriteFIle 的实际操作长度
Irp->IoStatus.Information = 0;
// 通知操作已经完成,完成后不提高当前的 IRQL
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// 返回当前处理的整体结果是成功还是失败
return STATUS_SUCCESS;
}
// DeviceType: 表示设备的类型,虚拟设备应该指定一个大于 0x8000
// Function: 一个自定义的消息,指明需要执行的是什么操作,应该大于 0x800 0x000-0x7ff为系统保留
// Method: 表示当前的消息以什么样的方式传递数据,直接IO,缓冲区IO等
// Access: 当前的消息具有什么样的权限,通常都是 FILE_ANY_ACCESS
#define BUFFER_METHOND CTL_CODE(0x8000, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define DIRECT_METHOND CTL_CODE(0x8000, 0x802, METHOD_IN_DIRECT, FILE_ANY_ACCESS)
#define NEITHER_METHOND CTL_CODE(0x8000, 0x803, METHOD_NEITHER, FILE_ANY_ACCESS)
// 在进行数据传递时,需要用到的结构体
typedef struct _INFO
{
int number;
char str[0x10];
} INFO, *PINFO;
#include <ntddk.h>
#include "code.h"
/*
// 输出位置
METHOD_IN_DIRECT irp->AssociatedIrp.SystemBuffer
METHOD_OUT_DIRECT irp->AssociatedIrp.SystemBuffer
METHOD_BUFFERED irp->AssociatedIrp.SystemBuffer
METHOD_NEITHER irpStack->Parameters.DeviceIoControl.Type3InputBuffer
// 输入位置
METHOD_IN_DIRECT irp->MdlAddress
METHOD_OUT_DIRECT irp->MdlAddress
METHOD_BUFFERED irp->AssociatedIrp.SystemBuffer
METHOD_NEITHER irp->UserBuffer
*/
缓冲区方式与IRP的关系如下:
在驱动层,依传输类型的不同,输入缓冲区的位置亦不同,见下表。
传输类型 位置
METHOD_IN_DIRECT irp->AssociatedIrp.SystemBuffer
METHOD_OUT_DIRECT irp->AssociatedIrp.SystemBuffer
METHOD_BUFFERED irp->AssociatedIrp.SystemBuffer
METHOD_NEITHER irpStack->Parameters.DeviceIoControl.Type3InputBuffer
在驱动层,依传输类型的不同,输出缓冲区的位置亦不同,见下表。
传输类型 位置
METHOD_IN_DIRECT irp->MdlAddress
METHOD_OUT_DIRECT irp->MdlAddress
METHOD_BUFFERED irp->AssociatedIrp.SystemBuffer
METHOD_NEITHER irp->UserBuffer
// 创建设备对象,接收的参数是所属的驱动对象
NTSTATUS CreateDevice(PDRIVER_OBJECT DriverObject)
{
DbgBreakPoint();
// 创建需要用到的一些变量
NTSTATUS Status = STATUS_SUCCESS;
PDEVICE_OBJECT DeviceObject = NULL;
// 初始化设备对象的名称,名称必须是 \\Device\\xxx 的形式
UNICODE_STRING DeviceName = { 0 };
RtlInitUnicodeString(&DeviceName, L"\\Device\\.min");
// 创建设备对象的函数,如果成功,返回 STATUS_SUCCESS
Status = IoCreateDevice(
DriverObject, // 所属驱动对象,创建后会被添加到它的 DeviceObject 链表中
0, // 设备的扩展空间大小,分配的空间会被 DeviceExtension 指向
&DeviceName, // 设备对象的名称
FILE_DEVICE_UNKNOWN, // 一般说明它是一个和硬件无关的虚拟设备
0, // 设备的属性信息
TRUE, // 是否独占 \ R3是否可访问
&DeviceObject); // 创建出的设备对象被保存到的地方
// 通过 NT_SUCCESS 判断创建是否成功
if (!NT_SUCCESS(Status))
{
KdPrint(("设备对象创建失败"));
return Status;
}
// 设备对象的名称只能被内核程序解析,为了让R3应用识别
// 需要设置符号链接名: \\DosDevice\\xxx 或 \\??\\xxx
UNICODE_STRING SymLinkName = { 0 };
RtlInitUnicodeString(&SymLinkName, L"\\??\\.min");
Status = IoCreateSymbolicLink(&SymLinkName, &DeviceName);
// 通过 NT_SUCCESS 判断创建是否成功
if (!NT_SUCCESS(Status))
{
IoDeleteDevice(DeviceObject);
KdPrint(("设备对象创建失败"));
return Status;
}
// 创建成功后,可以通过设置 Flags 指定读写的方式,如果没有指定
// 读写方式,就会采用两者都不的形式,此时访问的直接是用户空间
return Status;
}
// 用于实现默认的消息派遣函数
NTSTATUS DefaultDispatch(
PDEVICE_OBJECT DeviceObject, // 表示当前的消息是那个设备对象产生的
PIRP Irp) // IRP,对应的是三环程序的消息,保存了一些附加参数
{
UNREFERENCED_PARAMETER(DeviceObject);
// 设置消息的处理状态: 成功或失败 -> GetLastError
Irp->IoStatus.Status = STATUS_SUCCESS;
// 设置消息处理的内容长度: ReadFile 或者 WriteFIle 的实际操作长度
Irp->IoStatus.Information = 0;
// 通知操作已经完成,完成后不提高当前的 IRQL
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// 返回当前处理的整体结果是成功还是失败
return STATUS_SUCCESS;
}
// 用于实现默认的消息派遣函数
NTSTATUS DeviceIoControlDispatch(
PDEVICE_OBJECT DeviceObject, // 表示当前的消息是那个设备对象产生的
PIRP Irp) // IRP,对应的是三环程序的消息,保存了一些附加参数
{
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation(Irp);
DbgBreakPoint();
// 可以通过 irp 栈获取到 DeviceIoControl 的通知码
switch (Stack->Parameters.DeviceIoControl.IoControlCode)
{
/*case BufferWay:
Irp->AssociatedIrp.SystemBuffer; // R3->R0
Irp->MdlAddress; // R0->R3
break;
case DirectWay:
Irp->AssociatedIrp.SystemBuffer // R0->R3
Irp->MdlAddress; // R3->R0
break;
case NeitherWay:
Irp->UserBuffer; // R0->R3
Stack->Parameters.DeviceIoControl.Type3InputBuffer; // R3->R0
break;*/
default:
break;
}
// 设置消息的处理状态: 成功或失败 -> GetLastError
Irp->IoStatus.Status = STATUS_SUCCESS;
// 设置消息处理的内容长度: ReadFile 或者 WriteFIle 的实际操作长度
Irp->IoStatus.Information = 0;
// 通知操作已经完成,完成后不提高当前的 IRQL
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// 返回当前处理的整体结果是成功还是失败
return STATUS_SUCCESS;
}
#include <ntddk.h>
#include "header.h"
// 驱动程序的卸载函数,会在卸载时调用,通常用于删除设备对象
#pragma code_seg("PAGE") // 分页内存: 能过够被交换到页交换文件的内存
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
// 删除设备对象的时候,必须先删符号名,再删设备对象
UNICODE_STRING SymLinkName = { 0 };
RtlInitUnicodeString(&SymLinkName, L"\\??\\mmms");
IoDeleteSymbolicLink(&SymLinkName);
// 可以通过驱动对象的链表找到设备对象,进行删除
IoDeleteDevice(DriverObject->DeviceObject);
UNREFERENCED_PARAMETER(DriverObject);
}
// 驱动程序的入口函数,参数一表示当前的驱动对象,参数二是在注册表的路径
#pragma code_seg("INIT") // 只有在初始化的时候存在,初始化完毕被卸载的内存
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
// 只有设置了卸载函数的驱动程序才能被卸载
DriverObject->DriverUnload = DriverUnload;
// 创建设备对象,只有设备对象才能进行通信
CreateDevice(DriverObject);
// 为所有的 IRP 消息设置默认的派遣函数
for (int i = 0; i < 28; ++i)
DriverObject->MajorFunction[i] = DefaultDispatch;
// 创建设备对象之后,可以通过驱动对象设置消息的派遣函数(消息响应)
// 消息是由设备对象产生的,但统一由驱动对象进行处理,对于下面的
// 几个消息,如果不提供,就不能完成相应的函数调用
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceIoControlDispatch; // DeviceIoControl(自定义消息)
// 只有当返回值为 STATUS_SUCCESS 驱动才会被加载到系统中
return STATUS_SUCCESS;
}
7. R3 Basic
#include <stdio.h>
#include <windows.h>
#include "../07 device-io-control/code.h"
int main()
{
// 可以使用 CreateFile 去打开一个设备对象,要求管理员权限
HANDLE DeviceHandle = CreateFile(L"\\\\.\\.min", GENERIC_ALL,
NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
DWORD Bytes = 0;
INFO info = { 0x1234, "one" };
BYTE buffer[0x100] = { 0 };
// 1. 设备对象,需要向哪一个设备发出请求
// 2. 需要传递给 R0 的缓冲区和大小
// 3. 用于接受 R0 传回数据的缓冲区和大小
DeviceIoControl(DeviceHandle, NEITHER_METHOND,
&info, sizeof(info), // 需要传递给 R0 的缓冲区和大小
buffer, 0x100, &Bytes, // 用于接受 R0 传回的数据
NULL); // 重叠 IO 结构体
// 关闭句柄,会相应到 IRP 的 CLOSE 消息
CloseHandle(DeviceHandle);
system("pause");
return 0;
}
3. IRP(消息)
- IRP就如同winows应用程序中的MSG,IRP是一个通用结构,驱动程序比较复杂,存储的信息多
- 包含用来描述一个IO请求的完整信息
- IO管理器创建一个IRP来代表一个IO操作,并且将该IRP传递给正确的驱动程序,当此IO操作完成时再处理该请求包,相对的,驱动程序(上层的虚拟设备驱动或者底层的真实设备驱动)接收一个IRP,执行该IRP指定的操作,然后将IRP传回给IO管理器,告诉它该操作已经完成,或者应该传递给另一个驱动以进行进一步处理
- IRP的接收者是设备对象,处理器是驱动对象
IRP(I/O Request Package)
//0x70 bytes (sizeof)
struct _IRP
{
SHORT Type; //0x0
USHORT Size; //0x2
struct _MDL* MdlAddress; //0x4
ULONG Flags; //0x8
union
{
struct _IRP* MasterIrp; //0xc
LONG IrpCount; //0xc
VOID* SystemBuffer; //0xc
} AssociatedIrp; //0xc
struct _LIST_ENTRY ThreadListEntry; //0x10
struct _IO_STATUS_BLOCK IoStatus; //0x18
CHAR RequestorMode; //0x20
UCHAR PendingReturned; //0x21
CHAR StackCount; //0x22
CHAR CurrentLocation; //0x23
UCHAR Cancel; //0x24
UCHAR CancelIrql; //0x25
CHAR ApcEnvironment; //0x26
UCHAR AllocationFlags; //0x27
struct _IO_STATUS_BLOCK* UserIosb; //0x28
struct _KEVENT* UserEvent; //0x2c
union
{
struct
{
union
{
VOID (*UserApcRoutine)(VOID* arg1, struct _IO_STATUS_BLOCK* arg2, ULONG arg3); //0x30
VOID* IssuingProcess; //0x30
};
VOID* UserApcContext; //0x34
} AsynchronousParameters; //0x30
union _LARGE_INTEGER AllocationSize; //0x30
} Overlay; //0x30
VOID (*CancelRoutine)(struct _DEVICE_OBJECT* arg1, struct _IRP* arg2); //0x38
VOID* UserBuffer; //0x3c
union
{
struct
{
union
{
struct _KDEVICE_QUEUE_ENTRY DeviceQueueEntry; //0x40
VOID* DriverContext[4]; //0x40
};
struct _ETHREAD* Thread; //0x50
CHAR* AuxiliaryBuffer; //0x54
struct _LIST_ENTRY ListEntry; //0x58
union
{
struct _IO_STACK_LOCATION* CurrentStackLocation; //0x60
ULONG PacketType; //0x60
};
struct _FILE_OBJECT* OriginalFileObject; //0x64
} Overlay; //0x40
struct _KAPC Apc; //0x40
VOID* CompletionKey; //0x40
} Tail; //0x40
};
三. 驱动程序
- 驱动程序编写和用户层编写差不多,都属于事件驱动
- 通常在DriverEntry中创建好设备对象,并给驱动对象填写好处理的IRP函数
- 其他程序需要和驱动互动的时候,就会给设备对象发送IRP,操作系统会使用我们填写好的处理函数去处理IRP
四. 内核编程特点
1. 数据类型
2. 函数内核属性
3. 概念
4. 内核API
-
IO 管理器 创建设备
-
Ex 执行体 内存分配
-
RTL RunTimeLibrary运行时库函数 strcpy
-
Ke kernel内核相关
-
Zw 系统服务表 操作系统导出函数 0环
-
NT Natives 原生函数 3环到0环过度函数 用户的最顶层 内核的最底层
分页内存一般指页交换文件可以放在内存中也可以放在磁盘中,非分页内存只能放在内存中
用户层内存属性 私有的private IMAGE映像 MAPPING映射
5. 函数返回值
6. IRQL
中断请求级别
7. UNICODE_STRING
- RtlInitUnicodeString 初始化字符串
- RtlFreeUnicodeString 销毁字符串
- RtlCopyUnicodeString 拷贝字符串
- RtlAppendUnicodeStringToString 追加字符串
- RtlCompareUnicodeString 比较字符串
- RtlUnicodeStringToInteger 字符串转数字
- RtlIntegerToUnicodeString 数字转字符串
- Kdprint 输出调试信息
8. 内存操作
RtlZeroMemory (缓冲区,大小)清空内存为0
RtlFillMemory(缓冲区,大小,填充内容)
RtlCopyMemory(目的缓冲区,源缓冲区,大小)
ExFreePool(缓冲区)
9. MDL(内存描述符表)
Dispatch不能产生异常 ,因为异常也是DPC级别的
五. 设备对象
- 数据的传输我们称之为 I/O,在驱动程序中,I/O都是通过传递IRP实现的,而接收IRP的只能是设备对象
- 所以我们想要和驱动程序通讯最为常用的方式就是创建IRP
1. 创建设备对象
2. IRP的处理
1. IRP栈
IRP结构体是 I/O 信息的一部分,还有一个IRP栈对应的一个 IO_STACK_LOCATION结构体
获取当前IRP栈 PIO_STACK_LOCATION IoGetCurrentIrpStackLocation(In PIRP Irp)
有两个重要成员,分别是MajorFuction和MinorFuction分别记录了IRP的主类型(什么IRP)和子类型(子消息)
还有一个重要的联合体成员,根据不同的IRP传递不同的消息
2. 处理方式
- 填写不同的回调函数,类似MFC
- 填写相同的回调函数,类似SDK,再判断
// 用于处理所有非特殊操作的 IRP 请求
NTSTATUS DefaultDispath(
PDEVICE_OBJECT DeviceOBject, // 表示当前的消息是哪一个设备对象接受的
PIRP Irp) // IRP,接收到的消息是什么,类似 R3 的 MSG
{
UNREFERENCED_PARAMETER(DeviceOBject);
// 设置当前消息的处理状态,如果成功必须返回 STATUS_SUCCESS,它的返回值影响 R3 的 GetLastError
Irp->IoStatus.Status = STATUS_SUCCESS;
// 设置当前的消息处理了多少个字节的数据,影响 ReadFile 中返回的实际读写字节数
Irp->IoStatus.Information = 0;
// 通知 IO 管理器当前的 IRP 已经处理成功,需要返回给 R3
IoCompleteRequest(Irp, IO_NO_INCREMENT);
// 当前函数的返回结构,输出是否成功
return STATUS_SUCCESS;
}
3. 设备对象通讯方式
效率和安全的区分,
对于小不讲究效率 安全的用buffer
对于大型的讲究效率的用直接direct
对于 用user 同步不需要考虑安全 异步需要考虑
4. 控制码通讯
五. 文件操作
- 在内核编程中也是可以操作文件的,只需要在用户层提供的路径前加上**\\??\\**
最后一个带名称的内核对象,需要初始化为一个对象属性结构体
NT函数会检查地址是否可以访问
#include <ntifs.h>
#include <ntddk.h>
// 使用指定的方式创建或打开一个指定的文件或目录
HANDLE CreateFile(LPCWSTR pFilePath, ACCESS_MASK Access, BOOLEAN IsFile)
{
HANDLE FileHandle = NULL;
IO_STATUS_BLOCK IoStatusBlock = { 0 };
// 初始化文件或目录的名字保存到 UNICODE_STRING 中
UNICODE_STRING FilePath = { 0 };
RtlInitUnicodeString(&FilePath, pFilePath);
// 操作一个具名对象的时候,通常都需要提供结构体,描述文件的名字
OBJECT_ATTRIBUTES ObjectAttributes = { 0 };
InitializeObjectAttributes(
&ObjectAttributes, // 需要初始化的对象属性结构体
&FilePath, // 当前的对象描述的是哪一个名称
OBJ_CASE_INSENSITIVE, // 表示对名称不区分大小写
NULL, NULL); // 对象根目录和安全属性字段
// 根据用户传入的布尔值设置 CreateOption 的值是目录\文件
ULONG CreateOption = IsFile ? FILE_NON_DIRECTORY_FILE
: FILE_DIRECTORY_FILE;
// 内核层操作文件的函数是 ZwCreateFile,如果失败返回失败原因
NTSTATUS Status = ZwCreateFile(
&FileHandle, // 成功调用函数后用于保存句柄的变量
Access, // 以什么样的权限去操作文件 GENERIC_ALL
&ObjectAttributes, // 主要用于保存文件的路径
&IoStatusBlock, // 保存的是当前函数的执行结果
0, // 如果是创建新的文件,那么文件默认的大小是多少
FILE_ATTRIBUTE_NORMAL, // 创建或打开的文件应该具有什么样的属性
FILE_SHARE_VALID_FLAGS, // 文件允许的共享方式,和 R3 保持一致
FILE_OPEN_IF, // 打开\创建文件的方式 FILE_OPEN_IF 有就开
CreateOption, // 当前属性我们只需要关注操作的是文件还是目录
NULL, 0); // 描述扩展区域的大小,还有指向此区域的指针
// 判断打开路径的操作是否成功,如果失败返回-1,否则返回句柄
if (NT_SUCCESS(Status))
return FileHandle;
else
return (HANDLE)-1;
}
// 向指定文件内写入指定大小的指定数据
NTSTATUS WriteFile(HANDLE FileHandle, PVOID Buffer, ULONG Length, ULONG pOffset)
{
IO_STATUS_BLOCK IoStatusBlock = { 0 };
LARGE_INTEGER Offset = { pOffset, 0 };
// 向指定的文件内写入指定长度的数据
NTSTATUS Status = ZwWriteFile(
FileHandle, // 需要操作的到底是哪个文件
NULL, NULL, NULL, // 异步 IO 使用的三个参数,几乎不用
&IoStatusBlock, // 保存了操作的结果,例如字节数和是否成功
Buffer, Length, // 需要写入的数据以及写入的长度
&Offset, 0); // 需要操作的文件指针的位置
// 无论是否操作成功,都直接返回操作结果
return Status;
}
// 从指定文件读取指定大小的指定数据
NTSTATUS ReadFile(HANDLE FileHandle, PVOID Buffer, ULONG Length, ULONG pOffset)
{
IO_STATUS_BLOCK IoStatusBlock = { 0 };
LARGE_INTEGER Offset = { pOffset, 0 };
// 从指定文件读取指定长度的数据
NTSTATUS Status = ZwReadFile(
FileHandle, // 需要操作的到底是哪个文件
NULL, NULL, NULL, // 异步 IO 使用的三个参数,几乎不用
&IoStatusBlock, // 保存了操作的结果,例如字节数和是否成功
Buffer, Length, // 需要读取的数据以及读取的长度
&Offset, 0); // 需要操作的文件指针的位置
// 无论是否操作成功,都直接返回操作结果
return Status;
}
// 获取指定文件的基本信息,该函数主要获取文件的大小
LONGLONG GetFileSize(HANDLE FileHandle)
{
IO_STATUS_BLOCK IoStatusBlock = { 0 };
FILE_STANDARD_INFORMATION FileInfo = { 0 };
// 用于查询文件信息的函数,可以查询非常多的信息
NTSTATUS Status = ZwQueryInformationFile(
FileHandle, // 需要查询哪个文件的信息
&IoStatusBlock, // 查询成功了还是失败了
&FileInfo, // 查询到的信息保存到哪里,结构体不是固定的
sizeof(FileInfo), // 提供的缓冲区的大小
FileStandardInformation); // 需要查询的是哪种类型的信息
// 如果成功返回查询的结果,否则返回 0
if (NT_SUCCESS(Status))
return FileInfo.EndOfFile.QuadPart;
else
return 0;
}
// 删除文件,不需要指定文件句柄
NTSTATUS DeleteFile(LPCWSTR pFilePath)
{
// 初始化文件或目录的名字保存到 UNICODE_STRING 中
UNICODE_STRING FilePath = { 0 };
RtlInitUnicodeString(&FilePath, pFilePath);
// 操作一个具名对象的时候,通常都需要提供结构体,描述文件的名字
OBJECT_ATTRIBUTES ObjectAttributes = { 0 };
InitializeObjectAttributes(
&ObjectAttributes, // 需要初始化的对象属性结构体
&FilePath, // 当前的对象描述的是哪一个名称
OBJ_CASE_INSENSITIVE, // 表示对名称不区分大小写
NULL, NULL); // 对象根目录和安全属性字段
// 返回删除的结果
return ZwDeleteFile(&ObjectAttributes);
}
// 遍历指定目录下的所有文件
VOID ListDirectoryFile(LPCWSTR DirPath)
{
// 获取指定目录的句柄,用于后续的遍历
HANDLE DirHandle = CreateFile(DirPath, GENERIC_ALL, FALSE);
// 创建一些用于支持遍历的结构体和变量
IO_STATUS_BLOCK IoStatusBlock = { 0 };
ULONG InfoSize = sizeof(FILE_FULL_DIR_INFORMATION) + 260 * 2;
PFILE_FULL_DIR_INFORMATION FileInfo = (PFILE_FULL_DIR_INFORMATION)
ExAllocatePoolWithTag(NonPagedPool, InfoSize, 'elif');
// 遍历指定目录下的第一个文件,并返回遍历到的结果
NTSTATUS Status = ZwQueryDirectoryFile(
DirHandle, // 需要遍历的是哪一个[目录]
NULL, NULL, NULL, // 基本不会用到的三个异步IO参数
&IoStatusBlock, // 保存 IO 操作的结果
FileInfo, InfoSize, // 保存数据的结构体以及其大小
FileFullDirectoryInformation, // 需要查询到的是什么信息
TRUE, // 是否仅返回一项内容
NULL, // 如果想要查询指定文件的信息,就需要传入
TRUE); // 是否是从第一个文件开始遍历
// 如果第一个文件的信息遍历成功,就尝试继续遍历其他文件的信息
if (NT_SUCCESS(Status))
{
do {
// 输出遍历到的文件的信息,并清除结构体内容,否则下次遍历可能保存之前的信息
KdPrint(("%S\n", FileInfo->FileName));
RtlZeroMemory(FileInfo, InfoSize);
// 遍历指定目录下的第一个文件,并返回遍历到的结果
ZwQueryDirectoryFile(
DirHandle, // 需要遍历的是哪一个[目录]
NULL, NULL, NULL, // 基本不会用到的三个异步IO参数
&IoStatusBlock, // 保存 IO 操作的结果
FileInfo, InfoSize, // 保存数据的结构体以及其大小
FileFullDirectoryInformation, // 需要查询到的是什么信息
TRUE, // 是否仅返回一项内容
NULL, // 如果想要查询指定文件的信息,就需要传入
FALSE); // 是否是从第一个文件开始遍历
// 结束条件的判断是 IoStatusBlock 而不是函数的返回值
} while (IoStatusBlock.Status != STATUS_NO_MORE_FILES);
}
// 释放空间并关闭句柄
ExFreePoolWithTag(FileInfo, 'elif');
ZwClose(DirHandle);
}
#include "header.h"
// 驱动的卸载函数,会在祛痘被卸载的时候调用
#pragma code_seg("PAGE") // 分页内存,能够被交换到页交换文件中
VOID DriverUnload(PDRIVER_OBJECT DriverOBject)
{
UNREFERENCED_PARAMETER(DriverOBject);
}
// 驱动程序的入口函数,参数一表示的是当前的驱动对象,参数二是位于注册表的路径
#pragma code_seg("INIT") // 只有在初始化的时候存在,初始化完毕内存就被释放
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverOBject, PUNICODE_STRING RegistryPath)
{
DbgBreakPoint();
UNREFERENCED_PARAMETER(RegistryPath);
// 只有设置了卸载函数才能够停止驱动程序
DriverOBject->DriverUnload = DriverUnload;
/*
// 如果文件存在,则以只读的方式打开指定的文件,否则就创建它,路径是 \\??\\...
HANDLE FileHandle = CreateFile(L"\\??\\D:\\data.txt", GENERIC_WRITE, TRUE);
// 向文件内写入字符串: "hello world"
WriteFile(FileHandle, "hello world", 12, 0);
// 文件打开以后必须使用 ZwClose 进行关闭,否则会产生文件占用
ZwClose(FileHandle);
FileHandle = CreateFile(L"\\??\\D:\\data.txt", GENERIC_READ, TRUE);
// 向文件内写入字符串: "hello world"
CHAR Buffer[0x10] = { 0 };
ReadFile(FileHandle, Buffer, 0x10, 0);
// 查询文件的大小
LONGLONG FileSize = GetFileSize(FileHandle);
KdPrint(("FileSize: %lld\n", FileSize));
// 文件打开以后必须使用 ZwClose 进行关闭,否则会产生文件占用
ZwClose(FileHandle);
DeleteFile(L"\\??\\D:\\data.txt");
*/
ListDirectoryFile(L"\\??\\C:\\");
return STATUS_SUCCESS;
}
六. 进程与线程
1. 进程
- 在windows中,每一个进程都是由一个进程执行体块来表示
- EPROCESS包含了很多进程相关的信息,每一个版本的操作系统中的EPROCESS结构是不一样的
#include <ntifs.h>
#include <ntddk.h>
// 函数有导出,但是没有声明,需要自己提供
NTKERNELAPI UCHAR* PsGetProcessImageFileName(__in PEPROCESS Process);
VOID ListCurrentProcessAndThread()
{
// 提供一个 EPROCESS 的指针,用于接受查询到的内容
PETHREAD Thread = NULL;
PEPROCESS Process = NULL;
// 提供一个用于遍历的范围,以 4 为递增值,暴力遍历所有的进程,由于
// 进程和线程被放置在了同一个位置,所以两者的 id 是处于同意序列的
for (ULONG id = 4; id <= 5000; id += 4)
{
// 尝试使用 pid 找到相应的 EOROCESS 结构体,如果找到就输出信息
if (NT_SUCCESS(PsLookupProcessByProcessId(ULongToHandle(id), &Process)))
{
// 通过 windows 提供的内置函数获取名称
KdPrint(("[%d][P]: %s\n", id, PsGetProcessImageFileName(Process)));
// 如果操作使指针引用计数 +1 了,那么就需要 -1
ObDereferenceObject(Process);
}
// 如果不是进程,还有可能是线程,再进行一次判断
else if (NT_SUCCESS(PsLookupThreadByThreadId(ULongToHandle(id), &Thread)))
{
// 通过函数函数获取当前线程的所属进程
PEPROCESS Process2 = IoThreadToProcess(Thread);
KdPrint(("[%d][T]:%d\n", id, PsGetProcessId(Process2)));
// 操作完毕以后,需要手动的减少引用计数
ObDereferenceObject(Thread);
}
}
}
// 线程回调函数,这个函数是一个永不返回的函数
VOID WorkerThread(PVOID StartContext)
{
UNREFERENCED_PARAMETER(StartContext);
// 编写循环,输出内容
for (int i = 0; i < 10; ++i)
KdPrint(("%d\n", i));
// 任何一个系统线程,都必须使用下面的函数自我销毁
PsTerminateSystemThread(STATUS_SUCCESS);
}
// 创建线程
VOID CreateThread()
{
// 创建线程句柄用于保存线程
HANDLE Thread = NULL;
// 创建一个位于系统的线程
NTSTATUS Status = PsCreateSystemThread(&Thread, 0, NULL,
NULL, NULL, WorkerThread, NULL);
// 如果线程内核独享创建成功了,就尝试等待线程
if (NT_SUCCESS(Status))
{
PVOID ThreadObject = NULL;
Status = ObReferenceObjectByHandle(Thread, GENERIC_ALL, NULL, KernelMode, &ThreadObject, NULL);
if (NT_SUCCESS(Status))
{
KeWaitForSingleObject(ThreadObject, Executive, KernelMode, FALSE, 0);
KdPrint(("thread over....\n"));
ZwClose(Thread);
ObDereferenceObject(ThreadObject);
}
}
}
#include "header.h"
// 驱动程序的卸载函数,会在卸载时调用,通常用于删除设备对象
#pragma code_seg("PAGE") // 分页内存: 能过够被交换到页交换文件的内存
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
}
// 驱动程序的入口函数,参数一表示当前的驱动对象,参数二是在注册表的路径
#pragma code_seg("INIT") // 只有在初始化的时候存在,初始化完毕被卸载的内存
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgBreakPoint();
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
// 只有设置了卸载函数的驱动程序才能被卸载
DriverObject->DriverUnload = DriverUnload;
ListCurrentProcessAndThread();
CreateThread();
// 只有当返回值为 STATUS_SUCCESS 驱动才会被加载到系统中
return STATUS_SUCCESS;
}
2. 线程
- 线程也是一个内核对象,同样的每一个线程也有子的内存区域
- ETHREAD中包含了很多线程相关的信息,同样跟操作系统版本有关
SDT表 系统描述符表
定时器需要硬件支持 运行在DPC上,IO是每隔一秒,DPC每隔指定
操作系统提供的函数 返回值都是NTSTATUS
3. 进程挂靠
#include <ntifs.h>
// 驱动程序的卸载函数,会在卸载时调用,通常用于删除设备对象
#pragma code_seg("PAGE") // 分页内存: 能过够被交换到页交换文件的内存
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
}
// 驱动程序的入口函数,参数一表示当前的驱动对象,参数二是在注册表的路径
#pragma code_seg("INIT") // 只有在初始化的时候存在,初始化完毕被卸载的内存
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgBreakPoint();
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
// 只有设置了卸载函数的驱动程序才能被卸载
DriverObject->DriverUnload = DriverUnload;
// 进程挂靠可以让一个进程访问到另一个进程的内存空间
PEPROCESS Process = NULL;
PsLookupProcessByProcessId(ULongToHandle(4068), &Process);
KAPC_STATE ApcState = { 0 };
KeStackAttachProcess(Process, &ApcState);
// 在挂靠之后就可以访问其它进程的内存空间
CHAR* Buffer = (CHAR*)0x002CFD74;
Buffer[0] = Buffer[1] = Buffer[2] = '0';
KeUnstackDetachProcess(&ApcState);
// 只有当返回值为 STATUS_SUCCESS 驱动才会被加载到系统中
return STATUS_SUCCESS;
}
#include <stdio.h>
#include <windows.h>
int main()
{
char buffer[] = "hello 15pb";
printf("[%d]: %p %s\n", GetCurrentProcessId(), buffer, buffer);
system("pause");
printf("[%d]: %p %s\n", GetCurrentProcessId(), buffer, buffer);
system("pause");
return 0;
}
外挂修改 内存dump 内存校验保护
4. 同步操作
内核层同步方式
- 原子操作
- 事件
- 互斥体
- 信号量
- 自旋锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ROOAMCOF-1616297958263)(E:/%E8%BD%AF%E4%BB%B6%E4%B8%8B%E8%BD%BD/Typora/%E5%9B%BE%E7%89%87/image-20210218183918667.png)]
汇编层面 lock指令
快速互斥体只能一次打开 多次蓝屏
5. 注册表操作
使用封装过的RTL函数
6. 链表操作
7. 异常处理
- 在内核层,依然可以使用SEH进行异常处理,将可能出错的代码包裹在SEH中,有效防止蓝屏
七. 内核对象
1. 内核对象
2. 内核对象结构
任何一个对象结构体减去0x18就是header
3. 获取内核对象
4. Object_Hook
#include <ntifs.h>
// 要 HOOK 的函数保存的位置
typedef struct _OBJECT_TYPE_INITIALIZER {
USHORT Length;
UCHAR ObjectTypeFlags;
UCHAR CaseInsensitive;
UCHAR UnnamedObjectsOnly;
UCHAR UseDefaultObject;
UCHAR SecurityRequired;
UCHAR MaintainHandleCount;
UCHAR MaintainTypeList;
UCHAR SupportsObjectCallbacks;
UCHAR CacheAligned;
ULONG ObjectTypeCode;
BOOLEAN InvalidAttributes;
GENERIC_MAPPING GenericMapping;
BOOLEAN ValidAccessMask;
BOOLEAN RetainAccess;
POOL_TYPE PoolType;
BOOLEAN DefaultPagedPoolCharge;
BOOLEAN DefaultNonPagedPoolCharge;
PVOID DumpProcedure;
ULONG OpenProcedure;
PVOID CloseProcedure;
PVOID DeleteProcedure;
ULONG ParseProcedure;
ULONG SecurityProcedure;
ULONG QueryNameProcedure;
UCHAR OkayToCloseProcedure;
} OBJECT_TYPE_INITIALIZER, * POBJECT_TYPE_INITIALIZER;
// ObGetObjectType 的返回值
typedef struct _OBJECT_TYPE {
LIST_ENTRY TypeList;
UNICODE_STRING Name;
PVOID DefaultObject;
ULONG Index;
ULONG TotalNumberOfObjects;
ULONG TotalNumberOfHandles;
ULONG HighWaterNumberOfObjects;
ULONG HighWaterNumberOfHandles;
OBJECT_TYPE_INITIALIZER TypeInfo;
ULONG TypeLock;
ULONG Key;
LIST_ENTRY CallbackList;
} OBJECT_TYPE, * POBJECT_TYPE;
// 需要 HOOK 的函数的原型
typedef NTSTATUS(*PParseProcedure)(
IN PVOID ParseObject,
IN PVOID ObjectType,
IN OUT PACCESS_STATE AccessState,
IN KPROCESSOR_MODE AccessMode,
IN ULONG Attributes,
IN OUT PUNICODE_STRING CompleteName,
IN OUT PUNICODE_STRING RemainingName,
IN OUT PVOID Context OPTIONAL,
IN PSECURITY_QUALITY_OF_SERVICE SecurityQos OPTIONAL,
OUT PVOID* Object);
// ObGetObjectType 的函数原型
typedef POBJECT_TYPE(*PObGetObjectType)(PVOID Object);
// 使用指定的方式创建或打开一个文件或目录
HANDLE CreateFile(LPCWSTR pFilePath, ACCESS_MASK Access, ULONG CreateDisposition, BOOLEAN IsFile)
{
HANDLE FileHandle = NULL;
IO_STATUS_BLOCK IoStatusBlock = { 0 };
// 初始化文件或目录对应的路径字符串
UNICODE_STRING FilePath = { 0 };
RtlInitUnicodeString(&FilePath, pFilePath);
// 操作文件的时候,通常都需要提供这个结构体,描述文件的名称
OBJECT_ATTRIBUTES ObjectAttributes = { 0 };
InitializeObjectAttributes(
&ObjectAttributes, // 需要初始化的结构体
&FilePath, // 文件或目录的路径
OBJ_CASE_INSENSITIVE, // 名称不需要区分大小写
NULL, NULL); // 对象根目录路径
// 根据用户传入的布尔值设置 CreateOptions 的值为目录\非目录
ULONG CreateOptions = IsFile ? FILE_NON_DIRECTORY_FILE
: FILE_DIRECTORY_FILE;
// 内核层操作文件的函数,如果失败,返回原因
NTSTATUS Status = ZwCreateFile(
&FileHandle, // 成功调用函数后保存句柄的变量
Access, // 以什么样的权限操作文件 GENERIC_ALL
&ObjectAttributes, // 主要描述的是需要操作的路径
&IoStatusBlock, // 保存当前函数的执行结果
0, // 如果是覆盖或创建文件,表示默认的文件大小
FILE_ATTRIBUTE_NORMAL, // 创建出的文件具有什么样的属性
0, // 文件的允许的共享方式(读 | 写 | 删除)
CreateDisposition, // 创建的方式,FILE_CREATE 或 FILE_OPEN_IF
CreateOptions, // 可以通过这个字段设置操作的是文件还是目录
0, 0); // 描述扩展区域的大小以及指向它的指针
// 判断文件的操作是否成功,返回相应的值
if (NT_SUCCESS(Status))
return FileHandle;
else
return (HANDLE)-1;
}
// 获取到内置的函数
PObGetObjectType GetObjectTypeAddress()
{
UNICODE_STRING pslookup = RTL_CONSTANT_STRING(L"ObGetObjectType");
return (PObGetObjectType)MmGetSystemRoutineAddress(&pslookup);
}
// 保存 HOOK 的地址以及 HOOK 前的函数
PULONG HookAddress = NULL;
PParseProcedure HookFunction = NULL;
NTSTATUS MyParseProcedure(
IN PVOID ParseObject,
IN PVOID ObjectType,
IN OUT PACCESS_STATE AccessState,
IN KPROCESSOR_MODE AccessMode,
IN ULONG Attributes,
IN OUT PUNICODE_STRING CompleteName,
IN OUT PUNICODE_STRING RemainingName,
IN OUT PVOID Context OPTIONAL,
IN PSECURITY_QUALITY_OF_SERVICE SecurityQos OPTIONAL,
OUT PVOID* Object)
{
KdPrint(("hook success\n"));
return HookFunction(ParseObject, ObjectType, AccessState, AccessMode,
Attributes, CompleteName, RemainingName, Context, SecurityQos, Object);
}
// 开启对象函数的 HOOK,主要 HOOK 文件对象用于解析名称的函数
VOID OnObjectHook()
{
// 1. 随意的打开一个文件
HANDLE File = CreateFile(L"\\??\\D:\\data.txt", GENERIC_ALL, FILE_CREATE, TRUE);
// 2. 获取到它的结构体指针(对象指针)
PVOID ObjectBuffer = NULL;
ObReferenceObjectByHandle(File, GENERIC_ALL, NULL, KernelMode, &ObjectBuffer, NULL);
// 3. 获取到函数,用于取得对象的类型结构
PObGetObjectType ObGetObjectType = GetObjectTypeAddress();
POBJECT_TYPE Type = ObGetObjectType(ObjectBuffer);
// 4. 保存 HOOK 的函数地址和函数,方便后续进行还原
HookAddress = &Type->TypeInfo.ParseProcedure;
HookFunction = (PParseProcedure)Type->TypeInfo.ParseProcedure;
// 5. 使用自己的函数替代默认的函数
Type->TypeInfo.ParseProcedure = (ULONG)MyParseProcedure;
}
VOID OffObjectHook()
{
*HookAddress = (ULONG)HookFunction;
}
#include "header.h"
// 驱动程序的卸载函数,会在卸载时调用,通常用于删除设备对象
#pragma code_seg("PAGE") // 分页内存: 能过够被交换到页交换文件的内存
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
OffObjectHook();
UNREFERENCED_PARAMETER(DriverObject);
}
// 驱动程序的入口函数,参数一表示当前的驱动对象,参数二是在注册表的路径
#pragma code_seg("INIT") // 只有在初始化的时候存在,初始化完毕被卸载的内存
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgBreakPoint();
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
// 只有设置了卸载函数的驱动程序才能被卸载
DriverObject->DriverUnload = DriverUnload;
OnObjectHook();
// 只有当返回值为 STATUS_SUCCESS 驱动才会被加载到系统中
return STATUS_SUCCESS;
}
八. 系统调用
SSDT:主要处理 Kernel32.dll中的系统调用
OpenProcess,ReadFile
ShadowSSDT:主要处理user32.dll,GDI32.dll中的调用函数
PostMessage,FindWindow
ShadowSSDT并未导出,可用ida在win32k.sys中的导出表中搜索,结构与SSDT相似,且只在GUI环境下由有值,所以需要调用KeAttachStackProcess来切换到GUI线程里看到
SSDT表介绍
ntdll.dll模块中的函数有些以nt或zw开头的函数为了完成相应的功能需要进入内核,调用内核中以nt开头的函数来完成相应的功能。ntdll.dll里的函数在进入内核层之前首先将系统服务号传入eax寄存器中,然后调用KiSystemService函数进入内核层。进入内核后会根据eax值索引ssdt表里的函数进行执行相应地址的函数。
SSDT的每一项是一个系统服务函数的地址,可以通过HOOK这些函数完成特定的功能。
32位系统上SSDT是导出的,64位是不会导出的。
通过PCHunter查看win7 x64系统的SSDT表:
如何获得SSDT表的地址和每一个项对应的服务名称呢?
注意:内核文件有多个,操作系统会根据当前cpu和分页方式选择不同的内核文件。
ntoskrnl.exe - 单处理器,不支持PAE分页模式;
ntkrnlpa.exe - 单处理器,支持PAE分页模式;
ntkrnlmp.exe - 多处理器,不支持PAE分页模式;
ntkrpamp.exe - 多处理器,支持PAE分页模式。
32位系统
32系统上ntdll.dll使用mov eax,xxx传入索引值,可以通过遍历ntdll.dll查看每一个函数对应的服务号,从而找到服务函数名和服务编号的关系。另外,内核中32位的SSDT的起始地址是直接在ntoskrnl.exe中通过KeServiceDescriptorTable符号导出,不需要使用工具来获得,可以直接在驱动程序中引用该符号的地址。注意:在代码实现上应当引入头文件#include <ntimage.h>之后使用语句
extern SSDTEntry __declspec(dllimport) KeServiceDescriptorTable;
来获得KeServiceDescriptorTable的地址。
32位系统中KeServiceDescriptorTable结构如下图所示
#pragma pack(1)
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
PULONG ServiceTableBase;//SSDT的起始地址
PULONG ServiceCounterTableBase;//
ULONG NumberOfService;//SSDT表中服务函数的总数
PUCHAR ParamTableBase;//服务函数的参数个数数组的起始地址,数组的每一个成员占1字节,记录的值是对应函数的参数个数*4
} SSDTEntry, *PSSDTEntry;
#pragma pack()
ServiceTableBase的内容是SSDT表的起始地址,然后从ServiceTableBase开始是一个长度为NumberOfService的指针数组,每一项是4个字节,是SSDT表中每一个服务的函数地址。
在内核调试器windbg中使用dd KeServiceDescriptorTable命令查看KeServiceDescriptorTable数据,就可以看到SSDTEntry结构的每一项数据。
根据服务表号能够决定调用哪张表,服务表号如果为0则加0,SSDT
否则加0x10的位置,ShadowsSSDT
ntoskerl.exe 核心 组件模块
近跳保存eip
远跳保存cs和eip
1. KiFastCallEntry
; 将寄存器(段寄存器和栈)从 R3 切换到 R0
mov ecx,23h ; 通过 ecx 设置 ds 和 es 段选择子为 0x23
push 30h
pop fs ; 将 fs 段选择子切换为 0x30,此时指向 KPCR
mov ds,cx
mov es,cx
mov ecx,dword ptr fs:[40h] ; 获取 TSS 中的 R0 ESP 并进行设置
mov esp,dword ptr [ecx+4]
; 构建 _KTRAP_FRAME 保存 R3 的寄存器状态
push 23h
push edx
pushfd
push 2
add edx,8
popfd
or byte ptr [esp+1],2
push 1Bh
push dword ptr ds:[0FFDF0304h]
push 0
push ebp
push ebx
push esi
push edi
mov ebx,dword ptr fs:[1Ch] ; 指向 KPCR(自己) 的指针
push 3Bh
mov esi,dword ptr [ebx+124h] ; 保存 CurrentThread,即当前线程的 KTHREAD
push dword ptr [ebx]
mov dword ptr [ebx],0FFFFFFFFh
mov ebp,dword ptr [esi+28h] ; ebp 指向 KTRAP_FRAME 结构体首地址
push 1
sub esp,48h
sub ebp,29Ch
mov byte ptr [esi+13Ah],1 ; 设置 CurrentThread.PreviousMode 为 1
cmp ebp,esp
jne nt!KiFastCallEntry2+0x49 (83e760bb)
and dword ptr [ebp+2Ch],0
test byte ptr [esi+3],0DFh
mov dword ptr [esi+128h],ebp
jne nt!Dr_FastCallDrSave (83e75f70)
mov ebx,dword ptr [ebp+60h]
mov edi,dword ptr [ebp+68h]
mov dword ptr [ebp+0Ch],edx ; 设置参数指向用户层传递的函数参数
mov dword ptr [ebp+8],0BADB0D00h
mov dword ptr [ebp],ebx
mov dword ptr [ebp+4],edi
; 开启中断标志位
sti
; 通过调用号判定当前是 SSDT 还是 Shadow SSDT 调用
mov edi,eax ; 获取函数的调用号,调用号的组成是
shr edi,8 ; 19(未使用)+1(服务表号)+12(索引)
and edi,10h
mov ecx,edi ; 判断当前使用的是 ShadowSSDT 表
add edi,dword ptr [esi+0BCh] ; 还是 SSDT 表,并找到对应的结构
; 找到 VOID* ServiceTable
mov ebx,eax
and eax,0FFFh ; 获取到调用号中的(真实)索引部分
cmp eax,dword ptr [edi+8]
jae nt!KiBBTUnexpectedRange (83e75ea2) ; 如果超出了有效的调用号范围就报错
cmp ecx,10h
jne nt!KiFastCallEntry+0xce (83e7618e) ; 如果当前为 SSDT 表的调用则跳转
mov ecx,dword ptr [esi+88h] ; 如果是 Shadow SSDT 则进行其他操作
xor esi,esi
or esi,dword ptr [ecx+0F70h]
je nt!KiFastCallEntry+0xce (83e7618e)
push edx
push eax
call dword ptr [nt!KeGdiFlushUserBatch (83fa294c)]
pop eax
pop edx
; 如果当前是 SSDT 函数调用执行的操作
nt!KiFastCallEntry+0xce:
inc dword ptr fs:[6B0h] ; 产生系统调用的次数 KPCR->KeSystemCalls 加一
mov esi,edx ; esi 指向用户传递的参数在栈中的位置
xor ecx,ecx
mov edx,dword ptr [edi+0Ch] ; 找到 SSDT 中的参数表并保存到 edx 中
mov edi,dword ptr [edi+00h] ; 找到 SSDT 中的函数表并保存到 edi 中
mov cl,byte ptr [eax+edx] ; ecx 保存当前函数所需要的字节个数
mov edx,dword ptr [edi+eax*4] ; edx 保存当前函数的真实地址
sub esp,ecx ; 在栈中开辟空间用于保存所有的参数
shr ecx,2 ; 字节数 / 4 保存了参数的个数
mov edi,esp ; esi=用户空间参数 edi=系统空间参数
cmp esi,dword ptr [nt!MmUserProbeAddress (83fa271c)]
jae nt!KiSystemCallExit2+0xa5 (83e763e5) ; 判断用户参数是否在有效范围(0x7FFF0000)内
rep movs dword ptr es:[edi],dword ptr [esi] ; 将参数从用户空间拷贝到系统空间
test byte ptr [ebp+6Ch],1
je nt!KiFastCallEntry+0x115 (83e761d5) ; 获取 CPL 和 1 进行与运算,如果是 R3 则跳转
mov ecx,dword ptr fs:[124h] ; 如果是 R0 的调用则执行其他操作
mov edi,dword ptr [esp]
mov dword ptr [ecx+13Ch],ebx
mov dword ptr [ecx+12Ch],edi
nt!KiFastCallEntry+0x115:
mov ebx,edx ; 获取真实的函数地址
test byte ptr [nt!PerfGlobalGroupMask+0x8 (83f6f908)],40h
setne byte ptr [ebp+12h]
jne nt!KiServiceExit2+0x17b (83e76574)
call ebx ; 调用对应的 SSDT 函数
2. KPCR
//0x3748 bytes (sizeof)
struct _KPCR
{
union
{
struct _NT_TIB NtTib; //0x0
struct
{
struct _EXCEPTION_REGISTRATION_RECORD* Used_ExceptionList; //0x0
VOID* Used_StackBase; //0x4
VOID* Spare2; //0x8
VOID* TssCopy; //0xc
ULONG ContextSwitches; //0x10
ULONG SetMemberCopy; //0x14
VOID* Used_Self; //0x18
};
};
struct _KPCR* SelfPcr; //0x1c
struct _KPRCB* Prcb; //0x20
UCHAR Irql; //0x24
ULONG IRR; //0x28
ULONG IrrActive; //0x2c
ULONG IDR; //0x30
VOID* KdVersionBlock; //0x34
struct _KIDTENTRY* IDT; //0x38
struct _KGDTENTRY* GDT; //0x3c
struct _KTSS* TSS; //0x40
USHORT MajorVersion; //0x44
USHORT MinorVersion; //0x46
ULONG SetMember; //0x48
ULONG StallScaleFactor; //0x4c
UCHAR SpareUnused; //0x50
UCHAR Number; //0x51
UCHAR Spare0; //0x52
UCHAR SecondLevelCacheAssociativity; //0x53
ULONG VdmAlert; //0x54
ULONG KernelReserved[14]; //0x58
ULONG SecondLevelCacheSize; //0x90
ULONG HalReserved[16]; //0x94
ULONG InterruptMode; //0xd4
UCHAR Spare1; //0xd8
ULONG KernelReserved2[17]; //0xdc
struct _KPRCB PrcbData; //0x120
};
3. SysEntryHook
#include <ntddk.h>
// 原始函数
ULONG g_OldKiFastCallEntry = 0;
// 要保护的进程PID
ULONG g_pid = 0;
// 1. 获取原始KiFastCallEntry函数
// 1.1 ecx 保存寄存器号
// 1.2 rdmsr 调用指令
// 1.3 eax 返回保存原始函数
// 2. 将我们过滤函数替换到msr 0x176号寄存器中
// 2.1 ecx 保存寄存器号,eax 写入过滤函数
// 2.2 wrmsr 调用指令
// 3. 卸载钩子
// 3.1 ecx 保存寄存器号,eax 写入原始函数
// 2.2 wrmsr 调用指令
// 过滤函数
void _declspec(naked) MyKiFastCallEntry()
{
// 过滤 ZwOpenProcess,调用号eax == 0x0BE
// edx保存用户栈
// [edx + 0x00] : 返回地址1
// [edx + 0x04] : 返回地址2
// [edx + 0x08] : 参数1 ProcessHandle
// [edx + 0x0c] : 参数2 DesiredAccess
// [edx + 0x10] : 参数3 ObjectAttributes
// [edx + 0x14] : 参数4 ClientId
/*
https://github.com/j00ru/windows-syscalls/
*/
_asm
{
pushad; // 保存寄存器
cmp eax, 0x0BE; // 是否是ZwOpenProcess 函数
jne CallEnd; // 结束过滤
mov eax, [edx + 0x14]; // 获取第四个参数 ClientId
mov eax, [eax]; // ClientId->ProcessId
cmp eax, g_pid; // 判断是否要保存的进程
jne CallEnd;
mov[edx + 0x0c], 0; // 将权限改为0,无法访问
CallEnd:
popad; //恢复寄存器
jmp g_OldKiFastCallEntry; //调用原始函数
}
}
// 安装钩子
void InstallHook()
{
// 获取原始函数
_asm
{
// 从 MSR 0x176 的位置读取原有的 KiFastCallEntry 函数进行保存
mov ecx, 0x176;
rdmsr; //将msr176寄存器的内容保存eax
mov g_OldKiFastCallEntry, eax; //保存原始函数地址
}
// 设置钩子
_asm
{
mov ecx, 0x176;
mov eax, MyKiFastCallEntry; // 过滤函数
wrmsr; //写入到msr0x176寄存器
}
}
// 卸载钩子
void UnInstallHook()
{
// 设置钩子
_asm
{
mov ecx, 0x176;
mov eax, g_OldKiFastCallEntry; // 过滤函数
wrmsr; //写入到msr0x176寄存器
}
}
// 卸载函数
void UnLoadDriver(PDRIVER_OBJECT pDriver)
{
// 卸载钩子
UnInstallHook();
pDriver;
return;
}
// 入口函数
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pPath)
{
pDriver->DriverUnload = UnLoadDriver;
pPath;
// 要保护进程
g_pid = 2684;
// 安装钩子
InstallHook();
return STATUS_SUCCESS;
}
九. SSDT Hook
ssdt hook
系统服务描述表
管理系统函数ntdll
#include <ntddk.h>
// SSDT hook 保护一下计算器
#pragma pack(1)
typedef struct _ServiceDesriptorEntry
{
ULONG* ServiceTableBase; // 服务表基址
ULONG* ServiceCounterTableBase; // 计数表基址
ULONG NumberOfServices; // 表中项的个数
UCHAR* ParamTableBase; // 参数表基址
}SSDTEntry, * PSSDTEntry;
#pragma pack()
// 函数原型
typedef NTSTATUS(NTAPI* FnZwOpenProcess)(PHANDLE,
ACCESS_MASK,
POBJECT_ATTRIBUTES,
PCLIENT_ID);
// 导入SSDT,直接声明就能使用
NTSYSAPI SSDTEntry KeServiceDescriptorTable;
// 要保护的进程pid
ULONG g_pid = 0;
// 保存原始函数地址
FnZwOpenProcess g_OldZwOpenProcess;
// 过滤函数
NTSTATUS NTAPI MyZwOpenProcess(
__out PHANDLE ProcessHandle,
__in ACCESS_MASK DesiredAccess,
__in POBJECT_ATTRIBUTES ObjectAttributes,
__in_opt PCLIENT_ID ClientId
)
{
// 判断是否是我们的函数
if ((ULONG)ClientId->UniqueProcess == g_pid)
DesiredAccess = 0;
// 调用原始函数
return g_OldZwOpenProcess(ProcessHandle,
DesiredAccess,
ObjectAttributes,
ClientId);
}
// 关闭页保护
void OffPageProcted()
{
_asm
{
mov eax, cr0;
and eax, ~0x10000;
mov cr0, eax;
}
}
// 开页保护
void OnPageProcted()
{
_asm
{
mov eax, cr0;
or eax, 0x10000;
mov cr0, eax;
}
}
// SSDT hook函数
void InstallHook()
{
//1.保存原始函数地址
g_OldZwOpenProcess =
(FnZwOpenProcess)KeServiceDescriptorTable.ServiceTableBase[0xBE];
// 关闭页保护
OffPageProcted();
// 2. 替换我们的过滤函数
InterlockedExchange(
&KeServiceDescriptorTable.ServiceTableBase[0xBE],
MyZwOpenProcess);
// 开页保护
OnPageProcted();
}
// SSDT unhook函数
void UnInstallHook()
{
// 关闭页保护
OffPageProcted();
// 2. 替换我们的过滤函数
InterlockedExchange(
&KeServiceDescriptorTable.ServiceTableBase[0xBE],
g_OldZwOpenProcess);
// 开页保护
OnPageProcted();
}
// 卸载函数
void UnLoadDriver(PDRIVER_OBJECT pDriver)
{
// 卸载钩子
UnInstallHook();
pDriver;
return;
}
// 入口函数
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pPath)
{
pDriver->DriverUnload = UnLoadDriver;
pPath;
// 要保护进程
g_pid = 2720;
// 安装钩子
InstallHook();
return STATUS_SUCCESS;
}
十. 内核重载
sub esp,ecx
shr ecx,2
#include <ntifs.h>
#include <ntimage.h>
PDRIVER_OBJECT g_pDriver = NULL;
#pragma pack(1)
typedef struct _ServiceDesriptorEntry
{
ULONG *ServiceTableBase; // 服务表基址
ULONG *ServiceCounterTableBase; // 计数表基址
ULONG NumberOfServices; // 表中项的个数
UCHAR *ParamTableBase; // 参数表基址
}SSDTEntry, *PSSDTEntry;
#pragma pack()
// 导入SSDT
NTSYSAPI SSDTEntry KeServiceDescriptorTable;
PSSDTEntry g_pNewSSDT;//新的SSDT
ULONG g_JmpPoint;
PUCHAR pHookPoint;
// 打开文件
HANDLE KernelCreateFile(
IN PUNICODE_STRING pstrFile, // 文件路径符号链接
IN BOOLEAN bIsDir) // 是否为文件夹
{
HANDLE hFile = NULL;
NTSTATUS Status = STATUS_UNSUCCESSFUL;
IO_STATUS_BLOCK StatusBlock = { 0 };
ULONG ulShareAccess =
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
ULONG ulCreateOpt =
FILE_SYNCHRONOUS_IO_NONALERT;
// 1. 初始化OBJECT_ATTRIBUTES的内容
OBJECT_ATTRIBUTES objAttrib = { 0 };
ULONG ulAttributes =
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE;
InitializeObjectAttributes(
&objAttrib, // 返回初始化完毕的结构体
pstrFile, // 文件对象名称
ulAttributes, // 对象属性
NULL, NULL); // 一般为NULL
// 2. 创建文件对象
ulCreateOpt |= bIsDir ?
FILE_DIRECTORY_FILE : FILE_NON_DIRECTORY_FILE;
Status = ZwCreateFile(
&hFile, // 返回文件句柄
GENERIC_ALL, // 文件操作描述
&objAttrib, // OBJECT_ATTRIBUTES
&StatusBlock, // 接受函数的操作结果
0, // 初始文件大小
FILE_ATTRIBUTE_NORMAL, // 新建文件的属性
ulShareAccess, // 文件共享方式
FILE_OPEN_IF, // 文件存在则打开不存在则创建
ulCreateOpt, // 打开操作的附加标志位
NULL, // 扩展属性区
0); // 扩展属性区长度
if (!NT_SUCCESS(Status))
return (HANDLE)-1;
return hFile;
}
// 获取文件大小
ULONG64 KernelGetFileSize(IN HANDLE hfile)
{
// 查询文件状态
IO_STATUS_BLOCK StatusBlock = { 0 };
FILE_STANDARD_INFORMATION fsi = { 0 };
NTSTATUS Status = STATUS_UNSUCCESSFUL;
Status = ZwQueryInformationFile(
hfile, // 文件句柄
&StatusBlock, // 接受函数的操作结果
&fsi, // 根据最后一个参数的类型输出相关信息
sizeof(FILE_STANDARD_INFORMATION),
FileStandardInformation);
if (!NT_SUCCESS(Status))
return 0;
return fsi.EndOfFile.QuadPart;
}
// 读取文件
ULONG64 KernelReadFile(
IN HANDLE hfile, // 文件句柄
IN PLARGE_INTEGER Offset, // 从哪里开始读取
IN ULONG ulLength, // 读取多少字节
OUT PVOID pBuffer) // 保存数据的缓存
{
// 1. 读取文件
IO_STATUS_BLOCK StatusBlock = { 0 };
NTSTATUS Status = STATUS_UNSUCCESSFUL;
Status = ZwReadFile(
hfile, // 文件句柄
NULL, // 信号状态(一般为NULL)
NULL, NULL, // 保留
&StatusBlock, // 接受函数的操作结果
pBuffer, // 保存读取数据的缓存
ulLength, // 想要读取的长度
Offset, // 读取的起始偏移
NULL); // 一般为NULL
if (!NT_SUCCESS(Status)) return 0;
// 2. 返回实际读取的长度
return StatusBlock.Information;
}
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks; //双向链表
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
USHORT LoadCount;
USHORT TlsIndex;
union {
LIST_ENTRY HashLinks;
struct {
PVOID SectionPointer;
ULONG CheckSum;
};
};
union {
struct {
ULONG TimeDateStamp;
};
struct {
PVOID LoadedImports;
};
};
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
// 搜索内存特征
void * SearchMemory(char * buf, int BufLenth, char * Mem, int MaxLenth)
{
int MemIndex = 0;
int BufIndex = 0;
for (MemIndex = 0; MemIndex < MaxLenth; MemIndex++)
{
BufIndex = 0;
if (Mem[MemIndex] == buf[BufIndex] || buf[BufIndex] == '?')
{
int MemIndexTemp = MemIndex;
do
{
MemIndexTemp++;
BufIndex++;
} while ((Mem[MemIndexTemp] == buf[BufIndex] || buf[BufIndex] == '?') && BufIndex < BufLenth);
if (BufIndex == BufLenth)
{
return Mem + MemIndex;
}
}
}
return 0;
}
// 关闭页保护
void OffProtected()
{
__asm { //关闭内存保护
cli;
push eax;
mov eax, cr0;
and eax, ~0x10000;
mov cr0, eax;
pop eax;
}
}
// 开启页保护
void OnProtected()
{
__asm { //恢复内存保护
push eax;
mov eax, cr0;
or eax, 0x10000;
mov cr0, eax;
pop eax;
sti;
}
}
// 通过名称获取模块地址
ULONG32 MyGetModuleHandle(PUNICODE_STRING pModuleName)
{
PLDR_DATA_TABLE_ENTRY pLdr =
(PLDR_DATA_TABLE_ENTRY)g_pDriver->DriverSection;
LIST_ENTRY *pTemp = &pLdr->InLoadOrderLinks;
do
{
PLDR_DATA_TABLE_ENTRY pDriverInfo =
(PLDR_DATA_TABLE_ENTRY)pTemp;
if (RtlCompareUnicodeString(pModuleName, &pDriverInfo->BaseDllName, FALSE) == 0)
{
return pDriverInfo->DllBase;
}
pTemp = pTemp->Blink;
} while (pTemp != &pLdr->InLoadOrderLinks);
return 0;
}
//windows根据不同的环境,会加载不同的内核文件
//单核,开了PAE
//单核,没开PAE
//多核,开了PAE
//多核,没开PAE
// 读取内核模块到内存中
void ReadKernelToBuf(PWCHAR pPath, PUCHAR* pBuf)
{
//-----------------------------------------
UNICODE_STRING pKernelPath; //内核文件路径
HANDLE hFile = 0; //内核文件句柄
LARGE_INTEGER Offset = { 0 };//读取的偏移值
//-----------------------------------------
//1 打开文件
RtlInitUnicodeString(
&pKernelPath,
pPath);
hFile = KernelCreateFile(&pKernelPath, FALSE);
//2 获取文件大小
ULONG64 ulFileSize = KernelGetFileSize(hFile);
*pBuf = ExAllocatePool(NonPagedPool, ulFileSize);
RtlZeroMemory(*pBuf, ulFileSize);
//3 读取文件到内存
KernelReadFile(hFile, &Offset, ulFileSize, *pBuf);
}
// 展开内核PE文件
void ZKKernel(PUCHAR * pZkBUf, PUCHAR buf)
{
//1 获得DOS头,继而获得NT头,再获得扩展头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buf;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buf);
ULONG uZkSize = pNt->OptionalHeader.SizeOfImage;
//2 申请空间
*pZkBUf = ExAllocatePool(NonPagedPool, uZkSize);
RtlZeroMemory(*pZkBUf, uZkSize);
//3 开始展开
//3.1 先拷贝头部
memcpy(*pZkBUf, buf, pNt->OptionalHeader.SizeOfHeaders);
//3.2再拷贝区段
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
memcpy(
*pZkBUf + pSection[i].VirtualAddress,//本区段内存中的起始位置
buf + pSection[i].PointerToRawData, //本区段在文件中的位置
pSection[i].Misc.VirtualSize //本区段的大小
);
}
}
// 修复新内核重定位
void FixReloc(PUCHAR ZkBuf, PUCHAR OldBase)
{
typedef struct _TYPE {
USHORT Offset : 12;
USHORT Type : 4;
}TYPE, *PTYPE;
//1 获得DOS头,继而获得NT头,再获得扩展头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)ZkBuf;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + ZkBuf);
//2 获得重定位表
PIMAGE_DATA_DIRECTORY pRelocDir = (pNt->OptionalHeader.DataDirectory + 5);
PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)
(pRelocDir->VirtualAddress + ZkBuf);
//2.5 得到一个老内核与默认基址间的一个差值
ULONG uOffset = (ULONG)OldBase - pNt->OptionalHeader.ImageBase;
//3 开始修复重定位
while (pReloc->SizeOfBlock != 0)
{
ULONG uCount = (pReloc->SizeOfBlock - 8) / 2;//本0x1000内,有多少需要重定位的地方
ULONG uBaseRva = pReloc->VirtualAddress; //本0x1000的起始位置
PTYPE pType = (PTYPE)(pReloc + 1);
for (int i = 0; i < uCount; i++)
{
if (pType->Type == 3)
{
PULONG pRelocPoint = (uBaseRva + pType->Offset + ZkBuf);
//重定位后的地址 - 新基址 = 没重定位的地址 - 默认基址
//所以:重定位后的地址 = 新基址 - 默认基址 + 没重定位的地址
*pRelocPoint = uOffset + *pRelocPoint;
}
pType++;
}
pReloc = (PIMAGE_BASE_RELOCATION)((ULONG)pReloc + pReloc->SizeOfBlock);
}
}
// 修复旧SSDT表
void FixSSDT(PUCHAR pZKBuf, PUCHAR OldBase)
{
//新内核某位置1 - 新内核基址 = 老内核某位置1 - 老内核基址;
//新内核某位置1 = 新内核基址 - 老内核基址 + 老内核某位置1;
LONG Offset = (ULONG)pZKBuf - (ULONG)OldBase;
//1 得到新内核中的SSDT
g_pNewSSDT = (PSSDTEntry)((LONG)&KeServiceDescriptorTable + Offset);
//2 填充系统服务个数
g_pNewSSDT->NumberOfServices = KeServiceDescriptorTable.NumberOfServices;
//3 填充SSDT表
g_pNewSSDT->ServiceTableBase = (ULONG *)((PUCHAR)KeServiceDescriptorTable.ServiceTableBase + Offset);
//让所有的SSDT中保存的函数地址,都指向新内核
for (int i = 0; i < g_pNewSSDT->NumberOfServices; i++)
{
g_pNewSSDT->ServiceTableBase[i] = g_pNewSSDT->ServiceTableBase[i] + Offset;
}
//4 填充参数表
g_pNewSSDT->ParamTableBase = (PULONG)((PUCHAR)KeServiceDescriptorTable.ParamTableBase + Offset);
memcpy(g_pNewSSDT->ParamTableBase,
KeServiceDescriptorTable.ParamTableBase,
g_pNewSSDT->NumberOfServices
);
}
// 获取KiFastCallEntry函数
ULONG GetKiFastCallEntry()
{
ULONG uAddress = 0;
_asm
{
push eax;
push ecx;
mov ecx, 0x176;
rdmsr;
mov uAddress, eax;
pop ecx;
pop eax;
}
return uAddress;
}
// hook中过滤函数
ULONG FilterFun(ULONG SSdtBase, PULONG OldFun, ULONG Id)
{
//如果相等,说明调用的是SSDT中的函数
if (SSdtBase == (ULONG)KeServiceDescriptorTable.ServiceTableBase)
{
//使用思路:
//假如进程是OD,并且函数调用是190号,就走新内核中的函数,这样通过hookOpenProcess就无法拦住OD了。
return g_pNewSSDT->ServiceTableBase[Id];
}
return OldFun;
}
// inline Hook的回调函数
_declspec(naked)void MyHookFun()
{
//eax 里面是调用号,edx里面是老函数地址,edi里面是SSDT基址
_asm {
pushad;
pushfd;
push eax; //调用号
push edx; //原始函数地址
push edi; //SSDT基址
call FilterFun; // 自己的过滤函数,获取最真实函数地址 由于前面压入3个参数和pushfd,pushad ebx刚好在栈中的【esp+0x18]
mov dword ptr ds : [esp + 0x18], eax; // 【esp+0x18】 ebx的值,替换服务函数地址
popfd;
popad; // 恢复通用寄存器,ebx被替换成新的函数地址了
// 执行原始hook的5个字节
sub esp, ecx;
shr ecx, 2;
jmp g_JmpPoint; // 跳转回原来函数地址
}
}
// hook 目标的5个字节
UCHAR Old_Code[5] = { 0 };
// hookKiFastCallEntry函数
void OnHookKiFastCallEntry()
{
// KiFastCallEntry中特征值
char buf[] = { 0x2b, 0xe1, 0xc1, 0xe9, 0x02 };
// 获取KiFastCallEntry函数地址
ULONG KiFastCallEntryAdd = GetKiFastCallEntry();
// 找到hook点
pHookPoint = SearchMemory(buf, 5, (char*)KiFastCallEntryAdd, 0x200);
// 绕过前5个字节,应为被hook替换了
g_JmpPoint = (ULONG)(pHookPoint + 5);
// 备份旧的5个字节
memcpy(Old_Code, pHookPoint, 5);
// 关闭页保护
OffProtected();
// jmp xxxxxx
// 写入跳转目标地址 目标地址-指令所在-5
pHookPoint[0] = 0xE9;
*(ULONG*)(&pHookPoint[1]) = (ULONG)MyHookFun - (ULONG)pHookPoint - 5;
// 开启页保护
OnProtected();
}
//ntoskrnl - 单处理器,不支持PAE
//ntkrnlpa - 单处理器,支持PAE
//ntkrnlmp - 多处理器,不支持PAE
//ntkrpamp - 多处理器,支持PAE
// 在windows vista 开始后 所有的内核默认安装多核处理器方式(ntkrnlmp 或者 ntkrpamp)
// 然后拷贝到System32目录下,更改和单核处理名称一样
// 内核重载 开始
void KernelReload()
{
PUCHAR pBuf = NULL;
PUCHAR pZKBuf = NULL;
UNICODE_STRING KernelName;
//1 首先把内核文件读取到内存里 (默认开启PAE)
ReadKernelToBuf(L"\\??\\C:\\Windows\\System32\\ntkrnlpa.exe", &pBuf);
//2 把读到内存中的内核给展开成0x1000对齐
ZKKernel(&pZKBuf, pBuf);
ExFreePool(pBuf);
//3 修复新内核的重定位 ,虽然开启PAE(ntkrnlpa.exe),但是显示的名称 ntoskrnl.exe
RtlInitUnicodeString(&KernelName, L"ntoskrnl.exe");
ULONG32 uBase = MyGetModuleHandle(&KernelName);
FixReloc(pZKBuf, (PUCHAR)uBase);
//4 修复新的SSDT表
FixSSDT(pZKBuf, (PUCHAR)uBase);
//5 Hook掉KiFastCallEntry,在自己的Hook函数中判断应该走新内核还是老内核
OnHookKiFastCallEntry();
}
// 卸载内核钩子
void UnHook()
{
OffProtected();
memcpy(pHookPoint, Old_Code, 5);
OnProtected();
}
// 驱动卸载
VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
pDriver;
UnHook();
KdPrint(("Leave"));
}
// 驱动加载
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pPath)
{
UNREFERENCED_PARAMETER(pPath);
DbgBreakPoint();
g_pDriver = pDriver;
// 重载内核
KernelReload();
pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
标签:Status,IRP,函数,SUCCESS,对象,编程,Irp,内核 来源: https://blog.csdn.net/datouyu0824/article/details/115046842