Post

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

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 name
  • IoCreateSymbolicLink: 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: Untitled Device Creation APIs

We can confirm that this is the correct symbolic link name by using WinObj and Process Explorer from Sysinternals Suit

Untitled Device access permissions

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.

Untitled IOCTL Handling Routine

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.

Untitled Pointer Dereference Crash

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).

Indirect Function Call Indirect Function Call

Return value gets saved in user controlled buffer 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.

Target Function Target Function

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

  1. Use OpenProcessToken API to obtain token object – later used to retrieve its kernel-space address.
  2. Use the NtQuerySystemInformation API with SystemHandleInformation class to leak kernel addresses of all the objects with a handle.
  3. Compare the token object in the current process with the objects retrieved from NtQuerySystemInformation call to get it’s kernel address effectively bypassing kASLR.
  4. Build an IOCTL request for the vulnerable driver that will return 0xFFFFFFFE and set the 4th member to point to the token privileges field.
  5. Repeat set 4 to overwrite all fields in the _SEP_TOKEN_PRIVILEGES structure.
  6. 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.

This post is licensed under CC BY 4.0 by the author.