# tl;dr This article presents an easy/lazy way of integrating BOFs into your nim projects without implementing a full-scale COFF loader. This is a way I've first seen implemented in `sliver` C2. # COFF files COFF files (`.o/.obj`) are widely used for in-memory execution (in Cobalt Strike, for example), as they provide a more structured primitive for extensions than raw shellcode and allow the COFF loader to implement necessary primitives (like `BeaconPrintf` or WinAPI resolution) in any way desired. # loading COFF Dynamically loading object files is mostly about resolving and applying relocations to the `.obj` (while loading missing libraries). The best resource for creating your own and understaning how COFF loading works, imo, is 0xpats blog -- https://0xpat.github.io/Malware_development_part_8/ I, however, am way too lazy to rewrite from scratch what's already on GitHub -- the [trustedsec/COFFLoader](https://github.com/trustedsec/COFFLoader) repository, and namely, the `sliver` [fork](https://github.com/sliverarmory/COFFLoader/) of it, as the developers have done the work of creating a DLL export out of `RunCOFF` for us. Instead of properly reimplementing COFF loading, why not just load a DLL that will do everything for us? It's way easier to detect, but WCYD. Thus, the approach becomes very simple: ```j load COFFLoader reflectively in-memory -> GetProcAddress(LoadAndRun) -> build argument array -> LoadAndRun(COFF, callback) -> wait for callback and process output ``` # COFF arguments Beacon Object File argument format is very simple: ```j len(args)|len(arg1)| ... arg1 ... | len(arg2) | ... 4 bytes 4 bytes len(arg1) bytes 4 bytes ``` Despite argument length being provided, it is still expected that C strings are null-terminated. Lengths are little-endian, and the overall message length is never used by LoadAndRun, as it is supplied as a separate argument. If you do not want to generate them yourself, use trustedsec's `beacon_generate.py` and hex-decode the result. # PoC code Also available on [GitHub](https://github.com/zimnyaa/nim-lazy-bof). The PoC provided loads and runs `whoami.o` without arguments. The COFFLoader DLL provided on GitHub also prints beacon argument parser state, which I've used for debugging, but it is interchangeable with the DLL from `sliverarmory` releases. ```nim import winim import std/dynlib import system import ptr_math import memlib # wstring -> string proc lpwstrc(bytes: array[MAX_PATH, WCHAR]): string = result = newString(bytes.len) copyMem(result[0].addr, bytes[0].unsafeAddr, bytes.len) # BOF output callback proc callback(data: cstring, status: int): int {.gcsafe, stdcall.} = echo "[!] CALLBACK CALLED" echo data return # in-memory loading the COFFLoader dll const coffloader = staticReadDll("COFFLoader.x64.dll") proc loadcoff (data: LPVOID, length: int, callback: proc (data: cstring, status: int) : int {.stdcall, gcsafe.}) : int {.cdecl, memlib: coffloader, importc: "LoadAndRun".} # constant entrypoint arg var entrypoint_arg: array[11, byte] = [byte 0xff, 0xff, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x67, 0x6f, 0x00] # len(c"go"), c"go" # COFF arguments (empty) var coff_arg: array[4, byte] = [byte 0x00, 0x00, 0x00, 0x00] # COFF file var coff_file = readFile("whoami.o") echo "[+] Starting with ", GetLastError() echo "[+] loadcoff address -> ", toHex(cast[int](loadcoff)) echo "[+] callback address -> ", toHex(cast[int](callback)) var loader_args = VirtualAlloc(nil, 4 + len(coff_file) + len(entrypoint_arg) + len(coff_arg), MEM_COMMIT, PAGE_READWRITE) echo "[!] VirtualAlloc ", GetLastError(), " to ", toHex(cast[int](loader_args)) # "go" entrypoint copyMem(loader_args, addr entrypoint_arg, len(entrypoint_arg)) # file size var coffsize = len(coff_file) copyMem(loader_args + len(entrypoint_arg), &coff_size, 4) # file bytes copyMem(loader_args + len(entrypoint_arg) + 4, &coff_file[0], len(coff_file)) # args copyMem(loader_args + len(entrypoint_arg) + len(coff_file) + 4, addr coff_arg, len(coff_arg)) echo "[!] memory copied" echo "[!] args will be: ( ", toHex(cast[int](loader_args)),", ", toHex(cast[int](len(coff_file)+len(entrypoint_arg)+len(coff_arg)+4)),", ", toHex(cast[int](callback)), " )" discard loadcoff(loader_args, len(coff_file)+len(entrypoint_arg)+len(coff_arg)+4, callback) ``` > ![[Pasted image 20220419171238.png]] > running the loader