// // file: HelperInterface.m // project: (open-source) installer // description: interface for app installer comms // // created by Patrick Wardle // copyright (c) 2018 Objective-See. All rights reserved. // @import OSLog; #import "consts.h" #import "utilities.h" #import "HelperInterface.h" #import //script name #define CONF_SCRIPT @"configure.sh" //signing auth #define SIGNING_AUTH @"Developer ID Application: Objective-See, LLC (VBG97UB4TA)" /* GLOBALS */ //log handle extern os_log_t logHandle; //dispatch source for SIGTERM dispatch_source_t dispatchSource = nil; @implementation HelperInterface //uninstall // do uninstall logic and return result -(void)uninstall:(NSString*)app prefs:(NSString*)prefs reply:(void (^)(NSNumber*))reply; { //response BOOL response = NO; //dbg msg os_log_debug(logHandle, "XPC-request: uninstall"); //configure // pass in 'uninstall' flag if(0 == [self configure:app arguments:@[CMD_UNINSTALL, prefs]]) { //happy response = YES; } //reply to client reply([NSNumber numberWithBool:response]); return; } //configure // install or uninstall -(int)configure:(NSString*)app arguments:(NSArray*)args { //result int result = -1; //valdiated (copy) of app NSString* validatedApp = nil; //validate app validatedApp = [self validateApp:app]; if(nil == validatedApp) { //err msg os_log_error(logHandle, "ERROR: failed to validate copy of app"); //bail goto bail; } //dbg msg os_log_debug(logHandle, "validated %{public}@", app); //exec script result = [self execScript:validatedApp arguments:args]; if(noErr != result) { //err msg os_log_error(logHandle, "ERROR: failed to execute config script %{public}@ (%d)", CONF_SCRIPT, result); //bail goto bail; } //happy result = 0; bail: //always try to remove validated app if(YES != [[NSFileManager defaultManager] removeItemAtPath:validatedApp error:nil]) { //err msg os_log_error(logHandle, "ERROR: failed to remove validated app %{public}@", validatedApp); //set err result = -1; } return result; } //cleanup by removing self // since system install/launches us as root, client can't directly remove us -(void)cleanup:(void (^)(NSNumber*))reply { //response __block BOOL response = NO; //flag __block BOOL noErrors = YES; //helper plist __block NSString* helperPlist = nil; //binary __block NSString* helperBinary = nil; //error __block NSError* error = nil; //dbg msg os_log_debug(logHandle, "XPC-request: cleanup (removing self)"); //ignore sigterm // handling it via GCD dispatch signal(SIGTERM, SIG_IGN); //init dispatch source for SIGTERM dispatchSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGTERM, 0, dispatch_get_main_queue()); //set handler // deletes plist and self dispatch_source_set_event_handler(dispatchSource, ^{ //dbg msg os_log_debug(logHandle, "XPC: got SIGTERM, deleting plist & self!"); //init path to plist helperPlist = [@"/Library/LaunchDaemons" stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.plist", CONFIG_HELPER_ID]]; //delete plist if(YES != [[NSFileManager defaultManager] removeItemAtPath:helperPlist error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to delete %@ (%@)", helperPlist, error.description); //set error noErrors = NO; } //init path to binary helperBinary = [@"/Library/PrivilegedHelperTools" stringByAppendingPathComponent:CONFIG_HELPER_ID]; //delete self if(YES != [[NSFileManager defaultManager] removeItemAtPath:helperBinary error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to delete %@ (%@)", helperBinary, error.description); //set error noErrors = NO; } //no errors? // display dbg msg if(YES == noErrors) { //happy response = YES; //dbg msg os_log_debug(logHandle, "removed %@ and %@", helperPlist, helperBinary); } //reply to client reply([NSNumber numberWithBool:response]); //bye! exit(SIGTERM); }); //resume dispatch_resume(dispatchSource); //init path to plist helperPlist = [@"/Library/LaunchDaemons" stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.plist", CONFIG_HELPER_ID]]; //unload // will trigger sigterm execTask(LAUNCHCTL, @[@"unload", helperPlist], YES, NO); return; } //make copy of app and validate -(NSString*)validateApp:(NSString*)app { //copy of app NSString* appCopy = nil; //file manager NSFileManager* defaultManager = nil; //path to (now) validated app NSString* validatedApp = nil; //error NSError* error = nil; //grab default file manager defaultManager = [NSFileManager defaultManager]; //init path to app copy // *root-owned* tmp directory appCopy = [NSTemporaryDirectory() stringByAppendingPathComponent:app.lastPathComponent]; //dbg msg os_log_debug(logHandle, "validating %{public}@", appCopy); //delete if old copy is there if(YES == [defaultManager fileExistsAtPath:appCopy]) { //delete if(YES != [defaultManager removeItemAtPath:appCopy error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to delete %{public}@ (error: %{public}@)", appCopy, error.description); } } //copy app bundle to *root-owned* directory if(YES != [defaultManager copyItemAtPath:app toPath:appCopy error:&error]) { //err msg os_log_error(logHandle, "ERROR: failed to copy %{public}@ to %{public}@ (error: %{public}@)", app, appCopy, error.description); //bail goto bail; } //set group/owner to root/wheel if(YES != setFileOwner(appCopy, @0, @0, YES)) { //err msg os_log_error(logHandle, "ERROR: failed to set %{public}@ to be owned by root", appCopy); //bail goto bail; } //verify app // make sure it's signed, and by our signing auth if(noErr != verifyApp(appCopy, SIGNING_AUTH)) { //err msg os_log_error(logHandle, "ERROR: failed to validate %{public}@", appCopy); //bail goto bail; } //happy validatedApp = appCopy; bail: return validatedApp; } //execute config script -(int)execScript:(NSString*)validatedApp arguments:(NSArray*)arguments { //result int result = -1; //results NSDictionary* results = nil; //script NSString* script = nil; //app bundle NSBundle* appBundle = nil; //file manager NSFileManager* fileManager = nil; //current working directory NSString* currentWorkingDir = nil; //init file manager fileManager = [NSFileManager defaultManager]; //load app bundle appBundle = [NSBundle bundleWithPath:validatedApp]; if(nil == appBundle) { //err msg os_log_error(logHandle, "ERROR: failed to load app bundle for %{public}@", validatedApp); //bail goto bail; } //get path to config script script = [[appBundle resourcePath] stringByAppendingPathComponent:CONF_SCRIPT]; if(nil == script) { //err msg os_log_error(logHandle, "ERROR: failed to find config script %{public}@", CONF_SCRIPT); //bail goto bail; } //get current working directory currentWorkingDir = [fileManager currentDirectoryPath]; //change working directory // set it to (validated) app path's resources [fileManager changeCurrentDirectoryPath:[NSString stringWithFormat:@"%@/Contents/Resources/", validatedApp]]; //exec script // wait, but don't grab output results = execTask(script, arguments, YES, NO); //exit code? if(nil != results[EXIT_CODE]) { //grab result = [results[EXIT_CODE] intValue]; } //(re)set current working directory [fileManager changeCurrentDirectoryPath:currentWorkingDir]; bail: return result; } @end