使用IO控制码进行驱动间通信

image-20240206155604226

驱动对象用DRIVER_OBJECT数据结构表示,它作为驱动的一个实例被内核加载,并且内核对一个驱动只加 载一个实例。确切地说,是由内核中的I/O管理器负责加载的。

image-20240206160545671

每个驱动程序会创建一个或多个设备对象,用DEVICE_OBJECT数据结构表示。每个设备对象都会有一个指 针指向下一个设备对象,因此就形成一个设备链。设备链的第一个设备是由上一节介绍的DRIVER_OBJECT 结构体中指明的。设备对象保存设备特征和状态的信息。

image-20240206160842444

DriverA(Server)

定义IO控制码

1
2
3
4
5
6
7
#define IOCTRL_BASE 0x800 // 定义了IO控制码的基础值,确保IO控制码唯一
#define IOCTRL_CODE1(i)\
CTL_CODE(FILE_DEVICE_UNKNOWN,IOCTRL_BASE+i,METHOD_IN_DIRECT,FILE_ANY_ACCESS)
#define IOCTRL_CODE2(i)\
CTL_CODE(FILE_DEVICE_UNKNOWN,IOCTRL_BASE+i,METHOD_OUT_DIRECT,FILE_ANY_ACCESS)
#define DEVICE_IRP_READ IOCTRL_CODE1(1)
#define DEVICE_IRP_WRITE IOCTRL_CODE2(2)

CTL_CODE(DeviceType, Function, Method, Access)

  • DeviceType:指定设备类型。在这里使用FILE_DEVICE_UNKNOWN,意味着设备类型未知或不重要。
  • Function:提供功能码,与IOCTRL_BASE相加用于生成特定的控制码。
  • Method:指定数据传输方法。这里使用METHOD_IN_DIRECTMETHOD_OUT_DIRECT,分别用于输入和输出操作,其中直接I/O允许数据在用户空间和内核空间之间直接传输,减少拷贝开销。
  • Access:定义访问权限。这里使用FILE_ANY_ACCESS,表示任何人都可以访问该控制码。

进行通信(DriverEntry调用)

  1. 初始化和获取设备对象
  • 首先,函数初始化状态变量StatusSTATUS_UNSUCCESSFUL,并准备输入输出缓冲区。
  • 使用IoGetDeviceObjectPointer根据设备名称获取设备对象(DeviceObject)和文件对象(FileObject)。这一步是必要的,因为后续操作需要这些对象来构建IRP(I/O请求包)。
  1. 构建和发送IRP
  • 函数通过IoBuildDeviceIoControlRequest构建一个设备IO控制请求的IRP,指定IO控制代码(在这里是DEVICE_IRP_READ),输入输出缓冲区,以及一个事件对象Event用于同步操作。
  • 在IRP的堆栈位置中设置文件对象,这对于设备驱动处理IRP时可能是必需的。
  • 调用IoCallDriver将IRP发送给设备驱动。
  1. 等待操作完成
  • 如果IoCallDriver返回值表示操作未立即成功完成,代码使用KeWaitForSingleObject等待之前创建的事件被设备驱动信号化。这表示操作已完成。
  • 完成后,通过DbgPrint打印调试信息和操作结果。
  1. 清理资源
  • 在操作完成后,使用ObDereferenceObject减少文件对象的引用计数。注意,不需要为设备对象做这一步,因为IoGetDeviceObjectPointer只增加了文件对象的引用计数。

由于函数立即返回,NTSTAUS一般是失败的。可以使用GetLastError继续判断。操作完成的标准是KEVENT事件授信。

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
void Sub_1()
{

NTSTATUS Status = STATUS_UNSUCCESSFUL;
IO_STATUS_BLOCK IoStatusBlock = { 0 };
TCHAR InputBuffer[100] = { 0 };
TCHAR OutputBuffer[100] = { 0 };
PFILE_OBJECT FileObject;
PDEVICE_OBJECT DeviceObject;
do
{

//构建设备名字
UNICODE_STRING DeviceName = RTL_CONSTANT_STRING(L"\\device\\DriverB"); //字符串转换至Unicode中
//根据设备名字获取实际的设备对象和文件对象,等价于直接调用
//CreateFile(返回的句柄可以通过句柄找对象方式获得设备对象和文件对象)
Status = IoGetDeviceObjectPointer(&DeviceName, FILE_ALL_ACCESS, &FileObject, &DeviceObject);
if (!NT_SUCCESS(Status))
break;
//申请 IRP接口 生成DeviceIoControl 类型的Irp请求
LARGE_INTEGER offset = { 0 };
KEVENT Event = { 0 };
KeInitializeEvent(&Event, SynchronizationEvent, FALSE);
size_t InputSize = (wcslen(L"[A]--DEVICE_IRP_READ -->我要开始读取了") + 1) * 2; //发送数据的长度
RtlStringCbCopyNW(InputBuffer, sizeof(InputBuffer), L"[A]--DEVICE_IRP_READ -->我要开始读取了", InputSize);
//
PIRP Irp = IoBuildDeviceIoControlRequest(
DEVICE_IRP_READ, //判断请求码
DeviceObject,
InputBuffer,
100,
OutputBuffer,
100,
FALSE,
&Event,
&IoStatusBlock);

//
PIO_STACK_LOCATION IoStackLocation = IoGetNextIrpStackLocation(Irp); //获取当前的Irp堆栈
IoStackLocation->FileObject = FileObject;


//函数立马返回
Status = IoCallDriver(DeviceObject, Irp);

if (!NT_SUCCESS(Status))
{

DbgPrint("[A]---> 模拟调用 DeviceIoControl 成功\r\n");
//采用无限等待方式进行等待
KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, 0);
DbgPrint("[A]---> 数据读取完成\r\n");
DbgPrint("[A] --> read value is %ws \r\n", OutputBuffer);
}
else
{
DbgPrint("[A]---> 模拟调用 DeviceIoControl 失败\r\n");
}
} while (0);
ObDereferenceObject(FileObject); //减少引用,不需要解除Deviceobj, IoGetDeviceObjectPointer规定
return;
}

同样测试Write控制码

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
void Sub_2()
{

NTSTATUS Status = STATUS_UNSUCCESSFUL;
IO_STATUS_BLOCK IoStatusBlock = { 0 };
TCHAR InputBuffer[100] = { 0 };
TCHAR OutputBuffer[100] = { 0 };
PFILE_OBJECT FileObject;
PDEVICE_OBJECT DeviceObject;
do
{

//构建设备名字
UNICODE_STRING DeviceName = RTL_CONSTANT_STRING(L"\\device\\DriverB");
//根据设备名字获取实际的设备对象和文件对象,等价于直接调用
//CreateFile(返回的句柄可以通过句柄找对象方式获得设备对象和文件对象)
Status = IoGetDeviceObjectPointer(&DeviceName, FILE_ALL_ACCESS, &FileObject, &DeviceObject);
if (!NT_SUCCESS(Status))
break;
//3.申请 IRP接口 生成DeviceIoControl 类型的Irp请求
LARGE_INTEGER offset = { 0 };
KEVENT Event = { 0 };
KeInitializeEvent(&Event, SynchronizationEvent, FALSE);
size_t InputSize= (wcslen(L"[A]--DEVICE_WRITE -->我要开始读取了") + 1) * 2;
RtlStringCbCopyNW(InputBuffer, sizeof(InputBuffer), L"[A]--DEVICE_IRP_WRITE -->我要开始读取了", InputSize);

//向目标驱动的IRP_MJ_DEVICE_CONTROL
PIRP Irp = IoBuildDeviceIoControlRequest(
DEVICE_IRP_WRITE, //子功能码
DeviceObject,
InputBuffer,
100,
OutputBuffer,
100,
FALSE,
&Event,
&IoStatusBlock);

PIO_STACK_LOCATION IoStackLocation = IoGetNextIrpStackLocation(Irp);
IoStackLocation->FileObject = FileObject;
Status = IoCallDriver(DeviceObject, Irp);

if (!NT_SUCCESS(Status))
{

DbgPrint("[A]---> 模拟调用 DeviceIoControl 成功\r\n");
//采用无限等待方式进行等待
KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, 0);
DbgPrint("[A]---> 数据读取完成\r\n");
DbgPrint("[A] --> read value is %ws \r\n", OutputBuffer);
}
else
{
DbgPrint("[A]---> 模拟调用 DeviceIoControl 失败\r\n");
}
} while (0);

ObDereferenceObject(FileObject); //减少引用,不需要解除Deviceobj, IoGetDeviceObjectPointer规定
return;
}

DriverUnload不需要做任何操作。但需要定义。

DriverB(Client)

需要做好一样的定义。

1
2
3
4
5
6
7
#define IOCTRL_BASE 0x800
#define IOCTRL_CODE1(i)\
CTL_CODE(FILE_DEVICE_UNKNOWN,IOCTRL_BASE+i,METHOD_IN_DIRECT,FILE_ANY_ACCESS)
#define IOCTRL_CODE2(i)\
CTL_CODE(FILE_DEVICE_UNKNOWN,IOCTRL_BASE+i,METHOD_OUT_DIRECT,FILE_ANY_ACCESS)
#define DEVICE_IRP_READ IOCTRL_CODE1(1)
#define DEVICE_IRP_WRITE IOCTRL_CODE2(2)

DriverEntry

  1. 创建驱动设备对象
  2. 所有MajorFunction初始化为默认派遣例程
  3. IRP_MJ_DEVICE_CONTROL关联IoDispatchControl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
NTSTATUS Status;
int i = 0;

//注册其他驱动调用函数入口
DriverObject->DriverUnload = DriverUnload;
//创建驱动设备对象
Status = CreateDevice(DriverObject);

for (i = 0;i < IRP_MJ_MAXIMUM_FUNCTION;i++)
{
DriverObject->MajorFunction[i] = PassThroughDispatch; //默认派遣历程
}

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoDispatchControl; //主功能码与函数的关联

return Status;
}

创建驱动设备对象

  1. 使用UNICODE_STRING结构和RtlInitUnicodeString函数初始化设备名称为\\Device\\DriverB。这个名称在内核命名空间中唯一标识了驱动程序创建的设备。
  2. 调用IoCreateDevice函数创建设备对象。这个函数需要驱动程序对象、设备名称、设备类型(这里为FILE_DEVICE_UNKNOWN),以及是否独占访问(这里为TRUE)等参数。
  3. 设定DeviceObjectFlags属性,启用直接I/O(DO_DIRECT_IO)。这意味着数据传输将直接在用户缓冲区和设备之间进行,而不是使用中间缓冲区。
  4. 初始化符号链接名称为\\??\\DriverB,然后调用IoCreateSymbolicLink函数创建从符号链接到设备对象的映射。这样,用户模式应用程序可以通过符号链接访问设备。
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
NTSTATUS CreateDevice(
IN PDRIVER_OBJECT DriverObject)
{
NTSTATUS Status;
PDEVICE_OBJECT DeviceObject;


//创建设备名称
UNICODE_STRING DeviceName;
RtlInitUnicodeString(&DeviceName, L"\\Device\\DriverB");

//创建设备
Status = IoCreateDevice(DriverObject,NULL,
&DeviceName,
FILE_DEVICE_UNKNOWN,
0, TRUE,
&DeviceObject);
if (!NT_SUCCESS(Status))
return Status;

DeviceObject->Flags |= DO_DIRECT_IO;


//创建符号链接
UNICODE_STRING LinkName;
RtlInitUnicodeString(&LinkName, L"\\??\\DriverB");
Status = IoCreateSymbolicLink(&LinkName, &DeviceName);
if (!NT_SUCCESS(Status))
{
IoDeleteDevice(DeviceObject);
return Status;
}

return STATUS_SUCCESS;
}

IO派遣例程

  1. 初始化I/O控制码(IoControlCode)、输入和输出缓冲区的大小(InputSizeOutputSize),以及状态变量(Status)。调用IoGetCurrentIrpStackLocation获取当前IRP的堆栈位置,以便访问其中的参数。
  2. 函数使用switch语句根据IRP中的IO控制码的子功能码(IoControlCode)分别处理读写请求(DEVICE_IRP_READDEVICE_IRP_WRITE)。
    • 对于读请求(DEVICE_IRP_READ),函数尝试从MDL(内存描述列表)获取输入缓冲区地址,并将一段预定义的文本复制到系统缓冲区中作为输出。
    • 对于写请求(DEVICE_IRP_WRITE),函数从系统缓冲区读取输入数据,并尝试将一段预定义的文本复制到通过MDL获取的输出缓冲区中。
    • 最后都要完成IO请求(IoCompleteRequest),并对IOSB成员进行设置。

除了“缓冲区”方式读写设备外,另外一种方式是直接方式读写设备。这种方式需要创建完设备对象后, 在设置设备属性的时候,设置为DO_DIRECT_IO,而不是设置DO_BUFFERED_IO属性。操作系统先将用户模式的地址锁定后,操作系统用内存描述符表(MDL数据结构)记录这段内存。

image-20240206155839302

MDL记录这段虚拟内存,这段虚拟内存的大小存储在mdl->ByteCount里,这段虚拟内存的第一个页地址是 mdl->StartVa,这段虚拟内存的首地址对于第一个页地址的偏移量是mdl->ByteOffset。因此,这段虚拟 内存的首地址应该是mdl->StartVa+mdl->ByteOffset。DDK提供了几个宏方便程序员得到这几个数值。

1
2
3
#define MmGetMdlByteCount(Mdl) ((Mdl)->ByteCount) 
#define MmGetMdlByteOffset(Mdl) ((Mdl)->ByteOffset)
#define MmGetMdlVirtualAddress(Mdl) \ ((PVOID) ((PCHAR) ((Mdl)->StartVa) + (Mdl)->ByteOffset))
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
NTSTATUS IoDispatchControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
//Sure it's our driver

//如果不进入If直接完成Irp
if (1) //省略了判断是否为自己的驱动
{

if (NULL == Irp)
return STATUS_UNSUCCESSFUL;
BOOLEAN IsOk = FALSE;
ULONG IoControlCode = 0;
ULONG InputSize = 0;
ULONG OutputSize = 0;
NTSTATUS Status = STATUS_UNSUCCESSFUL;


PIO_STACK_LOCATION IrpStackLocation = NULL;
do
{
IrpStackLocation = IoGetCurrentIrpStackLocation(Irp);
IoControlCode = IrpStackLocation->Parameters.DeviceIoControl.IoControlCode; //获得子功能码
InputSize = IrpStackLocation->Parameters.DeviceIoControl.InputBufferLength;
OutputSize = IrpStackLocation->Parameters.DeviceIoControl.OutputBufferLength;


switch (IoControlCode)
{
case DEVICE_IRP_READ:
{
PVOID InputBuffer = NULL;
PVOID OutputBuffer = NULL;
InputBuffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);;
OutputBuffer = Irp->AssociatedIrp.SystemBuffer;
size_t v7 = (wcslen(L"[B]-->Read") + 1) * 2;
if (InputBuffer != NULL)
{
DbgPrint("[B]---> 获取到的输入缓冲区为: %ws\r\n", InputBuffer);
RtlCopyMemory(OutputBuffer, L"[B]-->Read", v7);
DbgPrint("[B]---> 设置的输出缓冲区为: %ws\r\n", OutputBuffer);
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = v7;
IoCompleteRequest(Irp, IO_NO_INCREMENT); //完成请求
return STATUS_UNSUCCESSFUL;
}
}
break;
case DEVICE_IRP_WRITE:
{
PVOID InputBuffer = NULL;
PVOID OutputBuffer = NULL;
InputBuffer = Irp->AssociatedIrp.SystemBuffer;
OutputBuffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);;
size_t v7 = (wcslen(L"[B]-->Write") + 1) * 2;
if (InputBuffer != NULL)
{
DbgPrint("[B]---> 获取到的输入缓冲区为: %ws\r\n", InputBuffer); //显示给你的数据
RtlCopyMemory(OutputBuffer, L"[B]-->Write", v7); //构建返回的数据
DbgPrint("[B]---> 设置的输出缓冲区为: %ws\r\n", OutputBuffer); //显示返回的数据 irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = v7; //Irp中的IoStatusBlock
IoCompleteRequest(Irp, IO_NO_INCREMENT); //完成请求
return STATUS_UNSUCCESSFUL;
}
}
break;
default:
break;
}
} while (FALSE);

if (!IsOk)
{
//请求码不是上面的情况
Irp->IoStatus.Status = Status; //Irp中的IoStatusBlock
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_UNSUCCESSFUL;
}
}
Irp->IoStatus.Status = STATUS_UNSUCCESSFUL;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}

默认派遣例程

1
2
3
4
5
6
7
NTSTATUS PassThroughDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
Irp->IoStatus.Status = STATUS_SUCCESS; //LastError()
Irp->IoStatus.Information = 0; //ReturnLength
IoCompleteRequest(Irp, IO_NO_INCREMENT); //将Irp返回给Io管理器
return STATUS_SUCCESS;
}

驱动卸载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
PDEVICE_OBJECT v1;
PDEVICE_OBJECT v2;
v1 = DriverObject->DeviceObject;
UNICODE_STRING LinkName;
RtlInitUnicodeString(&LinkName, L"\\??\\DriverB");
IoDeleteSymbolicLink(&LinkName);
while (v1 != NULL)
{
//删除符号链接

v2 = v1->NextDevice;
IoDeleteDevice(v1);
v1 = v2;
}
}