首页
社区
课程
招聘
[原创]内核层自己发送IRP请求操作文件全面总结(已完整调试, x86/x64各系统通用)
发表于: 2020-12-8 00:30 19838

[原创]内核层自己发送IRP请求操作文件全面总结(已完整调试, x86/x64各系统通用)

2020-12-8 00:30
19838

一. 概述    

Windows操作系统中,文件系统过滤驱动的过滤设备绑定在文件系统(FSD)设备之上,监视和过滤我们的文件访问。当应用层发起文件操作调用时内核层都会转换为IRP发送到设备栈中位于栈顶的设备,然后通过IO栈单元(IO_STACK_LOCATION)保存一些参数把IRP请求继续向下层设备发送,最后至FSD,由FSD完成实际的文件操作。卷参数块(VPB)保存了文件系统中在一个卷设备创建(卷的挂载)成功时的一些卷参数信息。wdm.h中对其结构定义如下:

文件系统过滤驱动对文件操作的过滤,实际上就是把过滤设备绑定在文件系统卷设备: Vpb->DeviceObject上来实现过滤的。

这里要总结的是:如何在内核层自己创建并填写IRP及下层栈单元,把请求直接送往FSD的卷设备上。常用的文件操作包括: 

文件的生成/打开(IRP_MJ_CREATE),

文件的关闭(IRP_MJ_CLEANUP:  表示句柄数为0,IRP_MJ_CLOSE: 表示文件对象引用计数为0,即将销毁),

文件读/写(IRP_MJ_READ/IRP_MJ_WRITE),

文件设置(IRP_MJ_SET_INFORMATION)

文件查询(IRP_MJ_QUERY_INFORMATION)

目录下项查询(MajorFuncton: IRP_MJ_DIRECTORY_CONTROL, MinorFunction: IRP_MN_QUERY_DIRECTORY)

每类操作对应的内核函数如下表所示:


二. 自己发送IRP请求操作文件的封装:

1. 实现MDL链解锁及释放,完成例程的功能, 后续的各IRP完成时统一使用

(1)解锁并释放MDL链

(2)文件操作完成例程:



2. 文件的打开/生成: 与其它IRP不同,在发送IRP_MJ_CREATE请求前就需要创建并填写好文件对象,FSD的卷设备可以从根目录pRootFileObject->Vpb->DeviceObject得到,对应的卷管理器生成的卷设备可以从: pRootFileObject->Vpb->RealDevice得到

    (1)创建文件对象可以用已导出但未公开的nt!ObCreateObject, 该函数原型如下:

      (2) 设置安全状态可以用已导出但未公开的nt!SeCreateAccessState,该函数原型如下:

    (3)自己创建并发送IRP进行文件打开/生成的实现:


3. 文件关闭的实现:  分两步:1)发送主功能号为IRP_MJ_CLEANUP的IRP向FSD表明: 文件对象句柄数已为0 =>发送主功能号为IRP_MJ_CLOSE的IRP向FSD表明: 文件对象引用计数已为0, 由FSD负责销毁文件对象

(1)文件关闭1: IRP_MJ_CLEANUP

(2) 文件关闭2:IRP_MJ_CLOSE


4. 文件读/写的实现:


5. 文件设置的实现:


6. 文件查询的实现:


7. 目录下项查询项查询的实现:  IRP主功能号为: IRP_MJ_DIRECTORY_CONTROL, 次功能号为: IRP_MN_QUERY_DIRECTORY


三. 文件IRP操作测试: 这里实现常用的几个功能就可以把前面的文件操作涵盖

  文件删除


2. 文件更名: 

(1) 如果仅仅是当前路径下的文件更名, 可以把下层的栈单元参数: pIrpSp->Parameters.SetFile.FileObject = NULL

(2) 这里着重要说明的是第2种情况, 把文件移动到同一卷的不同目录下,这方面的例子比较少,经过逆向分析nt!NtSetInformationFile=>nt!IopOpenLinkOrRenameTarget,可以得出: 需要新创建移动后的目标文件对象pTargetFileObject,并填写: pIrpSp->Parameters.SetFile.FileObject=pTargetFileObject

Win10 X64(1809)分析过程如下(由于函数体过长,一些无关紧要的就不贴出来了, 有兴趣的可以自己慢慢分析,这里只贴出关键处的分析):

1)nt!NtSetInformationFile分析关键处: 

在这个内核函数中对于类型为:FileRenameInformation,FileLinkInformation的文件设置会检查:

 pFileLinkOrRenameInfo->FileName及FileLinkOrRenameInfo->RootDirectory,当:  pFileLinkOrRenameInfo->FileName[0]==L'\\'或pFileLinkOrRenameInfo->RootDirectory!=NULL时会执行调用nt!IopOpenLinkOrRenameTarget创建移动后的目标文件, 根据返回的文件句柄就可以很容易得到目标文件对象指针pTargetFileObject了。


2)nt!IopOpenLinkOrRenameTarget分析

根据对nt!NtSetInformationFile进行的分析,不难推出:nt!IopOpenLinkOrRenameTarget函数原型如下:

pFileLinkOrRenameInfo->FileName[0]==L'\\'或pFileLinkOrRenameInfo->RootDirectory!=NULL时都会nt!NtSetInformationFile调用nt!IopOpenLinkOrRenameTarget, 这里考虑: pFileLinkOrRenameInfo->FileName包含完整路径的情况就可以实现后面的功能了。

分析关键处:


(3)根据前面的分析,得出文件更名IRP操作的实现:



3. 文件的拷贝/移动: 打开源文件及目标文件,从源文件读取再写入目标文件就可以了,如果需要移动,拷贝成功后再删除源文件。

#define FILE_TRANS_BUF_MAXLEN (4*1024*1024)


[培训]科锐逆向工程师培训第53期2025年7月8日开班!

最后于 2020-12-8 11:42 被低调putchar编辑 ,原因:
收藏
免费 18
支持
分享
最新回复 (46)
雪    币: 80
活跃值: (74)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
学习了,谢谢大佬
2020-12-8 20:33
0
雪    币: 2750
活跃值: (5122)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
学习一下,感谢分享
2020-12-8 22:42
0
雪    币: 83
活跃值: (1092)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
4
jianro兼容win7 到10?
2020-12-9 06:44
0
雪    币: 2674
活跃值: (2304)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
5
killpy jianro兼容win7 到10?

嗯!是兼容的!对ntfs/fastfat(fat32)文件系统发送IRP操作文件,各个版本的windows基本相同,

以前做小工具的时候从WinXP SP2到Win7 SP1都用过,Win10上现在也进行了调试,全部兼容。这里做个总结!

最后于 2020-12-9 09:32 被低调putchar编辑 ,原因:
2020-12-9 09:14
0
雪    币: 3187
活跃值: (13676)
能力值: ( LV12,RANK:322 )
在线值:
发帖
回帖
粉丝
6
好总结
2020-12-9 16:21
0
雪    币: 1056
活跃值: (2588)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
7
感谢分享
2020-12-9 17:38
0
雪    币: 42947
活跃值: (65767)
能力值: (RANK:135 )
在线值:
发帖
回帖
粉丝
8
感谢分享!
2020-12-9 17:49
0
雪    币: 13503
活跃值: (6919)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
9
膜拜,看原理应该跟pchunter差不多,绕过ssdt函数直接自行下发irp
2020-12-9 18:12
0
雪    币: 3888
活跃值: (4918)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
都快忘得差不多了
2020-12-9 20:16
0
雪    币: 2674
活跃值: (2304)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
11
hhkqqs 膜拜,看原理应该跟pchunter差不多,绕过ssdt函数直接自行下发irp[em_63]

您也不错!
实际中应该还有不少的工作量,比如下发IRP请求前: 检查FSD的IRP钩子,inline Hook钩子(ntfs.sys/fastfat.sys,属于文件系统驱动, 低版本的PG应该不会扫的),如果发现钩子就摘除一下,再下发下去。我这里主要是回顾和总结一下文件驱动方面的一些知识。

最后于 2020-12-21 23:03 被低调putchar编辑 ,原因:
2020-12-9 20:43
0
雪    币: 682
活跃值: (1515)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
除了nb以外说不出别的话来
2020-12-9 21:32
0
雪    币: 143
活跃值: (3356)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
13
感谢分享
2020-12-10 14:49
0
雪    币: 4064
活跃值: (4402)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
14
感谢分享
2020-12-10 22:09
0
雪    币: 1988
活跃值: (1566)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
15
虽然东西是老知识,但是做了这么详细的总结以及从win7到win10 都可以用,不错点赞!!!!!!!!!!!
2020-12-11 09:02
2
雪    币: 1077
活跃值: (3633)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
厉害!
2020-12-11 10:36
0
雪    币: 1599
活跃值: (5198)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
17
感谢分享!
2020-12-16 11:14
0
雪    币: 16
活跃值: (527)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
请问大佬,如果采用你上面说的那个直接发送给最底层的设备对象,过掉中间的过滤设备,这种情况下的请求有什么办法能过滤到吗
2020-12-16 16:54
0
雪    币: 2674
活跃值: (2304)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
19
沉醉星渊 请问大佬,如果采用你上面说的那个直接发送给最底层的设备对象,过掉中间的过滤设备,这种情况下的请求有什么办法能过滤到吗
微过滤管理器L"\\FileSystem\\FltMgr"本质上还是一个文件系统过滤驱动FltMgr.sys,通过把过滤设备绑定在pVpb->DeviceObject卷设备之上来监控文件操作的,如果发送IRP请求时指定ntfs.sys/fastfat.sys的文件系统卷设备pVpb->DeviceObject,请求不会送往过滤设备,MiniFilter是捕获不到的。
如果HOOK了ntfs.sys/fastfat.sys派发例程, pDriverObject->MajorFunction就可以过滤到。
但HOOK是可以摘除的,如果下发IRP请求前检测并摘除了IRP钩子/inline Hook钩子就可以绕过。
2020-12-16 17:32
0
雪    币: 16
活跃值: (527)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
明白了,谢谢您的回答
2020-12-16 18:32
0
雪    币: 16
活跃值: (527)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
请问大佬,有没有hook ntfs.sys派遣函数的例子呀
2020-12-17 09:44
0
雪    币: 2674
活跃值: (2304)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
22
沉醉星渊 请问大佬,有没有hook ntfs.sys派遣函数的例子呀

可以实现!成本比较高!一些标签水印系统就是HOOK的fsd的MajorFunction.
可以通过ObReferenceObjectByName获得名为: L“\\FileSystem\\Ntfs”和L"\\FileSystem\\fastfat"的驱动对象pDriverNtfs和pDriverFastFat.
然后: 对于关注的IRP把驱动对象对应的MajFunction替换成自己的MajForunction或者inline Hook后跳转到你的MajorFunction
实现上工作量比较大,处理上需要很细致:
FSD有文件系统控制设备和文件系统卷设备,文件系统控制设备可以收到卷的挂载, 文件系统识别器处理文件系统真正加载等有关的请求,文件系统卷设备会收到一些文件/目录的操作请求。


文件系统控制设备也能收到:IRP_MJ_CREATE, IRP_MJ_CLEANUP, IRP_MJ_CLOSE等请求

所以有必要区分文件系统的控制设备和卷设备,另外视情况决定是否要过滤卷影设备。

区分文件系统的控制设备和卷设备比较靠谱的方法就是:

IoRegisterFsRegistrationChange注册一个文件系统变动回调

//注册文件系统变动回调
NTSTATUS IoRegisterFsRegistrationChange(  
  PDRIVER_OBJECT          DriverObject,   //你的驱动对象指针
  PDRIVER_FS_NOTIFICATION DriverNotificationRoutine   //文件系统变动回调
);

//文件系统变动回调
void DriverFsNotification(
  _DEVICE_OBJECT *DeviceObject,  //激活或注销的文件系统的控制设备(包括文件系统识别器控制设备)
  BOOLEAN FsActive  当文件系统激活时FsActive为TRUE,文件系统注销时FsActive为FALSE
)

自己增删设备维护一个设备链(包含了文件系统识别器及当前已激活的文件系统的控制设备),只要不在设备链中的设备可以视为文件系统卷设备(包括卷影)。

卷影设备的判断sfilter中有例子: 

2d4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6q4P5s2m8x3K9h3k6W2x3o6l9I4x3g2)9J5c8Y4y4X3K9h3I4@1k6i4u0Q4x3X3b7I4

可以自行学习掌握。


有人可能会想到另一种方法: 用ObQueryNameString查询设备栈最底层的设备名,对于没有名字的就认为是文件系统卷设备。

但除了IRP_MJ_CREATE/IRP_MJ_CLOSE/IRP_MJ_CLEANUP外,文件系统其余IRP请求中用ObQueryNameString容易死锁。所以还是

注册一个文件系统变动回调自己增删设备维护一个设备链的方法稳妥些。


最后于 2020-12-20 18:44 被低调putchar编辑 ,原因:
2020-12-17 10:07
0
雪    币: 2674
活跃值: (2304)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
23
沉醉星渊 请问大佬,有没有hook ntfs.sys派遣函数的例子呀

以HOOK 64位ntfs主功能号为IRP_MJ_DIRECTORY_CONTROL派遣函数为例,的伪代码

1. 获得驱动对象指针伪代码:

extern POBJECT_TYPE *IoDriverObjectType; //自己外部声明: 导出驱动对象类型

//一个未公开的内核API函数,直接声明一下就可以用了
NTKERNELAPI
NTSTATUS NTAPI ObReferenceObjectByName(
	IN PUNICODE_STRING ObjectName,
	IN ULONG Attributes,
	OUT PACCESS_STATE AccessState OPTIONAL,
	IN ACCESS_MASK DesiredAccess,
	IN POBJECT_TYPE ObjectType OPTIONAL,
	IN KPROCESSOR_MODE AccessMode,
	IN PVOID ParseContext OPTIONAL,
	OUT PVOID *Object
);

...
//获得ntfs驱动对象指针
RtlInitUnicodeString(&unsObjectName,
    L"\\FileSystem\\Ntfs");
ntStatus=ObReferenceObjectByName(&unsObjectName,
    OBJ_CASE_INSENSITIVE,
    NULL,
    FILE_READ_DATA|FILE_WRITE_DATA,
    *IoDriverObjectType,
    KernelMode,
    NULL,
    (PVOID*)&g_pNtfsDriver);
if(NT_SUCCESS(ntStatus)){
    ...
    //挂钩
    //...
    ObDereferenceObject((PVOID)g_pNtfsDriver);
    ...
}

2. IRP钩子伪代码:

//IRP挂钩
if(NT_SUCCESS(ntStatus)){
    //保存ntfs以前的派遣例程,把ntfs的派遣例程替换成你自己的钩子函数
    g_sysDispatchDirectoryControl=InterlockedExchangePointer(&g_pNtfsDriver->MajorFunction[IRP_MJ_DIRECTORY_CONTROL],
         DispatchDirectoryControlHook);

    ObDereferenceObject((PVOID)pNtfsDriver);
}
...
//钩子函数
NTSTATUS DispatchDirectoryControlHook(IN PDEVICE_OBJECT pDeviceObject,
    IN PIRP pIrp)
{
     NTSTATUS ntStatus;
     
     //执行前过滤
     //...
     
     ntStatus=g_sysDispatchDirectoryControl(pDeviceObject,
         pIrp); //执行ntfs以前的派遣例程
         
     //执行后的过滤
     //...   
         
     return ntStatus;
}


2. inline hook 钩子伪代码

//中转函数(类似32位inline hook中的__declspec(naked)裸函数功能,必须保证至少容纳24字节)
//在hook时,前12字节被替换为: g_pNtfsDriver->MajorFunction[IRP_MJ_DIRECTORY_CONTROL]的前12个字节节
//然后: mov rax, imm64(机器码: 48 b8 64位立即数)
//jmp rax(机器码: ff e0)
//在hook时, imm64被替换为: (ULONG_PTR)g_pNtfsDriver->MajorFunction[IRP_MJ_DIRECTORY_CONTROL]+12
NTSTATUS NakedDispatchDirectoryControl(
	IN PDEVICE_OBJECT pDeviceObject,
	IN PIRP pIrp
)
{
   //这里可以自己任意写些功能性代码,只要保证函数体长度不少于24字节即可
   ...
}


//inline hook
VOID InlineHook(VOID)
{
    //mov rax,imm64 (机器码: 48 b8 64位立即数)
	//jmp rax(机器码: ff e0)
	UCHAR ucCode[12] = { 0x48,0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xe0 },  //imm64: 为钩子函数HookedDispatchDirectoryControl地址
	      ucCodeNaked[12] = { 0x48,0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xe0 }; //imm64: 为ntfs派遣例程地址+12
    ULONGLONG ullAddr;
    KIRQL kSavedIrql;
    
    //保存ntfs派遣例程的前12字节指令集
	/*
	    WIN7 SP0
		PAGE:00000000000B1680                 mov     [rsp+arg_0], rbx
        PAGE:00000000000B1685                 mov     [rsp+arg_8], rdx
        PAGE:00000000000B168A                 push    rsi
        PAGE:00000000000B168B                 push    rdi
	*/
	/*
	    WIN7 SP1
		PAGE:00000000000A97E0                 mov     [rsp+arg_0], rbx
        PAGE:00000000000A97E5                 mov     [rsp+arg_8], rdx
        PAGE:00000000000A97EA                 push    rsi
        PAGE:00000000000A97EB                 push    rdi
	*/
	RtlCopyMemory(g_oldCode,
		g_pNtfsDriver->MajorFunction[IRP_MJ_DIRECTORY_CONTROL],
		12);

	//钩子派遣例程地址
	ullAddr = (ULONGLONG)HookedDispatchDirectoryControl; 
	RtlCopyMemory(&ucCode[2],
		&ullAddr,
		8);

	//ntfs派遣例程地址的12字节偏处
	ullAddr = (ULONGLONG)g_pNtfsDriver->MajorFunction[IRP_MJ_DIRECTORY_CONTROL] + 12;
	RtlCopyMemory(&ucCodeNaked[2],
		&ullAddr,
		8);

	
         CloseWriteProtect(&kSavedIrql); //关闭写保护(自实现的中断请求级别提升及CR0的bit16置0)
	
	//Patch掉NakedDispatchDirectoryControl的前24字节
	//调用时, 先执行ntfs派遣例程原来的12字节指令集,然后跳转到ntfs派遣例程+12处,发挥类似裸函数的功能
	RtlCopyMemory(NakedDispatchDirectoryControl,
		g_oldCode,
		12); 
	RtlCopyMemory((PUCHAR)NakedDispatchDirectoryControl + 12,
		ucCodeNaked,
		12); 

	//修改ntfs派遣例程前12字节,以便跳转到我们的钩子函数
	RtlCopyMemory(g_pNtfsDriver->MajorFunction[IRP_MJ_DIRECTORY_CONTROL],
		ucCode,
		12);  
	
	RestoreWriteProtect(kSavedIrql); //恢复写保护(自实现的中断请求级别恢复及CR0bit16置1)
    
    return;
}

//inline unhook
VOID UnInlineHook(VOID)
{
    KIRQL kSavedIrql;
    
    CloseWriteProtect(&kSavedIrql); //关闭写保护(自实现的中断请求级别提升及CR0的bit16置0)
    RtlCopyMemory(g_pNtfsDriver->MajorFunction[IRP_MJ_DIRECTORY_CONTROL],
        g_oldCode,
        12); //恢复派遣例程前12字节
    RestoreWriteProtect(kSavedIrql); //恢复写保护 (自实现的中断请求级别恢复及CR0bit16置1)
    
    return;
}

...

//inline hook挂钩
if(NT_SUCCESS(ntStatus)){
    InlineHook();
    ObDereferenceObject((PVOID)g_pNtfsDriver);
}
...

//钩子函数
NTSTATUS DispatchDirectoryControlHook(IN PDEVICE_OBJECT pDeviceObject,
    IN PIRP pIrp)
{
    NTSTATUS ntStatus;
    
    //执行前过滤
    //... 
    
    //
	// 转去中转函数(类似32位inline hook的裸函数),最终转去执行ntfs的原来的派遣例程功能(这里作了改进,不用考虑线程同步)
	//
    ntStatus=NakedDispatchDirectoryControl(pDeviceObject,
        pIrp);
 
    //执行后的过滤
    //...  
 
    return ntStatus;
 }
最后于 2020-12-22 20:30 被低调putchar编辑 ,原因:
2020-12-20 15:46
0
雪    币: 16
活跃值: (527)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
谢谢大佬,我去研究一下
2020-12-21 14:39
0
雪    币: 300
活跃值: (2772)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
nice
2020-12-21 14:44
0
游客
登录 | 注册 方可回帖
返回