I’ve spent a fair bit of time analysing Windows binaries in security assessments, and one of the first things I check is whether the binary has the right protections compiled in. A surprising number of applications, even from reputable vendors, ship without one or more of these enabled. In this post, I’ll walk through each binary protection, explain what it actually does, how I’ve seen it matter in practice, and how to verify it’s enabled on any binary you’re working with.
The protections covered here are: ASLR, DEP, SafeSEH, Strong Naming, Authenticode, Control Flow Guard, and High Entropy VA. I’ll also note which apply to x86 (32-bit) vs x64 systems, because the difference matters more than most tutorials acknowledge.
1. ASLR (Address Space Layout Randomization)
ASLR randomizes where the OS loads memory segments (the stack, heap, and libraries) each time a process starts. The goal is simple: if an attacker can’t predict where things are in memory, exploiting a buffer overflow becomes dramatically harder.
Without ASLR, memory layouts are static and predictable. I’ve seen older internal tools compiled without it, and when you run checksec or PESecurity against them, the addresses are identical across reboots. That’s a gift to any attacker doing ROP chain development.
On Windows, ASLR is configured at the system level and also declared in the PE header of the binary via the IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE flag. You don’t write code to enable it; you compile with the right linker flag and ensure the OS policy is on.
# Enable ASLR via Windows Exploit Protection (PowerShell)
Set-ProcessMitigation -System -Enable ASLR
x86 vs x64: ASLR works on both, but x64 gets far more entropy. On x86, the limited address space (4 GB) means attackers can sometimes brute-force the randomization. On x64, that’s not practical.
2. DEP (Data Execution Prevention)
DEP marks memory regions (particularly the stack and heap) as non-executable. Even if an attacker successfully injects shellcode into one of these regions, the processor will refuse to execute it and throw an access violation instead.
In practice, DEP broke a huge class of classic shellcode injection attacks. Attackers responded by developing Return-Oriented Programming (ROP), but DEP combined with CFG (below) closes much of that gap.
To ensure DEP is declared in your binary, compile with /NXCOMPAT in Visual Studio. This sets the IMAGE_DLLCHARACTERISTICS_NX_COMPAT flag in the PE header.
# Check if DEP is enabled system-wide
Get-ProcessMitigation -System | Select-Object -ExpandProperty DEP
x86 vs x64: DEP is hardware-enforced on both architectures via the NX/XD bit in the CPU. On x64 it’s always available. On older x86 hardware without PAE or NX bit support, software DEP may fall back to a weaker enforcement mode.
3. SafeSEH (Safe Structured Exception Handling)
Windows uses Structured Exception Handling (SEH) to manage runtime exceptions. SEH overflow attacks work by overwriting the exception handler pointer on the stack with a value pointing to attacker-controlled code. SafeSEH prevents this by maintaining a compile-time list of valid exception handlers. At runtime, Windows validates any handler pointer against this list before executing it.
If an attacker overwrites the SEH pointer and tries to trigger it, Windows checks the pointer against the registered table. If it’s not there, execution is blocked. This is one of those protections that’s easy to overlook on x86 builds. I’ve seen plenty of legacy DLLs compiled without /SAFESEH that were sitting in otherwise-hardened applications.
# Link with SafeSEH (Visual Studio)
cl /GS my_program.c /link /SAFESEH
x86 vs x64: SafeSEH only applies to x86 (32-bit). On x64, Windows uses a table-based exception handling mechanism that doesn’t have the same SEH overwrite vulnerability, so /SAFESEH is irrelevant there.
4. Strong Naming
Strong naming is specific to .NET assemblies. It attaches a public/private key cryptographic signature to the assembly, giving it a globally unique identity. The runtime verifies this signature before loading the assembly, which prevents DLL substitution attacks. In that attack pattern, a malicious assembly with the same filename gets dropped into a folder and loaded instead of the legitimate one.
I’ve seen this matter most in plugin-based .NET applications where third-party assemblies are loaded from a shared directory. Without strong naming, swapping a DLL is trivial. With it, any modification to the assembly invalidates the signature and the runtime refuses to load it.
# Generate a strong name key pair
sn -k MyKey.snk
# Sign the assembly with it (in .csproj)
<AssemblyOriginatorKeyFile>MyKey.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
# Verify the signature
sn -v MyAssembly.dll
x86 vs x64: Strong naming applies to both 32-bit and 64-bit .NET assemblies equally. It’s a CLR-level feature, not architecture-dependent.
5. Authenticode
Authenticode is Microsoft’s code signing standard for native executables and DLLs. It uses a digital certificate (typically from a trusted CA like DigiCert or Sectigo) to sign the binary. Windows verifies this signature when the file is downloaded, installed, or executed. If the signature is missing or invalid, Windows warns users or blocks execution outright depending on policy.
From a security standpoint, Authenticode doesn’t prevent exploitation of a signed binary. What it does mean is that if someone tampers with your binary after signing, the signature breaks and Windows flags it. It’s also the mechanism behind SmartScreen reputation: unsigned or newly-signed executables get the “Windows protected your PC” warning.
# Sign a binary with signtool (requires a code-signing certificate)
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /a MyApp.exe
# Verify the signature
signtool verify /pa MyApp.exe
x86 vs x64: Authenticode works identically for both 32-bit and 64-bit PE files. The signing and verification process is the same regardless of architecture.
6. Control Flow Guard (CFG)
CFG is Microsoft’s mitigation against control-flow hijacking. This covers attacks where an attacker corrupts a function pointer or virtual table pointer to redirect execution somewhere unintended. CFG works by having the compiler build a bitmap of all valid indirect call targets at compile time. At runtime, before any indirect call is made, the CPU checks the target against this bitmap. If it’s not a valid target, execution is terminated.
This matters most against ROP-based exploits that have already bypassed DEP. Even if an attacker can redirect execution, CFG limits where they can jump to. Only legitimate function entry points are valid targets, which severely constrains what an attacker can chain together.
# Compile with CFG enabled
cl /GS /guard:cf my_program.c
# Verify CFG is set in the binary (PowerShell)
$bytes = [System.IO.File]::ReadAllBytes("C:\path\to\my_program.exe")
# Or use dumpbin:
dumpbin /headers my_program.exe | findstr "Guard"
x86 vs x64: CFG is supported on both, but it’s enabled by default in Visual Studio for x64. For x86 builds, you need to explicitly pass /guard:cf. It doesn’t get turned on automatically even in Release builds.
7. High Entropy VA (Virtual Address Space)
High Entropy VA is an extension of ASLR specific to 64-bit processes. It tells the OS to use a much larger range of addresses when randomizing, up to 1TB of virtual address space rather than the limited range standard ASLR uses. This makes brute-forcing the randomization completely impractical, even with multiple attempts.
On x64 binaries, this is declared with the IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA flag in the PE header. It requires both ASLR and the binary to be linked as a 64-bit image. There is no equivalent on x86 because 32-bit address spaces simply don’t have enough range to benefit.
# Link with High Entropy VA (Visual Studio linker flag)
link /HIGHENTROPYVA my_program.obj
# Or check it in dumpbin
dumpbin /headers my_program.exe | findstr "HIGH ENTROPY"
x86 vs x64: High Entropy VA is x64 only. It does not apply to 32-bit binaries at all.
How to Audit Any Binary: PESecurity
Rather than checking each protection manually, I use the NetSPI PESecurity PowerShell module to audit binaries in bulk. It checks all of the above protections in one pass and outputs a clean table. Here’s what a real audit run looks like against a mix of system and third-party DLLs:
# Import and run PESecurity
Import-Module .\Get-PESecurity.psm1
# Audit a single binary
Get-PESecurity -File C:\Windows\System32\ntdll.dll
# Audit an entire directory
Get-PESecurity -Directory C:\Program Files\SomeApp\ -Recursive
Sample output from a mixed audit. Notice how the legacy third-party DLL fails most checks while system binaries pass:
FileName ASLR DEP SafeSEH StrongNaming Authenticode CFG HighEntropyVA
-------------------- ----- ----- ------- ------------ ------------ ----- -------------
ntdll.dll True True N/A N/A True True True
kernel32.dll True True N/A N/A True True True
MyApp.exe True True True N/A True True True
legacy_plugin.dll False False False N/A False False False
MyDotNetLib.dll True True N/A True True True True
The legacy_plugin.dll row is the kind of finding that shows up regularly in third-party software assessments. All protections are off, likely because the DLL was compiled years ago and never updated. That single DLL sitting inside an otherwise-hardened application is enough to undermine several of the protections the host process has enabled.
Note: SafeSEH and StrongNaming show N/A for x64 native binaries and non-.NET binaries respectively. That’s expected, not a failure.
Quick Reference: Protection Support by Architecture
| Protection | x86 (32-bit) | x64 (64-bit) | Applies To |
|---|---|---|---|
| ASLR | ✅ Supported (low entropy) | ✅ Fully effective | Native binaries |
| DEP | ✅ Supported | ✅ Fully effective | Native binaries |
| SafeSEH | ✅ Manual flag required | ❌ Not applicable | Native x86 only |
| Strong Naming | ✅ Supported | ✅ Supported | .NET assemblies only |
| Authenticode | ✅ Supported | ✅ Supported | All PE files |
| CFG | ✅ Manual flag required | ✅ Default in VS | Native binaries |
| High Entropy VA | ❌ Not applicable | ✅ Fully effective | x64 native only |