Implemented profile embedding via patching a placeholder in the agent executable. Agent correctly deserializes and parses the profile and listener configuration.

This commit is contained in:
Jakob Friedl
2025-08-18 22:05:23 +02:00
parent 023a562be5
commit 84e8730b1e
15 changed files with 258 additions and 153 deletions

View File

@@ -2,7 +2,7 @@ import terminal, strformat, strutils, sequtils, tables, json, times, base64, sys
import ../[utils, globals]
import ../db/database
import ../message/packer
import ../protocol/packer
import ../../common/[types, utils]
#[
@@ -40,8 +40,8 @@ proc getTasks*(heartbeat: seq[byte]): seq[seq[byte]] =
# Deserialize checkin request to obtain agentId and listenerId
let
request: Heartbeat = cq.deserializeHeartbeat(heartbeat)
agentId = uuidToString(request.header.agentId)
listenerId = uuidToString(request.listenerId)
agentId = Uuid.toString(request.header.agentId)
listenerId = Uuid.toString(request.listenerId)
timestamp = request.timestamp
var result: seq[seq[byte]]
@@ -72,9 +72,9 @@ proc handleResult*(resultData: seq[byte]) =
let
taskResult = cq.deserializeTaskResult(resultData)
taskId = uuidToString(taskResult.taskId)
agentId = uuidToString(taskResult.header.agentId)
listenerId = uuidToString(taskResult.listenerId)
taskId = Uuid.toString(taskResult.taskId)
agentId = Uuid.toString(taskResult.header.agentId)
listenerId = Uuid.toString(taskResult.listenerId)
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"{$resultData.len} bytes received.")

View File

@@ -34,6 +34,7 @@ Commands:
info Display details for a specific agent.
kill Terminate the connection of an active listener and remove it from the interface.
interact Interact with an active agent.
build Generate a new agent to connect to an active listener.
Options:
-h, --help""")
@@ -124,65 +125,3 @@ proc agentInteract*(cq: Conquest, name: string) =
cq.interactAgent = nil
# Agent generation
proc agentBuild*(cq: Conquest, listener, sleep, payload: string) =
# Verify that listener exists
if not cq.dbListenerExists(listener.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] Listener {listener.toUpperAscii} does not exist.")
return
let listener = cq.listeners[listener.toUpperAscii]
# Create/overwrite nim.cfg file to set agent configuration
let AgentCtxFile = fmt"../src/agent/nim.cfg"
# Parse IP Address and store as compile-time integer to hide hardcoded-strings in binary from `strings` command
let (first, second, third, fourth) = parseOctets(listener.address)
# Covert the servers's public X25519 key to as base64 string
let publicKey = encode(cq.keyPair.publicKey)
let profileString = encode(cq.profile.toTomlString())
# The following shows the format of the agent configuration file that defines compile-time variables
let config = fmt"""
# Agent configuration
-d:ListenerUuid="{listener.listenerId}"
-d:Octet1="{first}"
-d:Octet2="{second}"
-d:Octet3="{third}"
-d:Octet4="{fourth}"
-d:ListenerPort={listener.port}
-d:SleepDelay={sleep}
-d:ServerPublicKey="{publicKey}"
-d:ProfileString="{profileString}"
""".replace(" ", "")
writeFile(AgentCtxFile, config)
cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Configuration file created.")
# Build agent by executing the ./build.sh script on the system.
let agentBuildScript = fmt"../src/agent/build.sh"
cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Building agent...")
try:
# Using the startProcess function from the 'osproc' module, it is possible to retrieve the output as it is received, line-by-line instead of all at once
let process = startProcess(agentBuildScript, options={poUsePath, poStdErrToStdOut})
let outputStream = process.outputStream
var line: string
while outputStream.readLine(line):
cq.writeLine(line)
let exitCode = process.waitForExit()
# Check if the build succeeded or not
if exitCode == 0:
cq.writeLine(fgGreen, "[+] ", resetStyle, "Agent payload generated successfully.")
else:
cq.writeLine(fgRed, styleBright, "[-] ", resetStyle, "Build script exited with code ", $exitCode)
except CatchableError as err:
cq.writeLine(fgRed, styleBright, "[-] ", resetStyle, "An error occurred: ", err.msg)

148
src/server/core/builder.nim Normal file
View File

@@ -0,0 +1,148 @@
import terminal, strformat, strutils, sequtils, tables, times, system, osproc, streams, base64, parsetoml
import ../utils
import ../../common/[types, utils, profile, serialize]
import ../db/database
const PLACEHOLDER = "PLACEHOLDER"
proc serializeConfiguration(cq: Conquest, listener: Listener, sleep: int): seq[byte] =
var packer = Packer.init()
# Add listener configuration
packer.add(uint8(CONFIG_LISTENER_UUID))
packer.add(uint32(sizeof(uint32)))
packer.add(string.toUuid(listener.listenerId))
packer.add(uint8(CONFIG_LISTENER_IP))
packer.add(uint32(listener.address.len))
packer.addData(string.toBytes(listener.address))
packer.add(uint8(CONFIG_LISTENER_PORT))
packer.add(uint32(sizeof(uint32)))
packer.add(uint32(listener.port))
packer.add(uint8(CONFIG_SLEEP_DELAY))
packer.add(uint32(sizeof(uint32)))
packer.add(uint32(sleep))
# Add key exchange information
packer.add(uint8(CONFIG_PUBLIC_KEY))
packer.add(uint32(sizeof(Key)))
packer.addData(cq.keyPair.publicKey)
# Add C2 profile string
let profileString = cq.profile.toTomlString()
packer.add(uint8(CONFIG_PROFILE))
packer.add(uint32(profileString.len))
packer.addData(string.toBytes(profileString))
let data = packer.pack()
cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Profile configuration serialized.")
return data
proc compile(cq: Conquest, placeholderLength: int): string =
let
cqDir = cq.profile.getString("conquest_directory")
configFile = fmt"{cqDir}/src/agent/nim.cfg"
exeFile = fmt"{cqDir}/bin/monarch.x64.exe"
agentBuildScript = fmt"{cqDir}/src/agent/build.sh"
# Create/overwrite nim.cfg file to set placeholder for agent configuration
let config = fmt"""
# Agent configuration
-d:CONFIGURATION={PLACEHOLDER & "A".repeat(placeholderLength - (2 * len(PLACEHOLDER))) & PLACEHOLDER}
-o:"{exeFile}"
""".replace(" ", "")
writeFile(configFile, config)
cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Configuration file created.")
# Build agent by executing the ./build.sh script on the system.
cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Compiling agent.")
try:
# Using the startProcess function from the 'osproc' module, it is possible to retrieve the output as it is received, line-by-line instead of all at once
let process = startProcess(agentBuildScript, options={poUsePath, poStdErrToStdOut})
let outputStream = process.outputStream
var line: string
while outputStream.readLine(line):
cq.writeLine(line)
let exitCode = process.waitForExit()
# Check if the build succeeded or not
if exitCode == 0:
cq.writeLine(fgGreen, "[*] ", resetStyle, "Agent payload generated successfully.")
return exeFile
else:
cq.writeLine(fgRed, styleBright, "[-] ", resetStyle, "Build script exited with code ", $exitCode)
return ""
except CatchableError as err:
cq.writeLine(fgRed, styleBright, "[-] ", resetStyle, "An error occurred: ", err.msg)
return ""
proc patch(cq: Conquest, unpatchedExePath: string, configuration: seq[byte]): bool =
cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Patching profile configuration into agent.")
try:
var exeBytes = readFile(unpatchedExePath)
# Find placeholder
let placeholderPos = exeBytes.find(PLACEHOLDER)
if placeholderPos == -1:
raise newException(CatchableError, "Placeholder not found.")
cq.writeLine(fgBlack, styleBright, "[+] ", resetStyle, fmt"Placeholder found at offset {placeholderPos}.")
# cq.writeLine(exeBytes[placeholderPos..placeholderPos + len(configuration)])
# Patch placeholder bytes
for i, c in Bytes.toString(configuration):
exeBytes[placeholderPos + i] = c
writeFile(unpatchedExePath, exeBytes)
cq.writeLine(fgGreen, "[+] ", resetStyle, fmt"Agent payload patched successfully: {unpatchedExePath}.")
except CatchableError as err:
cq.writeLine(fgRed, styleBright, "[-] ", resetStyle, "An error occurred: ", err.msg)
return false
return true
# Agent generation
proc agentBuild*(cq: Conquest, listener, sleep: string): bool {.discardable.} =
# Verify that listener exists
if not cq.dbListenerExists(listener.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] Listener {listener.toUpperAscii} does not exist.")
return false
let listener = cq.listeners[listener.toUpperAscii]
var config: seq[byte]
if sleep.isEmptyOrWhitespace():
# If no sleep value has been defined, take the default from the profile
config = cq.serializeConfiguration(listener, cq.profile.getInt("agent.sleep"))
else:
config = cq.serializeConfiguration(listener, parseInt(sleep))
let unpatchedExePath = cq.compile(config.len)
if unpatchedExePath.isEmptyOrWhitespace():
return false
if not cq.patch(unpatchedExePath, config):
return false
return true

View File

@@ -1,7 +1,7 @@
import prompt, terminal, argparse, parsetoml
import strutils, strformat, times, system, tables
import ./[agent, listener]
import ./[agent, listener, builder]
import ../[globals, utils]
import ../db/database
import ../../common/[types, utils, crypto, profile]
@@ -53,8 +53,8 @@ var parser = newParser:
command("build"):
help("Generate a new agent to connect to an active listener.")
option("-l", "--listener", help="Name of the listener.", required=true)
option("-s", "--sleep", help="Sleep delay in seconds.", default=some("10") )
option("-p", "--payload", help="Agent type.\n\t\t\t ", default=some("monarch"), choices = @["monarch"],)
option("-s", "--sleep", help="Sleep delay in seconds." )
# option("-p", "--payload", help="Agent type.\n\t\t\t ", default=some("monarch"), choices = @["monarch"],)
command("help"):
nohelpflag()
@@ -104,7 +104,7 @@ proc handleConsoleCommand(cq: Conquest, args: string) =
of "interact":
cq.agentInteract(opts.agent.get.interact.get.name)
of "build":
cq.agentBuild(opts.agent.get.build.get.listener, opts.agent.get.build.get.sleep, opts.agent.get.build.get.payload)
cq.agentBuild(opts.agent.get.build.get.listener, opts.agent.get.build.get.sleep)
else:
cq.agentUsage()
@@ -129,16 +129,13 @@ proc header() =
proc init*(T: type Conquest, profile: Profile): Conquest =
var cq = new Conquest
var prompt = Prompt.init()
cq.prompt = prompt
cq.prompt = Prompt.init()
cq.listeners = initTable[string, Listener]()
cq.agents = initTable[string, Agent]()
cq.interactAgent = nil
cq.profile = profile
cq.keyPair = loadKeyPair(profile.getString("private_key_file"))
cq.dbPath = profile.getString("database_file")
cq.profile = profile
return cq
proc startServer*(profilePath: string) =

View File

@@ -1,7 +1,7 @@
import times, strformat, terminal, tables, json, sequtils, strutils
import ../utils
import ../message/parser
import ../protocol/parser
import ../../modules/manager
import ../../common/[types, utils]
@@ -62,6 +62,7 @@ proc handleAgentCommand*(cq: Conquest, input: string) =
# Handle 'back' command
if parsedArgs[0] == "back":
cq.interactAgent = nil
return
# Handle 'help' command

View File

@@ -21,7 +21,7 @@ proc serializeTask*(cq: Conquest, task: var Task): seq[byte] =
packer.reset()
# Encrypt payload body
let (encData, gmac) = encrypt(cq.agents[uuidToString(task.header.agentId)].sessionKey, task.header.iv, payload, task.header.seqNr)
let (encData, gmac) = encrypt(cq.agents[Uuid.toString(task.header.agentId)].sessionKey, task.header.iv, payload, task.header.seqNr)
# Set authentication tag (GMAC)
task.header.gmac = gmac
@@ -42,7 +42,7 @@ proc deserializeTaskResult*(cq: Conquest, resultData: seq[byte]): TaskResult =
# Decrypt payload
let payload = unpacker.getBytes(int(header.size))
let decData= validateDecryption(cq.agents[uuidToString(header.agentId)].sessionKey, header.iv, payload, header.seqNr, header)
let decData= validateDecryption(cq.agents[Uuid.toString(header.agentId)].sessionKey, header.iv, payload, header.seqNr, header)
# Deserialize decrypted data
unpacker = Unpacker.init(Bytes.toString(decData))
@@ -102,8 +102,8 @@ proc deserializeNewAgent*(cq: Conquest, data: seq[byte]): Agent =
sleep = unpacker.getUint32()
return Agent(
agentId: uuidToString(header.agentId),
listenerId: uuidToString(listenerId),
agentId: Uuid.toString(header.agentId),
listenerId: Uuid.toString(listenerId),
username: username,
hostname: hostname,
domain: domain,
@@ -130,7 +130,7 @@ proc deserializeHeartbeat*(cq: Conquest, data: seq[byte]): Heartbeat =
# Decrypt payload
let payload = unpacker.getBytes(int(header.size))
let decData= validateDecryption(cq.agents[uuidToString(header.agentId)].sessionKey, header.iv, payload, header.seqNr, header)
let decData= validateDecryption(cq.agents[Uuid.toString(header.agentId)].sessionKey, header.iv, payload, header.seqNr, header)
# Deserialize decrypted data
unpacker = Unpacker.init(Bytes.toString(decData))

View File

@@ -77,8 +77,8 @@ proc createTask*(cq: Conquest, command: Command, arguments: seq[string]): Task =
# Construct the task payload prefix
var task: Task
task.taskId = uuidToUint32(generateUUID())
task.listenerId = uuidToUint32(cq.interactAgent.listenerId)
task.taskId = string.toUuid(generateUUID())
task.listenerId = string.toUuid(cq.interactAgent.listenerId)
task.timestamp = uint32(now().toTime().toUnix())
task.command = cast[uint16](command.commandType)
task.argCount = uint8(arguments.len)
@@ -105,7 +105,7 @@ proc createTask*(cq: Conquest, command: Command, arguments: seq[string]): Task =
taskHeader.packetType = cast[uint8](MSG_TASK)
taskHeader.flags = cast[uint16](FLAG_ENCRYPTED)
taskHeader.size = 0'u32
taskHeader.agentId = uuidtoUint32(cq.interactAgent.agentId)
taskHeader.agentId = string.toUuid(cq.interactAgent.agentId)
taskHeader.seqNr = nextSequence(taskHeader.agentId)
taskHeader.iv = generateIV() # Generate a random IV for AES-256 GCM
taskHeader.gmac = default(AuthenticationTag)