Exploiting Windows Kernel Drivers: System Mechanic (15.5.0.61)
Introduction
In the Windows Kernel Exploitation Training led by Ashfaq Ansari, he presents a challenge to write an exploit for the System Mechanic driver without providing further information. In this blog post, we’ll delve into driver reconnaissance, initial vulnerability discovery, and ultimately, driver exploitation.
Driver Recon
Device Symbolic Links
In order for a user-mode application to communicate with a kernel driver via IOCTLs (explained below), the user-mode process must open a handle to the device object using a symbolic link that the driver has created. The kernel API functions listed below are utilized to create the symbolic link.
IoCreateDevice
: Creates device nameIoCreateSymbolicLink
: Creates the symbolic link to the previously created device name.
The simplest method to discover this information is by examining the import table and using cross-referencing to identify which function employs these APIs, thereby swiftly pinpointing the device name of the target driver you are analyzing. In our case, it was the following function: Device Creation APIs
We can confirm that this is the correct symbolic link name by using WinObj and Process Explorer from Sysinternals Suit
Major Functions
Major Functions are essentially a set of predefined callback functions that a driver implements to handle various I/O requests. These functions form the backbone of a driver’s interaction with the system and applications. They are defined in a driver’s Dispatch Table, which is a part of the Driver Object. The Windows operating system invokes these functions in response to I/O requests from applications or other system components.
Each Major Function corresponds to a specific type of I/O request, such as read, write, device control, system control, and so on. By implementing these functions, a driver specifies how it will respond to these requests. The primary Major Functions include:
- IRP_MJ_CREATE: Invoked when a file object associated with the driver is being created. This is typically the first operation after a driver is loaded.
- IRP_MJ_CLOSE: Called when a file object is being closed. It allows the driver to perform cleanup tasks.
- IRP_MJ_READ and IRP_MJ_WRITE: Handle read and write requests to the device managed by the driver.
- IRP_MJ_DEVICE_CONTROL (IOCTL): Used for device-specific input/output operations that are not covered by standard Major Functions.
- IRP_MJ_CLEANUP: Invoked before IRP_MJ_CLOSE, allowing the driver to perform necessary cleanup operations associated with the closing of file objects.
- IRP_MJ_POWER: Manages power state transitions for the device, ensuring the device can enter and exit low-power states correctly.
When viewing the System Mechanic driver, We can see the callback function for IRP_MJ_DEVICE_CONTROL being set after the creation of Symbolic Link in the InitDevice routine.
Device IOCTL
IOCTLs, short for Input/Output Control, are defined as unique codes that enable an application to send commands to a driver to perform operations specific to the hardware or device that the driver controls. These operations can range from formatting a disk, changing the configuration of a device, to querying the status of a system component. Each IOCTL code represents a distinct command, accompanied by input and/or output parameters that provide additional data for the operation.
IOCTLs are implemented within a driver through the IRP_MJ_DEVICE_CONTROL Major Function. When an application issues an IOCTL command, the system packages the request into an I/O Request Packet (IRP) and dispatches it to the driver’s DeviceIoControl function.
In case of our System Mechanic driver we can find the IOCTL code by looking inside the function that I named irpDeviceIoctlHandler
.
In the screenshot above we can see that the IRP is parsed and the IOCTL received equals 0x226003
. If this IOCTL is not found System Mechanic returns the status 0xc0000010 (STATUS_INVALID_DEVICE_REQUEST). The IOCTL code can be parsed using the OSR Online IOCTL Decoder.
The parsed IOCTL provides two important pieces of information. First it shows that METHOD_NEITHER is used, this is useful because we know that the buffer will not be verified by the I/O Manager. Second we know that for a usermode application to send this IOCTL code, it will need to have Read privileges on the device handle. We already confirmed these privileges when viewing the device in WinObj.
Initial Vulnerability Discovery
Fuzzing & Crash Triage
I spent more time on reversing the driver because I wanted to gain a deeper understanding of the driver’s capabilities before deciding to write a quick and dirty fuzzer that would utilize the expected structures we uncovered during our driver analysis.
The structures that the driver expects are shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _arguments_to_func
{
uint64_t arg1;
uint64_t arg2;
int64_t arg3;
int64_t arg4;
} arguments_to_func;
typedef struct _input_buffer
{
DWORD function_index;
DWORD padding;
arguments_to_func *ptr_to_args;
PVOID ptr2_to_return;
} input_buffer;
The first crash occured fairly quickly and it looked like it was trying to dereference a pointer to structure that holds the arguments passed to the function.
This means that with our user inputs, we can invoke any of the eight functions from the function table. Since we control the four arguments passed to each function, we have a greater chance of discovering a vulnerability.
Vulnerability Analysis (Arbitrary Write)
For this driver, it was possible to achieve a simple yet effective arbitrary write without dedicating extensive time to code analysis. This was due to how the return value from a function was written to a user-supplied pointer. To successfully exploit this, I needed to identify a function that was easy to control and would return a specific value. This value could then be used to overwrite a kernel structure, thereby achieving Local Privilege Escalation (LPE).
Return value gets saved in user controlled buffer
By examining each of the functions from the function table, I found that I could call any of the functions, as most of them returned either 0xFFFFFFFE
or 0xFFFFFFF4
. However, not all of them had the same constraints, so I decided to look for a quick and easy-to-call function that wasn’t too lengthy or complex. For this purpose, I chose the function at index 5, which returns 0xFFFFFFFE
almost immediately if either the first or second argument fails the check.
Now, all we need to do is write an exploit that takes advantage of this vulnerability and enables most of the privileges on our process token. Each process in the kernel has a pointer to a token structure, and most kernel exploits rely on modifying this token structure to achieve privilege escalation.
Here is an _EPROCESS
structure which represents a process in the kernel:
Most of the fields have been removed due to the size of this structure. Full structure can be found here
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1: kd> !process 0 0 notepad.exe
PROCESS ffffd2836109b0c0
SessionId: 1 Cid: 1d1c Peb: b6b0c78000 ParentCid: 1374
DirBase: be01c000 ObjectTable: ffff9a8cc1ed15c0 HandleCount: 169.
Image: notepad.exe
0: kd> dt nt!_EPROCESS ffffd2836109b0c0
+0x000 Pcb : _KPROCESS
+0x438 ProcessLock : _EX_PUSH_LOCK
+0x440 UniqueProcessId : 0x00000000`00001d1c Void
+0x448 ActiveProcessLinks : _LIST_ENTRY [ 0xffffd283`6089a508 - 0xffffd283`60e34508 ]
+0x458 RundownProtect : _EX_RUNDOWN_REF
...
+0x4b8 Token : _EX_FAST_REF
...
As indicated, the _EPROCESS
structure includes a field named Token
, which is of type _EX_FAST_REF
. The _EX_FAST_REF
structure is utilized by the Object Manager for fast referencing, though we won’t delve into its intricate details here. For the purposes of understanding how to exploit the structure for privilege escalation, it’s essential to know that the _EX_FAST_REF structure contains a pointer to a _TOKEN
structure. However, before proceeding with manipulating the Token, it’s crucial to understand how the _EX_FAST_REF
structure is defined.
1
2
3
4
5
0: kd> dt -v nt!_EX_FAST_REF
struct _EX_FAST_REF, 3 elements, 0x8 bytes
+0x000 Object : Ptr64 to Void
+0x000 RefCnt : Bitfield Pos 0, 4 Bits
+0x000 Value : Uint8B
As we can see its just a simple union so why does the value stored in this structure can’t be used straight away?
The reason the value stored in the _EX_FAST_REF
structure cannot be used directly is due to the RefCnt
field, which represents the reference counting bits. These are the last 4 bits in the pointer that this structure holds. Essentially, the _EX_FAST_REF
is designed as a union to efficiently pack a pointer and reference count into a single entity, with the reference count occupying the lower bits of the pointer itself. This clever use of bit manipulation allows for fast referencing and dereferencing operations within the kernel’s object manager.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
struct _TOKEN
{
struct _TOKEN_SOURCE TokenSource; //0x0
struct _LUID TokenId; //0x10
struct _LUID AuthenticationId; //0x18
struct _LUID ParentTokenId; //0x20
union _LARGE_INTEGER ExpirationTime; //0x28
struct _ERESOURCE* TokenLock; //0x30
struct _LUID ModifiedId; //0x34
struct _SEP_TOKEN_PRIVILEGES Privileges; //0x40
struct _SEP_AUDIT_POLICY AuditPolicy; //0x58
ULONG SessionId; //0x78
ULONG UserAndGroupCount; //0x7c
ULONG RestrictedSidCount; //0x80
ULONG VariableLength; //0x84
ULONG DynamicCharged; //0x88
ULONG DynamicAvailable; //0x8c
ULONG DefaultOwnerIndex; //0x90
struct _SID_AND_ATTRIBUTES* UserAndGroups; //0x94
struct _SID_AND_ATTRIBUTES* RestrictedSids; //0x98
VOID* PrimaryGroup; //0x9c
ULONG* DynamicPart; //0xa0
struct _ACL* DefaultDacl; //0xa4
enum _TOKEN_TYPE TokenType; //0xa8
enum _SECURITY_IMPERSONATION_LEVEL ImpersonationLevel; //0xac
ULONG TokenFlags; //0xb0
UCHAR TokenInUse; //0xb4
ULONG IntegrityLevelIndex; //0xb8
ULONG MandatoryPolicy; //0xbc
struct _SEP_LOGON_SESSION_REFERENCES* LogonSession; //0xc0
struct _LUID OriginatingLogonSession; //0xc4
struct _SID_AND_ATTRIBUTES_HASH SidHash; //0xcc
struct _SID_AND_ATTRIBUTES_HASH RestrictedSidHash; //0x154
struct _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION* pSecurityAttributes; //0x1dc
VOID* Package; //0x1e0
struct _SID_AND_ATTRIBUTES* Capabilities; //0x1e4
ULONG CapabilityCount; //0x1e8
struct _SID_AND_ATTRIBUTES_HASH CapabilitiesHash; //0x1ec
struct _SEP_LOWBOX_NUMBER_ENTRY* LowboxNumberEntry; //0x274
struct _SEP_CACHED_HANDLES_ENTRY* LowboxHandlesEntry; //0x278
struct _AUTHZBASEP_CLAIM_ATTRIBUTES_COLLECTION* pClaimAttributes; //0x27c
VOID* TrustLevelSid; //0x280
struct _TOKEN* TrustLinkedToken; //0x284
VOID* IntegrityLevelSidValue; //0x288
struct _SEP_SID_VALUES_BLOCK* TokenSidValues; //0x28c
struct _SEP_LUID_TO_INDEX_MAP_ENTRY* IndexEntry; //0x290
struct _SEP_TOKEN_DIAG_TRACK_ENTRY* DiagnosticInfo; //0x294
struct _SEP_CACHED_HANDLES_ENTRY* BnoIsolationHandlesEntry; //0x298
VOID* SessionObject; //0x29c
ULONG VariablePart; //0x2a0
};
This structure represents the Access Token on windows and it includes various of information such as integrity level, privileges, groups etc.
Our exploit will be focusing on modifying the _SEP_TOKEN_PRIVILEGES
structure which holds value that represent which privileges are set for this process.
Exploitation
Exploit Stages
- Use
OpenProcessToken
API to obtain token object – later used to retrieve its kernel-space address. - Use the
NtQuerySystemInformation
API with SystemHandleInformation class to leak kernel addresses of all the objects with a handle. - Compare the token object in the current process with the objects retrieved from
NtQuerySystemInformation
call to get it’s kernel address effectively bypassing kASLR. - Build an IOCTL request for the vulnerable driver that will return
0xFFFFFFFE
and set the 4th member to point to the token privileges field. - Repeat set 4 to overwrite all fields in the
_SEP_TOKEN_PRIVILEGES
structure. - Spawn a cmd shell as
nt authority\system
Final Exploit
Full code of this exploit can be found here
Summary
This challenge served as an excellent test of the knowledge I gained from the training. It not only allowed me to apply the concepts in a practical setting but also deepened my understanding and appreciation for the intricacies involved in windows kernel exploitation.