首页
社区
课程
招聘
驱动开发学习:实现三环零环通信
发表于: 2024-8-15 10:53 2040

驱动开发学习:实现三环零环通信

2024-8-15 10:53
2040

三环与零环通信

因为零环中的程序不能像三环一样输出到控制台或者窗口,只能通过DbgPrint这样的调试符号打印观察到输出结果,所以才需要设计三环和零环通信。零环的特殊权限能做到三环无法完成的功能,而三环能帮助实现与用户的良好交流。

零环方面的准备

生成设备对象

设备对象是三环与零环沟通的桥梁。零环的程序可以像操纵文件一样操控这个设备对象。对于需要对驱动进行操纵或控制的需求,生成的设备对象叫“控制设备对象”,通过该对象可以实现对驱动的控制。(关于设备对象,驱动对象和请求之间的关系,在文章《关键对象》中有详细介绍)

生成设备对象用IoCreateDevice (看到这个api的前缀是Io,也可以联想到设备对象与通信之间的密切关系)

1
2
3
4
5
6
7
8
9
10
NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT Driverobject,
IN ULONG DeviceExtensionsize,
IN PUNICODE_STRING DeviceName OPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);

参数介绍:

  • 第一个参数可以从DriverEntry的参数表中获得,表示该驱动的驱动对象。(也就意味着,在当前驱动下创立了该设备对象)。
  • 第二个参数表示设备扩展大小(看示例)
  • 第三个参数表示设备的名称。(OPTIONAL的含义,是表示该设备可有名字也可以没有名字,也就暗示了打开或操纵一个设备不一定只用名字来操作)。
  • 第四个参数表示设备的类型,Windows已经考虑到了用户可能会使用到的设备类型。
  • 第五个参数表示创建设备对象的属性。
  • 第六个参数表示该设备是否为独占设备,该参数的设定直接决定了该设备能否打开多个句柄被多个进程操作。
  • 第七个参数则返回该设备对象的指针。

tips:对于使用IoCreateDevice创建的设备,默认要三环程序需要用到管理员权限才能打开。而我们并不知道实际的用户到底是具有怎样的权限。应对办法是用IoCreateDeviceSecure创建设备对象,其优点在于我们可以自己定义有怎样权限的用户才能打开设备(当然就可以设置成普通权限也可以打开了)。

1
2
3
4
5
6
7
8
9
10
11
12
NTSTATUS
IoCreateDeviceSecure(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionsize,
IN PUNICODE_STRING DeviceNameOPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
IN PCUNICODE_STRING DefaultSDDLString,
IN LPCGUID DeviceclassGuid,
OUT PDEVICE_OBJECT *Deviceobject,
);

该api新增加的两个参数:

IN PCUNICODE_STRING DefaultSDDLString,
IN LPCGUID DeviceclassGuid,

第一个参数填入字符串**”D:P(A;;GA;;;WD)”**即可保证任何权限都可以打开。

第二个参数是用CoCreateCuid生成的全球唯一标识符,用于区分不同设备(解决了设备重名的问题)。

tips:控制设备保存在全局变量中即可(因为一个驱动生成一个控制设备就足够了。三环用这个控制设备就可以完全操纵驱动)。

控制设备的名字与符号链接

为了三环的应用程序能与零环的设备进行通信,对创建的控制设备有以下两点要求。

  • 在创建设备对象时,要为设备创建名字
  • 为该控制设备创建符号链接

以上提到的两个参数都是UNICODE_STRING类型的。

创建符号链接使用api IoCreateSymbolicLink 而这个函数所用到的参数,就是前文提到的零环与三环通信所需的两个必备参数。

1
2
3
4
5
6
7
8
9
10
11
12
//生成符号链接
NTSTATUS
IoCreateSymbolicLink(
IN PUNICODE_STRING SymbolicLinkName,
IN PUNICODE_STRING DeviceName
);
 
//删除已经生成的符号链接
NTSTATUS
IoDeleteSymbolicLink(
IN PUNICODE_STRING SymbolicLinkName
);

tips:在写符号链接字符串和设备名字字符时,要考虑到两点。

  1. Windows的设备都像文件一样位于一个对象树的管理之下。一般而言,设备都位于\Device\这个路径下(但这不是绝对的),而生成的符号链接则一般位于??\这个路径下。这两个字符串其实是路径名。因此我们前面提到的设备名,符号链接那些,实际上都是定义成一个路径的形式。
  2. 同样要保证字符串不相同避免产生冲突问题。

删除设备对象

删除设备对象和符号链接一般在卸载驱动时进行了。(因为设备是归属于某个驱动的,如果驱动都卸载了那设备也就不复存在了,但是这个操作不像三环那样有操作系统帮我们回收,而是要在卸载函数中自己定义清理操作)

使用IoDeleteSymbolicLinkIoDeleteDevice即可清除对应的设备和符号链接。

分发函数

前面提到了设备对象和分发函数构成了整个内核的基本框架,可见理解分发函数的重要性。

首先,分发函数是与驱动对象进行绑定的,某个驱动有某些分发函数。具体而言,驱动的分发函数指针保存在驱动对象的**MajorFunction[]**数组中。

1
2
driver->MajorFunction[i] = fun;
//此处的i,由irp的功能号决定,相当于数组的每个位置都是特定的。fun表示定义的函数指针

定义不同的分发函数可以应对不同的请求。一般,一个分发函数的格式像下面这样定义。

1
2
3
4
NTSTATUS cwkDispatch(
IN PDEVICE_OBJECT dev,    //请求所要发送到的设备对象的指针
IN PIRP irp               //请求
)

整个过程类似于:三环或其他某些内核模块调用了某些api,这些api会产生对应的irp信号,然后操作系统根据irp信号,来决定传给哪个分发函数进行处理(例如在三环中,通过CreateFile来得到一个控制设备的句柄,内核中根据该控制设备可以找到对应的驱动对象,然后再根据不同的irp来分发到不同的函数)。

请求

内核中的IRP类似于网络传输中的数据包,内核中通过各种IRP请求来进行通信。每个请求都对应有一个主功能号,区分不同的功能。

例如:

打开请求的主功能号是IRP_MJ_CREATE。
关闭请求的主功能号是 IRP_MJ_CLOSE。
设备控制请求的主功能号是IRP_MJ_DEVICE_CONTROL。

对于PIRP,使用函数IoGetCurrentIrpStackLocation获得当前请求的栈空间,该函数返回PIO_STACK_LOCATION类型的指针,就可以使用该结构体中各种关于irp结构体的成员了。

(例如:通过PIO_STACK_LOCATION的MajorFunction就可以获得请求的主功能号)

1
2
PIRP irp;
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);

分发函数结束部部分

1
2
3
4
5
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = Length;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
return status;

三环的准备

三环中打开驱动

要控制驱动就要先获取该驱动控制设备对象句柄,使用CreateFile结合符号链接路径打开控制设备对象句柄。

tips:符号链接路径在应用层中为**\.**开头,写C代码时要进行转义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//文件打开的示例代码
#define IO_DEV_SYS_PATH L"\\\\.\\my_test_demo" //注意这里的路径应该是在零环中定义好的唯一路径,此处只是一个示例
int _tmain()
{
        HANDLE dev = NULL;
        //参数可以直接按照这样的写法填充
        dev = CreateFile(IO_DEV_SYS_PATH, GENERIC_READ|GENERIC_WRITE, 0, 0, OPEN_EXISTING,FILE_ATTRIBUTE_SYSTEM, 0);
         
        if(INVALID_HANDLE_VALUE)
        {
            printf("dev handle null");
            return -1;
        }
        else
        {
            printf("open DO success");
            ...
        }
}

不再使用该句柄时,调用CloseHandle就可以正常关闭了。

构造请求*

如果需要通信的内容不是已经定义好的irp请求号(例如一些常见的读写操作,对应IRP_MJ_READ, IRP_MJ_WRITE)已经取得控制设备句柄后。就可以构造请求协议,通过该句柄与驱动通信了。

前面提到,请求通过不同的功能号进行区分。所以在应用层,我们定义号不同的功能号,就能在与控制设备交互时,执行不同的操作。定义功能号的方法是使用CTL_CODE宏,该宏有四个参数。

1
2
3
4
5
6
#define CTL_CODE(DeviceType, Function, Method, Access) ( \
    ((DeviceType) << 16) | \
    ((Access) << 14) | \
    ((Function) << 2) | \
    (Method) \
)

DeviceType:表示对应的是什么样的驱动控制设备类型。(例如要操作的驱动是鼠标,则这里就应该填鼠标控制设备类型 FILE_DEVICE_MOUSE

Function:功能号核心数字,用于组成功能号(注意只能填0x7ff和0xfff之间的数)

Method:缓冲方式,定义为 METHOD_BUFFERED 即可。

Access:句柄拥有的操作权限。(FILE_WRITE_DATA则指写操作,与打开文件提供权限的写法一致)

进行通信

当成功打开句柄,正确构造请求后,就可以使用 DeviceloControl api 对设备发送请求了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//示例伪代码,注意此处直接传入str buffer到内核中是不安全的,有缓冲区溢出风险
#define MY_IRP (ULONG)CTL_CODE(FILE_DEVICE_UNKONWN, 0x888, METHOD_BUFFERED, FILE_WRITE_BUFFER)
 
int ask_dev(PCHAR str, HANDLE dev)
{
        if(!DeviceIoControl(dev, MY_IRP, str, strlen(str) + 1, NULL, 0, &ret_len, 0))
        {
         printf("wrong");
         return -2;
        }
        else
        {
        printf("success");
        ...
        }
}

通信的完整实例

目标:实现一个三环程序将字符串传给零环,再由该零环程序向另一个三环程序发送字符串,该三环程序将字符串打印到控制台。

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//零环代码
#include <wdm.h>
#include <ntddk.h> 
#include <Wdmsec.h>
 
#define MY_GUID L"a952eebb-84fc-4bd3-9708-3a8dc94f0c2c" //这段guid在实际使用中请自行调用CoCreateCuid生成
#define MY_CDO_NAME L"\\Device\\my_test_name"
#define MY_SYB_NAME L"\\??\\my_test_sybname123"
 
//定义内存分配标识
#define MEM_TAG 'MyTt'
char* DeviceBuffer = NULL;
int Length = -1;
 
//声明分发函数
NTSTATUS DispatchWrite(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS DispatchRead(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS DispatchCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp);
 
//定义一个全局的控制设备
PDEVICE_OBJECT my_cdo = NULL;
 
// 提供一个 Unload 函数只是为了让这个程序能动态卸载,方便调试
VOID DriverUnload(PDRIVER_OBJECT driver)
{
    DbgPrint("first:our driver is unloading..r\n");
    UNICODE_STRING my_syb_name = RTL_CONSTANT_STRING(MY_SYB_NAME);
 
    // 删除符号链接
    IoDeleteSymbolicLink(&my_syb_name);
 
        //释放内核buffer
    if (DeviceBuffer)
    {
        ExFreePoolWithTag(DeviceBuffer, MEM_TAG);
    }
 
    // 删除设备对象
    if (my_cdo)
    {
        IoDeleteDevice(driver->DeviceObject);
    }
}
 
NTSTATUS DispatchRead(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
 
    PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp);
    PVOID buffer = Irp->AssociatedIrp.SystemBuffer;
    ULONG length = irpSp->Parameters.Read.Length;
 
    // 检查 DeviceBuffer 是否已初始化且不为空
    if (DeviceBuffer == NULL || Length == -1)
    {
        DbgPrint("Device buffer is uninitialized!");
 
        Irp->IoStatus.Status = STATUS_NO_DATA_DETECTED;
        Irp->IoStatus.Information = 0;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
        return STATUS_NO_DATA_DETECTED;
    }
 
    // 检查用户缓冲区长度是否有效
    if (length == 0)
    {
        DbgPrint("Invalid read length!");
 
        Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
        Irp->IoStatus.Information = 0;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
        return STATUS_INVALID_PARAMETER;
    }
 
    // 将数据从 DeviceBuffer 复制到用户缓冲区
    RtlCopyMemory(buffer, DeviceBuffer, Length);
 
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = Length;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
    return STATUS_SUCCESS;
}
 
NTSTATUS DispatchWrite(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
 
    PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp);
    PVOID buffer = Irp->AssociatedIrp.SystemBuffer;
    ULONG length = irpSp->Parameters.Write.Length;
 
    if (DeviceBuffer == NULL || length == -1)
    {
        //为目标字符串分配内存
        DbgBreakPoint();
 
        DeviceBuffer = (char*)ExAllocatePoolWithTag(NonPagedPool, length, MEM_TAG);
 
        Length = length;
    }
    else if (length > 0x10)
    {
        DbgPrint("buffer too big");
 
        Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
        Irp->IoStatus.Information = 0;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
        return STATUS_INVALID_PARAMETER;
    }
 
    // 将数据写入设备的内存缓冲区
    if (DeviceBuffer != NULL)
    {
        RtlCopyMemory(DeviceBuffer, buffer, length);
 
        Length = length;
 
        DbgPrint("all right");
 
        Irp->IoStatus.Status = STATUS_SUCCESS;
        Irp->IoStatus.Information = length;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
        return STATUS_SUCCESS;
    }
    else
    {
        DbgPrint("Memory allocation failed!\n");
 
        Irp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;
        Irp->IoStatus.Information = 0;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
        return STATUS_INSUFFICIENT_RESOURCES;
    }
}
 
// 处理 IRP_MJ_CREATE 和 IRP_MJ_CLOSE 请求
NTSTATUS DispatchCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
 
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
 
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
 
    return STATUS_SUCCESS;
}
 
// DriverEntry,入口函数,相当于main
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
{
    UNREFERENCED_PARAMETER(reg_path);
 
    NTSTATUS status;
    //设备名定义在\Device\路径下
    UNICODE_STRING my_cdo_name = RTL_CONSTANT_STRING(MY_CDO_NAME);
    //定义万能打开权限
    UNICODE_STRING my_sddl = RTL_CONSTANT_STRING(L"D:P(A;;GA;;;WD)");
 
    //创建获得控制设备对象, 用这种方式创建的控制设备,三环程序可以不需要管理员权限也可以打开
    status = IoCreateDeviceSecure(
        driver,                     //设备从属的驱动对象
        0, &my_cdo_name,            //设备扩展,控制设备名字
        FILE_DEVICE_UNKNOWN,        //需要创建的设备类型,此处我们只是实现一个自己的通信设备没有实际对应的硬件模板
        FILE_DEVICE_SECURE_OPEN,    //表示系统可以检查其权限
        TRUE, &my_sddl,             //TRUE表示可以同时被多个3环程序打开,my_sddl表示万能打开权限
        (LPCGUID)&MY_GUID,          //GUID
        &my_cdo                     //返回的控制设备句柄
    );
    if (!NT_SUCCESS(status))
    {
        DbgPrint("IoCreateDeviceSecure error");
        return status;
    }
 
    //设置io方式 !重要!///////////////////////////
     
    // 设置 DO_BUFFERED_IO 标志,启用缓冲 I/O 模式
    my_cdo->Flags |= DO_BUFFERED_IO;
 
    // 清除 DO_DEVICE_INITIALIZING 标志
    my_cdo->Flags &= ~DO_DEVICE_INITIALIZING;
 
    //为刚刚生成的控制对象绑定符号链接
    //符号链接定义在\??\下
    UNICODE_STRING my_syb_name = RTL_CONSTANT_STRING(MY_SYB_NAME);
    status = IoCreateSymbolicLink(
        &my_syb_name,                //符号链接名
        &my_cdo_name               //控制设备名
    );
    if (!NT_SUCCESS(status))
    {
        DbgPrint("IoCreateSymbolicLink error");
        IoDeleteDevice(driver->DeviceObject);     //如果创建符号链接失败,记得关闭已经创建的控制设备
        return status;
    }
 
        //配置分发函数
    driver->DriverUnload = DriverUnload;
    driver->MajorFunction[IRP_MJ_READ] = DispatchRead;
    driver->MajorFunction[IRP_MJ_WRITE] = DispatchWrite;
    driver->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose;
    driver->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
 
    return STATUS_SUCCESS;
}
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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <windows.h>
 
#define CDO_SYB_NAME L"\\\\.\\my_test_sybname123"
 
int main()
{
    HANDLE device = NULL;
    DWORD moudle = 0;
    char buffer[0x20] = { 0 };
    DWORD retLen = 0;
 
    printf("you want read or write?\r\n");
    printf("1: Write\r\n");
    printf("2: Read\r\n");
     
    scanf("%d", &moudle);
 
    if (moudle == 1)
    {
        system("cls");
        printf("plz input a string:\r\n");
        getchar();
 
        // 使用 fgets 安全地读取输入
        if (fgets(buffer, sizeof(buffer), stdin) != NULL)
        {
            // 移除可能存在的换行符
            buffer[strcspn(buffer, "\n")] = 0;
        }
 
        printf("%s", buffer);
 
        //像文件一样打开这个设备
        device = CreateFile(CDO_SYB_NAME, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, 0);
        if (device == INVALID_HANDLE_VALUE)
        {
            printf("Open device failed.\r\n");
            return -1;
        }
        else
        {
            printf("Open device successfully.\r\n");
 
            system("pause");
 
            WriteFile(device, buffer, strlen(buffer) + 1, &retLen, NULL);
 
            CloseHandle(device);
            return 0;
        }
    }
    else if (moudle == 2)
    {
        system("cls");
        //像文件一样打开这个设备
        device = CreateFile(CDO_SYB_NAME, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, 0);
        if (device == INVALID_HANDLE_VALUE)
        {
            printf("Open device failed.\r\n");
            return -1;
        }
        else
        {
            printf("Open device successfully.\r\n");
 
            ReadFile(device, buffer, strlen(buffer) + 1, &retLen, NULL);
 
            printf("get str: %s\r\n", buffer);
 
            system("pause");
 
            CloseHandle(device);
            return 0;
        }
    }
     
    printf("parm error\r\n");
 
    return 0;
}

效果演示




收藏
免费 0
支持
分享
最新回复 (2)
雪    币: 2
活跃值: (58)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
很厉害!   有我年轻的影子。  好好去揭密windows未公开的内核函数吧
12小时前
1
雪    币: 9
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
加油
7小时前
0
游客
登录 | 注册 方可回帖
返回
//