CRT vs NoCRT: How the C Runtime Helps Defenders Catch Injected DLLs
When an attacker injects a DLL into a process, one of the first decisions they make - whether they realize it or not - is whether to link the C Runtime Library (CRT). That decision leaves distinct forensic traces that defenders can use to detect the injection.
1. What the CRT Actually Does
When you compile a DLL with Visual Studio using the default settings, the C Runtime Library is linked in. The CRT is not just printf and malloc - it’s a significant initialization framework that runs before your code.
When a CRT-linked DLL is loaded, this happens before DllMain executes:
- Security cookie initialization (
__security_init_cookie) - generates a random stack canary value - CRT heap initialization - sets up the CRT’s internal heap
- Thread-local storage initialization - initializes TLS slots
- Atexit/onexit registration - prepares cleanup handlers
- Floating-point initialization - configures FPU state
- Global C++ constructor calls - runs static object constructors (
_initterm)
The actual entry point of a CRT-linked DLL is not DllMain - it is _DllMainCRTStartup, which does all of the above and then calls your DllMain.
The Security Cookie
The security cookie (/GS flag, enabled by default) is the most visible CRT artifact. The function __security_init_cookie generates a random value at DLL load time and stores it in __security_cookie. Every function that uses stack buffers places this value on the stack and validates it before returning.
The initialization is easy to spot in a disassembler:
_DllMainCRTStartup:
call __security_init_cookie ; ← distinctive CRT artifact
jmp dllmain_dispatch
This single call is one of the most reliable indicators that a DLL was compiled with the CRT.
2. CRT-Linked DLLs: What Defenders See
A DLL compiled with the CRT has a recognizable fingerprint. Here’s what to look for.
Import Table
CRT-linked DLLs import from CRT libraries. The specific imports depend on the linking mode:
Dynamic CRT (/MD):
vcruntime140.dllucrtbase.dll(orapi-ms-win-crt-*.dllon newer Windows)- Possibly
msvcp140.dllfor C++ standard library
Static CRT (/MT):
- No CRT DLL imports (everything is compiled into the binary)
- But the code patterns are still present in the
.textsection
Entry Point Pattern
The entry point follows a predictable pattern:
; _DllMainCRTStartup
push rbp
mov rbp, rsp
sub rsp, 0x20
call __security_init_cookie
; ... CRT initialization ...
call dllmain_dispatch
; ... CRT cleanup ...
The call to __security_init_cookie near the entry point is a strong CRT indicator. This function reads RDTSC, GetCurrentProcessId, GetCurrentThreadId, GetSystemTimeAsFileTime, and QueryPerformanceCounter to generate entropy for the cookie. Those API calls or their patterns are detectable.
.rdata and .data Sections
CRT-linked DLLs contain specific global variables:
__security_cookie- the canary value (in.dataor.rdata)_onexit_table- atexit cleanup handlers__acrt_iob_funcreferences for stdio- CRT error messages as strings (“runtime error”, “assertion failed”)
Section Layout
A typical CRT DLL has well-structured sections:
.text - code (substantial, includes CRT runtime code)
.rdata - read-only data, vtables, CRT strings
.data - writable data, security cookie, global state
.pdata - exception handling unwind data
.rsrc - resources (optional)
.reloc - relocation table
The .pdata section (exception unwind information) is almost always present in CRT DLLs because the CRT uses structured exception handling.
3. Why Attackers Avoid the CRT
Sophisticated attackers compile DLLs without the CRT for several reasons.
A minimal NoCRT DLL can be 4-8 KB. A CRT-linked DLL starts at 50-100 KB. Smaller files are easier to inject, less likely to trigger size-based heuristics, and faster to write into remote process memory.
The CRT also pulls in dozens of API imports, and each import is a potential detection point. A NoCRT DLL can operate with just a handful of functions from ntdll.dll or kernel32.dll.
CRT initialization calls multiple API functions that EDR products monitor. Skipping it means the DLL’s entry point runs directly - less telemetry generated. The CRT also brings code the attacker doesn’t need, with its own behavior (heap allocations, TLS operations, exception handlers) that creates noise.
And many detection rules are tuned to CRT-compiled binaries because that’s what most software produces. A NoCRT binary doesn’t match those patterns - which is itself a signal, as we’ll see.
How NoCRT DLLs Are Built
// NoCRT entry point - no _DllMainCRTStartup wrapper
BOOL WINAPI _DllMainCRTStartup(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
if (fdwReason == DLL_PROCESS_ATTACH) {
// attacker code runs directly here
}
return TRUE;
}
Compiled with:
/NODEFAULTLIB- no CRT libraries/ENTRY:_DllMainCRTStartup- custom entry point/GS-- no security cookie (requires CRT)- No
printf,malloc,new- only direct Win32 or NT API calls
4. NoCRT DLLs: What Defenders See
The absence of CRT artifacts is just as distinctive as their presence.
Missing Security Cookie
No __security_init_cookie call at the entry point. No __security_cookie global. No __GSHandlerCheck exception handlers. For a DLL that does anything non-trivial, this is unusual.
A DLL with a .text section larger than 4 KB but no security cookie initialization is suspicious. Legitimate developers almost never disable /GS because it’s the default and has negligible performance cost.
Minimal Import Table
A NoCRT DLL often imports only from kernel32.dll or ntdll.dll, with a handful of functions:
kernel32.dll:
VirtualAlloc
VirtualProtect
CreateThread
LoadLibraryA
GetProcAddress
Or even more minimal, using only ntdll.dll native API:
ntdll.dll:
NtAllocateVirtualMemory
NtProtectVirtualMemory
NtCreateThreadEx
LdrLoadDll
A DLL that imports exclusively from ntdll.dll with no CRT imports is highly unusual for legitimate software. Most legitimate DLLs use the Win32 API layer.
Tiny File Size
A NoCRT DLL doing real work can be 4-15 KB. Legitimate DLLs with business logic are almost always larger. The distribution of DLL sizes in a normal process is skewed toward larger files.
Flag unsigned DLLs under 20 KB loaded into processes where the typical module size is much larger.
Flat Entry Point
NoCRT DLLs have a simple entry point that goes directly to attacker logic:
_DllMainCRTStartup:
cmp edx, 1 ; DLL_PROCESS_ATTACH
jne short return_true
; ... immediately does attacker work ...
return_true:
mov eax, 1
ret
Compare this with a CRT entry point that has initialization calls, exception handling setup, and a structured dispatch to DllMain. The difference is visible in static analysis.
Missing .pdata Section
NoCRT DLLs compiled without exception handling often lack a .pdata section entirely. On x64 Windows, the .pdata section contains unwind information for structured exception handling. Its absence means the DLL has no SEH support.
A x64 DLL without .pdata is unusual. Not definitive on its own, but combined with other NoCRT indicators it strengthens the signal.
5. The Signature Gap
This is the most straightforward detection opportunity.
Legitimate CRT-linked DLLs are almost always signed. Microsoft, Adobe, Google, game studios - every major software vendor signs their DLLs. The CRT itself (vcruntime140.dll, ucrtbase.dll) is Microsoft-signed.
An unsigned DLL that uses the CRT is suspicious because:
- If the developer was professional enough to use the CRT (standard build process), they would typically also sign their binaries
- Legitimate unsigned DLLs exist (open-source plugins, internal tools) but they are a small population
- An injected DLL is, by definition, not part of the original application - it will not be signed by the application vendor
An unsigned DLL loaded into a process where all other DLLs are signed is a strong anomaly signal. If it also uses the CRT, it was compiled with standard tooling but not through a standard release process.
Why CRT Makes Unsigned DLLs Easier to Catch
Here’s the thing: __security_init_cookie calls 4 external functions to gather entropy:
GetSystemTimeAsFileTimeQueryPerformanceCounterGetCurrentProcessIdGetCurrentThreadId
Every one of these can be hooked by a security product. When the hook fires, the defender inspects the return address on the call stack. That return address points back into the calling module - the injected DLL. Walking the call stack reveals the full chain:
GetSystemTimeAsFileTime ← hooked, EDR gets control
← __security_init_cookie ← return address is inside injected DLL
← _DllMainCRTStartup ← CRT entry point
← LdrpCallInitRoutine ← ntdll loader
The return address lands in a memory region. The defender checks: does this region belong to a signed, known module? If the return address resolves to an unsigned image, or to memory that is not backed by any loaded image at all, it is a strong injection indicator.
This is not a problem for legitimate DLLs. A signed module from a known vendor calling these same functions during normal CRT initialization will pass the return address check - the address resolves cleanly to a signed image with a valid certificate chain. The detection specifically targets the gap between “uses standard CRT tooling” and “did not go through a standard signing and distribution process.”
Beyond the security cookie, the CRT generates additional telemetry:
- CRT heap initialization calls
HeapCreateorRtlCreateHeap- same return address analysis applies - TLS callbacks are registered and executed - monitored by ETW
- If dynamic CRT (
/MD), loading the DLL triggers loads ofvcruntime140.dllanducrtbase.dll- module load events that EDR monitors
An attacker using a NoCRT DLL avoids all of these hook trigger points - but as covered in section 4, the absence of these patterns is also detectable through structural analysis.
6. Building a Detection Matrix
Combining these signals into a scoring system:
| Signal | CRT DLL (suspicious) | NoCRT DLL (suspicious) | Weight |
|---|---|---|---|
| Unsigned | Strong indicator | Strong indicator | High |
| No security cookie | N/A | Present | Medium |
| Minimal imports | Unlikely | Likely | Medium |
| Small file size (<20 KB) | Unlikely | Likely | Medium |
No .pdata section |
Unlikely | Likely | Low |
| ntdll-only imports | Very unlikely | Possible | High |
| Not in application manifest | Strong indicator | Strong indicator | High |
| Loaded after process init | Moderate indicator | Moderate indicator | Medium |
| No version info resource | Moderate indicator | Likely | Low |
Detection Algorithm
score = 0
if dll.is_unsigned:
score += 30
if dll.loaded_after_process_init:
score += 15
if not dll.has_security_cookie and dll.text_size > 0x1000:
score += 20 # NoCRT indicator
if dll.import_count < 5:
score += 15
if dll.file_size < 0x5000: # 20 KB
score += 10
if dll.imports_only_ntdll:
score += 25
if not dll.has_pdata_section:
score += 5
if not dll.has_version_info:
score += 5
if score >= 50:
alert("suspicious DLL injection detected")
This is a simplified example. Production EDR systems use more sophisticated scoring with machine learning and behavioral context. But the core signals are the same.
7. ETW and Kernel-Level Detection
Module Load Events
ETW provides IMAGE_LOAD events whenever a DLL is loaded. Each event includes:
- Image file path
- Image base address
- Image size
- Process ID
- Signing level and signature status
Monitoring these events for unsigned images loaded after process initialization is the foundation of DLL injection detection.
Thread Creation Events
DLL injection typically involves creating a remote thread (via CreateRemoteThread, NtCreateThreadEx, or APC injection). ETW THREAD_START events capture:
- Start address - does it point into a known module?
- Thread creation time relative to process creation
- Calling process (for remote thread creation)
A thread starting at an address that does not belong to any known signed module is a strong injection indicator.
Combining Telemetry
The strongest detection comes from correlating events:
IMAGE_LOADfor an unsigned DLL → timestamp T1THREAD_STARTwith start address in that DLL → timestamp T2- T2 shortly after T1 → high confidence injection
If the DLL also matches NoCRT patterns (small, minimal imports, no security cookie), the confidence increases further.
8. Practical Recommendations for Defenders
For EDR Engineers
Don’t just check signatures - also look for DLLs signed with revoked or untrusted certificates. Build a baseline of expected DLLs per application; any new DLL that appears in a stable application is worth investigating. Detect CRT absence, not just CRT presence - a DLL with no CRT artifacts doing complex work is more suspicious than one with the CRT. And watch for unexpected vcruntime140.dll or ucrtbase.dll loads, which signal something new was injected with CRT linkage.
For Malware Analysts
Check the entry point first. CRT vs NoCRT is immediately visible from the entry point structure. Examine import table density - NoCRT malware often resolves APIs dynamically after load, so look for GetProcAddress chains or manual export table walking. And don’t forget about statically linked CRT (/MT): no CRT imports show up, but the code is still there in the binary.
For Blue Teams
Sysmon Event ID 7 (Image Loaded) with signature status filtering catches unsigned DLLs immediately. WDAC or AppLocker can block unsigned DLLs from loading entirely in high-security environments. Module load auditing with baseline comparison detects any new DLL in monitored processes.
Conclusion
The CRT is not a security feature - it is a development convenience. But its presence or absence creates a distinctive forensic fingerprint that defenders can use.
An unsigned CRT-linked DLL is easy to catch because the CRT generates initialization telemetry, imports from known CRT libraries, and follows a recognizable structure. Attackers who avoid the CRT to reduce this footprint create a different but equally detectable pattern: minimal imports, no security cookie, tiny file size, and missing standard sections.
For defenders, the lesson is to detect in both directions:
- CRT present + unsigned = amateur or careless injection, catch on telemetry and signature
- CRT absent + unusual characteristics = deliberate evasion, catch on structural anomalies
Neither choice is invisible to a well-instrumented environment.