mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01df4623c7 | ||
|
|
c9cb91a22e | ||
|
|
1f9d60aecc | ||
|
|
52c5b5aade | ||
|
|
2d98173c51 | ||
|
|
5e3f13be70 | ||
|
|
90b894b88a | ||
|
|
6dc7387881 | ||
|
|
b14b017d72 |
@@ -7,11 +7,11 @@ PODS:
|
||||
- MOLCertificate (1.9)
|
||||
- MOLCodesignChecker (1.10):
|
||||
- MOLCertificate (~> 1.8)
|
||||
- MOLFCMClient (1.7):
|
||||
- MOLFCMClient (1.8):
|
||||
- MOLAuthenticatingURLSession (~> 2.4)
|
||||
- MOLXPCConnection (1.2):
|
||||
- MOLCodesignChecker (~> 1.9)
|
||||
- OCMock (3.4.1)
|
||||
- OCMock (3.4.2)
|
||||
|
||||
DEPENDENCIES:
|
||||
- FMDB
|
||||
@@ -37,9 +37,9 @@ SPEC CHECKSUMS:
|
||||
MOLAuthenticatingURLSession: c238aa1c9a7b1077eb39a6f40204bfe76a7d204e
|
||||
MOLCertificate: e9e88a396c57032cab847f51a46e20c730cd752a
|
||||
MOLCodesignChecker: b0d5db9d2f9bd94e0fd093891a5d40e5ad77cbc0
|
||||
MOLFCMClient: ee45348909351f232e2759c580329072ae7e02d4
|
||||
MOLFCMClient: 2bfbacd45cc11e1ca3c077e97b80401c4e4a54f1
|
||||
MOLXPCConnection: c27af5cb1c43b18319698b0e568a8ddc2fc1e306
|
||||
OCMock: 2cd0716969bab32a2283ff3a46fd26a8c8b4c5e3
|
||||
OCMock: ebe9ee1dca7fbed0ff9193ac0b3e2d8862ea56f6
|
||||
|
||||
PODFILE CHECKSUM: ddca043a7ace9ec600c108621c56d13a50d17236
|
||||
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
81133DAF1F3A75CE00917FF9 /* SNTCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SNTCommand.m; sourceTree = "<group>"; };
|
||||
81A00E7D1FD74EFF00A84676 /* SNTCompilerController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SNTCompilerController.h; sourceTree = "<group>"; };
|
||||
81A00E7E1FD74EFF00A84676 /* SNTCompilerController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SNTCompilerController.m; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -196,7 +196,16 @@
|
||||
/// If YES, enables bundle detection for blocked events. This property is not stored on disk.
|
||||
/// Its value is set by a sync server that supports bundles. Defaults to NO.
|
||||
///
|
||||
@property BOOL bundlesEnabled;
|
||||
@property BOOL enableBundles;
|
||||
|
||||
#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 enableTransitiveWhitelisting;
|
||||
|
||||
#pragma mark Server Auth Settings
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ static NSString *const kEnableMachineIDDecoration = @"EnableMachineIDDecoration"
|
||||
|
||||
// The keys managed by a sync server or mobileconfig.
|
||||
static NSString *const kClientModeKey = @"ClientMode";
|
||||
static NSString *const kEnableTransitiveWhitelistingKey = @"EnableTransitiveWhitelisting";
|
||||
static NSString *const kWhitelistRegexKey = @"WhitelistRegex";
|
||||
static NSString *const kBlacklistRegexKey = @"BlacklistRegex";
|
||||
|
||||
@@ -94,6 +95,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
Class data = [NSData class];
|
||||
_syncServerKeyTypes = @{
|
||||
kClientModeKey : number,
|
||||
kEnableTransitiveWhitelistingKey : number,
|
||||
kWhitelistRegexKey : re,
|
||||
kBlacklistRegexKey : re,
|
||||
kFullSyncLastSuccess : date,
|
||||
@@ -102,6 +104,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
};
|
||||
_forcedConfigKeyTypes = @{
|
||||
kClientModeKey : number,
|
||||
kEnableTransitiveWhitelistingKey : number,
|
||||
kFileChangesRegexKey : re,
|
||||
kWhitelistRegexKey : re,
|
||||
kBlacklistRegexKey : re,
|
||||
@@ -287,6 +290,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableTransitiveWhitelisting {
|
||||
return [self syncAndConfigStateSet];
|
||||
}
|
||||
|
||||
#pragma mark Public Interface
|
||||
|
||||
- (SNTClientMode)clientMode {
|
||||
@@ -311,6 +318,18 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)enableTransitiveWhitelisting {
|
||||
NSNumber *n = self.syncState[kEnableTransitiveWhitelistingKey];
|
||||
if (n) {
|
||||
return [n boolValue];
|
||||
}
|
||||
return [self.configState[kEnableTransitiveWhitelistingKey] boolValue];
|
||||
}
|
||||
|
||||
- (void)setEnableTransitiveWhitelisting:(BOOL)enabled {
|
||||
[self updateSyncStateForKey:kEnableTransitiveWhitelistingKey value:@(enabled)];
|
||||
}
|
||||
|
||||
- (NSRegularExpression *)whitelistPathRegex {
|
||||
return self.syncState[kWhitelistRegexKey] ?: self.configState[kWhitelistRegexKey];
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
ENCODE(@(self.state), @"state");
|
||||
ENCODE(@(self.type), @"type");
|
||||
ENCODE(self.customMsg, @"custommsg");
|
||||
ENCODE(@(self.timestamp), @"timestamp");
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)decoder {
|
||||
@@ -55,6 +79,7 @@
|
||||
_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 @@
|
||||
}
|
||||
|
||||
- (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
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
- (void)setSyncCleanRequired:(BOOL)cleanReqd reply:(void (^)(void))reply;
|
||||
- (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)setEnableBundles:(BOOL)bundlesEnabled reply:(void (^)(void))reply;
|
||||
- (void)setEnableTransitiveWhitelisting:(BOOL)enabled reply:(void (^)(void))reply;
|
||||
|
||||
///
|
||||
/// Syncd Ops
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
///
|
||||
/// Kernel ops
|
||||
///
|
||||
- (void)cacheCounts:(void (^)(uint64_t count))reply;
|
||||
- (void)cacheCounts:(void (^)(uint64_t rootCache, uint64_t nonRootCache))reply;
|
||||
- (void)cacheBucketCount:(void (^)(NSArray *))reply;
|
||||
- (void)checkCacheForVnodeID:(santa_vnode_id_t)vnodeID withReply:(void (^)(santa_action_t))reply;
|
||||
- (void)driverConnectionEstablished:(void (^)(BOOL))reply;
|
||||
@@ -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;
|
||||
|
||||
///
|
||||
@@ -70,7 +73,8 @@
|
||||
- (void)fullSyncLastSuccess:(void (^)(NSDate *))reply;
|
||||
- (void)ruleSyncLastSuccess:(void (^)(NSDate *))reply;
|
||||
- (void)syncCleanRequired:(void (^)(BOOL))reply;
|
||||
- (void)bundlesEnabled:(void (^)(BOOL))reply;
|
||||
- (void)enableBundles:(void (^)(BOOL))reply;
|
||||
- (void)enableTransitiveWhitelisting:(void (^)(BOOL))reply;
|
||||
|
||||
///
|
||||
/// GUI Ops
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
<string>IOKit</string>
|
||||
<key>IOUserClientClass</key>
|
||||
<string>com_google_SantaDriverClient</string>
|
||||
<key>IOMatchCategory</key>
|
||||
<string>com_google_SantaDriver</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>OSBundleLibraries</key>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -33,8 +41,10 @@ bool SantaDecisionManager::init() {
|
||||
decision_dataqueue_lock_ = lck_mtx_alloc_init(sdm_lock_grp_, sdm_lock_attr_);
|
||||
log_dataqueue_lock_ = lck_mtx_alloc_init(sdm_lock_grp_, sdm_lock_attr_);
|
||||
|
||||
decision_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>(10000, 2);
|
||||
root_decision_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>(5000, 2);
|
||||
non_root_decision_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>(500, 2);
|
||||
vnode_pid_map_ = new SantaCache<santa_vnode_id_t, uint64_t>(2000, 5);
|
||||
compiler_pid_set_ = new SantaCache<pid_t, pid_t>(500, 5);
|
||||
|
||||
decision_dataqueue_ = IOSharedDataQueue::withEntries(
|
||||
kMaxDecisionQueueEvents, sizeof(santa_message_t));
|
||||
@@ -45,14 +55,18 @@ bool SantaDecisionManager::init() {
|
||||
if (!log_dataqueue_) return kIOReturnNoMemory;
|
||||
|
||||
client_pid_ = 0;
|
||||
root_fsid_ = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SantaDecisionManager::free() {
|
||||
delete decision_cache_;
|
||||
delete root_decision_cache_;
|
||||
delete non_root_decision_cache_;
|
||||
delete vnode_pid_map_;
|
||||
|
||||
StopPidMonitorThreads();
|
||||
|
||||
if (decision_dataqueue_lock_) {
|
||||
lck_mtx_free(decision_dataqueue_lock_, sdm_lock_grp_);
|
||||
decision_dataqueue_lock_ = nullptr;
|
||||
@@ -91,6 +105,17 @@ void SantaDecisionManager::ConnectClient(pid_t pid) {
|
||||
|
||||
client_pid_ = pid;
|
||||
|
||||
// Determine root fsid
|
||||
vfs_context_t ctx = vfs_context_create(nullptr);
|
||||
if (ctx) {
|
||||
vnode_t root = vfs_rootvnode();
|
||||
if (root) {
|
||||
root_fsid_ = GetVnodeIDForVnode(ctx, root).fsid;
|
||||
vnode_put(root);
|
||||
}
|
||||
vfs_context_rele(ctx);
|
||||
}
|
||||
|
||||
// Any decisions made while the daemon wasn't
|
||||
// connected should be cleared
|
||||
ClearCache();
|
||||
@@ -195,27 +220,131 @@ 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
|
||||
|
||||
/**
|
||||
Return the correct cache for a given identifier.
|
||||
|
||||
@param identifier The identifier
|
||||
@return SantaCache* The cache to use
|
||||
*/
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *SantaDecisionManager::CacheForIdentifier(
|
||||
const santa_vnode_id_t identifier) {
|
||||
return (identifier.fsid == root_fsid_) ? root_decision_cache_ : non_root_decision_cache_;
|
||||
}
|
||||
|
||||
void SantaDecisionManager::AddToCache(
|
||||
santa_vnode_id_t identifier, santa_action_t decision, uint64_t microsecs) {
|
||||
auto decision_cache = CacheForIdentifier(identifier);
|
||||
|
||||
switch (decision) {
|
||||
case ACTION_REQUEST_BINARY:
|
||||
decision_cache_->set(identifier, (uint64_t)ACTION_REQUEST_BINARY << 56, 0);
|
||||
decision_cache->set(identifier, (uint64_t)ACTION_REQUEST_BINARY << 56, 0);
|
||||
break;
|
||||
case ACTION_RESPOND_ACK:
|
||||
decision_cache_->set(identifier, (uint64_t)ACTION_RESPOND_ACK << 56,
|
||||
decision_cache->set(identifier, (uint64_t)ACTION_RESPOND_ACK << 56,
|
||||
((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);
|
||||
if (!decision_cache_->set(identifier, val, ((uint64_t)ACTION_REQUEST_BINARY << 56))) {
|
||||
decision_cache_->set(identifier, val, ((uint64_t)ACTION_RESPOND_ACK << 56));
|
||||
if (!decision_cache->set(identifier, val, ((uint64_t)ACTION_REQUEST_BINARY << 56))) {
|
||||
decision_cache->set(identifier, val, ((uint64_t)ACTION_RESPOND_ACK << 56));
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -225,21 +354,26 @@ void SantaDecisionManager::AddToCache(
|
||||
|
||||
void SantaDecisionManager::RemoveFromCache(santa_vnode_id_t identifier) {
|
||||
if (unlikely(identifier.fsid == 0 && identifier.fileid == 0)) return;
|
||||
decision_cache_->remove(identifier);
|
||||
CacheForIdentifier(identifier)->remove(identifier);
|
||||
wakeup((void *)identifier.unsafe_simple_id());
|
||||
}
|
||||
|
||||
uint64_t SantaDecisionManager::CacheCount() const {
|
||||
return decision_cache_->count();
|
||||
uint64_t SantaDecisionManager::RootCacheCount() const {
|
||||
return root_decision_cache_->count();
|
||||
}
|
||||
|
||||
void SantaDecisionManager::ClearCache() {
|
||||
decision_cache_->clear();
|
||||
uint64_t SantaDecisionManager::NonRootCacheCount() const {
|
||||
return non_root_decision_cache_->count();
|
||||
}
|
||||
|
||||
void SantaDecisionManager::ClearCache(bool non_root_only) {
|
||||
if (!non_root_only) root_decision_cache_->clear();
|
||||
non_root_decision_cache_->clear();
|
||||
}
|
||||
|
||||
void SantaDecisionManager::CacheBucketCount(
|
||||
uint16_t *per_bucket_counts, uint16_t *array_size, uint64_t *start_bucket) {
|
||||
decision_cache_->bucket_counts(per_bucket_counts, array_size, start_bucket);
|
||||
root_decision_cache_->bucket_counts(per_bucket_counts, array_size, start_bucket);
|
||||
}
|
||||
|
||||
#pragma mark Decision Fetching
|
||||
@@ -248,7 +382,9 @@ santa_action_t SantaDecisionManager::GetFromCache(santa_vnode_id_t identifier) {
|
||||
auto result = ACTION_UNSET;
|
||||
uint64_t decision_time = 0;
|
||||
|
||||
uint64_t cache_val = decision_cache_->get(identifier);
|
||||
auto decision_cache = CacheForIdentifier(identifier);
|
||||
|
||||
uint64_t cache_val = decision_cache->get(identifier);
|
||||
if (cache_val == 0) return result;
|
||||
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
@@ -259,7 +395,7 @@ santa_action_t SantaDecisionManager::GetFromCache(santa_vnode_id_t identifier) {
|
||||
if (result == ACTION_RESPOND_DENY) {
|
||||
auto expiry_time = decision_time + (kMaxDenyCacheTimeMilliseconds * 1000);
|
||||
if (expiry_time < GetCurrentUptime()) {
|
||||
decision_cache_->remove(identifier);
|
||||
decision_cache->remove(identifier);
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
}
|
||||
@@ -406,6 +542,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 +584,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 +594,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;
|
||||
}
|
||||
@@ -448,28 +623,70 @@ int SantaDecisionManager::VnodeCallback(const kauth_cred_t cred,
|
||||
void SantaDecisionManager::FileOpCallback(
|
||||
const kauth_action_t action, const vnode_t vp,
|
||||
const char *path, const char *new_path) {
|
||||
if (!ClientConnected() || proc_selfpid() == client_pid_) return;
|
||||
if (!ClientConnected()) return;
|
||||
|
||||
if (vp) {
|
||||
// KAUTH_FILEOP_CLOSE implies KAUTH_FILEOP_CLOSE_MODIFIED, so remove it from the cache.
|
||||
if (action == KAUTH_FILEOP_CLOSE) {
|
||||
auto context = vfs_context_create(nullptr);
|
||||
RemoveFromCache(GetVnodeIDForVnode(context, vp));
|
||||
vfs_context_rele(context);
|
||||
}
|
||||
|
||||
// Don't log santad fileops.
|
||||
if (proc_selfpid() == client_pid_) return;
|
||||
|
||||
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);
|
||||
}
|
||||
PostToLogQueue(message);
|
||||
delete message;
|
||||
return;
|
||||
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);
|
||||
}
|
||||
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 +698,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 +742,13 @@ 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.
|
||||
[[fallthrough]];
|
||||
case KAUTH_FILEOP_DELETE:
|
||||
case KAUTH_FILEOP_EXEC:
|
||||
vp = reinterpret_cast<vnode_t>(arg0);
|
||||
@@ -580,7 +805,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -100,11 +123,14 @@ class SantaDecisionManager : public OSObject {
|
||||
void RemoveFromCache(santa_vnode_id_t identifier);
|
||||
|
||||
/// Returns the number of entries in the cache.
|
||||
uint64_t CacheCount() const;
|
||||
|
||||
/// Clears the cache.
|
||||
void ClearCache();
|
||||
uint64_t RootCacheCount() const;
|
||||
uint64_t NonRootCacheCount() const;
|
||||
|
||||
/**
|
||||
Clears the cache(s). If non_root_only is true, only the non-root cache
|
||||
is cleared.
|
||||
*/
|
||||
void ClearCache(bool non_root_only = false);
|
||||
|
||||
/**
|
||||
Fills out the per_bucket_counts array with the number of items in each bucket in the
|
||||
@@ -125,6 +151,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 +242,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.
|
||||
@@ -279,8 +328,22 @@ class SantaDecisionManager : public OSObject {
|
||||
return (uint64_t)((sec * 1000000) + usec);
|
||||
}
|
||||
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *decision_cache_;
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *root_decision_cache_;
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *non_root_decision_cache_;
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *vnode_pid_map_;
|
||||
SantaCache<pid_t, pid_t> *compiler_pid_set_;
|
||||
|
||||
/**
|
||||
Return the correct cache for a given identifier.
|
||||
|
||||
@param identifier The identifier
|
||||
@return SantaCache* The cache to use
|
||||
*/
|
||||
SantaCache<santa_vnode_id_t, uint64_t>* CacheForIdentifier(const santa_vnode_id_t identifier);
|
||||
|
||||
// This is the file system ID of the root filesystem,
|
||||
// used to determine which cache to use for requests
|
||||
uint64_t root_fsid_;
|
||||
|
||||
lck_grp_t *sdm_lock_grp_;
|
||||
lck_grp_attr_t *sdm_lock_grp_attr_;
|
||||
@@ -295,6 +358,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_;
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -176,7 +189,21 @@ IOReturn SantaDriverClient::clear_cache(
|
||||
SantaDriverClient *me = OSDynamicCast(SantaDriverClient, target);
|
||||
if (!me) return kIOReturnBadArgument;
|
||||
|
||||
me->decisionManager->ClearCache();
|
||||
const bool non_root_only = static_cast<const bool>(arguments->scalarInput[0]);
|
||||
me->decisionManager->ClearCache(non_root_only);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -186,7 +213,8 @@ IOReturn SantaDriverClient::cache_count(
|
||||
SantaDriverClient *me = OSDynamicCast(SantaDriverClient, target);
|
||||
if (!me) return kIOReturnBadArgument;
|
||||
|
||||
arguments->scalarOutput[0] = me->decisionManager->CacheCount();
|
||||
arguments->scalarOutput[0] = me->decisionManager->RootCacheCount();
|
||||
arguments->scalarOutput[1] = me->decisionManager->NonRootCacheCount();
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
|
||||
@@ -234,10 +262,12 @@ 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::cache_count, 0, 0, 1, 0 },
|
||||
{ &SantaDriverClient::clear_cache, 1, 0, 0, 0 },
|
||||
{ &SantaDriverClient::remove_cache_entry, 0, sizeof(santa_vnode_id_t), 0, 0 },
|
||||
{ &SantaDriverClient::cache_count, 0, 0, 2, 0 },
|
||||
{ &SantaDriverClient::check_cache, 0, sizeof(santa_vnode_id_t), 1, 0 },
|
||||
{ &SantaDriverClient::cache_bucket_count, 0, sizeof(santa_bucket_count_t),
|
||||
0, sizeof(santa_bucket_count_t) },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -69,7 +69,7 @@ REGISTER_COMMAND_NAME(@"cachehistogram")
|
||||
}
|
||||
printf("\n");
|
||||
} else {
|
||||
printf("%4llu: %llu\n", k, v);
|
||||
printf("%4llu bucket[s] have %llu %s\n", v, k, k > 1 ? "entries" : "entry");
|
||||
}
|
||||
}
|
||||
exit(0);
|
||||
|
||||
@@ -60,6 +60,12 @@ REGISTER_COMMAND_NAME(@"checkcache")
|
||||
} 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);
|
||||
|
||||
@@ -384,6 +384,12 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
case SNTEventStateBlockScope:
|
||||
[output appendString:@" (Scope)"];
|
||||
break;
|
||||
case SNTEventStateAllowCompiler:
|
||||
[output appendString:@" (Compiler)"];
|
||||
break;
|
||||
case SNTEventStateAllowTransitive:
|
||||
[output appendString:@" (Transitive)"];
|
||||
break;
|
||||
default:
|
||||
output = @"None".mutableCopy;
|
||||
break;
|
||||
|
||||
@@ -53,6 +53,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@" --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 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
@"\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 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
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 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
[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 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
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 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -87,19 +87,26 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
BOOL fileLogging = ([[SNTConfigurator configurator] fileChangesRegex] != nil);
|
||||
|
||||
// Kext status
|
||||
__block uint64_t cacheCount = -1;
|
||||
__block uint64_t rootCacheCount = -1, nonRootCacheCount = -1;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] cacheCounts:^(uint64_t count) {
|
||||
cacheCount = count;
|
||||
[[self.daemonConn remoteObjectProxy] cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) {
|
||||
rootCacheCount = rootCache;
|
||||
nonRootCacheCount = nonRootCache;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// 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);
|
||||
@@ -139,15 +146,22 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}];
|
||||
}
|
||||
|
||||
__block BOOL bundlesEnabled = NO;
|
||||
__block BOOL enableBundles = NO;
|
||||
if ([[SNTConfigurator configurator] syncBaseURL]) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] bundlesEnabled:^(BOOL response) {
|
||||
bundlesEnabled = response;
|
||||
[[self.daemonConn remoteObjectProxy] enableBundles:^(BOOL response) {
|
||||
enableBundles = response;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
|
||||
__block BOOL transitiveWhitelistingEnabled = NO;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] enableTransitiveWhitelisting:^(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");
|
||||
@@ -174,11 +188,14 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"watchdog_ram_peak" : @(ramPeak),
|
||||
},
|
||||
@"kernel" : @{
|
||||
@"cache_count" : @(cacheCount),
|
||||
@"root_cache_count" : @(rootCacheCount),
|
||||
@"non_root_cache_count": @(nonRootCacheCount),
|
||||
},
|
||||
@"database" : @{
|
||||
@"binary_rules" : @(binaryRuleCount),
|
||||
@"certificate_rules" : @(certRuleCount),
|
||||
@"compiler_rules" : @(compilerRuleCount),
|
||||
@"transitive_rules" : @(transitiveRuleCount),
|
||||
@"events_pending_upload" : @(eventCount),
|
||||
},
|
||||
@"sync" : @{
|
||||
@@ -187,7 +204,8 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"last_successful_full" : fullSyncLastSuccessStr ?: @"null",
|
||||
@"last_successful_rule" : ruleSyncLastSuccessStr ?: @"null",
|
||||
@"push_notifications" : pushNotifications ? @"Connected" : @"Disconnected",
|
||||
@"bundle_scanning" : @(bundlesEnabled)
|
||||
@"bundle_scanning" : @(enableBundles),
|
||||
@"transitive_whitelisting" : @(transitiveWhitelistingEnabled),
|
||||
},
|
||||
};
|
||||
NSData *statsData = [NSJSONSerialization dataWithJSONObject:stats
|
||||
@@ -203,10 +221,13 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %lld (Peak: %.2f%%)\n", "Watchdog CPU Events", cpuEvents, cpuPeak);
|
||||
printf(" %-25s | %lld (Peak: %.2fMB)\n", "Watchdog RAM Events", ramEvents, ramPeak);
|
||||
printf(">>> Kernel Info\n");
|
||||
printf(" %-25s | %lld\n", "Cache count", cacheCount);
|
||||
printf(" %-25s | %lld\n", "Root cache count", rootCacheCount);
|
||||
printf(" %-25s | %lld\n", "Non-root cache count", nonRootCacheCount);
|
||||
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) {
|
||||
@@ -217,7 +238,9 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %s\n", "Last Successful Rule Sync", [ruleSyncLastSuccessStr UTF8String]);
|
||||
printf(" %-25s | %s\n", "Push Notifications",
|
||||
(pushNotifications ? "Connected" : "Disconnected"));
|
||||
printf(" %-25s | %s\n", "Bundle Scanning", (bundlesEnabled ? "Yes" : "No"));
|
||||
printf(" %-25s | %s\n", "Bundle Scanning", (enableBundles ? "Yes" : "No"));
|
||||
printf(" %-25s | %s\n", "Transitive Whitelisting",
|
||||
(transitiveWhitelistingEnabled ? "Yes" : "No"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
return (@"If Santa is configured to synchronize with a server, "
|
||||
@"this is the command used for syncing.\n\n"
|
||||
@"Options:\n"
|
||||
@" --clean: Perform a clean sync, erasing all existing rules and requesting a"
|
||||
@" --clean: Perform a clean sync, erasing all existing rules and requesting a\n"
|
||||
@" clean sync from the server.");
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,15 @@ 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 kEnableBundles;
|
||||
extern NSString *const kEnableBundles_OLD;
|
||||
extern NSString *const kEnableTransitiveWhitelisting;
|
||||
extern NSString *const kEnableTransitiveWhitelisting_OLD;
|
||||
|
||||
extern NSString *const kEvents;
|
||||
extern NSString *const kFileSHA256;
|
||||
@@ -88,6 +93,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;
|
||||
|
||||
@@ -33,10 +33,17 @@ NSString *const kWhitelistRegex = @"whitelist_regex";
|
||||
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";
|
||||
|
||||
// NOTE: Both of the _OLD values will be removed at some indeterminate point in the future.
|
||||
NSString *const kEnableBundles = @"enable_bundles";
|
||||
NSString *const kEnableBundles_OLD = @"bundles_enabled";
|
||||
NSString *const kEnableTransitiveWhitelisting = @"enabled_transitive_whitelisting";
|
||||
NSString *const kEnableTransitiveWhitelisting_OLD = @"transitive_whitelisting_enabled";
|
||||
|
||||
NSString *const kEvents = @"events";
|
||||
NSString *const kFileSHA256 = @"file_sha256";
|
||||
@@ -88,6 +95,7 @@ NSString *const kRules = @"rules";
|
||||
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";
|
||||
|
||||
@@ -43,9 +43,14 @@
|
||||
|
||||
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);
|
||||
}];
|
||||
|
||||
@@ -82,7 +87,21 @@
|
||||
if (!resp) return NO;
|
||||
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setBundlesEnabled:[resp[kBundlesEnabled] boolValue] reply:^{
|
||||
NSNumber *enableBundles = resp[kEnableBundles];
|
||||
if (!enableBundles) {
|
||||
enableBundles = resp[kEnableBundles_OLD];
|
||||
}
|
||||
[[self.daemonConn remoteObjectProxy] setEnableBundles:[enableBundles boolValue] reply:^{
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
NSNumber *enableTransitiveWhitelisting = resp[kEnableTransitiveWhitelisting];
|
||||
if (!enableTransitiveWhitelisting) {
|
||||
enableTransitiveWhitelisting = resp[kEnableTransitiveWhitelisting_OLD];
|
||||
}
|
||||
BOOL enabled = [enableTransitiveWhitelisting boolValue];
|
||||
[[self.daemonConn remoteObjectProxy] setEnableTransitiveWhitelisting:enabled reply:^{
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
|
||||
@@ -136,6 +136,8 @@
|
||||
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 @@
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#import "SNTCommonEnums.h"
|
||||
#import "SNTDatabaseTable.h"
|
||||
|
||||
@class SNTCachedDecision;
|
||||
@class SNTRule;
|
||||
@class SNTNotificationMessage;
|
||||
|
||||
@@ -35,6 +36,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 +69,32 @@
|
||||
///
|
||||
- (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;
|
||||
|
||||
|
||||
///
|
||||
/// A map of a file hashes to cached decisions. This is used to pre-validate and whitelist
|
||||
/// certain critical system binaries that are integral to Santa's functionality.
|
||||
///
|
||||
@property(readonly, nonatomic)
|
||||
NSDictionary<NSString *, SNTCachedDecision *> *criticalSystemBinaries;
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,17 +17,34 @@
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
|
||||
#import "SNTCachedDecision.h"
|
||||
#import "SNTConfigurator.h"
|
||||
#import "SNTFileInfo.h"
|
||||
#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;
|
||||
@property NSDictionary *criticalSystemBinaries;
|
||||
@property(readonly) NSArray *criticalSystemBinaryPaths;
|
||||
@end
|
||||
|
||||
@implementation SNTRuleTable
|
||||
|
||||
- (NSArray *)criticalSystemBinaryPaths {
|
||||
return @[
|
||||
@"/usr/libexec/trustd", @"/usr/sbin/securityd", @"/usr/libexec/xpcproxy", @"/usr/sbin/ocspd"
|
||||
];
|
||||
}
|
||||
|
||||
- (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version {
|
||||
// Lock this database from other processes
|
||||
[[db executeQuery:@"PRAGMA locking_mode = EXCLUSIVE;"] close];
|
||||
@@ -57,7 +74,8 @@
|
||||
// Save hashes of the signing certs for launchd and santad.
|
||||
// Used to ensure rules for them are not removed.
|
||||
self.santadCertSHA = [[[[MOLCodesignChecker alloc] initWithSelf] leafCertificate] SHA256];
|
||||
self.launchdCertSHA = [[[[MOLCodesignChecker alloc] initWithPID:1] leafCertificate] SHA256];
|
||||
MOLCodesignChecker *launchdCSInfo = [[MOLCodesignChecker alloc] initWithPID:1];
|
||||
self.launchdCertSHA = launchdCSInfo.leafCertificate.SHA256;
|
||||
|
||||
// Ensure the certificates used to sign the running launchd/santad are whitelisted.
|
||||
// If they weren't previously and the database is not new, log an error.
|
||||
@@ -65,6 +83,13 @@
|
||||
@"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 (?, ?, ?)",
|
||||
@@ -73,6 +98,34 @@
|
||||
self.launchdCertSHA, @(SNTRuleStateWhitelist), @(SNTRuleTypeCertificate)];
|
||||
}
|
||||
|
||||
// Setup critical system binaries
|
||||
// TODO(tburgin): Add the Santa components to this feature and remove the santadCertSHA rule.
|
||||
NSMutableDictionary *bins = [NSMutableDictionary dictionary];
|
||||
for (NSString *path in self.criticalSystemBinaryPaths) {
|
||||
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:path];
|
||||
MOLCodesignChecker *csInfo = [binInfo codesignCheckerWithError:NULL];
|
||||
|
||||
// Make sure the critical system binary is signed by the same chain as launchd.
|
||||
if ([csInfo signingInformationMatches:launchdCSInfo]) {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
|
||||
cd.decision = SNTEventStateAllowBinary;
|
||||
cd.decisionExtra = @"critical system binary";
|
||||
cd.sha256 = binInfo.SHA256;
|
||||
|
||||
// Not needed, but nice for logging.
|
||||
cd.certSHA256 = csInfo.leafCertificate.SHA256;
|
||||
cd.certCommonName = csInfo.leafCertificate.commonName;
|
||||
|
||||
bins[binInfo.SHA256] = cd;
|
||||
} else {
|
||||
LOGE(@"Unable to validate critical system binary. pid 1: %@ and %@: %@ do not match.",
|
||||
launchdCSInfo.leafCertificate, path, csInfo.leafCertificate);
|
||||
}
|
||||
}
|
||||
|
||||
self.criticalSystemBinaries = bins;
|
||||
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
@@ -102,15 +155,30 @@
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)compilerRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE state=?",
|
||||
@(SNTRuleStateWhitelistCompiler)];
|
||||
}];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSUInteger)transitiveRuleCount {
|
||||
__block NSUInteger count = 0;
|
||||
[self inDatabase:^(FMDatabase *db) {
|
||||
count = [db longForQuery:@"SELECT COUNT(*) FROM rules WHERE state=?",
|
||||
@(SNTRuleStateWhitelistTransitive)];
|
||||
}];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (SNTRule *)ruleFromResultSet:(FMResultSet *)rs {
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
|
||||
rule.shasum = [rs stringForColumn:@"shasum"];
|
||||
rule.type = [rs intForColumn:@"type"];
|
||||
rule.state = [rs intForColumn:@"state"];
|
||||
rule.customMsg = [rs stringForColumn:@"custommsg"];
|
||||
|
||||
return rule;
|
||||
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 +233,7 @@
|
||||
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 +249,10 @@
|
||||
}
|
||||
} 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 +266,67 @@
|
||||
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 +338,8 @@
|
||||
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";
|
||||
|
||||
@@ -30,10 +30,15 @@
|
||||
- (void)logDeniedExecution:(SNTCachedDecision *)cd withMessage:(santa_message_t)message;
|
||||
- (void)logAllowedExecution:(santa_message_t)message;
|
||||
- (void)logBundleHashingEvents:(NSArray<SNTStoredEvent *> *)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;
|
||||
|
||||
@@ -21,10 +21,15 @@
|
||||
|
||||
#import "SNTCachedDecision.h"
|
||||
#import "SNTConfigurator.h"
|
||||
#import "SNTDatabaseController.h"
|
||||
#import "SNTRule.h"
|
||||
#import "SNTRuleTable.h"
|
||||
|
||||
@interface SNTEventLog ()
|
||||
@property NSMutableDictionary<NSNumber *, SNTCachedDecision *> *detailStore;
|
||||
@property dispatch_queue_t detailStoreQueue;
|
||||
// Cache for sha256 -> date of last timestamp reset.
|
||||
@property NSCache<NSString *, NSDate *> *timestampResetMap;
|
||||
@end
|
||||
|
||||
@implementation SNTEventLog
|
||||
@@ -40,6 +45,8 @@
|
||||
_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 @@
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -86,6 +86,21 @@
|
||||
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)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 {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
#import "SNTCommonEnums.h"
|
||||
#import "SNTCompilerController.h"
|
||||
#import "SNTConfigurator.h"
|
||||
#import "SNTDaemonControlController.h"
|
||||
#import "SNTDatabaseController.h"
|
||||
@@ -41,6 +42,7 @@
|
||||
@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 @@
|
||||
_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 @@
|
||||
[_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);
|
||||
@@ -252,7 +264,7 @@ void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
|
||||
[app.eventLog logDiskDisappeared:props];
|
||||
[app.driverManager flushCache];
|
||||
[app.driverManager flushCacheNonRootOnly:YES];
|
||||
}
|
||||
|
||||
- (void)startSyncd {
|
||||
@@ -304,7 +316,7 @@ void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
if (!new && !old) return;
|
||||
if (![new.pattern isEqualToString:old.pattern]) {
|
||||
LOGI(@"Changed [white|black]list regex, flushing cache");
|
||||
[self.driverManager flushCache];
|
||||
[self.driverManager flushCacheNonRootOnly:NO];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,7 +324,7 @@ void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
- (void)clientModeDidChange:(SNTClientMode)clientMode {
|
||||
if (clientMode == SNTClientModeLockdown) {
|
||||
LOGI(@"Changed client mode, flushing cache.");
|
||||
[self.driverManager flushCache];
|
||||
[self.driverManager flushCacheNonRootOnly:NO];
|
||||
}
|
||||
[[self.notQueue.notifierConnection remoteObjectProxy] postClientModeNotification:clientMode];
|
||||
}
|
||||
|
||||
31
Source/santad/SNTCompilerController.h
Normal file
31
Source/santad/SNTCompilerController.h
Normal file
@@ -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 <Foundation/Foundation.h>
|
||||
#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
|
||||
100
Source/santad/SNTCompilerController.m
Normal file
100
Source/santad/SNTCompilerController.m
Normal file
@@ -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
|
||||
@@ -70,9 +70,9 @@ double watchdogRAMPeak = 0;
|
||||
|
||||
#pragma mark Kernel ops
|
||||
|
||||
- (void)cacheCounts:(void (^)(uint64_t))reply {
|
||||
uint64_t count = [self.driverManager cacheCount];
|
||||
reply(count);
|
||||
- (void)cacheCounts:(void (^)(uint64_t, uint64_t))reply {
|
||||
NSArray<NSNumber *> *counts = [self.driverManager cacheCounts];
|
||||
reply([counts[0] unsignedLongLongValue], [counts[1] unsignedLongLongValue]);
|
||||
}
|
||||
|
||||
- (void)cacheBucketCount:(void (^)(NSArray *))reply {
|
||||
@@ -80,7 +80,7 @@ double watchdogRAMPeak = 0;
|
||||
}
|
||||
|
||||
- (void)flushCache:(void (^)(BOOL))reply {
|
||||
reply([self.driverManager flushCache]);
|
||||
reply([self.driverManager flushCacheNonRootOnly:NO]);
|
||||
}
|
||||
|
||||
- (void)checkCacheForVnodeID:(santa_vnode_id_t)vnodeID withReply:(void (^)(santa_action_t))reply {
|
||||
@@ -93,22 +93,35 @@ double watchdogRAMPeak = 0;
|
||||
|
||||
#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 {
|
||||
NSError *error;
|
||||
[[SNTDatabaseController ruleTable] addRules:rules cleanSlate:cleanSlate error:&error];
|
||||
SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable];
|
||||
|
||||
// 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");
|
||||
[self.driverManager flushCache];
|
||||
// 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;
|
||||
[ruleTable addRules:rules cleanSlate:cleanSlate error:&error];
|
||||
|
||||
// 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 flushCacheNonRootOnly:NO];
|
||||
}
|
||||
|
||||
reply(error);
|
||||
@@ -211,12 +224,21 @@ double watchdogRAMPeak = 0;
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)bundlesEnabled:(void (^)(BOOL))reply {
|
||||
reply([SNTConfigurator configurator].bundlesEnabled);
|
||||
- (void)enableBundles:(void (^)(BOOL))reply {
|
||||
reply([SNTConfigurator configurator].enableBundles);
|
||||
}
|
||||
|
||||
- (void)setBundlesEnabled:(BOOL)bundlesEnabled reply:(void (^)(void))reply {
|
||||
[[SNTConfigurator configurator] setBundlesEnabled:bundlesEnabled];
|
||||
- (void)setEnableBundles:(BOOL)enableBundles reply:(void (^)(void))reply {
|
||||
[[SNTConfigurator configurator] setEnableBundles:enableBundles];
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)enableTransitiveWhitelisting:(void (^)(BOOL))reply {
|
||||
reply([SNTConfigurator configurator].enableTransitiveWhitelisting);
|
||||
}
|
||||
|
||||
- (void)setEnableTransitiveWhitelisting:(BOOL)enabled reply:(void (^)(void))reply {
|
||||
[[SNTConfigurator configurator] setEnableTransitiveWhitelisting:enabled];
|
||||
reply();
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
///
|
||||
/// Get the number of binaries in the kernel's caches.
|
||||
///
|
||||
- (uint64_t)cacheCount;
|
||||
- (NSArray<NSNumber *> *)cacheCounts;
|
||||
|
||||
///
|
||||
/// Return an array representing all buckets in the kernel's decision cache where each number
|
||||
@@ -61,14 +61,19 @@
|
||||
///
|
||||
/// Flush the kernel's binary cache.
|
||||
///
|
||||
- (BOOL)flushCache;
|
||||
- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly;
|
||||
|
||||
///
|
||||
/// 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
|
||||
|
||||
@@ -201,14 +201,21 @@ static void driverAppearedHandler(void *info, io_iterator_t iterator) {
|
||||
sizeof(vnodeId),
|
||||
0,
|
||||
0);
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
return IOConnectCallStructMethod(_connection,
|
||||
kSantaUserClientAllowCompiler,
|
||||
&vnodeId,
|
||||
sizeof(vnodeId),
|
||||
0,
|
||||
0);
|
||||
default:
|
||||
return KERN_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
- (uint64_t)cacheCount {
|
||||
uint32_t input_count = 1;
|
||||
uint64_t cache_counts[1] = {0};
|
||||
- (NSArray<NSNumber *> *)cacheCounts {
|
||||
uint32_t input_count = 2;
|
||||
uint64_t cache_counts[2] = {0, 0};
|
||||
|
||||
IOConnectCallScalarMethod(_connection,
|
||||
kSantaUserClientCacheCount,
|
||||
@@ -217,18 +224,29 @@ static void driverAppearedHandler(void *info, io_iterator_t iterator) {
|
||||
cache_counts,
|
||||
&input_count);
|
||||
|
||||
return cache_counts[0];
|
||||
return @[ @(cache_counts[0]), @(cache_counts[1]) ];
|
||||
}
|
||||
|
||||
- (BOOL)flushCache {
|
||||
- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly {
|
||||
const uint64_t nonRoot = nonRootOnly;
|
||||
|
||||
return IOConnectCallScalarMethod(_connection,
|
||||
kSantaUserClientClearCache,
|
||||
0,
|
||||
0,
|
||||
&nonRoot,
|
||||
1,
|
||||
0,
|
||||
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;
|
||||
|
||||
@@ -115,36 +115,54 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
[_driverManager postToKernelAction:ACTION_RESPOND_ACK forVnodeID:message.vnode_id];
|
||||
}
|
||||
|
||||
// Get codesigning info about the file but only if it's a Mach-O.
|
||||
MOLCodesignChecker *csInfo;
|
||||
if (binInfo.isMachO) {
|
||||
NSError *csError;
|
||||
csInfo = [[MOLCodesignChecker alloc] initWithBinaryPath:binInfo.path
|
||||
fileDescriptor:binInfo.fileHandle.fileDescriptor
|
||||
error:&csError];
|
||||
// Ignore codesigning if there are any errors with the signature.
|
||||
if (csError) csInfo = nil;
|
||||
// If the binary is a critical system binary, don't check its signature. The binary was validated
|
||||
// by santad at startup.
|
||||
SNTCachedDecision *cd = self.ruleTable.criticalSystemBinaries[binInfo.SHA256];
|
||||
MOLCodesignChecker *csInfo; // Needed further down in this scope.
|
||||
if (!cd) {
|
||||
// Get codesigning info about the file but only if it's a Mach-O.
|
||||
if (binInfo.isMachO) {
|
||||
NSError *csError;
|
||||
csInfo = [[MOLCodesignChecker alloc] initWithBinaryPath:binInfo.path
|
||||
fileDescriptor:binInfo.fileHandle.fileDescriptor
|
||||
error:&csError];
|
||||
// Ignore codesigning if there are any errors with the signature.
|
||||
if (csError) csInfo = nil;
|
||||
}
|
||||
|
||||
// Actually make the decision (and refresh rule access timestamp).
|
||||
cd = [self.policyProcessor decisionForFileInfo:binInfo
|
||||
fileSHA256:nil
|
||||
certificateSHA256:csInfo.leafCertificate.SHA256];
|
||||
cd.certCommonName = csInfo.leafCertificate.commonName;
|
||||
}
|
||||
|
||||
// Actually make the decision.
|
||||
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,10 +206,10 @@ static size_t kLargeBinarySize = 30 * 1024 * 1024;
|
||||
});
|
||||
|
||||
// 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) {
|
||||
if ([[SNTConfigurator configurator] enableBundles] && binInfo.bundle) {
|
||||
// If the binary is part of a bundle, find and hash all the related binaries in the bundle.
|
||||
// Let the GUI know hashing is needed. Once the hashing is complete the GUI will send a
|
||||
// message to santad to perform the upload logic for bundles.
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
- (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 @@
|
||||
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] enableTransitiveWhitelisting]) {
|
||||
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] enableTransitiveWhitelisting]) {
|
||||
cd.decision = SNTEventStateAllowTransitive;
|
||||
return cd;
|
||||
} else {
|
||||
rule.state = SNTRuleStateUnknown;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -99,26 +99,60 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// Call in-kernel function: |kSantaUserClientClearCache|
|
||||
- (void)flushCache {
|
||||
IOConnectCallScalarMethod(self.connection, kSantaUserClientClearCache, 0, 0, 0, 0);
|
||||
uint64_t nonRootOnly = 0;
|
||||
IOConnectCallScalarMethod(self.connection, kSantaUserClientClearCache, &nonRootOnly, 1, 0, 0);
|
||||
}
|
||||
|
||||
#pragma mark - Connection Tests
|
||||
@@ -547,6 +581,127 @@
|
||||
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 <stdio.h>\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 <stdio.h>\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) {
|
||||
// All good
|
||||
}
|
||||
// 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 +775,8 @@
|
||||
[self clearCacheTests];
|
||||
[self blocksDeniedTracedBinaries];
|
||||
[self testLargeBinary];
|
||||
[self testPendingTransitiveRules];
|
||||
[self testNoTransitiveRules];
|
||||
|
||||
printf("\n-> Performance tests:\n");
|
||||
[self testCachePerformance];
|
||||
|
||||
@@ -191,15 +191,20 @@
|
||||
- (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;
|
||||
}];
|
||||
|
||||
|
||||
@@ -165,6 +165,71 @@
|
||||
forVnodeID:[self getVnodeId]]);
|
||||
}
|
||||
|
||||
- (void)testBinaryWhitelistCompilerRule {
|
||||
OCMStub([self.mockFileInfo isMachO]).andReturn(YES);
|
||||
OCMStub([self.mockFileInfo SHA256]).andReturn(@"a");
|
||||
OCMStub([self.mockConfigurator enableTransitiveWhitelisting]).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 enableTransitiveWhitelisting]).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 enableTransitiveWhitelisting]).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 enableTransitiveWhitelisting]).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");
|
||||
|
||||
Reference in New Issue
Block a user