← blog

ScoffLdr: a 2026 take on COFF / BOF loading

2026-07-24

I still reach for CoffeeLdr when I need a BOF to run outside Cobalt Strike. TrustedSec's COFFLoader and C5pider's CoffeeLdr solved the real problem years ago: take a .o, relocate it, wire Beacon* / imports, call go(). That foundation is solid. I am not rewriting reloc math for fun.

What I wanted next was to modernize everything around that success. Classic loaders were built in a world where private commit, RWX (or close enough), and sometimes a LoadLibrary + .text overwrite that stays on a module in the PEB were an acceptable trade. Memory scanners have moved on. "The BOF printed hello" is still necessary. It is no longer the whole story.

So I wrote ScoffLdr. Same job as CoffeeLdr. Same ABI for existing object files. A residency model updated for how processes get inspected in 2026. The name is just the COFF joke without using coffee again.

Quick reminder: what a BOF loader even is

A BOF is not a PE. It is a Windows COFF object: sections, symbols, relocs, an entry that is usually called go. The loader has to place those sections somewhere, resolve externals like BeaconPrintf or KERNEL32$VirtualAlloc, fix in the relocations, call the entry, and ideally clean up afterward. CoffeeLdr does all of that. ScoffLdr does too. The interesting fork is only underneath: residency and teardown.

What I actually changed

I am not claiming a stealth loader. I am claiming a posture that holds up better against the boring checks everyone already publishes.

No RWX. The image is built writable, then hardened. Code ends up PAGE_EXECUTE_READ. Data stays RW or RO. If a section would have been writable and executable, write gets dropped on purpose. I would rather break a weird BOF than ship another RWX blob.

Image-backed by default. --private still exists for labs and fallbacks, but the default path opens a System32 DLL, maps it with CreateFileMapping(..., SEC_IMAGE), stomps only what we need in .text, relocates in place so the bases are honest, flips to RX, runs go(), wipes, then UnmapViewOfFile. The mapping is gone. You do not keep a LoadLibrary'd souvenir in the LDR with SharedOriginal=0 pages hanging around after the BOF returns.

That last sentence is easy to miss. Copying the original .text bytes back into a module that is still loaded does not restore SharedOriginal. Unmapping the view does remove those dirty pages from the process. Mid-execution the stomped pages are still COW / non-original. That is fine for an ephemeral BOF: get in, run, leave.

A proper GOT instead of a FunMap soup. Externals land in a pointer table. In private mode that table is locked read-only after fill. The old "one RWX arena with everything in it" pattern is exactly what memory tooling likes to trip on.

Memory ops you can swap. Lab builds use VirtualAlloc / VirtualProtect. --nt flips alloc / protect / free to the ntdll Nt* calls. Symbol resolve can stay boring Win32. The useful bit for an implant later is the ops table (SCOFF_OPS): inject syscalls without touching the reloc engine again.

The pipeline, without the marketing

COFF bytes
  -> parse / size
  -> allocate residency (SEC_IMAGE .text  or  private RW)
  -> map sections + GOT
  -> resolve symbols, apply relocs
  -> harden protections (never RWX)
  -> call entry (go)
  -> wipe
  -> unmap or VirtualFree

Stomp opens System32\<cover.dll> (I default to xpsservices.dll), maps SEC_IMAGE, and uses that executable section as the image base. If the cover is missing or .text is too small, it falls back to private. That is deliberate. A lab box without the DLL should still run the hello BOF, not die on ceremony.

Does it actually run?

Yes. Same tiny hello object, three postures, same BeaconPrintf line:

ScoffLdr running hello.o with --stomp, --stomp --nt, and --private --nt

SEC_IMAGE, SEC_IMAGE + Nt*, private + Nt*. If reloc or GOT wiring is wrong, you do not get a polite string. You get a crash or unresolved.

ScoffLdr.exe --stomp hello.o
ScoffLdr.exe --stomp --nt hello.o
ScoffLdr.exe --private --nt hello.o

That was the first bar. Print the line three ways, then look at memory.

Looking at residency live

Printing hello is necessary. It is not sufficient. I wanted to see the memory map while the image was still mapped, before teardown erased the evidence. So the harness grew two boring flags: -v prints the bases, --pause <ms> sleeps after harden and before go(). System Informer (or Process Hacker) gets a fair window.

ScoffLdr.exe --stomp -v --pause 100000 hello.o
ScoffLdr.exe --private -v --pause 100000 hello.o

On stomp, search the printed sec-view / image base. You should land on an Image region backed by xpsservices.dll, with the stomped span in RX:

System Informer during SEC_IMAGE stomp pause: Image region for xpsservices.dll

Pause during --stomp. Same address the loader printed. Type Image, path xpsservices.dll, executable span RX.

Same BOF, --private. Search the printed image base. Now it is a Private 16 KB allocation: GOT page R, code page RX, still no RWX:

System Informer during private pause: Private 16 KB with R then RX

Pause during --private. Private commit, no DLL backing, no RWX blob. Same object file, different silhouette.

That pair is the whole argument. One .o, two shapes, neither left as the 2021 private RWX shell everyone already hunts.

Compared to CoffeeLdr

CoffeeLdr remains the reference, and the credit section is not ceremonial. ScoffLdr is built on that lineage: same BOF workflow, with the residency side brought up to date for 2026.

Both loaders run a BOF. That part is shared. CoffeeLdr-era designs often used private commit or a LoadLibrary stomp, which made sense when they shipped. ScoffLdr keeps the same goal and updates the default path: map with SEC_IMAGE, stomp only what it needs in .text, then unmap when it is done so the mapping does not have to linger in the LDR.

Protections follow the same modernization. Older loaders frequently left RWX, or close enough. ScoffLdr builds writable, then hardens to RX / RW / RO, and avoids leaving RWX on purpose. After go(), wipe the bytes, then drop the mapping (or free the private region). The interesting silhouette does not need to stick around.

The memory API is a smaller upgrade. Virtual* is fine in a lab. The ops table exists so an implant can move alloc / protect / free to Nt* or syscalls without rewriting reloc code again. Beacon inject and spawn stay stubs here, same as every honest lab loader. The host owns that surface.

None of this is clever reloc math. Anyone who has read TrustedSec already knows REL32 and IAT-style slots. The deliberate change is where the bytes sit, and how cleanly they can disappear after the BOF returns.

What this does not fix

Be honest about the window. While the BOF is running, stomped pages are still modified image pages. A scanner that catches you mid-execution still has something to look at. Stack spoofing is out of scope for the loader: calls out of the BOF look like calls from whatever residency you chose. Syscalls inside the BOF are a BOF-author problem (or a richer Beacon API that exposes your implant's stubs). ScoffLdr only makes those stubs easy to host later. And it is x64 only. I did not pretend otherwise.

Closing

Running the BOF is still the first milestone. The next one is leaving a process whose memory story matches 2026 expectations: no RWX souvenir, no dirty LDR leftover if you can avoid it, teardown that actually drops the mapping.

CoffeeLdr and COFFLoader remain the foundation. ScoffLdr is the residency modernization I wanted before wiring the same idea into a PIC agent.

Credit

This would not exist without CoffeeLdr from C5pider and TrustedSec's COFFLoader.