Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/logging-access-denied.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ policy denial occurrences plus diagnostic outcomes omitted from the policy file:

```json
{
"version": 2,
"version": 3,
"signatures": [
{
"signature": {
Expand Down Expand Up @@ -296,6 +296,9 @@ reason and their sanitized event properties:
converted to a safe absolute DOS or UNC path. For example,
`\Device\MountPointManager` is useful Devices-namespace evidence, but it is
not a directly authorable filesystem grant.
- `comActivation` and `comInterfaceCall` mean a classic COM class activation
or interface call was denied. The CLSID or IID is retained; a malformed
Comment thread
jacobdereje-msft marked this conversation as resolved.
Resolved
identifier is reported as `eventPayloadMalformed`.
- `unsupportedObjectType` means the event names a resource outside the
supported diagnostic model. Examples include `\BaseNamedObjects` as a
Directory, ALPC Ports such as
Expand Down
52 changes: 50 additions & 2 deletions src/mxc-sdk/src/core/learning_mode_core/verbose_logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ pub enum VerboseLoggingOutcomeReason {
UnusableResourcePath,
/// A capability event did not contain a usable capability denial.
UnresolvedCapability,
/// Classic COM class activation.
ComActivation,
/// Classic COM interface call.
ComInterfaceCall,
/// The event was valid but did not describe an actionable denial.
NotActionable,
}
Expand Down Expand Up @@ -303,7 +307,7 @@ pub struct VerboseLoggingDocumentSummary {

impl VerboseLoggingDocument {
/// Current verbose logging document schema version.
pub const VERSION: u32 = 2;
pub const VERSION: u32 = 3;
Comment thread
jacobdereje-msft marked this conversation as resolved.
Resolved
Comment thread
jacobdereje-msft marked this conversation as resolved.
Resolved

/// Builds an on-disk document from decoder aggregate state.
#[must_use]
Expand Down Expand Up @@ -466,6 +470,36 @@ mod tests {
assert_eq!(summary.actionable_overflow_occurrences, 0);
}

#[test]
fn com_signature_overflow_is_not_counted_as_actionable() {
let mut summary = VerboseLoggingSummary::default();
let mut signature: VerboseLoggingSignature = serde_json::from_value(serde_json::json!({
"provider": "kernelGeneral",
"providerGuid": "kernel",
"eventId": 0,
"reason": "unsupportedEventSchema",
"pid": 1,
"properties": []
}))
.unwrap();
for event_id in 0..MAX_VERBOSE_LOGGING_GROUPS as u16 {
signature.event_id = event_id;
summary.record(signature.clone());
}

signature.event_id = u16::MAX;
signature.reason = VerboseLoggingOutcomeReason::ComActivation;
signature.resource_type = Some(crate::learning_mode_core::ResourceType::Other);
summary.record(signature);

assert_eq!(summary.overflow_occurrences, 1);
assert_eq!(summary.actionable_overflow_occurrences, 0);
assert!(!summary
.signatures
.iter()
.any(|group| { group.signature.reason == VerboseLoggingOutcomeReason::ComActivation }));
}

#[test]
fn byte_budget_leaves_guarded_analysis_protocol_headroom() {
let mut summary = VerboseLoggingSummary::default();
Expand Down Expand Up @@ -531,12 +565,26 @@ mod tests {
summary.mark_actionable_limit_reached();

let value = serde_json::to_value(VerboseLoggingDocument::new(&summary)).unwrap();
assert_eq!(value["version"], 2);
assert_eq!(value["version"], 3);
assert_eq!(value["signatures"][0]["signature"]["reason"], "actionable");
assert_eq!(value["summary"]["actionableOverflowOccurrences"], 2);
assert_eq!(value["summary"]["actionableLimitReached"], true);
}

#[test]
fn document_serializes_distinct_nonactionable_com_reasons() {
for (reason, expected) in [
(VerboseLoggingOutcomeReason::ComActivation, "comActivation"),
(
VerboseLoggingOutcomeReason::ComInterfaceCall,
"comInterfaceCall",
),
] {
assert!(!reason.is_actionable());
assert_eq!(serde_json::to_value(reason).unwrap(), expected);
}
}

#[test]
fn sibling_path_replaces_the_actionable_extension() {
assert_eq!(
Expand Down
104 changes: 104 additions & 0 deletions src/mxc-sdk/src/core/learning_mode_windows/etl_decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1847,6 +1847,110 @@ mod tests {
assert_eq!(out[2].resource_type, ResourceType::Ui);
}

#[test]
fn com_access_checks_are_distinct_verbose_only_outcomes_in_both_modes() {
Comment thread
jacobdereje-msft marked this conversation as resolved.
Resolved
let activation_clsid = "{A47979D2-C419-11D9-A5B4-001185AD2B89}";
let call_iid = "{00000132-0000-0000-C000-000000000046}";
let events = vec![
kernel_event(
14,
100,
1,
&[
("Mode", "\"Normal\""),
("ObjectType", "\"ComActivationForClass\""),
("ObjectName", activation_clsid),
("AccessMask", "0x1"),
],
),
kernel_event(
14,
101,
2,
&[
("Mode", "\"Permissive\""),
("ObjectType", "\"ComActivationForClass\""),
("ObjectName", "\"{a47979d2-c419-11d9-a5b4-001185ad2b89}\""),
("AccessMask", "0xffffffff"),
],
),
kernel_event(
Comment thread
jacobdereje-msft marked this conversation as resolved.
Resolved
14,
102,
3,
&[
("Mode", "\"Permissive\""),
("ObjectType", "\"ComCallOnInterface\""),
("ObjectName", call_iid),
("AccessMask", "0x2"),
],
),
];

let analysis = resources_from_events(&events);

assert!(analysis.denials.is_empty());
let activation_signatures = analysis
.verbose_logging
.signatures
.iter()
.filter(|group| property(&group.signature, "ObjectType") == "ComActivationForClass")
.collect::<Vec<_>>();
assert_eq!(activation_signatures.len(), 2);
assert!(activation_signatures.iter().all(|group| {
group.signature.reason == VerboseLoggingOutcomeReason::ComActivation
&& group.signature.resource_type == Some(ResourceType::Other)
&& group.signature.access_type.is_none()
&& group.count == 1
}));
assert!(activation_signatures
.iter()
.any(|group| property(&group.signature, "ObjectName") == activation_clsid));

let call = analysis
.verbose_logging
.signatures
.iter()
.find(|group| property(&group.signature, "ObjectType") == "ComCallOnInterface")
.expect("COM interface call should remain in verbose logging");
assert_eq!(
call.signature.reason,
VerboseLoggingOutcomeReason::ComInterfaceCall
);
assert_eq!(call.signature.resource_type, Some(ResourceType::Other));
assert!(call.signature.access_type.is_none());
assert_eq!(property(&call.signature, "ObjectName"), call_iid);
}

#[test]
fn malformed_com_identifier_remains_classified_verbose_diagnostic() {
let events = vec![kernel_event(
14,
42,
1,
&[
("Mode", "\"Normal\""),
("ObjectType", "\"ComActivationForClass\""),
("ObjectName", "\"not-a-clsid\""),
Comment thread
jacobdereje-msft marked this conversation as resolved.
Resolved
("AccessMask", "0x1"),
],
)];

let analysis = resources_from_events(&events);

assert!(analysis.denials.is_empty());
assert_eq!(analysis.verbose_logging.signatures.len(), 1);
let signature = &analysis.verbose_logging.signatures[0].signature;
assert_eq!(
signature.reason,
VerboseLoggingOutcomeReason::EventPayloadMalformed
);
assert_eq!(signature.resource_type, Some(ResourceType::Other));
assert!(signature.access_type.is_none());
assert_eq!(property(signature, "ObjectType"), "ComActivationForClass");
assert_eq!(property(signature, "ObjectName"), "not-a-clsid");
}

#[test]
fn unidentified_capability_events_are_omitted() {
let events = vec![
Expand Down
112 changes: 112 additions & 0 deletions src/mxc-sdk/src/core/learning_mode_windows/extractors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ pub(crate) const ACCESS_CHECK_EVENT_ID: u16 = 14;
pub(crate) const LEARNING_MODE_VIOLATION_EVENT_ID: u16 = 27;
pub(crate) const CAPABILITY_DENIAL_EVENT_ID: u16 = 28;
pub(crate) const PRIVACY_ACCESS_CHECK_EVENT_ID: u16 = 4907;
const COM_ACTIVATION_OBJECT_TYPE: &str = "ComActivationForClass";
const COM_CALL_OBJECT_TYPE: &str = "ComCallOnInterface";

/// Pre-decoded event payload handed to the extractors.
///
Expand Down Expand Up @@ -197,6 +199,9 @@ pub(crate) fn verbose_logging_classification(
),
Some(ResourceType::Other),
),
object_type if com_outcome_reason(object_type).is_some() => {
(None, Some(ResourceType::Other))
}
"" => (Some(AccessType::Unknown), Some(ResourceType::Capability)),
_ => (None, None),
}
Expand Down Expand Up @@ -645,13 +650,15 @@ pub fn build_denial_from_access_check(
let object_type = find_prop(&parts.props, "ObjectType")
.ok_or(VerboseLoggingOutcomeReason::MissingObjectType)?;
let object_type_str = object_type.trim_matches('"');
let com_reason = com_outcome_reason(object_type_str);

let resource_type = match object_type_str {
"File" => ResourceType::File,
"Key" => ResourceType::Other,
"Section" | "SymbolicLink" | "Timer" => ResourceType::Other,
// A present-but-empty object type is a brokered-capability check.
"" => ResourceType::Capability,
_ if com_reason.is_some() => ResourceType::Other,
_ => return Err(VerboseLoggingOutcomeReason::UnsupportedObjectType),
};

Expand All @@ -668,6 +675,13 @@ pub fn build_denial_from_access_check(
(_, Some(name)) => name,
};

if let Some(reason) = com_reason {
if !is_guid_identifier(&object_name) {
return Err(VerboseLoggingOutcomeReason::EventPayloadMalformed);
Comment thread
jacobdereje-msft marked this conversation as resolved.
Resolved
}
return Err(reason);
Comment thread
jacobdereje-msft marked this conversation as resolved.
Resolved
}

if resource_type == ResourceType::File {
let app_path = find_prop(&parts.props, "AppPath")
.or_else(|| find_prop(&parts.props, "ApplicationPath"))
Expand Down Expand Up @@ -1004,6 +1018,22 @@ fn find_prop<'a>(props: &'a [(String, String)], name: &str) -> Option<&'a String
props.iter().find(|(k, _)| k == name).map(|(_, v)| v)
}

fn com_outcome_reason(object_type: &str) -> Option<VerboseLoggingOutcomeReason> {
match object_type {
COM_ACTIVATION_OBJECT_TYPE => Some(VerboseLoggingOutcomeReason::ComActivation),
COM_CALL_OBJECT_TYPE => Some(VerboseLoggingOutcomeReason::ComInterfaceCall),
_ => None,
}
}

fn is_guid_identifier(value: &str) -> bool {
let value = value
.strip_prefix('{')
.and_then(|value| value.strip_suffix('}'))
.unwrap_or(value);
GUID::try_from(value).is_ok()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -1352,6 +1382,88 @@ mod tests {
}
}

#[test]
fn access_check_com_denials_use_distinct_verbose_reasons() {
for (object_type, object_name, mode, expected_reason) in [
(
COM_ACTIVATION_OBJECT_TYPE,
"{A47979D2-C419-11D9-A5B4-001185AD2B89}",
"\"Normal\"",
VerboseLoggingOutcomeReason::ComActivation,
),
(
COM_CALL_OBJECT_TYPE,
"{00000132-0000-0000-C000-000000000046}",
"\"Permissive\"",
VerboseLoggingOutcomeReason::ComInterfaceCall,
),
] {
let p = parts(
14,
&[
("Mode", mode),
("ObjectType", object_type),
("ObjectName", object_name),
("AccessMask", "0xffffffff"),
],
);

assert_eq!(extract_denial(&p, 42, FIXED_FILETIME), Err(expected_reason));
assert_eq!(
verbose_logging_classification(&p),
(None, Some(ResourceType::Other))
);
}
}

#[test]
fn access_check_com_denials_require_guid_identifier() {
for (object_name, expected) in [
(None, VerboseLoggingOutcomeReason::MissingObjectName),
(Some("\"\""), VerboseLoggingOutcomeReason::MissingObjectName),
(
Some("\"not-a-guid\""),
VerboseLoggingOutcomeReason::EventPayloadMalformed,
),
(
Some("\"{00000132-0000-0000-C000-000000000046\""),
VerboseLoggingOutcomeReason::EventPayloadMalformed,
),
(
Some("\"00000132-0000-0000-C000-000000000046}\""),
VerboseLoggingOutcomeReason::EventPayloadMalformed,
),
] {
let mut properties = vec![("ObjectType", COM_CALL_OBJECT_TYPE)];
if let Some(object_name) = object_name {
properties.push(("ObjectName", object_name));
}
let p = parts(14, &properties);

assert_eq!(extract_denial(&p, 42, FIXED_FILETIME), Err(expected));
assert_eq!(
verbose_logging_classification(&p),
(None, Some(ResourceType::Other))
);
}
}

#[test]
fn access_check_rpc_interface_remains_unsupported() {
let p = parts(
14,
&[
("ObjectType", "\"RPC Interface\""),
("ObjectName", "\"f6beaff7-1e19-4fbb-9f8f-b89e2018337c\""),
],
);
assert_eq!(
extract_denial(&p, 1, FIXED_FILETIME),
Err(VerboseLoggingOutcomeReason::UnsupportedObjectType)
);
assert_eq!(verbose_logging_classification(&p), (None, None));
}

#[test]
fn access_check_unrecognized_object_type_is_dropped() {
let p = parts(
Expand Down
Loading
Loading