R3和R0的交互(NT框架的使用)

 
验证环境/使用工具: visual studio 2019、Windows XP、winobj、Dbjview
验证目标: 让R3和R0能通过读写交换数据,也就是驱动和R3做交互过程(NT框架包含了这个交互)

 

内核API(极简进化史):

  1. 系统 => 抽象驱动 => 硬件
  2. 抽象驱动是什么?
    1). 简明:驱动框架 => 读、写操作 => 硬件
    2).详细(进一步思考:文件的抽象,操作文件):驱动框架 => 文件(打开、读取、写入、控制、查询信息、关闭)
  3. 下发配置: 应用程序 => ReadFile => 系统 => 驱动::读取 => 厂家驱动 => 硬件
  4. 获取信息: 应用程序 <= ReadFile <= 系统 <= 驱动::读取 <= 厂家驱动 <= 硬件

 

糟糕,出师不利,出现了一点点点的问题: 整理代码之后,不知道为什么,vs2019中driverentry 的结构体死活打不开,要看里面的成员啊。
解决方案: 经过一番设置之后,还是不行,重新创建个项目试下,果然还得是重新创建项目,项目正常了。果然还得是老话:“重装系统能够解决99.9%的问题”

 

两步走计划:

  1. 注册派遣函数
  2. 驱动绑定到 "设备",来调用驱动
  3. 完成R3和R0的数据交互

 

0x01 注册派遣函数

sample.h

#pragma once

#include <ntddk.h>

VOID Unload(__in struct _DRIVER_OBJECT* DriverObject);
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT     DriverObject, PUNICODE_STRING    RegistryPath);

NTSTATUS DispatchCreat(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp);
NTSTATUS DispatchClose(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp);
NTSTATUS DispatchRead(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp);
NTSTATUS DispatchWrite(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp);
NTSTATUS DispatchControl(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp);

/*
IRP_MJ_DEVICE_CONTROL: 读写可以控制摄像头的打开关闭,控制则可以控制其他的,如:摄像头的旋转...[除了硬件厂商,其他人可不知道怎么让摄像头旋转]
是用来写不通用的功能; 读写的参数需要传一个缓冲区和一个长度,而control的参数,微软不知道传什么参数
control就是要传一些只有硬件厂家才能解析的数据

显卡(NVIDIA)的驱动安装之后,R3上有界面,设置显卡的一些特性,会调用DispatchControl这个函数,这里可以做一些硬件独有的功能;

有一种软件可以拿到硬件的各种信息:有没有可能把control的参数逆清楚,模仿参数发送数据,这样就可以拿到硬件的各种信息了
前几天在闲鱼买了太Macmini主机,最担心的就是有人通过这种方式来修改主机里的信息,如:8+256的主机刷成32+2t的,买家需要反复擦亮眼睛奥;

*/

sample.cpp

#include "sample.h"

#include <ntddk.h>
VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);
    DbgPrint("51[asm] Unload");
}

/*
    驱动对象: 微软提供一个结构体,这个结构体里保存所有的驱动信息
    入口函数第一个参数  PDRIVER_OBJECT  驱动对象,保存了结构体里的所有信息;
*/
NTSTATUS DriverEntry(
    PDRIVER_OBJECT     DriverObject,   /* PDRIVER_OBJECT 这个结构体保存了驱动的所有信息 */
    PUNICODE_STRING    RegistryPath)
{
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);

    NTSTATUS status = STATUS_SUCCESS;

    /* 注册派遣函数 */
    DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreat;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
    DriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchWrite;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchControl;

    DriverObject->DriverUnload = Unload;
    DbgPrint("51[asm] hello word! DriverEntry:%p,Unload:%p,&status:%p", DriverEntry, Unload, &status);

    return status;
}

NTSTATUS DispatchCreat(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchClose(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchRead(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchWrite(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchControl(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);
    return STATUS_SUCCESS;
}

 
 

0x02 驱动绑定到 "设备"

2.1 绑定步骤

驱动绑定到 "设备",通过"设备"来调用驱动
注意:
1) 驱动什么时候被调用?被谁调用?
那肯定是被设备调用,驱动绑定到哪个设备上?
微软抽象了 "设备" 的概念,然后把驱动绑定到设备上,"设备"被操作了,驱动就被调用了。
例如:可以绑定到键盘上,键盘驱动,键盘被操作的时候,驱动中的派遣函数就被调用了

 

2) 如果没有设备,OS拒绝通信;
OS考虑到内核驱动的问题,允许创建一个虚拟设备(提供结构体 DEVICE_OBJECT, 用来描述设备信息)
内核驱动和硬件驱动区别:
硬件设备驱动绑定的是硬件设备对象;
内核驱动绑定的是虚拟设备;

 

3) 内核提供的api叫创建设备;
OS在内核中创建 I/O 管理器(R3对硬件操作都发给IO管理器) =》 I/O 管理器就 根据设备找驱动来调用
驱动的交互由 I/O 管理器 负责;也包括派遣函数,也是 I/O 管理器调用;
那么,创建一个设备,也需要 I/O 管理器中创建;

 

4) 创建步骤: 官方文档中描述的添加设备的流程,按照这个流程来操作

file

2.2 实验验证

sample.cpp

#include "sample.h"

#include <ntddk.h>
VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);

    /* 驱动对象和设备对象绑定之后,设备对象会被记录到这里 DriverObject->DeviceObject; 需要判断对象是否绑定成功*/
    if (DriverObject->DeviceObject != NULL)
    {
        IoDeleteDevice(DriverObject->DeviceObject);
        DbgPrint("51[asm][%s:%d] IoDeleteDevice OK!",__FUNCTION__,__LINE__);
    }

    DbgPrint("51[asm][%s:%d] Unload", __FUNCTION__, __LINE__);
}

NTSTATUS DriverEntry(
    PDRIVER_OBJECT     DriverObject,   /* PDRIVER_OBJECT 这个结构体保存了驱动的所有信息 */
    PUNICODE_STRING    RegistryPath)
{
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);

    NTSTATUS Status = STATUS_SUCCESS;

    /* 注册派遣函数 */
    DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreat;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
    DriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchWrite;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchControl;

    /* rtl: run time library  C库(运行库)  微软重新做的C库,这个库更加安全; 这里做字符串使用的初始化 */
    UNICODE_STRING userDevName;
    RtlInitUnicodeString(&userDevName, DEVICE_NAME);

    /* 绑定设备 */
    PDEVICE_OBJECT pDeviceObj = NULL;
    Status = IoCreateDevice(DriverObject, 0, &userDevName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN | FILE_READ_ONLY_DEVICE | FILE_WRITE_ONCE_MEDIA, FALSE, &pDeviceObj);
    if (!NT_SUCCESS(Status))  /* 函数返回值统一是Status,这里安全性检查 */
    {
        DbgPrint("51[asm][%s:%d] IoCreateDevice err status:%p", __FUNCTION__, __LINE__, Status);
        DriverObject->DriverUnload = Unload; /* 卸载函数中一定要删除设备,否则卸载后无法再次启动,提示设备名已存在 */
        return Status;
    }

    /*
    PDEVICE_OBJECT pDeviceObj = NULL;  //  结构体里的 PVOID DeviceExtension;  可以申请空间大小
    IoCreateDevice(
            IN PDRIVER_OBJECT  DriverObject,      // 驱动对象
            IN ULONG  DeviceExtensionSize,        // 设备扩展: 微软定义了一个结构体 PDEVICE_OBJECT,如果硬件希望在结构体中增加一些成员,这里需要写成员的大小,OS会申请一段内存空间,那就就可以无限的给这个结构体增加成员;这里是额外申请一些空间
        IN PUNICODE_STRING  DeviceName  OPTIONAL, // 给设备名字是必须唯一的,注册的名字和其他的冲突了,会创建失败;可以给个NULL,微软会随机一个名字;  文档中说明设备名必须是有格式的: \Device\DeviceName
                                                  // 注意: PUNICODE_STRING  这里的字符串Unicode字符串,字符串需要经过处理
            IN DEVICE_TYPE  DeviceType,           // 选择未知设备就行,但是一定得给,否则无法通讯  FILE_DEVICE_UNKNOWN 
            IN ULONG  DeviceCharacteristics,      // 权限
            IN BOOLEAN  Exclusive,                // 关键参数,是否独占的方式,  独占: 只允许一个人打开这个设备  (杀毒软件会独占)
            OUT PDEVICE_OBJECT * DeviceObject
        );

    */
    DriverObject->DriverUnload = Unload;
    DbgPrint("51[asm][%s:%d] hello word! DriverEntry:%p,Unload:%p,&status:%p,pDeviceObj:%p", __FUNCTION__, __LINE__, DriverEntry, Unload, &Status,&pDeviceObj);

    return Status;
}

NTSTATUS DispatchCreat(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchClose(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchRead(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchWrite(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchControl(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);
    return STATUS_SUCCESS;
}

2.3 结果分析

2.3.1 安装和卸载打印没问题

file

 

2.3.2 在winobj 中可以查看自己写的"设备"

file

 

2.3.3 卸载能够卸载干净(卸载后,发现找不到这个驱动,卸载完成)

file

 

2.4 注意

需要做另外一个事情:
回调函数来了不能直接返回真,比如:R3向键盘驱动要按键,但是按键没有按下,怎么给?
但是R3还在等,这时的OS的IO管理器会把R3的线程挂起,但是就感受到软件卡死,调一个api之后回不来了;
注意:这里返回success,代表操作是成功的,不代表操作完成了(类似数据读写完成)
这里必须告诉IO管理器,请求是否结束;

NTSTATUS DispatchControl(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    /*
        1. 这个函数表示 请求是否结束
        2. 第一个参数:Irp(I/O Request Packet) 把所有的请求参数封装到这个结构体了
        3. 第二个参数,让挂起的线程快速恢复,把线程的优先级往上提;
        4. 当前IO_NO_INCREMENT: 不要提升优先级,什时候唤醒就什么时候结束
        5. 除了入口函数和卸载函数外,其他全部函数都是一样的,要这样处理;
    */
    IoCompleteRequest(Irp,IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

 

0x03 创建R3进行通信

3.1 实验验证

3.1.1 创建一个控制台项目,R3让内核做的事情,发给驱动,让驱动来做,这样就形成了一个内核程序,继续和驱动通信,直接操作这个"设备"

 

3.1.2
Ring3.cpp

#include <stdio.h>
#include <windows.h>

int main()
{
    // 打开设备, 这里文件的路径就是在驱动中创建的设备名字
    HANDLE  hFile = CreateFile("\\Device\\51asmMark", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    printf("hFile:%p err:%p\n", hFile, GetLastError());

    // 读写设备
    // 控制设备
    // 关闭设备
    system("pause");
    return 0;
}

 
3.1.3
注意: 把这个运行设置为静态库,因为虚拟机里没有静态库;

file

 

3.1.4
注意:用xp的版本,设置一下版本兼容

file

3.2 结果分析

3.2.1
验证程序是否正常执行,文件打开失败,产生3号错误…;

file

 

3.2.2
找不到指定的路径,是路径的问题嘛? 第一个参数是使用的驱动里的名字,并不是R3的名字,大逆不道,没有权限

file

3.3 符号链接

3.3.1
分析问题
驱动不存在“路径”的说法,为什么打不开? 没有权限
硬件的驱动设备决定着是否给3环提供功能,而现在驱动程序只允许OS操作,R3是不允许操作的,R3没权限啊;
那么,如果让驱动对R3公开,必须为R3创造一个名字

 

3.3.2
举例: 计算机中的"C:"盘
操作这个 "C:" 就相当于操作了硬盘,称为 符号链接
OS为硬盘创造的名字是固定的, 这个是硬盘在驱动中的名字,只有内核才能操作这个名字;

file

 

3.3.3
让R3操作这个设备,必须得有个盘符;把盘符映射到这个驱动设备上,这个就是链接 =》 符号链接
“C:" 是符号,操作的过程叫链接,所以是符号链接;

file

 

3.4 实验验证,更新驱动和R3,产生符号链接

3.4.1 =》 R3
Ring3.cpp

#include <stdio.h>
#include <windows.h>

int main()
{
    // 打开设备, 这里文件的路径就是在驱动中创建的设备名字
    HANDLE  hFile = CreateFile("\\\\.\\\\51asmMark", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    printf("hFile:%p err:%p\n", hFile, GetLastError());

    // 读写设备
    // 控制设备
    // 关闭设备
    system("pause");
    return 0;
}

 
3.4.2 驱动

sample.cpp

#include "sample.h"

VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);

    /* 驱动对象和设备对象绑定之后,设备对象会被记录到这里 DriverObject->DeviceObject; 需要判断对象是否绑定成功*/
    if (DriverObject->DeviceObject != NULL)
    {
        IoDeleteDevice(DriverObject->DeviceObject);
        DbgPrint("51[asm][%s:%d] IoDeleteDevice OK!",__FUNCTION__,__LINE__);
    }

    DbgPrint("51[asm][%s:%d] Unload", __FUNCTION__, __LINE__);
}

NTSTATUS DriverEntry(
    PDRIVER_OBJECT     DriverObject,   /* PDRIVER_OBJECT 这个结构体保存了驱动的所有信息 */
    PUNICODE_STRING    RegistryPath)
{
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);

    NTSTATUS Status = STATUS_SUCCESS;

    /* 注册派遣函数 */
    DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreat;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
    DriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchWrite;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchControl;

    /* rtl: run time library  C库(运行库)  微软重新做的C库,这个库更加安全; 这里做字符串使用的初始化 */
    UNICODE_STRING userDevName;
    RtlInitUnicodeString(&userDevName, DEVICE_NAME);

    /* 绑定设备 */
    PDEVICE_OBJECT pDeviceObj = NULL;
    Status = IoCreateDevice(DriverObject, 0, &userDevName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN | FILE_READ_ONLY_DEVICE | FILE_WRITE_ONCE_MEDIA, FALSE, &pDeviceObj);
    if (!NT_SUCCESS(Status))  /* 函数返回值统一是Status,这里安全性检查 */
    {
        DbgPrint("51[asm][%s:%d] IoCreateDevice err status:%p", __FUNCTION__, __LINE__, Status);
        DriverObject->DriverUnload = Unload; /* 卸载函数中一定要删除设备,否则卸载后无法再次启动,提示设备名已存在 */
        return Status;
    }
    /*  IoCreateDevice 参数填写注意事项:
    PDEVICE_OBJECT pDeviceObj = NULL;  //  结构体里的 PVOID DeviceExtension;  可以申请空间大小
    IoCreateDevice(
            IN PDRIVER_OBJECT  DriverObject,      // 驱动对象
            IN ULONG  DeviceExtensionSize,        // 设备扩展: 微软定义了一个结构体 PDEVICE_OBJECT,如果硬件希望在结构体中增加一些成员,这里需要写成员的大小,OS会申请一段内存空间,那就就可以无限的给这个结构体增加成员;这里是额外申请一些空间
        IN PUNICODE_STRING  DeviceName  OPTIONAL, // 给设备名字是必须唯一的,注册的名字和其他的冲突了,会创建失败;可以给个NULL,微软会随机一个名字;  文档中说明设备名必须是有格式的: \Device\DeviceName
                                                  // 注意: PUNICODE_STRING  这里的字符串Unicode字符串,字符串需要经过处理
            IN DEVICE_TYPE  DeviceType,           // 选择为止设备就行,但是一定得给,否则无法通讯  FILE_DEVICE_UNKNOWN
            IN ULONG  DeviceCharacteristics,      // 权限
            IN BOOLEAN  Exclusive,                // 关键参数,是否独占的方式,  独占: 只允许一个人打开这个设备  (杀毒软件会独占)
            OUT PDEVICE_OBJECT * DeviceObject
        );

    */

    /* 创建符号链接 */
    UNICODE_STRING strDosDeviceName;
    RtlInitUnicodeString(&strDosDeviceName, SYMBOL_NAME);
    Status = IoCreateSymbolicLink(&strDosDeviceName, &userDevName); /* R3使用的名字在文档中选:MS-DOS Device Names     R0使用的名字在文档中选:NT Device Names  */
    if (!NT_SUCCESS(Status))  
    {
        /* 驱动对象和设备对象绑定之后,设备对象会被记录到这里 DriverObject->DeviceObject; 需要判断对象是否绑定成功*/
        if (DriverObject->DeviceObject != NULL)
        {
            IoDeleteDevice(DriverObject->DeviceObject);
            IoDeleteSymbolicLink(&strDosDeviceName);
            DbgPrint("51[asm][%s:%d] IoCreateSymbolicLink err!", __FUNCTION__, __LINE__);
        }

        return Status;
    }

    DriverObject->DriverUnload = Unload;
    DbgPrint("51[asm][%s:%d] hello word! DriverEntry:%p,Unload:%p,&status:%p,pDeviceObj:%p", __FUNCTION__, __LINE__, DriverEntry, Unload, &Status,&pDeviceObj);

    return Status;
}

NTSTATUS DispatchCreat(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchClose(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchRead(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchWrite(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchControl(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    /*
        1. 这个函数表示 请求是否结束
        2. 第一个参数:Irp(I/O Request Packet) 把所有的请求参数疯转到这个结构体了
        3. 第二个参数,让挂起的线程快速恢复,把线程的优先级往上提;
        4. 当前IO_NO_INCREMENT: 不要提升优先级,什时候唤醒就什么时候结束
        5. 除了入口函数和卸载函数外,其他全部函数都是一样的,要这样处理;
    */
    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);
    IoCompleteRequest(Irp,IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

 

3.5 结果分析

出问题了:
第一次安装驱动成功,操作R3成功调用了回调函数
卸载驱动,这个卸载的函数没有调用,卸载打印输出内容不对; 第二次安装驱动失败!!!
猜想:可能是因为符号没有删除,符号名字重复了;

file

 

3.5.1 解决问题
解决驱动卸载后无法安装的问题: 在Unload中删除符号链接
sample.cpp

#include "sample.h"

char g_zMsg[] = "hello"; // 代表OS的任何数据

VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);

    /*  删除符号链接 */
    UNICODE_STRING strDosDeviceName;
    RtlInitUnicodeString(&strDosDeviceName, SYMBOL_NAME);
    IoDeleteSymbolicLink(&strDosDeviceName);

    /* 驱动对象和设备对象绑定之后,设备对象会被记录到这里 DriverObject->DeviceObject; 需要判断对象是否绑定成功*/
    if (DriverObject->DeviceObject != NULL)
    {
        IoDeleteDevice(DriverObject->DeviceObject);
        DbgPrint("51[asm][%s:%d] IoDeleteDevice OK!",__FUNCTION__,__LINE__);
    }

    DbgPrint("51[asm][%s:%d] Unload", __FUNCTION__, __LINE__);
}

 

3.6 通过R3来调用内核的read函数

Ring3.cpp

#include <stdio.h>
#include <windows.h>

int main()
{
    // 打开设备, 这里文件的路径就是在驱动中创建的设备名字
    HANDLE  hFile = CreateFile("\\\\.\\\\51asmMark", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    printf("hFile:%p err:%p\n", hFile, GetLastError());

    // 读写设备
    char acBuf[60] = { 0 };
    DWORD dwBytes = 0;
    BOOL bRet = ReadFile(hFile, acBuf, sizeof(acBuf), &dwBytes, NULL);
    printf("bRet:%d  dwBytes:%d  acBuf:%s", bRet, dwBytes, acBuf);

    // 控制设备
    // 关闭设备
    system("pause");
    return 0;
}

 

3.7 结果分析

调到了内核驱动里的read了
注意: 返回值成功,但是返回数据是0,因为请求完成了,内核没有向R3拷贝数据

file

 

0x04 R3和R0交互

  1. 驱动中DispatchRead函数里有R3的地址,对比输出验证;
  2. 从驱动中拷贝数据给R3;

4.1 实验验证

Ring3.cpp

#include <stdio.h>
#include <windows.h>

int main()
{
    // 打开设备, 这里文件的路径就是在驱动中创建的设备名字
    HANDLE  hFile = CreateFile("\\\\.\\\\51asmMark", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    printf("hFile:%p err:%p\n", hFile, GetLastError());

    // 读写设备
    char acBuf[60] = { 0 };
    printf("acBuf:%p\n", acBuf);  /* 驱动要输出R3的缓存地址,这里作为对照 */
    DWORD dwBytes = 0;
    BOOL bRet = ReadFile(hFile, acBuf, sizeof(acBuf), &dwBytes, NULL);
    printf("bRet:%d  dwBytes:%d  acBuf:%s\n", bRet, dwBytes, acBuf);

    // 控制设备
    // 关闭设备
    system("pause");
    return 0;
}

sample.cpp

#include "sample.h"

char g_zMsg[] = "hello"; // 代表OS的任何数据

VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);

    /*  删除符号链接 */
    UNICODE_STRING strDosDeviceName;
    RtlInitUnicodeString(&strDosDeviceName, SYMBOL_NAME);
    IoDeleteSymbolicLink(&strDosDeviceName);

    /* 驱动对象和设备对象绑定之后,设备对象会被记录到这里 DriverObject->DeviceObject; 需要判断对象是否绑定成功*/
    if (DriverObject->DeviceObject != NULL)
    {
        IoDeleteDevice(DriverObject->DeviceObject);
        DbgPrint("51[asm][%s:%d] IoDeleteDevice OK!",__FUNCTION__,__LINE__);
    }

    DbgPrint("51[asm][%s:%d] Unload", __FUNCTION__, __LINE__);
}

NTSTATUS DriverEntry(
    PDRIVER_OBJECT     DriverObject,   /* PDRIVER_OBJECT 这个结构体保存了驱动的所有信息 */
    PUNICODE_STRING    RegistryPath)
{
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);

    NTSTATUS Status = STATUS_SUCCESS;

    /* 注册派遣函数 */
    DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreat;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
    DriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchWrite;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchControl;

    /* rtl: run time library  C库(运行库)  微软重新做的C库,这个库更加安全; 这里做字符串使用的初始化 */
    UNICODE_STRING userDevName;
    RtlInitUnicodeString(&userDevName, DEVICE_NAME);

    /* 绑定设备 */
    PDEVICE_OBJECT pDeviceObj = NULL;
    Status = IoCreateDevice(DriverObject, 0, &userDevName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN | FILE_READ_ONLY_DEVICE | FILE_WRITE_ONCE_MEDIA, FALSE, &pDeviceObj);
    if (!NT_SUCCESS(Status))  /* 函数返回值统一是Status,这里安全性检查 */
    {
        DbgPrint("51[asm][%s:%d] IoCreateDevice err status:%p", __FUNCTION__, __LINE__, Status);
        DriverObject->DriverUnload = Unload; /* 卸载函数中一定要删除设备,否则卸载后无法再次启动,提示设备名已存在 */
        return Status;
    }
    /*  IoCreateDevice 参数填写注意事项:
    PDEVICE_OBJECT pDeviceObj = NULL;  //  结构体里的 PVOID DeviceExtension;  可以申请空间大小
    IoCreateDevice(
            IN PDRIVER_OBJECT  DriverObject,      // 驱动对象
            IN ULONG  DeviceExtensionSize,        // 设备扩展: 微软定义了一个结构体 PDEVICE_OBJECT,如果硬件希望在结构体中增加一些成员,这里需要写成员的大小,OS会申请一段内存空间,那就就可以无限的给这个结构体增加成员;这里是额外申请一些空间
        IN PUNICODE_STRING  DeviceName  OPTIONAL, // 给设备名字是必须唯一的,注册的名字和其他的冲突了,会创建失败;可以给个NULL,微软会随机一个名字;  文档中说明设备名必须是有格式的: \Device\DeviceName
                                                  // 注意: PUNICODE_STRING  这里的字符串Unicode字符串,字符串需要经过处理
            IN DEVICE_TYPE  DeviceType,           // 选择为止设备就行,但是一定得给,否则无法通讯  FILE_DEVICE_UNKNOWN
            IN ULONG  DeviceCharacteristics,      // 权限
            IN BOOLEAN  Exclusive,                // 关键参数,是否独占的方式,  独占: 只允许一个人打开这个设备  (杀毒软件会独占)
            OUT PDEVICE_OBJECT * DeviceObject
        );

    */

    /* 创建符号链接 */
    UNICODE_STRING strDosDeviceName;
    RtlInitUnicodeString(&strDosDeviceName, SYMBOL_NAME);
    Status = IoCreateSymbolicLink(&strDosDeviceName, &userDevName); /* R3使用的名字在文档中选:MS-DOS Device Names     R0使用的名字在文档中选:NT Device Names  */
    if (!NT_SUCCESS(Status))  
    {
        /* 驱动对象和设备对象绑定之后,设备对象会被记录到这里 DriverObject->DeviceObject; 需要判断对象是否绑定成功*/
        if (DriverObject->DeviceObject != NULL)
        {
            /*  删除符号链接 */
            IoDeleteSymbolicLink(&strDosDeviceName);

            IoDeleteDevice(DriverObject->DeviceObject);
            DbgPrint("51[asm][%s:%d] IoCreateSymbolicLink err!", __FUNCTION__, __LINE__);
        }

        return Status;
    }

    DriverObject->DriverUnload = Unload;
    DbgPrint("51[asm][%s:%d] hello word! DriverEntry:%p,Unload:%p,&status:%p,pDeviceObj:%p", __FUNCTION__, __LINE__, DriverEntry, Unload, &Status,&pDeviceObj);

    return Status;
}

NTSTATUS DispatchCreat(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchClose(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchRead(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    /* 验证这里是R3的缓存地址,输出一下 */
    DbgPrint("51[asm][%s:%d] Irp->UserBuffer:%p", __FUNCTION__, __LINE__, Irp->UserBuffer); 

    RtlCopyMemory(Irp->UserBuffer,g_zMsg,sizeof(g_zMsg));

    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchWrite(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchControl(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    /*
        1. 这个函数表示 请求是否结束
        2. 第一个参数:Irp(I/O Request Packet) 把所有的请求参数疯转到这个结构体了
        3. 第二个参数,让挂起的线程快速恢复,把线程的优先级往上提;
        4. 当前IO_NO_INCREMENT: 不要提升优先级,什时候唤醒就什么时候结束
        5. 除了入口函数和卸载函数外,其他全部函数都是一样的,要这样处理;
    */
    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);
    IoCompleteRequest(Irp,IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

 

4.2 结果分析

结果读取到了数据,但是读取的字节数位0 ???

file

 

4.3 读取R0的数据

sample.cpp

NTSTATUS DispatchRead(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    /* 验证这里是R3的缓存地址,输出一下 */
    DbgPrint("51[asm][%s:%d] Irp->UserBuffer:%p", __FUNCTION__, __LINE__, Irp->UserBuffer); 

    RtlCopyMemory(Irp->UserBuffer,g_zMsg,sizeof(g_zMsg));

    Irp->IoStatus.Status = STATUS_SUCCESS;        /* 告诉R3返回的状态 */
    Irp->IoStatus.Information = sizeof(g_zMsg);   /* 告诉R3读取的字节数 */

    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

 

4.4 结果

读取到6个字节的数据

file

 
0x04 完整代码记录:
R3:
Ring3.cpp

#include <stdio.h>
#include <windows.h>

int main()
{
    // 打开设备, 这里文件的路径就是在驱动中创建的设备名字
    HANDLE  hFile = CreateFile("\\\\.\\\\51asmMark", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    printf("hFile:%p err:%p\n", hFile, GetLastError());

    // 读写设备
    char acBuf[60] = { 0 };
    printf("acBuf:%p\n", acBuf);  /* 驱动要输出R3的缓存地址,这里作为对照 */
    DWORD dwBytes = 0;
    BOOL bRet = ReadFile(hFile, acBuf, sizeof(acBuf), &dwBytes, NULL);
    printf("bRet:%d  dwBytes:%d  acBuf:%s\n", bRet, dwBytes, acBuf);

    // 控制设备
    // 关闭设备
    system("pause");
    return 0;
}

 

驱动:
sample.cpp

#include "sample.h"

char g_zMsg[] = "hello"; // 代表OS的任何数据

VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
{
    UNREFERENCED_PARAMETER(DriverObject);

    /*  删除符号链接 */
    UNICODE_STRING strDosDeviceName;
    RtlInitUnicodeString(&strDosDeviceName, SYMBOL_NAME);
    IoDeleteSymbolicLink(&strDosDeviceName);

    /* 驱动对象和设备对象绑定之后,设备对象会被记录到这里 DriverObject->DeviceObject; 需要判断对象是否绑定成功*/
    if (DriverObject->DeviceObject != NULL)
    {
        IoDeleteDevice(DriverObject->DeviceObject);
        DbgPrint("51[asm][%s:%d] IoDeleteDevice OK!",__FUNCTION__,__LINE__);
    }

    DbgPrint("51[asm][%s:%d] Unload", __FUNCTION__, __LINE__);
}

NTSTATUS DriverEntry(
    PDRIVER_OBJECT     DriverObject,   /* PDRIVER_OBJECT 这个结构体保存了驱动的所有信息 */
    PUNICODE_STRING    RegistryPath)
{
    UNREFERENCED_PARAMETER(DriverObject);
    UNREFERENCED_PARAMETER(RegistryPath);

    NTSTATUS Status = STATUS_SUCCESS;

    /* 注册派遣函数 */
    DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreat;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
    DriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchWrite;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchControl;

    /* rtl: run time library  C库(运行库)  微软重新做的C库,这个库更加安全; 这里做字符串使用的初始化 */
    UNICODE_STRING userDevName;
    RtlInitUnicodeString(&userDevName, DEVICE_NAME);

    /* 绑定设备 */
    PDEVICE_OBJECT pDeviceObj = NULL;
    Status = IoCreateDevice(DriverObject, 0, &userDevName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN | FILE_READ_ONLY_DEVICE | FILE_WRITE_ONCE_MEDIA, FALSE, &pDeviceObj);
    if (!NT_SUCCESS(Status))  /* 函数返回值统一是Status,这里安全性检查 */
    {
        DbgPrint("51[asm][%s:%d] IoCreateDevice err status:%p", __FUNCTION__, __LINE__, Status);
        DriverObject->DriverUnload = Unload; /* 卸载函数中一定要删除设备,否则卸载后无法再次启动,提示设备名已存在 */
        return Status;
    }
    /*  IoCreateDevice 参数填写注意事项:
    PDEVICE_OBJECT pDeviceObj = NULL;  //  结构体里的 PVOID DeviceExtension;  可以申请空间大小
    IoCreateDevice(
            IN PDRIVER_OBJECT  DriverObject,      // 驱动对象
            IN ULONG  DeviceExtensionSize,        // 设备扩展: 微软定义了一个结构体 PDEVICE_OBJECT,如果硬件希望在结构体中增加一些成员,这里需要写成员的大小,OS会申请一段内存空间,那就就可以无限的给这个结构体增加成员;这里是额外申请一些空间
        IN PUNICODE_STRING  DeviceName  OPTIONAL, // 给设备名字是必须唯一的,注册的名字和其他的冲突了,会创建失败;可以给个NULL,微软会随机一个名字;  文档中说明设备名必须是有格式的: \Device\DeviceName
                                                  // 注意: PUNICODE_STRING  这里的字符串Unicode字符串,字符串需要经过处理
            IN DEVICE_TYPE  DeviceType,           // 选择为止设备就行,但是一定得给,否则无法通讯  FILE_DEVICE_UNKNOWN
            IN ULONG  DeviceCharacteristics,      // 权限
            IN BOOLEAN  Exclusive,                // 关键参数,是否独占的方式,  独占: 只允许一个人打开这个设备  (杀毒软件会独占)
            OUT PDEVICE_OBJECT * DeviceObject
        );

    */

    /* 创建符号链接 */
    UNICODE_STRING strDosDeviceName;
    RtlInitUnicodeString(&strDosDeviceName, SYMBOL_NAME);
    Status = IoCreateSymbolicLink(&strDosDeviceName, &userDevName); /* R3使用的名字在文档中选:MS-DOS Device Names     R0使用的名字在文档中选:NT Device Names  */
    if (!NT_SUCCESS(Status))  
    {
        /* 驱动对象和设备对象绑定之后,设备对象会被记录到这里 DriverObject->DeviceObject; 需要判断对象是否绑定成功*/
        if (DriverObject->DeviceObject != NULL)
        {
            /*  删除符号链接 */
            IoDeleteSymbolicLink(&strDosDeviceName);

            IoDeleteDevice(DriverObject->DeviceObject);
            DbgPrint("51[asm][%s:%d] IoCreateSymbolicLink err!", __FUNCTION__, __LINE__);
        }

        return Status;
    }

    DriverObject->DriverUnload = Unload;
    DbgPrint("51[asm][%s:%d] hello word! DriverEntry:%p,Unload:%p,&status:%p,pDeviceObj:%p", __FUNCTION__, __LINE__, DriverEntry, Unload, &Status,&pDeviceObj);

    return Status;
}

NTSTATUS DispatchCreat(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);

    // 这样返回的数据才是完整的
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchClose(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);

    // 这样返回的数据才是完整的
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchRead(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    /* 验证这里是R3的缓存地址,输出一下 */
    DbgPrint("51[asm][%s:%d] Irp->UserBuffer:%p", __FUNCTION__, __LINE__, Irp->UserBuffer); 

    RtlCopyMemory(Irp->UserBuffer,g_zMsg,sizeof(g_zMsg));

    Irp->IoStatus.Status = STATUS_SUCCESS;        /* 告诉R3返回的状态 */
    Irp->IoStatus.Information = sizeof(g_zMsg);   /* 告诉R3读取的字节数 */

    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;                      /* R3的ReadFile的返回值 */
}

NTSTATUS DispatchWrite(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);
    // 这样返回的数据才是完整的
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

NTSTATUS DispatchControl(_DEVICE_OBJECT* DeviceObject, _IRP* Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
    UNREFERENCED_PARAMETER(Irp);

    /*
        1. 这个函数表示 请求是否结束
        2. 第一个参数:Irp(I/O Request Packet) 把所有的请求参数疯转到这个结构体了
        3. 第二个参数,让挂起的线程快速恢复,把线程的优先级往上提;
        4. 当前IO_NO_INCREMENT: 不要提升优先级,什时候唤醒就什么时候结束
        5. 除了入口函数和卸载函数外,其他全部函数都是一样的,要这样处理;
    */
    DbgPrint("51[asm][%s:%d]", __FUNCTION__, __LINE__);

    // 这样返回的数据才是完整的
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = 0;
    IoCompleteRequest(Irp,IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

 
sample.h

#pragma once

#include <ntddk.h>

#define DEVICE_NAME L"\\Device\\51asmMark"
#define SYMBOL_NAME L"\\DosDevices\\51asmMark"
//#define SYMBOL_NAME L"\\??\\51asmMark"

VOID Unload(__in struct _DRIVER_OBJECT* DriverObject);
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT     DriverObject, PUNICODE_STRING    RegistryPath);

NTSTATUS DispatchCreat(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp);
NTSTATUS DispatchClose(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp);
NTSTATUS DispatchRead(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp);
NTSTATUS DispatchWrite(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp);
NTSTATUS DispatchControl(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp);
/*
IRP_MJ_DEVICE_CONTROL: 读写可以控制摄像头的打开关闭,控制则可以控制其他的,如:摄像头的旋转...[除了硬件厂商,其他人可不知道怎么让摄像头旋转]
是用来写不通用的功能; 读写的参数需要传一个缓冲区和一个长度,而control的参数,微软不知道传什么参数
control就是要传一些只有硬件厂家才能解析的数据

显卡(NVIDIA)的驱动安装之后,R3上有界面,设置显卡的一些特性,会调用DispatchControl这个函数,这里可以做一些硬件独有的功能;

有一种软件可以拿到硬件的各种信息:有没有可能把control的参数逆清楚,模仿参数发送数据,这样就可以拿到硬件的各种信息了
前几天在闲鱼买了太Macmini主机,最担心的就是有人通过这种方式来修改主机里的信息,如:8+256的主机刷成32+2t的,买家需要反复擦亮眼睛奥;

*/
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇