Skip to content

Commit

Permalink
santad: Add signal auth to tamper resistence. (#1360)
Browse files Browse the repository at this point in the history
Prior to this change, root users could kill the com.google.santa.daemon process. 
It would be immediately restarted by sysextd but this opens a very brief
window where protection is lost. Hooking AUTH_SIGNAL and blocking all
signals to the santad process except those sent by launchd lets us block
this without breaking upgrades, reboots, etc.

This leaves `launchctl kill` and friends as an avenue, so we're also
hooking for exec and blocking executions of launchctl that reference
com.google.santa.daemon except in known safe cases.
  • Loading branch information
russellhancox authored Jun 3, 2024
1 parent 53a2bbd commit a42dd6e
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
#import "Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h"

#include <EndpointSecurity/ESTypes.h>
#include <bsm/libbsm.h>
#include <string.h>
#include <algorithm>

#import "Source/common/SNTLogging.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<EndpointSecurityAPI> 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<std::string> 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
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ @implementation SNTEndpointSecurityTamperResistanceTest
- (void)testEnable {
// Ensure the client subscribes to expected event types
std::set<es_event_type_t> 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<MockEndpointSecurityAPI>();
Expand All @@ -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
Expand All @@ -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());
Expand All @@ -106,6 +107,12 @@ - (void)testHandleMessage {
{&benignTok, ES_AUTH_RESULT_ALLOW},
};

std::map<std::pair<pid_t, pid_t>, 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<MockEndpointSecurityAPI>();
Expand Down Expand Up @@ -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));

Expand Down

0 comments on commit a42dd6e

Please sign in to comment.