summaryrefslogtreecommitdiffstats
path: root/toolkit/xre/detect_win32k_conflicts/src
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/xre/detect_win32k_conflicts/src')
-rw-r--r--toolkit/xre/detect_win32k_conflicts/src/detect.rs165
-rw-r--r--toolkit/xre/detect_win32k_conflicts/src/error.rs36
-rw-r--r--toolkit/xre/detect_win32k_conflicts/src/ffi.rs57
-rw-r--r--toolkit/xre/detect_win32k_conflicts/src/lib.rs18
-rw-r--r--toolkit/xre/detect_win32k_conflicts/src/registry.rs360
5 files changed, 636 insertions, 0 deletions
diff --git a/toolkit/xre/detect_win32k_conflicts/src/detect.rs b/toolkit/xre/detect_win32k_conflicts/src/detect.rs
new file mode 100644
index 0000000000..e21c80e157
--- /dev/null
+++ b/toolkit/xre/detect_win32k_conflicts/src/detect.rs
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! This module contains the actual functions that explore the registry for the status
+//! of the conflicting mitigations.
+
+use super::error::DetectConflictError;
+use super::registry::{RegKey, RegValue};
+
+use std::ffi::OsStr;
+
+/// Subkey that the Windows Exploit Protection status is located in
+const EXPLOIT_PROTECTION_SUBKEY: &str =
+ "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options";
+
+/// Name of the value within that subkey that contains the exploit protection status
+const MITIGATION_VALUE_NAME: &str = "MitigationOptions";
+
+/// The bit number within the value with the StackPivot status
+const STACK_PIVOT_BIT: usize = 80;
+
+/// The bit number within the value with the CallerCheck status
+const CALLER_CHECK_BIT: usize = 84;
+
+/// The bit number within the value with the SimExec status
+const SIM_EXEC_BIT: usize = 88;
+
+/// Printable, FFI-safe structure containing the status of the conflicting mitigations
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, PartialEq)]
+pub struct ConflictingMitigationStatus {
+ /// The status of the CallerCheck feature. 0 = disabled, 1 = enabled
+ caller_check: bool,
+ /// The status of the SimExec feature. 0 = disabled, 1 = enabled
+ sim_exec: bool,
+ /// The status of the StackPivot feature. 0 = disabled, 1 = enabled
+ stack_pivot: bool,
+}
+
+impl std::fmt::Display for ConflictingMitigationStatus {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ use std::fmt::Write;
+ write_status(f, "CallerCheck", self.caller_check)?;
+ f.write_char(' ')?;
+ write_status(f, "SimExec", self.sim_exec)?;
+ f.write_char(' ')?;
+ write_status(f, "StackPivot", self.stack_pivot)
+ }
+}
+
+/// Search the registry for win32k-conflicting mitigations for the given process
+///
+/// `process_name` is the name of the process that will be checked for conflicting mitigations
+/// It uses the same case-insensitive algorithm that the Windows Registry uses itself to detect
+/// whether a process needs mitigations or not
+///
+/// (This will usually be `firefox.exe`)
+///
+/// # Caveat
+///
+/// Windows allows an arbitrary number of "filters", which are subkeys of the main key
+/// that also have a `MitigationOptions` value. They are used to allow filtering by path.
+///
+/// For simplicity, we "flatten" the path filters when we figure this out, and so the
+/// returned value will be the logical "OR" of all mitigations enabled with any filter
+///
+/// So if there are two entries for "C:\Firefox\firefox.exe" with mitigations `A` and `B` enabled
+/// and another entry for "C:\Program Files\Mozilla Firefox\firefox.exe" with mitigation `C`
+/// enabled, we will return that `A`, `B`, and `C` are all enabled
+pub fn get_conflicting_mitigations(
+ process_name: impl AsRef<OsStr>,
+) -> Result<ConflictingMitigationStatus, DetectConflictError> {
+ let process_name = process_name.as_ref();
+
+ let key = RegKey::root_local_machine()
+ .try_open_subkey(EXPLOIT_PROTECTION_SUBKEY)?
+ .ok_or(DetectConflictError::ExploitProtectionKeyMissing)?;
+
+ let process_key = match key.try_open_subkey(process_name)? {
+ Some(key) => key,
+ None => {
+ log::info!(
+ "process name {:?} not found in exploit protection",
+ process_name
+ );
+ return Ok(ConflictingMitigationStatus::default());
+ }
+ };
+
+ // First get mitigation status for the root key
+ let mut status = get_conflicting_mitigations_for_key(&process_key)?;
+
+ // Then get mitigation status for each subkey and logical-or them all together
+ for subkey_name in process_key.subkey_names() {
+ let subkey_name = subkey_name?;
+
+ let subkey = process_key
+ .try_open_subkey(subkey_name)?
+ .expect("a subkey somehow doesn't exist after enumerating it");
+
+ let subkey_status = get_conflicting_mitigations_for_key(&subkey)?;
+ status.caller_check |= subkey_status.caller_check;
+ status.sim_exec |= subkey_status.sim_exec;
+ status.stack_pivot |= subkey_status.stack_pivot;
+ }
+
+ log::info!(
+ "process name {:?} has mitigation status {:?}",
+ process_name,
+ status
+ );
+
+ Ok(status)
+}
+
+/// Check a key to see if it has a "MitigationOptions" value with any conflicting mitigations
+/// enabled
+fn get_conflicting_mitigations_for_key(
+ key: &RegKey,
+) -> Result<ConflictingMitigationStatus, DetectConflictError> {
+ let value = match key.try_get_value(MITIGATION_VALUE_NAME)? {
+ Some(value) => value,
+ None => return Ok(ConflictingMitigationStatus::default()),
+ };
+
+ let bits = match value {
+ RegValue::Binary(bits) => bits,
+ _ => return Ok(ConflictingMitigationStatus::default()),
+ };
+
+ Ok(ConflictingMitigationStatus {
+ caller_check: get_bit(&bits, CALLER_CHECK_BIT)?,
+ sim_exec: get_bit(&bits, SIM_EXEC_BIT)?,
+ stack_pivot: get_bit(&bits, STACK_PIVOT_BIT)?,
+ })
+}
+
+/// Retrieve a bit from a vector-of-bitflags, which is what will be returned by the registry
+/// entry for exploit protection. The bits are numbered first by increasing value within a byte,
+/// and then by increasing index within the slice.
+///
+/// Ex: bit 0 = (index 0, mask 0x01), bit 7 = (index 0, mask 0x80), bit 8 = (index 1, mask 0x01)
+fn get_bit(bytes: &[u8], bit_idx: usize) -> Result<bool, DetectConflictError> {
+ let byte = bytes
+ .get(bit_idx / 8)
+ .ok_or(DetectConflictError::RegValueTooShort)?;
+
+ Ok(if byte & (1 << (bit_idx % 8)) != 0 {
+ true
+ } else {
+ false
+ })
+}
+
+/// Write the status of a feature to the given formatter. `bit == 0` means feature is disabled,
+/// `bit == 1` means enabled
+fn write_status(
+ f: &mut std::fmt::Formatter<'_>,
+ name: &str,
+ bit: bool,
+) -> Result<(), std::fmt::Error> {
+ let status_str = if bit { "enabled" } else { "disabled" };
+ write!(f, "{} is {}.", name, status_str)
+}
diff --git a/toolkit/xre/detect_win32k_conflicts/src/error.rs b/toolkit/xre/detect_win32k_conflicts/src/error.rs
new file mode 100644
index 0000000000..cc73d04b2d
--- /dev/null
+++ b/toolkit/xre/detect_win32k_conflicts/src/error.rs
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! Contains the error type for this crate
+
+use thiserror::Error;
+
+/// The error type for this crate
+#[derive(Debug, Error)]
+pub enum DetectConflictError {
+ /// An exploit protection key was not found in the registry
+ #[error("exploit protection key missing")]
+ ExploitProtectionKeyMissing,
+ /// Failed to enumerate the next registry subkey
+ #[error("failed to enumerate next registry subkey. code: {0}")]
+ RegEnumKeyFailed(u32),
+ /// Failed to get a value from the registry
+ #[error("failed to get registry value. code: {0}")]
+ RegGetValueFailed(u32),
+ /// Failed to get the length of a value from the registry
+ #[error("failed to get registry value length. code: {0}")]
+ RegGetValueLenFailed(u32),
+ /// Failed to open a registry key
+ #[error("failed to open registry key. code: {0}")]
+ RegOpenKeyFailed(u32),
+ /// Exploit Protection registry value was too short
+ #[error("exploit protection registry value too short")]
+ RegValueTooShort,
+ /// Failed to query key for subkey length
+ #[error("failed to query key for max subkey length. code: {0}")]
+ RegQueryInfoKeyFailed(u32),
+ /// A registry value had an unsupported type
+ #[error("key has unsupported value type: {0}")]
+ UnsupportedValueType(u32),
+}
diff --git a/toolkit/xre/detect_win32k_conflicts/src/ffi.rs b/toolkit/xre/detect_win32k_conflicts/src/ffi.rs
new file mode 100644
index 0000000000..826f452726
--- /dev/null
+++ b/toolkit/xre/detect_win32k_conflicts/src/ffi.rs
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! C foreign-function interface for this crate
+
+use super::detect::{get_conflicting_mitigations, ConflictingMitigationStatus};
+
+/// C FFI for `get_conflicting_mitigations`
+///
+/// Checks the current process to see if any conflicting mitigations would be enabled
+/// by Windows Exploit Protection
+///
+/// `conflicting_mitigations` is an out pointer that will receive the status of the
+/// Win32k conflicting mitigations
+///
+/// On success, returns `1`. On failure, returns `0` and `conflicting_mitigations` is left
+/// untouched
+///
+/// # Safety
+///
+/// `conflicting_mitigations` must not be a non-const, non-null ptr and **must be initialized**
+///
+/// # Example
+///
+/// ```C++
+/// void myFunc() {
+/// ConflictingMitigationStatus status = {}; // GOOD - value-initializes the object
+/// if(!detect_win32k_conflicting_mitigations(&status)) {
+/// // error
+/// }
+/// }
+/// ```
+#[no_mangle]
+pub unsafe extern "C" fn detect_win32k_conflicting_mitigations(
+ conflicting_mitigations: &mut ConflictingMitigationStatus,
+) -> bool {
+ let process_path =
+ std::env::current_exe().expect("unable to determine the path of our executable");
+ let file_name = process_path
+ .file_name()
+ .expect("executable doesn't have a file name");
+
+ match get_conflicting_mitigations(file_name) {
+ Ok(status) => {
+ *conflicting_mitigations = status;
+ true
+ }
+ Err(e) => {
+ log::error!(
+ "Failed to get Win32k Lockdown conflicting mitigation status: {}",
+ e
+ );
+ false
+ }
+ }
+}
diff --git a/toolkit/xre/detect_win32k_conflicts/src/lib.rs b/toolkit/xre/detect_win32k_conflicts/src/lib.rs
new file mode 100644
index 0000000000..4333603b92
--- /dev/null
+++ b/toolkit/xre/detect_win32k_conflicts/src/lib.rs
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! Crate to detect Windows Exploit Protection mitigations that may interfere with Win32k
+//! Lockdown
+
+#![deny(missing_docs)]
+
+mod detect;
+mod error;
+mod ffi;
+mod registry;
+
+pub use self::detect::get_conflicting_mitigations;
+pub use self::detect::ConflictingMitigationStatus;
+pub use self::error::DetectConflictError;
+pub use self::ffi::detect_win32k_conflicting_mitigations;
diff --git a/toolkit/xre/detect_win32k_conflicts/src/registry.rs b/toolkit/xre/detect_win32k_conflicts/src/registry.rs
new file mode 100644
index 0000000000..f375a64284
--- /dev/null
+++ b/toolkit/xre/detect_win32k_conflicts/src/registry.rs
@@ -0,0 +1,360 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! This module contains helpers for exploring the Windows Registry
+
+use super::error::DetectConflictError;
+
+use std::ffi::{OsStr, OsString};
+use std::os::windows::ffi::{OsStrExt, OsStringExt};
+
+use winapi::{
+ shared::{
+ minwindef::HKEY,
+ winerror::{ERROR_FILE_NOT_FOUND, ERROR_NO_MORE_ITEMS, ERROR_SUCCESS},
+ },
+ um::{
+ winnt::{KEY_READ, REG_BINARY, REG_DWORD, REG_SZ},
+ winreg::{
+ RegCloseKey, RegEnumKeyExW, RegGetValueW, RegOpenKeyExW, RegQueryInfoKeyW,
+ HKEY_LOCAL_MACHINE, RRF_RT_REG_BINARY, RRF_RT_REG_DWORD, RRF_RT_REG_SZ,
+ },
+ },
+};
+
+/// An open key in the Windows Registry
+///
+/// (Note that Windows ref-counts keys internally, so we don't need subkeys to "hang on" to
+/// their parents)
+#[derive(Debug)]
+pub struct RegKey {
+ /// The Win32 handle of the open key
+ handle: HKEY,
+}
+
+impl RegKey {
+ /// Open the global HKEY_LOCAL_MACHINE handle
+ pub fn root_local_machine() -> RegKey {
+ RegKey {
+ handle: HKEY_LOCAL_MACHINE,
+ }
+ }
+ /// Try to open a subkey relative to this key with read-only access rights
+ ///
+ /// Returns `None` if the subkey doesn't exist
+ pub fn try_open_subkey(
+ &self,
+ subkey: impl AsRef<OsStr>,
+ ) -> Result<Option<RegKey>, DetectConflictError> {
+ let win32_subkey = to_win32_string(subkey.as_ref());
+
+ let mut subkey_handle = std::ptr::null_mut();
+ let rv = unsafe {
+ RegOpenKeyExW(
+ self.handle,
+ win32_subkey.as_ptr(),
+ 0, // options
+ KEY_READ,
+ &mut subkey_handle,
+ )
+ }
+ .try_into()
+ .unwrap();
+
+ match rv {
+ ERROR_SUCCESS => {
+ assert!(!subkey_handle.is_null());
+
+ Ok(Some(RegKey {
+ handle: subkey_handle,
+ }))
+ }
+ ERROR_FILE_NOT_FOUND => Ok(None),
+ _ => Err(DetectConflictError::RegOpenKeyFailed(rv)),
+ }
+ }
+ /// Try to get a value with the given name
+ ///
+ /// Returns `None` if there is no value with that name
+ ///
+ /// Note that you can pass the empty string to get the default value
+ pub fn try_get_value(
+ &self,
+ value_name: impl AsRef<OsStr>,
+ ) -> Result<Option<RegValue>, DetectConflictError> {
+ let win32_value_name = to_win32_string(value_name.as_ref());
+
+ // First we query the value's length and type so we know how to create the data buffer
+ let mut value_type = 0;
+ let mut value_len = 0;
+
+ let rv = unsafe {
+ RegGetValueW(
+ self.handle,
+ std::ptr::null(), // subkey
+ win32_value_name.as_ptr(),
+ RRF_RT_REG_BINARY | RRF_RT_REG_DWORD | RRF_RT_REG_SZ,
+ &mut value_type,
+ std::ptr::null_mut(), // no data ptr, just query length & type
+ &mut value_len,
+ )
+ }
+ .try_into()
+ .unwrap();
+
+ if rv == ERROR_FILE_NOT_FOUND {
+ return Ok(None);
+ }
+
+ if rv != ERROR_SUCCESS || value_len == 0 {
+ return Err(DetectConflictError::RegGetValueLenFailed(rv));
+ }
+
+ if value_type == REG_SZ {
+ // buffer length is in wide characters
+ let buffer_len = value_len / 2;
+
+ assert_eq!(buffer_len * 2, value_len);
+
+ let mut buffer = vec![0u16; buffer_len.try_into().unwrap()];
+
+ let rv = unsafe {
+ RegGetValueW(
+ self.handle,
+ std::ptr::null(), // subkey
+ win32_value_name.as_ptr(),
+ RRF_RT_REG_SZ,
+ std::ptr::null_mut(), // no need to query type again
+ buffer.as_mut_ptr().cast(),
+ &mut value_len,
+ )
+ }
+ .try_into()
+ .unwrap();
+
+ if rv != ERROR_SUCCESS {
+ return Err(DetectConflictError::RegGetValueFailed(rv));
+ }
+
+ Ok(Some(RegValue::String(from_win32_string(&buffer))))
+ } else if value_type == REG_BINARY {
+ let mut buffer = vec![0u8; value_len.try_into().unwrap()];
+
+ let rv = unsafe {
+ RegGetValueW(
+ self.handle,
+ std::ptr::null(), // subkey
+ win32_value_name.as_ptr(),
+ RRF_RT_REG_BINARY,
+ std::ptr::null_mut(), // no need to query type again
+ buffer.as_mut_ptr().cast(),
+ &mut value_len,
+ )
+ }
+ .try_into()
+ .unwrap();
+
+ if rv != ERROR_SUCCESS {
+ return Err(DetectConflictError::RegGetValueFailed(rv));
+ }
+
+ Ok(Some(RegValue::Binary(buffer)))
+ } else if value_type == REG_DWORD {
+ assert_eq!(value_len, 4);
+
+ let mut buffer = 0u32;
+
+ let rv = unsafe {
+ RegGetValueW(
+ self.handle,
+ std::ptr::null(), // subkey
+ win32_value_name.as_ptr(),
+ RRF_RT_REG_DWORD,
+ std::ptr::null_mut(), // no need to query type again
+ (&mut buffer as *mut u32).cast(),
+ &mut value_len,
+ )
+ }
+ .try_into()
+ .unwrap();
+
+ if rv != ERROR_SUCCESS {
+ return Err(DetectConflictError::RegGetValueFailed(rv));
+ }
+
+ Ok(Some(RegValue::Dword(buffer)))
+ } else {
+ Err(DetectConflictError::UnsupportedValueType(value_type))
+ }
+ }
+ /// Get an iterator that returns the names of all the subkeys of this key
+ pub fn subkey_names(&self) -> SubkeyNames<'_> {
+ SubkeyNames::new(self)
+ }
+}
+
+impl Drop for RegKey {
+ fn drop(&mut self) {
+ assert!(!self.handle.is_null());
+
+ if self.handle == HKEY_LOCAL_MACHINE {
+ return;
+ }
+
+ let rv: u32 = unsafe { RegCloseKey(self.handle) }.try_into().unwrap();
+
+ if rv != ERROR_SUCCESS {
+ log::warn!("failed to close opened registry key");
+ }
+ }
+}
+
+/// An iterator through the subkeys of a key
+///
+/// Returns items of type `Result<OsString, DetectConflictError>`. If an error occurs, calling `next()` again
+/// will likely just continue to result in errors, so it's best to just abandon ship at that
+/// point
+#[derive(Debug)]
+pub struct SubkeyNames<'a> {
+ /// The key that is being iterated
+ key: &'a RegKey,
+ /// The current index of the subkey -- Incremented each successful call to `next()`
+ index: u32,
+ /// A buffer that is large enough to hold the longest subkey name
+ buffer: Option<Vec<u16>>,
+}
+
+impl<'a> SubkeyNames<'a> {
+ /// Create a new subkey name iterator
+ fn new(key: &'a RegKey) -> SubkeyNames<'a> {
+ SubkeyNames {
+ key,
+ index: 0,
+ buffer: None,
+ }
+ }
+ /// Initialize `self.buffer` with a vector that's long enough to hold the longest subkey name
+ /// (if it isn't already)
+ fn create_buffer_if_needed(&mut self) -> Result<(), DetectConflictError> {
+ if self.buffer.is_some() {
+ return Ok(());
+ }
+
+ let mut max_key_length = 0;
+
+ let rv = unsafe {
+ RegQueryInfoKeyW(
+ self.key.handle,
+ std::ptr::null_mut(), // class ptr
+ std::ptr::null_mut(), // class len ptr
+ std::ptr::null_mut(), // reserved
+ std::ptr::null_mut(), // num of subkeys ptr
+ &mut max_key_length,
+ std::ptr::null_mut(), // max class length ptr
+ std::ptr::null_mut(), // number of values ptr
+ std::ptr::null_mut(), // longest value name ptr
+ std::ptr::null_mut(), // longest value data ptr
+ std::ptr::null_mut(), // DACL ptr
+ std::ptr::null_mut(), // last write time ptr
+ )
+ }
+ .try_into()
+ .unwrap();
+
+ if rv != ERROR_SUCCESS {
+ return Err(DetectConflictError::RegQueryInfoKeyFailed(rv));
+ }
+
+ // +1 for the null terminator
+ let max_key_length = usize::try_from(max_key_length).unwrap() + 1;
+
+ self.buffer = Some(vec![0u16; max_key_length]);
+
+ Ok(())
+ }
+}
+
+impl<'a> Iterator for SubkeyNames<'a> {
+ type Item = Result<OsString, DetectConflictError>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Err(e) = self.create_buffer_if_needed() {
+ return Some(Err(e));
+ }
+
+ let buffer = self.buffer.as_mut().unwrap();
+ let mut name_len = u32::try_from(buffer.len()).unwrap();
+
+ let rv = unsafe {
+ RegEnumKeyExW(
+ self.key.handle,
+ self.index,
+ buffer.as_mut_ptr(),
+ &mut name_len,
+ std::ptr::null_mut(), // reserved
+ std::ptr::null_mut(), // class ptr
+ std::ptr::null_mut(), // class len ptr
+ std::ptr::null_mut(), // last write time ptr
+ )
+ }
+ .try_into()
+ .unwrap();
+
+ if rv == ERROR_NO_MORE_ITEMS {
+ return None;
+ }
+
+ if rv != ERROR_SUCCESS {
+ return Some(Err(DetectConflictError::RegEnumKeyFailed(rv)));
+ }
+
+ self.index += 1;
+
+ // +1 for the null terminator
+ let name_len = usize::try_from(name_len).unwrap() + 1;
+
+ Some(Ok(from_win32_string(&buffer[0..name_len])))
+ }
+}
+
+/// A single value in the registry
+#[derive(Clone, Debug, PartialEq)]
+pub enum RegValue {
+ /// A REG_BINARY value
+ Binary(Vec<u8>),
+ /// A REG_DWORD value
+ Dword(u32),
+ /// A REG_SZ value
+ String(OsString),
+}
+
+/// Convert an OsStr to a null-terminated wide character string
+///
+/// The input string must not contain any interior null values, and the resulting wide
+/// character array will have a null terminator appended
+fn to_win32_string(s: &OsStr) -> Vec<u16> {
+ // +1 for the null terminator
+ let mut result = Vec::with_capacity(s.len() + 1);
+
+ for wc in s.encode_wide() {
+ assert_ne!(wc, 0);
+ result.push(wc);
+ }
+
+ result.push(0);
+ result
+}
+
+/// Convert a null-terminated wide character string into an OsString
+///
+/// The passed-in slice must have exactly one wide null character as the last element
+fn from_win32_string(s: &[u16]) -> OsString {
+ for (idx, wc) in s.iter().enumerate() {
+ if *wc == 0 {
+ assert_eq!(idx, s.len() - 1);
+ return OsString::from_wide(&s[0..idx]);
+ }
+ }
+ panic!("missing null terminator at end of win32 string");
+}