Added 'bof' module for executing object files and fixed handling of optional arguments.

This commit is contained in:
Jakob Friedl
2025-08-29 15:58:26 +02:00
parent 352b8fd8d1
commit 4ceb756cfd
8 changed files with 138 additions and 85 deletions

View File

@@ -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: "<stdarg.h>".} = 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

View File

@@ -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
return uint32.toBytes(uint32(argBytes.len())) & argBytes

View File

@@ -70,6 +70,4 @@ proc main() =
echo "[-] ", err.msg
when isMainModule:
test()
quit(0)
main()

View File

@@ -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

58
src/modules/bof.nim Normal file
View File

@@ -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))

View File

@@ -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]

View File

@@ -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}"

View File

@@ -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