Watch & Learn

Debugwar Blog

Step in or Step over, this is a problem ...

Iterate FSD driver dispatch functions and Hook detection

2023-04-10 @ UTC+0

FSD (File System Drivers) is located at the bottom of the system, closest to the disk driver. This thing usually doesn’t have a strong presence, but because it is in a critical position, it is often targeted by various malicious programs, security software, and other applications with various pure and impure purposes. Therefore, as a Windows bottom layer Copy&Past security engineer, we need to pay attention to this place.

From practice to theory

Theory is always boring. In order not to fall into the boring theory at the beginning, we might as well start from the familiar and visible places.

The picture below shows the situation of each partition in the disk manager on the author’s machine. You can see that there are C, D, and X partitions, which are NTFS, FAT32, and CDFS (CD) formats respectively.


In the world of Windows, the name of the partition is called “volume” (as the D drive in the picture above is called a "new volume"). If you want to access the data on the volume, then the data needs to be organized and stored according to the format required by the file system. The above NTFS, FAT32, CDFS are the file systems of each volume, and the data is stored on the media of each volume according to the prescribed format of each file system, thus forming the files and folders that we usually see on each disk.

Windows manages each volume and its corresponding file system in a tree structure. The picture below is the device object tree corresponding to the above drive letters.


From the above tree diagram, the author’s three partitions (including the “system reserved” partition) correspond to three PDO devices, the device names are called \Device\HarddiskVolume[1-3], and these devices are all mounted under VolMgr (that is, the volume manager).

Apart from VolMgr, these HarddiskVolumeX belong to the lowest level of physical devices. You can think that these devices represent multiple sector collections on the hard disk.

The following layer next to the bottom of the device stack is \Device\fvevol, this device is the famous Bitlocker encryption device, this device is responsible for decrypting the data read from the physical sector and passing it to the file system driver above it - or vice versa - encrypting the data that the upper file system driver tries to write to the disk and finally storing it on the disk.

Finally, after 0 to N layers, we will see the file system used by this volume (partition) on the device tree, which are Ntfs and fastfat (FAT32) in the picture above.

If you want to traverse the file system of each volume, you can get the DeviceObject through the Vpb member of _DEVICE_OBJECT. The following will explain with \Device\HarddiskVolume3 as an example, first look at the screenshots of several key data structures in the device tree:


Next, we observe the relevant data structures in the debugger from the FSDevice device object of \Device\HarddiskVolume3:

  1. kd> dt _DEVICE_OBJECT 0xfffffa8005bcf970  
  2. nt!_DEVICE_OBJECT  
  3.    +0x000 Type             : 0n3  
  4.    +0x002 Size             : 0x620  
  5.    +0x004 ReferenceCount   : 0n0  
  6.    +0x008 DriverObject     : 0xfffffa80`0497b750 _DRIVER_OBJECT  
  7.    +0x010 NextDevice       : 0xfffffa80`05683950 _DEVICE_OBJECT  
  8.    +0x018 AttachedDevice   : 0xfffffa80`05be74e0 _DEVICE_OBJECT  
  9.    +0x020 CurrentIrp       : (null)   
  10.    +0x028 Timer            : (null)   
  11.    +0x030 Flags            : 0  
  12.    +0x034 Characteristics  : 0  
  13.    +0x038 Vpb              : (null)   
  14.    +0x040 DeviceExtension  : 0xfffffa80`05bcfac0 Void  
  15.    +0x048 DeviceType       : 8  
  16.    +0x04c StackSize        : 8 ''  
  17.    +0x050 Queue            : <unnamed-tag>  
  18.    +0x098 AlignmentRequirement : 1  
  19.    +0x0a0 DeviceQueue      : _KDEVICE_QUEUE  
  20.    +0x0c8 Dpc              : _KDPC  
  21.    +0x108 ActiveThreadCount : 0  
  22.    +0x110 SecurityDescriptor : (null)   
  23.    +0x118 DeviceLock       : _KEVENT  
  24.    +0x130 SectorSize       : 0x200  
  25.    +0x132 Spare1           : 1  
  26.    +0x138 DeviceObjectExtension : 0xfffffa80`05bcff90 _DEVOBJ_EXTENSION  
  27.    +0x140 Reserved         : (null)   

Then observe its DriverObject member to get the file system used by this device:

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

Here you need to pay attention to the FastIoDispatch and MajorFunction members, which are the functions that the file system driver needs to use when opening, reading and writing volumes:

  1. kd> dx -id 0,0,fffffa80036f4380 -r1 ((ntkrnlmp!_FAST_IO_DISPATCH *)0xfffff88003e8d3c0)  
  2. ((ntkrnlmp!_FAST_IO_DISPATCH *)0xfffff88003e8d3c0)                 : 0xfffff88003e8d3c0 [Type: _FAST_IO_DISPATCH *]  
  3.     [+0x000] SizeOfFastIoDispatch : 0xe0 [Type: unsigned long]  
  4.     [+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 *)]  
  5.     [+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 *)]  
  6.     [+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 *)]  
  7.     [+0x020] FastIoQueryBasicInfo : 0xfffff88003ea1a3c : fastfat!FatFastQueryBasicInfo+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,unsigned char,_FILE_BASIC_INFORMATION *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]  
  8.     [+0x028] FastIoQueryStandardInfo : 0xfffff88003ea1ba0 : fastfat!FatFastQueryStdInfo+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,unsigned char,_FILE_STANDARD_INFORMATION *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]  
  9.     [+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 *)]  
  10.     [+0x038] FastIoUnlockSingle : 0xfffff88003eab15c : fastfat!FatFastUnlockSingle+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,_LARGE_INTEGER *,_EPROCESS *,unsigned long,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]  
  11.     [+0x040] FastIoUnlockAll  : 0xfffff88003eab2a0 : fastfat!FatFastUnlockAll+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_EPROCESS *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]  
  12.     [+0x048] FastIoUnlockAllByKey : 0xfffff88003eab3d8 : fastfat!FatFastUnlockAllByKey+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,void *,unsigned long,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]  
  13.     [+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 *)]  
  14.     [+0x058] AcquireFileForNtCreateSection : 0x0 : 0x0 [Type: void (__cdecl*)(_FILE_OBJECT *)]  
  15.     [+0x060] ReleaseFileForNtCreateSection : 0x0 : 0x0 [Type: void (__cdecl*)(_FILE_OBJECT *)]  
  16.     [+0x068] FastIoDetachDevice : 0x0 : 0x0 [Type: void (__cdecl*)(_DEVICE_OBJECT *,_DEVICE_OBJECT *)]  
  17.     [+0x070] FastIoQueryNetworkOpenInfo : 0xfffff88003ea1cdc : fastfat!FatFastQueryNetworkOpenInfo+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,unsigned char,_FILE_NETWORK_OPEN_INFORMATION *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]  
  18.     [+0x078] AcquireForModWrite : 0x0 : 0x0 [Type: long (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,_ERESOURCE * *,_DEVICE_OBJECT *)]  
  19.     [+0x080] MdlRead          : 0xfffff800040f73f0 : ntkrnlmp!FsRtlMdlReadDev+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,unsigned long,unsigned long,_MDL * *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]  
  20.     [+0x088] MdlReadComplete  : 0xfffff80003c642a0 : ntkrnlmp!FsRtlMdlReadCompleteDev+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_MDL *,_DEVICE_OBJECT *)]  
  21.     [+0x090] PrepareMdlWrite  : 0xfffff800040f80e0 : ntkrnlmp!FsRtlPrepareMdlWriteDev+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,unsigned long,unsigned long,_MDL * *,_IO_STATUS_BLOCK *,_DEVICE_OBJECT *)]  
  22.     [+0x098] MdlWriteComplete : 0xfffff80003f4b97c : ntkrnlmp!FsRtlMdlWriteCompleteDev+0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,_MDL *,_DEVICE_OBJECT *)]  
  23.     [+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 *)]  
  24.     [+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 *)]  
  25.     [+0x0b0] MdlReadCompleteCompressed : 0x0 : 0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_MDL *,_DEVICE_OBJECT *)]  
  26.     [+0x0b8] MdlWriteCompleteCompressed : 0x0 : 0x0 [Type: unsigned char (__cdecl*)(_FILE_OBJECT *,_LARGE_INTEGER *,_MDL *,_DEVICE_OBJECT *)]  
  27.     [+0x0c0] FastIoQueryOpen  : 0x0 : 0x0 [Type: unsigned char (__cdecl*)(_IRP *,_FILE_NETWORK_OPEN_INFORMATION *,_DEVICE_OBJECT *)]  
  28.     [+0x0c8] ReleaseForModWrite : 0x0 : 0x0 [Type: long (__cdecl*)(_FILE_OBJECT *,_ERESOURCE *,_DEVICE_OBJECT *)]  
  29.     [+0x0d0] AcquireForCcFlush : 0xfffff88003ead768 : fastfat!FatAcquireForCcFlush+0x0 [Type: long (__cdecl*)(_FILE_OBJECT *,_DEVICE_OBJECT *)]  
  30.     [+0x0d8] ReleaseForCcFlush : 0xfffff88003ead800 : fastfat!FatReleaseForCcFlush+0x0 [Type: long (__cdecl*)(_FILE_OBJECT *,_DEVICE_OBJECT *)]  
  31. kd> dx -id 0,0,fffffa80036f4380 -r1 (*((ntkrnlmp!long (__cdecl*(*)[28])(_DEVICE_OBJECT *,_IRP *))0xfffffa800497b7c0))  
  32. (*((ntkrnlmp!long (__cdecl*(*)[28])(_DEVICE_OBJECT *,_IRP *))0xfffffa800497b7c0))                 [Type: long (__cdecl* [28])(_DEVICE_OBJECT *,_IRP *)]  
  33.     [0]              : 0xfffff88003e95718 : fastfat!FatFsdCreate+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  34.     [1]              : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  35.     [2]              : 0xfffff88003e94bd0 : fastfat!FatFsdClose+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  36.     [3]              : 0xfffff88003e8891c : fastfat!FatFsdRead+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  37.     [4]              : 0xfffff88003e89350 : fastfat!FatFsdWrite+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  38.     [5]              : 0xfffff88003ea1ef4 : fastfat!FatFsdQueryInformation+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  39.     [6]              : 0xfffff88003ea1f8c : fastfat!FatFsdSetInformation+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  40.     [7]              : 0xfffff88003e9f010 : fastfat!FatFsdQueryEa+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  41.     [8]              : 0xfffff88003e9f010 : fastfat!FatFsdQueryEa+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  42.     [9]              : 0xfffff88003ea5954 : fastfat!FatFsdFlushBuffers+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  43.     [10]             : 0xfffff88003eb0c84 : fastfat!FatFsdQueryVolumeInformation+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  44.     [11]             : 0xfffff88003eb0d1c : fastfat!FatFsdSetVolumeInformation+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  45.     [12]             : 0xfffff88003e9b550 : fastfat!FatFsdDirectoryControl+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  46.     [13]             : 0xfffff88003ea6694 : fastfat!FatFsdFileSystemControl+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  47.     [14]             : 0xfffff88003e9a358 : fastfat!FatFsdDeviceControl+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  48.     [15]             : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  49.     [16]             : 0xfffff88003ead904 : fastfat!FatFsdShutdown+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  50.     [17]             : 0xfffff88003eaaf48 : fastfat!FatFsdLockControl+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  51.     [18]             : 0xfffff88003e940c4 : fastfat!FatFsdCleanup+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  52.     [19]             : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  53.     [20]             : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  54.     [21]             : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  55.     [22]             : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  56.     [23]             : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  57.     [24]             : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  58.     [25]             : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  59.     [26]             : 0xfffff80003c771d4 : ntkrnlmp!IopInvalidDeviceRequest+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  
  60.     [27]             : 0xfffff88003eabf34 : fastfat!FatFsdPnp+0x0 [Type: long (__cdecl*)(_DEVICE_OBJECT *,_IRP *)]  

Look at whether the functions obtained above are consistent with those in PCHunter:


OK, so far we have started from the actual machine partition, step by step to figure out how each callback function in the file system driver (PCHunter’s FSD tab) came, and observed the current address of each callback function in FSD through WinDBG.

I believe that when you read here, you should know how to get the current FSD driver dispatch function address:

  1. #define IRP_MJ_MINIMUM_FUNCTION             0x00  
  2. #define IRP_MJ_MAXIMUM_FUNCTION             0x1c  
  3. #define FAST_IO_MINIMUM_FUNCTION            0x00  
  4. #define FAST_IO_MAXIMUM_FUNCTION            0x1c  
  5.   
  6. ULONG_PTR Index = 0;  
  7. PFAST_IO_DISPATCH FastIORoutines = FSD->FastIoDispatch;  
  8. PDRIVER_DISPATCH *IRPRoutines = FSD->MajorFunction;  
  9.   
  10. for (Index = IRP_MJ_MINIMUM_FUNCTION; Index < IRP_MJ_MAXIMUM_FUNCTION; ++Index)  
  11. {  
  12.     PDRIVER_DISPATCH *FunctionAddress = IRPRoutines + Index;  
  13. }  
  14.   
  15. for (Index = FAST_IO_MINIMUM_FUNCTION; Index < FAST_IO_MAXIMUM_FUNCTION; ++Index)  
  16. {  
  17.     PULONG_PTR FunctionAddress = (PULONG_PTR)FastIORoutines + Index;  
  18. }  

So the next question is, how do you know the original function address of these dispatch functions? The method is actually very simple - find the place where these callback functions are initialized. For example, the following picture is the place where the NTFS file system (Ntfs.sys) initializes MajorFunction.


In the picture above, the functions starting with _Ntfs are the original addresses of each dispatch function (IDA parsed the symbol so it is not the address but the symbol name). But before formally introducing how to get the original function, we need to supplement some knowledge of the X86 instruction set.

Intel instruction introduction

In order to get the original function address, we need to parse the corresponding assembly instructions, that is, we need to parse the bytecode and identify the instructions we want from it.

The bad news is that x86 is a complex instruction set, and its instruction format has many situations. The good news is that we don’t need to parse all the instructions, just parse the parts we are interested in, in this case the problem scale is relatively controllable, after all, there are only a few situations, and enumeration can cover. The bad news is that we need to supplement some additional knowledge to understand the meaning of the bytecode during the parsing process.

It should be noted that the knowledge introduced in this article is only a very small part involved in “getting the original function address”, and it cannot cover the entire x86 instruction set, and there may even be situations that belong to the scope of unresolved problems but are not covered. If you want to get authoritative instruction set instructions, it is recommended to read reference 3 at the end of the article

X86 instruction set introduction

First, let’s take a look at the format of the instruction set under 32 bits Reference 3:


A complete instruction bytecode under 32 bits consists of the following fields:
  • 0 to multiple Prefix: optional, each Prefix occupies 1 byte.
  • Opcode: required, occupies 1 to 3 bytes.
  • ModR/M: optional, occupies 1 byte.
  • SIB: optional, occupies 1 byte.
  • Displacement: optional, occupies 1, 2, 4 bytes (note that there is no 3-byte situation), this field is referred to as Disp or DISP in the following text.
  • Immediate: optional, occupies 1, 2, 4 bytes.
In the entire process of parsing bytecode, we mainly focus on:
  • Opcode: used to distinguish the scenes we focus on and control the problem scale.
  • ModR/M: used to separate the registers and immediate value offsets we are concerned about.
  • Immediate: used to separate the original function address.
The MRod/M field is further formatted as follows, and the registers and immediate value  involved in the subsequent calculation of the FSD original function address need to be parsed from this field:


The three members in the picture above have the following meanings:
  • Mod: two bits, used to distinguish the large classification of the destination register (see the table below).
  • Reg: three bits, used to confirm the original operand using the register.
  • R/M: three bits, combined with Mod bits, used to further determine the addressing format of the destination address and separate the DISP offset.

IA64 (compatible with X86_64) instruction set introduction

Under 64 bits, the instruction format is actually the same as 32 bits, but there is a prefix called REX that needs to be discussed separately.


REX format


The first 4 bits of REX are fixed, and each bit of the last four bits represents a flag bit. Pay special attention to the R bit and B bit, these two bits should be prefixed to the Reg bit and R/M bit of ModR/M when Mod is not 11, as shown in the following figure:


After talking so much, it’s too abstract, in fact, after talking so much, the essence is the following table Reference 2:


First, according to the 2-bit Mod, that is, the red arrow column in the table above, determine which large category the current instruction belongs to.

Then, according to R/M, that is, the blue arrow column in the table above, continue to determine the addressing method of the destination operand, that is, which register (or memory address) the destination address uses.

After confirming Mod and R/M, you can determine a line in the table above, then look at the B bit of REX, if REX.B is 1, you should look at the row corresponding to the green box in the table above, otherwise look at the red box corresponding row. At this point, you can determine the instruction format of the instruction destination address part.

Finally, according to the R bit of REX, confirm Reg. If Rex.R is 1, look at the Reg value corresponding to the blue box in the table above, otherwise look at the Reg value of the yellow box. At this point, the source operand using the register of the instruction is determined.

Emmmmm, it’s still very abstract, then we will use the following instruction as an example in the following text, and actually parse it:

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

Instruction parsing example

If you want to do a good job, you must first sharpen your tools

In the previous section, we introduced the format of the X86 instruction. If you only analyze one instruction, the workload is still possible, but if you face hundreds of lines of code disassembled daily, you obviously cannot analyze them one by one manually.

At this time, an open source tool comes in handy, this tool is Zydis, we can use a command line tool provided by this disassembly engine to help us quickly parse bytecode:


In the picture above, the ZydisInfo command is used to parse a section of instructions under 64 bits, and we manually parse the instructions based on the output of the tool.

Instruction parsing

In the picture above, we focus on the following parts:

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

REX field: Given 0x48 = 01001000b, it can be known that REX.R = 0, REX.B = 0. MODRM: Given 0x05 = 00000101b, it can be known that Mod = 00, Reg = 000, R/M = 101. Since Mod is 00, it is located in the row where the blue box in the figure below is located. Since R/M is 101 and REX.B is 0 (vertical green arrow, only indicating the situation where REX.B is 1), it can be known that the form of the assembly instruction dst is [RIP/EIP]+disp32 (the form of REX.B is the same whether it is 1 or 0). Since Reg is 000 and REX.R is 0 (horizontal green arrow) and it is currently 64-bit assembly, it is confirmed that the src register is RAX.


At the same time, we know that the form of dst is [RIP/EIP]+disp32, so there is a 32-bit (4-byte) DISP, which is 0xFFDE4F1A(big-endian), and after calculation, it is a signed number, which is negative 0x21b0e6:

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

Combined with OPCODE being 0x89 Reference 2 for the move instruction:


So the final instruction is:

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

So now that you have a basic understanding of the principles of machine code analysis, let’s take a look at how to get the real original FSD callback function.


Analysis of FSD initialization operation

In terms of thinking, we need to reload a copy of the FSD kernel module, and then analyze the initialization part of MajorFunction and FastIoDispatch of the reloaded kernel module, get the original initialization address of the module, and finally calculate the initialization address in the current kernel module based on the offset.

Regarding the reloading (kernel) module, there are many other materials, omitted…

After actual analysis, there are three situations in the part of FSD initialization MajorFunction and FastIoDispatch, we discuss separately.

Situation one

Under the 32-bit system, it is usually implemented by directly assigning to the structure member. After analyzing the code of multiple system versions, this form is more common for the initialization of MajorFunction, such as the following disassembly code:

  1. INIT:0005106B 53                      push    ebx             ; DriverObject
  2. ……
  3. INIT:0005109D C7 43 38 A8 93 02 00    mov     dword ptr [ebx+38h], offset _FatFsdCreate@8 ; FatFsdCreate(x,x) 
  4. >> ZydisInfo -32 C7 43 38 A8 93 02 00  
  5. == [    INTEL ] ============================================================================================ 
  6.    ABSOLUTE: mov dword ptr ds:[ebx+0x38], 0x293A8  
  7.    RELATIVE: mov dword ptr ds:[ebx+0x38], 0x293A8  
  8.   
  9. == [ SEGMENTS ] ============================================================================================ 
  10. C7 43 38 A8 93 02 00   
  11. :  :  :  :..IMM  
  12. :  :  :..DISP  
  13. :  :..MODRM  
  14. :..OPCODE  

The above mov dword ptr [ebx+38h], offset _FatFsdCreate operation is equivalent to the following C program:

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

So in the above, ebx is the starting address of the structure, and from the context, this structure is _DRIVER_OBJECT, so you only need to analyze the value of the part added by ebx (DISP), and then calculate the offset corresponding to the symbol name according to the _DRIVER_OBJECT object to know which function it is. And the DISP field, has been analyzed from the output of ZydisInfo above as 0x38.

The problem here is, how can we use the program to analyze the several pieces of information we need above? The good news is that it can be achieved by transforming the open source ZydisInfo source code. The bad news is that the source code of ZydisInfo cannot be used under the kernel. Since the kernel module currently written by the author is expected to be all in one, it is necessary to parse binary instructions under the kernel, so the author wrote a temporary instruction parsing macro - note that this macro can only correctly parse limited mode bytecode, and cannot guarantee that it can be correctly parsed in all cases.

  1. #define X64_PARSE_INSTRUCTION(__address, __opcode, __mod, __register, __rm, __sib, __disp_offset) \  
  2.     { \  
  3.         UCHAR REX = 0; \  
  4.         UCHAR MODRM = 0; \  
  5.         UCHAR WorkOffset = 0; \  
  6.         /* 0x64-0x67 is PREFIX opcode */ \  
  7.         if (0x64 <= *(PUCHAR)__address && 0x67 >= *(PUCHAR)__address) \  
  8.         { \  
  9.             WorkOffset += 1; \  
  10.         } \  
  11.         /* 0x40-0x47 is REX opcode area */ \  
  12.         if (0x40 <= *((PUCHAR)__address + WorkOffset) && 0x4F >= *((PUCHAR)__address + WorkOffset)) \  
  13.         { \  
  14.             REX = *(PUCHAR)((PUCHAR)__address + WorkOffset); \  
  15.             WorkOffset += 1; \  
  16.         } \  
  17.         *__opcode   = *((PUCHAR)__address + WorkOffset); \  
  18.         WorkOffset += 1; \  
  19.         /* If this is a 2 bytes opcode (which is started by 0x0f) */ \  
  20.         if (0x0f == *__opcode) \  
  21.         { \  
  22.             *__opcode = *(PUCHAR)((PUCHAR)__address + WorkOffset); \  
  23.             WorkOffset += 1; \  
  24.         } \  
  25.         MODRM       = *(PUCHAR)((PUCHAR)__address + WorkOffset); \  
  26.         *__mod      = (MODRM & 0xC0) >> 6; \  
  27.         *__register = (((REX & 0x4) >> 2) << 3) | ((MODRM & 0x38) >> 3); \  
  28.         *__rm       = ((REX & 0x1) << 3) | (MODRM & 0x07); \  
  29.         /* There will be a SIB byte when Mod != 11b && Rm == 100b */ \  
  30.         if (0x3 != *__mod && 0x4 == *__rm) \  
  31.         { \  
  32.             *__sib = *(PUCHAR)(__address + WorkOffset + 1); \  
  33.             *__disp_offset = WorkOffset + 2; \  
  34.         } \  
  35.         else *__disp_offset = WorkOffset + 1; \  
  36.     }  
  37.   
  38. #define X86_PARSE_INSTRUCTION(__address, __opcode, __mod, __register, __rm, __disp_offset) \  
  39.     { \  
  40.         UCHAR MODRM    = 0; \  
  41.         UCHAR WorkOffset = 0; \  
  42.         /* 0x64-0x67 is PREFIX opcode */ \  
  43.         if (0x64 <= *(PUCHAR)__address && 0x67 >= *(PUCHAR)__address) \  
  44.         { \  
  45.             WorkOffset += 1; \  
  46.         } \  
  47.         *__opcode   = *(PUCHAR)((PUCHAR)__address + WorkOffset); \  
  48.         WorkOffset += 1; \  
  49.         /* If this is a 2 bytes opcode (which is started by 0x0f) */ \  
  50.         if (0x0f == *__opcode) \  
  51.         { \  
  52.             *__opcode = *(PUCHAR)((PUCHAR)__address + WorkOffset); \  
  53.             WorkOffset += 1; \  
  54.         } \  
  55.         MODRM       = *(PUCHAR)((PUCHAR)__address + WorkOffset); \  
  56.         *__mod      = (MODRM & 0xC0) >> 6; \  
  57.         *__register = (MODRM & 0x38) >> 3; \  
  58.         *__rm       = MODRM & 0x07; \  
  59.         /* There will be a SIB byte when Mod != 11b && Rm == 100b */ \  
  60.         if (0x3 != *__mod && 0x4 == *__rm) \  
  61.         { \  
  62.             *__disp_offset = WorkOffset + 2; \  
  63.         } \  
  64.         else *__disp_offset = WorkOffset + 1; \  
  65.     }  
  66.   
  67. // variable i stores the memory address of instruction
  68. UCHAR OpCode = 0, Mod = 0xFF, Register = 0xFF, Rm = 0xFF, Sib = 0, DispOffset = 0;  
  69. X86_PARSE_INSTRUCTION(i, &OpCode, &Mod, &Register, &Rm, &DispOffset);  
  70. X64_PARSE_INSTRUCTION(i, &OpCode, &Mod, &Register, &Rm, &Sib, &DispOffset);  

Situation two

In addition to the above situation where the offset is directly assigned, there is also an indirect assignment situation. Generally, the original function is first assigned to a register, and then the register is assigned to a piece of memory + offset. By analyzing the initialization code of multiple systems, this form is more common for the initialization of FAST_IO_DISPATCH, such as the following situation:

  1. // INIT:00000001C02BF980 48 8D 05 19 00 FA FF    lea     rax, NtfsFastIoCheckIfPossible  
  2. >> ZydisInfo -64 48 8D 05 19 00 FA FF  
  3. == [    INTEL ] ============================================================================================
  4.    ABSOLUTE: lea rax, ds:[0xFFFFFFFFFFFA0020]  
  5.    RELATIVE: lea rax, ds:[rip-0x5FFE7]  
  6. == [ SEGMENTS ] ============================================================================================ 
  7. 48 8D 05 19 00 FA FF   
  8. :  :  :  :..DISP  
  9. :  :  :..MODRM  
  10. :  :..OPCODE  
  11. :..REX  
  12. // INIT:00000001C02BF987 48 89 05 1A 4F DE FF    mov     cs:NtfsFastIoDispatch.FastIoCheckIfPossible, rax  
  13. >> ZydisInfo -64 48 89 05 1A 4F DE FF  
  14. == [ SEGMENTS ] ============================================================================================ 
  15. 48 89 05 1A 4F DE FF   
  16. :  :  :  :..DISP  
  17. :  :  :..MODRM  
  18. :  :..OPCODE  
  19. :..REX  

The above two lines of assembly code are equivalent to the following C code:

  1. XXXX NtfsFastIoCheckIfPossible(....)  
  2. {  
  3.     ....  
  4. }  
  5.   
  6. FAST_IO_DISPATCH FastIODispatch = {0};  
  7. FastIoDispatch.FastIoCheckIfPossible = NtfsFastIoCheckIfPossible;  

For this situation, we need to analyze the DISP offset and the involved register in the mov instruction, and then find the lea instruction that assigns to this register and the starting address of the data structure, and finally subtract the starting address from DISP to get the member offset in the structure for determining the member, and finally determine the original function address of this member based on the register value.

Situation three

Some dispatch functions cannot find the initialization place in the corresponding FSD module, such as the following picture is the initialization part of NTFS MajorFunction:


Although the order is chaotic, but after sorting out, it is found that the index operation of the MajorFunction array is not continuous (there is no assignment of index 1, 15). This is because when the kernel calls IoCreateDriver to create a driver object, it will set all the MajorFunction dispatch functions of the object to IopInvalidDeviceRequest by default:

  1. int __stdcall IoCreateDriver(int a1, int a2)  
  2. {  
  3.   PDRIVER_OBJECT v5, v29;  
  4.   ……  
  5.   result = ObCreateObject(0, (int)IoDriverObjectType, &a3, 0, 0, 196, 0, 0, (int)&v29);  
  6.   v5 = v29;  
  7.   if ( result >= 0 )  
  8.   {  
  9.     memset(v29, 0, 0xC4u);  
  10.     memset32(v5->MajorFunction, (int)IopInvalidDeviceRequest, 0x1Cu);  
  11.     ……  
  12.   }  
  13.   return result;  
  14. }  

Therefore, for the uninitialized MajorFunction (FastIoDispatch is the same) in FSD, the default value when creating the driver object is still retained:

Achievement

The theoretical part of the above text, after completion, it is confirmed that it can achieve the effect of obtaining the current and original FSD dispatch function:

Reference

Catalog
  • From practice to theory
  • Intel instruction introduction
  • X86 instruction set introduction
  • IA64 (compatible with X86_64) instruction set introduction
  • REX format
  • Instruction parsing example
  • If you want to do a good job, you must first sharpen your tools
  • Instruction parsing
  • Analysis of FSD initialization operation
  • Situation one
  • Situation two
  • Situation three
  • Achievement
  • Reference
  • CopyRight (c) 2020 - 2025 Debugwar.com

    Designed by Hacksign