Files

1088 lines
26 KiB
Objective-C
Executable File

//
// file: Utilities.m
// project: DND (shared)
// description: various helper/utility functions
//
// created by Patrick Wardle
// copyright (c) 2018 Objective-See. All rights reserved.
//
#import "Consts.h"
#import "Logging.h"
#import "Utilities.h"
#import <signal.h>
#import <unistd.h>
#import <libproc.h>
#import <sys/stat.h>
#import <arpa/inet.h>
#import <sys/socket.h>
#import <sys/sysctl.h>
#import <Security/Security.h>
#import <CommonCrypto/CommonDigest.h>
#import <LocalAuthentication/LocalAuthentication.h>
#import <SystemConfiguration/SystemConfiguration.h>
//init crash reporting
void initCrashReporting()
{
//sentry
NSBundle *sentry = nil;
//error
NSError* error = nil;
//class
Class SentryClient = nil;
//load senty
sentry = loadFramework(@"Sentry.framework");
if(nil == sentry)
{
//err msg
logMsg(LOG_ERR, @"failed to load 'Sentry' framework");
//bail
goto bail;
}
//get client class
SentryClient = NSClassFromString(@"SentryClient");
if(nil == SentryClient)
{
//bail
goto bail;
}
//set shared client
[SentryClient setSharedClient:[[SentryClient alloc] initWithDsn:CRASH_REPORTING_URL didFailWithError:&error]];
if(nil != error)
{
//log error
logMsg(LOG_ERR, [NSString stringWithFormat:@"initializing 'Sentry' failed with %@", error]);
//bail
goto bail;
}
//start crash handler
[[SentryClient sharedClient] startCrashHandlerWithError:&error];
if(nil != error)
{
//log error
logMsg(LOG_ERR, [NSString stringWithFormat:@"starting 'Sentry' crash handler failed with %@", error]);
//bail
goto bail;
}
bail:
return;
}
//loads a framework
// note: assumes it is in 'Framework' dir
NSBundle* loadFramework(NSString* name)
{
//handle
NSBundle* framework = nil;
//framework path
NSString* path = nil;
//init path
path = [NSString stringWithFormat:@"%@/../Frameworks/%@", [NSProcessInfo.processInfo.arguments[0] stringByDeletingLastPathComponent], name];
//standardize path
path = [path stringByStandardizingPath];
//init framework (bundle)
framework = [NSBundle bundleWithPath:path];
if(NULL == framework)
{
//bail
goto bail;
}
//load framework
if(YES != [framework loadAndReturnError:nil])
{
//bail
goto bail;
}
bail:
return framework;
}
//get app's version
// extracted from Info.plist
NSString* getAppVersion()
{
//read and return 'CFBundleVersion' from bundle
return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
}
//get path to (main) app
// login item is in app bundle, so parse up to get main app
NSString* getMainAppPath()
{
//path components
NSArray *pathComponents = nil;
//path to config (main) app
NSString* mainApp = nil;
//get path components
// then build full path to main app
pathComponents = [[[NSBundle mainBundle] bundlePath] pathComponents];
if(pathComponents.count > 4)
{
//init path to full (main) app
mainApp = [NSString pathWithComponents:[pathComponents subarrayWithRange:NSMakeRange(0, pathComponents.count - 4)]];
}
//when (still) nil
// use default path
if(nil == mainApp)
{
//default
mainApp = [@"/Applications" stringByAppendingPathComponent:APP_NAME];
}
return mainApp;
}
//get name of logged in user
NSString* getConsoleUser()
{
//copy/return user
return CFBridgingRelease(SCDynamicStoreCopyConsoleUser(NULL, NULL, NULL));
}
//verify that an app bundle is
// a) signed
// b) signed with signing auth
OSStatus verifyApp(NSString* path, NSString* signingAuth)
{
//status
OSStatus status = !noErr;
//signing req string
NSString *requirementString = nil;
//code
SecStaticCodeRef staticCode = NULL;
//signing reqs
SecRequirementRef requirementRef = NULL;
//init requirement string
requirementString = [NSString stringWithFormat:@"anchor trusted and certificate leaf [subject.CN] = \"%@\"", signingAuth];
//create static code
status = SecStaticCodeCreateWithPath((__bridge CFURLRef)([NSURL fileURLWithPath:path]), kSecCSDefaultFlags, &staticCode);
if(noErr != status)
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"SecStaticCodeCreateWithPath failed w/ %d", status]);
//bail
goto bail;
}
//create req string
status = SecRequirementCreateWithString((__bridge CFStringRef _Nonnull)(requirementString), kSecCSDefaultFlags, &requirementRef);
if( (noErr != status) ||
(requirementRef == NULL) )
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"SecRequirementCreateWithString failed w/ %d", status]);
//bail
goto bail;
}
//check if file is signed w/ apple dev id by checking if it conforms to req string
status = SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, requirementRef);
if(noErr != status)
{
logMsg(LOG_ERR, [NSString stringWithFormat:@"SecStaticCodeCheckValidity failed w/ %d", status]);
//bail
goto bail;
}
//happy
status = noErr;
bail:
//free req reference
if(NULL != requirementRef)
{
//free
CFRelease(requirementRef);
requirementRef = NULL;
}
//free static code
if(NULL != staticCode)
{
//free
CFRelease(staticCode);
staticCode = NULL;
}
return status;
}
//get state of lid
int getLidState()
{
//state
int state = stateUnavailable;
//registry entry for power management
io_registry_entry_t powerManagmentRE = MACH_PORT_NULL;
//reference to 'kAppleClamshellStateKey' property
CFBooleanRef clamshellState = NULL;
//get registry entry for power management root domain
powerManagmentRE = IORegistryEntryFromPath(kIOMasterPortDefault, kIOPowerPlane ":/IOPowerConnection/IOPMrootDomain");
if(MACH_PORT_NULL == powerManagmentRE)
{
//err msg
logMsg(LOG_ERR, @"failed to look up the registry entry for 'IOPMrootDomain'");
//error
goto bail;
}
//get reference to state of 'kAppleClamshellStateKey'
clamshellState = (CFBooleanRef)IORegistryEntryCreateCFProperty(powerManagmentRE, CFSTR(kAppleClamshellStateKey), kCFAllocatorDefault, 0);
if(NULL == clamshellState)
{
//err msg
logMsg(LOG_ERR, @"failed to get property for 'kAppleClamshellStateKey'");
//error
goto bail;
}
//get state
state = (LidState)CFBooleanGetValue(clamshellState);
bail:
//release
if(NULL != clamshellState)
{
//release
CFRelease(clamshellState);
//unset
clamshellState = NULL;
}
//release
if(MACH_PORT_NULL != powerManagmentRE)
{
//release
IOObjectRelease(powerManagmentRE);
//unset
powerManagmentRE = MACH_PORT_NULL;
}
return state;
}
//set dir's|file's group/owner
BOOL setFileOwner(NSString* path, NSNumber* groupID, NSNumber* ownerID, BOOL recursive)
{
//ret var
BOOL bRet = NO;
//owner dictionary
NSDictionary* fileOwner = nil;
//sub paths
NSArray *subPaths = nil;
//full path
// for recursive
NSString* fullPath = nil;
//init permissions dictionary
fileOwner = @{NSFileGroupOwnerAccountID:groupID, NSFileOwnerAccountID:ownerID};
//set group/owner
if(YES != [[NSFileManager defaultManager] setAttributes:fileOwner ofItemAtPath:path error:NULL])
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to set ownership for %@ (%@)", path, fileOwner]);
//bail
goto bail;
}
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"set ownership for %@ (%@)", path, fileOwner]);
//do it recursively
if(YES == recursive)
{
//sanity check
// make sure root starts with '/'
if(YES != [path hasSuffix:@"/"])
{
//add '/'
path = [NSString stringWithFormat:@"%@/", path];
}
//get all subpaths
subPaths = [[NSFileManager defaultManager] subpathsAtPath:path];
for(NSString *subPath in subPaths)
{
//init full path
fullPath = [path stringByAppendingString:subPath];
//set group/owner
if(YES != [[NSFileManager defaultManager] setAttributes:fileOwner ofItemAtPath:fullPath error:NULL])
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to set ownership for %@ (%@)", fullPath, fileOwner]);
//bail
goto bail;
}
}
}
//no errors
bRet = YES;
bail:
return bRet;
}
//given a pid
// get process's path
NSString* getProcessPath(pid_t pid)
{
//task path
NSString* processPath = nil;
//buffer for process path
char pathBuffer[PROC_PIDPATHINFO_MAXSIZE] = {0};
//status
int status = -1;
//'management info base' array
int mib[3] = {0};
//system's size for max args
int systemMaxArgs = 0;
//process's args
char* taskArgs = NULL;
//# of args
int numberOfArgs = 0;
//size of buffers, etc
size_t size = 0;
//reset buffer
bzero(pathBuffer, PROC_PIDPATHINFO_MAXSIZE);
//first attempt to get path via 'proc_pidpath()'
status = proc_pidpath(pid, pathBuffer, sizeof(pathBuffer));
if(0 != status)
{
//init task's name
processPath = [NSString stringWithUTF8String:pathBuffer];
}
//otherwise
// try via task's args ('KERN_PROCARGS2')
else
{
//init mib
// want system's size for max args
mib[0] = CTL_KERN;
mib[1] = KERN_ARGMAX;
//set size
size = sizeof(systemMaxArgs);
//get system's size for max args
if(-1 == sysctl(mib, 2, &systemMaxArgs, &size, NULL, 0))
{
//bail
goto bail;
}
//alloc space for args
taskArgs = (char*)malloc((size_t)systemMaxArgs);
if(NULL == taskArgs)
{
//bail
goto bail;
}
//init mib
// want process args
mib[0] = CTL_KERN;
mib[1] = KERN_PROCARGS2;
mib[2] = pid;
//set size
size = (size_t)systemMaxArgs;
//get process's args
if(-1 == sysctl(mib, 3, taskArgs, &size, NULL, 0))
{
//bail
goto bail;
}
//sanity check
// ensure buffer is somewhat sane
if(size <= sizeof(int))
{
//bail
goto bail;
}
//extract number of args
// at start of buffer
memcpy(&numberOfArgs, taskArgs, sizeof(numberOfArgs));
//extract task's name
// follows # of args (int) and is NULL-terminated
processPath = [NSString stringWithUTF8String:taskArgs + sizeof(int)];
}
bail:
//free process args
if(NULL != taskArgs)
{
//free
free(taskArgs);
//reset
taskArgs = NULL;
}
return processPath;
}
//exec a process with args
// if 'shouldWait' is set, wait and return stdout/in and termination status
NSMutableDictionary* execTask(NSString* binaryPath, NSArray* arguments, BOOL shouldWait)
{
//task
NSTask* task = nil;
//output pipe for stdout
NSPipe* stdOutPipe = nil;
//output pipe for stderr
NSPipe* stdErrPipe = nil;
//read handle for stdout
NSFileHandle* stdOutReadHandle = nil;
//read handle for stderr
NSFileHandle* stdErrReadHandle = nil;
//results dictionary
NSMutableDictionary* results = nil;
//output for stdout
NSMutableData *stdOutData = nil;
//output for stderr
NSMutableData *stdErrData = nil;
//init dictionary for results
results = [NSMutableDictionary dictionary];
//init task
task = [NSTask new];
//only setup pipes if wait flag is set
if(YES == shouldWait)
{
//init stdout pipe
stdOutPipe = [NSPipe pipe];
//init stderr pipe
stdErrPipe = [NSPipe pipe];
//init stdout read handle
stdOutReadHandle = [stdOutPipe fileHandleForReading];
//init stderr read handle
stdErrReadHandle = [stdErrPipe fileHandleForReading];
//init stdout output buffer
stdOutData = [NSMutableData data];
//init stderr output buffer
stdErrData = [NSMutableData data];
//set task's stdout
task.standardOutput = stdOutPipe;
//set task's stderr
task.standardError = stdErrPipe;
}
//set task's path
task.launchPath = binaryPath;
//set task's args
if(nil != arguments)
{
//set
task.arguments = arguments;
}
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"execing task, %@ (arguments: %@)", task.launchPath, task.arguments]);
//wrap task launch
@try
{
//launch
[task launch];
}
@catch(NSException *exception)
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to launch task (%@)", exception]);
//bail
goto bail;
}
//no need to wait
// can just bail w/ no output
if(YES != shouldWait)
{
//bail
goto bail;
}
//read in stdout/stderr
while(YES == [task isRunning])
{
//accumulate stdout
[stdOutData appendData:[stdOutReadHandle readDataToEndOfFile]];
//accumulate stderr
[stdErrData appendData:[stdErrReadHandle readDataToEndOfFile]];
}
//grab any leftover stdout
[stdOutData appendData:[stdOutReadHandle readDataToEndOfFile]];
//grab any leftover stderr
[stdErrData appendData:[stdErrReadHandle readDataToEndOfFile]];
//add stdout
if(0 != stdOutData.length)
{
//add
results[STDOUT] = stdOutData;
}
//add stderr
if(0 != stdErrData.length)
{
//add
results[STDERR] = stdErrData;
}
//add exit code
results[EXIT_CODE] = [NSNumber numberWithInteger:task.terminationStatus];
bail:
return results;
}
//given a process path and user
// return array of all matching pids
NSMutableArray* getProcessIDs(NSString* processPath, int userID)
{
//status
int status = -1;
//process IDs
NSMutableArray* processIDs = nil;
//# of procs
int numberOfProcesses = 0;
//array of pids
pid_t* pids = NULL;
//process info struct
struct kinfo_proc procInfo;
//size of struct
size_t procInfoSize = 0;
//mib
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, -1};
//init size
procInfoSize = sizeof(struct kinfo_proc);
//zero out
memset(&procInfo, 0, procInfoSize);
//get # of procs
numberOfProcesses = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
//alloc buffer for pids
pids = calloc((size_t)numberOfProcesses, sizeof(pid_t));
//alloc
processIDs = [NSMutableArray array];
//get list of pids
status = proc_listpids(PROC_ALL_PIDS, 0, pids, numberOfProcesses * (int)sizeof(pid_t));
if(status < 0)
{
//bail
goto bail;
}
//iterate over all pids
// get name for each process
for(int i = 0; i < numberOfProcesses; i++)
{
//skip blank pids
if(0 == pids[i])
{
//skip
continue;
}
//skip if path doesn't match
if(YES != [processPath isEqualToString:getProcessPath(pids[i])])
{
//next
continue;
}
//need to also match on user?
// caller can pass in -1 to skip this check
if(-1 != userID)
{
//init mib
mib[0x3] = pids[i];
//make syscall to get proc info for user
if( (0 != sysctl(mib, 0x4, &procInfo, &procInfoSize, NULL, 0)) ||
(0 == procInfoSize) )
{
//skip
continue;
}
//skip if user id doesn't match
if(userID != (int)procInfo.kp_eproc.e_ucred.cr_uid)
{
//skip
continue;
}
}
//got match
// add to list
[processIDs addObject:[NSNumber numberWithInt:pids[i]]];
}
bail:
//free buffer
if(NULL != pids)
{
//free
free(pids);
//reset
pids = NULL;
}
return processIDs;
}
//wait until a window is non nil
// then make it modal
void makeModal(NSWindowController* windowController)
{
//wait up to 1 second window to be non-nil
// then make modal
for(int i=0; i<20; i++)
{
//can make it modal once we have a window
if(nil != windowController.window)
{
//make modal on main thread
dispatch_sync(dispatch_get_main_queue(), ^{
//modal
[[NSApplication sharedApplication] runModalForWindow:windowController.window];
});
//all done
break;
}
//nap
[NSThread sleepForTimeInterval:0.05f];
}//until 1 second
return;
}
//check if an instance of an app is already running
BOOL isAppRunning(NSString* bundleID)
{
//flag
BOOL alreadyRunning = NO;
//aleady an instance?
// make that instance active and then bail
for(NSRunningApplication* runningApp in [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleID])
{
//another instance that's not this?
if(YES != [runningApp isEqual:[NSRunningApplication currentApplication]])
{
//set flag
alreadyRunning = YES;
//make (already) running instance first
[runningApp activateWithOptions:NSApplicationActivateAllWindows|NSApplicationActivateIgnoringOtherApps];
//done looking
break;
}
}
return alreadyRunning;
}
//for login item enable/disable
// we use the launch services APIs, since replacements don't always work :(
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
//toggle login item
// either add (install) or remove (uninstall)
BOOL toggleLoginItem(NSURL* loginItem, int toggleFlag)
{
//flag
BOOL wasToggled = NO;
//login item ref
LSSharedFileListRef loginItemsRef = NULL;
//login items
CFArrayRef loginItems = NULL;
//current login item
CFURLRef currentLoginItem = NULL;
//get reference to login items
loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
//add (install)
if(ACTION_INSTALL_FLAG == toggleFlag)
{
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"adding login item %@", loginItem]);
//add
LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, (__bridge CFURLRef)(loginItem), NULL, NULL);
//release item ref
if(NULL != itemRef)
{
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"added %@/%@", loginItem, itemRef]);
//release
CFRelease(itemRef);
//reset
itemRef = NULL;
}
//failed
else
{
//err msg
logMsg(LOG_ERR, @"failed to add login item");
//bail
goto bail;
}
//happy
wasToggled = YES;
}
//remove (uninstall)
else
{
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"removing login item %@", loginItem]);
//grab existing login items
loginItems = LSSharedFileListCopySnapshot(loginItemsRef, nil);
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"found %lu login items", (unsigned long)((__bridge NSArray *)loginItems).count]);
//iterate over all login items
// look for self, then remove it
for(id item in (__bridge NSArray *)loginItems)
{
//get current login item
currentLoginItem = LSSharedFileListItemCopyResolvedURL((__bridge LSSharedFileListItemRef)item, 0, NULL);
if(NULL == currentLoginItem)
{
//skip
continue;
}
//current login item match self?
if(YES == [(__bridge NSURL *)currentLoginItem isEqual:loginItem])
{
//remove
if(noErr != LSSharedFileListItemRemove(loginItemsRef, (__bridge LSSharedFileListItemRef)item))
{
//err msg
logMsg(LOG_ERR, @"failed to remove login item");
//bail
goto bail;
}
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"removed login item: %@", loginItem]);
//happy
wasToggled = YES;
//all done
goto bail;
}
//release
CFRelease(currentLoginItem);
//reset
currentLoginItem = NULL;
}//all login items
}//remove/uninstall
bail:
//release login items
if(NULL != loginItems)
{
//release
CFRelease(loginItems);
//reset
loginItems = NULL;
}
//release login ref
if(NULL != loginItemsRef)
{
//release
CFRelease(loginItemsRef);
//reset
loginItemsRef = NULL;
}
//release url
if(NULL != currentLoginItem)
{
//release
CFRelease(currentLoginItem);
//reset
currentLoginItem = NULL;
}
return wasToggled;
}
#pragma clang diagnostic pop
//touchID capable?
BOOL hasTouchID()
{
//flag
BOOL hasTouchID = NO;
//local auth context
LAContext* localauthContext = nil;
//error
NSError* error = nil;
//alloc/init
localauthContext = [[LAContext alloc] init];
//eval w/ try/catch as it can throw!
// note though we ignore "Code=-4 Touch Bar is not available in closed clamshell mode"
@try
{
//eval
if( (YES != [localauthContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) &&
(error.code != -4) )
{
//bail
goto bail;
}
}
//exception likely means 'unknown policy'
// so we can safely ignore/bail and 'hasTouchID' won't be set
@catch(NSException *exception)
{
//bail
goto bail;
}
//macOS only supports touchID
// TODO: update this if macOS ever has faceID! (see check below)
hasTouchID = YES;
/*
//set flag
if(@available(macOS 10.13.2, *))
{
hasTouchID = (LABiometryTypeTouchID == localauthContext.biometryType);
}
*/
bail:
return hasTouchID;
}
//get current console user
NSString* currentConsoleUser(SCDynamicStoreRef store)
{
//user
NSString* consoleUser = nil;
//grab console user
consoleUser = CFBridgingRelease(SCDynamicStoreCopyConsoleUser(store, NULL, NULL));
if(nil == consoleUser)
{
//bail
goto bail;
}
//ignore 'loginwindow'
if(YES == [consoleUser isEqualToString:@"loginwindow"])
{
//unset
consoleUser = nil;
//bail
goto bail;
}
bail:
return consoleUser;
}
//macOS Mojave+ gotta request camera access
// note, we check first, to see if request was already made
void requestCameraAccess()
{
//on macOS 10.14+
// trigger request for camera access
if(@available(macOS 10.14, *))
{
//auth status
AVAuthorizationStatus authStatus = AVAuthorizationStatusNotDetermined;
//get current status
authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
//already requested?
if(authStatus != AVAuthorizationStatusNotDetermined)
{
//bail
goto bail;
}
//request
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted)
{
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"user response for camera access: %d", granted]);
}];
}
bail:
return;
}
//check if (true) dark mode
BOOL isDarkMode()
{
//flag
BOOL darkMode = NO;
//not mojave?
// bail, since not true dark mode
if(YES != [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){10, 14, 0}])
{
//bail
goto bail;
}
//not dark mode?
if(YES != [[[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"] isEqualToString:@"Dark"])
{
//bail
goto bail;
}
//ok, mojave dark mode it is!
darkMode = YES;
bail:
return darkMode;
}