驱动开发:通过ReadFile与内核层通信

简介: 驱动与应用程序的通信是非常有必要的,内核中执行代码后需要将其动态显示给应用层,但驱动程序与应用层毕竟不在一个地址空间内,为了实现内核与应用层数据交互则必须有通信的方法,微软为我们提供了三种通信方式,如下先来介绍通过`ReadFile`系列函数实现的通信模式。

驱动与应用程序的通信是非常有必要的,内核中执行代码后需要将其动态显示给应用层,但驱动程序与应用层毕竟不在一个地址空间内,为了实现内核与应用层数据交互则必须有通信的方法,微软为我们提供了三种通信方式,如下先来介绍通过ReadFile系列函数实现的通信模式。

长话短说,不说没用的概念,首先系统中支持通信模式可以总结为三种。

  • 缓冲区方式读写(DO_BUFFERED_IO)
  • 直接方式读写(DO_DIRECT_IO)
  • 其他方式读写

而通过ReadFile,WriteFile系列函数实现的通信机制则属于缓冲区通信模式,在该模式下操作系统会将应用层中的数据复制到内核中,此时应用层调用ReadFile,WriteFile函数进行读写时,在驱动内会自动触发 IRP_MJ_READIRP_MJ_WRITE这两个派遣函数,在派遣函数内则可以对收到的数据进行各类处理。

首先需要实现初始化各类派遣函数这么一个案例,如下代码则是通用的一种初始化派遣函数的基本框架,分别处理了IRP_MJ_CREATE创建派遣,以及IRP_MJ_CLOSE关闭的派遣,此外函数DriverDefaultHandle的作用时初始化其他派遣用的,也就是将除去CREATE/CLOSE这两个派遣之外,其他的全部赋值成初始值的意思,当然不增加此段代码也是无妨,并不影响代码的实际执行。

#include <ntifs.h>

// 卸载驱动执行
VOID UnDriver(PDRIVER_OBJECT pDriver)
{
    PDEVICE_OBJECT pDev;                                        // 用来取得要删除设备对象
    UNICODE_STRING SymLinkName;                                 // 局部变量symLinkName
    pDev = pDriver->DeviceObject;
    IoDeleteDevice(pDev);                                           // 调用IoDeleteDevice用于删除设备
    RtlInitUnicodeString(&SymLinkName, L"\\??\\LySharkDriver");     // 初始化字符串将symLinkName定义成需要删除的符号链接名称
    IoDeleteSymbolicLink(&SymLinkName);                             // 调用IoDeleteSymbolicLink删除符号链接
    DbgPrint("驱动卸载完毕...");
}

// 创建设备连接
// LyShark.com
NTSTATUS CreateDriverObject(IN PDRIVER_OBJECT pDriver)
{
    NTSTATUS Status;
    PDEVICE_OBJECT pDevObj;
    UNICODE_STRING DriverName;
    UNICODE_STRING SymLinkName;

    // 创建设备名称字符串
    RtlInitUnicodeString(&DriverName, L"\\Device\\LySharkDriver");
    Status = IoCreateDevice(pDriver, 0, &DriverName, FILE_DEVICE_UNKNOWN, 0, TRUE, &pDevObj);

    // 指定通信方式为缓冲区
    pDevObj->Flags |= DO_BUFFERED_IO;

    // 创建符号链接
    RtlInitUnicodeString(&SymLinkName, L"\\??\\LySharkDriver");
    Status = IoCreateSymbolicLink(&SymLinkName, &DriverName);
    return STATUS_SUCCESS;
}

// 创建回调函数
NTSTATUS DispatchCreate(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    pIrp->IoStatus.Status = STATUS_SUCCESS;          // 返回成功
    DbgPrint("派遣函数 IRP_MJ_CREATE 执行 \n");
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);        // 指示完成此IRP
    return STATUS_SUCCESS;                           // 返回成功
}

// 关闭回调函数
NTSTATUS DispatchClose(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    pIrp->IoStatus.Status = STATUS_SUCCESS;          // 返回成功
    DbgPrint("派遣函数 IRP_MJ_CLOSE 执行 \n");
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);        // 指示完成此IRP
    return STATUS_SUCCESS;                           // 返回成功
}

// 默认派遣函数
NTSTATUS DriverDefaultHandle(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    NTSTATUS status = STATUS_SUCCESS;
    pIrp->IoStatus.Status = status;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);

    return status;
}

// 入口函数
// By: LyShark
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    // 调用创建设备
    CreateDriverObject(pDriver);

    pDriver->DriverUnload = UnDriver;                          // 卸载函数
    pDriver->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;    // 创建派遣函数
    pDriver->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;      // 关闭派遣函数

    // 初始化其他派遣
    for (ULONG i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
    {
        DbgPrint("初始化派遣: %d \n", i);
        pDriver->MajorFunction[i] = DriverDefaultHandle;
    }

    DbgPrint("驱动加载完成...");

    return STATUS_SUCCESS;
}

代码运行效果如下:

image.png

通用框架有了,接下来就是让该驱动支持使用ReadWrite的方式实现通信,首先我们需要在DriverEntry处增加两个派遣处理函数的初始化。

// 入口函数
// By: LyShark
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    // 调用创建设备
    CreateDriverObject(pDriver);

    // 初始化其他派遣
    for (ULONG i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
    {
        DbgPrint("初始化派遣: %d \n", i);
        pDriver->MajorFunction[i] = DriverDefaultHandle;
    }

    pDriver->DriverUnload = UnDriver;                          // 卸载函数
    pDriver->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;    // 创建派遣函数
    pDriver->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;      // 关闭派遣函数

    // 增加派遣处理
    pDriver->MajorFunction[IRP_MJ_READ] = DispatchRead;        // 读取派遣函数
    pDriver->MajorFunction[IRP_MJ_WRITE] = DispatchWrite;      // 写入派遣函数

    DbgPrint("驱动加载完成...");

    return STATUS_SUCCESS;
}

接着,我们需要分别实现这两个派遣处理函数,如下DispatchRead负责读取时触发,与之对应DispatchWrite负责写入触发。

  • 引言:
  • 对于读取请求I/O管理器分配一个与用户模式的缓冲区大小相同的系统缓冲区SystemBuffer,当完成请求时I/O管理器将驱动程序已经提供的数据从系统缓冲区复制到用户缓冲区。
  • 对于写入请求,会分配一个系统缓冲区并将SystemBuffer设置为地址,用户缓冲区的内容会被复制到系统缓冲区,但是不设置UserBuffer缓冲。

通过IoGetCurrentIrpStackLocation(pIrp)接收读写请求长度,偏移等基本参数,AssociatedIrp.SystemBuffer则是读写缓冲区,IoStatus.Information是输出缓冲字节数,Parameters.Read.Length是读取写入的字节数。

// 读取回调函数
NTSTATUS DispatchRead(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    NTSTATUS Status = STATUS_SUCCESS;
    PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation(pIrp);
    ULONG ulReadLength = Stack->Parameters.Read.Length;

    char szBuf[128] = "hello lyshark";

    pIrp->IoStatus.Status = Status;
    pIrp->IoStatus.Information = ulReadLength;
    DbgPrint("读取长度:%d \n", ulReadLength);

    // 取出字符串前5个字节返回给R3层
    memcpy(pIrp->AssociatedIrp.SystemBuffer, szBuf, ulReadLength);

    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return Status;
}

// 接收传入回调函数
// By: LyShark
NTSTATUS DispatchWrite(struct _DEVICE_OBJECT *DeviceObject, struct _IRP *Irp)
{
    NTSTATUS Status = STATUS_SUCCESS;
    PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation(Irp);
    ULONG ulWriteLength = Stack->Parameters.Write.Length;
    PVOID ulWriteData = Irp->AssociatedIrp.SystemBuffer;

    // 输出传入字符串
    DbgPrint("传入长度: %d 传入数据: %s \n", ulWriteLength, ulWriteData);

    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return Status;
}

如上部分都是在讲解驱动层面的读写派遣,应用层还没有介绍,在应用层我们只需要调用ReadFile函数当调用该函数时驱动中会使用DispatchRead派遣例程来处理这个请求,同理调用WriteFile函数则触发的是DispatchWrite派遣例程。

我们首先从内核中读出前五个字节并放入缓冲区内,输出该缓冲区内的数据,然后在调用写入,将hello lyshark写回到内核里里面,这段代码可以这样来写。

#include <iostream>
#include <Windows.h>
#include <winioctl.h>

int main(int argc, char *argv[])
{
  HANDLE hDevice = CreateFileA("\\\\.\\LySharkDriver", GENERIC_READ | GENERIC_WRITE, 0,
    NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hDevice == INVALID_HANDLE_VALUE)
  {
    CloseHandle(hDevice);
    return 0;
  }

  // 从内核读取数据到本地
  char buffer[128] = { 0 };
  ULONG length;

  // 读入到buffer长度为5
  // By:lyshark.com
  ReadFile(hDevice, buffer, 5, &length, 0);
  for (int i = 0; i < (int)length; i++)
  {
    printf("读取字节: %c", buffer[i]);
  }

  // 写入数据到内核
  char write_buffer[128] = "hello lyshark";
  ULONG write_length;
  WriteFile(hDevice, write_buffer, strlen(write_buffer), &write_length, 0);

  system("pause");
  CloseHandle(hDevice);
  return 0;
}

使用驱动工具安装我们的驱动,然后运行该应用层程序,实现通信,效果如下所示:

image.png

目录
相关文章
|
12月前
|
Linux
Linux驱动开发(使用I2C总线设备驱动模型编写AT24C02驱动程序)
Linux驱动开发(使用I2C总线设备驱动模型编写AT24C02驱动程序)
138 0
|
Ubuntu Linux 编译器
字符驱动设备原理及其相关函数(一)
字符驱动设备原理及其相关函数(一)
114 0
|
5月前
|
编解码 计算机视觉 Python
IPC机制在jetson中实现硬解码视频流数据通信的逻辑解析
IPC机制在jetson中实现硬解码视频流数据通信的逻辑解析
156 0
|
10月前
|
API Windows
5.3 Windows驱动开发:内核取应用层模块基址
在上一篇文章`《内核取ntoskrnl模块基地址》`中我们通过调用内核API函数获取到了内核进程`ntoskrnl.exe`的基址,当在某些场景中,我们不仅需要得到内核的基地址,也需要得到特定进程内某个模块的基地址,显然上篇文章中的方法是做不到的,本篇文章将实现内核层读取32位应用层中特定进程模块基址功能。
87 0
5.3 Windows驱动开发:内核取应用层模块基址
|
11月前
|
存储 安全 API
3.5 Windows驱动开发:应用层与内核层内存映射
在上一篇博文`《内核通过PEB得到进程参数》`中我们通过使用`KeStackAttachProcess`附加进程的方式得到了该进程的PEB结构信息,本篇文章同样需要使用进程附加功能,但这次我们将实现一个更加有趣的功能,在某些情况下应用层与内核层需要共享一片内存区域通过这片区域可打通内核与应用层的隔离,此类功能的实现依附于MDL内存映射机制实现。
146 0
3.5 Windows驱动开发:应用层与内核层内存映射
|
Linux
Linux驱动入门(5)LED驱动---驱动分层和分离,平台总线模型
Linux驱动入门(5)LED驱动---驱动分层和分离,平台总线模型
108 0
|
Linux
字符驱动设备原理及其相关函数(二)
字符驱动设备原理及其相关函数(二)
56 0
|
Ubuntu Linux 开发者
韦东山Linux驱动入门实验班(2)hello驱动---驱动层与应用层通讯,以及自动产生设备节点
韦东山Linux驱动入门实验班(2)hello驱动---驱动层与应用层通讯,以及自动产生设备节点
158 0
|
API C++ Windows
驱动开发:内核封装WSK网络通信接口
本章`LyShark`将带大家学习如何在内核中使用标准的`Socket`套接字通信接口,我们都知道`Windows`应用层下可直接调用`WinSocket`来实现网络通信,但在内核模式下应用层API接口无法使用,内核模式下有一套专有的`WSK`通信接口,我们对WSK进行封装,让其与应用层调用规范保持一致,并实现内核与内核直接通过`Socket`通信的案例。
339 0
驱动开发:内核封装WSK网络通信接口
|
网络安全
驱动开发:内核封装TDI网络通信接口
在上一篇文章`《驱动开发:内核封装WSK网络通信接口》`中,`LyShark`已经带大家看过了如何通过WSK接口实现套接字通信,但WSK实现的通信是内核与内核模块之间的,而如果需要内核与应用层之间通信则使用TDK会更好一些因为它更接近应用层,本章将使用TDK实现,TDI全称传输驱动接口,其主要负责连接`Socket`和协议驱动,用于实现访问传输层的功能,该接口比`NDIS`更接近于应用层,在早期Win系统中常用于实现过滤防火墙,同样经过封装后也可实现通信功能,本章将运用TDI接口实现驱动与应用层之间传输字符串,结构体,多线程收发等技术。
313 0
驱动开发:内核封装TDI网络通信接口