From 61dcc486d7ab31970216ba0d7f0b9977240cf643 Mon Sep 17 00:00:00 2001 From: Matt White <436037+mlw@users.noreply.github.com> Date: Mon, 4 Mar 2024 15:33:07 -0500 Subject: [PATCH 1/2] Support CDHash rules --- Source/common/BUILD | 1 + Source/common/SNTCachedDecision.h | 1 + Source/common/SNTCommonEnums.h | 3 + Source/common/SNTRule.m | 11 +++ Source/common/SNTRuleIdentifiers.h | 2 + Source/common/SNTRuleIdentifiers.m | 4 +- Source/common/SNTStoredEvent.h | 5 + Source/common/SNTStoredEvent.m | 2 + Source/common/SNTSyncConstants.h | 5 + Source/common/SNTSyncConstants.m | 5 + .../SNTXPCUnprivilegedControlInterface.h | 13 +-- Source/common/santa.proto | 1 + Source/santactl/Commands/SNTCommandFileInfo.m | 43 +++++--- Source/santactl/Commands/SNTCommandRule.m | 38 ++++++- Source/santactl/Commands/SNTCommandStatus.m | 3 + Source/santad/BUILD | 1 + Source/santad/DataLayer/SNTRuleTable.h | 5 + Source/santad/DataLayer/SNTRuleTable.m | 27 +++-- Source/santad/DataLayer/SNTRuleTableTest.m | 62 +++++++++++- .../Serializers/BasicString.mm | 2 + .../Serializers/BasicStringTest.mm | 2 + .../EndpointSecurity/Serializers/Protobuf.mm | 2 + .../Serializers/ProtobufTest.mm | 2 + Source/santad/SNTDaemonControlController.mm | 13 +-- Source/santad/SNTExecutionController.h | 2 + Source/santad/SNTExecutionController.mm | 3 + Source/santad/SNTExecutionControllerTest.mm | 71 +++++++++++++ Source/santad/SNTPolicyProcessor.h | 6 +- Source/santad/SNTPolicyProcessor.m | 82 ++++++++++----- Source/santad/SantadTest.mm | 93 +++++++++++++++++- .../testdata/binaryrules/allowed_cdhash | Bin 0 -> 33440 bytes .../santad/testdata/binaryrules/banned_cdhash | Bin 0 -> 33440 bytes Source/santad/testdata/binaryrules/rules.db | Bin 5824512 -> 5824512 bytes Source/santasyncservice/SNTSyncEventUpload.m | 3 + Source/santasyncservice/SNTSyncPreflight.m | 1 + Source/santasyncservice/SNTSyncTest.m | 3 + .../sync_eventupload_input_basic.plist | 6 ++ 37 files changed, 445 insertions(+), 78 deletions(-) create mode 100755 Source/santad/testdata/binaryrules/allowed_cdhash create mode 100755 Source/santad/testdata/binaryrules/banned_cdhash diff --git a/Source/common/BUILD b/Source/common/BUILD index 3e290b6ab..63f9ccd69 100644 --- a/Source/common/BUILD +++ b/Source/common/BUILD @@ -427,6 +427,7 @@ objc_library( deps = [ ":SNTCommonEnums", ":SNTRule", + ":SNTRuleIdentifiers", ":SNTStoredEvent", ":SNTXPCBundleServiceInterface", ":SantaVnode", diff --git a/Source/common/SNTCachedDecision.h b/Source/common/SNTCachedDecision.h index b6c2a9974..51505563c 100644 --- a/Source/common/SNTCachedDecision.h +++ b/Source/common/SNTCachedDecision.h @@ -40,6 +40,7 @@ @property NSArray *certChain; @property NSString *teamID; @property NSString *signingID; +@property NSString *cdhash; @property NSDictionary *entitlements; @property BOOL entitlementsFiltered; diff --git a/Source/common/SNTCommonEnums.h b/Source/common/SNTCommonEnums.h index b50e80e55..36fc9e5b1 100644 --- a/Source/common/SNTCommonEnums.h +++ b/Source/common/SNTCommonEnums.h @@ -46,6 +46,7 @@ typedef NS_ENUM(NSInteger, SNTAction) { typedef NS_ENUM(NSInteger, SNTRuleType) { SNTRuleTypeUnknown = 0, + SNTRuleTypeCDHash = 500, SNTRuleTypeBinary = 1000, SNTRuleTypeSigningID = 2000, SNTRuleTypeCertificate = 3000, @@ -84,6 +85,7 @@ typedef NS_ENUM(uint64_t, SNTEventState) { SNTEventStateBlockTeamID = 1ULL << 20, SNTEventStateBlockLongPath = 1ULL << 21, SNTEventStateBlockSigningID = 1ULL << 22, + SNTEventStateBlockCDHash = 1ULL << 23, // Bits 40-63 store allow decision types SNTEventStateAllowUnknown = 1ULL << 40, @@ -95,6 +97,7 @@ typedef NS_ENUM(uint64_t, SNTEventState) { SNTEventStateAllowPendingTransitive = 1ULL << 46, SNTEventStateAllowTeamID = 1ULL << 47, SNTEventStateAllowSigningID = 1ULL << 48, + SNTEventStateAllowCDHash = 1ULL << 49, // Block and Allow masks SNTEventStateBlock = 0xFFFFFFULL << 16, diff --git a/Source/common/SNTRule.m b/Source/common/SNTRule.m index 89aebd7aa..c2df3c190 100644 --- a/Source/common/SNTRule.m +++ b/Source/common/SNTRule.m @@ -15,6 +15,7 @@ #import "Source/common/SNTRule.h" #include +#include #include #import "Source/common/SNTSyncConstants.h" @@ -103,6 +104,14 @@ - (instancetype)initWithIdentifier:(NSString *)identifier break; } + case SNTRuleTypeCDHash: { + identifier = [[identifier lowercaseString] stringByTrimmingCharactersInSet:nonHex]; + if (identifier.length != CS_CDHASH_LEN * 2) { + return nil; + } + break; + } + default: { break; } @@ -173,6 +182,8 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict { type = SNTRuleTypeTeamID; } else if ([ruleTypeString isEqual:kRuleTypeSigningID]) { type = SNTRuleTypeSigningID; + } else if ([ruleTypeString isEqual:kRuleTypeCDHash]) { + type = SNTRuleTypeCDHash; } else { return nil; } diff --git a/Source/common/SNTRuleIdentifiers.h b/Source/common/SNTRuleIdentifiers.h index 681369df3..09e496886 100644 --- a/Source/common/SNTRuleIdentifiers.h +++ b/Source/common/SNTRuleIdentifiers.h @@ -26,6 +26,7 @@ #import struct RuleIdentifiers { + NSString *cdhash; NSString *binarySHA256; NSString *signingID; NSString *certificateSHA256; @@ -33,6 +34,7 @@ struct RuleIdentifiers { }; @interface SNTRuleIdentifiers : NSObject +@property(readonly) NSString *cdhash; @property(readonly) NSString *binarySHA256; @property(readonly) NSString *signingID; @property(readonly) NSString *certificateSHA256; diff --git a/Source/common/SNTRuleIdentifiers.m b/Source/common/SNTRuleIdentifiers.m index 6d48d83a0..88624202e 100644 --- a/Source/common/SNTRuleIdentifiers.m +++ b/Source/common/SNTRuleIdentifiers.m @@ -19,6 +19,7 @@ @implementation SNTRuleIdentifiers - (instancetype)initWithRuleIdentifiers:(struct RuleIdentifiers)identifiers { self = [super init]; if (self) { + _cdhash = identifiers.cdhash; _binarySHA256 = identifiers.binarySHA256; _signingID = identifiers.signingID; _certificateSHA256 = identifiers.certificateSHA256; @@ -28,7 +29,8 @@ - (instancetype)initWithRuleIdentifiers:(struct RuleIdentifiers)identifiers { } - (struct RuleIdentifiers)toStruct { - return (struct RuleIdentifiers){.binarySHA256 = self.binarySHA256, + return (struct RuleIdentifiers){.cdhash = self.cdhash, + .binarySHA256 = self.binarySHA256, .signingID = self.signingID, .certificateSHA256 = self.certificateSHA256, .teamID = self.teamID}; diff --git a/Source/common/SNTStoredEvent.h b/Source/common/SNTStoredEvent.h index 42694a1f3..97561cdaa 100644 --- a/Source/common/SNTStoredEvent.h +++ b/Source/common/SNTStoredEvent.h @@ -105,6 +105,11 @@ /// @property NSString *signingID; +/// +/// If the executed file was signed, this is the CDHash of the binary. +/// +@property NSString *cdhash; + /// /// The user who executed the binary. /// diff --git a/Source/common/SNTStoredEvent.m b/Source/common/SNTStoredEvent.m index 6c24571fa..75189b368 100644 --- a/Source/common/SNTStoredEvent.m +++ b/Source/common/SNTStoredEvent.m @@ -51,6 +51,7 @@ - (void)encodeWithCoder:(NSCoder *)coder { ENCODE(self.signingChain, @"signingChain"); ENCODE(self.teamID, @"teamID"); ENCODE(self.signingID, @"signingID"); + ENCODE(self.cdhash, @"cdhash"); ENCODE(self.executingUser, @"executingUser"); ENCODE(self.occurrenceDate, @"occurrenceDate"); @@ -97,6 +98,7 @@ - (instancetype)initWithCoder:(NSCoder *)decoder { _signingChain = DECODEARRAY(MOLCertificate, @"signingChain"); _teamID = DECODE(NSString, @"teamID"); _signingID = DECODE(NSString, @"signingID"); + _cdhash = DECODE(NSString, @"cdhash"); _executingUser = DECODE(NSString, @"executingUser"); _occurrenceDate = DECODE(NSDate, @"occurrenceDate"); diff --git a/Source/common/SNTSyncConstants.h b/Source/common/SNTSyncConstants.h index 9c8e2a415..0a8097f95 100644 --- a/Source/common/SNTSyncConstants.h +++ b/Source/common/SNTSyncConstants.h @@ -44,6 +44,7 @@ extern NSString *const kCompilerRuleCount; extern NSString *const kTransitiveRuleCount; extern NSString *const kTeamIDRuleCount; extern NSString *const kSigningIDRuleCount; +extern NSString *const kCDHashRuleCount; extern NSString *const kFullSyncInterval; extern NSString *const kFCMToken; extern NSString *const kFCMFullSyncInterval; @@ -70,12 +71,14 @@ extern NSString *const kDecisionAllowCertificate; extern NSString *const kDecisionAllowScope; extern NSString *const kDecisionAllowTeamID; extern NSString *const kDecisionAllowSigningID; +extern NSString *const kDecisionAllowCDHash; extern NSString *const kDecisionBlockUnknown; extern NSString *const kDecisionBlockBinary; extern NSString *const kDecisionBlockCertificate; extern NSString *const kDecisionBlockScope; extern NSString *const kDecisionBlockTeamID; extern NSString *const kDecisionBlockSigningID; +extern NSString *const kDecisionBlockCDHash; extern NSString *const kDecisionUnknown; extern NSString *const kDecisionBundleBinary; extern NSString *const kLoggedInUsers; @@ -101,6 +104,7 @@ extern NSString *const kCertValidFrom; extern NSString *const kCertValidUntil; extern NSString *const kTeamID; extern NSString *const kSigningID; +extern NSString *const kCDHash; extern NSString *const kQuarantineDataURL; extern NSString *const kQuarantineRefererURL; extern NSString *const kQuarantineTimestamp; @@ -125,6 +129,7 @@ extern NSString *const kRuleTypeBinary; extern NSString *const kRuleTypeCertificate; extern NSString *const kRuleTypeTeamID; extern NSString *const kRuleTypeSigningID; +extern NSString *const kRuleTypeCDHash; extern NSString *const kRuleCustomMsg; extern NSString *const kRuleCustomURL; extern NSString *const kCursor; diff --git a/Source/common/SNTSyncConstants.m b/Source/common/SNTSyncConstants.m index 6b2f3c06a..042b8bc89 100644 --- a/Source/common/SNTSyncConstants.m +++ b/Source/common/SNTSyncConstants.m @@ -44,6 +44,7 @@ NSString *const kTransitiveRuleCount = @"transitive_rule_count"; NSString *const kTeamIDRuleCount = @"teamid_rule_count"; NSString *const kSigningIDRuleCount = @"signingid_rule_count"; +NSString *const kCDHashRuleCount = @"cdhash_rule_count"; NSString *const kFullSyncInterval = @"full_sync_interval"; NSString *const kFCMToken = @"fcm_token"; NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval"; @@ -71,12 +72,14 @@ NSString *const kDecisionAllowScope = @"ALLOW_SCOPE"; NSString *const kDecisionAllowTeamID = @"ALLOW_TEAMID"; NSString *const kDecisionAllowSigningID = @"ALLOW_SIGNINGID"; +NSString *const kDecisionAllowCDHash = @"ALLOW_CDHASH"; NSString *const kDecisionBlockUnknown = @"BLOCK_UNKNOWN"; NSString *const kDecisionBlockBinary = @"BLOCK_BINARY"; NSString *const kDecisionBlockCertificate = @"BLOCK_CERTIFICATE"; NSString *const kDecisionBlockScope = @"BLOCK_SCOPE"; NSString *const kDecisionBlockTeamID = @"BLOCK_TEAMID"; NSString *const kDecisionBlockSigningID = @"BLOCK_SIGNINGID"; +NSString *const kDecisionBlockCDHash = @"BLOCK_CDHASH"; NSString *const kDecisionUnknown = @"UNKNOWN"; NSString *const kDecisionBundleBinary = @"BUNDLE_BINARY"; NSString *const kLoggedInUsers = @"logged_in_users"; @@ -102,6 +105,7 @@ NSString *const kCertValidUntil = @"valid_until"; NSString *const kTeamID = @"team_id"; NSString *const kSigningID = @"signing_id"; +NSString *const kCDHash = @"cdhash"; NSString *const kQuarantineDataURL = @"quarantine_data_url"; NSString *const kQuarantineRefererURL = @"quarantine_referer_url"; NSString *const kQuarantineTimestamp = @"quarantine_timestamp"; @@ -126,6 +130,7 @@ NSString *const kRuleTypeCertificate = @"CERTIFICATE"; NSString *const kRuleTypeTeamID = @"TEAMID"; NSString *const kRuleTypeSigningID = @"SIGNINGID"; +NSString *const kRuleTypeCDHash = @"CDHASH"; NSString *const kRuleCustomMsg = @"custom_msg"; NSString *const kRuleCustomURL = @"custom_url"; NSString *const kCursor = @"cursor"; diff --git a/Source/common/SNTXPCUnprivilegedControlInterface.h b/Source/common/SNTXPCUnprivilegedControlInterface.h index 5990c1a6b..02d61f52f 100644 --- a/Source/common/SNTXPCUnprivilegedControlInterface.h +++ b/Source/common/SNTXPCUnprivilegedControlInterface.h @@ -16,6 +16,7 @@ #import #import "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTRuleIdentifiers.h" #import "Source/common/SantaVnode.h" @class SNTRule; @@ -29,6 +30,7 @@ struct RuleCounts { int64_t transitive; int64_t teamID; int64_t signingID; + int64_t cdhash; }; /// @@ -55,17 +57,10 @@ struct RuleCounts { /// /// @param filePath A Path to the file, can be nil. -/// @param fileSHA256 The pre-calculated SHA256 hash for the file, can be nil. If nil the hash will -/// be calculated by this method from the filePath. -/// @param certificateSHA256 A SHA256 hash of the signing certificate, can be nil. -/// @note If fileInfo and signingCertificate are both passed in, the most specific rule will be -/// returned. Binary rules take precedence over cert rules. +/// @param identifiers The various identifiers to be used when making a decision. /// - (void)decisionForFilePath:(NSString *)filePath - fileSHA256:(NSString *)fileSHA256 - certificateSHA256:(NSString *)certificateSHA256 - teamID:(NSString *)teamID - signingID:(NSString *)signingID + identifiers:(SNTRuleIdentifiers *)identifiers reply:(void (^)(SNTEventState))reply; /// diff --git a/Source/common/santa.proto b/Source/common/santa.proto index 504476de8..d195226f2 100644 --- a/Source/common/santa.proto +++ b/Source/common/santa.proto @@ -284,6 +284,7 @@ message Execution { REASON_LONG_PATH = 9; REASON_NOT_RUNNING = 10; REASON_SIGNING_ID = 11; + REASON_CDHASH = 12; } optional Reason reason = 10; diff --git a/Source/santactl/Commands/SNTCommandFileInfo.m b/Source/santactl/Commands/SNTCommandFileInfo.m index 9888752b7..fadb710ff 100644 --- a/Source/santactl/Commands/SNTCommandFileInfo.m +++ b/Source/santactl/Commands/SNTCommandFileInfo.m @@ -22,6 +22,7 @@ #import "Source/common/SNTFileInfo.h" #import "Source/common/SNTLogging.h" #import "Source/common/SNTRule.h" +#import "Source/common/SNTRuleIdentifiers.h" #import "Source/common/SNTStoredEvent.h" #import "Source/common/SNTXPCBundleServiceInterface.h" #import "Source/common/SNTXPCControlInterface.h" @@ -45,6 +46,7 @@ static NSString *const kUniversalSigningChain = @"Universal Signing Chain"; static NSString *const kTeamID = @"Team ID"; static NSString *const kSigningID = @"Signing ID"; +static NSString *const kCDHash = @"CDHash"; // signing chain keys static NSString *const kCommonName = @"Common Name"; @@ -123,6 +125,7 @@ @interface SNTCommandFileInfo : SNTCommand @property(readonly, copy, nonatomic) SNTAttributeBlock downloadAgent; @property(readonly, copy, nonatomic) SNTAttributeBlock teamID; @property(readonly, copy, nonatomic) SNTAttributeBlock signingID; +@property(readonly, copy, nonatomic) SNTAttributeBlock cdhash; @property(readonly, copy, nonatomic) SNTAttributeBlock type; @property(readonly, copy, nonatomic) SNTAttributeBlock pageZero; @property(readonly, copy, nonatomic) SNTAttributeBlock codeSigned; @@ -201,8 +204,8 @@ + (NSString *)longHelpText { + (NSArray *)fileInfoKeys { return @[ kPath, kSHA256, kSHA1, kBundleName, kBundleVersion, kBundleVersionStr, kDownloadReferrerURL, - kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kType, kPageZero, - kCodeSigned, kRule, kSigningChain, kUniversalSigningChain + kDownloadURL, kDownloadTimestamp, kDownloadAgent, kTeamID, kSigningID, kCDHash, kType, + kPageZero, kCodeSigned, kRule, kSigningChain, kUniversalSigningChain ]; } @@ -236,6 +239,7 @@ - (instancetype)initWithDaemonConnection:(MOLXPCConnection *)daemonConn { kUniversalSigningChain : self.universalSigningChain, kTeamID : self.teamID, kSigningID : self.signingID, + kCDHash : self.cdhash, }; _printQueue = dispatch_queue_create("com.google.santactl.print_queue", DISPATCH_QUEUE_SERIAL); @@ -376,6 +380,8 @@ - (SNTAttributeBlock)rule { NSError *err; MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:&err]; + NSString *cdhash = + [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoUnique]; NSString *teamID = [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoTeamIdentifier]; NSString *identifier = @@ -394,15 +400,21 @@ - (SNTAttributeBlock)rule { } } - [[cmd.daemonConn remoteObjectProxy] decisionForFilePath:fileInfo.path - fileSHA256:fileInfo.SHA256 - certificateSHA256:err ? nil : csc.leafCertificate.SHA256 - teamID:teamID - signingID:signingID - reply:^(SNTEventState s) { - state = s; - dispatch_semaphore_signal(sema); - }]; + struct RuleIdentifiers identifiers = { + .cdhash = cdhash, + .binarySHA256 = fileInfo.SHA256, + .signingID = signingID, + .certificateSHA256 = err ? nil : csc.leafCertificate.SHA256, + .teamID = teamID, + }; + + [[cmd.daemonConn remoteObjectProxy] + decisionForFilePath:fileInfo.path + identifiers:[[SNTRuleIdentifiers alloc] initWithRuleIdentifiers:identifiers] + reply:^(SNTEventState s) { + state = s; + dispatch_semaphore_signal(sema); + }]; if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) { cmd.daemonUnavailable = YES; return kCommunicationErrorMsg; @@ -420,6 +432,8 @@ - (SNTAttributeBlock)rule { case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break; case SNTEventStateAllowSigningID: case SNTEventStateBlockSigningID: [output appendString:@" (SigningID)"]; break; + case SNTEventStateAllowCDHash: + case SNTEventStateBlockCDHash: [output appendString:@" (CDHash)"]; break; case SNTEventStateAllowScope: case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break; case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break; @@ -519,6 +533,13 @@ - (SNTAttributeBlock)signingID { }; } +- (SNTAttributeBlock)cdhash { + return ^id(SNTCommandFileInfo *cmd, SNTFileInfo *fileInfo) { + MOLCodesignChecker *csc = [fileInfo codesignCheckerWithError:NULL]; + return [csc.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoUnique]; + }; +} + #pragma mark - // Entry point for the command. diff --git a/Source/santactl/Commands/SNTCommandRule.m b/Source/santactl/Commands/SNTCommandRule.m index fd323ce36..b269bc24a 100644 --- a/Source/santactl/Commands/SNTCommandRule.m +++ b/Source/santactl/Commands/SNTCommandRule.m @@ -1,3 +1,4 @@ + /// Copyright 2015 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,7 +13,9 @@ /// See the License for the specific language governing permissions and /// limitations under the License. +#import #import +#import #import #import #import @@ -62,13 +65,14 @@ + (NSString *)longHelpText { @" Will add the hash of the file currently at that path.\n" @" Does not work with --check. Use the fileinfo verb to check.\n" @" the rule state of a file.\n" - @" --identifier {sha256|teamID|signingID}: identifier to add/remove/check\n" + @" --identifier {sha256|teamID|signingID|cdhash}: identifier to add/remove/check\n" @" --sha256 {sha256}: hash to add/remove/check [deprecated]\n" @"\n" @" Optionally:\n" @" --teamid: add or check a team ID rule instead of binary\n" @" --signingid: add or check a signing ID rule instead of binary (see notes)\n" @" --certificate: add or check a certificate sha256 rule instead of binary\n" + @" --cdhash: add or check a cdhash rule instead of binary\n" #ifdef DEBUG @" --force: allow manual changes even when SyncBaseUrl is set\n" #endif @@ -156,6 +160,8 @@ - (void)runWithArguments:(NSArray *)arguments { newRule.type = SNTRuleTypeTeamID; } else if ([arg caseInsensitiveCompare:@"--signingid"] == NSOrderedSame) { newRule.type = SNTRuleTypeSigningID; + } else if ([arg caseInsensitiveCompare:@"--cdhash"] == NSOrderedSame) { + newRule.type = SNTRuleTypeCDHash; } else if ([arg caseInsensitiveCompare:@"--path"] == NSOrderedSame) { if (++i > arguments.count - 1) { [self printErrorUsageAndExit:@"--path requires an argument"]; @@ -252,17 +258,29 @@ - (void)runWithArguments:(NSArray *)arguments { } else if (newRule.type == SNTRuleTypeCertificate) { MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL]; newRule.identifier = cs.leafCertificate.SHA256; + } else if (newRule.type == SNTRuleTypeCDHash) { + MOLCodesignChecker *cs = [fi codesignCheckerWithError:NULL]; + newRule.identifier = + [cs.signingInformation objectForKey:(__bridge NSString *)kSecCodeInfoIdentifier]; } else if (newRule.type == SNTRuleTypeTeamID || newRule.type == SNTRuleTypeSigningID) { // noop } } - if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) { + if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate || + newRule.type == SNTRuleTypeCDHash) { NSCharacterSet *nonHex = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet]; - if ([[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length != - 64) { + NSUInteger length = + [[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length; + + if ((newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) && + length != CC_SHA256_DIGEST_LENGTH * 2) { [self printErrorUsageAndExit:@"BINARY or CERTIFICATE rules require a valid SHA-256"]; + } else if (newRule.type == SNTRuleTypeCDHash && length != CS_CDHASH_LEN * 2) { + [self printErrorUsageAndExit: + [NSString stringWithFormat:@"CDHASH rules require a valid hex string of length %d", + CS_CDHASH_LEN * 2]]; } } @@ -289,7 +307,7 @@ - (void)runWithArguments:(NSArray *)arguments { } else { NSString *ruleType; switch (newRule.type) { - case SNTRuleTypeCertificate: + case SNTRuleTypeCertificate: ruleType = @"Certificate SHA-256"; break; case SNTRuleTypeBinary: { ruleType = @"SHA-256"; break; @@ -298,6 +316,14 @@ - (void)runWithArguments:(NSArray *)arguments { ruleType = @"Team ID"; break; } + case SNTRuleTypeSigningID: { + ruleType = @"Signing ID"; + break; + } + case SNTRuleTypeCDHash: { + ruleType = @"CDHash"; + break; + } default: ruleType = @"(Unknown type)"; } if (newRule.state == SNTRuleStateRemove) { @@ -351,6 +377,7 @@ + (NSString *)stringifyRule:(SNTRule *)rule withColor:(BOOL)colorize { switch (rule.type) { case SNTRuleTypeUnknown: [output appendString:@"Unknown"]; break; + case SNTRuleTypeCDHash: [output appendString:@"CDHash"]; break; case SNTRuleTypeBinary: [output appendString:@"Binary"]; break; case SNTRuleTypeSigningID: [output appendString:@"SigningID"]; break; case SNTRuleTypeCertificate: [output appendString:@"Certificate"]; break; @@ -396,6 +423,7 @@ - (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)da __block NSString *output; struct RuleIdentifiers identifiers = { + .cdhash = (rule.type == SNTRuleTypeCDHash) ? rule.identifier : nil, .binarySHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil, .certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil, .teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil, diff --git a/Source/santactl/Commands/SNTCommandStatus.m b/Source/santactl/Commands/SNTCommandStatus.m index 25407c920..dfb54a7fd 100644 --- a/Source/santactl/Commands/SNTCommandStatus.m +++ b/Source/santactl/Commands/SNTCommandStatus.m @@ -98,6 +98,7 @@ - (void)runWithArguments:(NSArray *)arguments { .transitive = -1, .teamID = -1, .signingID = -1, + .cdhash = -1, }; [rop databaseRuleCounts:^(struct RuleCounts counts) { ruleCounts = counts; @@ -213,6 +214,7 @@ - (void)runWithArguments:(NSArray *)arguments { @"certificate_rules" : @(ruleCounts.certificate), @"teamid_rules" : @(ruleCounts.teamID), @"signingid_rules" : @(ruleCounts.signingID), + @"cdhash_rules" : @(ruleCounts.cdhash), @"compiler_rules" : @(ruleCounts.compiler), @"transitive_rules" : @(ruleCounts.transitive), @"events_pending_upload" : @(eventCount), @@ -285,6 +287,7 @@ - (void)runWithArguments:(NSArray *)arguments { printf(" %-25s | %lld\n", "Certificate Rules", ruleCounts.certificate); printf(" %-25s | %lld\n", "TeamID Rules", ruleCounts.teamID); printf(" %-25s | %lld\n", "SigningID Rules", ruleCounts.signingID); + printf(" %-25s | %lld\n", "CDHash Rules", ruleCounts.cdhash); printf(" %-25s | %lld\n", "Compiler Rules", ruleCounts.compiler); printf(" %-25s | %lld\n", "Transitive Rules", ruleCounts.transitive); printf(" %-25s | %lld\n", "Events Pending Upload", eventCount); diff --git a/Source/santad/BUILD b/Source/santad/BUILD index aa98720a4..7f719b21e 100644 --- a/Source/santad/BUILD +++ b/Source/santad/BUILD @@ -204,6 +204,7 @@ objc_library( "//Source/common:SNTFileInfo", "//Source/common:SNTLogging", "//Source/common:SNTRule", + "//Source/common:SNTRuleIdentifiers", "@FMDB", "@MOLCertificate", "@MOLCodesignChecker", diff --git a/Source/santad/DataLayer/SNTRuleTable.h b/Source/santad/DataLayer/SNTRuleTable.h index 306b8a39e..ae78fd8ad 100644 --- a/Source/santad/DataLayer/SNTRuleTable.h +++ b/Source/santad/DataLayer/SNTRuleTable.h @@ -62,6 +62,11 @@ /// - (int64_t)signingIDRuleCount; +/// +/// @return Number of cdhash rules in the database +/// +- (int64_t)cdhashRuleCount; + /// /// @return Rule for given identifiers. /// Currently: binary, signingID, certificate or teamID (in that order). diff --git a/Source/santad/DataLayer/SNTRuleTable.m b/Source/santad/DataLayer/SNTRuleTable.m index 028d324d0..6ba2dfccc 100644 --- a/Source/santad/DataLayer/SNTRuleTable.m +++ b/Source/santad/DataLayer/SNTRuleTable.m @@ -313,6 +313,10 @@ - (int64_t)signingIDRuleCount { return [self ruleCountForRuleType:SNTRuleTypeSigningID]; } +- (int64_t)cdhashRuleCount { + return [self ruleCountForRuleType:SNTRuleTypeCDHash]; +} + - (SNTRule *)ruleFromResultSet:(FMResultSet *)rs { SNTRule *r = [[SNTRule alloc] initWithIdentifier:[rs stringForColumn:@"identifier"] state:[rs intForColumn:@"state"] @@ -331,6 +335,11 @@ - (SNTRule *)ruleForIdentifiers:(struct RuleIdentifiers)identifiers { if (staticRules.count) { // IMPORTANT: The order static rules are checked here should be the same // order as given by the SQL query for the rules database. + rule = staticRules[identifiers.cdhash]; + if (rule.type == SNTRuleTypeCDHash) { + return rule; + } + rule = staticRules[identifiers.binarySHA256]; if (rule.type == SNTRuleTypeBinary) { return rule; @@ -357,7 +366,7 @@ - (SNTRule *)ruleForIdentifiers:(struct RuleIdentifiers)identifiers { // NOTE: This code is written with the intention that the binary rule is searched for first // as Santa is designed to go with the most-specific rule possible. // - // The intended order of precedence is Binaries > Signing IDs > Certificates > Team IDs. + // The intended order of precedence is CDHash > Binaries > Signing IDs > Certificates > Team IDs. // // As such the query should have "ORDER BY type DESC" before the LIMIT, to ensure that is the // case. However, in all tested versions of SQLite that ORDER BY clause is unnecessary: the query @@ -372,13 +381,15 @@ - (SNTRule *)ruleForIdentifiers:(struct RuleIdentifiers)identifiers { // There is a test for this in SNTRuleTableTests in case SQLite behavior changes in the future. // [self inDatabase:^(FMDatabase *db) { - FMResultSet *rs = [db executeQuery:@"SELECT * FROM rules WHERE " - @" (identifier=? and type=1000) " - @"OR (identifier=? AND type=2000) " - @"OR (identifier=? AND type=3000) " - @"OR (identifier=? AND type=4000) LIMIT 1", - identifiers.binarySHA256, identifiers.signingID, - identifiers.certificateSHA256, identifiers.teamID]; + FMResultSet *rs = + [db executeQuery:@"SELECT * FROM rules WHERE " + @" (identifier=? and type=500) " + @"OR (identifier=? and type=1000) " + @"OR (identifier=? AND type=2000) " + @"OR (identifier=? AND type=3000) " + @"OR (identifier=? AND type=4000) LIMIT 1", + identifiers.cdhash, identifiers.binarySHA256, identifiers.signingID, + identifiers.certificateSHA256, identifiers.teamID]; if ([rs next]) { rule = [self ruleFromResultSet:rs]; } diff --git a/Source/santad/DataLayer/SNTRuleTableTest.m b/Source/santad/DataLayer/SNTRuleTableTest.m index 36ffd5190..ae88eba6b 100644 --- a/Source/santad/DataLayer/SNTRuleTableTest.m +++ b/Source/santad/DataLayer/SNTRuleTableTest.m @@ -57,6 +57,15 @@ - (SNTRule *)_exampleSigningIDRuleIsPlatform:(BOOL)isPlatformBinary { return r; } +- (SNTRule *)_exampleCDHashRule { + SNTRule *r = [[SNTRule alloc] init]; + r.identifier = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a"; + r.state = SNTRuleStateBlock; + r.type = SNTRuleTypeCDHash; + r.customMsg = @"A cdhash rule"; + return r; +} + - (SNTRule *)_exampleBinaryRule { SNTRule *r = [[SNTRule alloc] init]; r.identifier = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"; @@ -264,10 +273,32 @@ - (void)testFetchSigningIDRule { XCTAssertNil(r); } +- (void)testFetchCDHashRule { + [self.sut + addRules:@[ [self _exampleBinaryRule], [self _exampleTeamIDRule], [self _exampleCDHashRule] ] + ruleCleanup:SNTRuleCleanupNone + error:nil]; + + XCTAssertEqual([self.sut cdhashRuleCount], 1); + + SNTRule *r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){ + .cdhash = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a", + }]; + + XCTAssertNotNil(r); + XCTAssertEqualObjects(r.identifier, @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a"); + XCTAssertEqual(r.type, SNTRuleTypeCDHash); + + r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){ + .cdhash = @"nonexistent", + }]; + XCTAssertNil(r); +} + - (void)testFetchRuleOrdering { [self.sut addRules:@[ [self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule], - [self _exampleSigningIDRuleIsPlatform:NO] + [self _exampleSigningIDRuleIsPlatform:NO], [self _exampleCDHashRule] ] ruleCleanup:SNTRuleCleanupNone error:nil]; @@ -276,6 +307,21 @@ - (void)testFetchRuleOrdering { // See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID SNTRule *r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){ + .cdhash = @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a", + .binarySHA256 = + @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670", + .signingID = @"ABCDEFGHIJ:signingID", + .certificateSHA256 = + @"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258", + .teamID = @"ABCDEFGHIJ", + }]; + XCTAssertNotNil(r); + XCTAssertEqualObjects(r.identifier, @"dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a"); + XCTAssertEqual(r.type, SNTRuleTypeCDHash, @"Implicit rule ordering failed"); + + r = [self.sut + ruleForIdentifiers:(struct RuleIdentifiers){ + .cdhash = @"unknown", .binarySHA256 = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670", .signingID = @"ABCDEFGHIJ:signingID", @@ -290,6 +336,7 @@ - (void)testFetchRuleOrdering { r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){ + .cdhash = @"unknown", .binarySHA256 = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670", .signingID = @"ABCDEFGHIJ:signingID", @@ -304,6 +351,7 @@ - (void)testFetchRuleOrdering { r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){ + .cdhash = @"unknown", .binarySHA256 = @"unknown", .signingID = @"unknown", .certificateSHA256 = @@ -317,6 +365,7 @@ - (void)testFetchRuleOrdering { XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed"); r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){ + .cdhash = @"unknown", .binarySHA256 = @"unknown", .signingID = @"ABCDEFGHIJ:signingID", .certificateSHA256 = @"unknown", @@ -327,6 +376,7 @@ - (void)testFetchRuleOrdering { XCTAssertEqual(r.type, SNTRuleTypeSigningID, @"Implicit rule ordering failed (SigningID)"); r = [self.sut ruleForIdentifiers:(struct RuleIdentifiers){ + .cdhash = @"unknown", .binarySHA256 = @"unknown", .signingID = @"unknown", .certificateSHA256 = @"unknown", @@ -357,18 +407,22 @@ - (void)testRetrieveAllRulesWithEmptyDatabase { - (void)testRetrieveAllRulesWithMultipleRules { [self.sut addRules:@[ - [self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule], - [self _exampleSigningIDRuleIsPlatform:NO] + [self _exampleCertRule], + [self _exampleBinaryRule], + [self _exampleTeamIDRule], + [self _exampleSigningIDRuleIsPlatform:NO], + [self _exampleCDHashRule], ] ruleCleanup:SNTRuleCleanupNone error:nil]; NSArray *rules = [self.sut retrieveAllRules]; - XCTAssertEqual(rules.count, 4); + XCTAssertEqual(rules.count, 5); XCTAssertEqualObjects(rules[0], [self _exampleCertRule]); XCTAssertEqualObjects(rules[1], [self _exampleBinaryRule]); XCTAssertEqualObjects(rules[2], [self _exampleTeamIDRule]); XCTAssertEqualObjects(rules[3], [self _exampleSigningIDRuleIsPlatform:NO]); + XCTAssertEqualObjects(rules[4], [self _exampleCDHashRule]); } @end diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm b/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm index 3cda73241..62aa1c9da 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm @@ -94,12 +94,14 @@ static inline SanitizableString FilePath(const es_file_t *file) { case SNTEventStateAllowScope: return "SCOPE"; case SNTEventStateAllowTeamID: return "TEAMID"; case SNTEventStateAllowSigningID: return "SIGNINGID"; + case SNTEventStateAllowCDHash: return "CDHASH"; case SNTEventStateAllowUnknown: return "UNKNOWN"; case SNTEventStateBlockBinary: return "BINARY"; case SNTEventStateBlockCertificate: return "CERT"; case SNTEventStateBlockScope: return "SCOPE"; case SNTEventStateBlockTeamID: return "TEAMID"; case SNTEventStateBlockSigningID: return "SIGNINGID"; + case SNTEventStateBlockCDHash: return "CDHASH"; case SNTEventStateBlockLongPath: return "LONG_PATH"; case SNTEventStateBlockUnknown: return "UNKNOWN"; default: return "NOTRUNNING"; diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm b/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm index 4f59b81c0..5a2f22061 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm @@ -436,6 +436,7 @@ - (void)testGetReasonString { {SNTEventStateBlockScope, "SCOPE"}, {SNTEventStateBlockTeamID, "TEAMID"}, {SNTEventStateBlockSigningID, "SIGNINGID"}, + {SNTEventStateBlockCDHash, "CDHash"}, {SNTEventStateBlockLongPath, "LONG_PATH"}, {SNTEventStateAllowUnknown, "UNKNOWN"}, {SNTEventStateAllowBinary, "BINARY"}, @@ -446,6 +447,7 @@ - (void)testGetReasonString { {SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"}, {SNTEventStateAllowTeamID, "TEAMID"}, {SNTEventStateAllowSigningID, "SIGNINGID"}, + {SNTEventStateAllowCDHash, "CDHash"}, }; for (const auto &kv : stateToReason) { diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm b/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm index ab402a28b..b5c964c7c 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm @@ -299,12 +299,14 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info, case SNTEventStateAllowScope: return ::pbv1::Execution::REASON_SCOPE; case SNTEventStateAllowTeamID: return ::pbv1::Execution::REASON_TEAM_ID; case SNTEventStateAllowSigningID: return ::pbv1::Execution::REASON_SIGNING_ID; + case SNTEventStateAllowCDHash: return ::pbv1::Execution::REASON_CDHASH; case SNTEventStateAllowUnknown: return ::pbv1::Execution::REASON_UNKNOWN; case SNTEventStateBlockBinary: return ::pbv1::Execution::REASON_BINARY; case SNTEventStateBlockCertificate: return ::pbv1::Execution::REASON_CERT; case SNTEventStateBlockScope: return ::pbv1::Execution::REASON_SCOPE; case SNTEventStateBlockTeamID: return ::pbv1::Execution::REASON_TEAM_ID; case SNTEventStateBlockSigningID: return ::pbv1::Execution::REASON_SIGNING_ID; + case SNTEventStateBlockCDHash: return ::pbv1::Execution::REASON_CDHASH; case SNTEventStateBlockLongPath: return ::pbv1::Execution::REASON_LONG_PATH; case SNTEventStateBlockUnknown: return ::pbv1::Execution::REASON_UNKNOWN; default: return ::pbv1::Execution::REASON_NOT_RUNNING; diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm b/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm index 3ebd94036..6cf1c620f 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm @@ -443,6 +443,7 @@ - (void)testGetReasonEnum { {SNTEventStateBlockScope, ::pbv1::Execution::REASON_SCOPE}, {SNTEventStateBlockTeamID, ::pbv1::Execution::REASON_TEAM_ID}, {SNTEventStateBlockSigningID, ::pbv1::Execution::REASON_SIGNING_ID}, + {SNTEventStateBlockCDHash, ::pbv1::Execution::REASON_CDHASH}, {SNTEventStateBlockLongPath, ::pbv1::Execution::REASON_LONG_PATH}, {SNTEventStateAllowUnknown, ::pbv1::Execution::REASON_UNKNOWN}, {SNTEventStateAllowBinary, ::pbv1::Execution::REASON_BINARY}, @@ -453,6 +454,7 @@ - (void)testGetReasonEnum { {SNTEventStateAllowPendingTransitive, ::pbv1::Execution::REASON_PENDING_TRANSITIVE}, {SNTEventStateAllowTeamID, ::pbv1::Execution::REASON_TEAM_ID}, {SNTEventStateAllowSigningID, ::pbv1::Execution::REASON_SIGNING_ID}, + {SNTEventStateAllowCDHash, ::pbv1::Execution::REASON_CDHASH}, }; for (const auto &kv : stateToReason) { diff --git a/Source/santad/SNTDaemonControlController.mm b/Source/santad/SNTDaemonControlController.mm index e226e888c..1fa63f433 100644 --- a/Source/santad/SNTDaemonControlController.mm +++ b/Source/santad/SNTDaemonControlController.mm @@ -107,6 +107,7 @@ - (void)databaseRuleCounts:(void (^)(RuleCounts ruleTypeCounts))reply { .transitive = [rdb transitiveRuleCount], .teamID = [rdb teamIDRuleCount], .signingID = [rdb signingIDRuleCount], + .cdhash = [rdb cdhashRuleCount], }; reply(ruleCounts); @@ -177,17 +178,9 @@ - (void)retrieveAllRules:(void (^)(NSArray *, NSError *))reply { #pragma mark Decision Ops - (void)decisionForFilePath:(NSString *)filePath - fileSHA256:(NSString *)fileSHA256 - certificateSHA256:(NSString *)certificateSHA256 - teamID:(NSString *)teamID - signingID:(NSString *)signingID + identifiers:(SNTRuleIdentifiers *)identifiers reply:(void (^)(SNTEventState))reply { - reply([self.policyProcessor decisionForFilePath:filePath - fileSHA256:fileSHA256 - certificateSHA256:certificateSHA256 - teamID:teamID - signingID:signingID] - .decision); + reply([self.policyProcessor decisionForFilePath:filePath identifiers:identifiers].decision); } #pragma mark Config Ops diff --git a/Source/santad/SNTExecutionController.h b/Source/santad/SNTExecutionController.h index 1e95799c2..36af267bf 100644 --- a/Source/santad/SNTExecutionController.h +++ b/Source/santad/SNTExecutionController.h @@ -26,6 +26,8 @@ const static NSString *kBlockTeamID = @"BlockTeamID"; const static NSString *kAllowTeamID = @"AllowTeamID"; const static NSString *kBlockSigningID = @"BlockSigningID"; const static NSString *kAllowSigningID = @"AllowSigningID"; +const static NSString *kBlockCDHash = @"BlockCDHash"; +const static NSString *kAllowCDHash = @"AllowCDHash"; const static NSString *kBlockScope = @"BlockScope"; const static NSString *kAllowScope = @"AllowScope"; const static NSString *kAllowUnknown = @"AllowUnknown"; diff --git a/Source/santad/SNTExecutionController.mm b/Source/santad/SNTExecutionController.mm index b45874d3a..6bec271c7 100644 --- a/Source/santad/SNTExecutionController.mm +++ b/Source/santad/SNTExecutionController.mm @@ -162,6 +162,8 @@ - (void)incrementEventCounters:(SNTEventState)eventType { case SNTEventStateAllowTeamID: eventTypeStr = kAllowTeamID; break; case SNTEventStateBlockSigningID: eventTypeStr = kBlockSigningID; break; case SNTEventStateAllowSigningID: eventTypeStr = kAllowSigningID; break; + case SNTEventStateBlockCDHash: eventTypeStr = kBlockCDHash; break; + case SNTEventStateAllowCDHash: eventTypeStr = kAllowCDHash; break; case SNTEventStateBlockScope: eventTypeStr = kBlockScope; break; case SNTEventStateAllowScope: eventTypeStr = kAllowScope; break; case SNTEventStateBlockUnknown: eventTypeStr = kBlockUnknown; break; @@ -326,6 +328,7 @@ - (void)validateExecEvent:(const Message &)esMsg postAction:(bool (^)(SNTAction) se.signingChain = cd.certChain; se.teamID = cd.teamID; se.signingID = cd.signingID; + se.cdhash = cd.cdhash; se.pid = @(audit_token_to_pid(targetProc->audit_token)); se.ppid = @(audit_token_to_pid(targetProc->parent_audit_token)); se.parentName = @(esMsg.ParentProcessName().c_str()); diff --git a/Source/santad/SNTExecutionControllerTest.mm b/Source/santad/SNTExecutionControllerTest.mm index 5ee55d92c..e239dbe57 100644 --- a/Source/santad/SNTExecutionControllerTest.mm +++ b/Source/santad/SNTExecutionControllerTest.mm @@ -202,6 +202,7 @@ - (void)validateExecEvent:(SNTAction)wantAction }); es_process_t procExec = MakeESProcess(&fileExec); procExec.is_platform_binary = false; + procExec.codesigning_flags = CS_SIGNED | CS_VALID; es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc); esMsg.event.exec.target = &procExec; @@ -228,12 +229,20 @@ - (void)stubRule:(SNTRule *)rule forIdentifiers:(struct RuleIdentifiers)wantIden SNTRule *myRule = [[SNTRule alloc] init]; myRule.state = rule.state; myRule.type = rule.type; + + // Most tests do not set a specific cdhash, however an es_process_t created + // from MakeESProcess will have an all zeroes cdhash, not nil. To alleviate + // the need for all tests to specifiy the default cdhash, fix it up here. + static NSString *const zeroCDHash = RepeatedString(@"0", CS_CDHASH_LEN * 2); + wantIdentifiers.cdhash = wantIdentifiers.cdhash ?: zeroCDHash; + OCMStub([self.mockRuleDatabase ruleForIdentifiers:wantIdentifiers]) .ignoringNonObjectArgs() .andDo(^(NSInvocation *inv) { struct RuleIdentifiers gotIdentifiers = {}; [inv getArgument:&gotIdentifiers atIndex:2]; + XCTAssertEqualObjects(gotIdentifiers.cdhash, wantIdentifiers.cdhash); XCTAssertEqualObjects(gotIdentifiers.binarySHA256, wantIdentifiers.binarySHA256); XCTAssertEqualObjects(gotIdentifiers.signingID, wantIdentifiers.signingID); XCTAssertEqualObjects(gotIdentifiers.certificateSHA256, wantIdentifiers.certificateSHA256); @@ -270,6 +279,68 @@ - (void)testBinaryBlockRule { [self checkMetricCounters:kBlockBinary expected:@1]; } +- (void)testCDHashAllowRule { + SNTRule *rule = [[SNTRule alloc] init]; + rule.state = SNTRuleStateAllow; + rule.type = SNTRuleTypeCDHash; + + [self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}]; + + [self validateExecEvent:SNTActionRespondAllow + messageSetup:^(es_message_t *msg) { + msg->event.exec.target->cdhash[0] = 0xaa; + }]; + [self checkMetricCounters:kAllowCDHash expected:@1]; +} + +- (void)testCDHashBlockRule { + SNTRule *rule = [[SNTRule alloc] init]; + rule.state = SNTRuleStateBlock; + rule.type = SNTRuleTypeCDHash; + + [self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}]; + + [self validateExecEvent:SNTActionRespondDeny + messageSetup:^(es_message_t *msg) { + msg->event.exec.target->cdhash[0] = 0xaa; + }]; + [self checkMetricCounters:kBlockCDHash expected:@1]; +} + +- (void)testCDHashAllowCompilerRule { + OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(YES); + + SNTRule *rule = [[SNTRule alloc] init]; + rule.state = SNTRuleStateAllowCompiler; + rule.type = SNTRuleTypeCDHash; + + [self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}]; + + [self validateExecEvent:SNTActionRespondAllowCompiler + messageSetup:^(es_message_t *msg) { + msg->event.exec.target->cdhash[0] = 0xaa; + }]; + + [self checkMetricCounters:kAllowCompiler expected:@1]; +} + +- (void)testCDHashAllowCompilerRuleTransitiveRuleDisabled { + OCMStub([self.mockConfigurator enableTransitiveRules]).andReturn(NO); + + SNTRule *rule = [[SNTRule alloc] init]; + rule.state = SNTRuleStateAllowCompiler; + rule.type = SNTRuleTypeCDHash; + + [self stubRule:rule forIdentifiers:{.cdhash = @"aa00000000000000000000000000000000000000"}]; + + [self validateExecEvent:SNTActionRespondAllow + messageSetup:^(es_message_t *msg) { + msg->event.exec.target->cdhash[0] = 0xaa; + }]; + + [self checkMetricCounters:kAllowCDHash expected:@1]; +} + - (void)testSigningIDAllowRule { SNTRule *rule = [[SNTRule alloc] init]; rule.state = SNTRuleStateAllow; diff --git a/Source/santad/SNTPolicyProcessor.h b/Source/santad/SNTPolicyProcessor.h index 10170a4e9..6cefe5723 100644 --- a/Source/santad/SNTPolicyProcessor.h +++ b/Source/santad/SNTPolicyProcessor.h @@ -17,6 +17,7 @@ #import #import "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTRuleIdentifiers.h" @class MOLCodesignChecker; @class SNTCachedDecision; @@ -57,9 +58,6 @@ /// calculated, use the fileSHA256 parameter to save a second calculation of the hash. /// - (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath - fileSHA256:(nullable NSString *)fileSHA256 - certificateSHA256:(nullable NSString *)certificateSHA256 - teamID:(nullable NSString *)teamID - signingID:(nullable NSString *)signingID; + identifiers:(nonnull SNTRuleIdentifiers *)identifiers; @end diff --git a/Source/santad/SNTPolicyProcessor.m b/Source/santad/SNTPolicyProcessor.m index c5ae8211b..fb1372be5 100644 --- a/Source/santad/SNTPolicyProcessor.m +++ b/Source/santad/SNTPolicyProcessor.m @@ -45,6 +45,7 @@ - (instancetype)initWithRuleTable:(SNTRuleTable *)ruleTable { } - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo + cdhash:(nullable NSString *)cdhash fileSHA256:(nullable NSString *)fileSHA256 certificateSHA256:(nullable NSString *)certificateSHA256 teamID:(nullable NSString *)teamID @@ -54,6 +55,7 @@ - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileIn (NSDictionary *_Nullable (^_Nullable)( NSDictionary *_Nullable entitlements))entitlementsFilterCallback { SNTCachedDecision *cd = [[SNTCachedDecision alloc] init]; + cd.cdhash = cdhash; cd.sha256 = fileSHA256 ?: fileInfo.SHA256; cd.teamID = teamID; cd.signingID = signingID; @@ -74,7 +76,8 @@ - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileIn cd.certSHA256 = certificateSHA256; } else { // Grab the code signature, if there's an error don't try to capture - // any of the signature details. + // any of the signature details. Also clear out any rule lookup parameters + // that would require being validly signed. MOLCodesignChecker *csInfo = [fileInfo codesignCheckerWithError:&csInfoError]; if (csInfoError) { csInfo = nil; @@ -82,6 +85,7 @@ - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileIn [NSString stringWithFormat:@"Signature ignored due to error: %ld", (long)csInfoError.code]; cd.teamID = nil; cd.signingID = nil; + cd.cdhash = nil; } else { cd.certSHA256 = csInfo.leafCertificate.SHA256; cd.certCommonName = csInfo.leafCertificate.commonName; @@ -128,12 +132,35 @@ - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileIn } SNTRule *rule = - [self.ruleTable ruleForIdentifiers:(struct RuleIdentifiers){.binarySHA256 = cd.sha256, + [self.ruleTable ruleForIdentifiers:(struct RuleIdentifiers){.cdhash = cd.cdhash, + .binarySHA256 = cd.sha256, .signingID = cd.signingID, .certificateSHA256 = cd.certSHA256, .teamID = cd.teamID}]; if (rule) { switch (rule.type) { + case SNTRuleTypeCDHash: + switch (rule.state) { + case SNTRuleStateAllow: cd.decision = SNTEventStateAllowCDHash; return cd; + case SNTRuleStateAllowCompiler: + // If transitive rules are enabled, then SNTRuleStateAllowListCompiler rules + // become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if + // it were SNTRuleStateAllowCDHash. + if ([self.configurator enableTransitiveRules]) { + cd.decision = SNTEventStateAllowCompiler; + } else { + cd.decision = SNTEventStateAllowCDHash; + } + return cd; + case SNTRuleStateSilentBlock: + cd.silentBlock = YES; + // intentional fallthrough + case SNTRuleStateBlock: + cd.customMsg = rule.customMsg; + cd.decision = SNTEventStateBlockCDHash; + return cd; + default: break; + } case SNTRuleTypeBinary: switch (rule.state) { case SNTRuleStateAllow: cd.decision = SNTEventStateAllowBinary; return cd; @@ -260,25 +287,38 @@ - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileIn NSDictionary *_Nullable entitlements))entitlementsFilterCallback { NSString *signingID; NSString *teamID; + NSString *cdhash; const char *entitlementsFilterTeamID = NULL; - if (targetProc->signing_id.length > 0) { - if (targetProc->team_id.length > 0) { - entitlementsFilterTeamID = targetProc->team_id.data; - teamID = [NSString stringWithUTF8String:targetProc->team_id.data]; - signingID = - [NSString stringWithFormat:@"%@:%@", teamID, - [NSString stringWithUTF8String:targetProc->signing_id.data]]; - } else if (targetProc->is_platform_binary) { - entitlementsFilterTeamID = "platform"; - signingID = - [NSString stringWithFormat:@"platform:%@", - [NSString stringWithUTF8String:targetProc->signing_id.data]]; + if (targetProc->codesigning_flags & CS_SIGNED && targetProc->codesigning_flags & CS_VALID) { + if (targetProc->signing_id.length > 0) { + if (targetProc->team_id.length > 0) { + entitlementsFilterTeamID = targetProc->team_id.data; + teamID = [NSString stringWithUTF8String:targetProc->team_id.data]; + signingID = + [NSString stringWithFormat:@"%@:%@", teamID, + [NSString stringWithUTF8String:targetProc->signing_id.data]]; + } else if (targetProc->is_platform_binary) { + entitlementsFilterTeamID = "platform"; + signingID = + [NSString stringWithFormat:@"platform:%@", + [NSString stringWithUTF8String:targetProc->signing_id.data]]; + } } + + static NSString *const kCDHashFormatString = @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"; + + const uint8_t *buf = targetProc->cdhash; + cdhash = [[NSString alloc] initWithFormat:kCDHashFormatString, buf[0], buf[1], buf[2], buf[3], + buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], + buf[10], buf[11], buf[12], buf[13], buf[14], buf[15], + buf[16], buf[17], buf[18], buf[19]]; } return [self decisionForFileInfo:fileInfo + cdhash:cdhash fileSHA256:nil certificateSHA256:nil teamID:teamID @@ -293,10 +333,7 @@ - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileIn // Used by `$ santactl fileinfo`. - (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath - fileSHA256:(nullable NSString *)fileSHA256 - certificateSHA256:(nullable NSString *)certificateSHA256 - teamID:(nullable NSString *)teamID - signingID:(nullable NSString *)signingID { + identifiers:(nonnull SNTRuleIdentifiers *)identifiers { MOLCodesignChecker *csInfo; NSError *error; @@ -311,10 +348,11 @@ - (nonnull SNTCachedDecision *)decisionForFilePath:(nonnull NSString *)filePath } return [self decisionForFileInfo:fileInfo - fileSHA256:fileSHA256 - certificateSHA256:certificateSHA256 - teamID:teamID - signingID:signingID + cdhash:identifiers.cdhash + fileSHA256:identifiers.binarySHA256 + certificateSHA256:identifiers.certificateSHA256 + teamID:identifiers.teamID + signingID:identifiers.signingID isProdSignedCallback:^BOOL { if (csInfo) { // Development OID values defined by Apple and used by the Security Framework diff --git a/Source/santad/SantadTest.mm b/Source/santad/SantadTest.mm index afbfcba3c..fb0237ad5 100644 --- a/Source/santad/SantadTest.mm +++ b/Source/santad/SantadTest.mm @@ -21,7 +21,9 @@ #import #include #include +#include +#include #include #import "Source/common/SNTCachedDecision.h" @@ -39,12 +41,43 @@ using santa::santad::SantadDeps; using santa::santad::event_providers::endpoint_security::Message; -NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules"; +static int HexCharToInt(char hex) { + if (hex >= '0' && hex <= '9') { + return hex - '0'; + } else if (hex >= 'A' && hex <= 'F') { + return hex - 'A' + 10; + } else if (hex >= 'a' && hex <= 'f') { + return hex - 'a' + 10; + } else { + return -1; + } +} +void SetBinaryDataFromHexString(const char *hexStr, uint8_t *buf, size_t bufLen) { + assert(hexStr != NULL); + size_t hexStrLen = strlen(hexStr); + assert(hexStrLen > 0); + assert(hexStrLen % 2 == 0); + assert(hexStrLen / 2 == bufLen); + + for (size_t i = 0; i < hexStrLen; i += 2) { + int upper = HexCharToInt(hexStr[i]); + int lower = HexCharToInt(hexStr[i + 1]); + + assert(upper != -1); + assert(lower != -1); + + buf[i / 2] = (uint8_t)(upper << 4) | lower; + } +} + +static NSString *const testBinariesPath = @"santa/Source/santad/testdata/binaryrules"; static const char *kAllowedSigningID = "com.google.allowed_signing_id"; static const char *kBlockedSigningID = "com.google.blocked_signing_id"; static const char *kNoRuleMatchSigningID = "com.google.no_rule_match_signing_id"; static const char *kBlockedTeamID = "EQHXZ8M8AV"; static const char *kAllowedTeamID = "TJNVEKW352"; +static const char *kAllowedCDHash = "dedebf2eac732d873008b17b3e44a56599dd614b"; +static const char *kBlockedCDHash = "7218eddfee4d3eba4873dedf22d1391d79aea25f"; @interface SNTEndpointSecurityClient (Testing) @property(nonatomic) double defaultBudget; @@ -92,9 +125,8 @@ - (BOOL)checkBinaryExecution:(NSString *)binaryName OCMStub([mockConfigurator failClosed]).andReturn(NO); OCMStub([mockConfigurator fileAccessPolicyUpdateIntervalSec]).andReturn(600); - NSString *baseTestPath = @"santa/Source/santad/testdata/binaryrules"; NSString *testPath = [NSString pathWithComponents:@[ - [[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], baseTestPath + [[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testBinariesPath ]]; OCMStub([self.mockSNTDatabaseController databasePath]).andReturn(testPath); @@ -121,10 +153,11 @@ - (BOOL)checkBinaryExecution:(NSString *)binaryName NSString *binaryPath = [[NSString pathWithComponents:@[ testPath, binaryName ]] stringByResolvingSymlinksInPath]; struct stat fileStat; - lstat(binaryPath.UTF8String, &fileStat); + XCTAssertEqual(lstat(binaryPath.UTF8String, &fileStat), 0); es_file_t file = MakeESFile([binaryPath UTF8String], fileStat); es_process_t proc = MakeESProcess(&file); proc.is_platform_binary = false; + proc.codesigning_flags = CS_SIGNED | CS_VALID; // Set a 6.5 second deadline for the message and clamp deadline headroom to 5 // seconds. This means there is a 1.5 second leeway given for the processing block @@ -369,6 +402,58 @@ - (void)testBinaryWithSigningIDAllowRuleIsAllowedInLockdownMode { }]; } +- (void)testBinaryWithCDHashBlockRuleIsBlockedInLockdownMode { + [self checkBinaryExecution:@"banned_cdhash" + wantResult:ES_AUTH_RESULT_DENY + clientMode:SNTClientModeLockdown + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateBlockCDHash; + } + messageSetup:^(es_message_t *msg) { + SetBinaryDataFromHexString(kBlockedCDHash, msg->event.exec.target->cdhash, + sizeof(msg->event.exec.target->cdhash)); + }]; +} + +- (void)testBinaryWithCDHashBlockRuleIsBlockedInMonitorMode { + [self checkBinaryExecution:@"banned_cdhash" + wantResult:ES_AUTH_RESULT_DENY + clientMode:SNTClientModeMonitor + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateBlockCDHash; + } + messageSetup:^(es_message_t *msg) { + SetBinaryDataFromHexString(kBlockedCDHash, msg->event.exec.target->cdhash, + sizeof(msg->event.exec.target->cdhash)); + }]; +} + +- (void)testBinaryWithCDHashAllowRuleIsAllowedInMonitorMode { + [self checkBinaryExecution:@"allowed_cdhash" + wantResult:ES_AUTH_RESULT_ALLOW + clientMode:SNTClientModeMonitor + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateAllowCDHash; + } + messageSetup:^(es_message_t *msg) { + SetBinaryDataFromHexString(kAllowedCDHash, msg->event.exec.target->cdhash, + sizeof(msg->event.exec.target->cdhash)); + }]; +} + +- (void)testBinaryWithCDHashAllowRuleIsAllowedInLockdownMode { + [self checkBinaryExecution:@"allowed_cdhash" + wantResult:ES_AUTH_RESULT_ALLOW + clientMode:SNTClientModeMonitor + cdValidator:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateAllowCDHash; + } + messageSetup:^(es_message_t *msg) { + SetBinaryDataFromHexString(kAllowedCDHash, msg->event.exec.target->cdhash, + sizeof(msg->event.exec.target->cdhash)); + }]; +} + - (void)testBinaryWithSHA256AllowRuleAndBlockedTeamIDRuleIsAllowedInLockdownMode { [self checkBinaryExecution:@"banned_teamid_allowed_binary" wantResult:ES_AUTH_RESULT_ALLOW diff --git a/Source/santad/testdata/binaryrules/allowed_cdhash b/Source/santad/testdata/binaryrules/allowed_cdhash new file mode 100755 index 0000000000000000000000000000000000000000..170d6d88b082c291525d104b2b47df7110acd183 GIT binary patch literal 33440 zcmeI5U1%It6vyw*Zc5rslT@ns*^E9EwIvZP+J_c5HNmDzv!e)kQH|!QCWlcy-nY zk>~pODW|Bp!;MoE-$pSnyPCW}G4JPVYHy}CNFFBl8qa0->U+nz|5^U8dDqjJPR{Q| zOU|)d(q^nBF_2DB%yu0)*KzjQpOzf^;;t7@PH&3_dI#%or<_h+yeFw>^{UUc3g4Xn zR%+D#I3}ao^Q^<~x!3rWsyr?r00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z@Lv*`pAeI?gWAaKJ~3P?@?B>e#EJJOn#KzIn#O(%iVJkF=R|1zFdd>ja=IaOVs>!D z2(1|&_sc0d5Bqkb7P=s`rm60fVH>)W%H+}sJ!a@uMmKhDPsdF&tJ}G>;pnEN8)lZK z=?T*|;#o6e>B)?(Q;Mb<@tn$Jl9VtVJ!V=_d!Tq8_hy^4Z@Iwd{O8oh_uU z3l}~=Tu0}B8`WuIl@Bb8ioB`%$!KOPt|WkeH(Ef)7jO-n2lYC>sxgHL^NT-9|IB^#(f{?7xh zlm82W+H%P(&#u!K=`WR+O5R)A#qaCJY55C98M+FwoN^@Lds=9;drispQl9a!Ef!*r z&^7gG_hROsuj|}lUo@T0>@X5Co=8QVlsFu0{@|m%^V5adZHN;4{(Q)le^MRFT zQiuAEecN;VttZ}mCzAQ_=j3?ctI5enw9ZJ){O-}+yN13wd2r)_)vsM{{Jj+;nJ1?M zSN`$cuHR-pdG(FpV|}rgR((7*5nR$`uR5~khf}{ib^iIJ|K-(-?wpA?CC@Jnj(z#- zE1y5K_w<>=+UU8<>HpjDXPld2{?Qfh&K>G396z<`_pQsP`@#9s8pm%LqJ+d;yHI_H?v!{4+_5g z51hI8oO|zo&i&2oNBVR_Y2ZR_BI`xFOj(j7zl;PI<+k4u(IyqZu zYH?-ivWRu|(?lt|+aukT!)kq~GAG=)hSKa*k}@fC#*UmDP*bf}d#|&Zb?y|7d0%A< zh(ftf9VxT9d@Nf|RqM66dc;T>$6mc!SFbcKWjvd+O{>?PU#%B*^&WP|Irf@YoAXlU zttU(?Ax$fpQE?@|*Im7cE5dQLhC8Ghehx>;rEF{MZk6|SZr{{|7#k`-dslAojAbFVFYdn|TtM47>{%3{1=3P%?Iyt`= zE%~h7k~U*4iGg&2VzwK|xsG$s{j}ux7xx@HHN7nw=pC%TlX5z_b5Byy>Q$d>6}~zD z?bN9KaZEow=S67!Fdd>ja;714a&~aT z2(1|&_sc0d5Bqkb7P=_3rm60fVHi090Vr6)7CPAQsd#PceXNm9bh>M_%b+5^sc;2Yfyds{x+%V&Q>)UxyWbhePX zE?oHha2=ihZB(a;RX(sVDhi%Hx*z}oAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00RF50iEs{CXbNwC%wI&_4fWr0DnuoWU3#cuegUw8F#Jb#&WxDTDfF- z)F;JN9Pqeb?0<@SvOguAe>^Z`%7`WmTP*I6nwD59)P&OX2A}%kxT@u@Jk3 zuBlJEJDGpJu5*Wdv8ZJk2^mkMqS=%<9BlsJqdoJ}#o29*k8kQaSkuvQ{Y>+Lm1k3j z`i_6wbKP2_Y#G8^AmIlYZ{PmU3 zAK7!}>|t&6{FU_o?fA3VTVnpv74Oa+>MWi(z3KO@%cuLo4-NG!dvtQ^&2}>3Q2P^t C1z;-x literal 0 HcmV?d00001 diff --git a/Source/santad/testdata/binaryrules/rules.db b/Source/santad/testdata/binaryrules/rules.db index 162a51a89337d4984c871924326ba80268fc820d..a7bc8de49a33dcf56ea7328ccc8fbb2a9bdd70ae 100644 GIT binary patch delta 744 zcma*hId2m|9EaiAwILYq&MsE~C*cT?5CR+TVXw_)Ae`a8X)Lmv854=f5u%_$B8UPi z2trgybTp|#QW#NDBGK>#kdSDJgoJCv3p5c`if5|%zxqvm_-0OFy2}WjcMrRP@S5*! zA%rOxgj{f5-pQ3((=*<)?Be%@`)hFizI)U1wr&KwXzX{FOfpp&sU3{)i!i^WRn$7!geC@Gao)v8iuKT3^uGBGiE zPpg|_YCO3+UdwmX4YOZ}IPfc4sfN~(3U!)9G>A5WK>1*u#XjuE0USgPhj182FpQ%(hT}MalQ@MD eoW>cP#W|eE1zf}>T*eh##Wh@Sx!#Q*Z+`>Ee9f2u delta 512 zcmZ9`IWq%c0LJmnCXN;R?(14d5=Rh6Si!~-w>aazbrd><(JNvUR1#y1!i-Lnib)Zr z(u~FzP-$hxCoulHjNs~g38qiPg~QZzTQMGtn79;Q;y{@CtG=(>nbVYYpR>q1)3)zjH8mj>;WwVehjM2?qXH%>QH5&M zzymLQs6`#>(EvXh(S&9M(1KR9p&dbVpc8+;yAVP*de93SVf3LN0~kaELm0*gMlptQ pOkfgGOko-`n8h6C5yJu&v4mx;U=?dv#|AdBg>CF)q_z9~^b03-iOc{1 diff --git a/Source/santasyncservice/SNTSyncEventUpload.m b/Source/santasyncservice/SNTSyncEventUpload.m index d13075cf9..2389211d6 100644 --- a/Source/santasyncservice/SNTSyncEventUpload.m +++ b/Source/santasyncservice/SNTSyncEventUpload.m @@ -104,6 +104,7 @@ - (NSDictionary *)dictionaryForEvent:(SNTStoredEvent *)event { case SNTEventStateAllowScope: ADDKEY(newEvent, kDecision, kDecisionAllowScope); break; case SNTEventStateAllowTeamID: ADDKEY(newEvent, kDecision, kDecisionAllowTeamID); break; case SNTEventStateAllowSigningID: ADDKEY(newEvent, kDecision, kDecisionAllowSigningID); break; + case SNTEventStateAllowCDHash: ADDKEY(newEvent, kDecision, kDecisionAllowCDHash); break; case SNTEventStateBlockUnknown: ADDKEY(newEvent, kDecision, kDecisionBlockUnknown); break; case SNTEventStateBlockBinary: ADDKEY(newEvent, kDecision, kDecisionBlockBinary); break; case SNTEventStateBlockCertificate: @@ -112,6 +113,7 @@ - (NSDictionary *)dictionaryForEvent:(SNTStoredEvent *)event { case SNTEventStateBlockScope: ADDKEY(newEvent, kDecision, kDecisionBlockScope); break; case SNTEventStateBlockTeamID: ADDKEY(newEvent, kDecision, kDecisionBlockTeamID); break; case SNTEventStateBlockSigningID: ADDKEY(newEvent, kDecision, kDecisionBlockSigningID); break; + case SNTEventStateBlockCDHash: ADDKEY(newEvent, kDecision, kDecisionBlockCDHash); break; case SNTEventStateBundleBinary: ADDKEY(newEvent, kDecision, kDecisionBundleBinary); [newEvent removeObjectForKey:kExecutionTime]; @@ -155,6 +157,7 @@ - (NSDictionary *)dictionaryForEvent:(SNTStoredEvent *)event { newEvent[kSigningChain] = signingChain; ADDKEY(newEvent, kTeamID, event.teamID); ADDKEY(newEvent, kSigningID, event.signingID); + ADDKEY(newEvent, kCDHash, event.cdhash); return newEvent; #undef ADDKEY diff --git a/Source/santasyncservice/SNTSyncPreflight.m b/Source/santasyncservice/SNTSyncPreflight.m index 43b8e3b7e..ccd8e6999 100644 --- a/Source/santasyncservice/SNTSyncPreflight.m +++ b/Source/santasyncservice/SNTSyncPreflight.m @@ -95,6 +95,7 @@ - (BOOL)sync { requestDict[kTransitiveRuleCount] = @(counts.transitive); requestDict[kTeamIDRuleCount] = @(counts.teamID); requestDict[kSigningIDRuleCount] = @(counts.signingID); + requestDict[kCDHashRuleCount] = @(counts.cdhash); }]; [rop clientMode:^(SNTClientMode cm) { diff --git a/Source/santasyncservice/SNTSyncTest.m b/Source/santasyncservice/SNTSyncTest.m index 798ef14d3..788303078 100644 --- a/Source/santasyncservice/SNTSyncTest.m +++ b/Source/santasyncservice/SNTSyncTest.m @@ -346,6 +346,7 @@ - (void)testPreflightDatabaseCounts { SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState]; struct RuleCounts ruleCounts = { + .cdhash = 11, .binary = 5, .certificate = 8, .compiler = 2, @@ -362,6 +363,7 @@ - (void)testPreflightDatabaseCounts { error:nil validateBlock:^BOOL(NSURLRequest *req) { NSDictionary *requestDict = [self dictFromRequest:req]; + XCTAssertEqualObjects(requestDict[kCDHashRuleCount], @(ruleCounts.cdhash)); XCTAssertEqualObjects(requestDict[kBinaryRuleCount], @(ruleCounts.binary)); XCTAssertEqualObjects(requestDict[kCertificateRuleCount], @(ruleCounts.certificate)); XCTAssertEqualObjects(requestDict[kCompilerRuleCount], @(ruleCounts.compiler)); @@ -591,6 +593,7 @@ - (void)testEventUploadBasic { XCTAssertEqualObjects(event[kTeamID], @"012345678910"); XCTAssertEqualObjects(event[kSigningID], @"signing.id"); + XCTAssertEqualObjects(event[kCDHash], @"abc123"); event = events[1]; XCTAssertEqualObjects(event[kFileName], @"hub"); diff --git a/Source/santasyncservice/testdata/sync_eventupload_input_basic.plist b/Source/santasyncservice/testdata/sync_eventupload_input_basic.plist index d1fbf8c62..5827ebc08 100644 --- a/Source/santasyncservice/testdata/sync_eventupload_input_basic.plist +++ b/Source/santasyncservice/testdata/sync_eventupload_input_basic.plist @@ -101,6 +101,11 @@ CF$UID 40 + cdhash + + CF$UID + 41 + 14887 ff98fa0c0a1095fedcbe4d388a9760e71399a5c3c017a847ffa545663b57929a @@ -405,6 +410,7 @@ 012345678910 signing.id + abc123 $top From 106c7881fc28a3112a94cf894a369b676ab92a1d Mon Sep 17 00:00:00 2001 From: Matt White <436037+mlw@users.noreply.github.com> Date: Tue, 5 Mar 2024 00:05:03 -0500 Subject: [PATCH 2/2] Ensure hardened runtime for cdhash eval. Update docs. --- .../Serializers/BasicStringTest.mm | 4 +-- Source/santad/SNTExecutionControllerTest.mm | 31 +++++++++++++------ Source/santad/SNTPolicyProcessor.m | 21 ++++++++----- Source/santad/SantadTest.mm | 2 +- docs/concepts/events.md | 1 + docs/concepts/rules.md | 16 ++++++++-- docs/development/sync-protocol.md | 14 ++++++--- docs/index.md | 2 +- 8 files changed, 62 insertions(+), 29 deletions(-) diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm b/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm index 5a2f22061..1f36f43fd 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm @@ -436,7 +436,7 @@ - (void)testGetReasonString { {SNTEventStateBlockScope, "SCOPE"}, {SNTEventStateBlockTeamID, "TEAMID"}, {SNTEventStateBlockSigningID, "SIGNINGID"}, - {SNTEventStateBlockCDHash, "CDHash"}, + {SNTEventStateBlockCDHash, "CDHASH"}, {SNTEventStateBlockLongPath, "LONG_PATH"}, {SNTEventStateAllowUnknown, "UNKNOWN"}, {SNTEventStateAllowBinary, "BINARY"}, @@ -447,7 +447,7 @@ - (void)testGetReasonString { {SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"}, {SNTEventStateAllowTeamID, "TEAMID"}, {SNTEventStateAllowSigningID, "SIGNINGID"}, - {SNTEventStateAllowCDHash, "CDHash"}, + {SNTEventStateAllowCDHash, "CDHASH"}, }; for (const auto &kv : stateToReason) { diff --git a/Source/santad/SNTExecutionControllerTest.mm b/Source/santad/SNTExecutionControllerTest.mm index e239dbe57..017ab58d9 100644 --- a/Source/santad/SNTExecutionControllerTest.mm +++ b/Source/santad/SNTExecutionControllerTest.mm @@ -226,16 +226,6 @@ - (void)validateExecEvent:(SNTAction)wantAction { } - (void)stubRule:(SNTRule *)rule forIdentifiers:(struct RuleIdentifiers)wantIdentifiers { - SNTRule *myRule = [[SNTRule alloc] init]; - myRule.state = rule.state; - myRule.type = rule.type; - - // Most tests do not set a specific cdhash, however an es_process_t created - // from MakeESProcess will have an all zeroes cdhash, not nil. To alleviate - // the need for all tests to specifiy the default cdhash, fix it up here. - static NSString *const zeroCDHash = RepeatedString(@"0", CS_CDHASH_LEN * 2); - wantIdentifiers.cdhash = wantIdentifiers.cdhash ?: zeroCDHash; - OCMStub([self.mockRuleDatabase ruleForIdentifiers:wantIdentifiers]) .ignoringNonObjectArgs() .andDo(^(NSInvocation *inv) { @@ -289,6 +279,24 @@ - (void)testCDHashAllowRule { [self validateExecEvent:SNTActionRespondAllow messageSetup:^(es_message_t *msg) { msg->event.exec.target->cdhash[0] = 0xaa; + msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD; + }]; + [self checkMetricCounters:kAllowCDHash expected:@1]; +} + +- (void)testCDHashNoHardenedRuntimeRule { + SNTRule *rule = [[SNTRule alloc] init]; + rule.state = SNTRuleStateAllow; + rule.type = SNTRuleTypeCDHash; + + // No CDHash should be set when hardened runtime CS flags are not set + [self stubRule:rule forIdentifiers:{.cdhash = nil}]; + + [self validateExecEvent:SNTActionRespondAllow + messageSetup:^(es_message_t *msg) { + msg->event.exec.target->cdhash[0] = 0xaa; + // Ensure CS_HARD and CS_KILL are not set + msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID; }]; [self checkMetricCounters:kAllowCDHash expected:@1]; } @@ -303,6 +311,7 @@ - (void)testCDHashBlockRule { [self validateExecEvent:SNTActionRespondDeny messageSetup:^(es_message_t *msg) { msg->event.exec.target->cdhash[0] = 0xaa; + msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD; }]; [self checkMetricCounters:kBlockCDHash expected:@1]; } @@ -319,6 +328,7 @@ - (void)testCDHashAllowCompilerRule { [self validateExecEvent:SNTActionRespondAllowCompiler messageSetup:^(es_message_t *msg) { msg->event.exec.target->cdhash[0] = 0xaa; + msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD; }]; [self checkMetricCounters:kAllowCompiler expected:@1]; @@ -336,6 +346,7 @@ - (void)testCDHashAllowCompilerRuleTransitiveRuleDisabled { [self validateExecEvent:SNTActionRespondAllow messageSetup:^(es_message_t *msg) { msg->event.exec.target->cdhash[0] = 0xaa; + msg->event.exec.target->codesigning_flags = CS_SIGNED | CS_VALID | CS_KILL | CS_HARD; }]; [self checkMetricCounters:kAllowCDHash expected:@1]; diff --git a/Source/santad/SNTPolicyProcessor.m b/Source/santad/SNTPolicyProcessor.m index fb1372be5..cf34107d0 100644 --- a/Source/santad/SNTPolicyProcessor.m +++ b/Source/santad/SNTPolicyProcessor.m @@ -307,14 +307,19 @@ - (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileIn } } - static NSString *const kCDHashFormatString = @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" - "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"; - - const uint8_t *buf = targetProc->cdhash; - cdhash = [[NSString alloc] initWithFormat:kCDHashFormatString, buf[0], buf[1], buf[2], buf[3], - buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], - buf[10], buf[11], buf[12], buf[13], buf[14], buf[15], - buf[16], buf[17], buf[18], buf[19]]; + // Only consider the CDHash for processes that have CS_KILL or CS_HARD set. + // This ensures that the OS will kill the process if the CDHash was tampered + // with and code was loaded that didn't match a page hash. + if (targetProc->codesigning_flags & CS_KILL || targetProc->codesigning_flags & CS_HARD) { + static NSString *const kCDHashFormatString = @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"; + + const uint8_t *buf = targetProc->cdhash; + cdhash = [[NSString alloc] initWithFormat:kCDHashFormatString, buf[0], buf[1], buf[2], buf[3], + buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], + buf[10], buf[11], buf[12], buf[13], buf[14], + buf[15], buf[16], buf[17], buf[18], buf[19]]; + } } return [self decisionForFileInfo:fileInfo diff --git a/Source/santad/SantadTest.mm b/Source/santad/SantadTest.mm index fb0237ad5..d91aa6935 100644 --- a/Source/santad/SantadTest.mm +++ b/Source/santad/SantadTest.mm @@ -157,7 +157,7 @@ - (BOOL)checkBinaryExecution:(NSString *)binaryName es_file_t file = MakeESFile([binaryPath UTF8String], fileStat); es_process_t proc = MakeESProcess(&file); proc.is_platform_binary = false; - proc.codesigning_flags = CS_SIGNED | CS_VALID; + proc.codesigning_flags = CS_SIGNED | CS_VALID | CS_HARD | CS_KILL; // Set a 6.5 second deadline for the message and clamp deadline headroom to 5 // seconds. This means there is a 1.5 second leeway given for the processing block diff --git a/docs/concepts/events.md b/docs/concepts/events.md index dc013ead8..755378d20 100644 --- a/docs/concepts/events.md +++ b/docs/concepts/events.md @@ -73,6 +73,7 @@ JSON blob. Here is an example of Firefox being blocked and sent for upload: ], "team_id": "43AQ936H96", "signing_id": "org.mozilla.firefox", + "cdhash": "ac14c49901a9cd05ff7bceea122f534d3c6c6ab7", "file_bundle_name": "Firefox", "executing_user": "bur", "ppid": 1, diff --git a/docs/concepts/rules.md b/docs/concepts/rules.md index 9a726deb9..e6f895be0 100644 --- a/docs/concepts/rules.md +++ b/docs/concepts/rules.md @@ -1,3 +1,4 @@ + --- parent: Concepts --- @@ -9,11 +10,20 @@ parent: Concepts Rules provide the primary evaluation mechanism for allowing and blocking binaries with Santa on macOS. +### CDHash Rules + +CDHash rules use the a binary's code directory hash as an identifier. This is +the most specific rule in Santa. The code directory hash identifies a specific +version of a program, similar to a file hash. Note that the operating system +evaluates the cdhash lazily, only verifying pages of code when they're mapped +in. This means that it is possible for a file hash to change, but a binary could +still execute as long as modified pages are not mapped in. Santa only considers +CDHash rules for processes that have `CS_KILL` or `CS_HARD` codesigning flags +set to ensure that a process will be killed if the CDHash was tampered with. + ### Binary Rules -Binary rules use the SHA-256 hash of the entire binary as an identifier. This is -the most specific rule in Santa. Even a small change in the binary will alter -the SHA-256 hash, invalidating the rule. +Binary rules use the SHA-256 hash of the entire binary file as an identifier. ### Signing ID Rules diff --git a/docs/development/sync-protocol.md b/docs/development/sync-protocol.md index 8b7c7e2b7..6ae80b711 100644 --- a/docs/development/sync-protocol.md +++ b/docs/development/sync-protocol.md @@ -97,6 +97,8 @@ The request consists of the following JSON keys: | compiler_rule_count | NO | int | Number of compiler rules the client has time of sync | | transitive_rule_count | NO | int | Number of transitive rules the client has at the time of sync | | teamid_rule_count | NO | int | Number of TeamID rules the client has at the time of sync | 24 | +| signingid_rule_count | NO | int | Number of SigningID rules the client has at the time of sync | 11 | +| cdhash_rule_count | NO | int | Number of CDHash rules the client has at the time of sync | 22 | | client_mode | YES | string | The mode the client is operating in, either "LOCKDOWN" or "MONITOR" | LOCKDOWN | | request_clean_sync | NO | bool | The client has requested a clean sync of its rules from the server | true | @@ -114,6 +116,8 @@ The request consists of the following JSON keys: "primary_user" : "markowsky", "certificate_rule_count" : 2364, "teamid_rule_count" : 0, + "signingid_rule_count" : 12, + "cdhash_rule_count" : 34, "os_build" : "21F5048e", "transitive_rule_count" : 0, "os_version" : "12.4", @@ -208,7 +212,7 @@ sequenceDiagram | execution_time | NO | float64 | Unix timestamp of when the execution occurred | 23344234232 | | loggedin_users | NO | list of strings | List of usernames logged in according to utmp | ["markowsky"] | | current_sessions | NO | list of strings | List of user sessions | ["markowsky@console", "markowsky@ttys000"] | -| decision | YES | string | The decision Santa made for this binary, BUNDLE_BINARY is used to preemptively report binaries in a bundle. **Must be one of the examples** | "ALLOW_BINARY", "ALLOW_CERTIFICATE", "ALLOW_SCOPE", "ALLOW_TEAMID", "ALLOW_UNKNOWN", "BLOCK_BINARY", "BLOCK_CERTIFICATE", "BLOCK_SCOPE", "BLOCK_TEAMID", "BLOCK_UNKNOWN", "BUNDLE_BINARY" | +| decision | YES | string | The decision Santa made for this binary, BUNDLE_BINARY is used to preemptively report binaries in a bundle. **Must be one of the examples** | "ALLOW_BINARY", "ALLOW_CERTIFICATE", "ALLOW_SCOPE", "ALLOW_TEAMID", "ALLOW_SIGNINGID", "ALLOW_CDHASH" "ALLOW_UNKNOWN", "BLOCK_BINARY", "BLOCK_CERTIFICATE", "BLOCK_SCOPE", "BLOCK_TEAMID", "BLOCK_SIGNINGID", "BLOCK_CDHASH", "BLOCK_UNKNOWN", "BUNDLE_BINARY" | | file_bundle_id | NO | string | The executable's containing bundle's identifier as specified in the Info.plist | "com.apple.safari" | | file_bundle_path | NO | string | The path that the bundle resids in | /Applications/Santa.app | | file_bundle_executable_rel_path | NO | string | The relative path of the binary within the Bundle | "Contents/MacOS/AppName" | @@ -228,6 +232,7 @@ sequenceDiagram | signing_chain | NO | list of signing chain objects | Certs used to code sign the executable | See next section | | signing_id | NO | string | Signing ID of the binary that was executed | "EQHXZ8M8AV:com.google.Chrome" | | team_id | NO | string | Team ID of the binary that was executed | "EQHXZ8M8AV" | +| cdhash | NO | string | CDHash of the binary that was executed | "dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a" | #### Signing Chain Objects @@ -296,7 +301,8 @@ sequenceDiagram "markowsky@ttys003" ], "team_id": "EQHXZ8M8AV", - "signing_id": "EQHXZ8M8AV:com.google.santa" + "signing_id": "EQHXZ8M8AV:com.google.santa", + "cdhash": "dbe8c39801f93e05fc7bc53a02af5b4d3cfc670a" }] } ``` @@ -380,9 +386,9 @@ downloading if the rules need to be downloaded in multiple batches. | Key | Required | Type | Meaning | Example Value | |---|---|---|---|---| -| identifier | YES | string | The attribute of the binary the rule should match on e.g. the team ID of a binary or sha256 hash value | "ff2a7daa4c25cbd5b057e4471c6a22aba7d154dadfb5cce139c37cf795f41c9c" | +| identifier | YES | string | The attribute of the binary the rule should match on e.g. the signing ID, team ID, or CDHash of a binary or sha256 hash value | "ff2a7daa4c25cbd5b057e4471c6a22aba7d154dadfb5cce139c37cf795f41c9c" | | policy | YES | string | Identifies the action to perform in response to the rule matching (must be one of the examples) | "ALLOWLIST","ALLOWLIST_COMPILER", "BLOCKLIST", "REMOVE", "SILENT_BLOCKLIST" | -| rule\_type | YES | string | Identifies the type of rule (must be one of the examples) | "BINARY", "CERTIFICATE", "SIGNINGID", "TEAMID" | +| rule\_type | YES | string | Identifies the type of rule (must be one of the examples) | "BINARY", "CERTIFICATE", "SIGNINGID", "TEAMID", "CDHASH" | | custom\_msg | NO | string | A custom message to display when the rule matches | "Hello" | | custom\_url | NO | string | A custom URL to use for the open button when the rule matches | http://lmgtfy.app/?q=dont+download+malware | | creation\_time | NO | float64 | Time the rule was created | 1573543803.349378 | diff --git a/docs/index.md b/docs/index.md index 6177d66e3..85a0b4943 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,7 +15,7 @@ The project and the latest release is available on [**GitHub**](https://github.c * [**Multiple modes:**](concepts/mode.md) In the default `MONITOR` mode, all binaries except those marked as blocked will be allowed to run, whilst being logged and recorded in the events database. In `LOCKDOWN` mode, only listed binaries are allowed to run. * [**Event logging:**](concepts/events.md) All binary launches are logged. When in either mode, all unknown or denied binaries are stored in the database to enable later aggregation. -* [**Several supported rule types:**](concepts/rules.md) Executions can be allowed or denied by specifying rules based on several attributes. The supported rule types, in order of highest to lowest precedence are: binary hash, Signing ID, certificate hash, or Team ID. Since multiple rules can apply to a given binary, Santa will apply the rule with the highest precedence (i.e. you could use a Team ID rule to allow all binaries from some organization, but also add a Signing ID rule to deny a specific binary). Rules based on code signature properties (Signing ID, certificate hash, and Team ID) only apply if a bianry's signature validates correctly. +* [**Several supported rule types:**](concepts/rules.md) Executions can be allowed or denied by specifying rules based on several attributes. The supported rule types, in order of highest to lowest precedence are: CDHash, binary hash, Signing ID, certificate hash, or Team ID. Since multiple rules can apply to a given binary, Santa will apply the rule with the highest precedence (i.e. you could use a Team ID rule to allow all binaries from some organization, but also add a Signing ID rule to deny a specific binary). Rules based on code signature properties (Signing ID, certificate hash, and Team ID) only apply if a bianry's signature validates correctly. * **Path-based rules (via NSRegularExpression/ICU):** Binaries can be allowed/blocked based on the path they are launched from by matching against a configurable regex. * [**Failsafe cert rules:**](concepts/rules.md#built-in-rules) You cannot put in a deny rule that would block the certificate used to sign launchd, a.k.a. pid 1, and therefore all components used in macOS. The binaries in every OS update (and in some cases entire new versions) are therefore automatically allowed. This does not affect binaries from Apple's App Store, which use various certs that change regularly for common apps. Likewise, you cannot block Santa itself. * [**Components validate each other:**](binaries/index.md) Each of the components (the daemons, the GUI agent, and the command-line utility) communicate with each other using XPC and check that their signing certificates are identical before any communication is accepted.