Minimal C Without CRT
The C Runtime Library is a layer you didn't ask for and can't afford. It bloats your import table, adds detectable patterns to your binary, and links your code to infrastructure you don't control. This chapter shows you how to cut it out completely and write C programs that stand entirely on their own.
What the CRT Is and Why It Costs You
When you write a normal C program and compile it, the compiler and linker
quietly attach a large amount of code you never wrote. This is the
C Runtime Library (CRT) — a startup layer and standard library that handles
things like setting up the heap, initializing global variables, processing
command-line arguments, and providing functions like printf,
malloc, memcpy, and strlen.
On Windows, the CRT ships as a DLL (msvcrt.dll for the old
Microsoft CRT, or vcruntime140.dll + ucrtbase.dll
for the newer Universal CRT). Your compiled binary imports from that DLL.
That import is visible in your PE's import directory, in every security
product that looks at your binary, and in every sandbox report.
OS loads your PE
│
▼
OS jumps to entry point
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CRT STARTUP CODE │
│ (_start or mainCRTStartup — added by the linker) │
│ │
│ 1. Initialize the heap (HeapCreate) │
│ 2. Initialize security cookie (__security_init_cookie) │
│ 3. Parse command line into argc/argv │
│ 4. Run C++ global constructors (.CRT$XCA → .CRT$XCZ) │
│ 5. Set up atexit() handler chain │
│ 6. Call YOUR main() or WinMain() │
│ 7. Run atexit() handlers on return │
│ 8. Run C++ global destructors │
│ 9. Call ExitProcess() │
└─────────────────────────────────────────────────────────────┘
│
▼
Your main() finally runs
For a normal application, this machinery is useful. For an implant or loader, it is overhead you are paying for with no benefit — plus it adds detectable imports and code patterns to your binary before you even write a line.
Without CRT, the OS jumps directly to your code. You are in control from the first instruction. Your binary has no heap initialization, no global constructor machinery, no security cookie setup — and no CRT imports.
What You Give Up (And What You Replace It With)
Cutting the CRT means losing everything it provides. That's a longer list than you might expect, but everything on it has a replacement:
┌────────────────────┬──────────────────────────────────────────┐
│ CRT function │ CRT-free replacement │
├────────────────────┼──────────────────────────────────────────┤
│ malloc / free │ HeapAlloc / HeapFree (GetProcessHeap()) │
│ │ VirtualAlloc / VirtualFree │
├────────────────────┼──────────────────────────────────────────┤
│ memcpy │ Write your own (4 lines of C) or use │
│ memset │ RtlMoveMemory / RtlFillMemory (ntdll) │
│ memcmp │ — resolved via PEB walk, no import │
├────────────────────┼──────────────────────────────────────────┤
│ strlen / strcpy │ Write your own (trivial loops) │
│ strcmp / strcat │ │
├────────────────────┼──────────────────────────────────────────┤
│ printf / sprintf │ Use wsprintfA (user32.dll) for simple │
│ │ formatting, or write your own itoa/hex │
├────────────────────┼──────────────────────────────────────────┤
│ exit() │ ExitProcess() (kernel32.dll) │
├────────────────────┼──────────────────────────────────────────┤
│ C++ new / delete │ HeapAlloc / HeapFree with GetProcessHeap │
├────────────────────┼──────────────────────────────────────────┤
│ C++ exceptions │ Windows SEH (__try/__except) — no CRT │
│ │ needed for SEH, only for C++ EH │
├────────────────────┼──────────────────────────────────────────┤
│ argc / argv │ GetCommandLineA() + manual parsing, or │
│ │ just ignore — most implants don't need │
└────────────────────┴──────────────────────────────────────────┘
In practice, the functions you actually need for a loader or shellcode are so few that you can implement all of them in under 50 lines of C. Most implants need memory allocation, some string operations, and a way to call Win32 APIs. Everything else is noise.
The Entry Point: Taking Control From the OS
When the OS maps your PE into memory and transfers control, it jumps to the
address stored in the PE's Optional Header AddressOfEntryPoint field.
Normally the linker sets this to the CRT startup stub. Without the CRT, you
point it directly at your own function.
There are three ways to tell your linker to use your own entry point:
# MinGW: -e specifies the entry point symbol name
x86_64-w64-mingw32-gcc -nostdlib -nodefaultlibs -e _start \
-o loader.exe loader.c -Wl,-s
REM MSVC: /ENTRY specifies the entry point
cl.exe /NODEFAULTLIB loader.c /link /ENTRY:_start /SUBSYSTEM:CONSOLE
Your entry point function must have a very specific signature — not
int main(int argc, char** argv) but a raw function that the OS
can jump to with a clean call frame:
// The raw Windows entry point
// On x64 Windows: RCX = HINSTANCE, RDX = reserved
// But typically you don't use either — just ignore them
void _start(void) {
// You are now the first code that runs in this process.
// No CRT has initialized anything.
// The stack is valid. Registers are in OS-defined state.
// That is all you are guaranteed.
// ... your code ...
// You MUST call ExitProcess — returning from _start
// causes undefined behavior because there is no return address
ExitProcess(0);
}
In a normal program,
main() returns to the CRT startup code which
then calls ExitProcess(). Without the CRT, there is no return
address on the stack when the OS jumps to your entry point — or rather,
the return address is some OS-internal address that you don't want to return to.
Always end your entry point with an explicit ExitProcess() call,
or an infinite loop if you intend to run forever.
Calling Win32 APIs Without CRT
Here is the key insight that makes CRT-free Windows development work:
Win32 APIs are not part of the CRT. They live in
kernel32.dll, ntdll.dll, user32.dll, etc.
— DLLs that are already loaded by the OS before your entry point runs.
You can call them directly with no CRT involvement at all.
The only question is how to import them. You have two options:
Option A — Static Import Declaration (Simple but Visible)
Declare the functions with __declspec(dllimport) and link
against the import library. The linker adds them to your PE's import directory:
// Explicitly declare what you need — no #include required
__declspec(dllimport) BOOL WINAPI WriteFile(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED);
__declspec(dllimport) HANDLE WINAPI GetStdHandle(DWORD);
__declspec(dllimport) void WINAPI ExitProcess(UINT);
void _start(void) {
HANDLE out = GetStdHandle(-11); // STD_OUTPUT_HANDLE = -11
DWORD written;
WriteFile(out, "hello\n", 6, &written, NULL);
ExitProcess(0);
}
# Link against kernel32 import library
x86_64-w64-mingw32-gcc -nostdlib -nodefaultlibs -e _start \
-o hello.exe hello.c -lkernel32 -Wl,-s
This works and produces a clean binary. The downside: the import directory lists exactly which functions you use, which gives analysts a clean picture of your capabilities. For a loader or shellcode, you typically want to resolve APIs dynamically at runtime (Part 3) so the import table is empty.
Option B — Dynamic Resolution via GetProcAddress (Cleaner)
You can still use GetProcAddress to resolve functions at runtime even when you're CRT-free — GetProcAddress itself is in kernel32, which you import once, then use to find everything else:
// Import only the two bootstrapping functions
// Everything else resolved at runtime
__declspec(dllimport) HMODULE WINAPI GetModuleHandleA(LPCSTR);
__declspec(dllimport) FARPROC WINAPI GetProcAddress(HMODULE, LPCSTR);
__declspec(dllimport) void WINAPI ExitProcess(UINT);
// Define function pointer types for what we need
typedef HANDLE (WINAPI *fnGetStdHandle)(DWORD);
typedef BOOL (WINAPI *fnWriteFile)(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED);
void _start(void) {
HMODULE k32 = GetModuleHandleA("kernel32.dll");
fnGetStdHandle pGetStdHandle = (fnGetStdHandle)
GetProcAddress(k32, "GetStdHandle");
fnWriteFile pWriteFile = (fnWriteFile)
GetProcAddress(k32, "WriteFile");
HANDLE out = pGetStdHandle((DWORD)-11);
DWORD n;
pWriteFile(out, "hello via dynamic resolution\n", 29, &n, NULL);
ExitProcess(0);
}
Now your import table contains only three entries: GetModuleHandleA,
GetProcAddress, and ExitProcess. Everything else
is resolved at runtime with no static visibility. Part 3 takes this one step
further — you'll replace even those three imports with a PEB walk that resolves
everything at runtime with zero imports at all.
Writing Your Own Mini Standard Library
Without the CRT, you lose memcpy, memset,
strlen, and friends. These are trivial to replace. Every
offensive developer ends up writing a small header of utility functions
they carry across projects. Here is a complete minimal implementation:
// utils.h — CRT-free utility functions
// Include this in any CRT-free project
#pragma once
#include <windows.h>
// ── Memory ────────────────────────────────────────────────────────
static inline void* my_memcpy(void* dst, const void* src, SIZE_T n) {
BYTE* d = (BYTE*)dst;
const BYTE* s = (const BYTE*)src;
while (n--) *d++ = *s++;
return dst;
}
static inline void* my_memset(void* dst, int c, SIZE_T n) {
BYTE* d = (BYTE*)dst;
while (n--) *d++ = (BYTE)c;
return dst;
}
static inline int my_memcmp(const void* a, const void* b, SIZE_T n) {
const BYTE* p = (const BYTE*)a;
const BYTE* q = (const BYTE*)b;
while (n--) {
if (*p != *q) return (int)*p - (int)*q;
p++; q++;
}
return 0;
}
// ── Strings (ASCII) ───────────────────────────────────────────────
static inline SIZE_T my_strlen(const char* s) {
const char* p = s;
while (*p) p++;
return (SIZE_T)(p - s);
}
static inline int my_strcmp(const char* a, const char* b) {
while (*a && *a == *b) { a++; b++; }
return (unsigned char)*a - (unsigned char)*b;
}
static inline int my_strncmp(const char* a, const char* b, SIZE_T n) {
while (n && *a && *a == *b) { a++; b++; n--; }
if (!n) return 0;
return (unsigned char)*a - (unsigned char)*b;
}
static inline char* my_strcpy(char* dst, const char* src) {
char* d = dst;
while ((*d++ = *src++));
return dst;
}
// ── Memory allocation via the OS heap ─────────────────────────────
// Call once to get the heap handle, then use for all allocs.
// Avoids calling GetProcessHeap() every allocation.
static HANDLE g_heap = NULL;
static inline void heap_init(void) {
g_heap = GetProcessHeap();
}
static inline void* halloc(SIZE_T size) {
return HeapAlloc(g_heap, HEAP_ZERO_MEMORY, size);
}
static inline void hfree(void* ptr) {
HeapFree(g_heap, 0, ptr);
}
This is everything you need for the first several parts of this book. It fits in one header, has zero dependencies, and produces zero imports. The implementations are deliberately simple — not the fastest possible, but correct and auditable. When performance matters (Part 2 shellcode), you'll use compiler intrinsics or assembly for inner loops.
ntdll.dll exports RtlMoveMemory and
RtlFillMemory which work like memcpy and
memset. You can use them without the CRT by importing from
ntdll. They're a good choice once you're doing PEB walks (Part 3) and
already have ntdll's base address. For now, the pure-C implementations
above avoid any dependency at all.
Using Windows Headers Without the CRT
You can still #include <windows.h> in a CRT-free program.
The Windows SDK headers define types, constants, and function signatures —
they don't pull in the CRT. The confusion arises because many Windows
headers also include standard C headers (<stddef.h>,
<stdint.h>, <string.h>) which GCC
sometimes resolves to CRT-linked implementations.
To prevent the Windows headers from pulling in CRT headers you don't want,
define these macros before including windows.h:
// Tell windows.h to be minimal
#define WIN32_LEAN_AND_MEAN // exclude rarely-used Windows headers
#define VC_EXTRA_LEAN // even more exclusions
#define NOMINMAX // no min/max macros conflicting with templates
#define NOSERVICE // exclude service-related declarations
#define NOMCX // exclude modem configuration
#define NOIME // exclude input method editor
#include <windows.h>
// Now define replacements for the few CRT types you need
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef unsigned long long u64;
// And include your own utilities
#include "utils.h"
Controlling Section Layout
A PE file is divided into sections — .text for code,
.data for initialized globals, .rdata for
read-only data (string literals, constants), and so on. When you write
an implant, the layout of these sections matters for two reasons:
first, because sections with suspicious names or properties get flagged;
second, because the relative positions of sections affect how your
position-independent code (Part 2) addresses its own data.
With CRT, the linker creates its own section structure which you don't control. Without CRT, you can declare exactly what sections exist and what goes in them:
// Placing a variable in a specific section (MinGW/GCC syntax)
__attribute__((section(".mydata")))
static const char config_key[] = { 0x41, 0x42, 0x43 }; // "ABC" (example)
// Placing a function in a specific section
__attribute__((section(".payload")))
void payload_function(void) {
// This function lands in the .payload section
}
// MSVC syntax for section placement
#pragma section(".payload", execute, read)
__declspec(allocate(".payload"))
void __cdecl payload_function(void) { }
For now, you won't need to control sections manually. The default layout
from a clean CRT-free build is fine. This becomes important in Part 7
(Obfuscation) when you want to put your encrypted payload in a custom
section name that doesn't trigger PE scanners looking for .text
entropy anomalies.
Putting It Together: A Complete CRT-Free Program
Here is a full, self-contained CRT-free Windows program that allocates memory, writes a string to stdout, waits for user input, and exits cleanly. No CRT. No imports except the three bootstrapping functions. This is the template you'll build every subsequent project from.
// crtfree.c — complete CRT-free Windows program
// Compile:
// x86_64-w64-mingw32-gcc -Os -nostdlib -nodefaultlibs \
// -fno-ident -fno-asynchronous-unwind-tables \
// -ffunction-sections -fdata-sections \
// -e _start -o crtfree.exe crtfree.c \
// -lkernel32 -Wl,--gc-sections -Wl,-s
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
// ── Bootstrap imports (only these three appear in the import table) ──
__declspec(dllimport) HMODULE WINAPI GetModuleHandleA(LPCSTR);
__declspec(dllimport) FARPROC WINAPI GetProcAddress(HMODULE, LPCSTR);
__declspec(dllimport) void WINAPI ExitProcess(UINT);
// ── Function pointer types ─────────────────────────────────────────
typedef HANDLE (WINAPI *fn_GetStdHandle) (DWORD);
typedef BOOL (WINAPI *fn_WriteConsoleA) (HANDLE, LPCVOID, DWORD, LPDWORD, LPVOID);
typedef BOOL (WINAPI *fn_ReadConsoleA) (HANDLE, LPVOID, DWORD, LPDWORD, LPVOID);
typedef void* (WINAPI *fn_VirtualAlloc) (LPVOID, SIZE_T, DWORD, DWORD);
typedef BOOL (WINAPI *fn_VirtualFree) (LPVOID, SIZE_T, DWORD);
// ── Simple string length (no CRT) ─────────────────────────────────
static SIZE_T slen(const char* s) {
const char* p = s;
while (*p) p++;
return (SIZE_T)(p - s);
}
// ── Entry point ────────────────────────────────────────────────────
void _start(void) {
HMODULE k32 = GetModuleHandleA("kernel32.dll");
fn_GetStdHandle pGetStd = (fn_GetStdHandle) GetProcAddress(k32, "GetStdHandle");
fn_WriteConsoleA pWrite = (fn_WriteConsoleA)GetProcAddress(k32, "WriteConsoleA");
fn_ReadConsoleA pRead = (fn_ReadConsoleA) GetProcAddress(k32, "ReadConsoleA");
fn_VirtualAlloc pVAlloc = (fn_VirtualAlloc) GetProcAddress(k32, "VirtualAlloc");
fn_VirtualFree pVFree = (fn_VirtualFree) GetProcAddress(k32, "VirtualFree");
HANDLE hOut = pGetStd((DWORD)-11); // STD_OUTPUT_HANDLE
HANDLE hIn = pGetStd((DWORD)-10); // STD_INPUT_HANDLE
DWORD n = 0;
const char* msg = "CRT-free Windows program running.\nPress Enter: ";
pWrite(hOut, msg, (DWORD)slen(msg), &n, NULL);
// Allocate a buffer on the virtual address space — no heap init needed
char* buf = (char*)pVAlloc(NULL, 256, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (buf) {
pRead(hIn, buf, 255, &n, NULL);
const char* reply = "You typed something. Goodbye.\n";
pWrite(hOut, reply, (DWORD)slen(reply), &n, NULL);
pVFree(buf, 0, MEM_RELEASE);
}
ExitProcess(0);
}
Compile this and open it in PE-bear. You will see exactly three imports:
GetModuleHandleA, GetProcAddress, and
ExitProcess — all from kernel32.dll. No CRT.
No MSVCRT. Total binary size: under 4KB.
IMPORTS
└── kernel32.dll
├── GetModuleHandleA
├── GetProcAddress
└── ExitProcess
SECTIONS
├── .text (code — your _start function and slen)
└── .rdata (read-only data — the string literals)
DEBUG DIRECTORY: empty
EXPORT DIRECTORY: empty
RESOURCE DIR: empty
File size: ~3.5 KB
What Breaks Without CRT (And How to Diagnose It)
When you first go CRT-free, you will hit a few common linker errors. Here is what they mean and how to fix each one:
Error: undefined reference to `__chkstk_ms'
──────────────────────────────────────────────────────────────────
Cause: GCC inserts __chkstk_ms calls when a function allocates
more than 4KB on the stack. This symbol lives in the CRT.
Fix: Add -mno-stack-arg-probe to CFLAGS.
Also: avoid large stack allocations in CRT-free code.
Use VirtualAlloc for large buffers instead.
Error: undefined reference to `__main'
──────────────────────────────────────────────────────────────────
Cause: GCC generates a call to __main from your entry point to
run C++ global constructors. __main is a CRT function.
Fix: Use -e _start and name your entry point _start, not main.
Or add -fno-threadsafe-statics and avoid global C++ objects.
Error: undefined reference to `__security_check_cookie'
──────────────────────────────────────────────────────────────────
Cause: MSVC inserts stack cookie checks (/GS) that call this
function from the CRT.
Fix: Add /GS- to MSVC flags.
Error: undefined reference to `__acrt_iob_func' or `_errno'
──────────────────────────────────────────────────────────────────
Cause: A header you included (often <stdio.h> or <string.h>)
pulled in CRT declarations, and the linker found references
to CRT internals.
Fix: Don't include <stdio.h>, <string.h>, <stdlib.h>, or other
standard C headers. Use your own utils.h instead.
Add WIN32_LEAN_AND_MEAN to reduce windows.h bloat.
Error: LNK1561: entry point must be defined
──────────────────────────────────────────────────────────────────
Cause: The linker can't find the entry point you declared.
Fix: Check that your /ENTRY:_start flag matches the actual
function name in your source. C++ mangles names — use
extern "C" void _start() if compiling as C++.
Managing Global State Without CRT Initialization
One thing the CRT startup code does is run C++ global constructors — code
that initializes global objects before main() runs. Without
the CRT, none of that happens. If you declare a global variable that
requires runtime initialization (a C++ object with a constructor, a
pointer initialized by calling a function), it will not be initialized
when your entry point runs.
For CRT-free code, the rule is simple: only use global variables that are zero-initialized or have compile-time constant values. For everything else, initialize explicitly at the start of your entry point:
// WRONG: relies on constructor running before entry point
HANDLE g_heap = GetProcessHeap(); // GetProcessHeap() called at global init time
// — this does NOT happen without CRT
// RIGHT: zero-initialize the global, init in entry point
HANDLE g_heap = NULL;
void _start(void) {
g_heap = GetProcessHeap(); // explicitly called from entry point
// ... rest of your code
ExitProcess(0);
}
BSS-section globals (zero-initialized) are fine — the OS's PE loader zeroes the BSS section before calling your entry point. Globals initialized with integer or pointer constants are also fine — their values are baked into the binary. Only runtime-expression initialization fails.
Questions & Answers
Can I include <windows.h> but still skip the CRT?
Yes, and you should. windows.h is a header file — it
defines types, constants, and function signatures, but it doesn't link
any library by itself. The CRT comes from library files (msvcrt.lib,
libmsvcrt.a, etc.) that the linker links unless you say
-nodefaultlibs. So you can freely use windows.h
for all its type definitions and API declarations while completely excluding
the CRT from your final binary. The one thing to watch is that some
windows.h sub-headers include standard C headers which can
introduce CRT symbols — which is why WIN32_LEAN_AND_MEAN matters.
If I call HeapAlloc without CRT, does the heap exist?
Yes. The process heap exists before your entry point runs — the Windows
loader creates it as part of process initialization. GetProcessHeap()
returns a handle to it and is safe to call from the very first instruction
of your entry point. The CRT's _start calls
HeapCreate to make an additional private heap for
CRT's own use. You don't need that secondary heap — the process heap from
GetProcessHeap() is sufficient for everything.
Does this mean I can't use C++ at all in CRT-free code?
You can use most C++ features. Classes, templates, function overloading,
inline functions, constexpr — all fine. What fails is anything
that requires CRT support at runtime: exception handling with
try/catch (requires unwinding tables and CRT unwind helpers),
std::cout or any <iostream> objects (CRT-backed),
dynamic_cast with RTTI (depends on CRT type info), and global C++ objects
with non-trivial constructors (require the constructor runner in CRT startup).
The practical rule: use C-style code with C++ niceties (templates, type safety)
but avoid the parts of C++ that require a runtime to support them.
Why does my binary still have an import for ExitProcess even when I don't call it?
The MinGW linker adds an ExitProcess import automatically
when it generates the PE's startup code, even with -nostdlib.
This is because the process must terminate somehow, and the linker
is conservative. You can suppress this by providing a completely custom
linker script, but it's not worth the effort — a single
ExitProcess import from kernel32.dll is
completely normal. Every legitimate Windows program has it.
The flag -Wl,--no-insert-timestamp removes the timestamp
from PE headers if reproducible builds matter to you, but you can't easily
remove the ExitProcess import without extra work. Don't fight this one.
Is VirtualAlloc always the right choice over HeapAlloc?
It depends on what you're allocating. VirtualAlloc works at
page granularity (minimum 4KB, always a multiple of 4KB) and is the only
way to allocate memory with specific protection flags like PAGE_EXECUTE_READWRITE
— which you'll need constantly in Part 2 for shellcode testing.
HeapAlloc is more flexible for small arbitrary-size allocations
(a struct here, a buffer there) and uses the process heap which is already
allocated. In practice: use HeapAlloc via GetProcessHeap()
for general data allocations, and VirtualAlloc when you need
specific protection flags or very large allocations.