吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3094|回复: 4
收起左侧

[系统底层] [Windows Rootkits学习]第二弹:派遣函数、驱动对象设备对象和传输机制探秘!

  [复制链接]
N1nEmAn 发表于 2023-4-21 17:11
本帖最后由 N1nEmAn 于 2023-4-21 17:26 编辑

前言

今天我们的内容比较丰富,涉及派遣函数的理解和编写,驱动对象和设备对象的区别,三种传输机制的介绍和理解……让我们在内核世界更进一步吧!
我是原作者,因为这边很多人在看我的文章,我也非常感激!故在此发布供大家学习。友情链接:https://www.freebuf.com/articles/system/364393.html

1.派遣函数

目标

理解派遣函数的定义,并且在自己的驱动中编写派遣函数。

0x01 什么是I/O,什么是I/O请求包

简单来说,I/O请求就是软件层面向硬件层面的请求

I/O请求(Input/Output Request)是指应⽤程序或操作系统向设备发送的读取、写⼊、打开、关闭等
操作请求。例如,当您打开⼀个⽂件时,操作系统会向文件所在的磁盘驱动器发送⼀个I/O请求,以读
取⽂件的内容。当您向⽹络发送数据时,操作系统会向网络适配器发送⼀个I/O请求,以将数据发送出
去。

在Windows内核中,I/O请求通常使⽤I/O请求包(IRP)结构体来表⽰。IRP包含了请求的类型、请求
的参数、缓冲区地址、I/O状态等信息。当⼀个I/O请求被提交到内核中时,内核会将IRP插⼊到相应的
驱动程序的I/O请求队列中,并调⽤相应的派遣函数来处理该请求。在派遣函数中,驱动程序需要根据
请求的类型和参数来执⾏相应的操作,并根据操作的结果来更新IRP中的状态和返回值。
理解I/O请求的概念和处理⽅式对于驱动程序的开发⾄关重要。驱动程序需要处理各种类型的I/O请
求,例如读取和写⼊⽂件、打开和关闭设备、发送和接收⽹络数据等。

0x02 什么是派遣函数

简单来说,派遣函数就是处理IO请求包IRP的⼀个函数

派遣函数是Windows内核模式驱动程序中的⼀个重要函数,⽤于处理操作系统或⽤⼾空间应⽤程序发
出的系统调⽤或I/O请求。派遣函数接收⼀个IRP(I/O请求包)结构体,该结构体描述了应⽤程序请求
的操作类型和相关参数。然后,派遣函数会根据请求的操作类型和参数来执⾏相应的内核代码,完成
请求的操作。在编写驱动程序时,开发⼈员必须为每个操作类型编写⼀个派遣函数。

下面是⼀个简单的派遣函数示例:

NTSTATUS DispatchFunction(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) {
    // 根据请求的操作类型和参数执⾏相应的内核代码
    // ...
    // 完成请求的操作后,返回NTSTATUS类型的返回值
    return STATUS_SUCCESS;
}

在这个示例中,DispatchFunction 是⼀个派遣函数,它接收⼀个 PDEVICE_OBJECT 类型的设备对象
和⼀个 PIRP类型的请求结构体作为输⼊参数。开发⼈员需要根据请求的操作类型和参数来编写相应的内核代码,最后返回⼀个 NTSTATUS 类型的返回值。在驱动程序中,需要根据需求编写⾃⼰的派遣函数。例如,如果驱动程序需要⽀持⽂件操作,需要编写读取、写⼊和关闭⽂件的派遣函数。如果驱动程序需要⽀持⽹络操作,需要编写接收和发送⽹络数据的派遣函数。在编写派遣函数时,您要参考Windows内核开发⽂档和相关⽰例代码,确保代码符合内核编程的最佳实践。

0x03 编写派遣函数

我们基于之前helloworld的驱动编写。
这⾥编写了⼀个只有打印内容的派遣函数。

NTSTATUS MyCreate(PDEVICE_OBJECT pDeviceObject, PIRP pIrp){
    // 告诉编译器pDeviceObject参数当前未使⽤,防⽌编译器报警
    UNREFERENCED_PARAMETER(pDeviceObject);
    // 打印⽇志,记录创建请求的到来
    DbgPrint("Create request received!\n");
    // 设置IRP的返回状态为STATUS_SUCCESS
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    // 设置IRP的返回信息为0,表⽰没有任何返回信息
    pIrp->IoStatus.Information = 0;
    // 完成IRP的处理,将其返回给调⽤者
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    // 返回操作状态,这⾥设置为STATUS_SUCCESS表⽰操作成功
    return STATUS_SUCCESS;
}

⼀开始不知道NTSTATUS是⼀个什么类型,所以做了⼀下参考:

NTSTATUS是Windows内核中⼴泛使⽤的⼀种数据类型,表⽰操作的执⾏状态。NTSTATUS类型定义在ntstatus.h头⽂件中,并且其值是⼀个32位的⽆符号整数。NTSTATUS值的⾼16位表⽰操作的类型,例如成功、错误、信息等。低16位是错误码,它可以提供有关操作失败的详细信息。
每个NTSTATUS值都可以使⽤NTSTATUS_FROM_WIN32或HRESULT_FROM_NT等宏将其转换为WIN32或COM错误 代码。在内核模式编程中,驱动程序通常需要在执⾏操作时返回NTSTATUS类型的值,以告知操作的执⾏状态。例如,当驱动程序成功处理I/O请求时,它将返回STATUS_SUCCESS值,表⽰操作成功完成。如果操作失败,则驱动程序需要返回适当的NTSTATUS值,以便操作系统或其他驱动程序可以根据该值来识别和处理错误。在编写驱动程序时,开发⼈员需要仔细阅读Windows内核编程⽂档,了解NTSTATUS类型的各种取值及其含义,并在驱动程序中正确处理和返回NTSTATUS类型的值,以确保驱动程序的正确性和可靠性。

要注意,要在DriverEntry这⾥注册派遣函数

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING reg_path){
    NTSTATUS status;
    PDEVICE_OBJECT pDeviceObject;
    status = IoCreateDevice(pDriverObject, 0, NULL, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN,
    if (!NT_SUCCESS(status)){
    DbgPrint("Failed to create device object (0x%08X)\n", status);
    return status;
    }
    pDriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreate;
    DbgPrint("ok,your DispatchFunction is here!!!\n");
    if(NULL != pDriverObject){
    pDriverObject->DriverUnload = DriverUnload;
    }
    return STATUS_SUCCESS;
}

并且最后要卸载函数,否则会无法再次加载。像这样:
image
所以要在卸载函数中添加代码:

VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
{
    DbgPrint(" the driver unloaded successfully! \n");
    IoDeleteDevice(pDriverObject->DeviceObject);
    DbgPrint("the driver deleted successfully! \n");
    pDriverObject->MajorFunction[IRP_MJ_CREATE] = NULL;
}

总的代码就是(保存为dpf.c):

#include <ntddk.h>
#include <wdm.h>

// unload my driver
VOID DriverUnload(PDRIVER_OBJECT pDriverObject) 
{
    DbgPrint(" the driver unloaded successfully! \n");
    IoDeleteDevice(pDriverObject->DeviceObject);
    DbgPrint("the driver deleted successfully! \n");
    pDriverObject->MajorFunction[IRP_MJ_CREATE] = NULL;
}

NTSTATUS MyCreate(PDEVICE_OBJECT pDeviceObject, PIRP pIrp){
    // 告诉编译器pDeviceObject参数当前未使用,防止编译器报警
    UNREFERENCED_PARAMETER(pDeviceObject);
     // 打印日志,记录创建请求的到来
    DbgPrint("Create request received!\n");
     // 设置IRP的返回状态为STATUS_SUCCESS
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    // 设置IRP的返回信息为0,表示没有任何返回信息
    pIrp->IoStatus.Information = 0;
    // 完成IRP的处理,将其返回给调用者
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);

    // 返回操作状态,这里设置为STATUS_SUCCESS表示操作成功
    return STATUS_SUCCESS;
}

// the entry of driver
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING reg_path) 
{
    NTSTATUS status;
    PDEVICE_OBJECT pDeviceObject;
    UNICODE_STRING devName;
    RtlInitUnicodeString(&devName, L"\\Device\\N1nEmA1");
    status = IoCreateDevice(pDriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pDeviceObject);
    pDeviceObject -> Flags |= DO_BUFFERED_IO;
    if (!NT_SUCCESS(status)){
        DbgPrint("Failed to create device object (0x%08X)\n", status);
        return status;
    }
    pDriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreate;
    DbgPrint("ok,your DispatchFunction is here!!!\n");

    if(NULL != pDriverObject){
        pDriverObject->DriverUnload = DriverUnload;
    }

    return STATUS_SUCCESS;
}

0x04 配置⽂件
Sources:

#下边这行指定生成驱动名字HelloWorld.sys
TARGETNAME=DPFandOBJ
#下边这行指定生成文件的类型DRIVER指驱动
TARGETTYPE=DRIVER
#下边这行指定生成驱动所在的路径\SYS\xxx.sys
TARGETPATH=SYS
#下边这行指定相关头文件所在目录路径
INCLUDES=$(BASEDIR)\inc;\
      $(BASEDIR)\inc\wxp;\ 

##上边必空一行H:\WINDDK3790(DDK目录) 等价$(BASEDIR)
#下边这行指定驱动源代码*.cpp或者*.c
SOURCES=DPF.c\

Makefile.txt

!INCLUDE $(NTMAKEENV)\makefile.def

然后build和签名。
image

0x05 运行

由于暂时还没有传输IRP包,派遣函数暂时没有打印。三种传输方式我们会在后面探讨。
image

2.驱动对象及设备对象

目标

理解驱动对象及设备对象的基本定义,使⽤相关⼯具观察驱动对象及设备对象

0x01 基本定义

当我们编写 Windows 驱动程序时,驱动对象和设备对象是⾮常重要的概念。驱动对象代表整个驱动程序,
⽽设备对象则代表驱动程序所管理的⼀个设备(⼀个驱动对象⾥可以有很多设备对象)。下⾯简单介绍⼀下
这两个对象的基本定义:
驱动对象:

驱动对象是⼀个由操作系统内核为驱动程序所创建的数据结构,它代表整个驱动程序。它包含了驱动
程序所需要的⼀些信息,例如驱动程序⼊⼝点函数地址、设备对象列表、驱动程序所使⽤的资源等。
在驱动程序中,我们可以通过定义和操作驱动对象来实现对整个驱动程序的管理和控制。

设备对象:

设备对象则是⼀个由驱动程序所创建的数据结构,它代表驱动程序所管理的⼀个设备。设备对象包含
了设备的⼀些属性信息,例如设备类型、设备特征等。此外,设备对象还包含了⼀些操作函数指针,
⽤于处理各种设备请求,例如读、写、控制等。在驱动程序中,我们可以通过定义和操作设备对象来 实现对设备的管理和控制。

考虑到暂时⽤不到具体的结构体,我们等需要的时候再去查看,所以暂时不写进这⾥。

0x02 观察的准备

代码

为了观察,我们在派遣函数代码的DriverEntry中加上设备命名。改为:

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING reg_path) 
{
    NTSTATUS status;
    PDEVICE_OBJECT pDeviceObject;
    UNICODE_STRING devName;
    RtlInitUnicodeString(&devName, L"\\Device\\N1nEmA1");
    status = IoCreateDevice(pDriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pDeviceObject);
    pDeviceObject -> Flags |= DO_BUFFERED_IO;
    if (!NT_SUCCESS(status)){
        DbgPrint("Failed to create device object (0x%08X)\n", status);
        return status;
    }
    pDriverObject->MajorFunction[IRP_MJ_CREATE] = MyCreate;
    DbgPrint("ok,your DispatchFunction is here!!!\n");

    if(NULL != pDriverObject){
        pDriverObject->DriverUnload = DriverUnload;
    }

    return STATUS_SUCCESS;
}

代码中使⽤了IoCreateDevice函数来创建设备对象,并且在创建设备对象时会返回该设备对象的指针pDeviceObject。在创建设备对象时,可以通过传递⼀个设备名称的UNICODE_STRING结构体指针来为设备
对象命名,例如:

UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\N1nEmA1);
status = IoCreateDevice(pDriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN

这⾥将设备名称设置为N1nEmA1。

其中:RtlInitUnicodeString是⼀个Windows内核函数,它的作⽤是将Unicode字符串初始化,并将其
指针和⻓度存储在⼀个UNICODE_STRING结构中。

通过调⽤RtlInitUnicodeString函数,可以方便地将⼀个Unicode字符串初始化为⼀个 UNICODE_STRING结构。这个结构中包含了Unicode字符串的指针和长度等信息,可以在驱动开发中⽤于处理注册表路径、设备名、驱动名等Unicode字符串的操作。

配置

image
修改后改了下驱动名,意思是派遣函数和设备对象。

0x03 观察

在 Windows 操作系统中,我们可以使⽤⼀些⼯具来观察驱动对象和设备对象。之前,我们使⽤ DebugView
⼯具来查看驱动程序中的调试输出信息。这次,我们则使⽤ WinObj ⼯具来查看系统对象,包括驱动对象
和设备对象等。
记得⽤管理员权限打开Winobj\~
image
image
在Driver中观察到之前的HelloWorld驱动对象,以及这次的DPFandOBJ对象
image
在device观察到了我们刚才创立的N1nEmA1设备

2.5 细碎知识栈

本栏⽬⽤来写⼀些细碎的⼩知识,在这些任务中会⽤到的,可能对某些师傅来说是常识的东西…

0x01 什么是API

什么是API?什么是API函数?

简单来说,就是⽤来给不同程序之间通信交互⽤的。

API是Application Programming Interface的缩写,全称为应⽤程序编程接⼝。它是⼀组预定义的函
数、协议、⼯具等,⽤于不同应⽤程序之间进⾏通信和交互。API提供了⼀种标准的、可靠的⽅式,使
得应⽤程序之间可以相互调⽤和共享数据,从⽽实现更加复杂和强⼤的功能。API通常被⽤于编写软件
库、操作系统、应⽤程序和Web服务等,可以⼤⼤简化程序设计和开发的过程。

哪⾥是驱动程序的API函数?

派遣函数应该就是⼀种API函数。

驱动程序的API函数通常在驱动程序的源代码中实现。在Windows驱动程序开发中,API函数通常通过
驱动程序的设备对象来访问。设备对象的MajorFunction数组定义了驱动程序⽀持的各种IRP处理函
数,这些IRP处理函数即为驱动程序的API函数。应⽤程序可以通过发送IRP请求到设备对象来调⽤这些 API函数。

0x02 为什么学win7的驱动

即使现在已经使⽤的是 Windows 11 操作系统,但是了解 Windows 7 驱动开发也是很有⽤的。因为许多基
础的驱动开发概念和技术在 Windows 操作系统中是通用的。此外,某些老的驱动程序可能仍然在使⽤,并
且可能需要对其进⾏维护和更新。因此,掌握旧版本的驱动开发技术和概念也是很有⽤的。当你有了更多的
驱动开发经验后,学习新版本的驱动开发也会更加容易

3.三种不同的数据传输机制

目标

理解设备对象属性中三种不同的数据传输机制,在某个设备对象中赋予不同的传输机制定义后,在应⽤层编 写程序与驱动通信,尝试访问⽬标设备对象,展⽰不同的传输机制下的本质区别 。我们今天只进行理解。

0x01 定义和理解

设备对象属性中的三种不同的数据传输机制分别是缓冲区传输机制、直接IO传输机制和内存映射传输机制

传 输 机 制 具体定义和理解
缓冲区传输机制 应⽤程序通过调⽤驱动程序的API函数向驱动程序传递数据时,数据会⾸先被复制到⼀个内核缓 冲区中,然后再由驱动程序把数据从内核缓冲区复制到设备缓冲区中。这种传输机制⽐较简单安 全,但是需要额外的内存开销。 而且复制的过程会导致效率低。
直接IO传输机制 应⽤程序直接访问设备缓冲区,不需要借助内核缓冲区的中转。这种传输机制的好处是效率⾼, 但是需要处理并发访问的问题。
内存映射传输机制 应⽤程序访问的是设备缓冲区的虚拟内存地址,不需要进⾏物理内存到虚拟内存的复制。这种传 输机制不需要内存开销,但是需要处理并发访问的问题。

在缓冲区传输机制中,每个进程都会拥有自己的内核缓冲区,也就是说每个进程都有自己的一块内存用于与设备进行数据传输。这里的“自己的”指的是每个进程独立使用自己的内存区域,而不会与其他进程共享缓冲区。

相比之下,对于直接I/O和内存映射传输机制,每个进程并不能拥有自己独立的缓冲区,所有进程都会共享同一块内存区域,因此需要进行同步处理以避免并发访问问题。

传 输 机 制 具体定义和理解
直接IO传输机制 使⽤同步机制(如互斥量、信号量等)来保证并发访问的正确性
内存映射传输机制 使⽤内存映射⽂件对象的锁机制来保证并发访问的正确性

具体的过程和原理我们将在后⾯继续探讨。

尾声

有什么问题欢迎在评论区留言,共同探讨\~
ps:本文因为不小心清理了md文件,是从pdf中重新复制过来排版的。所以观赏性略有损失,还望包涵……(我也很愁)

免费评分

参与人数 3吾爱币 +9 热心值 +3 收起 理由
willJ + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
Caob997 + 1 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

xiexiaoxi 发表于 2023-4-21 19:30
感谢分享,学习学习。
ytfrdfiw 发表于 2023-4-21 21:48
ldw471427015 发表于 2023-4-22 14:31
 楼主| N1nEmAn 发表于 2023-4-23 11:17
ldw471427015 发表于 2023-4-22 14:31
让我回想起当年学内核的日子

哈哈哈哈哈哈哈哈哈哈大佬
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-31 02:04

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表