Files

1899 lines
45 KiB
Objective-C

//
// file: utilities.m
// project: BlockBlock (shared)
// description: various helper/utility functions
//
// created by Patrick Wardle
// copyright (c) 2017 Objective-See. All rights reserved.
//
#import "consts.h"
#import "utilities.h"
#import <dlfcn.h>
#import <signal.h>
#import <unistd.h>
#import <libproc.h>
#import <sys/stat.h>
#import <sys/sysctl.h>
#import <Carbon/Carbon.h>
#import <Security/Security.h>
#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonDigest.h>
#import <SystemConfiguration/SystemConfiguration.h>
@import OSLog;
/* GLOBALS */
//log handle
extern os_log_t logHandle;
//get app's version
// extracted from Info.plist
NSString* getAppVersion(void)
{
//read and return 'CFBundleVersion' from bundle
return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
}
//given an app binary
// try get app's bundle
NSBundle* getAppBundle(NSString* binaryPath)
{
//bundle
NSBundle* appBundle = nil;
//app path
NSString* appPath = nil;
//build app path
// assuming path is <blah.app>/Contents/MacOS/<blah>
appPath = [[[binaryPath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent];
if(YES != [appPath hasSuffix:@".app"])
{
//bail
goto bail;
}
//try to load app's bundle
appBundle = [NSBundle bundleWithPath:appPath];
if(nil == appBundle)
{
//bail
goto bail;
}
//sanity check
// binary paths match?
if(YES != [appBundle.executablePath isEqualToString:binaryPath])
{
//unset
appBundle = nil;
goto bail;
}
bail:
return appBundle;
}
//figure out binary's name
// either via app bundle, or from path
NSString* getBinaryName(NSString* path)
{
//name
NSString* name = nil;
//bundle
NSBundle* bundle = nil;
//try get bundle
// then extract name from 'CFBundleName'
bundle = getAppBundle(path);
if(nil != bundle)
{
//extract name
name = [bundle infoDictionary][@"CFBundleName"];
}
//no app bundle || no 'CFBundleName'?
// just use last component from the path
if(nil == name)
{
//set name
name = [path lastPathComponent];
}
return name;
}
//get path to (main) app of a login item
// login item is in app bundle, so parse up to get main app
NSString* getMainAppPath(void)
{
//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;
}
//give path to app
// get full path to its binary
NSString* getBundleExecutable(NSString* appPath)
{
//binary path
NSString* binaryPath = nil;
//app bundle
NSBundle* appBundle = nil;
//load app bundle
appBundle = [NSBundle bundleWithPath:appPath];
if(nil == appBundle)
{
//err msg
os_log_error(logHandle, "ERROR: failed to load app bundle for %{public}@", appPath);
//bail
goto bail;
}
//extract executable
binaryPath = [appBundle.executablePath stringByResolvingSymlinksInPath];
bail:
return binaryPath;
}
#ifndef DAEMON_BUILD
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
//get (true) parent
NSDictionary* getRealParent(pid_t pid)
{
//process info
NSDictionary* processInfo = nil;
//process serial number
ProcessSerialNumber psn = {0, kNoProcess};
//(parent) process serial number
ProcessSerialNumber ppsn = {0, kNoProcess};
//get process serial number from pid
if(noErr != GetProcessForPID(pid, &psn))
{
//err
goto bail;
}
//get process (carbon) info
processInfo = CFBridgingRelease(ProcessInformationCopyDictionary(&psn, (UInt32)kProcessDictionaryIncludeAllInformationMask));
if(nil == processInfo)
{
//err
goto bail;
}
//extract/convert parent ppsn
ppsn.lowLongOfPSN = [processInfo[@"ParentPSN"] longLongValue] & 0x00000000FFFFFFFFLL;
ppsn.highLongOfPSN = ([processInfo[@"ParentPSN"] longLongValue] >> 32) & 0x00000000FFFFFFFFLL;
//get parent process (carbon) info
processInfo = CFBridgingRelease(ProcessInformationCopyDictionary(&ppsn, (UInt32)kProcessDictionaryIncludeAllInformationMask));
if(nil == processInfo)
{
//err
goto bail;
}
bail:
return processInfo;
}
#pragma GCC diagnostic pop
//build an array of processes ancestry
// note: only call from UI session, due to use of carbon APIs
NSMutableArray* generateProcessHierarchy(pid_t pid, NSString* name)
{
//process hierarchy
NSMutableArray* processHierarchy = nil;
//current process id
pid_t currentPID = -1;
//parent pid
pid_t parentPID = -1;
//process name
NSString* parentName = nil;
//alloc
processHierarchy = [NSMutableArray array];
//parent
NSDictionary* parent = nil;
//add current process (leaf)
// parent(s) will then be added at front...
[processHierarchy addObject:[@{@"pid":[NSNumber numberWithInt:pid], @"name":valueForStringItem(name)} mutableCopy]];
//init current to self
currentPID = pid;
//scan back
while(YES)
{
//get (real) parent
parent = getRealParent(currentPID);
if(nil == parent)
{
break;
}
//get parent pid
parentPID = [parent[@"pid"] intValue];
//end of heirarchy?
if( (0 == parentPID) ||
(-1 == parentPID) ||
(currentPID == parentPID) )
{
//bail
break;
}
//get name
// first from bundle, then from executable
parentName = parent[@"CFBundleName"];
if(0 == name.length)
{
//via executable
parentName = [parent[@"CFBundleExecutable"] lastPathComponent];
}
//add parent
// always at front
[processHierarchy insertObject:[@{@"pid":[NSNumber numberWithInt:parentPID], @"name":valueForStringItem(parentName)} mutableCopy] atIndex:0];
//update
currentPID = parentPID;
}
return processHierarchy;
}
#endif
//check if something is nil
// if so, return a default ('unknown') value
NSString* valueForStringItem(NSString* item)
{
return (nil != item) ? item : @"unknown";
}
//find 'top-level' app of binary
// useful to determine if binary (or other app) is embedded in a 'parent' app bundle
NSString* topLevelApp(NSString* binaryPath)
{
//app path
NSString* appPath = nil;
//offset of (first) '.app'
NSRange offset;
//find first instance of '.app' in path
offset = [binaryPath rangeOfString:@".app/" options:NSCaseInsensitiveSearch];
if(NSNotFound == offset.location)
{
//bail
goto bail;
}
//extact app path
// from start, to & including '.app'
appPath = [binaryPath substringWithRange:NSMakeRange(0, offset.location+4)];
bail:
return appPath;
}
//verify that an app bundle is valid
// signed & with (our) signing auth / identifier
OSStatus verifyApp(NSString* path, NSString* signingAuth)
{
//status
OSStatus status = !noErr;
//signing req string
NSString *requirement = nil;
//code
SecStaticCodeRef staticCode = NULL;
//signing reqs
SecRequirementRef requirementRef = NULL;
//init signing req string
requirement = [NSString stringWithFormat:@"anchor apple generic and identifier \"%@\" and certificate leaf [subject.CN] = \"%@\" and info [CFBundleShortVersionString] >= \"1.0.0\"", INSTALLER_ID, signingAuth];
//create static code
status = SecStaticCodeCreateWithPath((__bridge CFURLRef)([NSURL fileURLWithPath:path]), kSecCSDefaultFlags, &staticCode);
if(noErr != status)
{
//err msg
os_log_error(logHandle, "ERROR: 'SecStaticCodeCreateWithPath' failed with %d/%#x", status, status);
goto bail;
}
//create req string
status = SecRequirementCreateWithString((__bridge CFStringRef _Nonnull)(requirement), kSecCSDefaultFlags, &requirementRef);
if( (noErr != status) ||
(requirementRef == NULL) )
{
//err msg
os_log_error(logHandle, "ERROR: 'SecRequirementCreateWithString' failed with %d/%#x", status, status);
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)
{
os_log_error(logHandle, "ERROR: 'SecStaticCodeCheckValidity failed with %d/%#x", status, status);
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 name of logged in user
NSString* getConsoleUser(void)
{
//copy/return user
return CFBridgingRelease(SCDynamicStoreCopyConsoleUser(NULL, NULL, NULL));
}
//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* path)
{
//app's bundle
NSBundle* appBundle = nil;
//standarized path
NSString* standardedPath = nil;
//app's path
NSString* appPath = nil;
//standardize path
standardedPath = [[path stringByStandardizingPath] stringByResolvingSymlinksInPath];
//first just try full path
appPath = standardedPath;
//try to find the app's bundle
do
{
//try to load app's bundle
appBundle = [NSBundle bundleWithPath:appPath];
//was an app passed in?
if(YES == [appBundle.bundlePath isEqualToString:standardedPath])
{
//all done
break;
}
//check for match
// binary path's match
if( (nil != appBundle) &&
(YES == [appBundle.executablePath isEqualToString:standardedPath]))
{
//all done
break;
}
//unset
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;
}
//set dir's|file's group/owner
BOOL setFileOwner(NSString* path, NSNumber* groupID, NSNumber* ownerID, BOOL recursive)
{
//ret var
BOOL bSetOwner = 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
os_log_error(logHandle, "ERROR: failed to set ownership for %{public}@ (%{public}@)", path, fileOwner);
goto bail;
}
//dbg msg
os_log_debug(logHandle, "set ownership for %{public}@ (%{public}@)", 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
os_log_error(logHandle, "ERROR: failed to set ownership for %{public}@ (%{public}@)", fullPath, fileOwner);
goto bail;
}
}
}
//no errors
bSetOwner = YES;
//bail
bail:
return bSetOwner;
}
//set permissions for file
BOOL setFilePermissions(NSString* file, int permissions, BOOL recursive)
{
//ret var
BOOL bSetPermissions = NO;
//file permissions
NSDictionary* filePermissions = nil;
//root directory
NSURL* root = nil;
//directory enumerator
NSDirectoryEnumerator* enumerator = nil;
//error
NSError* error = nil;
//init dictionary
filePermissions = @{NSFilePosixPermissions: [NSNumber numberWithInt:permissions]};
//apply file permissions recursively
if(YES == recursive)
{
//init root
root = [NSURL fileURLWithPath:file];
//init enumerator
enumerator = [[NSFileManager defaultManager] enumeratorAtURL:root includingPropertiesForKeys:[NSArray arrayWithObject:NSURLIsDirectoryKey] options:0 errorHandler:nil];
//set file permissions on each
for(NSURL* currentFile in enumerator)
{
//set permissions
if(YES != [[NSFileManager defaultManager] setAttributes:filePermissions ofItemAtPath:currentFile.path error:&error])
{
//err msg
os_log_error(logHandle, "ERROR: failed to set permissions for %{public}@ (%{public}@), %{public}@", currentFile.path, filePermissions, error);
goto bail;
}
}
}
//always set permissions on passed in file (or top-level directory)
// note: recursive enumerator skips root directory, so execute this always
if(YES != [[NSFileManager defaultManager] setAttributes:filePermissions ofItemAtPath:file error:NULL])
{
//err msg
os_log_error(logHandle, "ERROR: failed to set permissions for %{public}@ (%{public}@)", file, filePermissions);
goto bail;
}
//happy
bSetPermissions = YES;
bail:
return bSetPermissions;
}
//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;
}
//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 = sizeof(procInfo);
//mib
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, -1};
//clear buffer
memset(&procInfo, 0x0, procInfoSize);
//get # of procs
numberOfProcesses = proc_listallpids(NULL, 0);
if(-1 == numberOfProcesses)
{
//bail
goto bail;
}
//alloc buffer for pids
pids = calloc((unsigned long)numberOfProcesses, sizeof(pid_t));
//alloc
processIDs = [NSMutableArray array];
//get list of pids
status = proc_listallpids(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 < (int)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;
}
//enable/disable a menu
void toggleMenu(NSMenu* menu, BOOL shouldEnable)
{
//disable autoenable
menu.autoenablesItems = NO;
//iterate over
// set state of each item
for(NSMenuItem* item in menu.itemArray)
{
//set state
item.enabled = shouldEnable;
}
return;
}
//get an icon for a process
// for apps, this will be app's icon, otherwise just a standard system one
//get icon for a process
NSImage* getIconForProcess(NSString* path)
{
NSImage* icon = nil;
//prefer the app's icon
NSBundle *appBundle = findAppBundle(path);
if(appBundle) {
icon = [NSWorkspace.sharedWorkspace iconForFile:appBundle.bundlePath];
}
//otherwise, generic executable icon
if(!icon) {
if(@available(macOS 11.0, *)) {
icon = [NSWorkspace.sharedWorkspace iconForContentType:UTTypeUnixExecutable];
} else {
icon = [NSWorkspace.sharedWorkspace iconForFileType:@"public.unix-executable"];
}
}
[icon setSize:NSMakeSize(128, 128)];
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;
}
//find a process by name
pid_t findProcess(NSString* processName)
{
//pid
pid_t processID = 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);
if(-1 == numberOfProcesses)
{
//bail
goto bail;
}
//alloc buffer for pids
pids = calloc((unsigned long)numberOfProcesses, sizeof(pid_t));
//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 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;
}
//match?
if(YES == [processPath isEqualToString:processName])
{
//save
processID = pids[i];
//pau
break;
}
}//all procs
bail:
//free buffer
if(NULL != pids)
{
//free
free(pids);
}
return processID;
}
//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
os_log_debug(logHandle, "adding login item %{public}@", loginItem);
//add
LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, (__bridge CFURLRef)(loginItem), NULL, NULL);
//release item ref
if(NULL != itemRef)
{
//dbg msg
os_log_debug(logHandle, "added %{public}@/%{public}@", loginItem, itemRef);
//release
CFRelease(itemRef);
//reset
itemRef = NULL;
}
//failed
else
{
//err msg
os_log_error(logHandle, "ERROR: failed to add login item");
goto bail;
}
//happy
wasToggled = YES;
}
//remove (uninstall)
else
{
//dbg msg
os_log_debug(logHandle, "removing login item %{public}@", loginItem);
//grab existing login items
loginItems = LSSharedFileListCopySnapshot(loginItemsRef, nil);
//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
os_log_error(logHandle, "ERROR: failed to remove login item");
//bail
goto bail;
}
//dbg msg
os_log_debug(logHandle, "removed login item: %{public}@", 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
//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;
}
//hash a file
NSMutableString* hashFile(NSString* filePath)
{
//file's contents
NSData* fileContents = nil;
//hash digest
uint8_t digestSHA256[CC_SHA256_DIGEST_LENGTH] = {0};
//hash as string
NSMutableString* sha256 = nil;
//index var
NSUInteger index = 0;
//init
sha256 = [NSMutableString string];
//load file
if(nil == (fileContents = [NSData dataWithContentsOfFile:filePath]))
{
//bail
goto bail;
}
//sha256 it
CC_SHA256(fileContents.bytes, (unsigned int)fileContents.length, digestSHA256);
//convert to NSString
// iterate over each bytes in computed digest and format
for(index=0; index < CC_SHA256_DIGEST_LENGTH; index++)
{
//format/append
[sha256 appendFormat:@"%02lX", (unsigned long)digestSHA256[index]];
}
bail:
return sha256;
}
/*
//given a pid, get its parent (ppid)
pid_t getParentID(int pid)
{
//parent id
pid_t parentID = -1;
//kinfo_proc struct
struct kinfo_proc processStruct;
//size
size_t procBufferSize = sizeof(processStruct);
//mib
const u_int mibLength = 4;
//syscall result
int sysctlResult = -1;
//init mib
int mib[mibLength] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
//clear buffer
memset(&processStruct, 0x0, procBufferSize);
//make syscall
sysctlResult = sysctl(mib, mibLength, &processStruct, &procBufferSize, NULL, 0);
//check if got ppid
if( (noErr == sysctlResult) &&
(0 != procBufferSize) )
{
//save ppid
parentID = processStruct.kp_eproc.e_ppid;
//dbg msg
os_log_debug(logHandle, "extracted parent ID %d for process: %d", parentID, pid]);
}
return parentID;
}
*/
//start app
// note: executed with 'NSWorkspaceLaunchWithoutActivation'
BOOL startApplication(NSURL* path, NSUInteger launchOptions)
{
//status var
BOOL result = NO;
//error
NSError* error = nil;
//dbg msg
os_log_debug(logHandle, "starting application: %{public}@", path);
//launch it
if(nil == [[NSWorkspace sharedWorkspace] launchApplicationAtURL:path options:launchOptions configuration:@{} error:&error])
{
//err msg
os_log_error(logHandle, "ERROR: failed to launch application: %{public}@/%{public}@", path, error);
goto bail;
}
//happy
result = YES;
bail:
return result;
}
//wait for file to be written to disk
void waitForFile(NSString* path, float maxWait)
{
//wait interval
float waitInterval = 0.1f;
//count var for loop
NSUInteger count = 0;
//wait for file
do
{
//wait for file
if(YES == [[NSFileManager defaultManager] fileExistsAtPath:path])
{
//happy
break;
}
//nap
[NSThread sleepForTimeInterval:waitInterval];
//try up to specified max
} while(count++ < maxWait/waitInterval);
}
//given a bundle path
// wait for plist, then load bundle
NSBundle* getBundle(NSString* path, float maxWait)
{
//bundle
NSBundle* bundle = nil;
//plist path
NSString* plist = nil;
//sanity check
if(nil == path) goto bail;
//init path to plist in bundle
plist = [NSString pathWithComponents:@[path, @"/Contents/Info.plist"]];
//wait for plist
// indicator that bundle 'ready'
waitForFile(plist, maxWait);
//now load bundle
bundle = [NSBundle bundleWithPath:path];
bail:
//load/return bundle
return bundle;
}
//extract value from plist
// takes optional wait time...
id getValueFromPlist(NSString* plistFile, NSString* plistKey, BOOL insensitiveKey, float maxWait)
{
//return var
id plistValue;
//contents of plist
NSDictionary* plistContents = nil;
//wait for file
waitForFile(plistFile, maxWait);
//load it
plistContents = [NSDictionary dictionaryWithContentsOfFile:plistFile];
if(nil == plistContents)
{
//dbg msg
os_log_debug(logHandle, "failed to open/read %{public}@", plistFile);
//bail
goto bail;
}
//try grab value
plistValue = plistContents[plistKey];
//not found?
// try insensitve search
if( (nil == plistValue) &&
(YES == insensitiveKey))
{
//search all keys
for(NSString* key in plistContents)
{
//key match?
// extract value
if(YES == [key.lowercaseString isEqualToString:plistKey.lowercaseString])
{
//found!
plistValue = plistContents[key];
//done
break;
}
}
}
bail:
return plistValue;
}
//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, BOOL grabOutput)
{
//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 alloc] init];
//sanity check
// NSTask throws if path isn't found...
if(YES != [NSFileManager.defaultManager fileExistsAtPath:binaryPath])
{
//bail
goto bail;
}
//only setup pipes if wait flag is set
if(YES == grabOutput)
{
//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
os_log_debug(logHandle, "execing task, %{public}@ (arguments: %{public}@)", task.launchPath, task.arguments);
//wrap task launch
@try
{
//launch
[task launch];
}
@catch(NSException *exception)
{
//err msg
os_log_error(logHandle, "ERROR: failed to launch task (%{public}@)", exception);
//bail
goto bail;
}
//no need to wait
// can just bail w/ no output
if( (YES != shouldWait) &&
(YES != grabOutput) )
{
//bail
goto bail;
}
//wait
// ...but no output
else if( (YES == shouldWait) &&
(YES != grabOutput) )
{
//wait
[task waitUntilExit];
//add exit code
results[EXIT_CODE] = [NSNumber numberWithInteger:task.terminationStatus];
//bail
goto bail;
}
//grab output?
// even if wait not set, still will wait!
else
{
//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:
//dbg msg
os_log_debug(logHandle, "task completed with %{public}@", results);
return results;
}
//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;
}
//check if a file is restricted (SIP)
BOOL isFileRestricted(NSString* file)
{
//flag
BOOL restricted = NO;
//info
struct stat info = {0};
//clear
memset(&info, 0x0, sizeof(struct stat));
//get file info
if(0 == lstat(file.UTF8String, &info))
{
//check flags
restricted = (BOOL)(info.st_flags & SF_RESTRICTED);
}
return restricted;
}
//in dark mode?
BOOL isDarkMode(void)
{
return [[[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"] isEqualToString:@"Dark"];
}
//convert es_string_token_t to string
NSString* convertStringToken(es_string_token_t* stringToken)
{
//string
NSString* string = nil;
//sanity check(s)
if( (NULL == stringToken) ||
(NULL == stringToken->data) ||
(stringToken->length <= 0) )
{
//bail
goto bail;
}
//convert to data, then to string
string = [[NSString alloc] initWithBytes:stringToken->data length:stringToken->length encoding:NSUTF8StringEncoding];
bail:
return string;
}
//convert a textview to a clickable hyperlink
void makeTextViewHyperlink(NSTextField* textField, NSURL* url)
{
//hyperlink
NSMutableAttributedString *hyperlinkString = nil;
//range
NSRange range = {0};
//init hyper link
hyperlinkString = [[NSMutableAttributedString alloc] initWithString:textField.stringValue];
//init range
range = NSMakeRange(0, [hyperlinkString length]);
//start editing
[hyperlinkString beginEditing];
//add url
[hyperlinkString addAttribute:NSLinkAttributeName value:url range:range];
//make it blue
[hyperlinkString addAttribute:NSForegroundColorAttributeName value:[NSColor blueColor] range:NSMakeRange(0, [hyperlinkString length])];
//underline
[hyperlinkString addAttribute:
NSUnderlineStyleAttributeName value:[NSNumber numberWithInt:NSSingleUnderlineStyle] range:NSMakeRange(0, [hyperlinkString length])];
//done editing
[hyperlinkString endEditing];
//set text
[textField setAttributedStringValue:hyperlinkString];
return;
}
#ifdef DAEMON_BUILD
//get current working directory of process
NSString* getCWD(pid_t pid) {
struct proc_vnodepathinfo vinfo = {0};
if(proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, &vinfo, sizeof(vinfo)) > 0) {
NSString* cwd = [NSString stringWithUTF8String:vinfo.pvi_cdir.vip_path];
if(cwd.length) {
return [cwd stringByResolvingSymlinksInPath];
}
}
return nil;
}
//extract scripts from process arguments
NSArray* getScripts(pid_t pid, NSMutableArray* args, NSString* cwd) {
if(args.count < 2) {
return nil;
}
if(!cwd) {
cwd = getCWD(pid);
}
BOOL isDirectory = NO;
NSMutableArray* scripts = [NSMutableArray array];
for(NSUInteger i = 1; i < args.count; i++) {
NSString* arg = args[i];
//resolve relative paths against cwd
NSString* fullPath = nil;
if([arg hasPrefix:@"/"]) {
fullPath = arg;
} else if(cwd) {
fullPath = [cwd stringByAppendingPathComponent:arg];
} else {
continue;
}
//resolve symlinks for consistent paths
fullPath = [fullPath stringByResolvingSymlinksInPath];
//must be an existing regular file
if([NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory] && !isDirectory) {
[scripts addObject:fullPath];
}
}
return scripts.count > 0 ? scripts : nil;
}
//mach time to nanoseconds
// from: https://developer.apple.com/documentation/apple-silicon/addressing-architectural-differences-in-your-macos-code
uint64_t machTimeToNanoseconds(uint64_t machTime)
{
uint64_t nanoseconds = 0;
static mach_timebase_info_data_t sTimebase;
if (sTimebase.denom == 0)
(void)mach_timebase_info(&sTimebase);
nanoseconds = ((machTime * sTimebase.numer) / sTimebase.denom);
return nanoseconds;
}
//check if item is downloaded
BOOL isDownloaded(NSString* path) {
//sanity check
if(!path.length) {
return NO;
}
//get flags
uint32_t quarantineFlags = getQuarantineFlags(path);
//not quarantined?
if(quarantineFlags == QTN_NOT_QUARANTINED) {
os_log_debug(logHandle, "%{public}@ is not quarantined", path);
return NO;
}
//quarantined, but allowed
if(quarantineFlags & QTN_FLAG_USER_APPROVED) {
os_log_debug(logHandle, "%{public}@ is quarantined, but user approved", path);
return NO;
}
return YES;
}
//get items quarantine flags
// thanks: https://trac.webkit.org/changeset/281056/webkit
uint32_t getQuarantineFlags(NSString* path)
{
//error
int error = noErr;
//flags
uint32_t flags = QTN_NOT_QUARANTINED;
//once token
static dispatch_once_t onceToken = 0;
//dylib handle
static void* handle = NULL;
//function pointers
static qtn_file_t(*qtn_file_alloc_FP)(void) = NULL;
static void (*qtn_file_free_FP)(qtn_file_t qf) = NULL;
static uint32_t (*qtn_file_get_flags_FP)(qtn_file_t qf) = NULL;
static int (*qtn_file_init_with_path_FP)(qtn_file_t qf, const char *path) = NULL;
//quarantine file
qtn_file_t quarantineFile = NULL;
//dbg msg
os_log_debug(logHandle, "checking if %{public}@ is quarantined", path);
//sanity check(s)
if(0 == path.length)
{
//err msg
os_log_debug(logHandle, "invalid path");
//bail
goto bail;
}
//load/open framework
dispatch_once(&onceToken, ^{
//open quarantine dylib
handle = dlopen("/usr/lib/system/libquarantine.dylib", RTLD_LAZY);
if(NULL != handle)
{
//resolve function pointers
qtn_file_free_FP = dlsym(handle, "_qtn_file_free");
qtn_file_alloc_FP = dlsym(handle, "_qtn_file_alloc");
qtn_file_get_flags_FP = dlsym(handle, "_qtn_file_get_flags");
qtn_file_init_with_path_FP = dlsym(handle, "_qtn_file_init_with_path");
}
//err
else
{
//err msg
os_log_error(logHandle, "ERROR: failed to 'dlopen' the 'libquarantine.dylib'");
}
});
//sanity check(s)
if( (NULL == qtn_file_free_FP) ||
(NULL == qtn_file_alloc_FP) ||
(NULL == qtn_file_get_flags_FP) ||
(NULL == qtn_file_init_with_path_FP) )
{
//err msg
os_log_error(logHandle, "ERROR: failed to resolve 'libquarantine' function pointers");
//bail
goto bail;
}
//alloc file
quarantineFile = qtn_file_alloc_FP();
if(NULL == quarantineFile)
{
//bail
goto bail;
}
//init file
error = qtn_file_init_with_path_FP(quarantineFile, [NSURL fileURLWithPath:path].path.fileSystemRepresentation);
if(ENOENT == error)
{
//bail
goto bail;
}
//not quarantined?
if(QTN_NOT_QUARANTINED == error)
{
//dbg msg
os_log_debug(logHandle, "%{public}@ is *not* quarantined (QTN_NOT_QUARANTINED)", path);
//bail
goto bail;
}
//get flags
flags = qtn_file_get_flags_FP(quarantineFile);
//dbg msg
os_log_debug(logHandle, "quarantine flags: %#x", flags);
bail:
//cleanup
if(NULL != quarantineFile)
{
//free
qtn_file_free_FP(quarantineFile);
quarantineFile = NULL;
}
return flags;
}
#endif