diff --git a/Santa.xcodeproj/project.pbxproj b/Santa.xcodeproj/project.pbxproj index 7e55512c0..1878f16c8 100644 --- a/Santa.xcodeproj/project.pbxproj +++ b/Santa.xcodeproj/project.pbxproj @@ -149,6 +149,8 @@ 59D56CF2D9C5BD9B7E3CC56D /* libPods-santad-santabs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B98F4051188ECB7D024331 /* libPods-santad-santabs.a */; }; 81133DB01F3A76F700917FF9 /* SNTCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81133DAF1F3A75CE00917FF9 /* SNTCommand.m */; }; 81133DB11F3A77C600917FF9 /* SNTCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 81133DAF1F3A75CE00917FF9 /* SNTCommand.m */; }; + 81A00E7F1FD74F8E00A84676 /* SNTCompilerController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81A00E7E1FD74EFF00A84676 /* SNTCompilerController.m */; }; + 81A00E801FD74F9100A84676 /* SNTCompilerController.m in Sources */ = {isa = PBXBuildFile; fileRef = 81A00E7E1FD74EFF00A84676 /* SNTCompilerController.m */; }; B352A545B76783D568A6D0C5 /* libPods-Santa.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 90E9D568200AB9B642E06272 /* libPods-Santa.a */; }; C714F8B11D8044D400700EDF /* SNTCommandFileInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DCD5FBE1909D64A006B445C /* SNTCommandFileInfo.m */; }; C714F8B21D8044FE00700EDF /* SNTCommandController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D35BDAB18FD7CFD00921A21 /* SNTCommandController.m */; }; @@ -425,6 +427,8 @@ 7D949AA996AEAC326A4F6596 /* libPods-LogicTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-LogicTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 81133DAE1F3A75CE00917FF9 /* SNTCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SNTCommand.h; sourceTree = ""; }; 81133DAF1F3A75CE00917FF9 /* SNTCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SNTCommand.m; sourceTree = ""; }; + 81A00E7D1FD74EFF00A84676 /* SNTCompilerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SNTCompilerController.h; sourceTree = ""; }; + 81A00E7E1FD74EFF00A84676 /* SNTCompilerController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SNTCompilerController.m; sourceTree = ""; }; 8EF10E4B8C86CED022C72F1B /* Pods-santactl.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-santactl.debug.xcconfig"; path = "Pods/Target Support Files/Pods-santactl/Pods-santactl.debug.xcconfig"; sourceTree = ""; }; 90E9D568200AB9B642E06272 /* libPods-Santa.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Santa.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A6A91785C40257CC156B4F05 /* Pods-Santa.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Santa.release.xcconfig"; path = "Pods/Target Support Files/Pods-Santa/Pods-Santa.release.xcconfig"; sourceTree = ""; }; @@ -768,6 +772,8 @@ 0DB8ACC0185662DC00FEF9C7 /* SNTApplication.m */, 0DE71A731B95F7F900518526 /* SNTCachedDecision.h */, 0DE71A741B95F7F900518526 /* SNTCachedDecision.m */, + 81A00E7D1FD74EFF00A84676 /* SNTCompilerController.h */, + 81A00E7E1FD74EFF00A84676 /* SNTCompilerController.m */, 0D8E18CB19107B56000F89B8 /* SNTDaemonControlController.h */, 0D8E18CC19107B56000F89B8 /* SNTDaemonControlController.m */, 0D63DD5A1906FCB400D346C4 /* SNTDatabaseController.h */, @@ -1380,6 +1386,7 @@ 0D41DAD41A7C28C800A890FE /* SNTEventTableTest.m in Sources */, 0D3AFBEE18FB4C6C0087BCEE /* SNTApplication.m in Sources */, 0DD0D48F194F78F8005F27EB /* SNTFileInfoTest.m in Sources */, + 81A00E801FD74F9100A84676 /* SNTCompilerController.m in Sources */, 0DC5D86E191AED220078A5C0 /* SNTRuleTable.m in Sources */, 0DD0D492194F9BEF005F27EB /* SNTLogging.m in Sources */, 0DE71A761B95F7F900518526 /* SNTCachedDecision.m in Sources */, @@ -1490,6 +1497,7 @@ 0D377C2A17A071B7008453DB /* SNTEventTable.m in Sources */, 0DE50F681912716A007B2B0C /* SNTRule.m in Sources */, 0DB77FD81CCE824A004DF060 /* SNTBlockMessage.m in Sources */, + 81A00E7F1FD74F8E00A84676 /* SNTCompilerController.m in Sources */, 0D37C10F18F6029A0069BC61 /* SNTDatabaseTable.m in Sources */, C748E8A720696595006CFD1B /* SNTFileEventLog.m in Sources */, C748E8A3206964E1006CFD1B /* SNTEventLog.m in Sources */, diff --git a/Source/common/SNTCommonEnums.h b/Source/common/SNTCommonEnums.h index 7c07b9604..6d7703450 100644 --- a/Source/common/SNTCommonEnums.h +++ b/Source/common/SNTCommonEnums.h @@ -33,6 +33,9 @@ typedef NS_ENUM(NSInteger, SNTRuleState) { SNTRuleStateBlacklist = 2, SNTRuleStateSilentBlacklist = 3, SNTRuleStateRemove = 4, + + SNTRuleStateWhitelistCompiler = 5, + SNTRuleStateWhitelistTransitive = 6, }; typedef NS_ENUM(NSInteger, SNTClientMode) { @@ -58,6 +61,9 @@ typedef NS_ENUM(NSInteger, SNTEventState) { SNTEventStateAllowBinary = 1 << 25, SNTEventStateAllowCertificate = 1 << 26, SNTEventStateAllowScope = 1 << 27, + SNTEventStateAllowCompiler = 1 << 28, + SNTEventStateAllowTransitive = 1 << 29, + SNTEventStateAllowPendingTransitive = 1 << 30, // Block and Allow masks SNTEventStateBlock = 0xFF << 16, diff --git a/Source/common/SNTConfigurator.h b/Source/common/SNTConfigurator.h index a4c5d80a6..80b2326f0 100644 --- a/Source/common/SNTConfigurator.h +++ b/Source/common/SNTConfigurator.h @@ -198,6 +198,15 @@ /// @property BOOL bundlesEnabled; +#pragma mark Transitive Whitelisting Settings + +/// +/// If YES, binaries marked with SNTRuleStateWhitelistCompiler rules are allowed to transitively +/// whitelist any executables that they produce. If NO, SNTRuleStateWhitelistCompiler rules are +/// interpreted as if they were simply SNTRuleStateWhitelist rules. Defaults to NO. +/// +@property BOOL transitiveWhitelistingEnabled; + #pragma mark Server Auth Settings /// diff --git a/Source/common/SNTConfigurator.m b/Source/common/SNTConfigurator.m index 83b44b4c4..775f760e4 100644 --- a/Source/common/SNTConfigurator.m +++ b/Source/common/SNTConfigurator.m @@ -76,6 +76,7 @@ @implementation SNTConfigurator // The keys managed by a sync server or mobileconfig. static NSString *const kClientModeKey = @"ClientMode"; +static NSString *const kTransitiveWhitelistingEnabledKey = @"TransitiveWhitelistingEnabled"; static NSString *const kWhitelistRegexKey = @"WhitelistRegex"; static NSString *const kBlacklistRegexKey = @"BlacklistRegex"; @@ -94,6 +95,7 @@ - (instancetype)init { Class data = [NSData class]; _syncServerKeyTypes = @{ kClientModeKey : number, + kTransitiveWhitelistingEnabledKey : number, kWhitelistRegexKey : re, kBlacklistRegexKey : re, kFullSyncLastSuccess : date, @@ -102,6 +104,7 @@ - (instancetype)init { }; _forcedConfigKeyTypes = @{ kClientModeKey : number, + kTransitiveWhitelistingEnabledKey : number, kFileChangesRegexKey : re, kWhitelistRegexKey : re, kBlacklistRegexKey : re, @@ -287,6 +290,10 @@ + (NSSet *)keyPathsForValuesAffectingEnableMachineIDDecoration { return [self configStateSet]; } ++ (NSSet *)keyPathsForValuesAffectingTransitiveWhitelistingEnabled { + return [self configStateSet]; +} + #pragma mark Public Interface - (SNTClientMode)clientMode { @@ -311,6 +318,14 @@ - (void)setSyncServerClientMode:(SNTClientMode)newMode { } } +- (BOOL)transitiveWhitelistingEnabled { + return [self.configState[kTransitiveWhitelistingEnabledKey] boolValue]; +} + +- (void)setTransitiveWhitelistingEnabled:(BOOL)enabled { + [self updateSyncStateForKey:kTransitiveWhitelistingEnabledKey value:@(enabled)]; +} + - (NSRegularExpression *)whitelistPathRegex { return self.syncState[kWhitelistRegexKey] ?: self.configState[kWhitelistRegexKey]; } diff --git a/Source/common/SNTKernelCommon.h b/Source/common/SNTKernelCommon.h index 856d0fa87..2d5d1a81d 100644 --- a/Source/common/SNTKernelCommon.h +++ b/Source/common/SNTKernelCommon.h @@ -33,9 +33,11 @@ enum SantaDriverMethods { kSantaUserClientOpen, kSantaUserClientAllowBinary, + kSantaUserClientAllowCompiler, kSantaUserClientDenyBinary, kSantaUserClientAcknowledgeBinary, kSantaUserClientClearCache, + kSantaUserClientRemoveCacheEntry, kSantaUserClientCacheCount, kSantaUserClientCheckCache, kSantaUserClientCacheBucketCount, @@ -47,7 +49,7 @@ enum SantaDriverMethods { typedef enum { QUEUETYPE_DECISION, - QUEUETYPE_LOG + QUEUETYPE_LOG, } santa_queuetype_t; // Enum defining actions that can be passed down the IODataQueue and in @@ -64,6 +66,10 @@ typedef enum { ACTION_RESPOND_DENY = 21, ACTION_RESPOND_TOOLONG = 22, ACTION_RESPOND_ACK = 23, + ACTION_RESPOND_ALLOW_COMPILER = 24, + // The following response is stored only in the kernel decision cache. + // It is removed by SNTCompilerController + ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE = 25, // NOTIFY ACTION_NOTIFY_EXEC = 30, @@ -72,13 +78,17 @@ typedef enum { ACTION_NOTIFY_LINK = 33, ACTION_NOTIFY_EXCHANGE = 34, ACTION_NOTIFY_DELETE = 35, + ACTION_NOTIFY_WHITELIST = 36, // ERROR ACTION_ERROR = 99, } santa_action_t; #define RESPONSE_VALID(x) \ - (x == ACTION_RESPOND_ALLOW || x == ACTION_RESPOND_DENY) + (x == ACTION_RESPOND_ALLOW || \ + x == ACTION_RESPOND_DENY || \ + x == ACTION_RESPOND_ALLOW_COMPILER || \ + x == ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE) // Struct to manage vnode IDs typedef struct santa_vnode_id_t { diff --git a/Source/common/SNTRule.h b/Source/common/SNTRule.h index bc97a6a2b..65445eda6 100644 --- a/Source/common/SNTRule.h +++ b/Source/common/SNTRule.h @@ -41,12 +41,32 @@ /// @property(copy) NSString *customMsg; +/// +/// The time when this rule was last retrieved from the rules database, if rule is transitive. +/// Stored as number of seconds since 00:00:00 UTC on 1 January 2001. +/// +@property(readonly) NSUInteger timestamp; + /// /// Designated initializer. /// +- (instancetype)initWithShasum:(NSString *)shasum + state:(SNTRuleState)state + type:(SNTRuleType)type + customMsg:(NSString *)customMsg + timestamp:(NSUInteger)timestamp; + +/// +/// Initialize with a default timestamp: current time if rule state is transitive, 0 otherwise. +/// - (instancetype)initWithShasum:(NSString *)shasum state:(SNTRuleState)state type:(SNTRuleType)type customMsg:(NSString *)customMsg; +/// +/// Sets timestamp of rule to the current time. +/// +- (void)resetTimestamp; + @end diff --git a/Source/common/SNTRule.m b/Source/common/SNTRule.m index 6f3d77f51..b0b1394f2 100644 --- a/Source/common/SNTRule.m +++ b/Source/common/SNTRule.m @@ -14,22 +14,45 @@ #import "SNTRule.h" +@interface SNTRule() +@property(readwrite) NSUInteger timestamp; +@end + @implementation SNTRule - (instancetype)initWithShasum:(NSString *)shasum state:(SNTRuleState)state type:(SNTRuleType)type - customMsg:(NSString *)customMsg { + customMsg:(NSString *)customMsg + timestamp:(NSUInteger)timestamp { self = [super init]; if (self) { _shasum = shasum; _state = state; _type = type; _customMsg = customMsg; + _timestamp = timestamp; } return self; } +- (instancetype)initWithShasum:(NSString *)shasum + state:(SNTRuleState)state + type:(SNTRuleType)type + customMsg:(NSString *)customMsg { + self = [self initWithShasum:shasum + state:state + type:type + customMsg:customMsg + timestamp:0]; + // Initialize timestamp to current time if rule is transitive. + if (self && state == SNTRuleStateWhitelistTransitive) { + [self resetTimestamp]; + } + return self; +} + + #pragma mark NSSecureCoding #pragma clang diagnostic push @@ -46,6 +69,7 @@ - (void)encodeWithCoder:(NSCoder *)coder { ENCODE(@(self.state), @"state"); ENCODE(@(self.type), @"type"); ENCODE(self.customMsg, @"custommsg"); + ENCODE(@(self.timestamp), @"timestamp"); } - (instancetype)initWithCoder:(NSCoder *)decoder { @@ -55,6 +79,7 @@ - (instancetype)initWithCoder:(NSCoder *)decoder { _state = [DECODE(NSNumber, @"state") intValue]; _type = [DECODE(NSNumber, @"type") intValue]; _customMsg = DECODE(NSString, @"custommsg"); + _timestamp = [DECODE(NSNumber, @"timestamp") unsignedIntegerValue]; } return self; } @@ -80,8 +105,14 @@ - (NSUInteger)hash { } - (NSString *)description { - return [NSString stringWithFormat:@"SNTRule: SHA-256: %@, State: %ld, Type: %ld", - self.shasum, self.state, self.type]; + return [NSString stringWithFormat:@"SNTRule: SHA-256: %@, State: %ld, Type: %ld, Timestamp: %lu", + self.shasum, self.state, self.type, (unsigned long)self.timestamp]; +} + +# pragma mark Last-access Timestamp + +- (void)resetTimestamp { + self.timestamp = (NSUInteger)[[NSDate date] timeIntervalSinceReferenceDate]; } @end diff --git a/Source/common/SNTXPCControlInterface.h b/Source/common/SNTXPCControlInterface.h index 66da818f7..58337cdee 100644 --- a/Source/common/SNTXPCControlInterface.h +++ b/Source/common/SNTXPCControlInterface.h @@ -47,6 +47,7 @@ - (void)setWhitelistPathRegex:(NSString *)pattern reply:(void (^)(void))reply; - (void)setBlacklistPathRegex:(NSString *)pattern reply:(void (^)(void))reply; - (void)setBundlesEnabled:(BOOL)bundlesEnabled reply:(void (^)(void))reply; +- (void)setTransitiveWhitelistingEnabled:(BOOL)enabled reply:(void (^)(void))reply; /// /// Syncd Ops diff --git a/Source/common/SNTXPCUnprivilegedControlInterface.h b/Source/common/SNTXPCUnprivilegedControlInterface.h index 836e405b1..63bac6547 100644 --- a/Source/common/SNTXPCUnprivilegedControlInterface.h +++ b/Source/common/SNTXPCUnprivilegedControlInterface.h @@ -41,7 +41,10 @@ /// /// Database ops /// -- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate))reply; +- (void)databaseRuleCounts:(void (^)(int64_t binary, + int64_t certificate, + int64_t compiler, + int64_t transitive))reply; - (void)databaseEventCount:(void (^)(int64_t count))reply; /// @@ -71,6 +74,7 @@ - (void)ruleSyncLastSuccess:(void (^)(NSDate *))reply; - (void)syncCleanRequired:(void (^)(BOOL))reply; - (void)bundlesEnabled:(void (^)(BOOL))reply; +- (void)transitiveWhitelistingEnabled:(void (^)(BOOL))reply; /// /// GUI Ops diff --git a/Source/santa-driver/SantaDecisionManager.cc b/Source/santa-driver/SantaDecisionManager.cc index ab2b970b7..f24097c38 100644 --- a/Source/santa-driver/SantaDecisionManager.cc +++ b/Source/santa-driver/SantaDecisionManager.cc @@ -14,6 +14,14 @@ #include "SantaDecisionManager.h" +// This is a made-up KAUTH_FILEOP constant which represents a +// KAUTH_VNODE_WRITE_DATA event that gets passed to SantaDecisionManager's +// FileOpCallback method. The KAUTH_FILEOP_* constants are defined in +// sys/kauth.h and run from 1--7. KAUTH_VNODE_WRITE_DATA is already defined as +// 4 so it overlaps with the other KAUTH_FILEOP_* constants and can't be used. +// We define KAUTH_FILEOP_WRITE as something much greater than 7. +#define KAUTH_FILEOP_WRITE 100 + #define super OSObject OSDefineMetaClassAndStructors(SantaDecisionManager, OSObject); @@ -35,6 +43,7 @@ bool SantaDecisionManager::init() { decision_cache_ = new SantaCache(10000, 2); vnode_pid_map_ = new SantaCache(2000, 5); + compiler_pid_set_ = new SantaCache(500, 5); decision_dataqueue_ = IOSharedDataQueue::withEntries( kMaxDecisionQueueEvents, sizeof(santa_message_t)); @@ -53,6 +62,8 @@ void SantaDecisionManager::free() { delete decision_cache_; delete vnode_pid_map_; + StopPidMonitorThreads(); + if (decision_dataqueue_lock_) { lck_mtx_free(decision_dataqueue_lock_, sdm_lock_grp_); decision_dataqueue_lock_ = nullptr; @@ -195,6 +206,90 @@ kern_return_t SantaDecisionManager::StopListener() { return kIOReturnSuccess; } +# pragma mark Monitoring PIDs + +// Arguments that are passed to pid_monitor thread. +typedef struct { + pid_t pid; // process to monitor + SantaDecisionManager *sdm; // reference to SantaDecisionManager +} pid_monitor_info; + +// Function executed in its own thread used to monitor a compiler process for +// termination and then remove the process pid from cache of compiler pids. +static void pid_monitor(void *param, __unused wait_result_t wait_result) { + pid_monitor_info *info = (pid_monitor_info *)param; + if (info && info->sdm) { + uint32_t sleep_time = info->sdm->PidMonitorSleepTimeMilliseconds(); + while (!info->sdm->PidMonitorThreadsShouldExit()) { + proc_t proc = proc_find(info->pid); + if (!proc) break; + proc_rele(proc); + IOSleep(sleep_time); + } + info->sdm->ForgetCompilerPid(info->pid); + info->sdm->DecrementPidMonitorThreadCount(); + } + thread_terminate(current_thread()); +} + +// TODO(nguyenphillip): Look at moving pid monitoring out of SDM entirely, +// maybe by creating a dedicated class to do this that SDM could then query. +void SantaDecisionManager::MonitorCompilerPidForExit(pid_t pid) { + // Don't start any new threads if compiler_pid_set_ doesn't exist. + if (!compiler_pid_set_) return; + auto info = new pid_monitor_info; + info->pid = pid; + info->sdm = this; + thread_t thread = THREAD_NULL; + IncrementPidMonitorThreadCount(); + if (KERN_SUCCESS != kernel_thread_start(pid_monitor, (void *)info, &thread)) { + LOGE("couldn't start pid monitor thread"); + DecrementPidMonitorThreadCount(); + } + thread_deallocate(thread); +} + +void SantaDecisionManager::ForgetCompilerPid(pid_t pid) { + if (compiler_pid_set_) compiler_pid_set_->remove(pid); +} + +bool SantaDecisionManager::PidMonitorThreadsShouldExit() const { + return compiler_pid_set_ == nullptr; +} + +bool SantaDecisionManager::StopPidMonitorThreads() { + // Each pid_monitor thread checks for the existence of compiler_pid_set_. + // As soon as they see that it's gone, they should terminate and decrement + // SantaDecisionManager's pid_monitor_thread_count. When this count decreases + // to zero all threads have finished. + auto temp = compiler_pid_set_; + compiler_pid_set_ = nullptr; + delete temp; + + // Sleep time between checks starts at 10 ms, but increases to 5 sec after + // 10 sec have passed without the thread count dropping to 0. + unsigned int sleep_time_milliseconds = 10; + unsigned int total_wait_time = 0; + + while (pid_monitor_thread_count_ > 0) { + if (sleep_time_milliseconds == 10) { + total_wait_time += sleep_time_milliseconds; + if (total_wait_time >= 10000) { + sleep_time_milliseconds = 5000; + LOGD("Waited %d ms for pid monitor threads to quit, switching sleep" + "time to %d ms", total_wait_time, sleep_time_milliseconds); + } + } + IOSleep(sleep_time_milliseconds); + } + LOGD("Pid monitor threads stopped."); + return true; +} + +uint32_t SantaDecisionManager::PidMonitorSleepTimeMilliseconds() const { + return kPidMonitorSleepTimeMilliseconds; +} + #pragma mark Cache Management void SantaDecisionManager::AddToCache( @@ -208,6 +303,7 @@ void SantaDecisionManager::AddToCache( ((uint64_t)ACTION_REQUEST_BINARY << 56)); break; case ACTION_RESPOND_ALLOW: + case ACTION_RESPOND_ALLOW_COMPILER: case ACTION_RESPOND_DENY: { // Decision is stored in upper 8 bits, timestamp in remaining 56. uint64_t val = ((uint64_t)decision << 56) | (microsecs & 0xFFFFFFFFFFFFFF); @@ -216,6 +312,12 @@ void SantaDecisionManager::AddToCache( } break; } + case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE: { + // Decision is stored in upper 8 bits, timestamp in remaining 56. + uint64_t val = ((uint64_t)decision << 56) | (microsecs & 0xFFFFFFFFFFFFFF); + decision_cache_->set(identifier, val, 0); + break; + } default: break; } @@ -406,6 +508,34 @@ void SantaDecisionManager::DecrementListenerInvocations() { OSDecrementAtomic(&listener_invocations_); } +void SantaDecisionManager::IncrementPidMonitorThreadCount() { + OSIncrementAtomic(&pid_monitor_thread_count_); +} + +void SantaDecisionManager::DecrementPidMonitorThreadCount() { + OSDecrementAtomic(&pid_monitor_thread_count_); +} + +bool SantaDecisionManager::IsCompilerProcess(pid_t pid) { + for (;;) { + // Find the parent pid. + proc_t proc = proc_find(pid); + if (!proc) return false; + pid_t ppid = proc_ppid(proc); + proc_rele(proc); + // Quit if process is launchd or has no parent. + if (ppid == 0 || pid == ppid) break; + pid_t val = compiler_pid_set_->get(pid); + // If pid was in compiler_pid_set_ then make sure that it has the same + // parent pid as when it was set. + if (val) return val == ppid; + // If pid not in the set, then quit unless we want to check ancestors. + if (!kCheckCompilerAncestors) break; + pid = ppid; + } + return false; +} + #pragma mark Callbacks int SantaDecisionManager::VnodeCallback(const kauth_cred_t cred, @@ -420,7 +550,9 @@ int SantaDecisionManager::VnodeCallback(const kauth_cred_t cred, auto returnedAction = FetchDecision(cred, vp, vnode_id); switch (returnedAction) { - case ACTION_RESPOND_ALLOW: { + case ACTION_RESPOND_ALLOW: + case ACTION_RESPOND_ALLOW_COMPILER: + case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE: { auto proc = vfs_context_proc(ctx); if (proc) { pid_t pid = proc_pid(proc); @@ -428,6 +560,15 @@ int SantaDecisionManager::VnodeCallback(const kauth_cred_t cred, // pid_t is 32-bit; pid is in upper 32 bits, ppid in lower. uint64_t val = ((uint64_t)pid << 32) | (ppid & 0xFFFFFFFF); vnode_pid_map_->set(vnode_id, val); + if (returnedAction == ACTION_RESPOND_ALLOW_COMPILER && ppid != 0) { + // Do some additional bookkeeping for compilers: + // We associate the pid with a compiler so that when we see it later + // in the context of a KAUTH_FILEOP event, we'll recognize it. + compiler_pid_set_->set(pid, ppid); + // And start polling for the compiler process termination, so that we + // can remove the pid from our cache of compiler pids. + MonitorCompilerPidForExit(pid); + } } return KAUTH_RESULT_ALLOW; } @@ -450,26 +591,58 @@ void SantaDecisionManager::FileOpCallback( const char *path, const char *new_path) { if (!ClientConnected() || proc_selfpid() == client_pid_) return; - if (vp) { + if (vp && action == KAUTH_FILEOP_EXEC) { auto context = vfs_context_create(nullptr); auto vnode_id = GetVnodeIDForVnode(context, vp); vfs_context_rele(context); - if (action == KAUTH_FILEOP_EXEC) { - auto message = NewMessage(nullptr); - message->vnode_id = vnode_id; - message->action = ACTION_NOTIFY_EXEC; - strlcpy(message->path, path, sizeof(message->path)); - uint64_t val = vnode_pid_map_->get(vnode_id); - if (val) { - // pid_t is 32-bit, so pid is in upper 32 bits, ppid in lower. - message->pid = (val >> 32); - message->ppid = (val & ~0xFFFFFFFF00000000); + auto message = NewMessage(nullptr); + message->vnode_id = vnode_id; + message->action = ACTION_NOTIFY_EXEC; + strlcpy(message->path, path, sizeof(message->path)); + uint64_t val = vnode_pid_map_->get(vnode_id); + if (val) { + // pid_t is 32-bit, so pid is in upper 32 bits, ppid in lower. + message->pid = (val >> 32); + message->ppid = (val & ~0xFFFFFFFF00000000); + } + PostToLogQueue(message); + delete message; + return; + } + + // For transitive whitelisting decisions, we must check for KAUTH_FILEOP_CLOSE events from a + // known compiler process. But we must also check for KAUTH_FILEOP_RENAME events because clang + // under Xcode 9 will, if the output file already exists, write to a temp file, delete the + // existing file, then rename the temp file, without ever closing it. So in this scenario, + // the KAUTH_FILEOP_RENAME is the only chance we have of whitelisting the output. + if (action == KAUTH_FILEOP_CLOSE || (action == KAUTH_FILEOP_RENAME && new_path)) { + auto message = NewMessage(nullptr); + if (IsCompilerProcess(message->pid)) { + // Fill out the rest of the message details and send it to the decision queue. + auto context = vfs_context_create(nullptr); + vnode_t real_vp = vp; + // We have to manually look up the vnode pointer from new_path for KAUTH_FILEOP_RENAME. + if (!real_vp && new_path && ERR_SUCCESS == vnode_lookup(new_path, 0, &real_vp, context)) { + vnode_put(real_vp); } - PostToLogQueue(message); - delete message; - return; + if (real_vp) message->vnode_id = GetVnodeIDForVnode(context, real_vp); + vfs_context_rele(context); + message->action = ACTION_NOTIFY_WHITELIST; + const char *real_path = (action == KAUTH_FILEOP_CLOSE) ? path : new_path; + strlcpy(message->path, real_path, sizeof(message->path)); + proc_name(message->pid, message->pname, sizeof(message->pname)); + PostToDecisionQueue(message); + // Add a temporary allow rule to the decision cache for this vnode_id + // while SNTCompilerController decides whether or not to add a + // permanent rule for the new file to the rules database. This is + // because checking if the file is a Mach-O binary and hashing it might + // not finish before an attempt to execute it. + AddToCache(message->vnode_id, ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE, 0); } + delete message; + // Don't need to do anything else for FILEOP_CLOSE, but FILEOP_RENAME should fall through. + if (action == KAUTH_FILEOP_CLOSE) return; } // Filter out modifications to locations that are definitely @@ -481,7 +654,8 @@ void SantaDecisionManager::FileOpCallback( proc_name(message->pid, message->pname, sizeof(message->pname)); switch (action) { - case KAUTH_FILEOP_CLOSE: + case KAUTH_FILEOP_WRITE: + // This is actually a KAUTH_VNODE_WRITE_DATA event. message->action = ACTION_NOTIFY_WRITE; break; case KAUTH_FILEOP_RENAME: @@ -524,6 +698,12 @@ extern "C" int fileop_scope_callback( char *new_path = nullptr; switch (action) { + case KAUTH_FILEOP_CLOSE: + // We only care about KAUTH_FILEOP_CLOSE events where the closed file + // was modified. + if (!(arg2 & KAUTH_FILEOP_CLOSE_MODIFIED)) + return KAUTH_RESULT_DEFER; + // Intentional fallthrough to get vnode reference. case KAUTH_FILEOP_DELETE: case KAUTH_FILEOP_EXEC: vp = reinterpret_cast(arg0); @@ -580,7 +760,9 @@ extern "C" int vnode_scope_callback( char path[MAXPATHLEN]; int pathlen = MAXPATHLEN; vn_getpath(vp, path, &pathlen); - sdm->FileOpCallback(KAUTH_FILEOP_CLOSE, vp, path, nullptr); + // KAUTH_VNODE_WRITE_DATA events are translated into fake KAUTH_FILEOP_WRITE + // events so that we can handle them in the FileOpCallback function. + sdm->FileOpCallback(KAUTH_FILEOP_WRITE, vp, path, nullptr); sdm->DecrementListenerInvocations(); } diff --git a/Source/santa-driver/SantaDecisionManager.h b/Source/santa-driver/SantaDecisionManager.h index f224ad24d..cf4b5d9e0 100644 --- a/Source/santa-driver/SantaDecisionManager.h +++ b/Source/santa-driver/SantaDecisionManager.h @@ -85,6 +85,29 @@ class SantaDecisionManager : public OSObject { */ kern_return_t StopListener(); + /** + This spins off a new thread for each process that we monitor. Generally the + threads should be short-lived, since they die as soon as their associated + compiler process dies. + */ + void MonitorCompilerPidForExit(pid_t pid); + + /// Remove the given pid from cache of compiler pids. + void ForgetCompilerPid(pid_t pid); + + /// Returns true when SantaDecisionManager wants monitor threads to exit. + bool PidMonitorThreadsShouldExit() const; + + /** + Stops the pid monitor threads. Waits until all threads have stopped before + returning. This also frees the compiler_pid_set_. Returns true if all + threads exited cleanly. Returns false if timed out while waiting. + */ + bool StopPidMonitorThreads(); + + /// Returns how long pid monitor should sleep between termination checks. + uint32_t PidMonitorSleepTimeMilliseconds() const; + /// Adds a decision to the cache, with a timestamp. void AddToCache(santa_vnode_id_t identifier, const santa_action_t decision, @@ -125,6 +148,19 @@ class SantaDecisionManager : public OSObject { /// Decrements the count of active callbacks pending. void DecrementListenerInvocations(); + /// Increments the count of active pid monitor threads. + void IncrementPidMonitorThreadCount(); + + /// Decrements the count of active pid monitor threads. + void DecrementPidMonitorThreadCount(); + + /** + Determine if pid belongs to a compiler process. When + kCheckCompilerAncestors is set to true, this also checks all ancestor + processes of the pid. + */ + bool IsCompilerProcess(pid_t pid); + /** Fetches the vnode_id for a given vnode. @@ -203,6 +239,16 @@ class SantaDecisionManager : public OSObject { */ static const uint32_t kMaxLogQueueEvents = 2048; + /// How long pid monitor thread should sleep between termination checks. + static const uint32_t kPidMonitorSleepTimeMilliseconds = 1000; + + /** + When set to true, Santa will check all ancestors of a process to determine + if it is a compiler. + TODO(nguyenphillip): this setting (and others above) should be configurable. + */ + static const bool kCheckCompilerAncestors = false; + /** Fetches a response from the daemon. Handles both daemon death and failure to post messages to the daemon. @@ -281,6 +327,7 @@ class SantaDecisionManager : public OSObject { SantaCache *decision_cache_; SantaCache *vnode_pid_map_; + SantaCache *compiler_pid_set_; lck_grp_t *sdm_lock_grp_; lck_grp_attr_t *sdm_lock_grp_attr_; @@ -295,6 +342,7 @@ class SantaDecisionManager : public OSObject { uint32_t failed_log_queue_requests_; int32_t listener_invocations_; + int32_t pid_monitor_thread_count_ = 0; pid_t client_pid_; diff --git a/Source/santa-driver/SantaDriverClient.cc b/Source/santa-driver/SantaDriverClient.cc index 7c77daadd..d4bb0bed9 100644 --- a/Source/santa-driver/SantaDriverClient.cc +++ b/Source/santa-driver/SantaDriverClient.cc @@ -145,6 +145,19 @@ IOReturn SantaDriverClient::allow_binary( return kIOReturnSuccess; } +IOReturn SantaDriverClient::allow_compiler( + OSObject *target, void *reference, IOExternalMethodArguments *arguments) { + SantaDriverClient *me = OSDynamicCast(SantaDriverClient, target); + if (!me) return kIOReturnBadArgument; + + if (arguments->structureInputSize != sizeof(santa_vnode_id_t)) return kIOReturnInvalid; + santa_vnode_id_t *vnode_id = (santa_vnode_id_t *)arguments->structureInput; + if (vnode_id->fsid == 0 || vnode_id->fileid == 0) return kIOReturnInvalid; + me->decisionManager->AddToCache(*vnode_id, ACTION_RESPOND_ALLOW_COMPILER); + + return kIOReturnSuccess; +} + IOReturn SantaDriverClient::deny_binary( OSObject *target, void *reference, IOExternalMethodArguments *arguments) { SantaDriverClient *me = OSDynamicCast(SantaDriverClient, target); @@ -181,6 +194,19 @@ IOReturn SantaDriverClient::clear_cache( return kIOReturnSuccess; } +IOReturn SantaDriverClient::remove_cache_entry( + OSObject *target, void *reference, IOExternalMethodArguments *arguments) { + SantaDriverClient *me = OSDynamicCast(SantaDriverClient, target); + if (!me) return kIOReturnBadArgument; + + if (arguments->structureInputSize != sizeof(santa_vnode_id_t)) return kIOReturnInvalid; + santa_vnode_id_t *vnode_id = (santa_vnode_id_t *)arguments->structureInput; + if (vnode_id->fsid == 0 || vnode_id->fileid == 0) return kIOReturnInvalid; + me->decisionManager->RemoveFromCache(*vnode_id); + + return kIOReturnSuccess; +} + IOReturn SantaDriverClient::cache_count( OSObject *target, void *reference, IOExternalMethodArguments *arguments) { SantaDriverClient *me = OSDynamicCast(SantaDriverClient, target); @@ -234,9 +260,11 @@ IOReturn SantaDriverClient::externalMethod( // Function ptr, input scalar count, input struct size, output scalar count, output struct size { &SantaDriverClient::open, 0, 0, 0, 0 }, { &SantaDriverClient::allow_binary, 0, sizeof(santa_vnode_id_t), 0, 0 }, + { &SantaDriverClient::allow_compiler, 0, sizeof(santa_vnode_id_t), 0, 0 }, { &SantaDriverClient::deny_binary, 0, sizeof(santa_vnode_id_t), 0, 0 }, { &SantaDriverClient::acknowledge_binary, 0, sizeof(santa_vnode_id_t), 0, 0 }, { &SantaDriverClient::clear_cache, 0, 0, 0, 0 }, + { &SantaDriverClient::remove_cache_entry, 0, sizeof(santa_vnode_id_t), 0, 0 }, { &SantaDriverClient::cache_count, 0, 0, 1, 0 }, { &SantaDriverClient::check_cache, 0, sizeof(santa_vnode_id_t), 1, 0 }, { &SantaDriverClient::cache_bucket_count, 0, sizeof(santa_bucket_count_t), diff --git a/Source/santa-driver/SantaDriverClient.h b/Source/santa-driver/SantaDriverClient.h index 039f67250..ed5e63cd6 100644 --- a/Source/santa-driver/SantaDriverClient.h +++ b/Source/santa-driver/SantaDriverClient.h @@ -74,7 +74,7 @@ class com_google_SantaDriverClient : public IOUserClient { OSObject *target, void *reference) override; /// - /// The userpsace callable methods are below. Each method corresponds + /// The userspace callable methods are below. Each method corresponds /// to an entry in SantaDriverMethods. /// @@ -84,7 +84,11 @@ class com_google_SantaDriverClient : public IOUserClient { /// The daemon calls this to allow a binary. static IOReturn allow_binary( - OSObject *target, void *reference,IOExternalMethodArguments *arguments); + OSObject *target, void *reference, IOExternalMethodArguments *arguments); + + /// The daemon calls this to allow a compiler binary. + static IOReturn allow_compiler( + OSObject *target, void *reference, IOExternalMethodArguments *arguments); /// The daemon calls this to deny a binary. static IOReturn deny_binary( @@ -99,6 +103,10 @@ class com_google_SantaDriverClient : public IOUserClient { static IOReturn clear_cache( OSObject *target, void *reference, IOExternalMethodArguments *arguments); + /// The daemon call this to remove a single cache entry. + static IOReturn remove_cache_entry( + OSObject *target, void *reference, IOExternalMethodArguments *arguments); + /// The daemon calls this to find out how many items are in the cache static IOReturn cache_count( OSObject *target, void *reference, IOExternalMethodArguments *arguments); diff --git a/Source/santactl/Commands/SNTCommandCheckCache.m b/Source/santactl/Commands/SNTCommandCheckCache.m index e823598c9..48d4887cf 100644 --- a/Source/santactl/Commands/SNTCommandCheckCache.m +++ b/Source/santactl/Commands/SNTCommandCheckCache.m @@ -60,6 +60,12 @@ - (void)runWithArguments:(NSArray *)arguments { } else if (action == ACTION_RESPOND_DENY) { LOGI(@"File exists in [blacklist] kernel cache"); exit(0); + } else if (action == ACTION_RESPOND_ALLOW_COMPILER) { + LOGI(@"File exists in [whitelist compiler] kernel cache"); + exit(0); + } else if (action == ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE) { + LOGI(@"File exists in [whitelist pending_transitive] kernel cache"); + exit(0); } else if (action == ACTION_UNSET) { LOGE(@"File does not exist in cache"); exit(1); diff --git a/Source/santactl/Commands/SNTCommandFileInfo.m b/Source/santactl/Commands/SNTCommandFileInfo.m index 2ba55c860..4442f6adb 100644 --- a/Source/santactl/Commands/SNTCommandFileInfo.m +++ b/Source/santactl/Commands/SNTCommandFileInfo.m @@ -384,6 +384,12 @@ - (SNTAttributeBlock)rule { case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break; + case SNTEventStateAllowCompiler: + [output appendString:@" (Compiler)"]; + break; + case SNTEventStateAllowTransitive: + [output appendString:@" (Transitive)"]; + break; default: output = @"None".mutableCopy; break; diff --git a/Source/santactl/Commands/SNTCommandRule.m b/Source/santactl/Commands/SNTCommandRule.m index 2e4dc0c03..eb7be8512 100644 --- a/Source/santactl/Commands/SNTCommandRule.m +++ b/Source/santactl/Commands/SNTCommandRule.m @@ -53,6 +53,7 @@ + (NSString *)longHelpText { @" --whitelist: add to whitelist\n" @" --blacklist: add to blacklist\n" @" --silent-blacklist: add to silent blacklist\n" + @" --compiler: whitelist and mark as a compiler\n" @" --remove: remove existing rule\n" @" --check: check for an existing rule\n" @"\n" @@ -65,12 +66,22 @@ + (NSString *)longHelpText { @"\n" @" Optionally:\n" @" --certificate: add or check a certificate sha256 rule instead of binary\n" +#ifdef DEBUG + @" --force: allow manual changes even when SyncBaseUrl is set\n" +#endif @" --message {message}: custom message\n"); } - (void)runWithArguments:(NSArray *)arguments { SNTConfigurator *config = [SNTConfigurator configurator]; + // DEBUG builds add a --force flag to allow manually adding/removing rules during testing. +#ifdef DEBUG + if ([config syncBaseURL] && + ![arguments containsObject:@"--check"] && + ![arguments containsObject:@"--force"]) { +#else if ([config syncBaseURL] && ![arguments containsObject:@"--check"]) { +#endif printf("SyncBaseURL is set, rules are managed centrally.\n"); exit(1); } @@ -92,6 +103,8 @@ - (void)runWithArguments:(NSArray *)arguments { newRule.state = SNTRuleStateBlacklist; } else if ([arg caseInsensitiveCompare:@"--silent-blacklist"] == NSOrderedSame) { newRule.state = SNTRuleStateSilentBlacklist; + } else if ([arg caseInsensitiveCompare:@"--compiler"] == NSOrderedSame) { + newRule.state = SNTRuleStateWhitelistCompiler; } else if ([arg caseInsensitiveCompare:@"--remove"] == NSOrderedSame) { newRule.state = SNTRuleStateRemove; } else if ([arg caseInsensitiveCompare:@"--check"] == NSOrderedSame) { @@ -116,6 +129,10 @@ - (void)runWithArguments:(NSArray *)arguments { [self printErrorUsageAndExit:@"--message requires an argument"]; } newRule.customMsg = arguments[i]; +#ifdef DEBUG + } else if ([arg caseInsensitiveCompare:@"--force"] == NSOrderedSame) { + // Don't do anything special. +#endif } else { [self printErrorUsageAndExit:[@"Unknown argument: " stringByAppendingString:arg]]; } @@ -192,6 +209,12 @@ - (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)da case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break; + case SNTEventStateAllowCompiler: + [output appendString:@" (Compiler)"]; + break; + case SNTEventStateAllowTransitive: + [output appendString:@" (Transitive)"]; + break; default: output = @"None".mutableCopy; break; @@ -214,6 +237,22 @@ - (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)da printf("Cannot communicate with daemon"); exit(1); } + + dispatch_group_enter(group); + [[daemonConn remoteObjectProxy] databaseRuleForBinarySHA256:fileSHA256 + certificateSHA256:certificateSHA256 + reply:^(SNTRule *r) { + if (r.state == SNTRuleStateWhitelistTransitive) { + NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp]; + [output appendString:[NSString stringWithFormat:@"\nlast access date: %@", [date description]]]; + } + dispatch_group_leave(group); + }]; + if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) { + printf("Cannot communicate with daemon"); + exit(1); + } + printf("%s\n", output.UTF8String); exit(0); } diff --git a/Source/santactl/Commands/SNTCommandStatus.m b/Source/santactl/Commands/SNTCommandStatus.m index 888c95de4..af38e12d5 100644 --- a/Source/santactl/Commands/SNTCommandStatus.m +++ b/Source/santactl/Commands/SNTCommandStatus.m @@ -96,10 +96,16 @@ - (void)runWithArguments:(NSArray *)arguments { // Database counts __block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1; + __block int64_t compilerRuleCount = -1, transitiveRuleCount = -1; dispatch_group_enter(group); - [[self.daemonConn remoteObjectProxy] databaseRuleCounts:^(int64_t binary, int64_t certificate) { + [[self.daemonConn remoteObjectProxy] databaseRuleCounts:^(int64_t binary, + int64_t certificate, + int64_t compiler, + int64_t transitive) { binaryRuleCount = binary; certRuleCount = certificate; + compilerRuleCount = compiler; + transitiveRuleCount = transitive; dispatch_group_leave(group); }]; dispatch_group_enter(group); @@ -148,6 +154,13 @@ - (void)runWithArguments:(NSArray *)arguments { }]; } + __block BOOL transitiveWhitelistingEnabled = NO; + dispatch_group_enter(group); + [[self.daemonConn remoteObjectProxy] transitiveWhitelistingEnabled:^(BOOL response) { + transitiveWhitelistingEnabled = response; + dispatch_group_leave(group); + }]; + // Wait a maximum of 5s for stats collected from daemon to arrive. if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) { fprintf(stderr, "Failed to retrieve some stats from daemon\n\n"); @@ -179,6 +192,8 @@ - (void)runWithArguments:(NSArray *)arguments { @"database" : @{ @"binary_rules" : @(binaryRuleCount), @"certificate_rules" : @(certRuleCount), + @"compiler_rules" : @(compilerRuleCount), + @"transitive_rules" : @(transitiveRuleCount), @"events_pending_upload" : @(eventCount), }, @"sync" : @{ @@ -187,7 +202,8 @@ - (void)runWithArguments:(NSArray *)arguments { @"last_successful_full" : fullSyncLastSuccessStr ?: @"null", @"last_successful_rule" : ruleSyncLastSuccessStr ?: @"null", @"push_notifications" : pushNotifications ? @"Connected" : @"Disconnected", - @"bundle_scanning" : @(bundlesEnabled) + @"bundle_scanning" : @(bundlesEnabled), + @"transitive_whitelisting" : @(transitiveWhitelistingEnabled), }, }; NSData *statsData = [NSJSONSerialization dataWithJSONObject:stats @@ -207,6 +223,8 @@ - (void)runWithArguments:(NSArray *)arguments { printf(">>> Database Info\n"); printf(" %-25s | %lld\n", "Binary Rules", binaryRuleCount); printf(" %-25s | %lld\n", "Certificate Rules", certRuleCount); + printf(" %-25s | %lld\n", "Compiler Rules", compilerRuleCount); + printf(" %-25s | %lld\n", "Transitive Rules", transitiveRuleCount); printf(" %-25s | %lld\n", "Events Pending Upload", eventCount); if (syncURLStr) { @@ -218,6 +236,8 @@ - (void)runWithArguments:(NSArray *)arguments { printf(" %-25s | %s\n", "Push Notifications", (pushNotifications ? "Connected" : "Disconnected")); printf(" %-25s | %s\n", "Bundle Scanning", (bundlesEnabled ? "Yes" : "No")); + printf(" %-25s | %s\n", "Transitive Whitelisting", + (transitiveWhitelistingEnabled ? "Yes" : "No")); } } diff --git a/Source/santactl/Commands/sync/SNTCommandSyncConstants.h b/Source/santactl/Commands/sync/SNTCommandSyncConstants.h index 336a2d14a..1e9e65fd1 100644 --- a/Source/santactl/Commands/sync/SNTCommandSyncConstants.h +++ b/Source/santactl/Commands/sync/SNTCommandSyncConstants.h @@ -33,10 +33,13 @@ extern NSString *const kWhitelistRegex; extern NSString *const kBlacklistRegex; extern NSString *const kBinaryRuleCount; extern NSString *const kCertificateRuleCount; +extern NSString *const kCompilerRuleCount; +extern NSString *const kTransitiveRuleCount; extern NSString *const kFCMToken; extern NSString *const kFCMFullSyncInterval; extern NSString *const kFCMGlobalRuleSyncDeadline; extern NSString *const kBundlesEnabled; +extern NSString *const kTransitiveWhitelistingEnabled; extern NSString *const kEvents; extern NSString *const kFileSHA256; @@ -88,6 +91,7 @@ extern NSString *const kRules; extern NSString *const kRuleSHA256; extern NSString *const kRulePolicy; extern NSString *const kRulePolicyWhitelist; +extern NSString *const kRulePolicyWhitelistCompiler; extern NSString *const kRulePolicyBlacklist; extern NSString *const kRulePolicySilentBlacklist; extern NSString *const kRulePolicyRemove; diff --git a/Source/santactl/Commands/sync/SNTCommandSyncConstants.m b/Source/santactl/Commands/sync/SNTCommandSyncConstants.m index c8a6f05c6..42cc8a4df 100644 --- a/Source/santactl/Commands/sync/SNTCommandSyncConstants.m +++ b/Source/santactl/Commands/sync/SNTCommandSyncConstants.m @@ -33,10 +33,13 @@ NSString *const kBlacklistRegex = @"blacklist_regex"; NSString *const kBinaryRuleCount = @"binary_rule_count"; NSString *const kCertificateRuleCount = @"certificate_rule_count"; +NSString *const kCompilerRuleCount = @"compiler_rule_count"; +NSString *const kTransitiveRuleCount = @"transitive_rule_count"; NSString *const kFCMToken = @"fcm_token"; NSString *const kFCMFullSyncInterval = @"fcm_full_sync_interval"; NSString *const kFCMGlobalRuleSyncDeadline = @"fcm_global_rule_sync_deadline"; NSString *const kBundlesEnabled = @"bundles_enabled"; +NSString *const kTransitiveWhitelistingEnabled = @"transitive_whitelisting_enabled"; NSString *const kEvents = @"events"; NSString *const kFileSHA256 = @"file_sha256"; @@ -88,6 +91,7 @@ NSString *const kRuleSHA256 = @"sha256"; NSString *const kRulePolicy = @"policy"; NSString *const kRulePolicyWhitelist = @"WHITELIST"; +NSString *const kRulePolicyWhitelistCompiler = @"WHITELIST_COMPILER"; NSString *const kRulePolicyBlacklist = @"BLACKLIST"; NSString *const kRulePolicySilentBlacklist = @"SILENT_BLACKLIST"; NSString *const kRulePolicyRemove = @"REMOVE"; diff --git a/Source/santactl/Commands/sync/SNTCommandSyncPreflight.m b/Source/santactl/Commands/sync/SNTCommandSyncPreflight.m index f18657ec6..612b8f129 100644 --- a/Source/santactl/Commands/sync/SNTCommandSyncPreflight.m +++ b/Source/santactl/Commands/sync/SNTCommandSyncPreflight.m @@ -43,9 +43,14 @@ - (BOOL)sync { dispatch_group_t group = dispatch_group_create(); dispatch_group_enter(group); - [[self.daemonConn remoteObjectProxy] databaseRuleCounts:^(int64_t binary, int64_t certificate) { + [[self.daemonConn remoteObjectProxy] databaseRuleCounts:^(int64_t binary, + int64_t certificate, + int64_t compiler, + int64_t transitive) { requestDict[kBinaryRuleCount] = @(binary); requestDict[kCertificateRuleCount] = @(certificate); + requestDict[kCompilerRuleCount] = @(compiler); + requestDict[kTransitiveRuleCount] = @(transitive); dispatch_group_leave(group); }]; @@ -86,6 +91,14 @@ - (BOOL)sync { dispatch_group_leave(group); }]; + dispatch_group_enter(group); + if ([resp[kTransitiveWhitelistingEnabled] respondsToSelector:@selector(boolValue)]) { + BOOL enabled = [resp[kTransitiveWhitelistingEnabled] boolValue]; + [[self.daemonConn remoteObjectProxy] setTransitiveWhitelistingEnabled:enabled reply:^{ + dispatch_group_leave(group); + }]; + } + self.syncState.eventBatchSize = [resp[kBatchSize] unsignedIntegerValue] ?: kDefaultEventBatchSize; self.syncState.FCMToken = resp[kFCMToken]; diff --git a/Source/santactl/Commands/sync/SNTCommandSyncRuleDownload.m b/Source/santactl/Commands/sync/SNTCommandSyncRuleDownload.m index 4db8d8a7c..2fd079ac8 100644 --- a/Source/santactl/Commands/sync/SNTCommandSyncRuleDownload.m +++ b/Source/santactl/Commands/sync/SNTCommandSyncRuleDownload.m @@ -136,6 +136,8 @@ - (SNTRule *)ruleFromDictionary:(NSDictionary *)dict { NSString *policyString = dict[kRulePolicy]; if ([policyString isEqual:kRulePolicyWhitelist]) { newRule.state = SNTRuleStateWhitelist; + } else if ([policyString isEqual:kRulePolicyWhitelistCompiler]) { + newRule.state = SNTRuleStateWhitelistCompiler; } else if ([policyString isEqual:kRulePolicyBlacklist]) { newRule.state = SNTRuleStateBlacklist; } else if ([policyString isEqual:kRulePolicySilentBlacklist]) { @@ -161,7 +163,7 @@ - (SNTRule *)ruleFromDictionary:(NSDictionary *)dict { } // Check rule for extra notification related info. - if (newRule.state == SNTRuleStateWhitelist) { + if (newRule.state == SNTRuleStateWhitelist || newRule.state == SNTRuleStateWhitelistCompiler) { // primaryHash is the bundle hash if there was a bundle hash included in the rule, otherwise // it is simply the binary hash. NSString *primaryHash = dict[kFileBundleHash]; diff --git a/Source/santad/DataLayer/SNTRuleTable.h b/Source/santad/DataLayer/SNTRuleTable.h index 57884bdc0..33093f946 100644 --- a/Source/santad/DataLayer/SNTRuleTable.h +++ b/Source/santad/DataLayer/SNTRuleTable.h @@ -35,6 +35,16 @@ /// - (NSUInteger)binaryRuleCount; +/// +/// @return Number of compiler rules in the database +/// +- (NSUInteger)compilerRuleCount; + +/// +/// @return Number of transitive rules in the database +/// +- (NSUInteger)transitiveRuleCount; + /// /// @return Number of certificate rules in the database /// @@ -58,4 +68,24 @@ /// - (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate error:(NSError **)error; +/// +/// Checks the given array of rules to see if adding any of them to the rules database would +/// require the kernel's decision cache to be flushed. This should happen if +/// 1. any of the rules is not a SNTRuleStateWhitelist +/// 2. a SNTRuleStateWhitelist rule is replacing a SNTRuleStateWhitelistCompiler rule. +/// +/// @param rules Array of SNTRule that may be added to database. +/// @return YES if kernel cache should be flushed after adding the new rules. +- (BOOL)addedRulesShouldFlushDecisionCache:(NSArray *)rules; + +/// +/// Update timestamp for given rule to the current time. +/// +- (void)resetTimestampForRule:(SNTRule *)rule; + +/// +/// Remove transitive rules that haven't been used in a long time. +/// +- (void)removeOutdatedTransitiveRules; + @end diff --git a/Source/santad/DataLayer/SNTRuleTable.m b/Source/santad/DataLayer/SNTRuleTable.m index 9d5a3b948..4862bb853 100644 --- a/Source/santad/DataLayer/SNTRuleTable.m +++ b/Source/santad/DataLayer/SNTRuleTable.m @@ -21,9 +21,16 @@ #import "SNTLogging.h" #import "SNTRule.h" +// TODO(nguyenphillip): this should be configurable. +// How many rules must be in database before we start trying to remove transitive rules. +static const NSUInteger kTransitiveRuleCullingThreshold = 500000; +// Consider transitive rules out of date if they haven't been used in six months. +static const NSUInteger kTransitiveRuleExpirationSeconds = 6 * 30 * 24 * 3600; + @interface SNTRuleTable () @property NSString *santadCertSHA; @property NSString *launchdCertSHA; +@property NSDate *lastTransitiveRuleCulling; @end @implementation SNTRuleTable @@ -65,6 +72,13 @@ - (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version { @"FROM rules " @"WHERE (shasum=? OR shasum=?) AND state=? AND type=2", self.santadCertSHA, self.launchdCertSHA, @(SNTRuleStateWhitelist)]; + + if (version < 3) { + // Add timestamp column for tracking age of transitive rules. + [db executeUpdate:@"ALTER TABLE 'rules' ADD 'timestamp' INTEGER"]; + newVersion = 3; + } + if (ruleCount != 2) { if (version > 0) LOGE(@"Started without launchd/santad certificate rules in place!"); [db executeUpdate:@"INSERT INTO rules (shasum, state, type) VALUES (?, ?, ?)", @@ -102,15 +116,30 @@ - (NSUInteger)certificateRuleCount { return count; } -- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs { - SNTRule *rule = [[SNTRule alloc] init]; +- (NSUInteger)compilerRuleCount { + __block NSUInteger count = 0; + [self inDatabase:^(FMDatabase *db) { + count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE state=?", + @(SNTRuleStateWhitelistCompiler)]; + }]; + return count; +} - rule.shasum = [rs stringForColumn:@"shasum"]; - rule.type = [rs intForColumn:@"type"]; - rule.state = [rs intForColumn:@"state"]; - rule.customMsg = [rs stringForColumn:@"custommsg"]; +- (NSUInteger)transitiveRuleCount { + __block NSUInteger count = 0; + [self inDatabase:^(FMDatabase *db) { + count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE state=?", + @(SNTRuleStateWhitelistTransitive)]; + }]; + return count; +} - return rule; +- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs { + return [[SNTRule alloc] initWithShasum:[rs stringForColumn:@"shasum"] + state:[rs intForColumn:@"state"] + type:[rs intForColumn:@"type"] + customMsg:[rs stringForColumn:@"custommsg"] + timestamp:[rs intForColumn:@"timestamp"]]; } - (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256 @@ -165,7 +194,7 @@ - (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate for (SNTRule *rule in rules) { if (![rule isKindOfClass:[SNTRule class]] || rule.shasum.length == 0 || rule.state == SNTRuleStateUnknown || rule.type == SNTRuleTypeUnknown) { - [self fillError:error code:SNTRuleTableErrorInvalidRule message:nil]; + [self fillError:error code:SNTRuleTableErrorInvalidRule message:rule.description]; *rollback = failed = YES; return; } @@ -181,9 +210,10 @@ - (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate } } else { if (![db executeUpdate:@"INSERT OR REPLACE INTO rules " - @"(shasum, state, type, custommsg) " - @"VALUES (?, ?, ?, ?);", - rule.shasum, @(rule.state), @(rule.type), rule.customMsg]) { + @"(shasum, state, type, custommsg, timestamp) " + @"VALUES (?, ?, ?, ?, ?);", + rule.shasum, @(rule.state), @(rule.type), rule.customMsg, + @(rule.timestamp)]) { [self fillError:error code:SNTRuleTableErrorInsertOrReplaceFailed message:[db lastErrorMessage]]; @@ -197,6 +227,67 @@ - (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate return !failed; } +- (BOOL)addedRulesShouldFlushDecisionCache:(NSArray *)rules { + // Check for non-plain-whitelist rules first before querying the database. + for (SNTRule *rule in rules) { + if (rule.state != SNTRuleStateWhitelist) return YES; + } + + // If still here, then all rules in the array are whitelist rules. So now we look for whitelist + // rules where there is a previously existing whitelist compiler rule for the same shasum. + // If so we find such a rule, then cache should be flushed. + __block BOOL flushDecisionCache = NO; + [self inTransaction:^(FMDatabase *db, BOOL *rollback) { + for (SNTRule *rule in rules) { + // Whitelist certificate rules are ignored + if (rule.type == SNTRuleTypeCertificate) continue; + + if ([db longForQuery: + @"SELECT COUNT(*) FROM rules WHERE shasum=? AND type=? AND state=? LIMIT 1", + rule.shasum, @(SNTRuleTypeBinary), @(SNTRuleStateWhitelistCompiler)] > 0) { + flushDecisionCache = YES; + break; + } + } + }]; + + return flushDecisionCache; +} + +// Updates the timestamp to current time for the given rule. +- (void)resetTimestampForRule:(SNTRule *)rule { + if (!rule) return; + [rule resetTimestamp]; + [self inDatabase:^(FMDatabase *db) { + if (![db executeUpdate:@"UPDATE rules SET timestamp=? WHERE shasum=? AND type=?", + @(rule.timestamp), rule.shasum, @(rule.type)]) { + LOGE(@"Could not update timestamp for rule with sha256=%@", rule.shasum); + } + }]; +} + +- (void)removeOutdatedTransitiveRules { + // Don't attempt to remove transitive rules unless it's been at least an hour since the + // last time we tried to remove them. + if (self.lastTransitiveRuleCulling && + -[self.lastTransitiveRuleCulling timeIntervalSinceNow] < 3600) return; + + // Don't bother removing rules unless rule database is large. + if ([self ruleCount] < kTransitiveRuleCullingThreshold) return; + // Determine what timestamp qualifies as outdated. + NSUInteger outdatedTimestamp = + [[NSDate date] timeIntervalSinceReferenceDate] - kTransitiveRuleExpirationSeconds; + + [self inDatabase:^(FMDatabase *db) { + if (![db executeUpdate:@"DELETE FROM rules WHERE state=? AND timestamp < ?", + @(SNTRuleStateWhitelistTransitive), @(outdatedTimestamp)]) { + LOGE(@"Could not remove outdated transitive rules"); + } + }]; + + self.lastTransitiveRuleCulling = [NSDate date]; +} + // Helper to create an NSError where necessary. // The return value is irrelevant but the static analyzer complains if it's not a BOOL. - (BOOL)fillError:(NSError **)error code:(SNTRuleTableError)code message:(NSString *)message { @@ -208,7 +299,8 @@ - (BOOL)fillError:(NSError **)error code:(SNTRuleTableError)code message:(NSStri d[NSLocalizedDescriptionKey] = @"Empty rule array"; break; case SNTRuleTableErrorInvalidRule: - d[NSLocalizedDescriptionKey] = @"Rule array contained invalid entry"; + d[NSLocalizedDescriptionKey] = + [NSString stringWithFormat:@"Rule array contained invalid entry: %@", message]; break; case SNTRuleTableErrorInsertOrReplaceFailed: d[NSLocalizedDescriptionKey] = @"A database error occurred while inserting/replacing a rule"; diff --git a/Source/santad/Logs/SNTEventLog.h b/Source/santad/Logs/SNTEventLog.h index d9e6b59e9..87f639b0a 100644 --- a/Source/santad/Logs/SNTEventLog.h +++ b/Source/santad/Logs/SNTEventLog.h @@ -30,10 +30,15 @@ - (void)logDeniedExecution:(SNTCachedDecision *)cd withMessage:(santa_message_t)message; - (void)logAllowedExecution:(santa_message_t)message; - (void)logBundleHashingEvents:(NSArray *)events; +- (void)writeLog:(NSString *)log; -// Getter and setter for cached decisions. -- (SNTCachedDecision *)cachedDecisionForMessage:(santa_message_t)message; +// Methods for storing, retrieving, and removing cached decisions. - (void)cacheDecision:(SNTCachedDecision *)cd; +- (SNTCachedDecision *)cachedDecisionForMessage:(santa_message_t)message; +- (void)forgetCachedDecisionForVnodeId:(santa_vnode_id_t)vnodeId; + +// Method used to record the freshness of transitive rules. +- (void)resetTimestampForCachedDecision:(SNTCachedDecision *)cd; // String formatter helpers. - (void)addArgsForPid:(pid_t)pid toString:(NSMutableString *)str; diff --git a/Source/santad/Logs/SNTEventLog.m b/Source/santad/Logs/SNTEventLog.m index 40ec21f91..18d2a40d3 100644 --- a/Source/santad/Logs/SNTEventLog.m +++ b/Source/santad/Logs/SNTEventLog.m @@ -21,10 +21,15 @@ #import "SNTCachedDecision.h" #import "SNTConfigurator.h" +#import "SNTDatabaseController.h" +#import "SNTRule.h" +#import "SNTRuleTable.h" @interface SNTEventLog () @property NSMutableDictionary *detailStore; @property dispatch_queue_t detailStoreQueue; +// Cache for sha256 -> date of last timestamp reset. +@property NSCache *timestampResetMap; @end @implementation SNTEventLog @@ -40,6 +45,8 @@ - (instancetype)init { _userNameMap.countLimit = 100; _groupNameMap = [[NSCache alloc] init]; _groupNameMap.countLimit = 100; + _timestampResetMap = [[NSCache alloc] init]; + _timestampResetMap.countLimit = 100; _dateFormatter = [[NSDateFormatter alloc] init]; _dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; @@ -93,6 +100,28 @@ - (SNTCachedDecision *)cachedDecisionForMessage:(santa_message_t)message { return cd; } +- (void)forgetCachedDecisionForVnodeId:(santa_vnode_id_t)vnodeId { + dispatch_sync(self.detailStoreQueue, ^{ + [self.detailStore removeObjectForKey:@(vnodeId.fileid)]; + }); +} + +// Whenever a cached decision resulting from a transitive whitelist rule is used to allow the +// execution of a binary, we update the timestamp on the transitive rule in the rules database. +// To prevent writing to the database too often, we space out consecutive writes by 3600 seconds. +- (void)resetTimestampForCachedDecision:(SNTCachedDecision *)cd { + if (cd.decision != SNTEventStateAllowTransitive) return; + NSDate *lastUpdate = [self.timestampResetMap objectForKey:cd.sha256]; + if (!lastUpdate || -[lastUpdate timeIntervalSinceNow] > 3600) { + SNTRule *rule = [[SNTRule alloc] initWithShasum:cd.sha256 + state:SNTRuleStateWhitelistTransitive + type:SNTRuleTypeBinary + customMsg:nil]; + [[SNTDatabaseController ruleTable] resetTimestampForRule:rule]; + [self.timestampResetMap setObject:[NSDate date] forKey:cd.sha256]; + } +} + /** Sanitizes a given string if necessary, otherwise returns the original. */ diff --git a/Source/santad/Logs/SNTSyslogEventLog.m b/Source/santad/Logs/SNTSyslogEventLog.m index a3b616776..a029e7b9b 100644 --- a/Source/santad/Logs/SNTSyslogEventLog.m +++ b/Source/santad/Logs/SNTSyslogEventLog.m @@ -86,6 +86,21 @@ - (void)logExecution:(santa_message_t)message withDecision:(SNTCachedDecision *) r = @"BINARY"; logArgs = YES; break; + case SNTEventStateAllowCompiler: + d = @"ALLOW"; + r = @"COMPILER"; + logArgs = YES; + break; + case SNTEventStateAllowTransitive: + d = @"ALLOW"; + r = @"TRANSITIVE"; + logArgs = YES; + break; + case SNTEventStateAllowPendingTransitive: + d = @"ALLOW"; + r = @"PENDING_TRANSITIVE"; + logArgs = YES; + break; case SNTEventStateAllowCertificate: d = @"ALLOW"; r = @"CERT"; @@ -183,6 +198,10 @@ - (void)logDeniedExecution:(SNTCachedDecision *)cd withMessage:(santa_message_t) - (void)logAllowedExecution:(santa_message_t)message { SNTCachedDecision *cd = [self cachedDecisionForMessage:message]; [self logExecution:message withDecision:cd]; + + // We also reset the timestamp for transitive rules here, because it happens to be where we + // have access to both the execution notification and the sha256 associated with rule. + [self resetTimestampForCachedDecision:cd]; } - (void)logDiskAppeared:(NSDictionary *)diskProperties { diff --git a/Source/santad/SNTApplication.m b/Source/santad/SNTApplication.m index 8793d5864..65e2cc662 100644 --- a/Source/santad/SNTApplication.m +++ b/Source/santad/SNTApplication.m @@ -19,6 +19,7 @@ #import #import "SNTCommonEnums.h" +#import "SNTCompilerController.h" #import "SNTConfigurator.h" #import "SNTDaemonControlController.h" #import "SNTDatabaseController.h" @@ -41,6 +42,7 @@ @interface SNTApplication () @property SNTDriverManager *driverManager; @property SNTEventLog *eventLog; @property SNTExecutionController *execController; +@property SNTCompilerController *compilerController; @property MOLXPCConnection *controlConnection; @property SNTNotificationQueue *notQueue; @property pid_t syncdPID; @@ -123,6 +125,10 @@ - (instancetype)init { _controlConnection.exportedObject = dc; [_controlConnection resume]; + // Initialize the transitive whitelisting controller object. + _compilerController = [[SNTCompilerController alloc] initWithDriverManager:_driverManager + eventLog:_eventLog]; + // Initialize the binary checker object _execController = [[SNTExecutionController alloc] initWithDriverManager:_driverManager ruleTable:ruleTable @@ -165,6 +171,12 @@ - (void)beginListeningForDecisionRequests { [_execController validateBinaryWithMessage:message]; break; } + case ACTION_NOTIFY_WHITELIST: { + // Determine if we should add a transitive whitelisting rule for this new file. + // Requires that writing process was a compiler and that new file is executable. + [self.compilerController createTransitiveRule:message]; + break; + } default: { LOGE(@"Received decision request without a valid action: %d", message.action); exit(1); diff --git a/Source/santad/SNTCompilerController.h b/Source/santad/SNTCompilerController.h new file mode 100644 index 000000000..37a810592 --- /dev/null +++ b/Source/santad/SNTCompilerController.h @@ -0,0 +1,31 @@ +/// Copyright 2017 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import +#import "SNTKernelCommon.h" + +@class SNTDriverManager; +@class SNTEventLog; + +@interface SNTCompilerController : NSObject +// Designated initializer takes a SNTEventLog instance so that we can +// call saveDecisionDetails: to create a fake cached decision for transitive +// rule creation requests that are still pending. +- (instancetype)initWithDriverManager:(SNTDriverManager *)driverManager + eventLog:(SNTEventLog *)eventLog; + +// Whenever an executable file is closed or renamed whitelist the resulting file. +// We assume that we have already determined that the writing process was a compiler. +- (void)createTransitiveRule:(santa_message_t)message; +@end diff --git a/Source/santad/SNTCompilerController.m b/Source/santad/SNTCompilerController.m new file mode 100644 index 000000000..2abb89684 --- /dev/null +++ b/Source/santad/SNTCompilerController.m @@ -0,0 +1,100 @@ +/// Copyright 2017 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import "SNTCompilerController.h" + +#import "SNTCachedDecision.h" +#import "SNTCommonEnums.h" +#import "SNTDatabaseController.h" +#import "SNTDriverManager.h" +#import "SNTEventLog.h" +#import "SNTFileInfo.h" +#import "SNTKernelCommon.h" +#import "SNTLogging.h" +#import "SNTRule.h" +#import "SNTRuleTable.h" + +@interface SNTCompilerController () +@property SNTDriverManager *driverManager; +@property SNTEventLog *eventLog; +@end + +@implementation SNTCompilerController + +- (instancetype)initWithDriverManager:(SNTDriverManager *)driverManager + eventLog:(SNTEventLog *)eventLog { + self = [super init]; + if (self) { + _driverManager = driverManager; + _eventLog = eventLog; + } + return self; +} + +// Adds a fake cached decision to SNTEventLog for pending files. If the file +// is executed before we can create a transitive rule for it, then we can at +// least log the pending decision info. +- (void)saveFakeDecision:(santa_message_t)message { + SNTCachedDecision *cd = [[SNTCachedDecision alloc] init]; + cd.decision = SNTEventStateAllowPendingTransitive; + cd.vnodeId = message.vnode_id; + cd.sha256 = @"pending"; + [self.eventLog cacheDecision:cd]; +} + +- (void)removeFakeDecision:(santa_message_t)message { + [self.eventLog forgetCachedDecisionForVnodeId:message.vnode_id]; +} + +// Assume that this method is called only when we already know that the writing process is a +// compiler. It checks if the closed file is executable, and if so, transitively whitelists it. +// The passed in message contains the pid of the writing process and path of closed file. +- (void)createTransitiveRule:(santa_message_t)message { + [self saveFakeDecision:message]; + + char *target = message.path; + + // Check if this file is an executable. + SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:@(target)]; + if (fi.isExecutable) { + // Check if there is an existing (non-transitive) rule for this file. We leave existing rules + // alone, so that a whitelist or blacklist rule can't be overwritten by a transitive one. + SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable]; + SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256 certificateSHA256:nil]; + if (!prevRule || prevRule.state == SNTRuleStateWhitelistTransitive) { + // Construct a new transitive whitelist rule for the executable. + SNTRule *rule = [[SNTRule alloc] initWithShasum:fi.SHA256 + state:SNTRuleStateWhitelistTransitive + type:SNTRuleTypeBinary + customMsg:@""]; + + // Add the new rule to the rules database. + NSError *err; + if (![ruleTable addRules:@[ rule ] cleanSlate:NO error:&err]) { + LOGE(@"unable to add new transitive rule to database: %@", err.localizedDescription); + } else { + [self.eventLog + writeLog:[NSString stringWithFormat:@"action=WHITELIST|pid=%d|path=%s|sha256=%@", + message.pid, target, fi.SHA256]]; + } + } + } + + // Remove the temporary allow rule in the kernel decision cache. + [self.driverManager removeCacheEntryForVnodeID:message.vnode_id]; + // Remove the "pending" decision info from SNTEventLog. + [self removeFakeDecision:message]; +} + +@end diff --git a/Source/santad/SNTDaemonControlController.m b/Source/santad/SNTDaemonControlController.m index 97b08177d..f7515444d 100644 --- a/Source/santad/SNTDaemonControlController.m +++ b/Source/santad/SNTDaemonControlController.m @@ -93,21 +93,34 @@ - (void)driverConnectionEstablished:(void (^)(BOOL))reply { #pragma mark Database ops -- (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate))reply { +- (void)databaseRuleCounts:(void (^)(int64_t binary, + int64_t certificate, + int64_t compiler, + int64_t transitive))reply { SNTRuleTable *rdb = [SNTDatabaseController ruleTable]; - reply([rdb binaryRuleCount], [rdb certificateRuleCount]); + reply([rdb binaryRuleCount], [rdb certificateRuleCount], + [rdb compilerRuleCount], [rdb transitiveRuleCount]); } - (void)databaseRuleAddRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate reply:(void (^)(NSError *error))reply { + SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable]; + + // If any rules are added that are not plain whitelist rules, then flush decision cache. + // In particular, the addition of whitelist compiler rules should cause a cache flush. + // We also flush cache if a whitelist compiler rule is replaced with a whitelist rule. + BOOL flushCache = (cleanSlate || [ruleTable addedRulesShouldFlushDecisionCache:rules]); + NSError *error; - [[SNTDatabaseController ruleTable] addRules:rules cleanSlate:cleanSlate error:&error]; + [ruleTable addRules:rules cleanSlate:cleanSlate error:&error]; - // If any rules were added that were not whitelist, flush cache. - NSPredicate *p = [NSPredicate predicateWithFormat:@"SELF.state != %d", SNTRuleStateWhitelist]; - if ([rules filteredArrayUsingPredicate:p].count || cleanSlate) { - LOGI(@"Received non-whitelist rule, flushing cache"); + // Whenever we add rules, we can also check for and remove outdated transitive rules. + [ruleTable removeOutdatedTransitiveRules]; + + // The actual cache flushing happens after the new rules have been added to the database. + if (flushCache) { + LOGI(@"Flushing decision cache"); [self.driverManager flushCache]; } @@ -220,6 +233,15 @@ - (void)setBundlesEnabled:(BOOL)bundlesEnabled reply:(void (^)(void))reply { reply(); } +- (void)transitiveWhitelistingEnabled:(void (^)(BOOL))reply { + reply([SNTConfigurator configurator].transitiveWhitelistingEnabled); +} + +- (void)setTransitiveWhitelistingEnabled:(BOOL)enabled reply:(void (^)(void))reply { + [[SNTConfigurator configurator] setTransitiveWhitelistingEnabled:enabled]; + reply(); +} + #pragma mark GUI Ops - (void)setNotificationListener:(NSXPCListenerEndpoint *)listener { diff --git a/Source/santad/SNTDriverManager.h b/Source/santad/SNTDriverManager.h index d9e44ff2b..11a01a916 100644 --- a/Source/santad/SNTDriverManager.h +++ b/Source/santad/SNTDriverManager.h @@ -64,11 +64,16 @@ - (BOOL)flushCache; /// -/// Check the kernel cache for a VnodeID +/// Check the kernel cache for a VnodeID. /// - (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID; /// Returns whether the connection to the driver has been established. @property(readonly) BOOL connectionEstablished; +/// +/// Remove single entry from the kernel cache for given VnodeID. +/// +- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeId; + @end diff --git a/Source/santad/SNTDriverManager.m b/Source/santad/SNTDriverManager.m index c56f16efc..a10963012 100644 --- a/Source/santad/SNTDriverManager.m +++ b/Source/santad/SNTDriverManager.m @@ -201,6 +201,13 @@ - (kern_return_t)postToKernelAction:(santa_action_t)action forVnodeID:(santa_vno sizeof(vnodeId), 0, 0); + case ACTION_RESPOND_ALLOW_COMPILER: + return IOConnectCallStructMethod(_connection, + kSantaUserClientAllowCompiler, + &vnodeId, + sizeof(vnodeId), + 0, + 0); default: return KERN_INVALID_ARGUMENT; } @@ -229,6 +236,15 @@ - (BOOL)flushCache { 0) == KERN_SUCCESS; } +- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeId { + return IOConnectCallStructMethod(_connection, + kSantaUserClientRemoveCacheEntry, + &vnodeId, + sizeof(vnodeId), + 0, + 0); +} + - (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID { uint64_t output; uint32_t outputCnt = 1; diff --git a/Source/santad/SNTExecutionController.m b/Source/santad/SNTExecutionController.m index f40d7a747..d6ffb6631 100644 --- a/Source/santad/SNTExecutionController.m +++ b/Source/santad/SNTExecutionController.m @@ -126,25 +126,37 @@ - (void)validateBinaryWithMessage:(santa_message_t)message { if (csError) csInfo = nil; } - // Actually make the decision. + // Actually make the decision (and refresh rule access timestamp). SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo fileSHA256:nil certificateSHA256:csInfo.leafCertificate.SHA256]; cd.certCommonName = csInfo.leafCertificate.commonName; cd.vnodeId = message.vnode_id; - // Formulate an action from the decision + // Formulate an initial action from the decision. santa_action_t action = (SNTEventStateAllow & cd.decision) ? ACTION_RESPOND_ALLOW : ACTION_RESPOND_DENY; - // Save decision details for logging the execution later. - if (action == ACTION_RESPOND_ALLOW) [_eventLog cacheDecision:cd]; + // Upgrade the action to ACTION_RESPOND_ALLOW_COMPILER when appropriate, because we want the + // kernel to track this information in its decision cache. + if (cd.decision == SNTEventStateAllowCompiler) { + action = ACTION_RESPOND_ALLOW_COMPILER; + } + + // Save decision details for logging the execution later. For transitive rules, we also use + // the shasum stored in the decision details to update the rule's timestamp whenever an + // ACTION_NOTIFY_EXEC message related to the transitive rule is received. + if (action == ACTION_RESPOND_ALLOW || action == ACTION_RESPOND_ALLOW_COMPILER) { + [_eventLog cacheDecision:cd]; + } // Send the decision to the kernel. [_driverManager postToKernelAction:action forVnodeID:message.vnode_id]; // Log to database if necessary. if (cd.decision != SNTEventStateAllowBinary && + cd.decision != SNTEventStateAllowCompiler && + cd.decision != SNTEventStateAllowTransitive && cd.decision != SNTEventStateAllowCertificate && cd.decision != SNTEventStateAllowScope) { SNTStoredEvent *se = [[SNTStoredEvent alloc] init]; @@ -188,7 +200,7 @@ - (void)validateBinaryWithMessage:(santa_message_t)message { }); // If binary was blocked, do the needful - if (action != ACTION_RESPOND_ALLOW) { + if (action != ACTION_RESPOND_ALLOW && action != ACTION_RESPOND_ALLOW_COMPILER) { [_eventLog logDeniedExecution:cd withMessage:message]; if ([[SNTConfigurator configurator] bundlesEnabled] && binInfo.bundle) { diff --git a/Source/santad/SNTPolicyProcessor.m b/Source/santad/SNTPolicyProcessor.m index 0b6c30432..a5666eba0 100644 --- a/Source/santad/SNTPolicyProcessor.m +++ b/Source/santad/SNTPolicyProcessor.m @@ -39,6 +39,7 @@ - (instancetype)initWithRuleTable:(SNTRuleTable *)ruleTable { - (SNTCachedDecision *)decisionForFileInfo:(SNTFileInfo *)fileInfo fileSHA256:(NSString *)fileSHA256 certificateSHA256:(NSString *)certificateSHA256 { + SNTCachedDecision *cd = [[SNTCachedDecision alloc] init]; cd.sha256 = fileSHA256 ?: fileInfo.SHA256; cd.certSHA256 = certificateSHA256; @@ -59,6 +60,26 @@ - (SNTCachedDecision *)decisionForFileInfo:(SNTFileInfo *)fileInfo cd.customMsg = rule.customMsg; cd.decision = SNTEventStateBlockBinary; return cd; + case SNTRuleStateWhitelistCompiler: + // If transitive whitelisting is enabled, then SNTRuleStateWhiteListCompiler rules + // become SNTEventStateAllowCompiler decisions. Otherwise we treat the rule as if + // it were SNTRuleStateWhitelist. + if ([[SNTConfigurator configurator] transitiveWhitelistingEnabled]) { + cd.decision = SNTEventStateAllowCompiler; + } else { + cd.decision = SNTEventStateAllow; + } + return cd; + case SNTRuleStateWhitelistTransitive: + // If transitive whitelisting is enabled, then SNTRuleStateWhitelistTransitive + // rules become SNTEventStateAllowTransitive decisions. Otherwise, we treat the + // rule as if it were SNTRuleStateUnknown. + if ([[SNTConfigurator configurator] transitiveWhitelistingEnabled]) { + cd.decision = SNTEventStateAllowTransitive; + return cd; + } else { + rule.state = SNTRuleStateUnknown; + } default: break; } break; diff --git a/Tests/KernelTests/main.mm b/Tests/KernelTests/main.mm index b3e70c6f8..b439a29f4 100644 --- a/Tests/KernelTests/main.mm +++ b/Tests/KernelTests/main.mm @@ -99,20 +99,53 @@ - (NSString *)sha256ForPath:(NSString *)path { return @(buf); } +/// Return the path to the version of ld being used by clang. +- (NSString *)ldPath { + static NSString *path; + if (!path) { + NSTask *xcrun = [self taskWithPath:@"/usr/bin/xcrun"]; + xcrun.arguments = @[@"-f", @"ld"]; + xcrun.standardOutput = [NSPipe pipe]; + @try { + [xcrun launch]; + [xcrun waitUntilExit]; + } @catch (NSException *exception) { + return nil; + } + if (xcrun.terminationStatus != 0) return nil; + NSData *data = [[xcrun.standardOutput fileHandleForReading] readDataToEndOfFile]; + if (!data) return nil; + path = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + } + return path; +} + #pragma mark - Driver Helpers /// Call in-kernel function: |kSantaUserClientAllowBinary| or |kSantaUserClientDenyBinary| /// passing the |vnodeID|. - (void)postToKernelAction:(santa_action_t)action forVnodeID:(santa_vnode_id_t)vnodeid { - if (action == ACTION_RESPOND_DENY) { - IOConnectCallStructMethod(self.connection, kSantaUserClientDenyBinary, - &vnodeid, sizeof(vnodeid), 0, 0); - } else if (action == ACTION_RESPOND_ACK) { - IOConnectCallStructMethod(self.connection, kSantaUserClientAcknowledgeBinary, - &vnodeid, sizeof(vnodeid), 0, 0); - } else { - IOConnectCallStructMethod(self.connection, kSantaUserClientAllowBinary, - &vnodeid, sizeof(vnodeid), 0, 0); + switch (action) { + case ACTION_RESPOND_ALLOW: + IOConnectCallStructMethod(self.connection, kSantaUserClientAllowBinary, + &vnodeid, sizeof(vnodeid), 0, 0); + break; + case ACTION_RESPOND_DENY: + IOConnectCallStructMethod(self.connection, kSantaUserClientDenyBinary, + &vnodeid, sizeof(vnodeid), 0, 0); + break; + case ACTION_RESPOND_ACK: + IOConnectCallStructMethod(self.connection, kSantaUserClientAcknowledgeBinary, + &vnodeid, sizeof(vnodeid), 0, 0); + break; + case ACTION_RESPOND_ALLOW_COMPILER: + IOConnectCallStructMethod(self.connection, kSantaUserClientAllowCompiler, + &vnodeid, sizeof(vnodeid), 0, 0); + break; + default: + TFAILINFO("postToKernelAction:forVnodeID: received unknown action type: %d", action); + break; } } @@ -547,6 +580,127 @@ - (void)testLargeBinary { TPASS(); } +- (void)testPendingTransitiveRules { + TSTART("Adds pending transitive whitelist rules"); + + NSString *ldPath = [self ldPath]; + if (!ldPath) { + TFAILINFO("Couldn't get path to ld"); + } + + // Clear out cached decisions from any previous tests. + [self flushCache]; + + __block int ldCount = 0; + __block int helloCount = 0; + self.handlerBlock = ^santa_action_t(santa_message_t msg) { + if (!strcmp(ldPath.UTF8String, msg.path)) { + ldCount++; + return ACTION_RESPOND_ALLOW_COMPILER; + } else if (!strcmp("/private/tmp/hello", msg.path)) { + helloCount++; + return ACTION_RESPOND_DENY; + } + return ACTION_RESPOND_ALLOW; + }; + // Write source file to /tmp/hello.c + FILE *out = fopen("/tmp/hello.c", "wb"); + fprintf(out, "#include \nint main(void) { printf(\"Hello, world!\\n\"); }"); + fclose(out); + // Then compile it with clang and ld, the latter of which has been marked as a compiler. + NSTask *clang = [self taskWithPath:@"/usr/bin/clang"]; + clang.arguments = @[@"-o", @"/private/tmp/hello", @"/private/tmp/hello.c"]; + [clang launch]; + [clang waitUntilExit]; + // Make sure that our version of ld marked as compiler was run. This assumes that + // "xcode-select -p" returns "/Applications/Xcode.app/Contents/Developer" + if (ldCount != 1) { + TFAILINFO("Didn't record run of ld"); + } + // Check if we can now run /private/tmp/hello. If working correctly, there will already be + // a pending transitive rule in the cache, so no decision request will be sent to listener. + // If for some reason a decision request is sent, then binary will be denied. + NSTask *hello = [self taskWithPath:@"/private/tmp/hello"]; + @try { + [hello launch]; + [hello waitUntilExit]; + } @catch (NSException *exception) { + TFAILINFO("could not launch /private/tmp/hello: %s", exception.reason.UTF8String); + } + // Check that the listener was not consulted for the decision. + if (helloCount > 0) { + TFAILINFO("pending decision for /private/tmp/hello was not in cache"); + } + + // Clean up + remove("/tmp/hello"); + remove("/tmp/hello.c"); + + TPASS(); +} + +- (void)testNoTransitiveRules { + TSTART("No transitive rule generated by non-compiler"); + + NSString *ldPath = [self ldPath]; + if (!ldPath) { + TFAILINFO("Couldn't get path to ld"); + } + + // Clear out cached decisions from any previous tests. + [self flushCache]; + + __block int ldCount = 0; + __block int helloCount = 0; + self.handlerBlock = ^santa_action_t(santa_message_t msg) { + if (!strcmp(ldPath.UTF8String, msg.path)) { + ldCount++; + return ACTION_RESPOND_ALLOW; + } else if (!strcmp("/private/tmp/hello", msg.path)) { + helloCount++; + return ACTION_RESPOND_DENY; + } + return ACTION_RESPOND_ALLOW; + }; + // Write source file to /tmp/hello.c + FILE *out = fopen("/tmp/hello.c", "wb"); + fprintf(out, "#include \nint main(void) { printf(\"Hello, world!\\n\"); }"); + fclose(out); + // Then compile it with clang and ld, neither of which have been marked as a compiler. + NSTask *clang = [self taskWithPath:@"/usr/bin/clang"]; + clang.arguments = @[@"-o", @"/private/tmp/hello", @"/private/tmp/hello.c"]; + @try { + [clang launch]; + [clang waitUntilExit]; + } @catch (NSException *exception) { + TFAILINFO("Couldn't launch clang"); + } + // Make sure that our version of ld was run. This assumes that "xcode-select -p" + // returns "/Applications/Xcode.app/Contents/Developer" + if (ldCount != 1) { + TFAILINFO("Didn't record run of ld"); + } + // Check that we cannot run /private/tmp/hello. + NSTask *hello = [self taskWithPath:@"/private/tmp/hello"]; + @try { + [hello launch]; + [hello waitUntilExit]; + TFAILINFO("Should not have been able to launch /private/tmp/hello"); + } @catch (NSException *exception) { + TPASS(); + } + // Check that there wasn't a decision for /private/tmp/hello in the cache. + if (helloCount != 1) { + TFAILINFO("decision for /private/tmp/hello found in cache"); + } + + // Clean up + remove("/tmp/hello"); + remove("/tmp/hello.c"); + + TPASS(); +} + #pragma mark - Main - (void)unloadDaemon { @@ -620,6 +774,8 @@ - (void)runTests { [self clearCacheTests]; [self blocksDeniedTracedBinaries]; [self testLargeBinary]; + [self testPendingTransitiveRules]; + [self testNoTransitiveRules]; printf("\n-> Performance tests:\n"); [self testCachePerformance]; diff --git a/Tests/LogicTests/SNTCommandSyncTest.m b/Tests/LogicTests/SNTCommandSyncTest.m index a539b12d6..75c1020f4 100644 --- a/Tests/LogicTests/SNTCommandSyncTest.m +++ b/Tests/LogicTests/SNTCommandSyncTest.m @@ -191,15 +191,20 @@ - (void)testPreflightBasicResponse { - (void)testPreflightDatabaseCounts { SNTCommandSyncPreflight *sut = [[SNTCommandSyncPreflight alloc] initWithState:self.syncState]; - int64_t bin = 5, cert = 8; - OCMStub([self.daemonConnRop databaseRuleCounts:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(bin), - OCMOCK_VALUE(cert), - nil])]); + int64_t bin = 5, cert = 8, compiler = 2, transitive = 19; + OCMStub([self.daemonConnRop databaseRuleCounts:([OCMArg invokeBlockWithArgs: + OCMOCK_VALUE(bin), + OCMOCK_VALUE(cert), + OCMOCK_VALUE(compiler), + OCMOCK_VALUE(transitive), + nil])]); [self stubRequestBody:nil response:nil error:nil validateBlock:^BOOL(NSURLRequest *req) { NSDictionary *requestDict = [self dictFromRequest:req]; XCTAssertEqualObjects(requestDict[kBinaryRuleCount], @(5)); XCTAssertEqualObjects(requestDict[kCertificateRuleCount], @(8)); + XCTAssertEqualObjects(requestDict[kCompilerRuleCount], @(2)); + XCTAssertEqualObjects(requestDict[kTransitiveRuleCount], @(19)); return YES; }]; diff --git a/Tests/LogicTests/SNTExecutionControllerTest.m b/Tests/LogicTests/SNTExecutionControllerTest.m index 7171fe28c..64c4ed586 100644 --- a/Tests/LogicTests/SNTExecutionControllerTest.m +++ b/Tests/LogicTests/SNTExecutionControllerTest.m @@ -165,6 +165,71 @@ - (void)testCertificateBlacklistRule { forVnodeID:[self getVnodeId]]); } +- (void)testBinaryWhitelistCompilerRule { + OCMStub([self.mockFileInfo isMachO]).andReturn(YES); + OCMStub([self.mockFileInfo SHA256]).andReturn(@"a"); + OCMStub([self.mockConfigurator transitiveWhitelistingEnabled]).andReturn(YES); + + SNTRule *rule = [[SNTRule alloc] init]; + rule.state = SNTRuleStateWhitelistCompiler; + rule.type = SNTRuleTypeBinary; + OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil]).andReturn(rule); + + [self.sut validateBinaryWithMessage:[self getMessage]]; + + OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_ALLOW_COMPILER + forVnodeID:[self getVnodeId]]); +} + +- (void)testBinaryWhitelistCompilerRuleDisabled { + OCMStub([self.mockFileInfo isMachO]).andReturn(YES); + OCMStub([self.mockFileInfo SHA256]).andReturn(@"a"); + OCMStub([self.mockConfigurator transitiveWhitelistingEnabled]).andReturn(NO); + + SNTRule *rule = [[SNTRule alloc] init]; + rule.state = SNTRuleStateWhitelistCompiler; + rule.type = SNTRuleTypeBinary; + OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil]).andReturn(rule); + + [self.sut validateBinaryWithMessage:[self getMessage]]; + + OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_ALLOW + forVnodeID:[self getVnodeId]]); +} + +- (void)testBinaryWhitelistTransitiveRule { + OCMStub([self.mockFileInfo isMachO]).andReturn(YES); + OCMStub([self.mockFileInfo SHA256]).andReturn(@"a"); + OCMStub([self.mockConfigurator transitiveWhitelistingEnabled]).andReturn(YES); + + SNTRule *rule = [[SNTRule alloc] init]; + rule.state = SNTRuleStateWhitelistTransitive; + rule.type = SNTRuleTypeBinary; + OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil]).andReturn(rule); + + [self.sut validateBinaryWithMessage:[self getMessage]]; + + OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_ALLOW + forVnodeID:[self getVnodeId]]); +} + +- (void)testBinaryWhitelistTransitiveRuleDisabled { + OCMStub([self.mockFileInfo isMachO]).andReturn(YES); + OCMStub([self.mockFileInfo SHA256]).andReturn(@"a"); + OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown); + OCMStub([self.mockConfigurator transitiveWhitelistingEnabled]).andReturn(NO); + + SNTRule *rule = [[SNTRule alloc] init]; + rule.state = SNTRuleStateWhitelistTransitive; + rule.type = SNTRuleTypeBinary; + OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil]).andReturn(rule); + + [self.sut validateBinaryWithMessage:[self getMessage]]; + + OCMVerify([self.mockDriverManager postToKernelAction:ACTION_RESPOND_DENY + forVnodeID:[self getVnodeId]]); +} + - (void)testDefaultDecision { OCMStub([self.mockFileInfo isMachO]).andReturn(YES); OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");