OverSight/OverSightXPC/Enumerator.m

532 lines
14 KiB
Objective-C

//
// Enumerator.m
// cameraUsers
//
// Created by Patrick Wardle on 9/9/16.
// Copyright (c) 2016 Objective-See. All rights reserved.
//
#import "main.h"
#import "Consts.h"
#import "Logging.h"
#import "Utilities.h"
#import "Enumerator.h"
#import <libproc.h>
#import <sys/sysctl.h>
//ignored mach sender procs
// TODO: maybe just ignore apple signed daemons/bg procs!?
static NSArray* ignoredProcs = nil;
@implementation Enumerator
@synthesize machSenders;
@synthesize videoActive;
//init
-(instancetype)init
{
//init
if(self = [super init])
{
//alloc dictionary
machSenders = [NSMutableDictionary dictionary];
//init ignored procs
ignoredProcs = @[
@"/sbin/launchd",
@"/usr/libexec/lsd",
@"/usr/sbin/notifyd",
@"/usr/sbin/syslogd",
@"/usr/sbin/cfprefsd",
@"/usr/libexec/avconferenced",
@"/usr/libexec/opendirectoryd",
@"/usr/libexec/UserEventAgent",
@"/System/Library/CoreServices/launchservicesd",
@"/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/CVMServer",
@"/System/Library/Frameworks/CoreGraphics.framework/Versions/A/Resources/WindowServer",
@"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Support/coreservicesd",
@"/System/Library/Frameworks/VideoToolbox.framework/Versions/A/XPCServices/VTDecoderXPCService.xpc/Contents/MacOS/VTDecoderXPCService"
];
}
return self;
}
//singleton interface
+(id)sharedManager
{
//instance
static Enumerator *sharedEnumerator = nil;
//once token
static dispatch_once_t onceToken;
//init
// ->only exec'd once though :)
dispatch_once(&onceToken, ^{
//init
sharedEnumerator = [[self alloc] init];
});
return sharedEnumerator;
}
//find 'VDCAssistant' or 'AppleCameraAssistant'
-(pid_t)findCameraAssistant
{
//pid of assistant
pid_t cameraAssistant = 0;
//status
int status = -1;
//# of procs
int numberOfProcesses = 0;
//array of pids
pid_t* pids = NULL;
//process path
NSString* processPath = nil;
//get # of procs
numberOfProcesses = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
//alloc buffer for pids
pids = calloc(numberOfProcesses, sizeof(pid_t));
//get list of pids
status = proc_listpids(PROC_ALL_PIDS, 0, pids, numberOfProcesses * sizeof(pid_t));
if(status < 0)
{
//bail
goto bail;
}
//iterate over all pids
// ->get name for each via helper function
for(int i = 0; i < numberOfProcesses; ++i)
{
//skip blank pids
if(0 == pids[i])
{
//skip
continue;
}
//get name
processPath = getProcessPath(pids[i]);
if( (nil == processPath) ||
(0 == processPath.length) )
{
//skip
continue;
}
//is 'VDCAssistant'?
if(YES == [processPath isEqualToString:VDC_ASSISTANT])
{
//save
cameraAssistant = pids[i];
//pau
break;
}
//is 'AppleCameraAssistant'?
else if(YES == [processPath isEqualToString:APPLE_CAMERA_ASSISTANT])
{
//save
cameraAssistant = pids[i];
//pau
break;
}
}
//bail
bail:
//free buffer
if(NULL != pids)
{
//free
free(pids);
}
return cameraAssistant;
}
//forever, baseline by getting all current procs that have sent a mach msg to *Assistant
// ->ensures its only invoke while camera is not in use, so these are all just baselined procs
-(void)start
{
//baseline forever
// ->though logic will skip if video is active
while(YES)
{
//sync baselining
@synchronized(self)
{
//only baseline if video isn't active
if(YES != self.videoActive)
{
//dbg msg
logMsg(LOG_DEBUG, @"baselining mach senders...");
//enumerate procs that have send mach messages
self.machSenders = [self enumMachSenders:[self findCameraAssistant]];
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"found %lu baselined mach senders: %@", (unsigned long)self.machSenders.count, self.machSenders]);
}
}
//nap for a minute
[NSThread sleepForTimeInterval:60];
}
return;
}
//enumerate all (recent) process that appear to be using video
-(NSMutableArray*)enumVideoProcs
{
//current procs
NSMutableArray* videoProcs = nil;
//mach senders
NSMutableDictionary* currentSenders = nil;
//candidate video procs
// ->those that have new mach message
NSMutableArray* candidateVideoProcs = nil;
//pid of camera assistant process
pid_t cameraAssistant = 0;
//alloc
candidateVideoProcs = [NSMutableArray array];
//sync this logic
// ->prevent baselining thread from doing anything
@synchronized(self)
{
//find 'VDCAssistant' or 'AppleCameraAssistant'
cameraAssistant = [self findCameraAssistant];
if(0 == cameraAssistant)
{
//err msg
logMsg(LOG_ERR, @"failed to find VDCAssistant/AppleCameraAssistant process");
//bail
goto bail;
}
//get procs that currrently have sent Mach msg to *Assistant
// ->returns dictionary of process id, and number of mach messages
currentSenders = [self enumMachSenders:cameraAssistant];
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"found %lu current mach senders: %@", (unsigned long)currentSenders.count, currentSenders]);
//remove any known/existing senders
for(NSNumber* processID in currentSenders.allKeys)
{
//add any candidate procs
// ->those that have new mach message
if( [currentSenders[processID] intValue] > [self.machSenders[processID] intValue])
{
//ignore client/requestor
if(clientPID == processID.intValue)
{
//ignore
continue;
}
//add
[candidateVideoProcs addObject:processID];
}
}
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"found %lu candidate video procs: %@", (unsigned long)candidateVideoProcs.count, candidateVideoProcs]);
//update
self.machSenders = currentSenders;
//invoke 'sample' to confirm that candidates are using CMIO/video inputs
// ->note, will skip FaceTime.app on macOS Sierra, as it doesn't do CMIO stuff directly
videoProcs = [self sampleCandidates:candidateVideoProcs];
}//sync
//bail
bail:
return videoProcs;
}
//get procs that currrently have sent Mach msg to *Assistant
// ->returns dictionary of process id, and number of mach messages
-(NSMutableDictionary*)enumMachSenders:(pid_t)cameraAssistant
{
//senders
NSMutableDictionary* senders = nil;
//results from 'lsmp' cmd
NSString* results = nil;
//substrings
NSArray* subStrings = nil;
//process id
NSNumber* processID = nil;
//process path
NSString* processPath = nil;
//alloc
senders = [NSMutableDictionary dictionary];
//exec 'lsmp' w/ pid of camera asssistant to get mach ports
results = [[NSString alloc] initWithData:execTask(LSMP, @[@"-p", @(cameraAssistant).stringValue]) encoding:NSUTF8StringEncoding];
if( (nil == results) ||
(0 == results.length) )
{
//bail
goto bail;
}
//parse results
// ->looking for (<pid>) process name
for(NSString* line in [results componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\n"]])
{
//skip blank lines
if(0 == line.length)
{
//skip
continue;
}
//parse on '()'
subStrings = [line componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"()"]];
if(subStrings.count < 3)
{
//skip
continue;
}
//skip 'unknown' processes
// output looks like "(-) Unknown Process"
if(YES == [[subStrings objectAtIndex:0x1] isEqualToString:@"-"])
{
//skip
continue;
}
//extract process id
// ->insides '()', so will be second substring
processID = @([[subStrings objectAtIndex:0x1] integerValue]);
if(nil == processID)
{
//skip
continue;
}
//ignore self
if(cameraAssistant == processID.intValue)
{
//skip
continue;
}
//get process path
// ->skip blank/unknown procs
processPath = getProcessPath(processID.intValue);
if( (nil == processPath) ||
(0 == processPath.length) )
{
//skip
continue;
}
//ignore apple daemons (that send mach messages, etc)
if(YES == [ignoredProcs containsObject:processPath])
{
//skip
continue;
}
//add/inc to dictionary
senders[processID] = @([senders[processID] unsignedIntegerValue] + 1);
}
//bail
bail:
return senders;
}
//invoke 'sample' to confirm candidates are using CMIO/video inputs
-(NSMutableArray*)sampleCandidates:(NSArray*)currentSenders
{
//current procs
NSMutableArray* videoProcs = nil;
//results from 'sample' cmd
NSString* results = nil;
//process path
NSString* processPath = nil;
//alloc
videoProcs = [NSMutableArray array];
//invoke 'sample' on each
// ->skips FaceTime.app though on macOS Sierra
for(NSNumber* processID in currentSenders)
{
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"processing %d for sampling", processID.intValue]);
//get process path
// ->skip ones that fail
processPath = getProcessPath(processID.intValue);
if( (nil == processPath) ||
(0 == processPath.length) )
{
//next
continue;
}
//if we're running on macOS Sierra and there is only 1 candidate proc and its FaceTime
// ->don't sample, as it does thing wierdly....
if( (YES == [processPath isEqualToString:FACE_TIME]) &&
([getOSVersion() [@"minorVersion"] intValue] >= 12) )
{
//dbg msg
logMsg(LOG_DEBUG, @"not sampling as candidate app is FaceTime on macOS Sierra");
//add
[videoProcs addObject:processID];
//next
continue;
}
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"sampling %d", processID.intValue]);
//exec 'sample' to get threads/dylibs
// ->uses 1.0 seconds for sampling time
results = [[NSString alloc] initWithData:execTask(SAMPLE, @[processID.stringValue, @"1"]) encoding:NSUTF8StringEncoding];
if( (nil == results) ||
(0 == results.length) )
{
//skip
continue;
}
//sampling a process creates a temp file
//->delete it!
[self deleteSampleFile:processPath];
//for now, just check for 'CMIOGraph::DoWork'
// ->TODO: could look for dylibs, other calls, etc
if(YES != [results containsString:@"CMIOGraph::DoWork"])
{
//skip
continue;
}
//looks like a video proc!
[videoProcs addObject:processID];
}
return videoProcs;
}
//'sample' binary creates a file
// ->this looks for that file and deletes it
-(void)deleteSampleFile:(NSString*)processPath
{
//error
NSError* error = nil;
//files
NSArray* files = nil;
//grab all files in /tmp
files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"/tmp/" error:&error];
if(nil != error)
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to enumerate files in /tmp, %@", error]);
//bail
goto bail;
}
//find/delete file
for(NSString* file in files)
{
//skip non-sample files
if(YES != [file hasSuffix:@".sample.txt"])
{
//skip
continue;
}
//ignore files that don't contain process name
if(YES != [file containsString:[processPath lastPathComponent]])
{
//skip
continue;
}
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"deleting sample file: %@", file]);
//delete
if(YES != [[NSFileManager defaultManager] removeItemAtPath:[@"/tmp" stringByAppendingPathComponent:file] error:&error])
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to delete %@ (%@)", file, error]);
//bail
goto bail;
}
}//all files
//bail
bail:
return;
}
//set status of video
-(void)updateVideoStatus:(BOOL)isEnabled
{
//sync
@synchronized(self)
{
//set
self.videoActive = isEnabled;
}
return;
}
@end