遍历FSD驱动派遣函数与Hook检测
2023-8-21 18:0:17 Author: 看雪学苑(查看原文) 阅读量:5 收藏

FSD(File System Drivers)位于系统底层,是和磁盘驱动最近的地方。它平时存在感不强,但由于本身身处要地,所以经常被各种恶意程序、安全软件以及其他怀有各种纯洁和不纯洁目的的应用盯上。因此,作为Windows底层Ctrl C&V安全工程师,我们要关注这个地方。


从实践到理论

理论总是枯燥的,为了不一开始就陷入到枯燥的理论中,我们不妨从大家熟悉的、看得见的地方着手。

下图是笔者机器上磁盘管理器中各个分区的情况。可以看到共有C、D、X三个分区,分别为NTFS、FAT32、CDFS(光盘)格式。

在Windows的世界里,分区的名字叫做“卷”(如上图的D盘就叫新加卷)。如果想要存取卷上的数据,那么就需要数据按照文件系统要求的格式组织存放。上面的NTFS、FAT32、CDFS即为各个卷的文件系统,数据按照各个文件系统的规定格式存储在各个卷的介质上从而形成了大家平时在各个盘上看到的文件、文件夹等。

Windows是以树状结构来管理各个卷以及其对应文件系统的。下图即上面各个盘符对应的设备对象树。

从上面的树状图可见,笔者的三个分区(包括“系统保留”分区)分别对应三个PDO设备,设备名分别叫做\Device\HarddiskVolume[1-3],这些设备都挂载在VolMgr下(也就是卷管理器)。

除去VolMgr,这几个HarddiskVolumeX属于最底层的物理设备,大家可以理解这几个设备代表的就是硬盘上的多个扇区集合。

再接下来位于设备栈次底层的是\Device\fvevol,这个设备即大名鼎鼎的Bitlocker加密设备,该设备负责解密从物理扇区中读取的数据并传递到他上层的文件系统驱动中——或者反过来——将上层文件系统驱动试图写入到磁盘上的数据进行加密最终存储到磁盘上。

最后再经过0到N层,我们会在设备树上看到该卷(分区)使用的文件系统,上图中分别是Ntfs和fastfat(FAT32)。

如果想要遍历每个卷的文件系统,可以通过_DEVICE_OBJECT的Vpb成员获取DeviceObject得到。下面以\Device\HarddiskVolume3为例进行说明,首先看一下设备树几个关键数据结构的截图:

接下来我们从调试器中观察上图中相关的数据结构,从\Device\HarddiskVolume3的FSDevice设备对象开始:

    

kd> dt _DEVICE_OBJECT 0xfffffa8005bcf970
nt!_DEVICE_OBJECT
+0x000 Type : 0n3
+0x002 Size : 0x620
+0x004 ReferenceCount : 0n0
+0x008 DriverObject : 0xfffffa80`0497b750 _DRIVER_OBJECT
+0x010 NextDevice : 0xfffffa80`05683950 _DEVICE_OBJECT
+0x018 AttachedDevice : 0xfffffa80`05be74e0 _DEVICE_OBJECT
+0x020 CurrentIrp : (null)
+0x028 Timer : (null)
+0x030 Flags : 0
+0x034 Characteristics : 0
+0x038 Vpb : (null)
+0x040 DeviceExtension : 0xfffffa80`05bcfac0 Void
+0x048 DeviceType : 8
+0x04c StackSize : 8 ''
+0x050 Queue : <unnamed-tag>
+0x098 AlignmentRequirement : 1
+0x0a0 DeviceQueue : _KDEVICE_QUEUE
+0x0c8 Dpc : _KDPC
+0x108 ActiveThreadCount : 0
+0x110 SecurityDescriptor : (null)
+0x118 DeviceLock : _KEVENT
+0x130 SectorSize : 0x200
+0x132 Spare1 : 1
+0x138 DeviceObjectExtension : 0xfffffa80`05bcff90 _DEVOBJ_EXTENSION
+0x140 Reserved : (null)

然后观察其DriverObject成员即可获取该设备使用的文件系统:

    

kd> dt _DRIVER_OBJECT 0xfffffa80`0497b750
nt!_DRIVER_OBJECT
+0x000 Type : 0n4
+0x002 Size : 0n336
+0x008 DeviceObject : 0xfffffa80`05bcf970 _DEVICE_OBJECT
+0x010 Flags : 0x92
+0x018 DriverStart : 0xfffff880`03e86000 Void
+0x020 DriverSize : 0x36000
+0x028 DriverSection : 0xfffffa80`0394b660 Void
+0x030 DriverExtension : 0xfffffa80`0497b8a0 _DRIVER_EXTENSION
+0x038 DriverName : _UNICODE_STRING "\FileSystem\fastfat"
+0x048 HardwareDatabase : 0xfffff800`04157558 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
+0x050 FastIoDispatch : 0xfffff880`03e8d3c0 _FAST_IO_DISPATCH
+0x058 DriverInit : 0xfffff880`03eb8a0c long fastfat!GsDriverEntry+0
+0x060 DriverStartIo : (null)
+0x068 DriverUnload : 0xfffff880`03e87d8c void fastfat!FatUnload+0
+0x070 MajorFunction : [28] 0xfffff880`03e95718 long fastfat!FatFsdCreate+0

这里需要注意FastIoDispatch和MajorFunction成员,这里面即文件系统驱动在打开、读写卷时要用到的函数:

    

kd> dx -id 0,0,fffffa80036f4380 -r1 ((ntkrnlmp!_FAST_IO_DISPATCH *)0xfffff88003e8d3c0)
((ntkrnlmp!_FAST_IO_DISPATCH *)0xfffff88003e8d3c0) : 0xfffff88003e8d3c0 [Type: _FAST_IO_DISPATCH *]
[+0x000] SizeOfFastIoDispatch : 0xe0 [Type: unsigned long]
[+0x008] FastIoCheckIfPossible : 0xfffff88003ea1954 : fastfat!FatFastIoCheckIfPossible+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,unsigned long,unsigned char,unsigned long,unsigned char,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x010] FastIoRead : 0xfffff800040e7a60 : ntkrnlmp!FsRtlCopyRead+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,unsigned long,unsigned char,unsigned long,void *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x018] FastIoWrite : 0xfffff80004086970 : ntkrnlmp!FsRtlCopyWrite+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,unsigned long,unsigned char,unsigned long,void *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x020] FastIoQueryBasicInfo : 0xfffff88003ea1a3c : fastfat!FatFastQueryBasicInfo+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,unsigned char,_FILE_BASIC_INFORMATION *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x028] FastIoQueryStandardInfo : 0xfffff88003ea1ba0 : fastfat!FatFastQueryStdInfo+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,unsigned char,_FILE_STANDARD_INFORMATION *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x030] FastIoLock : 0xfffff88003eaafe0 : fastfat!FatFastLock+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,_LARGE_INTEGER *,_EPROCESS *,unsigned long,unsigned char,unsigned char,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x038] FastIoUnlockSingle : 0xfffff88003eab15c : fastfat!FatFastUnlockSingle+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,_LARGE_INTEGER *,_EPROCESS *,unsigned long,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x040] FastIoUnlockAll : 0xfffff88003eab2a0 : fastfat!FatFastUnlockAll+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_EPROCESS *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x048] FastIoUnlockAllByKey : 0xfffff88003eab3d8 : fastfat!FatFastUnlockAllByKey+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,void *,unsigned long,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x050] FastIoDeviceControl : 0x0 : 0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,unsigned char,void *,unsigned long,void *,unsigned long,unsigned long,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x058] AcquireFileForNtCreateSection : 0x0 : 0x0 [Type: void (__cdecl*)(_FILE_OBJECT *)]
[+0x060] ReleaseFileForNtCreateSection : 0x0 : 0x0 [Type: void (__cdecl*)(_FILE_OBJECT *)]
[+0x068] FastIoDetachDevice : 0x0 : 0x0 [Type: void (__cdecl*)(_DEVICE_OBJECT *,_DEVICE_OBJECT *)]
[+0x070] FastIoQueryNetworkOpenInfo : 0xfffff88003ea1cdc : fastfat!FatFastQueryNetworkOpenInfo+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,unsigned char,_FILE_NETWORK_OPEN_INFORMATION *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x078] AcquireForModWrite : 0x0 : 0x0 [Type: long (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,_ERESOURCE * *,_DEVICE_OBJECT *)]
[+0x080] MdlRead : 0xfffff800040f73f0 : ntkrnlmp!FsRtlMdlReadDev+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,unsigned long,unsigned long,_MDL * *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x088] MdlReadComplete : 0xfffff80003c642a0 : ntkrnlmp!FsRtlMdlReadCompleteDev+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_MDL *,_DEVICE_OBJECT *)]
[+0x090] PrepareMdlWrite : 0xfffff800040f80e0 : ntkrnlmp!FsRtlPrepareMdlWriteDev+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,unsigned long,unsigned long,_MDL * *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]
[+0x098] MdlWriteComplete : 0xfffff80003f4b97c : ntkrnlmp!FsRtlMdlWriteCompleteDev+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,_MDL *,_DEVICE_OBJECT *)]
[+0x0a0] FastIoReadCompressed : 0x0 : 0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,unsigned long,unsigned long,void *,_MDL * *,_IO_STATUS_BLOCK *,_COMPRESSED_DATA_INFO *,unsigned long,_DEVICE_OBJECT *)]
[+0x0a8] FastIoWriteCompressed : 0x0 : 0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,unsigned long,unsigned long,void *,_MDL * *,_IO_STATUS_BLOCK *,_COMPRESSED_DATA_INFO *,unsigned long,_DEVICE_OBJECT *)]
[+0x0b0] MdlReadCompleteCompressed : 0x0 : 0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_MDL *,_DEVICE_OBJECT *)]
[+0x0b8] MdlWriteCompleteCompressed : 0x0 : 0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,_MDL *,_DEVICE_OBJECT *)]
[+0x0c0] FastIoQueryOpen : 0x0 : 0x0 [Type: unsigned char (__cdecl*)(_IRP *,_FILE_NETWORK_OPEN_INFORMATION *,_DEVICE_OBJECT *)]
[+0x0c8] ReleaseForModWrite : 0x0 : 0x0 [Type: long (__cdecl*)(_FILE_OBJECT *,_ERESOURCE *,_DEVICE_OBJECT *)]
[+0x0d0] AcquireForCcFlush : 0xfffff88003ead768 : fastfat!FatAcquireForCcFlush+0x0 [Type: long (__cdecl*)(_FILE_OBJECT *,_DEVICE_OBJECT *)]
[+0x0d8] ReleaseForCcFlush : 0xfffff88003ead800 : fastfat!FatReleaseForCcFlush+0x0 [Type: long (__cdecl*)(_FILE_OBJECT *,_DEVICE_OBJECT *)]
kd> dx -id 0,0,fffffa80036f4380 -r1 (*((ntkrnlmp!long (__cdecl*(*)[28])(_DEVICE_OBJECT *,_IRP *))0xfffffa800497b7c0))
(*((ntkrnlmp!long (__cdecl*(*)[28])(_DEVICE_OBJECT *,_IRP *))0xfffffa800497b7c0)) [Type: long (__cdecl* [28])(_DEVICE_OBJECT *,_IRP *)]
[0] : 0xfffff88003e95718 : fastfat!FatFsdCreate+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[1] : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[2] : 0xfffff88003e94bd0 : fastfat!FatFsdClose+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[3] : 0xfffff88003e8891c : fastfat!FatFsdRead+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[4] : 0xfffff88003e89350 : fastfat!FatFsdWrite+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[5] : 0xfffff88003ea1ef4 : fastfat!FatFsdQueryInformation+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[6] : 0xfffff88003ea1f8c : fastfat!FatFsdSetInformation+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[7] : 0xfffff88003e9f010 : fastfat!FatFsdQueryEa+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[8] : 0xfffff88003e9f010 : fastfat!FatFsdQueryEa+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[9] : 0xfffff88003ea5954 : fastfat!FatFsdFlushBuffers+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[10] : 0xfffff88003eb0c84 : fastfat!FatFsdQueryVolumeInformation+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[11] : 0xfffff88003eb0d1c : fastfat!FatFsdSetVolumeInformation+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[12] : 0xfffff88003e9b550 : fastfat!FatFsdDirectoryControl+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[13] : 0xfffff88003ea6694 : fastfat!FatFsdFileSystemControl+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[14] : 0xfffff88003e9a358 : fastfat!FatFsdDeviceControl+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[15] : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[16] : 0xfffff88003ead904 : fastfat!FatFsdShutdown+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[17] : 0xfffff88003eaaf48 : fastfat!FatFsdLockControl+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[18] : 0xfffff88003e940c4 : fastfat!FatFsdCleanup+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[19] : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[20] : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[21] : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[22] : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[23] : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[24] : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[25] : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[26] : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]
[27] : 0xfffff88003eabf34 : fastfat!FatFsdPnp+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]

看一下上面得到的函数是否和PCHunter中的一致:

OK,到此我们从实际机器的分区开始,一步步搞清楚了文件系统驱动中(PCHunter的FSD标签页)每个回调函数是怎么来的,而且通过WinDBG观察了FSD中每个回调函数的当前地址。

相信读到此处,各位应该知道如何获取当前FSD驱动派遣函数地址了:

    

#define IRP_MJ_MINIMUM_FUNCTION 0x00
#define IRP_MJ_MAXIMUM_FUNCTION 0x1c
#define FAST_IO_MINIMUM_FUNCTION 0x00
#define FAST_IO_MAXIMUM_FUNCTION 0x1c

ULONG_PTR Index = 0;
PFAST_IO_DISPATCH FastIORoutines = FSD->FastIoDispatch;
PDRIVER_DISPATCH *IRPRoutines = FSD->MajorFunction;

for (Index = IRP_MJ_MINIMUM_FUNCTION; Index < IRP_MJ_MAXIMUM_FUNCTION; ++Index)
{
PDRIVER_DISPATCH *FunctionAddress = IRPRoutines + Index;
}

for (Index = FAST_IO_MINIMUM_FUNCTION; Index < FAST_IO_MAXIMUM_FUNCTION; ++Index)
{
PULONG_PTR FunctionAddress = (PULONG_PTR)FastIORoutines + Index;
}

那么接下来的问题是,如何知道这些派遣函数的原始函数地址呢?方法其实很简单——找到初始化这些回调函数的地方即可。例如下图是NTFS文件系统(Ntfs.sys)初始化MajorFunction的地方。

上图中,以_Ntfs开头的函数即各个派遣函数的原始地址(IDA解析了符号因此显示的不是地址而是符号名)。不过在正式介绍如何获取原始函数之前,我们需要补充一些X86指令集的知识。


Intel指令简单介绍

为了得到原始函数地址,我们需要解析对应的汇编指令,也就是说我们需要解析字节码并从中识别出我们想要的指令

坏消息是,x86是复杂指令集,其指令格式有很多种情况。好消息是,我们并不需要解析全部的指令,仅解析我们感兴趣的部分即可,这种情况下问题规模较为可控,毕竟情况就那么几种、穷举可以覆盖。坏消息是,我们需要补充一些额外的知识才能理解解析过程中的字节码含义。

需要说明的是,本文中介绍的知识仅限于“获得原始函数地址”涉及到的很小一部分,而且无法覆盖整个x86指令集,甚至会出现属于待解决问题范围但是却没有覆盖到的情况。如果要获取权威的指令集说明,建议阅读文末的参考3。

X86指令集介绍

首先来看一下32位下指令集的格式 参考3:

32位下一个完整的指令字节码由以下几个字段组成:

◆0到多个Prefix:可选,每个Prefix占用1个字节。
◆Opcode:必选,占用1到3个字节。
◆ModR/M:可选,占用1个字节。
◆SIB:可选,占用1个字节。
◆Displacement:可选,占用1、2、4个字节(注意没有3字节的情况),此字段下文中简称Disp或DISP。
◆Immediate:可选,占用1、2、4个字节。

在整个解析字节码的过程中,我们主要关注的是:

◆Opcode:用于区分我们关注的场景,控制问题规模。
◆ModR/M:用于分离我们关注的寄存器(Register)与立即数偏移。
◆Immediate:用于分离原始函数的地址。

其中MRod/M字段进一步格式如下,后续计算FSD原函数地址涉及到的寄存器与立即数均需要从此字段中解析得出:

上图中的字段,三个成员含义如下:

◆Mod:两位,用于区分目的寄存器的大分类(详见下文中的表格)。
◆Reg:三位,用于确认原操作数使用的寄存器。
◆R/M:三位,结合Mod位,用于进一步确定目的地址的寻址格式从而分离出DISP偏移。

IA64(兼容X86_64)指令集介绍

64位下,其实指令格式和32位是一样的,只是有个叫做REX的Prefix需要被单独拿出来讨论。

REX格式

REX的前4位是固定的,后四位每个bit都代表一个标志位。要特别注意R位和B位,这两位在Mod不为11时应该分别前置于ModR/M的Reg位和R/M位,如下图:

讲了这么多,太抽象了,其实啰嗦了这么久本质上就是下面这张表参考2。

首先应该根据2位的Mod,也就上表红色箭头列,确定当前指令属于哪个大的分类。

然后根据R/M,也就是上表蓝色箭头列,继续确定目的操作数的寻址方式,即目的地址使用哪个寄存器(或内存地址)。

确认Mod和R/M后,可以在上表中确定一行,此时再看REX的B位,若REX.B为1,则应该看上表绿框中对应的行,否则看红框对应行。此时便可确定指令目的地址部分的指令格式。

最后,根据REX的R位,确认Reg。若Rex.R为1,则看上表蓝框中对应的Reg取值,否则看黄框的Reg取值。此时便确定了指令源操作数使用的寄存器。

Emmmmm, 依然很抽象, 那我们下文就以如下指令为例,实际解析一下吧:

    

48 89 05 1A 4F DE FF mov qword ptr ds:[rip-0x21b0e6], rax


指令解析实例

工欲善其事,必先利其器

上一节我们介绍了X86指令的格式,如果只单独分析一条指令的话工作量还可以,但是如果面对日常反汇编成百上千行代码显然无法一条条人工分析。

此时,一个开源工具就派上了大用场,这个工具就是Zydis,我们可以用这个反汇编引擎中提供的一个命令行工具来帮助我们快速解析字节码:

上图中使用ZydisInfo命令解析了64位下的一段指令,我们根据工具的输出,人工解析一下指令。

指令解析

上图中我们关注如下部分:

48 89 05 1A 4F DE FF
: : : :..DISP
: : :..MODRM
: :..OPCODE
:..REX

REX字段: 由0x48 = 01001000b, 可知REX.R = 0, REX.B = 0。MODRM: 由0x05 = 00000101b, 可知Mod = 00, Reg = 000, R/M = 101。由于Mod为00,因此定位位于下图中的蓝框区域所在的行。由于R/M为101且REX.B为0(竖直绿色箭头,只标识了REX.B为1的情况),因此可知汇编指令的dst形式为[RIP/EIP]+disp32(REX.B无论是1还是0形式都一样)。由于Reg为000且REX.R为0(水平绿色箭头)且当前为64位汇编,因此确认src的寄存器为RAX。

同时,我们知道dst的形式是[RIP/EIP]+disp32,因此还有32位(4个字节)的DISP,也就是0xFFDE4F1A(big-endian),经过计算为有符号数负0x21b0e6:

>> rax2 =16 '(0-0xffde4f1a)&0x00000000FFFFFFFF'
0x21b0e6

结合OPCODE为0x89 参考2为move指令 :

因此最终指令为:

48 89 05 1A 4F DE FF mov qword ptr ds:[rip-0x21b0e6], rax

那么,现在你已经对机器码分析基本原理有了一定了解,就让我们来看一下真正的原始FSD回调函数如何获取吧。


分析FSD初始化操作

思路上,我们需要重载一份FSD的内核模块,然后分析重载内核模块对MajorFunction与FastIoDispatch的初始化部分,得出模块原始的初始化地址,最后根据偏移计算初始化地址在现内核模块中的地址。

有关重载(内核)模块的内容,有很多其他资料,略……

经过实际分析,FSD初始化MajorFunction与FastIoDispatch的部分有三种情况,我们分开来讨论。

情况一

32位系统下,一般是直接赋值给结构体成员实现的,经过分析多个系统版本的代码,这种形式多见于对MajorFunction的初始化,例如下面的反汇编代码:

INIT:0005106B 53 push ebx ; DriverObject
……
INIT:0005109D C7 43 38 A8 93 02 00 mov dword ptr [ebx+38h], offset [email protected] ; FatFsdCreate(x,x)
>> ZydisInfo -32 C7 43 38 A8 93 02 00
== [ INTEL ] ============================================================================================
ABSOLUTE: mov dword ptr ds:[ebx+0x38], 0x293A8
RELATIVE: mov dword ptr ds:[ebx+0x38], 0x293A8

== [ SEGMENTS ] ============================================================================================
C7 43 38 A8 93 02 00
: : : :..IMM
: : :..DISP
: :..MODRM
:..OPCODE

上面mov dword ptr [ebx+38h], offset _FatFsdCreate这行操作,等价于如下C程序:

    

PDRIVER_OBJECT DriverObject = ...;
DriverObject->MajorFunction[IRP_MJ_CREATE] = (PDRIVER_DISPATCH)_FatFsdCreate;

所以上文中ebx为结构的起始地址,从上下文来看该结构是_DRIVER_OBJECT,因此只需要分析出ebx加的这一部分的值(DISP),然后根据_DRIVER_OBJECT对象计算出偏移对应的符号名即可得知是哪个函数。而DISP字段,通过上面ZydisInfo的输出已经分析出来了为0x38。

此处的问题是,我们如何用程序分析出上述我们需要的几个信息呢?好消息是,可以借助改造开源的ZydisInfo源码实现,坏消息是ZydisInfo的源码无法在内核下使用。由于笔者目前写的内核模块期望是all in one式的,因此需要在内核下解析二进制指令,于是笔者写了个临时的指令解析宏——注意,这个宏仅能正确解析有限模式的字节码、并不能保证所有情况下都能正确解析。

    

#define X64_PARSE_INSTRUCTION(__address, __opcode, __mod, __register, __rm, __sib, __disp_offset) \
{ \
UCHAR REX = 0; \
UCHAR MODRM = 0; \
UCHAR WorkOffset = 0; \
/* 0x64-0x67 is PREFIX opcode */ \
if (0x64 <= *(PUCHAR)__address && 0x67 >= *(PUCHAR)__address) \
{ \
WorkOffset += 1; \
} \
/* 0x40-0x47 is REX opcode area */ \
if (0x40 <= *((PUCHAR)__address + WorkOffset) && 0x4F >= *((PUCHAR)__address + WorkOffset)) \
{ \
REX = *(PUCHAR)((PUCHAR)__address + WorkOffset); \
WorkOffset += 1; \
} \
*__opcode = *((PUCHAR)__address + WorkOffset); \
WorkOffset += 1; \
/* If this is a 2 bytes opcode (which is started by 0x0f) */ \
if (0x0f == *__opcode) \
{ \
*__opcode = *(PUCHAR)((PUCHAR)__address + WorkOffset); \
WorkOffset += 1; \
} \
MODRM = *(PUCHAR)((PUCHAR)__address + WorkOffset); \
*__mod = (MODRM & 0xC0) >> 6; \
*__register = (((REX & 0x4) >> 2) << 3) | ((MODRM & 0x38) >> 3); \
*__rm = ((REX & 0x1) << 3) | (MODRM & 0x07); \
/* There will be a SIB byte when Mod != 11b && Rm == 100b */ \
if (0x3 != *__mod && 0x4 == *__rm) \
{ \
*__sib = *(PUCHAR)(__address + WorkOffset + 1); \
*__disp_offset = WorkOffset + 2; \
} \
else *__disp_offset = WorkOffset + 1; \
}

#define X86_PARSE_INSTRUCTION(__address, __opcode, __mod, __register, __rm, __disp_offset) \
{ \
UCHAR MODRM = 0; \
UCHAR WorkOffset = 0; \
/* 0x64-0x67 is PREFIX opcode */ \
if (0x64 <= *(PUCHAR)__address && 0x67 >= *(PUCHAR)__address) \
{ \
WorkOffset += 1; \
} \
*__opcode = *(PUCHAR)((PUCHAR)__address + WorkOffset); \
WorkOffset += 1; \
/* If this is a 2 bytes opcode (which is started by 0x0f) */ \
if (0x0f == *__opcode) \
{ \
*__opcode = *(PUCHAR)((PUCHAR)__address + WorkOffset); \
WorkOffset += 1; \
} \
MODRM = *(PUCHAR)((PUCHAR)__address + WorkOffset); \
*__mod = (MODRM & 0xC0) >> 6; \
*__register = (MODRM & 0x38) >> 3; \
*__rm = MODRM & 0x07; \
/* There will be a SIB byte when Mod != 11b && Rm == 100b */ \
if (0x3 != *__mod && 0x4 == *__rm) \
{ \
*__disp_offset = WorkOffset + 2; \
} \
else *__disp_offset = WorkOffset + 1; \
}

// i为存放待解析指令字节的内存指针
UCHAR OpCode = 0, Mod = 0xFF, Register = 0xFF, Rm = 0xFF, Sib = 0, DispOffset = 0;
X86_PARSE_INSTRUCTION(i, &OpCode, &Mod, &Register, &Rm, &DispOffset);
X64_PARSE_INSTRUCTION(i, &OpCode, &Mod, &Register, &Rm, &Sib, &DispOffset);

情况二

除了上述直接对偏移赋值的情况,还有一种间接赋值的情况。一般是先将原始函数赋值给一个寄存器,然后再将寄存器赋值给一块内存+偏移,通过分析多个系统的初始化代码,这种形式多见于对FAST_IO_DISPATCH的初始化,例如下面的情况:

// INIT:00000001C02BF980 48 8D 05 19 00 FA FF lea rax, NtfsFastIoCheckIfPossible
>> ZydisInfo -64 48 8D 05 19 00 FA FF
== [ INTEL ] ============================================================================================
ABSOLUTE: lea rax, ds:[0xFFFFFFFFFFFA0020]
RELATIVE: lea rax, ds:[rip-0x5FFE7]
== [ SEGMENTS ] ============================================================================================
48 8D 05 19 00 FA FF
: : : :..DISP
: : :..MODRM
: :..OPCODE
:..REX
// INIT:00000001C02BF987 48 89 05 1A 4F DE FF mov cs:NtfsFastIoDispatch.FastIoCheckIfPossible, rax
>> ZydisInfo -64 48 89 05 1A 4F DE FF
== [ SEGMENTS ] ============================================================================================
48 89 05 1A 4F DE FF
: : : :..DISP
: : :..MODRM
: :..OPCODE
:..REX

上面两行汇编代码,等价于如下C代码:

XXXX NtfsFastIoCheckIfPossible(....)
{
....
}

FAST_IO_DISPATCH FastIODispatch = {0};
FastIoDispatch.FastIoCheckIfPossible = NtfsFastIoCheckIfPossible;

针对这种情况,我们需要分析出mov指令中的DISP偏移与涉及到的寄存器,然后再向上寻找对该寄存器赋值的lea指令以及数据结构的起始地址,最后将DISP减去起始地址得出在结构中的成员偏移用于确定成员,最后根据寄存器值确定该成员的原始函数地址。

情况三

某些派遣函数在对应FSD模块中无法找到初始化的地方,例如下图是NTFS对MajorFunction的初始化部分:

虽然顺序是乱的,但是经过整理后发现对MajorFunction数组的下标操作并不是连续的(没有下标1、15的赋值)。这是因为在内核调用IoCreateDriver创建驱动对象时,会将对象的MajorFunction派遣函数默认全部设置为IopInvalidDeviceRequest:

    

int __stdcall IoCreateDriver(int a1, int a2)
{
PDRIVER_OBJECT v5, v29;
……
result = ObCreateObject(0, (int)IoDriverObjectType, &a3, 0, 0, 196, 0, 0, (int)&v29);
v5 = v29;
if ( result >= 0 )
{
memset(v29, 0, 0xC4u);
memset32(v5->MajorFunction, (int)IopInvalidDeviceRequest, 0x1Cu);
……
}
return result;
}

因此对于FSD中未初始化的MajorFunction(FastIoDispatch同理),依然保留了创建驱动对象时的默认值:


成果

上文中的理论部分,完成之后证实可以达成获取当前与原始FSD派遣函数的效果:

理解反汇编引擎hacker_disassembler_engine(HDE)

(https://wonderkun.cc/2019/03/16/%E7%90%86%E8%A7%A3%E5%8F%8D%E6%B1%87%E7%BC%96%E5%BC%95%E6%93%8Ehacker_disassembler_engine(HDE)/)

X86 Opcode and Instruction Reference Home

(http://ref.x86asm.net/coder64.html#modrm_byte_32_64)

Intel architectures software developer vol 2a manual

(https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-2a-manual.pdf)

看雪ID:Hacksign

https://bbs.kanxue.com/user-home-156241.htm

*本文为看雪论坛精华文章,由 Hacksign 原创,转载请注明来自看雪社区

# 往期推荐

1、在 Windows下搭建LLVM 使用环境

2、深入学习smali语法

3、安卓加固脱壳分享

4、Flutter 逆向初探

5、一个简单实践理解栈空间转移

6、记一次某盾手游加固的脱壳与修复

球分享

球点赞

球在看


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458513816&idx=1&sn=c0e69ff57b22e3903927ebea5554c8b4&chksm=b18ec11286f948042b65105c7d43602260a274d4db607c9aba77379f4e45c6d9b36cce469583&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh