首页
社区
课程
招聘
[原创]minifilter文件过滤驱动--实现查看文本内容替换为指定文本
发表于: 2020-12-23 21:00 15746

[原创]minifilter文件过滤驱动--实现查看文本内容替换为指定文本

2020-12-23 21:00
15746

写这篇文章不仅仅只为介绍功能的实现,也是为了作为一篇笔记来备忘,因此我会写的比较详细。文中的注释有的来自微软官方的解释翻译,有的来自Windows内核安全与驱动开发书中的解释,也有的来自我个人的理解。代码是在Windows内核安全与驱动开发第13章中minifilter代码的基础上完成的(我实现的功能源码中没有)。
吾爱破解那篇同名文章也是我写的,吾爱id:白影33

MyMinifilter.sys:实现根据应用层软件输入,选择是否文本内容替换,过滤readbuffer内容,禁止notepad打开。
MyDriverR3:系统驱动没有交互的界面,通过MyDriverR3与驱动进行交互。

默认情况下:安装驱动后,读取true.txt的内容会被替换为false.txt的内容。(false.txt里全为0,ture.txt全为1)

当在MyDriverR3输入52pojie:读取true.txt,就是ture.txt。
再次启动MyDriverR3输入0:读取true.txt的内容会被替换为false.txt的内容。

大家应该注意到了,要过滤文本内容,为什么要禁止打开notepad,原因是filemap方式(解决办法我发评论),我在实践中,使用debugview查看打印替换或的缓冲区内容,显示替换成false.txt,然而打开notepad却还是true.txt里的内容,我又在网上下了个EditPlus测试,用EditPlus打开查看为false.txt,替换成功,我又下了个notepad++测试,也是替换成功,所以有了这么一个折中的办法,禁止打开notepad,这个很简单对create进行过滤,是打开notepad就让它失败,(评论里我发了一个好点的办法,具体看评论)

这里说明我遇到的难点,以及解决办法。
难点:对目标文件打开后无IRP_MJ_READ的IRP。
解决方案:通过查阅书籍,发现为window文件缓冲机制导致的问题

window文件缓冲机制是:

window读文件会首先从文件缓冲中读取,如果文件缓冲中没有,
就会触发缺页异常从而发送IRP_MJ_READ从硬盘里读取,放入文件缓冲中。

如果文件缓冲中有,那就直接从内存中读取,不会发送IRP_MJ_READ。

对于这个问题书籍里用CcPurgeCacheSection然后和一些函数清除文件缓冲,
不知道是不是因为我的是minifilter而他的是普通的文件过滤驱动的区别,我调用该函数即使返回显示成功,依然没有IRP_MJ_READ,书上的办法对于我来说,行不通。

在一次测试中发现:如果对文件内容进行修改,点击保存就会有IRP_MJ_READ,
据此猜想,window在每次对原文件的修改保存后,会发送IRP_MJ_READ从硬盘上重新读取内容同步到文件缓冲里,这样我就有了一个大胆的想法。

在DriverEntry里用ZwWriteFile对文件写入空格(注意重入问题),这样就会触发read的irp,我就把false.txt内容替换到文件缓冲,然后在NPMiniMessage与应用层交互的函数里,也对文件进行写入触发read的irp,根据应用层输入的字符串,来判断替换为那个文件的内容

这里是记录自己在写驱动过程中遇到的问题,以及解决办法。

问题1:驱动安装蓝屏,windbg调试后发现FltRegisterFilter注册函数失败
解决办法:minifilter需要修改注册表,我用Windows内核安全与驱动开发的minifilter的inf文件右键安装后,再用drivermonitor安装FltRegisterFilter就返回成功

问题2:minifilter读后操作回调例程 里的PAGED_CODE();断下,PAGED_CODE();检查irql是否低于或等于APC_LEVEL,不成立就中断
解决办法:通过查阅微软官方文档发现minifilter后操作回调例程可以运行在高于APC_LEVEL的irql,不需要用PAGED_CODE来检查。

问题3:FltGetFileNameInformation会不时蓝屏
解决办法:通过对比官方代码,发现一些失败的情况没有直接返回,把而是失败的irp的数据给FltGetFileNameInformation会蓝屏,加上代码
if (!NT_SUCCESS( Data->IoStatus.Status ) || (STATUS_REPARSE == Data->IoStatus.Status))
{
return FLT_POSTOP_FINISHED_PROCESSING;
}
检查,如果失败就直接返回,不调用FltGetFileNameInformation。

问题4:ZwReadFile/ZwWriteFile蓝屏
解决办法: 参数 8_Inopt PLARGE_INTEGER ByteOffset,不能直接给数,
例如:错误ZwReadFile(myhand2, NULL, NULL, NULL, &BLOCK2, str2, 500, 50, NULL);
正确 LARGE_INTEGER size2.QuadPart = 0;
status = ZwReadFile(myhand2, NULL, NULL, NULL, &BLOCK2, str2, 500, &size2, NULL);
(血的教训,蓝了我一下午)

问题5:RtlCopyMemory蓝屏
解决办法:我是在post回调函数中使用这个函数,post函数IRQL高一些,
微软官方的解释的意思是如果在高IRQL使用,原地址和目标地址的内存最好都是非分页内存。

问题6:DriverEntry的ZwReadFile蓝屏,
解决办法:调用ZwReadFile读取目标文件内容用于替换,而我又过滤了read的,read里面又需要用到这个ZwReadFile读取的内容
此时为空,导致蓝屏,解决办法用一个变量,读完后给它赋值,而后在read过滤时检查,如果变量没有赋值,就不过滤。

使用我发的文件里的inf安装驱动,然后用drivermonitor启动,过滤的文件位置默认为c盘,就是c:\,想放在其他位置修改源码里的路径就行,true.txt和false.txt复制到这个路径就行了,当然你也可以自己创建这两个文件,
过滤目标文件也可以通过修改源码里的名字进行修改,
编译环境:vs2013+wdk8.1,测试成功系统:win7x64
所有的文件链接:https://pan.baidu.com/s/1eyOwGI7XoCok1KN00_xPEg
提取码:ras6
解压密码:52pojie

 
 
 
 
 
 
 
 
 
 
 
#include "minifilter.h"
NTSTATUS
DriverEntry(
__in PDRIVER_OBJECT DriverObject,  //PDRIVER_OBJECT 驱动数据结构(驱动对象)的指针
__in PUNICODE_STRING RegistryPath//PUNICODE_STRING 内核字符串数组,驱动以服务的形式加载,这个字符串为驱动在注册表的路径
)
{
        NTSTATUS status;              //返回值状态
        PSECURITY_DESCRIPTOR sd;     //安全描述符。
        OBJECT_ATTRIBUTES oa;        //对象属性
        UNICODE_STRING uniString;                //用于通信端口名称
 
        UNREFERENCED_PARAMETER(RegistryPath);//避免编译器关于未引用参数的警告
 
 
        // 注册FilterRegistration,告诉系统我设置的回调例程
        //1个参数是本驱动的驱动对象,是在入口函数DriverEntry 中作为参数传入的。
        //2个参数就是一个注册信息的结构,这个结构内含描述这个过滤器的所有信息
        //3个参数 是一个返回参数,返回注册成功的微过滤器句柄,在下面调用函数FltStartFiltering时会用到
        //DbgPrint("statuswww=\n");
        status = FltRegisterFilter(DriverObject,
                &FilterRegistration,
                &gFilterHandle);
 
        ASSERT(NT_SUCCESS(status));//检查结果是否成功,失败后触发异常被调试器接管
 
        if (NT_SUCCESS(status)) {//检查结果是否成功,
 
                //
                //  开始函数,只有一个参数为之前获取的句柄
                //
 
                status = FltStartFiltering(gFilterHandle);
 
                if (!NT_SUCCESS(status)) {
                        //如果失败就取消注册,只有一个参数为之前获取的句柄
                        FltUnregisterFilter(gFilterHandle);
                }
        }
 
        //FltBuildDefaultSecurityDescriptor构建一个默认的安全描述符,用于FltCreateCommunicationPort。
        //参数1指向调用方分配的变量的指针,该变量接收到指向新创建的不透明指针
        //参数2指定调用者需要对端口对象的访问类型的标志的位掩码。系统定义的DesiredAccess标志集确定了minifilter驱动程序通信端口对象的以下特定访问权限。
        status = FltBuildDefaultSecurityDescriptor(&sd, FLT_PORT_ALL_ACCESS);
 
        if (!NT_SUCCESS(status)) {//检查结果是否成功,未成功就跳向最终处理部分
                goto final;
        }
 
        //函数初始化Unicode字符的计数字符串
        RtlInitUnicodeString(&uniString, MINISPY_PORT_NAME);
 
        //宏初始化不透明的OBJECT_ATTRIBUTES结构,该结构将对象句柄的属性指定给打开句柄的例程。
        //参数1为要被初始化的结构体,参数2一个指向Unicode字符串的指针,该字符串包含要为其打开句柄的对象的名称。
        //这必须是一个完全限定的对象名,或者由RootDirectory参数指定的对象目录的相对路径名。
        //参数3标志位,此处指定为只在内核模式访问,与对大小写不区分
        //参数4在ObjectName参数中指定的路径名的根对象目录句柄。如果ObjectName是完全限定的对象名称,
        //则RootDirectory为空。使用ZwCreateDirectoryObject获得对象目录的句柄。
        //参数5指定创建对象时应用于该对象的安全描述符。此参数是可选的。驱动程序可以指定NULL来接受对象的默认安全性。
        InitializeObjectAttributes(&oa,
                &uniString,
                OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
                NULL,
                sd);
 
        //创建一个通信服务器端口,minifilter驱动程序可以在该端口上接收来自用户模式应用程序的连接请求。
        // 参数1调用者的不透明过滤器指针
        //参数2指向调用方分配的变量的指针,该变量接收通信服务器端口的不透明端口句柄。minifilter驱动程序使用这个句柄来监听来自用户模式应用程序的连接请求。
        //参数3指向OBJECT_ATTRIBUTES结构的指针,该结构指定通信服务器端口的属性。
        //此结构必须由之前的InitializeObjectAttributes调用初始化。此参数是必需的,不能为空。通信端口对象的此结构的成员包括以下内容。
        //参数4指向由minifilter驱动程序定义的上下文信息的指针。这个信息可以用来区分由相同的minifilter驱动程序创建的多个通信服务器端口。
        //过滤器管理器将这个上下文指针作为参数传递给ConnectNotifyCallback例程。该参数是可选的,可以为NULL。
        //参数5 NPMiniConneet是用户态与内核态建立连接时内核会调用到的函数。
        //参数6 NPMiniDisconnect 是用户态与内核态连接结束时内核会调用到的函数。。
        //参数7 NPMiniMessage是用户态与内核态传送数据时内核会调用到的函数
        //参数8此服务器端口所允许的最大并发客户端连接数。此参数是必需的,且必须大于零。
        status = FltCreateCommunicationPort(gFilterHandle,
                &gServerPort,
                &oa,
                NULL,
                NPMiniConnect,
                NPMiniDisconnect,
                NPMiniMessage,
                1);
        //释放分配的安全描述符
        FltFreeSecurityDescriptor(sd);
 
        if (!NT_SUCCESS(status)) {
                goto final;
        }
 
        // 返回值
 
        // 首先初始化含有文件路径的OBJECT_ATTRIBU
        UNICODE_STRING mytext1;  //文件名字符串
        OBJECT_ATTRIBUTES ATTRIBUTES1;
        IO_STATUS_BLOCK BLOCK1;//可拥有的控制权 
        LARGE_INTEGER size1;  //写的偏移大小
 
        str1 = ExAllocatePool(NonPagedPool, 500);//分配500的非分页内存
        RtlZeroMemory(str1, 500);//0内存
 
        RtlInitUnicodeString(&mytext1, L"\\??\\C:\\true.txt");//初始化文件名 
        memset(&ATTRIBUTES1, 0, sizeof(OBJECT_ATTRIBUTES));                        //对象属性清空 
        size1.QuadPart = 0;
 
        InitializeObjectAttributes(&ATTRIBUTES1, &mytext1, OBJ_CASE_INSENSITIVE, NULL, NULL);//对象属性关键是文件名字 不区分大小写 ,参数意义同上
 
        //打开目标文件获取句柄
        //参数1:一个指向句柄变量的指针,该句柄用于接收文件的句柄。
        //参数2:指定ACCESS_MASK值,该值确定请求对对象的访问。除了为所有类型的对象定义的访问权限之外,调用者还可以指定以下特定于文件的访问权限。
        //参数3:OBJECT_ATTRIBUTES结构的指针,该结构指定对象名和其他属性。使用InitializeObjectAttributes初始化该结构。
        //参数4:用于接收最终完成状态和请求操作的其他信息
        //参数5:它包含创建或覆盖的文件的初始分配大小,可以为Null
        //参数6:表示创建或覆盖文件时要设置的文件属性。调用者通常指定FILE_ATTRIBUTE_NORMAL,它设置默认属性
        //参数7:共享访问的类型,
        //参数8:指定在文件存在或不存在时执行的操作
        //参数9:指定驱动程序创建或打开文件时应用的选项
        //参数10:对于设备和中间驱动程序,此参数必须为空指针
        //参数11:对于设备和中间驱动程序,此参数必须为零。
        status = ZwCreateFile(&myhandl, GENERIC_ALL, &ATTRIBUTES1, &BLOCK1, NULL, FILE_ATTRIBUTE_NORMAL,
                FILE_SHARE_READ, FILE_OPEN_IF, FILE_NON_DIRECTORY_FILE, NULL, 0);
 
        //通过句柄读取目标文件放入str1
        if (!NT_SUCCESS(status))
        {
                FirsT = 0;
                goto final;
        }
        else
        {
                status = ZwReadFile(myhandl, NULL, NULL, NULL, &BLOCK1, str1, 500, &size1, NULL);
                FirsT = 1;//防止重入
                DbgPrint("status=%X\n", status);
 
        }
 
        //以下内容基本同上只是文件的对象不一样
        UNICODE_STRING mytext2;
        OBJECT_ATTRIBUTES ATTRIBUTES2;
        IO_STATUS_BLOCK BLOCK2;//可拥有的控制权 
        LARGE_INTEGER size2;
        str2 = ExAllocatePool(NonPagedPool, 500);
        RtlZeroMemory(str2, 500);
        RtlInitUnicodeString(&mytext2, L"\\??\\C:\\false.txt");//初始化文件名 
        memset(&ATTRIBUTES2, 0, sizeof(OBJECT_ATTRIBUTES));                        //对象属性清空 
        size2.QuadPart = 0;
        InitializeObjectAttributes(&ATTRIBUTES2, &mytext2, OBJ_CASE_INSENSITIVE, NULL, NULL);//对象属性关键是文件名字 不区分大小写 
        status = ZwCreateFile(&myhand2, GENERIC_ALL, &ATTRIBUTES2, &BLOCK2, NULL, FILE_ATTRIBUTE_NORMAL,
                FILE_SHARE_READ, FILE_OPEN_IF, FILE_NON_DIRECTORY_FILE, NULL, 0);
        if (!NT_SUCCESS(status))
        {
                FirsT = 0;
                goto final;
        }
        else
        {
                status = ZwReadFile(myhand2, NULL, NULL, NULL, &BLOCK2, str2, 500, &size2, NULL);
                DbgPrint("status2=%X\n", status);
 
        }
 
 
 
        //对目标文件进行写入,从而触发read
        PVOID str3 = ExAllocatePool(PagedPool, 10);
        RtlZeroMemory(str3, 6);
        RtlCopyMemory(str3, "...", strlen("..."));
        size1.HighPart = 0;
        size1.QuadPart = 500;
        status = ZwWriteFile(myhandl, NULL, NULL, NULL, &BLOCK1, str3, 6, &size1, NULL);
 
 
        //关闭句柄
        ZwClose(myhandl);
        ZwClose(myhand2);
        final ://失败处理处
 
        if (!NT_SUCCESS(status)) {
 
                if (NULL != gServerPort) {
                        //关闭过滤器驱动程序的通信服务器端口
                        FltCloseCommunicationPort(gServerPort);
                }
 
                if (NULL != gFilterHandle) {
                        //取消注册
                        FltUnregisterFilter(gFilterHandle);
                }
        }
        return status;
}
FLT_PREOP_CALLBACK_STATUS
MyPostRead(
_Inout_ PFLT_CALLBACK_DATA Data,
_In_ PCFLT_RELATED_OBJECTS FltObjects,
_In_opt_ PVOID CompletionContext,
_In_ FLT_POST_OPERATION_FLAGS Flags
)
{
 
        UNREFERENCED_PARAMETER(Flags);
        UNREFERENCED_PARAMETER(CompletionContext);
        char FileName[260] = "X:";//存放解析后的文件名字
 
        NTSTATUS status;  //返回值状态
        PFLT_FILE_NAME_INFORMATION nameInfo; //名称信息
        //
        // 对失败的irp进行过滤,如果失败了,就不用继续
        if (!NT_SUCCESS(Data->IoStatus.Status) ||
                (STATUS_REPARSE == Data->IoStatus.Status)) {
 
                return FLT_POSTOP_FINISHED_PROCESSING;
        }
 
 
 
        //FltGetFileNameInformation例程返回文件或目录的名称信息。                                                                              
        status = FltGetFileNameInformation(Data,//PFLT_CALLBACK_DATA结构的指针,它是I/O操作的回调数据结构。此参数是必需的,不能为空。
                FLT_FILE_NAME_NORMALIZED |//使nameInfo参数接收包含文件规范化名称的结构的地址。
                FLT_FILE_NAME_QUERY_DEFAULT,//如果查询文件系统以获取文件名目前还不安全,则FltGetFileNameInformation将不执行任何操作。
                //否则,FltGetFileNameInformation查询过滤器管理器的名称缓存以获取文件名信息。
                //如果在缓存中没有找到该名称,则FltGetFileNameInformation查询文件系统并缓存结果。
                &nameInfo);//包含文件名信息。
 
        if (NT_SUCCESS(status))
        {
                //解析FLT_FILE_NAME_INFORMATION结构的内容。
                status = FltParseFileNameInformation(nameInfo);
 
 
                //参数1大小写转换,后放入参数2
                if (NPUnicodeStringToChar(&nameInfo->Name, FileName))
                {
                        //DbgPrint("FileName is %s\n", FileName);
                        if (strstr(FileName, "TRUE.TXT") > 0)
                        {
                                PUCHAR buf1;
 
                                if (Data->Iopb->Parameters.Read.MdlAddress != 0)
                                {
                                        //如果为mdl方式读取文件,就获取地址
                                        buf1 = MmGetSystemAddressForMdl(Data->Iopb->Parameters.Read.MdlAddress);
 
                                }
                                else
                                {
                                        //如果为ReadBuffer就直接给指针
                                        buf1 = Data->Iopb->Parameters.Read.ReadBuffer;
                                }
                                //判断应用层发来的指令是否为显示true.txt
                                if (command == 2)
                                {
                                        if (FirsT == 1)
                                        {
                                                //复制内存,填写长度
                                                RtlCopyMemory(buf1, str1, 500);
                                                Data->IoStatus.Information = Data->Iopb->Parameters.Read.Length;
                                        }
                                }
                                else
                                {
                                        //DbgBreakPoint();
                                        if (FirsT == 1)
                                        {
                                                //复制内存,填写长度
                                                RtlCopyMemory(buf1, str2, 500);
                                                Data->IoStatus.Information = Data->Iopb->Parameters.Read.Length;
                                        }
 
                                }
                                DbgPrint("ReadBuffed=%s", buf1);
                                //释放文件名信息结构体的内存
                                FltReleaseFileNameInformation(nameInfo);
                                return FLT_POSTOP_FINISHED_PROCESSING;
                        }
                }
 
                FltReleaseFileNameInformation(nameInfo);
        }
 
        return FLT_POSTOP_FINISHED_PROCESSING;
 
}
 
NTSTATUS
NPMiniMessage(
__in PVOID ConnectionCookie,
__in_bcount_opt(InputBufferSize) PVOID InputBuffer,
__in ULONG InputBufferSize,
__out_bcount_part_opt(OutputBufferSize, *ReturnOutputBufferLength) PVOID OutputBuffer,
__in ULONG OutputBufferSize,
__out PULONG ReturnOutputBufferLength
)
{
        //写入文件需要的各种参数,意义同dirverEntry
        NTSTATUS status;
        UNICODE_STRING mytext1;
        OBJECT_ATTRIBUTES ATTRIBUTES1;
        IO_STATUS_BLOCK BLOCK1;//可拥有的控制权 
        LARGE_INTEGER size1;
        PVOID str4 = ExAllocatePool(PagedPool, 40);
        RtlZeroMemory(str4, 40);
        RtlInitUnicodeString(&mytext1, L"\\??\\C:\\true.txt");//初始化文件名 
        memset(&ATTRIBUTES1, 0, sizeof(OBJECT_ATTRIBUTES));                        //对象属性清空 
        size1.QuadPart = 0;
 
 
        PAGED_CODE();
        //分配内存,存储字符串,用于和应用层字符串比较
        PVOID test;
        test = ExAllocatePool(PagedPool, 10);
        RtlCopyMemory(test, "0", strlen("0"));
        PVOID test2;
        test2 = ExAllocatePool(PagedPool, 50);
        RtlCopyMemory(test2, "52pojie", strlen("52pojie"));
 
 
        UNREFERENCED_PARAMETER(ConnectionCookie);
        UNREFERENCED_PARAMETER(OutputBufferSize);
        UNREFERENCED_PARAMETER(OutputBuffer);
 
        //通过应用层输入的字符串,来显示相应的文本
        if (strcmp(InputBuffer, test) == 0)
        {
                command = 1;
        }
        else if (strcmp(InputBuffer, test2) == 0)
        {
                command = 2;
        }
        else
        {
                status = STATUS_INVALID_PARAMETER;
 
        }
        //向文件写入,来触发read,由于需要内容不同,所以通过改变ONOROFF,实现两种写入依次执行
        if (ONOROFF == 0)
        {
                //向文件写入空格
                ONOROFF = ONOROFF + 1;
                InitializeObjectAttributes(&ATTRIBUTES1, &mytext1, OBJ_CASE_INSENSITIVE, NULL, NULL);//对象属性关键是文件名字 不区分大小写 
                status = ZwCreateFile(&myhandl, GENERIC_ALL, &ATTRIBUTES1, &BLOCK1, NULL, FILE_ATTRIBUTE_NORMAL,
                        FILE_SHARE_READ, FILE_OPEN_IF, FILE_NON_DIRECTORY_FILE, NULL, 0);
                RtlCopyMemory(str4, "    ", strlen("    "));
                size1.HighPart = 0;
                size1.QuadPart = 500;
                status = ZwWriteFile(myhandl, NULL, NULL, NULL, &BLOCK1, str4, 8, &size1, NULL);
                ZwClose(myhandl);
        }
        else
        {
                //向文件写入"****",
                ONOROFF = ONOROFF - 1;
                InitializeObjectAttributes(&ATTRIBUTES1, &mytext1, OBJ_CASE_INSENSITIVE, NULL, NULL);//对象属性关键是文件名字 不区分大小写 
                status = ZwCreateFile(&myhandl, GENERIC_ALL, &ATTRIBUTES1, &BLOCK1, NULL, FILE_ATTRIBUTE_NORMAL,
                        FILE_SHARE_READ, FILE_OPEN_IF, FILE_NON_DIRECTORY_FILE, NULL, 0);
                RtlCopyMemory(str4, "****", strlen("****"));
                size1.HighPart = 0;
                size1.QuadPart = 500;
                status = ZwWriteFile(myhandl, NULL, NULL, NULL, &BLOCK1, str4, 8, &size1, NULL);
                ZwClose(myhandl);
        }
 
        //释放内存
        ExFreePool(str4);
        ExFreePool(test);
        ExFreePool(test2);
        return STATUS_SUCCESS;
}
 
BOOLEAN NPUnicodeStringToChar(PUNICODE_STRING UniName, char Name[])
{
        ANSI_STRING        AnsiName;
        NTSTATUS        ntstatus;
        char*                nameptr;
 
        __try {
                ntstatus = RtlUnicodeStringToAnsiString(&AnsiName, UniName, TRUE);
 
                if (AnsiName.Length < 260) {
                        nameptr = (PCHAR)AnsiName.Buffer;
                        //转换成大写和复制到缓冲区
                        strcpy(Name, _strupr(nameptr));
                        //DbgPrint("NPUnicodeStringToChar : %s\n", Name);       
                }
                RtlFreeAnsiString(&AnsiName);
        }
        __except (EXCEPTION_EXECUTE_HANDLER) {
                DbgPrint("NPUnicodeStringToChar EXCEPTION_EXECUTE_HANDLER\n");
                return FALSE;
        }
        return TRUE;
}
 
NTSTATUS NPInstanceSetup(
        __in PCFLT_RELATED_OBJECTS FltObjects,
        __in FLT_INSTANCE_SETUP_FLAGS Flags,
        __in DEVICE_TYPE VolumeDeviceType,
        __in FLT_FILESYSTEM_TYPE VolumeFilesystemType
        )
{
        UNREFERENCED_PARAMETER(FltObjects);
        UNREFERENCED_PARAMETER(Flags);
        UNREFERENCED_PARAMETER(VolumeDeviceType);
        UNREFERENCED_PARAMETER(VolumeFilesystemType);
 
        PAGED_CODE();
 
 
 
        return STATUS_SUCCESS;
}
 
 
NTSTATUS
NPInstanceQueryTeardown(
__in PCFLT_RELATED_OBJECTS FltObjects,
__in FLT_INSTANCE_QUERY_TEARDOWN_FLAGS Flags
)
{
        UNREFERENCED_PARAMETER(FltObjects);
        UNREFERENCED_PARAMETER(Flags);
 
        PAGED_CODE();
 
 
        return STATUS_SUCCESS;
}
 
 
VOID NPInstanceTeardownStart(
        __in PCFLT_RELATED_OBJECTS FltObjects,
        __in FLT_INSTANCE_TEARDOWN_FLAGS Flags
        )
{
        UNREFERENCED_PARAMETER(FltObjects);
        UNREFERENCED_PARAMETER(Flags);
 
        PAGED_CODE();
 
 
}
 
 
VOID NPInstanceTeardownComplete(
        __in PCFLT_RELATED_OBJECTS FltObjects,
        __in FLT_INSTANCE_TEARDOWN_FLAGS Flags
        )
{
        UNREFERENCED_PARAMETER(FltObjects);
        UNREFERENCED_PARAMETER(Flags);
 
        PAGED_CODE();
 
 
}
 
NTSTATUS
NPUnload(
__in FLT_FILTER_UNLOAD_FLAGS Flags
)
{
        UNREFERENCED_PARAMETER(Flags);
 
        PAGED_CODE();
 
 
        ExFreePool(str1);
        ExFreePool(str2);
        FltCloseCommunicationPort(gServerPort);
 
        FltUnregisterFilter(gFilterHandle);
 
        return STATUS_SUCCESS;
}
 
FLT_PREOP_CALLBACK_STATUS
NPPreCreate(
__inout PFLT_CALLBACK_DATA Data,
__in PCFLT_RELATED_OBJECTS FltObjects,
__deref_out_opt PVOID *CompletionContext
)
{
        char FileName[260] = "X:";
 
        NTSTATUS status;
        PFLT_FILE_NAME_INFORMATION nameInfo;
 
        UNREFERENCED_PARAMETER(FltObjects);
        UNREFERENCED_PARAMETER(CompletionContext);
 
        PAGED_CODE();
 
 
        status = FltGetFileNameInformation(Data,
                FLT_FILE_NAME_NORMALIZED |
                FLT_FILE_NAME_QUERY_DEFAULT,
                &nameInfo);
        if (NT_SUCCESS(status)) {
 
                if (1) {
                        FltParseFileNameInformation(nameInfo);
                        if (NPUnicodeStringToChar(&nameInfo->Name, FileName)) {
 
                                if (strstr(FileName, "NOTEPAD.EXE") > 0) {
 
 
                                        FltReleaseFileNameInformation(nameInfo);
                                        return FLT_PREOP_COMPLETE;
                                }
                        }
                }
                FltReleaseFileNameInformation(nameInfo);
        }
 
        return FLT_PREOP_SUCCESS_WITH_CALLBACK;
}
 
 
 
 
NTSTATUS
NPMiniConnect(
__in PFLT_PORT ClientPort,
__in PVOID ServerPortCookie,
__in_bcount(SizeOfContext) PVOID ConnectionContext,
__in ULONG SizeOfContext,
__deref_out_opt PVOID *ConnectionCookie
)
{
        DbgPrint("[mini-filter] NPMiniConnect");
        PAGED_CODE();
 
        UNREFERENCED_PARAMETER(ServerPortCookie);
        UNREFERENCED_PARAMETER(ConnectionContext);
        UNREFERENCED_PARAMETER(SizeOfContext);
        UNREFERENCED_PARAMETER(ConnectionCookie);
 
        ASSERT(gClientPort == NULL);
        gClientPort = ClientPort;
        return STATUS_SUCCESS;
}
 
VOID
NPMiniDisconnect(
__in_opt PVOID ConnectionCookie
)
{
        PAGED_CODE();
        UNREFERENCED_PARAMETER(ConnectionCookie);
        DbgPrint("[mini-filter] NPMiniDisconnect");
 
        //  Close our handle
        FltCloseClientPort(gFilterHandle, &gClientPort);
}
#include "minifilter.h"
NTSTATUS
DriverEntry(
__in PDRIVER_OBJECT DriverObject,  //PDRIVER_OBJECT 驱动数据结构(驱动对象)的指针
__in PUNICODE_STRING RegistryPath//PUNICODE_STRING 内核字符串数组,驱动以服务的形式加载,这个字符串为驱动在注册表的路径
)
{
        NTSTATUS status;              //返回值状态
        PSECURITY_DESCRIPTOR sd;     //安全描述符。
        OBJECT_ATTRIBUTES oa;        //对象属性
        UNICODE_STRING uniString;                //用于通信端口名称
 
        UNREFERENCED_PARAMETER(RegistryPath);//避免编译器关于未引用参数的警告
 
 
        // 注册FilterRegistration,告诉系统我设置的回调例程
        //1个参数是本驱动的驱动对象,是在入口函数DriverEntry 中作为参数传入的。
        //2个参数就是一个注册信息的结构,这个结构内含描述这个过滤器的所有信息
        //3个参数 是一个返回参数,返回注册成功的微过滤器句柄,在下面调用函数FltStartFiltering时会用到
        //DbgPrint("statuswww=\n");
        status = FltRegisterFilter(DriverObject,
                &FilterRegistration,
                &gFilterHandle);
 
        ASSERT(NT_SUCCESS(status));//检查结果是否成功,失败后触发异常被调试器接管
 
        if (NT_SUCCESS(status)) {//检查结果是否成功,
 
                //
                //  开始函数,只有一个参数为之前获取的句柄
                //
 
                status = FltStartFiltering(gFilterHandle);
 
                if (!NT_SUCCESS(status)) {
                        //如果失败就取消注册,只有一个参数为之前获取的句柄
                        FltUnregisterFilter(gFilterHandle);
                }
        }
 
        //FltBuildDefaultSecurityDescriptor构建一个默认的安全描述符,用于FltCreateCommunicationPort。
        //参数1指向调用方分配的变量的指针,该变量接收到指向新创建的不透明指针
        //参数2指定调用者需要对端口对象的访问类型的标志的位掩码。系统定义的DesiredAccess标志集确定了minifilter驱动程序通信端口对象的以下特定访问权限。
        status = FltBuildDefaultSecurityDescriptor(&sd, FLT_PORT_ALL_ACCESS);
 
        if (!NT_SUCCESS(status)) {//检查结果是否成功,未成功就跳向最终处理部分
                goto final;
        }
 
        //函数初始化Unicode字符的计数字符串
        RtlInitUnicodeString(&uniString, MINISPY_PORT_NAME);
 
        //宏初始化不透明的OBJECT_ATTRIBUTES结构,该结构将对象句柄的属性指定给打开句柄的例程。
        //参数1为要被初始化的结构体,参数2一个指向Unicode字符串的指针,该字符串包含要为其打开句柄的对象的名称。
        //这必须是一个完全限定的对象名,或者由RootDirectory参数指定的对象目录的相对路径名。
        //参数3标志位,此处指定为只在内核模式访问,与对大小写不区分
        //参数4在ObjectName参数中指定的路径名的根对象目录句柄。如果ObjectName是完全限定的对象名称,
        //则RootDirectory为空。使用ZwCreateDirectoryObject获得对象目录的句柄。
        //参数5指定创建对象时应用于该对象的安全描述符。此参数是可选的。驱动程序可以指定NULL来接受对象的默认安全性。
        InitializeObjectAttributes(&oa,
                &uniString,
                OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
                NULL,
                sd);
 
        //创建一个通信服务器端口,minifilter驱动程序可以在该端口上接收来自用户模式应用程序的连接请求。
        // 参数1调用者的不透明过滤器指针
        //参数2指向调用方分配的变量的指针,该变量接收通信服务器端口的不透明端口句柄。minifilter驱动程序使用这个句柄来监听来自用户模式应用程序的连接请求。
        //参数3指向OBJECT_ATTRIBUTES结构的指针,该结构指定通信服务器端口的属性。
        //此结构必须由之前的InitializeObjectAttributes调用初始化。此参数是必需的,不能为空。通信端口对象的此结构的成员包括以下内容。
        //参数4指向由minifilter驱动程序定义的上下文信息的指针。这个信息可以用来区分由相同的minifilter驱动程序创建的多个通信服务器端口。
        //过滤器管理器将这个上下文指针作为参数传递给ConnectNotifyCallback例程。该参数是可选的,可以为NULL。
        //参数5 NPMiniConneet是用户态与内核态建立连接时内核会调用到的函数。
        //参数6 NPMiniDisconnect 是用户态与内核态连接结束时内核会调用到的函数。。
        //参数7 NPMiniMessage是用户态与内核态传送数据时内核会调用到的函数
        //参数8此服务器端口所允许的最大并发客户端连接数。此参数是必需的,且必须大于零。
        status = FltCreateCommunicationPort(gFilterHandle,
                &gServerPort,
                &oa,
                NULL,
                NPMiniConnect,
                NPMiniDisconnect,
                NPMiniMessage,
                1);
        //释放分配的安全描述符
        FltFreeSecurityDescriptor(sd);
 
        if (!NT_SUCCESS(status)) {
                goto final;
        }
 
        // 返回值
 
        // 首先初始化含有文件路径的OBJECT_ATTRIBU
        UNICODE_STRING mytext1;  //文件名字符串
        OBJECT_ATTRIBUTES ATTRIBUTES1;
        IO_STATUS_BLOCK BLOCK1;//可拥有的控制权 
        LARGE_INTEGER size1;  //写的偏移大小
 
        str1 = ExAllocatePool(NonPagedPool, 500);//分配500的非分页内存
        RtlZeroMemory(str1, 500);//0内存
 
        RtlInitUnicodeString(&mytext1, L"\\??\\C:\\true.txt");//初始化文件名 
        memset(&ATTRIBUTES1, 0, sizeof(OBJECT_ATTRIBUTES));                        //对象属性清空 
        size1.QuadPart = 0;
 
        InitializeObjectAttributes(&ATTRIBUTES1, &mytext1, OBJ_CASE_INSENSITIVE, NULL, NULL);//对象属性关键是文件名字 不区分大小写 ,参数意义同上
 
        //打开目标文件获取句柄
        //参数1:一个指向句柄变量的指针,该句柄用于接收文件的句柄。
        //参数2:指定ACCESS_MASK值,该值确定请求对对象的访问。除了为所有类型的对象定义的访问权限之外,调用者还可以指定以下特定于文件的访问权限。
        //参数3:OBJECT_ATTRIBUTES结构的指针,该结构指定对象名和其他属性。使用InitializeObjectAttributes初始化该结构。
        //参数4:用于接收最终完成状态和请求操作的其他信息
        //参数5:它包含创建或覆盖的文件的初始分配大小,可以为Null
        //参数6:表示创建或覆盖文件时要设置的文件属性。调用者通常指定FILE_ATTRIBUTE_NORMAL,它设置默认属性
        //参数7:共享访问的类型,
        //参数8:指定在文件存在或不存在时执行的操作
        //参数9:指定驱动程序创建或打开文件时应用的选项
        //参数10:对于设备和中间驱动程序,此参数必须为空指针
        //参数11:对于设备和中间驱动程序,此参数必须为零。
        status = ZwCreateFile(&myhandl, GENERIC_ALL, &ATTRIBUTES1, &BLOCK1, NULL, FILE_ATTRIBUTE_NORMAL,
                FILE_SHARE_READ, FILE_OPEN_IF, FILE_NON_DIRECTORY_FILE, NULL, 0);
 
        //通过句柄读取目标文件放入str1
        if (!NT_SUCCESS(status))
        {
                FirsT = 0;
                goto final;
        }
        else
        {
                status = ZwReadFile(myhandl, NULL, NULL, NULL, &BLOCK1, str1, 500, &size1, NULL);
                FirsT = 1;//防止重入
                DbgPrint("status=%X\n", status);
 
        }
 
        //以下内容基本同上只是文件的对象不一样
        UNICODE_STRING mytext2;
        OBJECT_ATTRIBUTES ATTRIBUTES2;
        IO_STATUS_BLOCK BLOCK2;//可拥有的控制权 
        LARGE_INTEGER size2;
        str2 = ExAllocatePool(NonPagedPool, 500);
        RtlZeroMemory(str2, 500);
        RtlInitUnicodeString(&mytext2, L"\\??\\C:\\false.txt");//初始化文件名 
        memset(&ATTRIBUTES2, 0, sizeof(OBJECT_ATTRIBUTES));                        //对象属性清空 
        size2.QuadPart = 0;
        InitializeObjectAttributes(&ATTRIBUTES2, &mytext2, OBJ_CASE_INSENSITIVE, NULL, NULL);//对象属性关键是文件名字 不区分大小写 
        status = ZwCreateFile(&myhand2, GENERIC_ALL, &ATTRIBUTES2, &BLOCK2, NULL, FILE_ATTRIBUTE_NORMAL,
                FILE_SHARE_READ, FILE_OPEN_IF, FILE_NON_DIRECTORY_FILE, NULL, 0);
        if (!NT_SUCCESS(status))
        {
                FirsT = 0;
                goto final;
        }
        else
        {
                status = ZwReadFile(myhand2, NULL, NULL, NULL, &BLOCK2, str2, 500, &size2, NULL);
                DbgPrint("status2=%X\n", status);
 
        }
 
 
 
        //对目标文件进行写入,从而触发read
        PVOID str3 = ExAllocatePool(PagedPool, 10);
        RtlZeroMemory(str3, 6);
        RtlCopyMemory(str3, "...", strlen("..."));
        size1.HighPart = 0;
        size1.QuadPart = 500;
        status = ZwWriteFile(myhandl, NULL, NULL, NULL, &BLOCK1, str3, 6, &size1, NULL);
 
 
        //关闭句柄
        ZwClose(myhandl);
        ZwClose(myhand2);
        final ://失败处理处
 
        if (!NT_SUCCESS(status)) {
 
                if (NULL != gServerPort) {
                        //关闭过滤器驱动程序的通信服务器端口
                        FltCloseCommunicationPort(gServerPort);
                }
 
                if (NULL != gFilterHandle) {
                        //取消注册
                        FltUnregisterFilter(gFilterHandle);
                }
        }
        return status;
}
FLT_PREOP_CALLBACK_STATUS
MyPostRead(
_Inout_ PFLT_CALLBACK_DATA Data,
_In_ PCFLT_RELATED_OBJECTS FltObjects,
_In_opt_ PVOID CompletionContext,
_In_ FLT_POST_OPERATION_FLAGS Flags
)
{
 
        UNREFERENCED_PARAMETER(Flags);
        UNREFERENCED_PARAMETER(CompletionContext);
        char FileName[260] = "X:";//存放解析后的文件名字
 
        NTSTATUS status;  //返回值状态
        PFLT_FILE_NAME_INFORMATION nameInfo; //名称信息
        //
        // 对失败的irp进行过滤,如果失败了,就不用继续
        if (!NT_SUCCESS(Data->IoStatus.Status) ||
                (STATUS_REPARSE == Data->IoStatus.Status)) {
 
                return FLT_POSTOP_FINISHED_PROCESSING;
        }
 
 
 
        //FltGetFileNameInformation例程返回文件或目录的名称信息。                                                                              
        status = FltGetFileNameInformation(Data,//PFLT_CALLBACK_DATA结构的指针,它是I/O操作的回调数据结构。此参数是必需的,不能为空。
                FLT_FILE_NAME_NORMALIZED |//使nameInfo参数接收包含文件规范化名称的结构的地址。
                FLT_FILE_NAME_QUERY_DEFAULT,//如果查询文件系统以获取文件名目前还不安全,则FltGetFileNameInformation将不执行任何操作。
                //否则,FltGetFileNameInformation查询过滤器管理器的名称缓存以获取文件名信息。
                //如果在缓存中没有找到该名称,则FltGetFileNameInformation查询文件系统并缓存结果。
                &nameInfo);//包含文件名信息。
 
        if (NT_SUCCESS(status))
        {
                //解析FLT_FILE_NAME_INFORMATION结构的内容。
                status = FltParseFileNameInformation(nameInfo);
 
 
                //参数1大小写转换,后放入参数2
                if (NPUnicodeStringToChar(&nameInfo->Name, FileName))
                {
                        //DbgPrint("FileName is %s\n", FileName);
                        if (strstr(FileName, "TRUE.TXT") > 0)
                        {
                                PUCHAR buf1;
 
                                if (Data->Iopb->Parameters.Read.MdlAddress != 0)
                                {
                                        //如果为mdl方式读取文件,就获取地址
                                        buf1 = MmGetSystemAddressForMdl(Data->Iopb->Parameters.Read.MdlAddress);
 
                                }
                                else
                                {
                                        //如果为ReadBuffer就直接给指针
                                        buf1 = Data->Iopb->Parameters.Read.ReadBuffer;
                                }
                                //判断应用层发来的指令是否为显示true.txt
                                if (command == 2)
                                {
                                        if (FirsT == 1)
                                        {
                                                //复制内存,填写长度
                                                RtlCopyMemory(buf1, str1, 500);
                                                Data->IoStatus.Information = Data->Iopb->Parameters.Read.Length;
                                        }
                                }
                                else
                                {
                                        //DbgBreakPoint();
                                        if (FirsT == 1)
                                        {
                                                //复制内存,填写长度
                                                RtlCopyMemory(buf1, str2, 500);
                                                Data->IoStatus.Information = Data->Iopb->Parameters.Read.Length;
                                        }
 
                                }
                                DbgPrint("ReadBuffed=%s", buf1);
                                //释放文件名信息结构体的内存
                                FltReleaseFileNameInformation(nameInfo);
                                return FLT_POSTOP_FINISHED_PROCESSING;
                        }
                }
 
                FltReleaseFileNameInformation(nameInfo);
        }
 
        return FLT_POSTOP_FINISHED_PROCESSING;
 
}
 
NTSTATUS
NPMiniMessage(
__in PVOID ConnectionCookie,
__in_bcount_opt(InputBufferSize) PVOID InputBuffer,
__in ULONG InputBufferSize,
__out_bcount_part_opt(OutputBufferSize, *ReturnOutputBufferLength) PVOID OutputBuffer,
__in ULONG OutputBufferSize,
__out PULONG ReturnOutputBufferLength
)
{
        //写入文件需要的各种参数,意义同dirverEntry
        NTSTATUS status;
        UNICODE_STRING mytext1;
        OBJECT_ATTRIBUTES ATTRIBUTES1;
        IO_STATUS_BLOCK BLOCK1;//可拥有的控制权 
        LARGE_INTEGER size1;

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2020-12-28 17:22 被zx_838741编辑 ,原因:
收藏
免费 9
支持
分享
最新回复 (13)
雪    币: 729
活跃值: (388)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
干货
2020-12-26 23:00
0
雪    币: 113
活跃值: (76)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
我记得notepad是通过FileMapping的方式打开的文件,并不是用什么什么内部保护,但是IRP_NOCACHE和IRP_PAGING_IO 是可以抓到。屏蔽notepad治标不治本,写一个FileMapping方式打开的测试程序,应该有一样的问题
2020-12-27 23:48
0
雪    币: 377
活跃值: (5996)
能力值: ( LV4,RANK:55 )
在线值:
发帖
回帖
粉丝
4
dragonwang 我记得notepad是通过FileMapping的方式打开的文件,并不是用什么什么内部保护,但是IRP_NOCACHE和IRP_PAGING_IO 是可以抓到。屏蔽notepad治标不治本,写一个Fi ...

受教了,我还真不知道,那就先把这个也列为一种可能的情况。

你说通过FileMapping的方式,我百度查了一下,五个相关函数,调试notepad只有MapViewOfFile断下。

第一个参数为文件句柄,控制台打开1.txt文件查看句柄,

调试notepad,打开1.txt,然后在这个函数断下的地方一一核对,未发现该文件句柄,


从百度上看mapfile的解释,文件是加载到内存,才能印射,

那加载的时候不会触发read的irp吗。

https://blog.csdn.net/zj510/article/details/85059638?depth_1-

从该文章看mapfile触发的irp也并非你说的


方便告诉一下关于你说的东西的相关书籍或文章吗?

我好去验证



2020-12-28 10:34
0
雪    币: 377
活跃值: (5996)
能力值: ( LV4,RANK:55 )
在线值:
发帖
回帖
粉丝
5

本人看的书籍为Windows内核安全与驱动开发,我又去看了一下关于文件过滤的12章,并没有过滤您说的irp,

不过还是感谢你的指出,让我知道了新东西

最后于 2020-12-28 10:53 被zx_838741编辑 ,原因:
2020-12-28 10:51
0
雪    币: 113
活跃值: (76)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
6

我说的那两个是PFLT_CALLBACK_DATA->Iopb->IrpFlags的属性,就是第一次从磁盘读入缓存会带有相关属性,在miniflt_pre_read能捕获到,剩下的其他读写操作miniflt好像就捕获不到了(这个是我的记忆印象)。我记得以fileMapping打开文件的时候,好像有个IRP_xxx指令发出来,我记不清了,网上应该能查到。我想说的是,你遇到的不是特殊处理程序,是有一类问题没处理,简单的屏蔽某个进程其实是没有解决。你自己写个filemapping方法的测试程序,效果应该和notepad是一样的情况。
你在这方面再查查资料看看能不能完善一下你的作品

最后于 2020-12-28 14:57 被dragonwang编辑 ,原因:
2020-12-28 14:47
0
雪    币: 377
活跃值: (5996)
能力值: ( LV4,RANK:55 )
在线值:
发帖
回帖
粉丝
7
dragonwang 我说的那两个是PFLT_CALLBACK_DATA-&gt;Iopb-&gt;IrpFlags的属性,就是第一次从磁盘读入缓存会带有相关属性,在miniflt_pre_read能捕获到 ...

完善有个简单的办法,就不修改或者发新贴了,直接评论里说一下,直接让打开这个的irp返回失败就行了,射是那个irp我还没找到。


2020-12-28 17:19
0
雪    币: 210
活跃值: (1707)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
问题2 你说错了 不能这么用 虽然是在高IRQL等级下 但是建议你看下后回调whensafe函数的使用 你要让他irql等级降下来再继续操作可以做的事情就多了
2021-1-5 19:19
0
雪    币: 377
活跃值: (5996)
能力值: ( LV4,RANK:55 )
在线值:
发帖
回帖
粉丝
9
wx_0xC05StackOver 问题2 你说错了 不能这么用 虽然是在高IRQL等级下 但是建议你看下后回调whensafe函数的使用 你要让他irql等级降下来再继续操作可以做的事情就多了

学到了

最后于 2021-3-22 10:28 被zx_838741编辑 ,原因:
2021-1-6 12:47
0
雪    币: 407
活跃值: (1816)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
下咯 请你去看看降低irql函数的解释,只有自己使用函数升上的irql,才能用降低,它本身就是高的,不是我升的,不能降,还有我不明白你说的whensafe是啥,第一次看见这个东西能详细说一下吗
FltDoCompletionProcessingWhenSafe
2021-1-6 13:48
0
雪    币: 377
活跃值: (5996)
能力值: ( LV4,RANK:55 )
在线值:
发帖
回帖
粉丝
11
Thead FltDoCompletionProcessingWhenSafe

学到了


最后于 2021-3-20 11:20 被zx_838741编辑 ,原因:
2021-1-6 14:36
0
雪    币: 206
活跃值: (2001)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
加载驱动有啥办法不用安装inf文件吗哥哥
2021-4-13 17:46
0
雪    币: 256
活跃值: (382)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
麦瑞鸭 加载驱动有啥办法不用安装inf文件吗哥哥
2021-10-26 14:54
0
雪    币: 201
活跃值: (201)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
过来瞅瞅,看来做文件过滤的不少哇~
2022-11-21 15:08
0
游客
登录 | 注册 方可回帖
返回
//