SindriKit 1.4.0: Stealth Execution with COFF Object Loading and Injection

While Reflective PE Loading is the traditional way for executing arbitrary payloads from memory, it is sometimes heavier than necessary. Full PE files carry overhead: headers, diverse sections, and full compilation chains. For post-exploitation, operators often require smaller payloads.

Enter COFF (Common Object File Format) and BOFs (Beacon Object Files). COFF files are unlinked object files, representing code that has been compiled but not yet linked into a full PE. They are smaller and leave a minimal memory footprint.

SindriKit 1.4.0 introduces a complete COFF Object Loading and Injection ecosystem, allowing operators to parse, relocate, and execute BOFs dynamically from memory, either locally or remotely.

Source, architecture docs, and PoCs: SindriKit on GitHub.

Why COFF/BOF?

coffe

When you compile a C program, the compiler generates a .obj file (COFF). The linker then takes one or more .obj files and links them with standard libraries (like the C Runtime) to produce a .exe or .dll (PE).

By stopping at the .obj stage, BOFs offer several advantages:

  1. Size: BOFs do not include the C Runtime (CRT) or standard library bloat. A payload that might be 150KB as a PE can be reduced to 3KB as a BOF.
  2. Stealth: They are smaller, less structured than PEs, and harder to signature.
  3. Flexibility: Since they are unlinked, all external API calls are left as unresolved symbols. The loader is responsible for resolving these symbols at runtime, giving the operator total control over how imports are handled.

The SindriKit COFF Engine

To execute a BOF, SindriKit must perform the job of a linker at runtime. The new snd_ldr_coff_load engine handles this entirely in memory without touching the disk.

Dynamic Layout Assembly

Unlike a PE which has a contiguous SizeOfImage defined in its headers, a COFF object is just a collection of loose sections (.text, .data, .bss). SindriKit dynamically calculates the required memory, allocates a single contiguous block, and maps the sections.

BOF Symbol Resolution

BOFs declare external OS APIs using a specific convention: [ModuleName]$[FunctionName] (e.g., USER32$MessageBoxA). SindriKit’s snd_ldr_coff_resolve_symbols parses these, dynamically loads the target modules using the injected mod_api, and resolves the exact function addresses.

Crucially, for x64 payloads where the target Windows API might be more than 4GB away (beyond a 32-bit relative jump), the engine automatically builds JMP [RIP+0] trampolines to safely bridge the gap.

+-------------------+
| .text (Code)      |
+-------------------+
| .data (Init)      |
+-------------------+
| Symbol Map        |
+-------------------+
| IAT (Resolved)    |
+-------------------+
| x64 Trampolines   | --> JMP [Target API]
+-------------------+
| .bss (Uninit)     |
+-------------------+

Modular Execution vs. Chain Orchestration

SindriKit is designed around dependency injection and state machines. You can choose exactly how much control you want over the loading process.

For maximum ease of use, you can use the high-level chain functions. Executing a BOF locally is as simple as initializing the context and calling the loader and executor:

// 1. Prepare the loader context (dependency injection)
snd_ldr_coff_ctx_t ctx = {0};
ctx.raw_source = &bof_buffer;
ctx.mem_api    = &snd_mem_sys;
ctx.mod_api    = &snd_mod_nt;

// 2. Load the COFF image (Parse -> Allocate -> Resolve -> Relocate)
snd_ldr_coff_load(&ctx);

// 3. Execute the entry point with arguments
snd_ldr_coff_execute_image(&ctx, "go", args, args_len);

For advanced operators who need granular control, you can step through the engine manually:

snd_coff_parse(...);
snd_ldr_coff_allocate_and_copy_sections(&ctx);
// ... intercept and modify sections or permissions here ...
snd_ldr_coff_resolve_symbols(&ctx);
snd_ldr_coff_apply_relocations(&ctx);
snd_ldr_coff_apply_memory_protections(&ctx);

Remote Injection

Because SindriKit enforces strict separation of concerns, adding remote COFF injection (snd_inj_classic_coff) required zero changes to the loader or the injection primitives.

The orchestrator simply leverages the existing local loader to parse, resolve, and apply relocations (adjusted for the remote base address). It then uses the existing shared injection context (snd_inj_ctx_t) to allocate memory in the target process, write the fully baked image and the BOF argument buffer, and spawn a remote thread.

The modular architecture ensures that whether you are loading a PE locally, injecting shellcode remotely, or orchestrating a remote BOF execution, you rely on the exact same primitives.

Conclusion

With version 1.4.0, SindriKit provides a COFF parsing and execution engine. By supporting BOFs, operators can deploy smaller payloads while maintaining the execution control that defines the framework.

*