Chapter 02

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.

What happens between the OS and your code — with CRT
  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 functions and their CRT-free replacements
  ┌────────────────────┬──────────────────────────────────────────┐
  │ 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);
}
Never return from your entry point
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.

Why not just use RtlMoveMemory and RtlFillMemory?
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.

What PE-bear shows for this binary
  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:

Common CRT-free linker errors and fixes
  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.