Files

902 lines
20 KiB
Objective-C

//
// utilities.m
// ReiKey
//
// Created by Patrick Wardle on 12/24/18.
// Copyright © 2018 Objective-See. All rights reserved.
//
@import Sentry;
#import <libproc.h>
#import <sys/sysctl.h>
#import "consts.h"
#import "logging.h"
#import "utilities.h"
//disable std err
void disableSTDERR()
{
//file handle
int devNull = -1;
//open /dev/null
devNull = open("/dev/null", O_RDWR);
//dup
dup2(devNull, STDERR_FILENO);
//close
close(devNull);
return;
}
//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;
}
//get app's version
// extracted from Info.plist
NSString* getAppVersion()
{
//read and return 'CFBundleVersion' from bundle
return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
}
//determine if installed
// simply checks if application exists in /Applications or ~/Applications
BOOL isInstalled()
{
//flag
BOOL isInstalled = NO;
//check ~/Applications
if(YES == [[NSFileManager defaultManager] fileExistsAtPath:[[USER_APPS_FOLDER stringByExpandingTildeInPath] stringByAppendingPathComponent:APP_NAME]])
{
//set flag
isInstalled = YES;
//done
goto bail;
}
//also check /Applications
if(YES == [[NSFileManager defaultManager] fileExistsAtPath:[SYSTEM_APPS_FOLDER stringByAppendingPathComponent:APP_NAME]])
{
//set flag
isInstalled = YES;
//done
goto bail;
}
bail:
return isInstalled;
}
//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;
//add (install)
if(ACTION_INSTALL == toggleFlag)
{
//always try first remove
// don't care if this fails
removeLoginItem(loginItem);
//now add
wasToggled = addLoginItem(loginItem);
}
//remove (uninstall)
else
{
//remove
wasToggled = removeLoginItem(loginItem);
}
bail:
return wasToggled;
}
//add a login item
BOOL addLoginItem(NSURL* loginItem)
{
//flag
BOOL wasAdded = NO;
//login items ref
LSSharedFileListRef loginItemsRef = NULL;
//login item ref
LSSharedFileListItemRef loginItemRef = NULL;
//get reference to login items
loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"adding login item %@", loginItem]);
//add
loginItemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, (__bridge CFURLRef)(loginItem), NULL, NULL);
if(NULL == loginItemRef)
{
//bail
goto bail;
}
//happy
wasAdded = YES;
bail:
//release item ref
if(NULL != loginItemRef)
{
//release
CFRelease(loginItemRef);
//reset
loginItemRef = NULL;
}
//release login ref
if(NULL != loginItemsRef)
{
//release
CFRelease(loginItemsRef);
//reset
loginItemsRef = NULL;
}
return wasAdded;
}
//remove a login item
BOOL removeLoginItem(NSURL* loginItem)
{
//flag
BOOL wasRemoved = 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);
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"removing login item %@", loginItem]);
//grab existing login items
loginItems = LSSharedFileListCopySnapshot(loginItemsRef, nil);
//iterate over all login items
// look for matching one, 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?
// full path, or, name match
if( (YES == [(__bridge NSURL *)currentLoginItem isEqual:loginItem]) ||
(YES == [((__bridge NSURL *)currentLoginItem).path hasSuffix:loginItem.path.lastPathComponent]) )
{
//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: %@", currentLoginItem]);
//happy
wasRemoved = YES;
//keep going though
}
//release
CFRelease(currentLoginItem);
//reset
currentLoginItem = NULL;
}//all login items
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 wasRemoved;
}
#pragma clang diagnostic pop
//exec a process
BOOL execTask(NSString* binaryPath, NSArray* arguments, BOOL wait)
{
//flag
BOOL wasExec = NO;
//task
NSTask *task = nil;
//init task
task = [[NSTask alloc] init];
//set task's path
task.launchPath = binaryPath;
//set task's args
if(nil != arguments)
{
//add
task.arguments = arguments;
}
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"@exec'ing %@ (args: %@)", binaryPath, arguments]);
//wrap task launch
@try
{
//launch
[task launch];
}
@catch(NSException* exception)
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"task failed with %@", exception]);
//bail
goto bail;
}
//wait?
if(YES == wait)
{
//wait
[task waitUntilExit];
}
//happy
wasExec = YES;
bail:
return wasExec;
}
//get process name
// either via app bundle, or path
NSString* getProcessName(NSString* path)
{
//process name
NSString* processName = nil;
//app bundle
NSBundle* appBundle = nil;
//try find an app bundle
appBundle = findAppBundle(path);
if(nil != appBundle)
{
//grab name from app's bundle
processName = [appBundle infoDictionary][@"CFBundleName"];
}
//still nil?
// ->just grab from path
if(nil == processName)
{
//from path
processName = [path lastPathComponent];
}
return processName;
}
//given a path to binary
// parse it back up to find app's bundle
NSBundle* findAppBundle(NSString* binaryPath)
{
//app's bundle
NSBundle* appBundle = nil;
//app's path
NSString* appPath = nil;
//first just try full path
appPath = binaryPath;
//try to find the app's bundle/info dictionary
do
{
//try to load app's bundle
appBundle = [NSBundle bundleWithPath:appPath];
//check for match
// ->binary path's match
if( (nil != appBundle) &&
(YES == [appBundle.executablePath isEqualToString:binaryPath]))
{
//all done
break;
}
//always unset bundle var since it's being returned
// ->and at this point, its not a match
appBundle = nil;
//remove last part
// ->will try this next
appPath = [appPath stringByDeletingLastPathComponent];
//scan until we get to root
// ->of course, loop will exit if app info dictionary is found/loaded
} while( (nil != appPath) &&
(YES != [appPath isEqualToString:@"/"]) &&
(YES != [appPath isEqualToString:@""]) );
return appBundle;
}
//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
unsigned long systemMaxArgs = 0;
//process's args
char* taskArgs = NULL;
//# of args
int numberOfArgs = 0;
//size of buffers, etc
size_t size = 0;
//reset buffer
memset(pathBuffer, 0x0, 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 = malloc(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
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;
}
//get an icon for a process
// for apps, this will be app's icon, otherwise just a standard system one
NSImage* getIconForProcess(NSString* path)
{
//icon's file name
NSString* iconFile = nil;
//icon's path
NSString* iconPath = nil;
//icon's path extension
NSString* iconExtension = nil;
//icon
NSImage* icon = nil;
//system's document icon
static NSImage* documentIcon = nil;
//bundle
NSBundle* appBundle = nil;
//invalid path?
if(YES != [[NSFileManager defaultManager] fileExistsAtPath:path])
{
//bail
goto bail;
}
//first try grab bundle
// ->then extact icon from this
appBundle = findAppBundle(path);
if(nil != appBundle)
{
//get file
iconFile = appBundle.infoDictionary[@"CFBundleIconFile"];
//get path extension
iconExtension = [iconFile pathExtension];
//if its blank (i.e. not specified)
// go with 'icns'
if(YES == [iconExtension isEqualTo:@""])
{
//set type
iconExtension = @"icns";
}
//set full path
iconPath = [appBundle pathForResource:[iconFile stringByDeletingPathExtension] ofType:iconExtension];
//load it
icon = [[NSImage alloc] initWithContentsOfFile:iconPath];
}
//process is not an app or couldn't get icon
// try to get it via shared workspace
if( (nil == appBundle) ||
(nil == icon) )
{
//extract icon
icon = [[NSWorkspace sharedWorkspace] iconForFile:path];
//load system document icon
// ->static var, so only load once
if(nil == documentIcon)
{
//load
documentIcon = [[NSWorkspace sharedWorkspace] iconForFileType:
NSFileTypeForHFSTypeCode(kGenericDocumentIcon)];
}
//if 'iconForFile' method doesn't find and icon, it returns the system 'document' icon
// the system 'application' icon seems more applicable, so use that here...
if(YES == [icon isEqual:documentIcon])
{
//set icon to system 'applicaiton' icon
icon = [[NSWorkspace sharedWorkspace]
iconForFileType: NSFileTypeForHFSTypeCode(kGenericApplicationIcon)];
}
//'iconForFileType' returns small icons
[icon setSize:NSMakeSize(128, 128)];
}
bail:
return icon;
}
//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;
}
//path to installed app
// if admin: /Applications/<app name>
// if user: ~/Applications/<app name>
NSString* appPath()
{
//app path
NSString* appPath = nil;
//default to /Applications
appPath = [SYSTEM_APPS_FOLDER stringByAppendingPathComponent:APP_NAME];
//can't write to /Applications?
// fall back to user's ~/Applications directory
if(YES != [[NSFileManager defaultManager] isWritableFileAtPath:SYSTEM_APPS_FOLDER])
{
//set to ~/Applications
appPath = [[USER_APPS_FOLDER stringByExpandingTildeInPath] stringByAppendingPathComponent:APP_NAME];
}
bail:
return appPath;
}
//path to login item
NSString* loginItemPath()
{
//init path to login item app
return [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:[NSString stringWithFormat:@"/Contents/Library/LoginItems/%@.app", LOGIN_ITEM_NAME]];
}
//start app
// note: executed with 'NSWorkspaceLaunchWithoutActivation'
BOOL startApplication(NSURL* path, NSUInteger launchOptions)
{
//status var
BOOL result = NO;
//error
NSError* error = nil;
//dbg msg
logMsg(LOG_DEBUG, [NSString stringWithFormat:@"starting application: %@", path]);
//launch it
if(nil == [[NSWorkspace sharedWorkspace] launchApplicationAtURL:path options:launchOptions configuration:@{} error:&error])
{
//err msg
logMsg(LOG_ERR, [NSString stringWithFormat:@"failed to launch application: %@/%@", path, error]);
//bail
goto bail;
}
//happy
result = YES;
bail:
return result;
}
//check if process is alive
BOOL isProcessAlive(pid_t processID)
{
//ret var
BOOL bIsAlive = NO;
//signal status
int signalStatus = -1;
//send kill with 0 to determine if alive
signalStatus = kill(processID, 0);
//is alive?
if( (0 == signalStatus) ||
((0 != signalStatus) && (errno != ESRCH)) )
{
//alive!
bIsAlive = YES;
}
return bIsAlive;
}
//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;
}
//transform app state
OSStatus transformApp(ProcessApplicationTransformState newState)
{
//serial number
// init with current process
ProcessSerialNumber psn = { 0, kCurrentProcess };
//transform and return
return TransformProcessType(&psn, newState);
}
//check if (full) dark mode
// meaning, Mojave+ and dark mode enabled
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;
}
//prettify JSON
NSString* prettifyJSON(NSString* output)
{
//data
NSData* data = nil;
//object
id object = nil;
//pretty data
NSData* prettyData = nil;
//pretty string
NSString* prettyString = nil;
//covert to data
data = [output dataUsingEncoding:NSUTF8StringEncoding];
//convert to JSON
// wrap since we are serializing JSON
@try
{
//serialize
object = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
//covert to pretty data
prettyData = [NSJSONSerialization dataWithJSONObject:object options:NSJSONWritingPrettyPrinted error:nil];
}
@catch(NSException *exception)
{
;
}
//covert to pretty string
if(nil != prettyData)
{
//convert to string
prettyString = [[NSString alloc] initWithData:prettyData encoding:NSUTF8StringEncoding];
}
else
{
//error
prettyString = @"{\"ERROR\" : \"failed to covert output to JSON\"}";
}
return prettyString;
}