Added build log to client UI.

This commit is contained in:
Jakob Friedl
2025-10-02 12:10:46 +02:00
parent 5c0beb36ff
commit ab48bc5795
8 changed files with 137 additions and 22 deletions

View File

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

View File

@@ -2,6 +2,7 @@ import whisky
import tables, strutils, strformat, json, parsetoml, base64, os # native_dialogs
import ./utils/[appImGui, globals]
import ./views/[dockspace, sessions, listeners, eventlog, console]
import ./views/modals/generatePayload
import ../common/[types, utils, crypto]
import ./websocket
@@ -118,6 +119,10 @@ proc main(ip: string = "localhost", port: int = 37573) =
except IOError:
discard
# Close and reset the payload generation modal window when the payload was received
listenersTable.generatePayloadModal.resetModalValues()
igClosePopupToLevel(0, false)
of CLIENT_CONSOLE_ITEM:
let agentId = event.data["agentId"].getStr()
consoles[agentId].addItem(
@@ -132,7 +137,14 @@ proc main(ip: string = "localhost", port: int = 37573) =
event.data["message"].getStr(),
event.timestamp
)
of CLIENT_BUILDLOG_ITEM:
listenersTable.generatePayloadModal.addBuildlogItem(
cast[LogType](event.data["logType"].getInt()),
event.data["message"].getStr(),
event.timestamp
)
else: discard
# Draw/update UI components/views

View File

@@ -264,7 +264,7 @@ proc draw*(component: ConsoleComponent, connection: WsConnection) =
var labelSize: ImVec2
igCalcTextSize(addr labelSize, ICON_FA_MAGNIFYING_GLASS, nil, false, 0.0f)
let searchBoxWidth: float32 = 200.0f
let searchBoxWidth: float32 = 400.0f
igSameLine(0.0f, availableSize.x - (labelSize.x + textSpacing) - searchBoxWidth)
# Show tooltip when hovering the search icon

View File

@@ -1,4 +1,4 @@
import strutils, sequtils
import strutils, sequtils, times
import imguin/[cimgui, glfw_opengl, simple]
import ../../utils/[appImGui, colors]
import ../../../common/[types, profile, utils]
@@ -13,6 +13,8 @@ type
spoofStack: bool
sleepMaskTechniques: seq[string]
moduleSelection: DualListSelectionComponent[Module]
buildLog: ConsoleItems
proc AgentModal*(): AgentModalComponent =
result = new AgentModalComponent
@@ -36,12 +38,42 @@ proc AgentModal*(): AgentModalComponent =
result.moduleSelection = DualListSelection(modules, moduleName, compareModules, moduleDesc)
proc resetModalValues(component: AgentModalComponent) =
result.buildlog = new ConsoleItems
result.buildLog.items = @[]
proc resetModalValues*(component: AgentModalComponent) =
component.listener = 0
component.sleepDelay = 5
component.sleepMask = 0
component.spoofStack = false
component.moduleSelection.reset()
component.buildLog.items = @[]
proc addBuildlogItem*(component: AgentModalComponent, itemType: LogType, data: string, timestamp: int64 = now().toTime().toUnix()) =
for line in data.split("\n"):
component.buildLog.items.add(ConsoleItem(
timestamp: timestamp,
itemType: itemType,
text: line
))
proc print(component: AgentModalComponent, item: ConsoleItem) =
case item.itemType:
of LOG_INFO, LOG_INFO_SHORT:
igTextColored(CONSOLE_INFO, $item.itemType)
of LOG_ERROR, LOG_ERROR_SHORT:
igTextColored(CONSOLE_ERROR, $item.itemType)
of LOG_SUCCESS, LOG_SUCCESS_SHORT:
igTextColored(CONSOLE_SUCCESS, $item.itemType)
of LOG_WARNING, LOG_WARNING_SHORT:
igTextColored(CONSOLE_WARNING, $item.itemType)
of LOG_COMMAND:
igTextColored(CONSOLE_COMMAND, $item.itemType)
of LOG_OUTPUT:
igTextColored(vec4(0.0f, 0.0f, 0.0f, 0.0f), $item.itemType)
igSameLine(0.0f, 0.0f)
igTextUnformatted(item.text.cstring, nil)
proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBuildInformation =
@@ -109,6 +141,38 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
igSeparator()
igDummy(vec2(0.0f, 10.0f))
igText("Build log: ")
try:
# Set styles of the eventlog window
igPushStyleColor_Vec4(ImGui_Col_FrameBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
igPushStyleColor_Vec4(ImGui_Col_ScrollbarBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
igPushStyleColor_Vec4(ImGui_Col_Border.int32, vec4(0.2f, 0.2f, 0.2f, 1.0f))
igPushStyleVar_Float(ImGui_StyleVar_FrameBorderSize .int32, 1.0f)
let buildLogHeight = 250.0f
let childWindowFlags = ImGuiChildFlags_NavFlattened.int32 or ImGui_ChildFlags_Borders.int32 or ImGui_ChildFlags_AlwaysUseWindowPadding.int32 or ImGuiChildFlags_FrameStyle.int32
if igBeginChild_Str("##Log", vec2(-1.0f, buildLogHeight), childWindowFlags, ImGuiWindowFlags_HorizontalScrollbar.int32):
# Display eventlog items
for item in component.buildLog.items:
component.print(item)
# Auto-scroll to bottom
if igGetScrollY() >= igGetScrollMaxY():
igSetScrollHereY(1.0f)
except IndexDefect:
# CTRL+A crashes when no items are in the eventlog
discard
finally:
igPopStyleColor(3)
igPopStyleVar(1)
igEndChild()
igDummy(vec2(0.0f, 10.0f))
igSeparator()
igDummy(vec2(0.0f, 10.0f))
# Enable "Build" button if at least one module has been selected
igBeginDisabled(component.moduleSelection.items[1].len() == 0)
@@ -126,9 +190,6 @@ proc draw*(component: AgentModalComponent, listeners: seq[UIListener]): AgentBui
spoofStack: component.spoofStack,
modules: modules
)
component.resetModalValues()
igCloseCurrentPopup()
igEndDisabled()
igSameLine(0.0f, textSpacing)

View File

@@ -83,16 +83,16 @@ type
CONFIG_PROFILE = 5'u8
LogType* {.size: sizeof(uint8).} = enum
LOG_INFO = " [INFO] "
LOG_ERROR = " [FAIL] "
LOG_SUCCESS = " [DONE] "
LOG_WARNING = " [WARN] "
LOG_COMMAND = " [>>>>] "
LOG_INFO = "[INFO] "
LOG_ERROR = "[FAIL] "
LOG_SUCCESS = "[DONE] "
LOG_WARNING = "[WARN] "
LOG_COMMAND = "[>>>>] "
LOG_OUTPUT = ""
LOG_INFO_SHORT = " [*] "
LOG_ERROR_SHORT = " [-] "
LOG_SUCCESS_SHORT = " [+] "
LOG_WARNING_SHORT = " [!] "
LOG_INFO_SHORT = "[*] "
LOG_ERROR_SHORT = "[-] "
LOG_SUCCESS_SHORT = "[+] "
LOG_WARNING_SHORT = "[!] "
SleepObfuscationTechnique* = enum
NONE = 0'u8
@@ -264,7 +264,8 @@ type
CLIENT_AGENT_PAYLOAD = 104'u8 # Return agent payload binary
CLIENT_CONSOLE_ITEM = 105'u8 # Add entry to a agent's console
CLIENT_EVENTLOG_ITEM = 106'u8 # Add entry to the eventlog
CLIENT_LOOT = 107'u8 # Download file or screenshot to the operator desktop
CLIENT_BUILDLOG_ITEM = 107'u8 # Add entry to the build log
CLIENT_LOOT = 108'u8 # Download file or screenshot to the operator desktop
Event* = object
eventType*: EventType

View File

@@ -4,6 +4,7 @@ import ../globals
import ../core/logger
import ../db/database
import ../../common/[types, utils, serialize, crypto]
import ../websocket
const PLACEHOLDER = "PLACEHOLDER"
@@ -49,6 +50,8 @@ proc serializeConfiguration(cq: Conquest, listener: Listener, sleep: int, sleepT
wipeKey(aesKey)
cq.info("Profile configuration serialized.")
cq.client.sendBuildlogItem(LOG_INFO_SHORT, "Profile configuration serialized.")
return encMaterial & encData
proc replaceAfterPrefix(content, prefix, value: string): string =
@@ -79,9 +82,11 @@ proc compile(cq: Conquest, placeholderLength: int, modules: uint32): string =
writeFile(configFile, config)
cq.info(fmt"Placeholder created ({placeholder.len()} bytes).")
cq.client.sendBuildlogItem(LOG_INFO_SHORT, fmt"Placeholder created ({placeholder.len()} bytes).")
# Build agent by executing the ./build.sh script on the system.
cq.info("Compiling agent.")
cq.client.sendBuildlogItem(LOG_INFO_SHORT, "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
@@ -97,9 +102,13 @@ proc compile(cq: Conquest, placeholderLength: int, modules: uint32): string =
# Check if the build succeeded or not
if exitCode == 0:
cq.info("Agent payload generated successfully.")
cq.client.sendBuildlogItem(LOG_INFO_SHORT, "Agent payload generated successfully.")
return exeFile
else:
cq.error("Build script exited with code ", $exitCode)
cq.client.sendBuildlogItem(LOG_ERROR_SHORT, "Build script exited with code " & $exitCode)
return ""
except CatchableError as err:
@@ -109,6 +118,7 @@ proc compile(cq: Conquest, placeholderLength: int, modules: uint32): string =
proc patch(cq: Conquest, unpatchedExePath: string, configuration: seq[byte]): seq[byte] =
cq.info("Patching profile configuration into agent.")
cq.client.sendBuildlogItem(LOG_INFO_SHORT, "Patching profile configuration into agent.")
try:
var exeBytes = readFile(unpatchedExePath)
@@ -119,6 +129,7 @@ proc patch(cq: Conquest, unpatchedExePath: string, configuration: seq[byte]): se
raise newException(CatchableError, "Placeholder not found.")
cq.info(fmt"Placeholder found at offset 0x{placeholderPos:08X}.")
cq.client.sendBuildlogItem(LOG_INFO_SHORT, fmt"Placeholder found at offset 0x{placeholderPos:08X}.")
# Patch placeholder bytes
for i, c in Bytes.toString(configuration):
@@ -127,10 +138,12 @@ proc patch(cq: Conquest, unpatchedExePath: string, configuration: seq[byte]): se
writeFile(unpatchedExePath, exeBytes)
cq.success(fmt"Agent payload patched successfully: {unpatchedExePath}.")
cq.client.sendBuildlogItem(LOG_SUCCESS_SHORT, fmt"Agent payload patched successfully: {unpatchedExePath}.")
return string.toBytes(exeBytes)
except CatchableError as err:
cq.error("An error occurred: ", err.msg)
cq.client.sendBuildlogItem(LOG_ERROR_SHORT, "An error occurred: " & err.msg)
return @[]

View File

@@ -1,4 +1,4 @@
import terminal, parsetoml, json, math, base64
import terminal, parsetoml, json, math, base64, times
import strutils, strformat, system, tables
import ./core/[listener, builder]
@@ -100,12 +100,28 @@ proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.
of CloseEvent:
# Set the client instance to nil again to prevent debug error messages
cq.client = nil
var lastCtrlCTime = fromUnix(0)
var ctrlC = 0
proc handleCtrlC() {.noconv.} =
let now = getTime()
if now - lastCtrlCTime > initDuration(seconds = 2):
ctrlC = 0
inc ctrlC
lastCtrlCTime = now
if ctrlC == 1:
echo "\nPress Ctrl+C again to exit."
else:
echo "\nExiting."
quit(0)
proc startServer*(profilePath: string) =
# Ensure that the conquest root directory was passed as a compile-time define
when not defined(CONQUEST_ROOT):
quit(0)
# Handle team server exit
setControlCHook(handleCtrlC)
header()

View File

@@ -116,3 +116,15 @@ proc sendConsoleItem*(client: WsConnection, agentId: string, logType: LogType, m
)
if client != nil:
client.ws.sendEvent(event, client.sessionKey)
proc sendBuildlogItem*(client: WsConnection, logType: LogType, message: string) =
let event = Event(
eventType: CLIENT_BUILDLOG_ITEM,
timestamp: now().toTime().toUnix(),
data: %*{
"logType": cast[uint8](logType),
"message": message
}
)
if client != nil:
client.ws.sendEvent(event, client.sessionKey)