admin 管理员组文章数量: 1086866
内核编程
内核编程
一. 驱动程序概述
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 版本时被调用DbgPrintKdPrint(("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建立快速通讯
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 进行交互,需要依赖设备对象传递 IRPSHORT Type; //0x0 // 表示结构体的类型,所以的驱动对象都为 4SHORT 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; //0x0struct _LIST_ENTRY InMemoryOrderLinks; //0x8struct _LIST_ENTRY InInitializationOrderLinks; //0x10VOID* DllBase; //0x18VOID* EntryPoint; //0x1cULONG SizeOfImage; //0x20struct _UNICODE_STRING FullDllName; //0x24struct _UNICODE_STRING BaseDllName; //0x2c// ... 后面还有一些字段,由于用不到,为了节省代码量,直接不考虑
} *PLDR_DATA_TABLE_ENTRY;/* _LDR_DATA_TABLE_ENTRY结构体
//0x78 bytes (sizeof)
struct _LDR_DATA_TABLE_ENTRY
{struct _LIST_ENTRY InLoadOrderLinks; 双向链表 //0x0struct _LIST_ENTRY InMemoryOrderLinks; //0x8struct _LIST_ENTRY InInitializationOrderLinks; //0x10VOID* DllBase; //0x18VOID* EntryPoint; //0x1cULONG SizeOfImage; //0x20struct _UNICODE_STRING FullDllName; //0x24struct _UNICODE_STRING BaseDllName; //0x2cULONG Flags; //0x34USHORT LoadCount; //0x38USHORT TlsIndex; //0x3aunion{struct _LIST_ENTRY HashLinks; //0x3cstruct{VOID* SectionPointer; //0x3cULONG CheckSum; //0x40};};union{ULONG TimeDateStamp; //0x44VOID* LoadedImports; //0x44};struct _ACTIVATION_CONTEXT* EntryPointActivationContext; //0x48VOID* PatchInformation; //0x4cstruct _LIST_ENTRY ForwarderLinks; //0x50struct _LIST_ENTRY ServiceTagLinks; //0x58struct _LIST_ENTRY StaticLinks; //0x60VOID* ContextInformation; //0x68ULONG OriginalBase; //0x6cunion _LARGE_INTEGER LoadTime; //0x70
};
*///0x8 bytes (sizeof)
struct _LIST_ENTRY
{struct _LIST_ENTRY* Flink; //0x0struct _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_SUCCESSStatus = IoCreateDevice(DriverObject, // 所属的驱动对象,设备对象创建后会添加到其 DeviceObjict 成员中0, // 设备的扩展空间大小,分配的空间会由 DeviceExtension 字段指向&DeviceName, // 设备对象的名称,必须符合格式 \\Device\\DeviceNameFILE_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 或 \\??\\xxxUNICODE_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 的 GetLastErrorIrp->IoStatus.Status = STATUS_SUCCESS;// 设置当前的消息处理了多少个字节的数据,影响 ReadFile 中返回的实际读写字节数Irp->IoStatus.Information = 0;// 通知 IO 管理器当前的 IRP 已经处理成功,需要返回给 R3IoCompleteRequest(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; // CreateFileDriverObject->MajorFunction[IRP_MJ_CLOSE] = CloseDispath; // CloseFileDriverObject->MajorFunction[IRP_MJ_READ] = ReadDispath; // ReadFileDriverObject->MajorFunction[IRP_MJ_WRITE] = WriteDispath; // WriteFile// 创建一个设备对象,如果失败则返回错误码NTSTATUS Status = CreateDevice(DriverObject);// 如果返回的不是 STATUS_SUCESS,驱动会安装失败,意味着 DriverUnload 不会调用return Status;
}//0xb8 bytes (sizeof)
struct _DEVICE_OBJECT
{SHORT Type; //0x0USHORT Size; //0x2LONG ReferenceCount; //0x4struct _DRIVER_OBJECT* DriverObject; //0x8struct _DEVICE_OBJECT* NextDevice; //0xcstruct _DEVICE_OBJECT* AttachedDevice; //0x10struct _IRP* CurrentIrp; //0x14struct _IO_TIMER* Timer; //0x18ULONG Flags; //0x1cULONG Characteristics; //0x20struct _VPB* Vpb; //0x24VOID* DeviceExtension; //0x28ULONG DeviceType; //0x2cCHAR StackSize; //0x30union{struct _LIST_ENTRY ListEntry; //0x34struct _WAIT_CONTEXT_BLOCK Wcb; //0x34} Queue; //0x34ULONG AlignmentRequirement; //0x5cstruct _KDEVICE_QUEUE DeviceQueue; //0x60struct _KDPC Dpc; //0x74ULONG ActiveThreadCount; //0x94VOID* SecurityDescriptor; //0x98struct _KEVENT DeviceLock; //0x9cUSHORT SectorSize; //0xacUSHORT Spare1; //0xaestruct _DEVOBJ_EXTENSION* DeviceObjectExtension; //0xb0VOID* 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);// 在进行一系列操作后,需要关闭设备对象句柄,使用 CloseHandleCloseHandle(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_SUCCESSStatus = IoCreateDevice(DriverObject, // 所属的驱动对象,设备对象创建后会添加到其 DeviceObjict 成员中0, // 设备的扩展空间大小,分配的空间会由 DeviceExtension 字段指向&DeviceName, // 设备对象的名称,必须符合格式 \\Device\\DeviceNameFILE_DEVICE_UNKNOWN, // 设备对象的类型,特指硬件无关的虚拟设备对象0, // 设备对象的属性FALSE, // 是否启用独占模式,同一时刻能被打开几次&DeviceObject); // 创建出的设备对象,由哪一个指针指向// 通过 NT_SUCCESS 判断函数的调用是否成功if (!NT_SUCCESS(Status)){KdPrint(("设备对象创建失败,检查原因,错误码(%08X)\n", Status));return Status;}// 设备对象的名称只能在内核中被直接的解析,为了 R3 能够识别并操作设备对象,需要// 创建与设备名称直接关联的符号链接名,必须写作: \\DosDevices\\xxx 或 \\??\\xxxUNICODE_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 的 GetLastErrorIrp->IoStatus.Status = STATUS_SUCCESS;// 设置当前的消息处理了多少个字节的数据,影响 ReadFile 中返回的实际读写字节数Irp->IoStatus.Information = 0;// 通知 IO 管理器当前的 IRP 已经处理成功,需要返回给 R3IoCompleteRequest(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; // ReadFileDriverObject->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_SUCCESSStatus = IoCreateDevice(DriverObject, // 所属的驱动对象,设备对象创建后会添加到其 DeviceObjict 成员中0, // 设备的扩展空间大小,分配的空间会由 DeviceExtension 字段指向&DeviceName, // 设备对象的名称,必须符合格式 \\Device\\DeviceNameFILE_DEVICE_UNKNOWN, // 设备对象的类型,特指硬件无关的虚拟设备对象0, // 设备对象的属性FALSE, // 是否启用独占模式,同一时刻能被打开几次&DeviceObject); // 创建出的设备对象,由哪一个指针指向// 通过 NT_SUCCESS 判断函数的调用是否成功if (!NT_SUCCESS(Status)){KdPrint(("设备对象创建失败,检查原因,错误码(%08X)\n", Status));return Status;}// 设备对象的名称只能在内核中被直接的解析,为了 R3 能够识别并操作设备对象,需要// 创建与设备名称直接关联的符号链接名,必须写作: \\DosDevices\\xxx 或 \\??\\xxxUNICODE_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 的 GetLastErrorIrp->IoStatus.Status = STATUS_SUCCESS;// 设置当前的消息处理了多少个字节的数据,影响 ReadFile 中返回的实际读写字节数Irp->IoStatus.Information = 0;// 通知 IO 管理器当前的 IRP 已经处理成功,需要返回给 R3IoCompleteRequest(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; // ReadFileDriverObject->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_SUCCESSStatus = IoCreateDevice(DriverObject, // 所属的驱动对象,设备对象创建后会添加到其 DeviceObjict 成员中0, // 设备的扩展空间大小,分配的空间会由 DeviceExtension 字段指向&DeviceName, // 设备对象的名称,必须符合格式 \\Device\\DeviceNameFILE_DEVICE_UNKNOWN, // 设备对象的类型,特指硬件无关的虚拟设备对象0, // 设备对象的属性FALSE, // 是否启用独占模式,同一时刻能被打开几次&DeviceObject); // 创建出的设备对象,由哪一个指针指向// 通过 NT_SUCCESS 判断函数的调用是否成功if (!NT_SUCCESS(Status)){KdPrint(("设备对象创建失败,检查原因,错误码(%08X)\n", Status));return Status;}// 设备对象的名称只能在内核中被直接的解析,为了 R3 能够识别并操作设备对象,需要// 创建与设备名称直接关联的符号链接名,必须写作: \\DosDevices\\xxx 或 \\??\\xxxUNICODE_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 的 GetLastErrorIrp->IoStatus.Status = STATUS_SUCCESS;// 设置当前的消息处理了多少个字节的数据,影响 ReadFile 中返回的实际读写字节数Irp->IoStatus.Information = 0;// 通知 IO 管理器当前的 IRP 已经处理成功,需要返回给 R3IoCompleteRequest(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; // ReadFileDriverObject->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_SUCCESSStatus = 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 或 \\??\\xxxUNICODE_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);// 设置消息的处理状态: 成功或失败 -> GetLastErrorIrp->IoStatus.Status = STATUS_SUCCESS;// 设置消息处理的内容长度: ReadFile 或者 WriteFIle 的实际操作长度Irp->IoStatus.Information = 0;// 通知操作已经完成,完成后不提高当前的 IRQLIoCompleteRequest(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;}// 设置消息的处理状态: 成功或失败 -> GetLastErrorIrp->IoStatus.Status = STATUS_SUCCESS;// 设置消息处理的内容长度: ReadFile 或者 WriteFIle 的实际操作长度Irp->IoStatus.Information = 0;// 通知操作已经完成,完成后不提高当前的 IRQLIoCompleteRequest(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.SystemBufferMETHOD_OUT_DIRECT irp->AssociatedIrp.SystemBufferMETHOD_BUFFERED irp->AssociatedIrp.SystemBufferMETHOD_NEITHER irpStack->Parameters.DeviceIoControl.Type3InputBuffer// 输入位置METHOD_IN_DIRECT irp->MdlAddressMETHOD_OUT_DIRECT irp->MdlAddressMETHOD_BUFFERED irp->AssociatedIrp.SystemBufferMETHOD_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_SUCCESSStatus = 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 或 \\??\\xxxUNICODE_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);// 设置消息的处理状态: 成功或失败 -> GetLastErrorIrp->IoStatus.Status = STATUS_SUCCESS;// 设置消息处理的内容长度: ReadFile 或者 WriteFIle 的实际操作长度Irp->IoStatus.Information = 0;// 通知操作已经完成,完成后不提高当前的 IRQLIoCompleteRequest(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->R0Irp->MdlAddress; // R0->R3break;case DirectWay:Irp->AssociatedIrp.SystemBuffer // R0->R3Irp->MdlAddress; // R3->R0break;case NeitherWay:Irp->UserBuffer; // R0->R3Stack->Parameters.DeviceIoControl.Type3InputBuffer; // R3->R0break;*/default:break;}// 设置消息的处理状态: 成功或失败 -> GetLastErrorIrp->IoStatus.Status = STATUS_SUCCESS;// 设置消息处理的内容长度: ReadFile 或者 WriteFIle 的实际操作长度Irp->IoStatus.Information = 0;// 通知操作已经完成,完成后不提高当前的 IRQLIoCompleteRequest(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; //0x0USHORT Size; //0x2struct _MDL* MdlAddress; //0x4ULONG Flags; //0x8union{struct _IRP* MasterIrp; //0xcLONG IrpCount; //0xcVOID* SystemBuffer; //0xc} AssociatedIrp; //0xcstruct _LIST_ENTRY ThreadListEntry; //0x10struct _IO_STATUS_BLOCK IoStatus; //0x18CHAR RequestorMode; //0x20UCHAR PendingReturned; //0x21CHAR StackCount; //0x22CHAR CurrentLocation; //0x23UCHAR Cancel; //0x24UCHAR CancelIrql; //0x25CHAR ApcEnvironment; //0x26UCHAR AllocationFlags; //0x27struct _IO_STATUS_BLOCK* UserIosb; //0x28struct _KEVENT* UserEvent; //0x2cunion{struct{union{VOID (*UserApcRoutine)(VOID* arg1, struct _IO_STATUS_BLOCK* arg2, ULONG arg3); //0x30VOID* IssuingProcess; //0x30};VOID* UserApcContext; //0x34} AsynchronousParameters; //0x30union _LARGE_INTEGER AllocationSize; //0x30} Overlay; //0x30VOID (*CancelRoutine)(struct _DEVICE_OBJECT* arg1, struct _IRP* arg2); //0x38VOID* UserBuffer; //0x3cunion{struct{union{struct _KDEVICE_QUEUE_ENTRY DeviceQueueEntry; //0x40VOID* DriverContext[4]; //0x40};struct _ETHREAD* Thread; //0x50CHAR* AuxiliaryBuffer; //0x54struct _LIST_ENTRY ListEntry; //0x58union{struct _IO_STACK_LOCATION* CurrentStackLocation; //0x60ULONG PacketType; //0x60};struct _FILE_OBJECT* OriginalFileObject; //0x64} Overlay; //0x40struct _KAPC Apc; //0x40VOID* 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 的 GetLastErrorIrp->IoStatus.Status = STATUS_SUCCESS;// 设置当前的消息处理了多少个字节的数据,影响 ReadFile 中返回的实际读写字节数Irp->IoStatus.Information = 0;// 通知 IO 管理器当前的 IRP 已经处理成功,需要返回给 R3IoCompleteRequest(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;elsereturn (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); // 需要查询的是哪种类型的信息// 如果成功返回查询的结果,否则返回 0if (NT_SUCCESS(Status))return FileInfo.EndOfFile.QuadPart;elsereturn 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 了,那么就需要 -1ObDereferenceObject(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_IFCreateOptions, // 可以通过这个字段设置操作的是文件还是目录0, 0); // 描述扩展区域的大小以及指向它的指针// 判断文件的操作是否成功,返回相应的值if (NT_SUCCESS(Status))return FileHandle;elsereturn (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 30hpop fs ; 将 fs 段选择子切换为 0x30,此时指向 KPCRmov ds,cxmov es,cxmov ecx,dword ptr fs:[40h] ; 获取 TSS 中的 R0 ESP 并进行设置 mov esp,dword ptr [ecx+4]; 构建 _KTRAP_FRAME 保存 R3 的寄存器状态push 23hpush edx pushfd push 2add edx,8popfdor byte ptr [esp+1],2push 1Bhpush dword ptr ds:[0FFDF0304h]push 0push ebppush ebxpush esipush edimov ebx,dword ptr fs:[1Ch] ; 指向 KPCR(自己) 的指针push 3Bhmov esi,dword ptr [ebx+124h] ; 保存 CurrentThread,即当前线程的 KTHREADpush dword ptr [ebx]mov dword ptr [ebx],0FFFFFFFFhmov ebp,dword ptr [esi+28h] ; ebp 指向 KTRAP_FRAME 结构体首地址push 1sub esp,48hsub ebp,29Chmov byte ptr [esi+13Ah],1 ; 设置 CurrentThread.PreviousMode 为 1cmp ebp,espjne nt!KiFastCallEntry2+0x49 (83e760bb)and dword ptr [ebp+2Ch],0test byte ptr [esi+3],0DFhmov dword ptr [esi+128h],ebpjne 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],0BADB0D00hmov dword ptr [ebp],ebxmov 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* ServiceTablemov ebx,eaxand 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,esior esi,dword ptr [ecx+0F70h]je nt!KiFastCallEntry+0xce (83e7618e)push edxpush eaxcall dword ptr [nt!KeGdiFlushUserBatch (83fa294c)]pop eaxpop edx; 如果当前是 SSDT 函数调用执行的操作nt!KiFastCallEntry+0xce:inc dword ptr fs:[6B0h] ; 产生系统调用的次数 KPCR->KeSystemCalls 加一mov esi,edx ; esi 指向用户传递的参数在栈中的位置xor ecx,ecxmov 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],ebxmov dword ptr [ecx+12Ch],edint!KiFastCallEntry+0x115:mov ebx,edx ; 获取真实的函数地址test byte ptr [nt!PerfGlobalGroupMask+0x8 (83f6f908)],40hsetne byte ptr [ebp+12h]jne nt!KiServiceExit2+0x17b (83e76574)call ebx ; 调用对应的 SSDT 函数
2. KPCR
//0x3748 bytes (sizeof)
struct _KPCR
{union{struct _NT_TIB NtTib; //0x0struct{struct _EXCEPTION_REGISTRATION_RECORD* Used_ExceptionList; //0x0VOID* Used_StackBase; //0x4VOID* Spare2; //0x8VOID* TssCopy; //0xcULONG ContextSwitches; //0x10ULONG SetMemberCopy; //0x14VOID* Used_Self; //0x18};};struct _KPCR* SelfPcr; //0x1cstruct _KPRCB* Prcb; //0x20UCHAR Irql; //0x24ULONG IRR; //0x28ULONG IrrActive; //0x2cULONG IDR; //0x30VOID* KdVersionBlock; //0x34struct _KIDTENTRY* IDT; //0x38struct _KGDTENTRY* GDT; //0x3cstruct _KTSS* TSS; //0x40USHORT MajorVersion; //0x44USHORT MinorVersion; //0x46ULONG SetMember; //0x48ULONG StallScaleFactor; //0x4cUCHAR SpareUnused; //0x50UCHAR Number; //0x51UCHAR Spare0; //0x52UCHAR SecondLevelCacheAssociativity; //0x53ULONG VdmAlert; //0x54ULONG KernelReserved[14]; //0x58ULONG SecondLevelCacheSize; //0x90ULONG HalReserved[16]; //0x94ULONG InterruptMode; //0xd4UCHAR Spare1; //0xd8ULONG KernelReserved2[17]; //0xdcstruct _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/*/*/_asm{pushad; // 保存寄存器cmp eax, 0x0BE; // 是否是ZwOpenProcess 函数jne CallEnd; // 结束过滤mov eax, [edx + 0x14]; // 获取第四个参数 ClientIdmov eax, [eax]; // ClientId->ProcessIdcmp 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寄存器的内容保存eaxmov 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); // 一般为NULLif (!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 得到新内核中的SSDTg_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// 写入跳转目标地址 目标地址-指令所在-5pHookPoint[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.exeRtlInitUnicodeString(&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;
}
本文标签: 内核编程
版权声明:本文标题:内核编程 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1693760960a241263.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论