驱动篇——常规的0环与3环通信
写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
看此教程之前,问个问题,你明确学驱动的目的了吗?你的开发环境准备好了吗?上一节的内容学会了吗? 没有的话就不要继续了,请重新学习前面驱动篇的教程内容继续。
🔒 华丽的分割线 🔒
练习及参考
本次答案均为参考,可以与我的答案不一致,但必须成功通过。
1️⃣ 遍历内核模块,输出模块名称,基址以及大小。
🔒 点击查看答案 🔒
此题目不难,就是一个循环双向链表的遍历,代码见下面的折叠,效果如下:
🔒 点击查看代码 🔒
#include <ntddk.h>
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
DbgPrint("卸载成功!!!");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;
DbgPrint("\n=====驱动遍历 By 寂静的羽夏 cnblog=====\n");
LIST_ENTRY* lethis = (LIST_ENTRY*)DriverObject->DriverSection;
LIST_ENTRY* item = lethis;
DRIVER_OBJECT obj;
while (1)
{
PUNICODE_STRING name = (PUNICODE_STRING)(((UINT32)item) + 0x2c);
UINT32 DllBase = *(UINT32*)(((UINT32)item) + 0x18);
UINT32 ImgSize= *(UINT32*)(((UINT32)item) + 0x20);
DbgPrint("DriverName : %wZ\nDllBase : %x\nImgSize : %x\n======\n", name, DllBase, ImgSize);
item = item->Blink;
if (item == lethis)
{
break;
}
}
return STATUS_SUCCESS;
}
2️⃣ 编写一个函数,找到一个未导出的函数,并调用。(例子:找到PspTerminateProcess
,通过调用这个函数结束记事本进程)
🔒 点击查看答案 🔒
根据PE
的知识,我们可以通过基址+偏移的方式定位该函数,这个是最简洁的方式。当然可以通过特征码的方式,不过效率低,特征码找不好还不准确。
我们先在WinDbg
找找这个函数在哪里:
kd> x nt!_PspTerminateProcess
805c9da4 nt!PspTerminateProcess (_PspTerminateProcess@8)
这个函数是在内核文件导出,分页不同,导出的函数偏移可能不同,下面是在2-9-9-12
分页模式下做的实验,如果在10-10-12
分页可能函数的位置不同:
我们只需要获取函数偏移,获取基地址,加起来即是函数地址,然后调用即可,代码见折叠,必要位置具有注释。
好了,我们尝试一下能不能终止进程,先在WinDbg
找到EPROCESS
结构体的地址:
Failed to get VadRoot
PROCESS 89cb7918 SessionId: 0 Cid: 0454 Peb: 7ffdf000 ParentCid: 05fc
DirBase: 12d002e0 ObjectTable: e1072a18 HandleCount: 44.
Image: notepad.exe
89cb7918
就是我们需要的地址,修改调用PspTerminateProcess
的第一个参数的数值,然后编译。在虚拟机进行注册启动驱动效果如下:
由于这个函数很底层,可以干掉很多流氓软件,甚至杀软都不放过。比如火绒(已将该情况上报给火绒官方,乱搞后果自负):
🔒 点击查看代码 🔒
#include <ntddk.h>
typedef NTSTATUS (__stdcall *PspTerminateProcess)(INT32,INT32);
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
DbgPrint("卸载成功!!!");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;
UNICODE_STRING ntkrnl;
RtlInitUnicodeString(&ntkrnl, L"ntoskrnl.exe"); //有意思的是即使是 2-9-9-12 分页,还是这个名字
LIST_ENTRY* lethis = (LIST_ENTRY*)DriverObject->DriverSection;
LIST_ENTRY* item = lethis;
DRIVER_OBJECT obj;
UINT32 DllBase = 0;
while (1)
{
PUNICODE_STRING name = (PUNICODE_STRING)(((UINT32)item) + 0x2c);
if (!RtlCompareUnicodeString(name,&ntkrnl,FALSE))
{
DllBase = *(UINT32*)(((UINT32)item) + 0x18);
break;
}
item = item->Blink;
if (item == lethis)
{
break;
}
}
if (DllBase)
{
PspTerminateProcess pspTerminateProcess = (PspTerminateProcess)(DllBase + 0xF1DA4); //0xF1DA4 就是偏移
pspTerminateProcess(0x89b56c98, 0); //第一个参数根据自己的填
}
return STATUS_SUCCESS;
}
3️⃣ 通过断链实现隐藏驱动模块。
🔒 点击查看答案 🔒
此题目不难,就是一个链表断链,效果如下:
PCHunter
这个ARK
工具仍能发现我们的模块,指明为隐藏驱动。但是你用普通的API
试试,你绝对发现不了它。
🔒 点击查看代码 🔒
#include <ntddk.h>
LIST_ENTRY* lethis;
LIST_ENTRY* fle;
LIST_ENTRY* ble;
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
fle->Blink = lethis;
ble->Flink = lethis;
DbgPrint("卸载成功!!!");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;
lethis = (LIST_ENTRY*)DriverObject->DriverSection;
fle = lethis->Flink;
ble = lethis->Blink;
fle->Blink = lethis->Blink;
ble->Flink = lethis->Flink;
DbgPrint("加载并隐藏成功!!!");
return STATUS_SUCCESS;
}
设备对象
我们在开发窗口程序的时候,消息被封装成一个结构体:MSG
。在内核开发时,消息被封装成另外一个结构体:IRP
,英文全称:I/O Request Package
。在窗口程序中,能够接收消息的只能是窗口对象。在内核中,能够接收IRP
消息的只能是设备对象。示意图如下所示:
常规通信流程
为了实现3环程序与驱动程序正常的通信功能,微软提供了一系列的API
。我们可以通过它来实现常规的通信。我们的硬盘、键盘、显卡想要工作,在Windows
平台都需要用此实现通信,来实现想要的功能。下面我来介绍具体流程。
创建设备对象
如果MSG
需要传递,就必须创建一个窗体,因为只有窗体才有消息队列这个东西,才嗯那个接收消息。如果想要驱动实现通信,就必须有一个设备对象。我们可以用下面的代码实现创建设备:
//创建设备名称
UNICODE_STRING Devicename;
RtlInitUnicodeString(&Devicename,L"\\Device\\MyDevice");
//创建设备
IoCreateDevice(
pDriver, //当前设备所属的驱动对象
0,
&Devicename, //设备对象的名称
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&pDeviceObj //设备对象指针
);
设置交互数据的方式
既然设备对象创建好了,我们需要规定一个“协议”,就是3环程序与驱动交互的协议。具体有如下几个方式:
缓冲区方式读写(DO_BUFFERED_IO
) :操作系统将应用程序提供缓冲区的数据复制到内核模式下的地址中。
直接方式读写(DO_DIRECT_IO
) :操作系统会将用户模式下的缓冲区锁住。然后操作系统将这段缓冲区在内核模式地址再次映射一遍。这样,用户模式的缓冲区和内核模式的缓冲区指向的是同一区域的物理内存。缺点就是要单独占用物理页面。
其他方式读写(在调用IoCreateDevice
创建设备后对pDevObj->Flags
即不设置DO_BUFFERED_IO
也不设置DO_DIRECT_IO
此时就是其他方式。在使用其他方式读写设备时,派遣函数直接读写应用程序提供的缓冲区地址。在驱动程序中,直接操作应用程序的缓冲区地址是很危险的。只有驱动程序与应用程序运行在相同线程上下文的情况下,才能使用这种方式。如果CPU
中的任务切换了,即CR3
切换掉了,在高2GB
的驱动仍在使用该方式读取低2GB
内存,导致读到的数据和实际不符,导致错误,故强烈不推荐此方式。
用代码设置交互数据的方式举例如下:
pDeviceObj->Flags |= DO_BUFFERED_IO;
创建符号链接
设备对象创建好了,通信方式也约定好了,但3环的程序仍找不到你的驱动对象。设备名称的作用是给内核对象用的,如果要在3环访问,必须要有符号链接。其实就是一个别名,没有这个别名,在3环不可见。用代码实现如下:
//创建符号链接名称
RtlInitUnicodeString(&SymbolicLinkName,L"\\??\\MyTestDriver");
//创建符号链接
IoCreateSymbolicLink(&SymbolicLinkName,&Devicename);
有些细节需要特别注意:内核模式下,符号链接是以\??\
开头的,如C盘就是\??\C:
。而在用户模式下,则是以\\.\
开头的,如C盘就是\\.\C:
。
IRP
前面的代码都写好的,驱动与3环的通信的基础就搭建好了。但是,如果真正实现通信,还得需要注册派遣函数。
如上图所示,我们在编写Win32
窗体程序时。假设我在窗体点击了鼠标,操作系统就会产生一个消息,用MSG
这个结构体封装一下,派发给窗体对象。目标窗体对象接受到后发现它是鼠标单击消息。窗体对象中注册了很多回调函数:鼠标点击回调、鼠标双击回调、键盘键按下回调等等。然后进一步处理是单击,就调用单击回调函数。同理,我们在3环调用CreateFile
函数,操作系统就会产生一个IRP
派发给设备对象,目标设备对象处理方式和窗体消息没啥差别。接下来我们看看IRP
的类型:
当应用层通过CreateFile
、ReadFile
、WriteFile
、CloseHandle
等函数打开、从设备读取数据、向设备写入数据、关闭设备的时候,会使操作系统分别产生出IRP_MJ_CREATE
、IRP_MJ_READ
、IRP_MJ_WRITE
、IRP_MJ_CLOSE
等不同的IRP
。值得注意的是,我们之前使用CreateFile
这个东西只是为了创建文件,其实它的本质是与设备对象创建访问,我们3环程序想要通过符号链接与驱动建立通讯,就必须通过这个函数。
当然IRP
不止上面的这几种,我们再给出常见的IRP
:
IRP类型 | 来源 |
---|---|
IRP_MJ_DEVICE_CONTROL | 使用 DeviceControl 函数时产生 |
IRP_MJ_POWER | 在操作系统处理电源消息时产生 |
IRP_MJ_SHUTDOWN | 关闭系统前时产生 |
我们最常用的IRP
有IRP_MJ_DEVICE_CONTROL
、IRP_MJ_CREATE
和IRP_MJ_CLOSE
,以实现交互、创建访问、关闭访问的功能。
派遣函数
了解了上面的东西,我们如何注册派遣函数呢?我们再看一下DRIVER_OBJECT
这个东西:
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT;
有没有注意到MajorFunction
这个成员,它是一个数组,具有28个,我们的派遣函数都会在这里面,如何注册我们用如下代码形式:
//设置卸载函数
pDriverObject->DriverUnload = 卸载函数;
//设置派遣函数
pDriverObject->MajorFunction[IRP_MJ_CREATE] = 派遣函数1;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = 派遣函数2;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = 派遣函数3;
pDriverObject->MajorFunction[IRP_MJ_READ] = 派遣函数4;
pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = 派遣函数5;
pDriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] = 派遣函数6;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = 派遣函数7;
pDriverObject->MajorFunction[IRP_MJ_SHUTDOWN] = 派遣函数8;
pDriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = 派遣函数9;
派遣函数的格式
回调函数都有自己的格式,派遣函数也不例外,它的格式如下:
NTSTATUS MyDispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
//处理自己的业务……
//设置返回状态
pIrp->IoStatus.Status = STATUS_SUCCESS; //GetLastError 函数得到的就是该值
pIrp->IoStatus.Information = 0; //返回给3环多少数据 没有填0
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
本节练习
本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。
俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习不多,请保质保量的完成。
1️⃣ 实现一个工具,利用未导出的函数PspTerminateProcess
杀死软件(驱动的加载可不用代码实现,使用本教程工具进行加载)。
下一篇
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15516178.html