Skip to content

Commit

Permalink
Added handling for Remount events to USB mass storage blocking (googl…
Browse files Browse the repository at this point in the history
…e#818)

* Added handling for Remount events to USB mass storage blocking.
  • Loading branch information
pmarkowsky authored Jun 22, 2022
1 parent 01bd1bf commit fde5f52
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 31 deletions.
1 change: 1 addition & 0 deletions Source/santad/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ santa_unit_test(
"@MOLXPCConnection",
"@OCMock",
],
tags = ["exclusive"],
)

santa_unit_test(
Expand Down
73 changes: 54 additions & 19 deletions Source/santad/EventProviders/SNTDeviceManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ - (void)listenES API_AVAILABLE(macos(10.15)) {

es_event_type_t events[] = {
ES_EVENT_TYPE_AUTH_MOUNT,
ES_EVENT_TYPE_AUTH_REMOUNT,
};

es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t));
Expand Down Expand Up @@ -199,44 +200,77 @@ - (void)handleAuthMount:(const es_message_t *)m
return;
}

long mountMode = m->event.mount.statfs->f_flags;
struct statfs *eventStatFS;
BOOL isRemount = NO;

switch (m->event_type) {
case ES_EVENT_TYPE_AUTH_MOUNT: eventStatFS = m->event.mount.statfs; break;
case ES_EVENT_TYPE_AUTH_REMOUNT:
eventStatFS = m->event.remount.statfs;
isRemount = YES;
break;
default:
LOGE(@"Unexpected Event Type passed to DeviceManager handleAuthMount: %d", m->event_type);
// Fail closed.
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
assert("SNTDeviceManager: unexpected event type");
return;
}

long mountMode = eventStatFS->f_flags;
pid_t pid = audit_token_to_pid(m->process->audit_token);
LOGI(@"SNTDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
LOGD(@"SNTDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
m->process->executable->path.data, pid, mountMode);

DADiskRef disk =
DADiskCreateFromBSDName(NULL, self.diskArbSession, m->event.mount.statfs->f_mntfromname);
DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, eventStatFS->f_mntfromname);
CFAutorelease(disk);

// TODO(tnek): Log all of the other attributes available in diskInfo into a structured log format.
NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk));
BOOL isInternal = [diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceInternalKey] boolValue];
BOOL isRemovable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaRemovableKey] boolValue];
BOOL isUSB =
[diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey] isEqualTo:@"USB"];

if (!isRemovable || !isUSB) {
BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue];
NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey];
BOOL isUSB = [protocol isEqualToString:@"USB"];
BOOL isVirtual = [protocol isEqualToString: @"Virtual Interface"];

NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey];

// TODO: check kind and protocol for banned things (e.g. MTP).
LOGD(@"SNTDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d isRemovable: %d "
@"isEjectable: %d",
protocol, kind, isInternal, isRemovable, isEjectable);

// If the device is internal or virtual we are okay with the operation. We
// also are okay with operations for devices that are non-removal as long as
// they are NOT a USB device.
if (isInternal || isVirtual || (!isRemovable && !isEjectable && !isUSB)) {
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
return;
}

SNTDeviceEvent *event = [[SNTDeviceEvent alloc]
initWithOnName:[NSString stringWithUTF8String:m->event.mount.statfs->f_mntonname]
fromName:[NSString stringWithUTF8String:m->event.mount.statfs->f_mntfromname]];
initWithOnName:[NSString stringWithUTF8String:eventStatFS->f_mntonname]
fromName:[NSString stringWithUTF8String:eventStatFS->f_mntfromname]];

BOOL shouldRemount = self.remountArgs != nil && [self.remountArgs count] > 0;

if (shouldRemount) {
event.remountArgs = self.remountArgs;
long remountOpts = mountArgsToMask(self.remountArgs);
if (mountMode & remountOpts) {

LOGD(@"SNTDeviceManager: mountMode: %@", maskToMountArgs(mountMode));
LOGD(@"SNTDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts));

if ((mountMode & remountOpts) == remountOpts && !isRemount) {
LOGD(@"SNTDeviceManager: Allowing as mount as flags match remountOpts");
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
return;
}

long newMode = mountMode | remountOpts;
LOGI(@"SNTDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)",
m->event.mount.statfs->f_mntfromname, m->event.mount.statfs->f_mntonname, mountMode,
newMode);
eventStatFS->f_mntfromname, eventStatFS->f_mntonname, mountMode, newMode);
[self remount:disk mountMode:newMode];
}

Expand Down Expand Up @@ -305,16 +339,17 @@ - (void)handleESMessageWithTimeout:(const es_message_t *)m
- (void)handleESMessage:(const es_message_t *)m
withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) {
switch (m->event_type) {
case ES_EVENT_TYPE_AUTH_MOUNT: {
[self handleAuthMount:m withClient:c];
// Intentional fallthrough
case ES_EVENT_TYPE_AUTH_REMOUNT: {
[[fallthrough]];
}
// TODO(tnek): log any extra data here about mounts.
case ES_EVENT_TYPE_NOTIFY_MOUNT: {
case ES_EVENT_TYPE_AUTH_MOUNT: {
[self handleAuthMount:m withClient:c];
break;
}
default: LOGE(@"SNTDeviceManager: unexpected event type: %d", m->event_type);

default:
LOGE(@"SNTDeviceManager: unexpected event type: %d", m->event_type);
break;
}
}

Expand Down
122 changes: 110 additions & 12 deletions Source/santad/EventProviders/SNTDeviceManagerTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ - (void)setUp {
fclose(stdout);
}

- (ESResponse *)triggerTestMount:(SNTDeviceManager *)deviceManager
mockES:(MockEndpointSecurity *)mockES
mockDA:(MockDiskArbitration *)mockDA {
- (ESResponse *)triggerTestMountEvent:(SNTDeviceManager *)deviceManager
mockES:(MockEndpointSecurity *)mockES
mockDA:(MockDiskArbitration *)mockDA
eventType:(es_event_type_t)eventType
diskInfoOverrides:(NSDictionary *)diskInfo {
if (!deviceManager.subscribed) {
// [deviceManager listen] is synchronous, but we want to asynchronously dispatch it
// with an enforced timeout to ensure that we never run into issues where the client
Expand Down Expand Up @@ -84,26 +86,39 @@ - (ESResponse *)triggerTestMount:(SNTDeviceManager *)deviceManager
@"DAMediaBSDName" : test_mntfromname,
};

if (diskInfo != nil) {
NSMutableDictionary *mergedDiskDescription = [disk.diskDescription mutableCopy];
for (NSString *key in diskInfo) {
mergedDiskDescription[key] = diskInfo[key];
}
disk.diskDescription = (NSDictionary *)mergedDiskDescription;
}

[mockDA insert:disk bsdName:test_mntfromname];

ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
m.binaryPath = @"/System/Library/Filesystems/msdos.fs/Contents/Resources/mount_msdos";
m.message->action_type = ES_ACTION_TYPE_AUTH;
m.message->event_type = ES_EVENT_TYPE_AUTH_MOUNT;
m.message->event = (es_events_t){.mount = {.statfs = fs}};
m.message->event_type = eventType;
if (eventType == ES_EVENT_TYPE_AUTH_MOUNT) {
m.message->event = (es_events_t){.mount = {.statfs = fs}};
} else {
m.message->event = (es_events_t){.remount = {.statfs = fs}};
}
}];

XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
XCTestExpectation *mountExpectation =
[self expectationWithDescription:@"Wait for response from ES"];
__block ESResponse *got;
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_MOUNT
[mockES registerResponseCallback:eventType
withCallback:^(ESResponse *r) {
got = r;
[expectation fulfill];
[mountExpectation fulfill];
}];

[mockES triggerHandler:m.message];

[self waitForExpectations:@[ expectation ] timeout:60.0];
[self waitForExpectations:@[ mountExpectation ] timeout:60.0];
free(fs);

return got;
Expand All @@ -118,7 +133,12 @@ - (void)testUSBBlockDisabled {

SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
deviceManager.blockUSBMount = NO;
ESResponse *got = [self triggerTestMount:deviceManager mockES:mockES mockDA:mockDA];
ESResponse *got = [self triggerTestMountEvent:deviceManager
mockES:mockES
mockDA:mockDA
eventType:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:nil];

XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
}

Expand All @@ -145,7 +165,11 @@ - (void)testRemount {
[expectation fulfill];
};

ESResponse *got = [self triggerTestMount:deviceManager mockES:mockES mockDA:mockDA];
ESResponse *got = [self triggerTestMountEvent:deviceManager
mockES:mockES
mockDA:mockDA
eventType:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:nil];

XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
XCTAssertEqual(mockDA.wasRemounted, YES);
Expand Down Expand Up @@ -179,7 +203,11 @@ - (void)testBlockNoRemount {
[expectation fulfill];
};

ESResponse *got = [self triggerTestMount:deviceManager mockES:mockES mockDA:mockDA];
ESResponse *got = [self triggerTestMountEvent:deviceManager
mockES:mockES
mockDA:mockDA
eventType:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:nil];

XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);

Expand All @@ -190,4 +218,74 @@ - (void)testBlockNoRemount {
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
}

- (void)testEnsureRemountsCannotChangePerms {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];

MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
[mockDA reset];

SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
deviceManager.blockUSBMount = YES;
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];

XCTestExpectation *expectation =
[self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"];

__block NSString *gotmntonname, *gotmntfromname;
__block NSArray<NSString *> *gotRemountedArgs;
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
gotRemountedArgs = event.remountArgs;
gotmntonname = event.mntonname;
gotmntfromname = event.mntfromname;
[expectation fulfill];
};

ESResponse *got = [self triggerTestMountEvent:deviceManager
mockES:mockES
mockDA:mockDA
eventType:ES_EVENT_TYPE_AUTH_REMOUNT
diskInfoOverrides:nil];

XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
XCTAssertEqual(mockDA.wasRemounted, YES);

[self waitForExpectations:@[ expectation ] timeout:10.0];

XCTAssertEqualObjects(gotRemountedArgs, deviceManager.remountArgs);
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
}

- (void)testEnsureDMGsDoNotPrompt {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];

MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
[mockDA reset];

SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
deviceManager.blockUSBMount = YES;
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];

deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
XCTFail(@"Should not be called");
};

NSDictionary *diskInfo = @{
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey: @"Virtual Interface",
(__bridge NSString *)kDADiskDescriptionDeviceModelKey: @"Disk Image",
(__bridge NSString *)kDADiskDescriptionMediaNameKey: @"disk image",
};


ESResponse *got = [self triggerTestMountEvent:deviceManager
mockES:mockES
mockDA:mockDA
eventType:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:diskInfo];

XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
XCTAssertEqual(mockDA.wasRemounted, NO);
}
@end

0 comments on commit fde5f52

Please sign in to comment.