Jump to content
Nytro

Mimidrv In Depth: Exploring Mimikatz’s Kernel Driver

Recommended Posts

Posted

Mimidrv In Depth: Exploring Mimikatz’s Kernel Driver

Matt Hand
Jan 13 · 29 min read

Mimikatz provides the opportunity to leverage kernel mode functions through the included driver, Mimidrv. Mimidrv is a signed Windows Driver Model (WDM) kernel mode software driver meant to be used with the standard Mimikatz executable by prefixing relevant commands with an exclamation point (!). Mimidrv is undocumented and relatively underutilized, but provides a very interesting look into what we can do while operating at ring 0.

The goals of this post is to familiarize operators with the capability that Mimidrv provides, put forth some documentation to be used as a reference, introduce those who haven’t had much time working with the kernel to some core concepts, and provide defensive recommendations for mitigating driver-based threats.

Why use Mimidrv?

Simply put, the kernel is king. There are some Windows functionalities available that can’t be called from user mode, such as modifying running processes’ attributes and interacting directly with other loaded drivers. As we will delve into a later in this post, the driver provides us with a method to call these functions via a user mode application.

Loading Mimidrv

The first step in using Mimikatz’s driver is to issue the command !+. This command implants and starts the driver from user mode and requires that your current token has SeLoadDriverPrivilege assigned.

1*8z-vK9A3LIVkkLaK2kxifA.png?q=20
1*8z-vK9A3LIVkkLaK2kxifA.png

Mimikatz first checks if the driver exists in the current working directory, and if it finds the driver on disk, it begins creating the service. Service creation is done via the Service Control Manager (SCM) API functions. Specifically, advapi32!ServiceCreate is used to register the service with the following attributes:

If the service is created successfully, the “Everyone” group is granted access to the service, allowing any user on the system to interact with the service. For example, a low-privilege user can stop the service.

1*OgQs14JOMqwXJ3VNVg1Rig.png?q=20
1*OgQs14JOMqwXJ3VNVg1Rig.png

Note: This is one of the reasons that post-op clean up is so important. Don’t forget to remove the driver (!-) when you are done so that you don’t leave it implanted for someone else to use.

If that completes successfully, the service is finally started with a call to StartService.

0*9TFAU2JQ_38YeyXN?q=20
0*9TFAU2JQ_38YeyXN

Post-Load Actions

Once the service starts, it is Mimidrv’s turn to complete its setup. The driver does not do anything atypical during its startup process, but it may seem complicated you haven’t developed WDM drivers before.

Every driver must have a defined DriverEntry function that is called as soon as the driver is loaded and is used to set up the requirements for the driver to run. You can think of this similarly to a main() function in user mode code. In Mimidrv’s DriverEntry function, there are four main things that happen.

1. Create the Device Object

Clients do not talk directly to drivers, but rather device objects. Kernel mode drivers must create at least 1 device object, however this device object still can’t be accessed directly by user mode code without a symbolic link. We’ll cover the symbolic link a little later, but the creation of the device object must occur first.

To create the device object, a call to nt!IoCreateDevice is made with some important details. Most notable of this is the third parameter, DeviceName. This is set in globals.h as “mimidrv”.

This newly created device object can be seen with WinObj.

1*4HMuZKx6Lmya_x0Pous0uQ.png?q=20
1*4HMuZKx6Lmya_x0Pous0uQ.png

2. Set the DispatchDeviceControl and Unload Functions

If that device object creation succeeds, it defines its DispatchDeviceControl function, registered at the IRP_MJ_DEVICE_CONTROL index in its MajorFunction dispatch table, as the MimiDispatchDeviceControl function. What this means is that any time it receives a IRP_MJ_DEVICE_CONTROL request, such as from kernel32!DeviceIoControl, Mimidrv will call its internal MimiDispatchDeviceControl function which will process the request. We will cover how this works in the “User Mode Interaction via MimiDispatchDeviceControl” section.

Just as every driver must specify a DriveryEntry function, it must define a corresponding Unload function that is executed when the driver is unloaded. Mimidrv’s DriverUnload function is about as simple as it gets and its only job is to delete the symbolic link and then device object.

3. Create the Symbolic Link

As mentioned earlier, if a driver wants to allow user mode code to interact with it, it must create a symbolic link. This symbolic link will be used by user mode applications, such as through calls to nt!CreateFile and kernel32!DeviceIoControl, in place of a “normal” file to send data to and receive data from the driver.

1*j3-rmPCm_xSMmCuHIdoXqQ.png?q=20
1*j3-rmPCm_xSMmCuHIdoXqQ.png

To create the symbolic link, Mimidrv makes a call to nt!IoCreateSymbolicLink with the name of the symbolic link and the device object as arguments. The newly created device object and associated symlink can be seen in WinObj:

0*2l5HBJiXxEiktrte?q=20
0*2l5HBJiXxEiktrte

4. Initialize Aux_klib

Finally, it initializes the Aux_klib library using AuxKlibInitialize, which must be done before being able to call any function in that library (more on that in the “Modules” section).

User Mode Interaction via MimiDispatchDeviceControl

After initialization, a driver’s job is simply to handle requests to it. It does this through a partially opaque feature called I/O request packets (IRPs).These IRPs contain I/O Control Codes (IOCTLs) which are mapped to function codes. These typically start at 0x8000, but Mimikatz starts at 0x000, against Microsoft’s recommendation. Mimikatz currently defines 23 IOCTLs in ioctl.h. Each one of these IOCTLs is mapped to a function. When Mimidrv receives one of these 23 defined IOCTLs, it calls the mapped function. This is where the core functionality of Mimidrv lies.

Sending IRPs

In order to get the driver to execute one of the functions mapped to the IOCTLs, we have to send an IRP from user mode via the symbolic link created earlier. Mimikatz handles this in the kuhl_m_kernel_do function, which trickles down to a call to nt!CreateFile to get a handle on the device object and kernel32!DeviceIoControl to sent the IRP. This hits the IRP_MJ_DEVICE_CONTROL major function, which was defined as MimiDispatchDeviceControl, and walks down the list of internally defined functions by their IOCTL codes. When a command is entered with the prefix “!”, it checks the KUHL_K_C structure, kuhl_k_c_kernel, to get the IOCTL associated with the command. The structure is defined as:

In the struct, 19 commands are defined as:

Despite there being 23 IOCTLs, there are only 19 commands available via Mimikatz. This is because 4 of the functions related to interacting with virtual memory are not mapped to commands. The IOCTLs and associated functions are:

  • IOCTL_MIMIDRV_VM_READkkll_m_memory_vm_read
  • IOCTL_MIMIDRV_VM_WRITEkkll_m_memory_vm_write
  • IOCTL_MIMIDRV_VM_ALLOCkkll_m_memory_vm_alloc
  • IOCTL_MIMIDRV_VM_FREEkkll_m_memory_vm_free

Driver Function Internals

The commands can be broken down into 7 groups— General, Process, Notify, Modules, Filters, Memory, and SSDT. These are, for the most part (minus the General functions), logically organized in the Mimidrv source code with file name format kkll_m_<group>.c.

General

!ping

The ping command can be used to test the ability to write data to and receive data from Mimidrv. This is done through Benjamin’s kprintf function, which is really just a simplified call to nt!RtlStringCbPrintfExW which allows the use of the KIWI_BUFFER structure to keep the code tidy.

!bsod

As alluded to by the name, this functionality bluescreens the box. This is done via a call to KeBugCheck with a bugcheck code of MANUALLY_INITIATED_CRASH, which will be shown on the bluescreen under the “stop code”.

0*IxaRPEICeft5RP4z?q=20
0*IxaRPEICeft5RP4z

!sysenvset & !sysenvdel

The !sysenvset command sets a system environment variable, but not in the traditional sense (e.g. modifying %PATH%). Instead, on systems configured with Secure Boot, it modifies a variable in the UEFI firmware store, specifically Kernel_Lsa_Ppl_Config, which is associated with the RunAsPPL value in the registry. The GUID that it writes this value to, 77fa9abd-0359–4d32-bd60–28f4e78f784b, is the Protected Store which Windows can use to store values that it wants to protect from user and admin modification. This effectively overrides the registry, so even if you were to modify the RunAsPPL key and reboot, LSASS would still be protected.

The !sysenvdel does the opposite and removes this environment variable. The RunAsPPL registry key could then be deleted, the system rebooted, and then we could get a handle on LSASS.


Process

The first group of modules we’ll really dig into is the Process group, which allows for interaction and modification of user mode processes. Because we will be working with processes in this section, it is important to understand what they look like from the kernel’s perspective. Processes in the kernel center around the EPROCESS structure, an opaque structure that serves as the object for a process. Inside of the structure are all of the attributes of a process that we are familiar with, such as the process ID, token information, and process environment block (PEB).

1*MjxbQP8e31aNysb1Fnk1vg.png?q=20
1*MjxbQP8e31aNysb1Fnk1vg.png

EPROCESS structures in the kernel are connected through a circular doubly-linked list. The list head is stored in the kernel variable PsActiveProcessHead and is used as the “beginning” of the list. Each EPROCESS structure contains a member, ActiveProcessLinks, of the type LIST_ENTRY. The LIST_ENTRY structure has 2 components — a forward link (Flink) and a backward link (Blink). The Flink points to the Flink of the next EPROCESS structure in the list. The Blink points to the Flink of the previous EPROCESS structure in the list. The Flink of the last structure in the list points to the Flink of PsActiveProcessHead. This creates a loop of EPROCESS structures and is represented in this simplified graphic.

0*FKQsDx7iyDo06nme?q=20
0*FKQsDx7iyDo06nme

!process

The first module gives us a list of processes running on the system, along with some additional information about them. This works by walking the linked list described earlier using 2 Windows version-specific offsets — EprocessNext and EprocessFlags2. EprocessNext is the offset in the current EPROCESS structure containing the address of the ActiveProcessLinks member, where the Flink to the next process can be read (e.g. 0x02f0 in Windows 10 1903). EProcessFlags2 is a second set of ULONG bitfields introduced in Windows Vista, hence why this is only shown when running on systems Vista and above, used to give use some more detail. Specifically:

  • PrimaryTokenFrozen — Uses a ternary to return “F-Tok” if the primary token is frozen and nothing if it isn’t. If PrimaryTokenFrozen is not set, we can swap in our token such as in the case of suspended processes. In a vast majority of cases, you will find that the primary token is frozen.
  • SignatureProtect — This is actually 2 values - SignatureLevel and SectionSignatureLevel. SignatureLevel defines the signature requirements of the primary module. SectionSignatureLevel defines the minimum signature level requirements of a DLL to be loaded into the process.
  • Protection — These 3 values, Type, Audit, and Signer, are members of the PS_PROTECTION structure which represent the process’ protection status. Most important of these is Type, which maps to the following statuses, which you may recognize as PP/PPL:
1*EtK6NBn1YhPvcsRAArtu9w.png?q=20
1*EtK6NBn1YhPvcsRAArtu9w.png

!processProtect

The !processProtect function is one of, if not the most, used functionalities supplied by Mimidrv. Its objective is to add or remove process protection from a process, most commonly LSASS. The way it goes about modifying the protection status is relatively simple:

  1. Use nt!PsLookupProcessByProcessId to get a handle on a process’ EPROCESS structure by its PID.
  2. Go to the version-specific offset of SignatureProtect in the EPROCESS structure.
  3. Patches 5 values — SignatureLevel, SectionSignatureLevel, Type, Audit, and Signer (the last 3 being members of the PS_PROTECTION struct) — depending on whether or not it is protecting or unprotecting the process.
  4. If protecting, the values will be 0x3f, 0x3f, 2, 0, 6, representing a protected signer of WinTcb and protection level of Max.
  5. If unprotecting, the values will be 0, 0, 0, 0, 0, representing an unprotected process.
  6. Finally, dereference the EPROCESS object.
0*hvmZgwD1ZOrapzqN?q=20
0*hvmZgwD1ZOrapzqN

This module is particularly relevant for us as attackers because most obviously we can remove protection from LSASS in order to extract credentials, but more interestingly we can protect an arbitrary process and use that to get a handle on another protected process. For example, we use !processProtect to protect our running mimikatz.exe and then run some command to extract credentials from LSASS and it should work despite LSASS being protected. An example of this use case is shown below.

0*wSlq8y6sOC7WYZav?q=20
0*wSlq8y6sOC7WYZav

!processToken

Continuing with another operationally-relevant function is !processToken which can be used to duplicate a process token and pass it to an attacker-specified process. This is most commonly used during DCShadow attacks and is similar to token::elevate, but modifies the process token instead of the thread token.

With no arguments passed, this function will grant all cmd.exe, powershell.exe, and mimikatz.exe processes a NT AUTHORITY\SYSTEM token. Alternatively, it takes “to” and “from” parameters which can be used to define the process you wish to copy the token from and process you want to copy it to.

0*hlut77KbWQN4Y1fR?q=20
0*hlut77KbWQN4Y1fR

To duplicate the token, Mimikatz first sets the “to” and “from” PIDs to the user-supplied values, or “0” if not set, and then places them in a MIMIDRV_PROCESS_TOKEN_FROM_TO struct, which sent to Mimidrv via IOCTL_MIMIDRV_PROCESS_TOKEN.

Once Mimidrv receives the PIDs specified by the user, it gets handles on the “to” and “from” processes using nt!PsLookupProcessByProcessId. If it was able to get a handle on those processes, it uses nt!ObOpenObjectByPointer to get a kernel handle (OBJ_KERNEL_HANDLE) on the “from” process. This is required by the following call to nt!ZwOpenProcessTokenEx, which will return a handle on the “from” process’ token.

At this point, the logic forks somewhat. In the first case where the user has supplied their own “to” process, Mimidrv calls kkll_m_process_token_toProcess. This function first uses nt!ObOpenObjectByPointer to get a kernel handle on the “to” process. Then it calls ZwDuplicateToken to get the token from the “from” process and stash it in an undocumented PROCESS_ACCESS_TOKEN struct as the Token attribute. If the system is running Windows Vista or above, it sets PrimaryTokenFrozen (described in the !process section) and then calls the undocumented nt!ZwSetInformationProcess function to do the actual work of giving the duplicated token to the “to” process. Once that completes, it cleans up by closing the handles to the “to” process and PROCESS_ACCESS_TOKEN struct.

In the event that no “to” process was specified, Mimidrv leverages the kkll_m_process_enum function used in !process to walk the list of processes on the system. Instead of using the kkll_m_process_list_callback callback, it uses kkll_m_process_systoken_callback, which uses ntdll!RtlCompareMemory to check if the ImageFileName matches “mimikatz.exe”, “cmd.exe”, or “powershell.exe”. If it does, it passes a handle to that process to kkll_m_process_token_toProcess and the functionality described in the paragraph before this is used to grant a duplicated token to that process, and then it continues walking the linked list looking for other matches.

1*4wMfz46sU0_6I4vu_9vxwA.png?q=20
1*4wMfz46sU0_6I4vu_9vxwA.png

!processPrivilege

This is a relatively simple function that grants all privileges (e.g. SeDebugPrivilege, SeLoadDriverPrivilege), but includes some interesting code that highlights the power of operating in ring 0. Before we jump into exactly how Mimidrv modifies the target process token, it is important to understand what a token looks like in the kernel.

As discussed earlier, the EPROCESS structure contains attributes of a process, including the token (offset 0x360 in Windows 10 1903). You may notice that the token of the type EX_FAST_REF rather than TOKEN.

1*N7x-DahK958UwDbQ6bRhaA.png?q=20
1*N7x-DahK958UwDbQ6bRhaA.png

This is some internal Windows weirdness, but these pointers are built around that fact that that kernel structures are aligned on a 16-byte boundary on x64 systems. Due to this alignment, spare bits in the pointer are available to be used for reference counting. Where this becomes relevant for us is that the last 1 byte of the pointer will be the reference to our object — in this case a pointer to the TOKEN structure.

To demonstrate this practically, let’s hunt down the token of the System process in WinDbg. First, we get the address of the EPROCESS structure for the process.

1*C4XoX8jy3qFNXvqHnvAoOQ.png?q=20
1*C4XoX8jy3qFNXvqHnvAoOQ.png

Because we know that the token EX_FAST_REF will be at offset 0x360, we can use WinDbg’s calculator to do some quick math and give us the memory address at the result of the equation.

1*pxl18_W3Otk2m1QlY1lZCA.png?q=20
1*pxl18_W3Otk2m1QlY1lZCA.png

Now that we have the address of the EX_FAST_REF, we can change the last byte to 0 to get the address of our TOKEN structure, which we’ll examine with the !token extension.

1*jaYdVKUMpz8kyXkfM3cuYQ.png?q=20
1*jaYdVKUMpz8kyXkfM3cuYQ.png

So now that we can identify the TOKEN structure, we can examine some of its attributes.

1*2Qh9E82BNo9fz9sivriwnQ.png?q=20
1*2Qh9E82BNo9fz9sivriwnQ.png

Most relevant to !processPrivileges is the Privileges attribute (offset 0x40 on Vista and above). This attribute is of the type SEP_TOKEN_PRIVILEGES which contains 3 attributes — Present, Enabled, and EnabledByDefault. These are bitmasks representing the token permissions we are used to seeing (SeDebugPrivilege, SeLoadDriverPrivilege, etc.).

1*Q-c1a3Hh60y7DSp1PFAwew.png?q=20
1*Q-c1a3Hh60y7DSp1PFAwew.png

If we examine the function called by Mimidrv when we issue the !processPrivileges command, we can see that these bitmasks are being overwritten to enable all privileges on the primary token of the target process. Here’s what the result looks like in the GUI.

1*HEYD8Mv1CjLOldzrsz2N1w.png?q=20
1*HEYD8Mv1CjLOldzrsz2N1w.png

And here it is in the debugger while inspecting the memory at the Privileges offset.

1*RB9hMkZAvyZBo_lXNl0ymg.png?q=20
1*RB9hMkZAvyZBo_lXNl0ymg.png

To sum this module up, !processPrivileges overwrites a specific bitmask in a target process’ TOKEN structure which grants all permissions to the target process.


Notify

The kernel provides ways for drivers to “subscribe” to specific events that happen on a system by registering callback functions to be executed when the specific event happens. Common examples of this are shutdown handlers, which allow the driver to perform some action when the system is shutting down (often for persistence), and process creation notifications, which let the driver know whenever a new process is started on the system (commonly used by EDRs).

These modules allow us to find drivers that subscribe to specific event notifications and where their callback function is located. The code Mimidrv uses to do this is a bit hard to read, but the general flow is:

  1. Search for a string of bytes, specifically the opcodes directly after a LEA instruction containing the pointer to a structure in system memory.
  2. Work with the structure (or pointers to structures) at the address passed in the LEA instruction to find the address of the callback functions.
  3. Return some details about the function, such as the driver that it belongs to.

!notifProcess

A driver can opt to receive notifications when a process is created or destroyed by using nt!PsSetCreateProcessNotifyRoutine(Ex/Ex2) with a callback function specified in the first parameter. When a process is created, a process object for the newly created process is returned along with a PS_CREATE_NOTIFY_INFO structure, which contains a ton of relevant information about the newly created process, including its parent process ID and command line arguments. A simple implementation of process notifications can be found here.

This type of notification has some advantages over Event Tracing for Windows (ETW), namely that there is no delay in receiving the creation/termination notifications and because the process object is passed to our driver, we have a way to prevent the process from starting during a pre-operation callback. Seems pretty useful for an EDR product, eh?

We first begin by searching for the pattern of bytes (opcodes starting at LEA RCX,[RBX*8] in the screenshot below) between the addresses of nt!PsSetCreateProcessNotifyRoutine and nt!IoCreateDriver which marks the start of the undocumented nt!PspSetCreateProcessNotifyRoutine array.

1*CdnRgUYG0ybWY0rHAxa80A.png?q=20
1*CdnRgUYG0ybWY0rHAxa80A.png
1*K3QT4zV6oja-Ew2_QIg7Jw.png?q=20
1*K3QT4zV6oja-Ew2_QIg7Jw.png

At the address of nt!PspSetCreateProcessNotifyRoute is an array of ≤64 pointers to EX_FAST_REF structures.

1*q4Agk8ONMRSm3CNC7itZDg.png?q=20
1*q4Agk8ONMRSm3CNC7itZDg.png

When a process is created/terminated, nt!PspCallProcessNotifyRoutines walks through this array and calls all of the callbacks registered by drivers on the system. In this array, we will work with the 3rd item (0xffff9409c37c7e6f). The last 4 bits of these pointer addresses are insignificant, so they are removed which gives us the address of the EX_CALLBACK_ROUTINE_BLOCK.

1*8Gwb1ndDB6djkvAZ1KHgWA.png?q=20
1*8Gwb1ndDB6djkvAZ1KHgWA.png

The EX_CALLBACK_ROUTINE_BLOCK structure is undocumented, but thanks to the folks over at ReactOS, we have it defined here as:

The first 8 bytes of the structure represent an EX_RUNDOWN_REF structure, so we can jump past them to get the address of the callback function inside of a driver.

1*fzefgmUyj38wtnJQlhGAcQ.png?q=20
1*fzefgmUyj38wtnJQlhGAcQ.png

We then take that address and see which module is loaded at that address.

1*IDUCK_mnsGVXZUkyJpvXUg.png?q=20
1*IDUCK_mnsGVXZUkyJpvXUg.png

And there we can see that this is the address of the process notification callback for WdFilter.sys, Defender’s driver!

1*UrrF6CQuKMKT6kZasKbfXg.png?q=20
1*UrrF6CQuKMKT6kZasKbfXg.png

Could we write a RET instruction at this address to neuter this functionality in the driver? 😈

!notifThread

The !notifThread command is nearly identical to the !notifProcess command, but it searches for the address of nt!PspCreateThreadNotifyRoutine to find the pointers to the thread notification callback functions instead of nt!PspCreateProcessNotifyRoutine.

1*xXcqY6NDoCx6-16QBoB1Vg.png?q=20
1*xXcqY6NDoCx6-16QBoB1Vg.png
1*6UfLkSOjjERGKQdSRLitRA.png?q=20
1*6UfLkSOjjERGKQdSRLitRA.png

!notifImage

These notifications allow a driver to receive and event whenever an image (e.g. driver, DLL, EXE) is mapped into memory. Just as in the function above, !notifImage simply changes the array it is searching for to nt!PspLoadImageNotifyRoutine in order to locate the pointers to image load notification callback routines.

1*wPJq0kMh9tRqhqJYBB30Jw.png?q=20
1*wPJq0kMh9tRqhqJYBB30Jw.png

From there it follows the exact same process of bitshifting to get the address of the callback function.

1*e1rKTK53sD3-D74xWfdVqQ.png?q=20
1*e1rKTK53sD3-D74xWfdVqQ.png

!notifReg

A driver can register pre- and post-operation callbacks for registry events, such as when a key is read, created, or modified, using nt!CmRegisterCallback(Ex). While this functionality isn’t as common as the types we discussed previously, it gives developers a way to prevent the modification of protected registry keys.

This module is simpler than the previous 3 in that it really centers around finding and working with a single undocumented structure. Mimidrv searches for the address to nt!CallbackListHead, which is a doubly-linked list that contains the pointer to the address of the registry notification callback routine. This structure can be documented as:

At the offset 0x28 in this structure is the address of the registered callback routine.

1*nqKiij9mukIyL51Ff4HuFQ.png?q=20
1*nqKiij9mukIyL51Ff4HuFQ.png

Mimidrv simply iterates through the linked list getting the callback function addresses and passing them to kkll_m_modules_fromAddr to get the offset of the function in its driver.

1*gXy2Oh8X87wTGz_cFiegqw.png?q=20
1*gXy2Oh8X87wTGz_cFiegqw.png

!notifObject

Note: This command is not working in release 2.2.0 2019122 against Win10 1903 and returns 0x490 (ERROR_NOT_FOUND) when calling kernel32!DeviceIoControl, likely due to not being able to find the address of nt!ObTypeDirectoryObject. I will update this section if it is modified and working again.

Finally, a driver can register a callback to receive notifications when there are attempts to open or duplicate handles to processes, threads, or desktops, such as in the event of token stealing. This is useful for many different types of software, and is used by AVG’s driver to protect its user mode processes from being debugged.

These callbacks can be either pre-operation or post-operation. Pre-operation callbacks allow the driver to modify the requested handle, such as the requested access, before the operation which returns a handle is complete. A post-operation callback allows the driver to perform some action after the operation has completed.

Mimidrv first searches for the address of nt!ObpTypeDirectoryObject, which holds a pointer to the OBJECT_DIRECTORY structure.

1*NVzjz54XCKBKs4TtA7jE_A.png?q=20
1*NVzjz54XCKBKs4TtA7jE_A.png

The “HashBuckets” member of this structure is a linked list of OBJECT_DIRECTORY_ENTRY structures, each containing an object value at offset 0x8.

1*N4IpJ75-qsKaba0BdmxESQ.png?q=20
1*N4IpJ75-qsKaba0BdmxESQ.png
1*0-ETqfjp9CH46LGvDT2z1A.png?q=20
1*0-ETqfjp9CH46LGvDT2z1A.png

Each of these Objects are OBJECT_TYPE structures containing details about the specific type of object (processes, tokens, etc.) which are more easily viewed with WinDbg’s !object extension. The Hash number is the index in the HashBucket above.

1*nrMGslav22vyhsGMhaQYXQ.png?q=20
1*nrMGslav22vyhsGMhaQYXQ.png

Mimidrv then extracts the Name member from the OBJECT_TYPE structure.

1*Hn0a2xSwk0z4SL_DOxNHIQ.png?q=20
1*Hn0a2xSwk0z4SL_DOxNHIQ.png

The other member of note is CallbackList, which defines a list of pre- and post-operation callbacks which have been registered by nt!ObRegisterCallbacks. It is a LIST_ENTRY structure that points to the undocumented CALLBACK_ENTRY_ITEM structure. Mimidrv iterates through the linked list of CALLBACK_ENTRY_ITEM structures, passing each one to kkll_m_notify_desc_object_callback where the pointer from the pre-/post-operation callback is extracted and passed to kkll_m_modules_fromAddr in order to find the offset in the driver that the callback belongs to.

Finally, Mimidrv loops through an array of 8 object methods starting from the OBJECT_TYPE + 0x70. If a pointer is set, Mimidrv passes it to kkll_m_modules_fromAddr to get the address of the object method and returns it to the user. This can be seen in the example below for the Process object type.

1*U9V_4cx9exuw-Laqj4rahw.png?q=20
1*U9V_4cx9exuw-Laqj4rahw.png
Object Method Pointers for the Process Object Type

While this function is not working on the latest release of Windows 10, the output would be similar to this:

1*HHW_vJO4t0MQ6qW4PBT3ow.png?q=20
1*HHW_vJO4t0MQ6qW4PBT3ow.png
Source: https://www.slideshare.net/ASF-WS/asfws-2014-rump-session

Modules

While this section only contains 1 command, it also contains another core kernel concept — memory pools. Memory pools are kernel objects that allow chunks of memory to be allocated from a designated memory region, either paged or nonpaged. Each of these types has a specific use case.

The paged pool is virtual memory that can be paged in/out (i.e. read/written) to the page file on disk, C:\pagefile.sys). This is the recommended pool for drivers to use.

The nonpaged pool can’t be paged out and will always live in RAM. This is required in specific situations where page faults can’t be tolerated, such as when processing Interrupt Service Routines (ISRs) and during Deferred Procedure Calls (DPCs).

Here’s an example of a standard allocation of paged pool memory:

The last item to note is the third and final parameter of nt!ExAllocatePoolWithTag, the pool tag. This is typically a unique 4-byte ASCII value and is used to help track down drivers with memory leaks. In the example above, the memory would be tagged with “MATT” (the tag is little endian). Mimidrv uses the pool tag “kiwi”, which would be shown as “iwik”, as seen in Pavel Yosifovich’s PoolMonX below.

1*qQ2H1fHefd9ZjB21S3gqrw.png?q=20
1*qQ2H1fHefd9ZjB21S3gqrw.png

!modules

The !modules command lists details about drivers loaded on the system. This command primarily centers around the aux_klib!AuxKlibQueryModuleInformation function.

Mimidrv first uses aux_klib!AuxKlibQueryModuleInformation to get the total amount of memory it will need to allocate in order to hold the AUX_MODULE_EXTENDED_INFO structs containing the module information. Once it receives that, it will use nt!ExAllocatePoolWithTag to allocate the required amount of memory from the paged pool using its pool tag, “kiwi”.

Some quick math happens to determine the number of images loaded by dividing the size returned by the first call to aux_klib!AuxKlibQueryModuleInformation by the size of the AUX_MODULE_EXTENDED_INFO struct. A subsequent call to aux_klib!AuxKlibQueryModuleInformation is made to get all of the module information and store it for processing. Mimidrv then iterates through this pool of memory using the callback function kkll_m_modules_list_callback to copy the base address, image size, and file name into the output buffer which will be sent back to the user.

1*m7yil_2ItIBr5e-rvsPuew.png?q=20
1*m7yil_2ItIBr5e-rvsPuew.png

Filters

While we have primarily been exploring software drivers, there are 2 other types, filters and minifilters, that Mimidrv allows use to interact with.

Filter drivers are considered legacy but are still supported. There are many types of filter drivers, but they all serve to expand the functionality of devices by filtering IRPs. Different subclasses of filter drivers exist to serve specific jobs, such as file system filter drives and network filter drivers. Example of a file system filter driver would be an antivirus engine, backup agent, or an encryption agent.

The most common filter driver you will see is FltMgr.sys, which exposes functionality required by filesystem filters so that developers can more easily develop minifilter drivers.

Minifilter drivers are Microsoft’s recommendation for filter driver development and include some distinct advantages, including being able to be unloaded without a reboot and reduced code complexity. These types of drivers are more common than legacy filter drivers and can be listed/managed with fltmc.exe.

1*xxphobOFoIS-kuM6lQyTyg.png?q=20
1*xxphobOFoIS-kuM6lQyTyg.png

The biggest difference between these 2 types in the context of Mimidrv is that minifilter drivers are managed via the Filter Manager APIs.

!filters

The !filters command works almost exactly the same as the !modules command, but instead leverages nt!IoEnumerateRegisteredFiltersList to get a list of registered filesystem filter drivers on the system, stores them in a DRIVER_OBJECT struct, and prints out the index of the driver as well as the DriverName member.

1*hEOpeTLVW8Q27UzN7fhICQ.png?q=20
1*hEOpeTLVW8Q27UzN7fhICQ.png

!minifilters

The !minifilters command displays the minifilter drivers registered on the system. This function is a little tough to read, but that’s because the functions Mimidrv needs to call have memory requirements that aren’t known at runtime, so it makes a request solely to get the amount of memory required, allocates that memory, and then makes the real request. To help understand what is going on, it is helpful to break down each step by primary function.

  1. FltEnumerateFilters — The first call is to fltmgr!FltEnumerateFilters, which enumerates all registered minifilter drivers on the system and return a list of pointers.
  2. FltGetFilterInformation — Next, we iterate over this list of pointers, calling fltmgr!FltGetFilterInformation to get a FILTER_FULL_INFORMATION structure back, containing details about each of the minifilters.
  3. FltEnumerateInstances — For each of the minifilters, fltmgr!FltEnumerateInstances is used to get a list of instance pointers.
  4. FltGetVolumeFromInstance — Next, fltmgr!FltGetVolumeFromInstance is used to return the volume each minifilter is attached to (e.g. \Device\HarddiskVolume4). Note that minifilters can have multiple instances attached to different volumes.
  5. Get details about pre- and post-operation callbacks — We’ll dig into this next.
  6. FltObjectDereference — When all instances have been iterated through, fltmgr!FltObjectDereference is used to deference each instance and the list of minifilters.

As you can see, Mimidrv makes use of some pretty standard Filter Manager API functions. However, step 5 is a bit odd in that it gets information about the minifilter using hardcoded offsets and makes calls to kkll_m_modules_fromAddr to get offsets without much indiction of what we are looking at. In the output of !minifilters, there are addresses of PreCallback and/or PostCallback, but what are these?

Minifilter drivers may register up to 1 pre-operation callback and up to 1 post-operation callback for each operation that it needs to filter. When the Filter Manager processes an I/O operation, it passes the request down the driver stack starting with the minifilter with the highest altitude that has registered a pre-operation callback. This is the minifilter’s opportunity to act on the I/O operation before it is passed to the file system for completion. After the I/O operation is complete, the Filter Manager again passes down the driver stack for drivers with registered post-operation callbacks. Within these callbacks, the drivers can interact with the data, such as examining it or modifying it.

In order to understand what Mimidrv is parsing out, lets dig into an example from the output of !minifilters on my system, specifically for the Named Pipe Service Triggers driver, npsvctrig.sys.

1*UuzNdVOf8aS4UPP7JUEA_g.png?q=20
1*UuzNdVOf8aS4UPP7JUEA_g.png

We’ll crack open WinDbg and first look for our registered filters.

1*xC0Z4LLAYhI0Xnm_vN-rxA.png?q=20
1*xC0Z4LLAYhI0Xnm_vN-rxA.png

Here we can see an instance of npsvctrig at address 0xffffc18f97e34cb0. Inspecting the FLT_INSTANCE structure at this address shows the member CallbackNodes at offset 0x0a0.

1*WHpe0CJFx_YSuC859ZrrMw.png?q=20
1*WHpe0CJFx_YSuC859ZrrMw.png

There are 3 CALLBACK_NODE structures (screenshot snipped for viewing).

1*lj4OajCHsuCYmunR3uteAg.png?q=20
1*lj4OajCHsuCYmunR3uteAg.png

Inspecting the first CALLBACK_NODE structure at 0xffffc18f97e34d50, we can see the PostOperation attribute (offset 0x20) has an address of 0xfffff8047e5f6010, the same that was shown in Mimikatz for “CLOSE”, which correlates to IRP_MJ_CLOSE. That means that this is a pointer to the post-operation callback’s address!

1*KWQ7_dILN--7qscETS2CCA.png?q=20
1*KWQ7_dILN--7qscETS2CCA.png

But what about the offset inside the driver show in the output? To get this for us, Mimidrv calls kkll_m_modules_fromAddr, which in turn calls kkll_m_modules_enum, which we walked through in the “Modules” section, but this time with a callback function of kkll_m_modules_fromAddr_callback. This callback returns the address of the callback, the filename of the driver excluding the path, and the offset of the address we provided from the image’s base address.

If we take a quick look at the offset 0x6010 inside of npsvctrig.sys, we can see that it is the start of its NptrigPostCreateCallback function.

1*qEZj7N8jNjEfiLGRMUS2pg.png?q=20
1*qEZj7N8jNjEfiLGRMUS2pg.png

Memory

These functions, while not implemented as commands available to the user, allow interaction with kernel memory and expose some interesting nuances to consider when working with memory in the kernel. These could be called by Mimikatz as they have correlating IOCTLs, so it is worth walking through what they do.

kkll_m_memory_vm_read

If the name didn’t give it away, this function could be used to read memory in the kernel. It is a very simple function but introduces 2 concepts we haven’t explored yet — Memory Descriptor Lists (MDLs) and page locking.

Virtual memory should be contiguous, but physical memory can be all over the place. Windows uses MDLs to describe the physical page layout for a virtual memory buffer which helps in describing and mapping memory properly.

In some cases we may need to access data quickly and directly and we don’t want the memory manager messing with that data (e.g. paging it to disk). To make sure that this doesn’t happen, we can use nt!MmProbeAndLockPages to lock the physical pages mapped by the virtual pages in memory temporarily so they can’t be paged out. This function requires that an operation be specified when called which describes what will be done. These can be either IoReadAccess, IoWriteAccess, or IoModifyAccess. After the operation completes, nt!MmUnlockPages is used to unlock the pages.

The 2 concepts make up most of kkll_m_memory_vm_read. A MDL is allocated using nt!IoAllocateMdl, pages are locked with the nt!IoReadAccess specified, nt!RtlCopyMemory is used to copy memory from the MDL to the output buffer, and then the pages are unlocked with a call to nt!MmUnlockPages. This allows us to read arbitrary memory from the kernel.

kkll_m_memory_vm_write

This function is a mirror image of kkll_m_memory_vm_read, but the Dest and From parameters are switched as we are writing to an address described by the MDL as opposed to reading from it.

kkll_m_memory_vm_alloc

The kkll_m_memory_vm_alloc function allows for allocation of arbitrarily-sized memory from the non-paged pool by calling nt!ExAllocatePoolWithTag. and returns a pointer to the address where memory was allocated.

This could be used in place of some of the direct calls to nt!ExAllocatePoolWithTag in Mimidrv as it implements error checking which could make the code a little more stable and easier to read.

kkll_m_memory_vm_free

As with all other types of memory, non-paged pool memory must be freed. The kkll_m_memory_vm_free function does just that with a call to nt!ExFreePoolWithTag.

Like the function above, this could be used in place of direct calls to nt!ExFreePoolWithTag, but isn’t currently being used by Mimidrv.


SSDT

When a user mode application needs to create a file by using kernel32!CreateFile, how is the disk accessed and storage allocated for the user? Accessing system resources is a function of the kernel but these resources are needed by user mode applications, so there needs to be a way to make requests to the kernel. Windows makes use of system calls, or syscalls, to make this possible.

Under the hood, here’s a rough view of what kernel32!CreateFile is actually doing:

1*OvGXRYroJuXGgtFqNTliRw.png?q=20
1*OvGXRYroJuXGgtFqNTliRw.png

Right at the boundary between user mode and kernel mode, you can see a call to sysenter (this could also be substituted for syscall depending on the processor), which is used to transfer from user mode to kernel mode. This instruction takes a number, specifically a system service number, in the EAX register which determines which system call to make. @j00ru maintains a list of Windows syscalls and their service numbers on his blog.

In our kernel32!CreateFile example, ntdll!NtCreateFile places 0x55 into EAX before the SYSCALL instruction.

1*NwWoh61UbXd8TRSdGhi9_Q.png?q=20
1*NwWoh61UbXd8TRSdGhi9_Q.png

On the SYSCALL, KiSystemService in ring 0 receives the request and looks up the system service function in the System Service Descriptor Table (SSDT), KeServiceDescriptorTable. The SSDT holds pointers to kernel functions, and in this case we are looking for nt!NtCreateFile.

In the past, rootkits would hook the SSDT and replace the pointer to kernel functions so that when system services were called, a function inside of their rootkit would be executed instead. Thankfully, Kernel Patch Protection (KPP/PatchGuard) protects critical kernel structures, such as the SSDT, from modification so this technique does not work on modern x64 systems.

!ssdt

The !ssdt command locates the KeServiceDescriptorTable in memory by searching for an OS version-specific pattern (0xd3, 0x41, 0x3b, 0x44, 0x3a, 0x10, 0x0f, 0x83 in Windows 10 1803+) which marks the pointer to the KeServiceDescriptorTable structure.

1*84RCGvpb9_hKwA9mk47HzA.png?q=20
1*84RCGvpb9_hKwA9mk47HzA.png

Inside of the KeServiceDescriptorTable structure is a pointer to another structure, KiServiceTable, which contains an array of 32-bit offsets relative to KiServiceTable itself.

1*RrTX9lHB5DGQlsO1SsqYAg.png?q=20
1*RrTX9lHB5DGQlsO1SsqYAg.png

Because we can’t really work with these offsets in WinDbg as they are left-shifted 4 bits, we can right-shift it by 4 bits and add it to KiServiceTable to get the correct address.

1*m-i9EEaaMdAoBJu-XRem7Q.png?q=20
1*m-i9EEaaMdAoBJu-XRem7Q.png

We can also use some of WinDbg’s more advanced features to process the offsets and print out the module located at the calculated addresses to get the addresses of all services.

1*IKpEs8bPRMKKT6w1LtoK4A.png?q=20
1*IKpEs8bPRMKKT6w1LtoK4A.png

This is the exact same thing the Mimidrv is doing after locating KeServiceDescriptorTable in order to locate pointers to services. If first prints out the index (e.g. 85 for NtCreateFile as shown in the earlier WinDbg screenshot) followed by the address. Then kkll_m_modules_fromAddr, which you’ll remember from earlier sections, is called to get the offset of the service/function inside of ntoskrnl.exe.

1*ZFvh2tVb3N0LFh1lDDdDEA.png?q=20
1*ZFvh2tVb3N0LFh1lDDdDEA.png

Using the indexes provided by WinDbg, we can see the the address at index 0 points to nt!NtAccessCheck. which resides at offset 0x112340 in ntoskrnl.exe.

1*JVLGTU5ak6oqM2bs_hdT0A.png?q=20
1*JVLGTU5ak6oqM2bs_hdT0A.png
1*p703fOfVJA9e7MOJqBMIWg.png?q=20
1*p703fOfVJA9e7MOJqBMIWg.png

Defending Against Driver-Based Threats

Now that we’ve covered the inner workings of Mimidrv, how do we prevent the bad guys from getting in implanted on our systems in the first place? Using drivers against Windows 10 systems introduces some unique challenges for us as attackers, the largest of which being that drivers must be signed.

Mimidrv has many static indicators that are easily modifiable, but require recompilation and re-signing using a new EV certificate. Because of the cost that comes with modifying Mimidrv, a brittle detection is still worth implementing. A few of the default indicators for Mimidrv implantation and organized by source are:

Windows Event ID 7045/4697 — Service Creation

  • Service Name: “mimikatz driver (mimidrv)”
  • Service File Name: *\mimidrv.sys
  • Service Type: kernel mode driver (0x1)
  • Service Start Type: auto start (2)

Note: Event ID 4697 contains information about the account that loaded the driver, which could aide in hunting. Audit Security System Extension must be configured via Group Policy for this event to be generated.

1*hp12K-idtn8zuS47Weacpw.png?q=20
1*hp12K-idtn8zuS47Weacpw.png

Sysmon Event ID 11 — File Creation

  • TargetFilename: *\mimidrv.sys

Sysmon Event ID 6 — Driver Loaded

  • ImageLoaded: *\mimidrv.sys
  • SignatureStatus: Expired

Another more broad approach to this problem is to step back even further and looks at the attributes of unwanted drivers as a whole.

Third-party drivers are an inevitability for most organizations, but knowing what the standard is for your fleet and identifying anomalies is a worthwhile exercise. Windows Defender Application Control (WDAC) makes this incredibly simple to audit on Windows 10 systems.

My colleague Matt Graeber wrote an excellent post on deploying a Code Integrity Policy and beginning to audit the loading of any non-Windows, Early Load AntiMalware (ELAM), or Hardware Abstraction Layer (HAL) drivers. After a reboot, the system will begin generating logs with Event ID 3076 for any driver that would have been blocked with the base policy.

1*7J7XONLq3GkyfgYTnIdnbQ.png?q=20
1*7J7XONLq3GkyfgYTnIdnbQ.png

From here, we can begin to figure out which drivers are needed outside of the base policy, grant exemptions for them, and begin tuning detection logic to allow analysts to triage anomalous driver loads more efficiently.

Further Reading

If you have found this material interesting, here are some resources that cover some of the details that I glossed over in this post:

Posts By SpecterOps Team Members

Posts from SpecterOps team members on various topics relating information security

Thanks to Matt Graeber. 

 

 

Written by

I like red teaming, picking up heavy things, and burritos. Adversary Simulation @ SpecterOps. github.com/matterpreter

 

Sursa: https://posts.specterops.io/mimidrv-in-depth-4d273d19e148

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...