diff --git a/Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.mm b/Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.mm index e04160b4d..ee934577b 100644 --- a/Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.mm +++ b/Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.mm @@ -15,7 +15,9 @@ #import "Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h" #include +#include #include +#include #import "Source/common/SNTLogging.h" #include "Source/santad/DataLayer/WatchItemPolicy.h" @@ -85,6 +87,26 @@ - (void)handleMessage:(Message &&)esMsg break; } + case ES_EVENT_TYPE_AUTH_SIGNAL: { + // Only block signals sent to us and not from launchd. + if (audit_token_to_pid(esMsg->event.signal.target->audit_token) == getpid() && + audit_token_to_pid(esMsg->process->audit_token) != 1) { + LOGW(@"Preventing attempt to kill Santa daemon"); + result = ES_AUTH_RESULT_DENY; + } + break; + } + + case ES_EVENT_TYPE_AUTH_EXEC: { + // When not running a debug build, prevent attempts to kill Santa + // by launchctl commands. +#ifndef DEBUG + result = ValidateLaunchctlExec(esMsg); + if (result == ES_AUTH_RESULT_DENY) LOGW(@"Preventing attempt to kill Santa daemon"); +#endif + break; + } + case ES_EVENT_TYPE_AUTH_KEXTLOAD: { // TODO(mlw): Since we don't package the kext anymore, we should consider removing this. // TODO(mlw): Consider logging when kext loads are attempted. @@ -120,15 +142,53 @@ - (void)enable { for (const auto &path : protectedPaths) { watchPaths.push_back({path, WatchItemPathType::kLiteral}); } + watchPaths.push_back({"/Library/SystemExtensions", WatchItemPathType::kPrefix}); + watchPaths.push_back({"/bin/launchctl", WatchItemPathType::kLiteral}); // Begin watching the protected set [super muteTargetPaths:watchPaths]; [super subscribeAndClearCache:{ ES_EVENT_TYPE_AUTH_KEXTLOAD, + ES_EVENT_TYPE_AUTH_SIGNAL, + ES_EVENT_TYPE_AUTH_EXEC, ES_EVENT_TYPE_AUTH_UNLINK, ES_EVENT_TYPE_AUTH_RENAME, }]; } +es_auth_result_t ValidateLaunchctlExec(const Message &esMsg) { + es_string_token_t exec_path = esMsg->event.exec.target->executable->path; + if (strncmp(exec_path.data, "/bin/launchctl", exec_path.length) != 0) { + return ES_AUTH_RESULT_ALLOW; + } + + // Ensure there are at least 2 arguments after the command + std::shared_ptr esApi = esMsg.ESAPI(); + uint32_t argCount = esApi->ExecArgCount(&esMsg->event.exec); + if (argCount < 2) { + return ES_AUTH_RESULT_ALLOW; + } + + // Check for some allowed subcommands + es_string_token_t arg = esApi->ExecArg(&esMsg->event.exec, 1); + static const std::unordered_set safe_commands{ + "blame", "help", "hostinfo", "list", "plist", "print", "procinfo", + }; + if (safe_commands.find(std::string(arg.data, arg.length)) != safe_commands.end()) { + return ES_AUTH_RESULT_ALLOW; + } + + // Check whether com.google.santa.daemon is in the argument list. + // launchctl no longer accepts PIDs to operate on. + for (int i = 2; i < argCount; i++) { + es_string_token_t arg = esApi->ExecArg(&esMsg->event.exec, i); + if (strnstr(arg.data, "com.google.santa.daemon", arg.length) != NULL) { + return ES_AUTH_RESULT_DENY; + } + } + + return ES_AUTH_RESULT_ALLOW; +} + @end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityTamperResistanceTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityTamperResistanceTest.mm index 517ed1ca1..bd6187fa2 100644 --- a/Source/santad/EventProviders/SNTEndpointSecurityTamperResistanceTest.mm +++ b/Source/santad/EventProviders/SNTEndpointSecurityTamperResistanceTest.mm @@ -49,9 +49,8 @@ @implementation SNTEndpointSecurityTamperResistanceTest - (void)testEnable { // Ensure the client subscribes to expected event types std::set expectedEventSubs{ - ES_EVENT_TYPE_AUTH_KEXTLOAD, - ES_EVENT_TYPE_AUTH_UNLINK, - ES_EVENT_TYPE_AUTH_RENAME, + ES_EVENT_TYPE_AUTH_KEXTLOAD, ES_EVENT_TYPE_AUTH_SIGNAL, ES_EVENT_TYPE_AUTH_EXEC, + ES_EVENT_TYPE_AUTH_UNLINK, ES_EVENT_TYPE_AUTH_RENAME, }; auto mockESApi = std::make_shared(); @@ -70,6 +69,8 @@ - (void)testEnable { // Setup mocks to handle muting the rules db and events db EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, testing::_, WatchItemPathType::kLiteral)) .WillRepeatedly(testing::Return(true)); + EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, testing::_, WatchItemPathType::kPrefix)) + .WillRepeatedly(testing::Return(true)); SNTEndpointSecurityTamperResistance *tamperClient = [[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi @@ -86,7 +87,7 @@ - (void)testEnable { - (void)testHandleMessage { es_file_t file = MakeESFile("foo"); es_process_t proc = MakeESProcess(&file); - es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_LINK, &proc, ActionType::Auth); es_file_t fileEventsDB = MakeESFile(kEventsDBPath.data()); es_file_t fileRulesDB = MakeESFile(kRulesDBPath.data()); @@ -106,6 +107,12 @@ - (void)testHandleMessage { {&benignTok, ES_AUTH_RESULT_ALLOW}, }; + std::map, es_auth_result_t> pidsToResult{ + {{getpid(), 31838}, ES_AUTH_RESULT_DENY}, + {{getpid(), 1}, ES_AUTH_RESULT_ALLOW}, + {{435, 98381}, ES_AUTH_RESULT_ALLOW}, + }; + dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0); auto mockESApi = std::make_shared(); @@ -232,6 +239,31 @@ - (void)testHandleMessage { } } + // Check SIGNAL tamper events + { + esMsg.event_type = ES_EVENT_TYPE_AUTH_SIGNAL; + + for (const auto &kv : pidsToResult) { + Message msg(mockESApi, &esMsg); + es_process_t target_proc = MakeESProcess(&file); + target_proc.audit_token = MakeAuditToken(kv.first.first, 42); + esMsg.event.signal.target = &target_proc; + esMsg.process->audit_token = MakeAuditToken(kv.first.second, 42); + + [mockTamperClient + handleMessage:std::move(msg) + recordEventMetrics:^(EventDisposition d) { + XCTAssertEqual(d, kv.second == ES_AUTH_RESULT_DENY ? EventDisposition::kProcessed + : EventDisposition::kDropped); + dispatch_semaphore_signal(semaMetrics); + }]; + + XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window"); + XCTAssertEqual(gotAuthResult, kv.second); + XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW); + } + } + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); XCTAssertTrue(OCMVerifyAll(mockTamperClient));