From ab48bc5795a582fd409459ca3bc3b004d34c3f4d Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:10:46 +0200 Subject: [PATCH] Added build log to client UI. --- src/agent/nim.cfg | 2 +- src/client/main.nim | 14 +++- src/client/views/console.nim | 2 +- src/client/views/modals/generatePayload.nim | 71 +++++++++++++++++++-- src/common/types.nim | 21 +++--- src/server/core/builder.nim | 13 ++++ src/server/main.nim | 24 +++++-- src/server/websocket.nim | 12 ++++ 8 files changed, 137 insertions(+), 22 deletions(-) diff --git a/src/agent/nim.cfg b/src/agent/nim.cfg index f5b2a19..4124d00 100644 --- a/src/agent/nim.cfg +++ b/src/agent/nim.cfg @@ -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" \ No newline at end of file diff --git a/src/client/main.nim b/src/client/main.nim index dea94e0..90a6685 100644 --- a/src/client/main.nim +++ b/src/client/main.nim @@ -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 diff --git a/src/client/views/console.nim b/src/client/views/console.nim index b0028d3..5f9b395 100644 --- a/src/client/views/console.nim +++ b/src/client/views/console.nim @@ -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 diff --git a/src/client/views/modals/generatePayload.nim b/src/client/views/modals/generatePayload.nim index c95e749..aa0314e 100644 --- a/src/client/views/modals/generatePayload.nim +++ b/src/client/views/modals/generatePayload.nim @@ -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) diff --git a/src/common/types.nim b/src/common/types.nim index a5d78fa..9ee33a1 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -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 diff --git a/src/server/core/builder.nim b/src/server/core/builder.nim index 7ecb590..f390196 100644 --- a/src/server/core/builder.nim +++ b/src/server/core/builder.nim @@ -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 @[] diff --git a/src/server/main.nim b/src/server/main.nim index f9cdb8e..7574a5f 100644 --- a/src/server/main.nim +++ b/src/server/main.nim @@ -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() diff --git a/src/server/websocket.nim b/src/server/websocket.nim index 1144a6f..1b90841 100644 --- a/src/server/websocket.nim +++ b/src/server/websocket.nim @@ -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)