Skip to content

Commit

Permalink
Add functional test for executing a binary on SNTApplication. (#562)
Browse files Browse the repository at this point in the history
This adds a full functional test for starting up an SNTApplication
(with as few mocks as possible) and executing it with a directly
recorded & collected EndpointSecurity event.

This also fixes a potential race condition and segfault on Santa startup: due
to es_subscribe being called first, it's possible for an es event to arrive
before listenForDecisionRequests or listenForLogRequests are called,
causing the SNTEndpointSecurityManager callbacks to call a nil pointer.
  • Loading branch information
tnek committed Aug 10, 2021
1 parent 1fcb63d commit 10f2d85
Show file tree
Hide file tree
Showing 16 changed files with 239 additions and 12 deletions.
1 change: 1 addition & 0 deletions BUILD
Expand Up @@ -228,6 +228,7 @@ test_suite(
"//Source/common:SNTPrefixTreeTest",
"//Source/santactl:SNTCommandFileInfoTest",
"//Source/santactl:SNTCommandSyncTest",
"//Source/santad:SNTApplicationTest",
"//Source/santad:SNTEventTableTest",
"//Source/santad:SNTExecutionControllerTest",
"//Source/santad:SNTRuleTableTest",
Expand Down
24 changes: 23 additions & 1 deletion Source/santad/BUILD
Expand Up @@ -76,8 +76,8 @@ objc_library(
objc_library(
name = "EndpointSecurityTestLib",
srcs = [
"EventProviders/EndpointSecurityTestUtil.mm",
"EventProviders/EndpointSecurityTestUtil.h",
"EventProviders/EndpointSecurityTestUtil.mm",
],
testonly = 1,
sdk_dylibs = [
Expand Down Expand Up @@ -213,3 +213,25 @@ santa_unit_test(
],

)

santa_unit_test(
name = "SNTApplicationTest",
data = [
"//Source/santad/testdata:binaryrules_testdata",
],
srcs = [
"SNTApplicationTest.m"
],
deps = [
":santad_lib",
":EndpointSecurityTestLib",
"@MOLCodesignChecker",
"@MOLXPCConnection",
"@OCMock",
],
minimum_os_version = "10.15",
sdk_dylibs = [
"EndpointSecurity",
"bsm",
],
)
4 changes: 2 additions & 2 deletions Source/santad/DataLayer/SNTRuleTable.m
Expand Up @@ -39,7 +39,7 @@ @interface SNTRuleTable ()

@implementation SNTRuleTable

- (NSArray *)criticalSystemBinaryPaths {
+ (NSArray *)criticalSystemBinaryPaths {
return @[
@"/usr/libexec/trustd",
@"/usr/sbin/securityd",
Expand All @@ -58,7 +58,7 @@ - (NSArray *)criticalSystemBinaryPaths {

- (void)setupSystemCriticalBinaries {
NSMutableDictionary *bins = [NSMutableDictionary dictionary];
for (NSString *path in self.criticalSystemBinaryPaths) {
for (NSString *path in [SNTRuleTable criticalSystemBinaryPaths]) {
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:path];
MOLCodesignChecker *csInfo = [binInfo codesignCheckerWithError:NULL];

Expand Down
9 changes: 9 additions & 0 deletions Source/santad/EventProviders/EndpointSecurityTestUtil.h
Expand Up @@ -13,9 +13,12 @@
/// limitations under the License.

#include <EndpointSecurity/EndpointSecurity.h>
#include <Foundation/Foundation.h>
#include <bsm/libbsm.h>

CF_EXTERN_C_BEGIN
es_string_token_t MakeStringToken(const NSString *s);
CF_EXTERN_C_END

@interface ESResponse : NSObject
@property(nonatomic) es_auth_result_t result;
Expand All @@ -26,6 +29,7 @@ typedef void (^ESCallback)(ESResponse *);

// Singleton wrapper around all of the kernel-level EndpointSecurity framework functions.
@interface MockEndpointSecurity : NSObject
@property BOOL subscribed;
- (void)reset;
- (void)registerResponseCallback:(ESCallback)callback;
- (void)triggerHandler:(es_message_t *)msg;
Expand All @@ -50,3 +54,8 @@ API_UNAVAILABLE(ios, tvos, watchos)
es_respond_result_t es_respond_auth_result(es_client_t *_Nonnull client,
const es_message_t *_Nonnull message,
es_auth_result_t result, bool cache);

API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos)
es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
uint32_t event_count);
24 changes: 20 additions & 4 deletions Source/santad/EventProviders/EndpointSecurityTestUtil.mm
Expand Up @@ -18,19 +18,21 @@

#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h"

CF_EXTERN_C_BEGIN
es_string_token_t MakeStringToken(const NSString *s) {
return es_string_token_t{
return (es_string_token_t){
.data = [s UTF8String],
.length = [s length],
};
}
CF_EXTERN_C_END

@implementation ESResponse
@end

@interface MockEndpointSecurity ()
@property NSMutableArray<ESCallback> *responseCallbacks;
@property void *client;
@property NSObject *client;
@property es_handler_block_t handler;
@end

Expand All @@ -39,6 +41,7 @@ - (instancetype)init {
self = [super init];
if (self) {
_responseCallbacks = [NSMutableArray array];
_subscribed = YES;
}
return self;
};
Expand All @@ -48,17 +51,22 @@ - (void)reset {
[self.responseCallbacks removeAllObjects];
self.handler = nil;
self.client = nil;
self.subscribed = NO;
}
};

- (void)newClient:(es_client_t *_Nullable *_Nonnull)client
handler:(es_handler_block_t __strong)handler {
self.client = (void *)client;
// es_client_t is generally used as a pointer to an opaque struct (secretly a mach port).
// We just want to set it to something nonnull for passing initialization checks. It shouldn't
// ever be directly dereferenced.
self.client = [[NSObject alloc] init];
*client = (__bridge es_client_t *)self.client;
self.handler = handler;
}

- (void)triggerHandler:(es_message_t *)msg {
return self.handler((es_client_t *)self.client, msg);
self.handler((__bridge es_client_t *)self.client, msg);
}

- (void)registerResponseCallback:(ESCallback)callback {
Expand Down Expand Up @@ -116,3 +124,11 @@ es_respond_result_t es_respond_auth_result(es_client_t *_Nonnull client,
result:result
cache:cache];
};

API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos)
es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
uint32_t event_count) {
[MockEndpointSecurity mockEndpointSecurity].subscribed = YES;
return ES_RETURN_SUCCESS;
}
6 changes: 6 additions & 0 deletions Source/santad/EventProviders/SNTEndpointSecurityManager.mm
Expand Up @@ -40,6 +40,12 @@ @implementation SNTEndpointSecurityManager
- (instancetype)init API_AVAILABLE(macos(10.15)) {
self = [super init];
if (self) {
// To avoid nil deref from es_events arriving before listenForDecisionRequests or
// listenForLogRequests in the MockEndpointSecurity testing util.
_decisionCallback = ^(santa_message_t) {
};
_logCallback = ^(santa_message_t) {
};
[self establishClient];
_prefixTree = new SNTPrefixTree();
_esAuthQueue =
Expand Down
145 changes: 145 additions & 0 deletions Source/santad/SNTApplicationTest.m
@@ -0,0 +1,145 @@
/// Copyright 2021 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 <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#import <MOLCertificate/MOLCertificate.h>
#import <MOLCodesignChecker/MOLCodesignChecker.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>

#import "Source/common/SNTConfigurator.h"
#import "Source/santad/SNTApplication.h"
#import "Source/santad/SNTDatabaseController.h"

#include "Source/santad/EventProviders/EndpointSecurityTestUtil.h"

@interface SNTApplicationTest : XCTestCase
@property id mockSNTDatabaseController;
@end

@implementation SNTApplicationTest
- (void)setUp {
[super setUp];
fclose(stdout);
self.mockSNTDatabaseController = OCMClassMock([SNTDatabaseController class]);
XCTAssertTrue([[SNTConfigurator configurator] enableSystemExtension]);
}

- (void)tearDown {
[self.mockSNTDatabaseController stopMocking];
[super tearDown];
}

- (void)checkBinaryExecution:(NSString *)binaryName
testPath:(NSString *)testPath
wantResult:(es_auth_result_t)wantResult {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];

OCMStub([self.mockSNTDatabaseController databasePath]).andReturn(testPath);

SNTApplication *app = [[SNTApplication alloc] init];
[app start];

// es events will start flowing in as soon as es_subscribe is called, regardless
// of whether we're ready or not for it.
XCTestExpectation *santaInit =
[self expectationWithDescription:@"Wait for Santa to subscribe to EndpointSecurity"];

dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
while (!mockES.subscribed)
;
[santaInit fulfill];
});

[self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) {
if (error) {
XCTFail(@"Santa's subscription to EndpointSecurity timed out "
@"with error: %@",
error);
}
}];

XCTestExpectation *expectation =
[self expectationWithDescription:@"Wait for santa's Auth dispatch queue"];
__block ESResponse *got = nil;
[mockES registerResponseCallback:^(ESResponse *r) {
@synchronized(self) {
got = r;
[expectation fulfill];
}
}];

NSString *binaryPath = [NSString pathWithComponents:@[ testPath, binaryName ]];
es_file_t binary = {.path = MakeStringToken(binaryPath)};
es_process_t proc = {
.executable = &binary,
.is_es_client = false,
.codesigning_flags = 570509313,
.session_id = 12345,
.group_id = 12345,
.ppid = 12345,
.original_ppid = 12345,
.is_platform_binary = false,
};
es_event_exec_t exec_event = {
.target = &proc,
};
es_events_t event = {.exec = exec_event};
es_message_t m = {
.version = 4,
.mach_time = 181576143417379,
.event_type = ES_EVENT_TYPE_AUTH_EXEC,
.event = event,
.action_type = ES_ACTION_TYPE_AUTH,
.deadline = DISPATCH_TIME_FOREVER,
.process = &proc,
.seq_num = 1,
};

[mockES triggerHandler:&m];

[self
waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) {
if (error) {
XCTFail(
@"Santa auth test on binary \"%@/%@\" timed out with error: %@",
testPath, binaryName, error);
}
}];

XCTAssertEqual(got.result, wantResult, @"received unexpected ES response on executing \"%@/%@\"",
testPath, binaryName);
}

- (void)testBinaryRules {
NSString *testPath = @"santa/Source/santad/testdata/binaryrules";
NSDictionary *testCases = @{
@"badbinary" : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
@"goodbinary" : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
};
NSString *fullTestPath = [NSString pathWithComponents:@[
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testPath
]];

for (NSString *binary in testCases) {
[self checkBinaryExecution:binary
testPath:fullTestPath
wantResult:[testCases[binary] intValue]];
}
}

@end
2 changes: 2 additions & 0 deletions Source/santad/SNTDatabaseController.h
Expand Up @@ -35,4 +35,6 @@
+ (SNTEventTable *)eventTable;
+ (SNTRuleTable *)ruleTable;

+ (NSString *const)databasePath;

@end
16 changes: 11 additions & 5 deletions Source/santad/SNTDatabaseController.m
Expand Up @@ -27,12 +27,17 @@ @implementation SNTDatabaseController
static NSString *const kRulesDatabaseName = @"rules.db";
static NSString *const kEventsDatabaseName = @"events.db";

+ (NSString *const)databasePath {
return kDatabasePath;
}

+ (SNTEventTable *)eventTable {
static SNTEventTable *eventDatabase;
static dispatch_once_t eventDatabaseToken;
dispatch_once(&eventDatabaseToken, ^{
[self createDatabasePath];
NSString *fullPath = [kDatabasePath stringByAppendingPathComponent:kEventsDatabaseName];
NSString *fullPath =
[[SNTDatabaseController databasePath] stringByAppendingPathComponent:kEventsDatabaseName];
FMDatabaseQueue *dbq = [[FMDatabaseQueue alloc] initWithPath:fullPath];
chown([fullPath UTF8String], 0, 0);
chmod([fullPath UTF8String], 0600);
Expand All @@ -54,7 +59,8 @@ + (SNTRuleTable *)ruleTable {
static dispatch_once_t ruleDatabaseToken;
dispatch_once(&ruleDatabaseToken, ^{
[self createDatabasePath];
NSString *fullPath = [kDatabasePath stringByAppendingPathComponent:kRulesDatabaseName];
NSString *fullPath =
[[SNTDatabaseController databasePath] stringByAppendingPathComponent:kRulesDatabaseName];
FMDatabaseQueue *dbq = [[FMDatabaseQueue alloc] initWithPath:fullPath];
chown([fullPath UTF8String], 0, 0);
chmod([fullPath UTF8String], 0600);
Expand Down Expand Up @@ -82,13 +88,13 @@ + (void)createDatabasePath {
NSFilePosixPermissions : @0755
};

if (![fm fileExistsAtPath:kDatabasePath]) {
[fm createDirectoryAtPath:kDatabasePath
if (![fm fileExistsAtPath:[SNTDatabaseController databasePath]]) {
[fm createDirectoryAtPath:[SNTDatabaseController databasePath]
withIntermediateDirectories:YES
attributes:attrs
error:nil];
} else {
[fm setAttributes:attrs ofItemAtPath:kDatabasePath error:nil];
[fm setAttributes:attrs ofItemAtPath:[SNTDatabaseController databasePath] error:nil];
}
}

Expand Down
5 changes: 5 additions & 0 deletions Source/santad/testdata/BUILD
@@ -0,0 +1,5 @@
filegroup(
name = "binaryrules_testdata",
srcs = glob(["binaryrules/*"]),
visibility = ["//:santa_package_group"],
)
Binary file added Source/santad/testdata/binaryrules/badbinary
Binary file not shown.
8 changes: 8 additions & 0 deletions Source/santad/testdata/binaryrules/badbinary.c
@@ -0,0 +1,8 @@
#include <stdio.h>

int main(int argc, char* argv[]) {
const char* evil =
"X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
printf("%s", evil);
return 0;
}
Binary file added Source/santad/testdata/binaryrules/goodbinary
Binary file not shown.
5 changes: 5 additions & 0 deletions Source/santad/testdata/binaryrules/goodbinary.c
@@ -0,0 +1,5 @@
#include <stdio.h>
int main(int argc, char* argv[]) {
printf("Hello world!");
return 0;
}
Binary file added Source/santad/testdata/binaryrules/rules.db
Binary file not shown.

0 comments on commit 10f2d85

Please sign in to comment.