Skip to main content

windows_erg/mitigation/
mod.rs

1//! Process mitigation policies.
2//!
3//! This module provides ergonomic helpers for applying and querying process
4//! mitigation policies.
5//!
6//! # Important limitations
7//! - `SetProcessMitigationPolicy` only applies to the current process.
8//! - Querying mitigations supports current and external processes (when allowed).
9
10use std::borrow::Cow;
11
12use windows::Win32::Foundation::HANDLE;
13use windows::Win32::System::SystemServices::{
14    PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY, PROCESS_MITIGATION_CHILD_PROCESS_POLICY,
15    PROCESS_MITIGATION_DYNAMIC_CODE_POLICY, PROCESS_MITIGATION_IMAGE_LOAD_POLICY,
16    PROCESS_MITIGATION_PAYLOAD_RESTRICTION_POLICY,
17};
18use windows::Win32::System::Threading::{
19    GetCurrentProcess, GetCurrentProcessId, GetProcessMitigationPolicy, OpenProcess,
20    PROCESS_MITIGATION_POLICY, PROCESS_QUERY_LIMITED_INFORMATION, ProcessChildProcessPolicy,
21    ProcessDynamicCodePolicy, ProcessImageLoadPolicy, ProcessPayloadRestrictionPolicy,
22    ProcessSignaturePolicy, SetProcessMitigationPolicy,
23};
24
25use crate::error::{AccessDeniedError, Error, MitigationError, MitigationOperationError, Result};
26use crate::types::ProcessId;
27use crate::utils::OwnedHandle;
28
29const BINARY_SIGNED_MICROSOFT_SIGNED_ONLY: u32 = 0x1;
30
31const DYNAMIC_CODE_PROHIBIT: u32 = 0x1;
32
33const IMAGE_LOAD_NO_REMOTE: u32 = 0b01;
34const IMAGE_LOAD_PREFER_SYSTEM32_IMAGES: u32 = 0b0100;
35
36const PAYLOAD_RESTRICTION_ENABLE_EXPORT_ADDRESS_FILTER: u32 = 0b0001;
37const PAYLOAD_RESTRICTION_ENABLE_EXPORT_ADDRESS_FILTER_PLUS: u32 = 0b000100;
38const PAYLOAD_RESTRICTION_ENABLE_IMPORT_ADDRESS_FILTER: u32 = 0b00010000;
39const PAYLOAD_RESTRICTION_ENABLE_ROP_STACK_PIVOT: u32 = 0b0001000000;
40const PAYLOAD_RESTRICTION_ENABLE_ROP_CALLER_CHECK: u32 = 0b000100000000;
41const PAYLOAD_RESTRICTION_ENABLE_ROP_SIM_EXEC: u32 = 0b010000000000;
42
43const PAYLOAD_RESTRICTION_MASK: u32 = PAYLOAD_RESTRICTION_ENABLE_EXPORT_ADDRESS_FILTER
44    | PAYLOAD_RESTRICTION_ENABLE_EXPORT_ADDRESS_FILTER_PLUS
45    | PAYLOAD_RESTRICTION_ENABLE_IMPORT_ADDRESS_FILTER
46    | PAYLOAD_RESTRICTION_ENABLE_ROP_STACK_PIVOT
47    | PAYLOAD_RESTRICTION_ENABLE_ROP_CALLER_CHECK
48    | PAYLOAD_RESTRICTION_ENABLE_ROP_SIM_EXEC;
49
50const CHILD_RESTRICTION_NO_PROCESS_CREATION: u32 = 0b001;
51const ERROR_INVALID_PARAMETER_HRESULT: i32 = -2147024809;
52
53/// Supported process mitigations.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum ProcessMitigation {
56    /// Restrict binaries to Microsoft-signed images only.
57    MicrosoftSignedOnly,
58    /// Prevent loading images from remote locations.
59    BlockRemoteImages,
60    /// Prefer loading images from System32.
61    PreferSystem32Images,
62    /// Block dynamic code generation (ACG).
63    DisableDynamicCode,
64    /// Enable payload restrictions (EAF, IAF, ROP checks).
65    RestrictPayload,
66    /// Prevent this process from creating child processes.
67    BlockChildProcessCreation,
68}
69
70impl ProcessMitigation {
71    fn policy_name(self) -> &'static str {
72        match self {
73            ProcessMitigation::MicrosoftSignedOnly => "signature",
74            ProcessMitigation::BlockRemoteImages => "image_load_no_remote",
75            ProcessMitigation::PreferSystem32Images => "image_load_prefer_system32",
76            ProcessMitigation::DisableDynamicCode => "dynamic_code",
77            ProcessMitigation::RestrictPayload => "payload_restriction",
78            ProcessMitigation::BlockChildProcessCreation => "child_process",
79        }
80    }
81}
82
83/// Query result for supported process mitigations.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct ProcessMitigationStatus {
86    /// Process ID that was queried.
87    pub process_id: ProcessId,
88    /// Whether Microsoft-signed-only policy is enabled.
89    pub microsoft_signed_only: bool,
90    /// Whether remote image loading is blocked.
91    pub block_remote_images: bool,
92    /// Whether System32 images are preferred.
93    pub prefer_system32_images: bool,
94    /// Whether dynamic code generation is blocked.
95    pub disable_dynamic_code: bool,
96    /// Whether payload restrictions are active.
97    pub restrict_payload: bool,
98    /// Whether child process creation is blocked.
99    pub block_child_process_creation: bool,
100}
101
102/// Builder for mitigation application.
103#[derive(Debug, Default, Clone)]
104pub struct MitigationPlan {
105    mitigations: Vec<ProcessMitigation>,
106}
107
108impl MitigationPlan {
109    /// Create an empty mitigation plan.
110    pub fn new() -> Self {
111        Self {
112            mitigations: Vec::with_capacity(8),
113        }
114    }
115
116    /// Enable one mitigation in this plan.
117    pub fn enable(mut self, mitigation: ProcessMitigation) -> Self {
118        if !self.mitigations.contains(&mitigation) {
119            self.mitigations.push(mitigation);
120        }
121        self
122    }
123
124    /// Apply all configured mitigations to the current process.
125    pub fn apply_to_current(&self) -> Result<()> {
126        let has_signature = self
127            .mitigations
128            .contains(&ProcessMitigation::MicrosoftSignedOnly);
129        let has_block_remote = self
130            .mitigations
131            .contains(&ProcessMitigation::BlockRemoteImages);
132        let has_prefer_system32 = self
133            .mitigations
134            .contains(&ProcessMitigation::PreferSystem32Images);
135        let has_dynamic_code = self
136            .mitigations
137            .contains(&ProcessMitigation::DisableDynamicCode);
138        let has_payload = self
139            .mitigations
140            .contains(&ProcessMitigation::RestrictPayload);
141        let has_child_block = self
142            .mitigations
143            .contains(&ProcessMitigation::BlockChildProcessCreation);
144
145        if has_signature {
146            apply_single_mitigation(ProcessMitigation::MicrosoftSignedOnly)?;
147        }
148
149        if has_block_remote || has_prefer_system32 {
150            apply_image_load_mitigation(has_block_remote, has_prefer_system32)?;
151        }
152
153        if has_dynamic_code {
154            apply_single_mitigation(ProcessMitigation::DisableDynamicCode)?;
155        }
156
157        if has_payload {
158            apply_single_mitigation(ProcessMitigation::RestrictPayload)?;
159        }
160
161        if has_child_block {
162            apply_single_mitigation(ProcessMitigation::BlockChildProcessCreation)?;
163        }
164
165        Ok(())
166    }
167
168    /// Emit compile-time linker directives for enabled mitigations.
169    ///
170    /// This method prints `cargo:rustc-link-arg` directives to stdout, intended for use
171    /// in a build.rs script. Each linker directive enables binary-level protections on
172    /// the compiled executable.
173    ///
174    /// # Supported Mitigations
175    ///
176    /// Only mitigations with direct binary-level equivalents emit directives:
177    /// - `MicrosoftSignedOnly` → `/DEPENDENTLOADFLAG:0x800` + `/INTEGRITYCHECK`
178    /// - `DisableDynamicCode` → `/guard:cf` (Control Flow Guard)
179    /// - `RestrictPayload` → `/HIGHENTROPYVA` (High Entropy ASLR)
180    ///
181    /// The following are runtime-only and produce no compile-time output:
182    /// - `BlockRemoteImages`, `PreferSystem32Images`, `BlockChildProcessCreation`
183    ///
184    /// # Example (in build.rs)
185    ///
186    /// ```no_run
187    /// use windows_erg::mitigation::{MitigationPlan, ProcessMitigation};
188    ///
189    /// let plan = MitigationPlan::new()
190    ///     .enable(ProcessMitigation::DisableDynamicCode)
191    ///     .enable(ProcessMitigation::MicrosoftSignedOnly);
192    ///
193    /// plan.emit_compile_time();
194    /// ```
195    pub fn emit_compile_time(&self) {
196        self.emit_compile_time_with_compat(false);
197    }
198
199    /// Emit compile-time linker directives with optional CET Shadow Stack support.
200    ///
201    /// Similar to [`emit_compile_time`], but optionally adds `/CETCOMPAT` for
202    /// hardware-enforced stack protection (Control-flow Enforcement Technology).
203    /// Requires Windows 11+ and compatible CPU.
204    ///
205    /// # Arguments
206    ///
207    /// * `enable_compat` - If `true`, additionally emits `/CETCOMPAT`
208    ///
209    /// # Example (in build.rs)
210    ///
211    /// ```no_run
212    /// use windows_erg::mitigation::{MitigationPlan, ProcessMitigation};
213    ///
214    /// let plan = MitigationPlan::new()
215    ///     .enable(ProcessMitigation::DisableDynamicCode);
216    ///
217    /// plan.emit_compile_time_with_compat(true);
218    /// ```
219    pub fn emit_compile_time_with_compat(&self, enable_compat: bool) {
220        for mitigation in &self.mitigations {
221            match mitigation {
222                ProcessMitigation::MicrosoftSignedOnly => {
223                    println!("cargo:rustc-link-arg=/DEPENDENTLOADFLAG:0x800");
224                    println!("cargo:rustc-link-arg=/INTEGRITYCHECK");
225                }
226                ProcessMitigation::DisableDynamicCode => {
227                    println!("cargo:rustc-link-arg=/guard:cf");
228                }
229                ProcessMitigation::RestrictPayload => {
230                    println!("cargo:rustc-link-arg=/HIGHENTROPYVA");
231                }
232                // Runtime-only mitigations: no compile-time equivalent
233                ProcessMitigation::BlockRemoteImages
234                | ProcessMitigation::PreferSystem32Images
235                | ProcessMitigation::BlockChildProcessCreation => {}
236            }
237        }
238
239        if enable_compat {
240            println!("cargo:rustc-link-arg=/CETCOMPAT");
241        }
242    }
243}
244
245/// Query mitigation status for the current process.
246pub fn query_current() -> Result<ProcessMitigationStatus> {
247    query_process(ProcessId::new(unsafe { GetCurrentProcessId() }))
248}
249
250/// Query mitigation status for a specific process.
251pub fn query_process(process_id: ProcessId) -> Result<ProcessMitigationStatus> {
252    let process = open_query_process_handle(process_id)?;
253    query_from_handle(process_id, process.raw())
254}
255
256fn apply_single_mitigation(mitigation: ProcessMitigation) -> Result<()> {
257    match mitigation {
258        ProcessMitigation::MicrosoftSignedOnly => {
259            let mut policy = PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY::default();
260            policy.Anonymous.Anonymous._bitfield = BINARY_SIGNED_MICROSOFT_SIGNED_ONLY;
261            set_policy(
262                ProcessSignaturePolicy,
263                &policy as *const _ as *const _,
264                std::mem::size_of_val(&policy),
265                mitigation.policy_name(),
266            )
267        }
268        ProcessMitigation::BlockRemoteImages => apply_image_load_mitigation(true, false),
269        ProcessMitigation::PreferSystem32Images => apply_image_load_mitigation(false, true),
270        ProcessMitigation::DisableDynamicCode => {
271            let mut policy = PROCESS_MITIGATION_DYNAMIC_CODE_POLICY::default();
272            policy.Anonymous.Anonymous._bitfield = DYNAMIC_CODE_PROHIBIT;
273            set_policy(
274                ProcessDynamicCodePolicy,
275                &policy as *const _ as *const _,
276                std::mem::size_of_val(&policy),
277                mitigation.policy_name(),
278            )
279        }
280        ProcessMitigation::RestrictPayload => apply_payload_mitigation(),
281        ProcessMitigation::BlockChildProcessCreation => {
282            let mut policy = PROCESS_MITIGATION_CHILD_PROCESS_POLICY::default();
283            policy.Anonymous.Anonymous._bitfield = CHILD_RESTRICTION_NO_PROCESS_CREATION;
284            set_policy(
285                ProcessChildProcessPolicy,
286                &policy as *const _ as *const _,
287                std::mem::size_of_val(&policy),
288                mitigation.policy_name(),
289            )
290        }
291    }
292}
293
294fn apply_image_load_mitigation(
295    block_remote_images: bool,
296    prefer_system32_images: bool,
297) -> Result<()> {
298    let mut policy = PROCESS_MITIGATION_IMAGE_LOAD_POLICY::default();
299    let mut flags = 0u32;
300    if block_remote_images {
301        flags |= IMAGE_LOAD_NO_REMOTE;
302    }
303    if prefer_system32_images {
304        flags |= IMAGE_LOAD_PREFER_SYSTEM32_IMAGES;
305    }
306
307    policy.Anonymous.Anonymous._bitfield = flags;
308    set_policy(
309        ProcessImageLoadPolicy,
310        &policy as *const _ as *const _,
311        std::mem::size_of_val(&policy),
312        "image_load",
313    )
314}
315
316fn apply_payload_mitigation() -> Result<()> {
317    let candidate_masks = [
318        PAYLOAD_RESTRICTION_MASK,
319        PAYLOAD_RESTRICTION_MASK & !PAYLOAD_RESTRICTION_ENABLE_ROP_SIM_EXEC,
320        PAYLOAD_RESTRICTION_ENABLE_EXPORT_ADDRESS_FILTER
321            | PAYLOAD_RESTRICTION_ENABLE_EXPORT_ADDRESS_FILTER_PLUS
322            | PAYLOAD_RESTRICTION_ENABLE_IMPORT_ADDRESS_FILTER
323            | PAYLOAD_RESTRICTION_ENABLE_ROP_STACK_PIVOT
324            | PAYLOAD_RESTRICTION_ENABLE_ROP_CALLER_CHECK,
325        PAYLOAD_RESTRICTION_ENABLE_EXPORT_ADDRESS_FILTER
326            | PAYLOAD_RESTRICTION_ENABLE_IMPORT_ADDRESS_FILTER
327            | PAYLOAD_RESTRICTION_ENABLE_ROP_STACK_PIVOT,
328        PAYLOAD_RESTRICTION_ENABLE_EXPORT_ADDRESS_FILTER,
329    ];
330
331    let mut last_error: Option<windows::core::Error> = None;
332
333    for mask in candidate_masks {
334        let mut policy = PROCESS_MITIGATION_PAYLOAD_RESTRICTION_POLICY::default();
335        policy.Anonymous.Anonymous._bitfield = mask;
336
337        match set_policy_raw(
338            ProcessPayloadRestrictionPolicy,
339            &policy as *const _ as *const _,
340            std::mem::size_of_val(&policy),
341        ) {
342            Ok(()) => return Ok(()),
343            Err(err) => {
344                if err.code().0 == ERROR_INVALID_PARAMETER_HRESULT {
345                    last_error = Some(err);
346                    continue;
347                }
348                return Err(map_windows_apply_error(err, "payload_restriction"));
349            }
350        }
351    }
352
353    Err(map_windows_apply_error(
354        last_error.unwrap_or_else(windows::core::Error::from_win32),
355        "payload_restriction",
356    ))
357}
358
359fn set_policy(
360    policy: PROCESS_MITIGATION_POLICY,
361    value: *const core::ffi::c_void,
362    len: usize,
363    policy_name: &'static str,
364) -> Result<()> {
365    set_policy_raw(policy, value, len).map_err(|e| map_windows_apply_error(e, policy_name))
366}
367
368fn set_policy_raw(
369    policy: PROCESS_MITIGATION_POLICY,
370    value: *const core::ffi::c_void,
371    len: usize,
372) -> windows::core::Result<()> {
373    unsafe { SetProcessMitigationPolicy(policy, value, len) }
374}
375
376fn query_from_handle(process_id: ProcessId, handle: HANDLE) -> Result<ProcessMitigationStatus> {
377    unsafe {
378        let mut signature = PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY::default();
379        get_policy(
380            handle,
381            ProcessSignaturePolicy,
382            &mut signature as *mut _ as *mut _,
383            std::mem::size_of_val(&signature),
384            "signature",
385            process_id,
386        )?;
387
388        let mut image_load = PROCESS_MITIGATION_IMAGE_LOAD_POLICY::default();
389        get_policy(
390            handle,
391            ProcessImageLoadPolicy,
392            &mut image_load as *mut _ as *mut _,
393            std::mem::size_of_val(&image_load),
394            "image_load",
395            process_id,
396        )?;
397
398        let mut dynamic_code = PROCESS_MITIGATION_DYNAMIC_CODE_POLICY::default();
399        get_policy(
400            handle,
401            ProcessDynamicCodePolicy,
402            &mut dynamic_code as *mut _ as *mut _,
403            std::mem::size_of_val(&dynamic_code),
404            "dynamic_code",
405            process_id,
406        )?;
407
408        let mut payload = PROCESS_MITIGATION_PAYLOAD_RESTRICTION_POLICY::default();
409        get_policy(
410            handle,
411            ProcessPayloadRestrictionPolicy,
412            &mut payload as *mut _ as *mut _,
413            std::mem::size_of_val(&payload),
414            "payload_restriction",
415            process_id,
416        )?;
417
418        let mut child = PROCESS_MITIGATION_CHILD_PROCESS_POLICY::default();
419        get_policy(
420            handle,
421            ProcessChildProcessPolicy,
422            &mut child as *mut _ as *mut _,
423            std::mem::size_of_val(&child),
424            "child_process",
425            process_id,
426        )?;
427
428        let signature_flags = signature.Anonymous.Anonymous._bitfield;
429        let image_load_flags = image_load.Anonymous.Anonymous._bitfield;
430        let dynamic_flags = dynamic_code.Anonymous.Anonymous._bitfield;
431        let payload_flags = payload.Anonymous.Anonymous._bitfield;
432        let child_flags = child.Anonymous.Anonymous._bitfield;
433
434        Ok(ProcessMitigationStatus {
435            process_id,
436            microsoft_signed_only: (signature_flags & BINARY_SIGNED_MICROSOFT_SIGNED_ONLY) != 0,
437            block_remote_images: (image_load_flags & IMAGE_LOAD_NO_REMOTE) != 0,
438            prefer_system32_images: (image_load_flags & IMAGE_LOAD_PREFER_SYSTEM32_IMAGES) != 0,
439            disable_dynamic_code: (dynamic_flags & DYNAMIC_CODE_PROHIBIT) != 0,
440            restrict_payload: (payload_flags & PAYLOAD_RESTRICTION_MASK) != 0,
441            block_child_process_creation: (child_flags & CHILD_RESTRICTION_NO_PROCESS_CREATION)
442                != 0,
443        })
444    }
445}
446
447fn get_policy(
448    handle: HANDLE,
449    policy: PROCESS_MITIGATION_POLICY,
450    out_value: *mut core::ffi::c_void,
451    len: usize,
452    policy_name: &'static str,
453    process_id: ProcessId,
454) -> Result<()> {
455    unsafe { GetProcessMitigationPolicy(handle, policy, out_value, len) }
456        .map_err(|e| map_windows_query_error(e, policy_name, process_id))
457}
458
459fn map_windows_apply_error(err: windows::core::Error, policy_name: &'static str) -> Error {
460    let code = err.code().0;
461    if code == 5 {
462        return Error::AccessDenied(AccessDeniedError::with_reason(
463            "current process",
464            "set mitigation policy",
465            Cow::Owned(format!("{}: access denied", policy_name)),
466        ));
467    }
468
469    Error::Mitigation(MitigationError::ApplyFailed(
470        MitigationOperationError::new("apply", policy_name)
471            .with_reason(Cow::Owned(err.to_string()))
472            .with_code(code),
473    ))
474}
475
476fn map_windows_query_error(
477    err: windows::core::Error,
478    policy_name: &'static str,
479    process_id: ProcessId,
480) -> Error {
481    let code = err.code().0;
482    if code == 5 {
483        return Error::AccessDenied(AccessDeniedError::with_reason(
484            Cow::Owned(format!("process {}", process_id.as_u32())),
485            "query mitigation policy",
486            Cow::Owned(format!("{}: access denied", policy_name)),
487        ));
488    }
489
490    Error::Mitigation(MitigationError::QueryFailed(
491        MitigationOperationError::new("query", policy_name)
492            .with_process_id(process_id.as_u32())
493            .with_reason(Cow::Owned(err.to_string()))
494            .with_code(code),
495    ))
496}
497
498fn open_query_process_handle(process_id: ProcessId) -> Result<OwnedHandle> {
499    let current = ProcessId::new(unsafe { GetCurrentProcessId() });
500    if process_id == current {
501        return Ok(OwnedHandle::borrowed(unsafe { GetCurrentProcess() }));
502    }
503
504    let handle = unsafe {
505        OpenProcess(
506            PROCESS_QUERY_LIMITED_INFORMATION,
507            false,
508            process_id.as_u32(),
509        )
510    }
511    .map_err(|e| map_windows_query_error(e, "open_process", process_id))?;
512
513    Ok(OwnedHandle::new(handle))
514}
515
516#[cfg(test)]
517mod tests {
518    use super::{MitigationPlan, ProcessMitigation};
519
520    #[test]
521    fn mitigation_plan_enable_deduplicates_values() {
522        let plan = MitigationPlan::new()
523            .enable(ProcessMitigation::DisableDynamicCode)
524            .enable(ProcessMitigation::DisableDynamicCode)
525            .enable(ProcessMitigation::MicrosoftSignedOnly);
526
527        let debug = format!("{plan:?}");
528        let count = debug.matches("DisableDynamicCode").count();
529        assert_eq!(count, 1);
530        assert!(debug.contains("MicrosoftSignedOnly"));
531    }
532}