avatar

Catalog
3环与0环通信(常规方式)

本篇介绍一下3环与0环通信的原理(常规方式),介绍与之相关的结构体,对象等,最后代码实现并模拟操作系统进行3环和0环的通信。

设备对象

内核通信的对象

内核中的通信,与应用层窗口间的通信类似,只是封装消息的结构体不同,接收消息结构体的对象不同

  • 窗口通信

    1)消息结构体:MSG

    2)接收消息的对象:窗口对象(Hwnd)

  • 内核通信

    1)消息结构体:IRP(I/O Request Package)

    2)接收消息的对象:设备对象(DeviceObject)

所以想要在内核通信需要有(至少)一个用来接收和发送消息的设备对象

创建设备对象

窗口对象的创建也与设备对象也有不少共同点,这里继续拿来类比

  • 窗口对象
    1)可以创建多个

    2)需要指定父窗口

  • 设备对象
    1)可以创建多个

    2)需要指定所属驱动对象

Windows提供了内核函数IoCreateDevice用来创建设备对象,参考如下代码:

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define DEVICE_NAME L"\\Device\\MyDevice"	

PDEVICE_OBJECT pDeviceObj = NULL;
UNICODE_STRING DeviceName;
RtlInitUnicodeString(&DeviceName, DEVICE_NAME);
NTSTATUS status = IoCreateDevice(pDriverObj,
0,
&DeviceName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&pDeviceObj);

if (status != STATUS_SUCCESS) {
DbgPrint("Device Create Failed!\n");
return status;
}
  • pDriverObj:指定该设备创建后属于哪个驱动对象(PDRIVER_OBJECT)
  • &DeviceName:定义了一个DEVICE_NAME的宏,被用来初始化设备对象的名字。这个名字不能随便改(即”\\Device\\xxxxx”形式),在设备创建时,会根据设备名,将该设备挂到一个名为Device的树形结构中,几乎所有设备都挂在这。若改变此值,则会挂到其它树中。
  • FILE_DEVICE_UNKNOWN:该处填写设备的类型,由于我们并没有实际的设备,所以选择UNKNOWN
  • &pDeviceObj:这个参数可以看作是一个二级指针,它指向一个地址。这个地址存着一个指针pDeviceObj,这个指针指向一个设备结构体。另一个要说明的是,这个参数是一个OUT类型的参数,原本pDeviceObj指向的内容是空的,在执行完设备创建的函数后,其指向创建出的设备对象

以上为几个比较关键的参数介绍,其余参数按照上述代码填写即可,具体含义可以参考官方文档

数据传输方式

首先查看一下设备对象这个结构体,发现它有很多字段,这里我们只需要关注其中一个,就是Flags,这是一个四字节的值,设置了3环和0环数据交互的方式。语法如下:

c
1
2
//这里必须是“|=”,不能直接写成“=”
pDeviceObj->Flags |= DO_BUFFERED_IO;

来看看有哪几种方式:

  • 缓冲区方式读写(DO_BUFFERED_IO):I/O管理器会在内核空间中分配一块内存,把用户空间的数据复制到这块内存中,这样内核程序就可以访问这些数据,实现数据通信。适合数据量较小时使用。(之前介绍的跨进程读取内存用的就是这种方法)
  • 直接方式读写(DO_DIRECT_IO):I/O管理器会将用户空间内的某片内存对应的物理页锁住,同时在内核空间再映射一份,这样内核空间线性地址与用户空间线性地址对应的是同一个物理页,这是双方均可以对这个物理页的内容进行读写,实现数据通信,此方法适合数据量较大时使用。(类似_KUSER_SHARED_DATA结构)
  • 默认方式读写(NEITHER_IO):当创建完设备对象后,不设置Flags的值,使用的就是此类读写方式。默认读写方式,仅仅提供给内核程序用户空间的线性地址,直接进行数据的读取。这样做的坏处是,如果发生线程切换,读取的就不再是同一份数据,容易造成程序读取错误。

通常情况下,我们实验的数据不会太大,主要采取DO_BUFFERED_IO这种方式。这里有一点要注意的是,在设置DeviceObject.Flags的值时,千万不要直接用”=”,必须使用”|=”,因为在创建设备对象结构体时,Flags是有初始值的,若这里直接给Flags赋值,会刷新掉之前的初始值,导致程序执行时发生错误(驱动技仅能成功执行一次,第二次会失败)。

设置符号链接

Windows规定,应用层的程序是不能直接访问设备对象的,所以符号链接诞生了。符号链接可以与设备对象绑定,这样应用层的程序可以通过符号链接进行对设备对象的访问。符号链接在内核与3环的形式有所不同:

  • 内核:符号链接以”\??\“开头,例如C盘就是”\??\C:”
  • 用户模式:符号链接以”\\.\“开头,例如C就算”\\.\C:”

具体在代码中还需要加入更多的“\“用来转译符号,代码如下:

Ring0

c
1
2
3
4
5
#define SYM_LINK_NAME L"\\??\\MyRing3Device"

UNICODE_STRING SymbolicLinkName;
RtlInitUnicodeString(&SymbolicLinkName, SYM_LINK_NAME);
IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName);

Ring3

c
1
2
3
4
5
6
7
8
9
10
11
#define SYM_LINK_NAME L"\\\\.\\MyRing3Device"
//3环这里直接使用符号链接指向0环创建设备作为CreateFile的参数
//不用再次设置符号链接,符号链接是在0环设置的,这里在3环主要介绍用法
HANDLE hDevice = CreateFileW(
SYM_LINK_NAME,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);

IRP与派遣函数

对比消息处理

先来看一张图

继续拿3环的窗口应用来做对比

  • 用户空间:当用户单击鼠标时,会触发一个事件,操作系统会将这个事件的内容描述信息封装到一个MSG结构中,作为消息发送给窗口对象,窗口对象接收到消息,会根据这个消息的类型,来执行相应的处理函数,我们称这种处理函数叫做回调函数

    1)触发事件:鼠标点击等

    2)消息结构体:MSG

    3)消息接收对象:窗口对象

    4)处理函数:窗口回调函数

  • 内核空间:当3环程序调用CreateFile函数时,这是操作系统会产生相应的IRP,这个IRP封装了3环程序调用的相关描述信息,接着会把IRP发送给内核空间的设备对象,设备对象会解析IRP,然后会根据IRP提供的信息,执行相应的派遣函数

    1)触发事件:3环程序调用CreateFile函数等

    2)消息结构体:IRP

    3)消息接收对象:设备对象

    4)处理函数:派遣函数

IRP的类型

正如3环的窗口对象,在接收到不同类型消息时会执行不同的回调函数。当应用层通过CreateFile,ReadFile,WriteFile,CloseHandle等函数对设备进行操作时,也会使操作系统产生不同种类的IRP,这里简要总结一下部分3环函数与IRP的对应关系

应用层函数 IRP种类
CreateFile IRP_MJ_CREATE
ReadFile IRP_MJ_READ
WriteFile IRP_MJ_WRITE
CloseHandle IRP_MJ_CLOSE
DeviceIoControl IRP_MJ_DEVICE_CONTROL

派遣函数

1)注册派遣函数

当IRP传递给设备对象后,会根据IRP的种类调用特定的派遣函数。不同的IRP对应不同的派遣函数,NT框架预定了28种派遣函数,可以在驱动对象MajorFunction数组中注册这些派遣函数

代码示例:

c
1
2
3
//这里仅设置了两个用来演示
pDriverObj->MajorFunction[IRP_MJ_CREATE] = IrpCreateProc;
pDriverObj->MajorFunction[IRP_MJ_CLOSE] = IrpCloseProc;

其中IrpCreateProc和IrpCloseProc都是我们需要自己定义的派遣函数,遵守一定的格式

2)派遣函数格式

这里以IrpCreateProc来举例:

c
1
2
3
4
5
6
7
8
9
10
11
NTSTATUS IrpCreateProc(PDEVICE_OBJECT pDeviceObj, PIRP pIrp) {
DbgPrint("Irp Create Dispatch Function...\n");
/*
处理自己的业务
*/

pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
  • 参数:

    1)设备对象指针

    2)IRP指针

  • IRP是一个结构体,通过指针可以指向IRP内部的一个字段IoStatus(_IO_STATUS_BLOCK结构),该结构中有两个字段:

    1)Status:三环程序调用GetLastError得到的就是这个值

    2)Information:返回给3环多少数据,没有则填0

  • IoCompleteRequest:表示调用方已完成所有I/O请求处理操作,并将给定的IRP返回给I/O管理器

以上是必须设置的值/执行的语句,完成后,即可

代码实现

有了以上基础后就可以实现简单的3环和0环的通信了,这里附上代码:

Ring0

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include "ntifs.h"

#define DEVICE_NAME L"\\Device\\MyDevice"
#define SYM_LINK_NAME L"\\??\\MyRing3Device"
#define OPER1 CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)

VOID Drvier_Unload(PDRIVER_OBJECT pDriverObj);
NTSTATUS IrpCreateProc(PDEVICE_OBJECT pDeviceObj, PIRP pIrp);
NTSTATUS IrpCloseProc(PDEVICE_OBJECT pDeviceObj, PIRP pIrp);

//Driver Entry
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING RegistryPath) {
DbgPrint("Driver is running!\n");

PDEVICE_OBJECT pDeviceObj = NULL;
NTSTATUS status = 0;

//Create Deivce Object
UNICODE_STRING DeviceName;
RtlInitUnicodeString(&DeviceName, DEVICE_NAME);
status = IoCreateDevice(pDriverObj,
0,
&DeviceName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&pDeviceObj);

if (status != STATUS_SUCCESS) {
DbgPrint("Device Create Failed!\n");
return status;
}
else {
DbgPrint("Device Create Success!\n");
}

//Set Communicate Ways
//注意这里一定要用"|=", 而不能直接用"=",因为在创建Device
pDeviceObj->Flags |= DO_BUFFERED_IO;

//Create Symbollic Link
UNICODE_STRING SymbolicLinkName;
RtlInitUnicodeString(&SymbolicLinkName, SYM_LINK_NAME);
IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName);

//Set Dispatch Function
pDriverObj->MajorFunction[IRP_MJ_CREATE] = IrpCreateProc;
pDriverObj->MajorFunction[IRP_MJ_CLOSE] = IrpCloseProc;

//Set Unload Function
pDriverObj->DriverUnload = Drvier_Unload;

return STATUS_SUCCESS;
}

NTSTATUS IrpCreateProc(PDEVICE_OBJECT pDeviceObj, PIRP pIrp) {
DbgPrint("Irp Create Dispatch Function...\n");

pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}


NTSTATUS IrpCloseProc(PDEVICE_OBJECT pDeviceObj, PIRP pIrp) {
DbgPrint("Irp Close Dispatch Function...\n");

pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}


VOID Drvier_Unload(PDRIVER_OBJECT pDriverObj) {
//Delete SymbolicLink
UNICODE_STRING SymbolicLinkName;
RtlInitUnicodeString(&SymbolicLinkName, SYM_LINK_NAME);
IoDeleteSymbolicLink(&SymbolicLinkName);

//Delete Deivce
IoDeleteDevice(pDriverObj->DeviceObject);

DbgPrint("Unload Success!\n");
}

Ring3

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "stdafx.h"
#include "Windows.h"
#include "winioctl.h"

#define OPCODE1 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define OPCODE2 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x900, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define SYM_LINK_NAME L"\\\\.\\MyRing3Device"

int main(int argc, char* argv[])
{
//Call IRP_MJ_CREATE
getchar();
HANDLE hDevice = CreateFileW(
SYM_LINK_NAME,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);

if (hDevice == INVALID_HANDLE_VALUE){
printf("Create File Failed!");
getchar();
return -1;
} else {
printf("Create File Success!");
}

//Call IRP_MJ_CLOSE
getchar();
BOOL bCH = CloseHandle(hDevice);
if(bCH != 0){
printf("Close File Success!");
}
getchar();
return 0;
}

程序测试

  1. 载入并运行驱动
  2. 运行3环程序
  3. 执行CreateFile
  4. 执行CloseHandle

查看运行结果,可以看到,在调用3环函数时,会执行相应的驱动函数。若想增加相应功能,只需在派遣函数中写上,即可在调用3环函数时执行,实现3环和0环的通信。当然,这样的结果,看着并不明显,下一篇,会介绍IRP这个结构,并进一步完善这份代码

参考链接

参考文章:CTL_CODE宏 详解

参考笔记:Joney,张嘉杰

参考代码:Joney,张嘉杰

Author: cataLoc
Link: http://cata1oc.github.io/2020/04/12/3%E7%8E%AF%E4%B8%8E0%E7%8E%AF%E9%80%9A%E4%BF%A1%EF%BC%88%E5%B8%B8%E8%A7%84%E6%96%B9%E5%BC%8F%EF%BC%89/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶