From 6fecd889e388f7642969d58bfe429a756d29413f Mon Sep 17 00:00:00 2001 From: comex Date: Thu, 29 Jan 2015 21:31:11 -0500 Subject: a bunch of stuff that should have been committed separately --- Makefile | 2 +- ios-bootstrap/bundle-loader.m | 164 +++++++++++++++++++++++++++++++++++- ios-bootstrap/inject-into-launchd.c | 10 +++ ios-bootstrap/posixspawn-hook.c | 119 ++++++++++++++------------ ios-bootstrap/unrestrict.c | 8 ++ lib/darwin/interpose.c | 1 + lib/darwin/unrestrict.c | 34 +++++--- script/gen-deb.sh | 2 +- 8 files changed, 268 insertions(+), 72 deletions(-) diff --git a/Makefile b/Makefile index 7dcc20a..091bf59 100644 --- a/Makefile +++ b/Makefile @@ -174,7 +174,7 @@ all: safety-dance out/posixspawn-hook.dylib: ios-bootstrap/posixspawn-hook.c out/libsubstitute.dylib $(CC) -dynamiclib -o $@ $< -Lout -lsubstitute out/bundle-loader.dylib: ios-bootstrap/bundle-loader.m out/libsubstitute.dylib - $(CC) -dynamiclib -o $@ $< -Lout -lsubstitute + $(CC) -dynamiclib -o $@ $< -fobjc-arc -Lout -framework Foundation -framework CoreFoundation out/unrestrict: ios-bootstrap/unrestrict.c out/libsubstitute.dylib $(CC) -o $@ $< -Lout -lsubstitute ldid -Sent.plist $@ diff --git a/ios-bootstrap/bundle-loader.m b/ios-bootstrap/bundle-loader.m index 532b844..45c37cd 100644 --- a/ios-bootstrap/bundle-loader.m +++ b/ios-bootstrap/bundle-loader.m @@ -1,5 +1,165 @@ -#include +#import +#import +#include +extern char ***_NSGetArgv(void); + +#define PREFIX "Substitute bundle loader: " + +enum test_filters_ret { + PASSED, + FAILED, + INVALID +}; + +static double id_to_double(id o) { + if ([o isKindOfClass:[NSString class]]) { + NSScanner *scan = [NSScanner scannerWithString:o]; + double d; + if (![scan scanDouble:&d] || !scan.atEnd) + return NAN; + return d; + } else if ([o isKindOfClass:[NSNumber class]]) { + return [o doubleValue]; + } else { + return NAN; + } +} + +static enum test_filters_ret test_filters(NSDictionary *plist_dict) { + + NSDictionary *filter = [plist_dict objectForKey:@"Filter"]; + if (!filter) + return PASSED; + if (![filter isKindOfClass:[NSDictionary class]]) + return INVALID; + + for (NSString *key in [filter allKeys]) { + if (!([key isEqualToString:@"CoreFoundationVersion"] || + [key isEqualToString:@"Classes"] || + [key isEqualToString:@"Bundles"] || + [key isEqualToString:@"Executables"])) { + return INVALID; + } + } + NSArray *cfv = [filter objectForKey:@"CoreFoundationVersion"]; + if (cfv) { + if (![cfv isKindOfClass:[NSArray class]] || + [cfv count] == 0 || + [cfv count] > 2) + return INVALID; + double version = kCFCoreFoundationVersionNumber; + double minimum = id_to_double([cfv objectAtIndex:0]); + if (minimum != minimum) + return INVALID; + if (version < minimum) + return FAILED; + id supremum_o = [cfv objectAtIndex:1]; + if (supremum_o) { + double supremum = id_to_double(supremum_o); + if (supremum != supremum) + return INVALID; + if (version >= supremum) + return FAILED; + } + } + + NSArray *classes = [filter objectForKey:@"Classes"]; + if (classes) { + if (![classes isKindOfClass:[NSArray class]]) + return INVALID; + for (NSString *name in classes) { + if (![name isKindOfClass:[NSString class]]) + return INVALID; + if (NSClassFromString(name)) + goto ok1; + } + return FAILED; + ok1:; + } + + NSArray *bundles = [filter objectForKey:@"Bundles"]; + if (bundles) { + NSString *main_identifier = [[NSBundle mainBundle] bundleIdentifier]; + if (!main_identifier) + main_identifier = @""; + if (![bundles isKindOfClass:[NSArray class]]) + return INVALID; + for (NSString *identifier in bundles) { + if (![identifier isKindOfClass:[NSString class]]) + return INVALID; + if ([identifier isEqualToString:main_identifier]) + goto ok2; + } + return FAILED; + ok2:; + } + + + NSArray *executables = [filter objectForKey:@"Executables"]; + if (executables) { + const char *argv0 = (*_NSGetArgv())[0]; + NSString *exe = nil; + if (argv0) { + NSString *nsargv0 = [NSString stringWithCString:argv0 + encoding:NSUTF8StringEncoding]; + exe = [[nsargv0 pathComponents] lastObject]; + } + if (!exe) + exe = @""; + if (![executables isKindOfClass:[NSArray class]]) + return INVALID; + for (NSString *name in executables) { + if (![name isKindOfClass:[NSString class]]) + return INVALID; + if ([name isEqualToString:exe]) + goto ok3; + } + return FAILED; + ok3:; + } + return PASSED; +} + +/* this is DYLD_INSERT_LIBRARIES'd, not injected. */ __attribute__((constructor)) static void init() { - syslog(LOG_WARNING, "Hi!"); + NSError *err; + NSString *base = @"/Library/Substitute/DynamicLibraries"; + NSArray *list = [[NSFileManager defaultManager] + contentsOfDirectoryAtPath:base + error:&err]; + if (!list) + return; + + for (NSString *dylib in list) { + NSLog(@"?%@", dylib); + if (![[dylib pathExtension] isEqualToString:@"dylib"]) + continue; + NSString *plist = [[dylib stringByDeletingPathExtension] + stringByAppendingPathExtension:@"plist"]; + NSString *full_plist = [base stringByAppendingPathComponent:plist]; + NSDictionary *plist_dict = [NSDictionary dictionaryWithContentsOfFile: + full_plist]; + if (!plist_dict) { + NSLog(@PREFIX "missing, unreadable, or invalid plist '%@' for dylib '%@'; unlike Substrate, we require plists", full_plist, dylib); + continue; + } + enum test_filters_ret ret = test_filters(plist_dict); + if (ret == FAILED) { + continue; + } else if (ret == INVALID) { + NSLog(@PREFIX "bad data in plist '%@' for dylib '%@'", full_plist, dylib); + continue; + } + NSString *full_dylib = [base stringByAppendingPathComponent:dylib]; + const char *c_dylib = [full_dylib cStringUsingEncoding:NSUTF8StringEncoding]; + if (!c_dylib) { + NSLog(@PREFIX "Not loading weird dylib path %@", full_dylib); + continue; + } + NSLog(@"Substitute loading %@", full_dylib); + if (!dlopen(c_dylib, RTLD_LAZY)) { + NSLog(@PREFIX "Failed to load %@: %s", full_dylib, dlerror()); + } + } } diff --git a/ios-bootstrap/inject-into-launchd.c b/ios-bootstrap/inject-into-launchd.c index 1da4a02..5624cbc 100644 --- a/ios-bootstrap/inject-into-launchd.c +++ b/ios-bootstrap/inject-into-launchd.c @@ -74,4 +74,14 @@ int main(UNUSED int argc, char **argv) { substitute_strerror(ret), error); return 0; } + /* wait for it to finish */ + static struct { + mach_msg_header_t hdr; + mach_msg_trailer_t huh; + } msg; + kr = mach_msg_overwrite(NULL, MACH_RCV_MSG, 0, sizeof(msg), port, + MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL, + &msg.hdr, 0); + if (kr) + syslog(LOG_EMERG, "mach_msg_overwrite: %x", kr); } diff --git a/ios-bootstrap/posixspawn-hook.c b/ios-bootstrap/posixspawn-hook.c index 9ea9621..f1aff4e 100644 --- a/ios-bootstrap/posixspawn-hook.c +++ b/ios-bootstrap/posixspawn-hook.c @@ -8,6 +8,7 @@ #include #include #include +#include extern char ***_NSGetEnviron(void); @@ -28,6 +29,7 @@ static void open_logfp_if_necessary() { } #define psh_log(fmt, args...) do { \ open_logfp_if_necessary(); \ + syslog(LOG_EMERG, fmt, ##args); \ fprintf(logfp, fmt "\n", ##args); \ fflush(logfp); \ } while(0) @@ -46,7 +48,7 @@ static bool advance(char **strp, const char *template) { return false; } -static bool spawn_unrestrict_me(pid_t pid, bool should_resume) { +static bool spawn_unrestrict(pid_t pid, bool should_resume) { const char *prog = "/Library/Substitute/unrestrict"; char pid_s[32]; sprintf(pid_s, "%ld", (long) pid); @@ -59,8 +61,9 @@ static bool spawn_unrestrict_me(pid_t pid, bool should_resume) { } int xstat; /* reap intermediate to avoid zombie - if it doesn't work, not a big deal */ - if (waitpid(prog_pid, &xstat, 0)) + if (waitpid(prog_pid, &xstat, 0) == -1) psh_log("posixspawn-hook: couldn't waitpid"); + psh_log("unrestrict xstat=%x", xstat); return true; } @@ -75,6 +78,29 @@ static int hook_posix_spawn_generic(__typeof__(posix_spawn) *old, char *const *envp_to_use = envp; char *const *my_envp = envp ? envp : *_NSGetEnviron(); posix_spawnattr_t my_attr = NULL; + + if (attrp) { + posix_spawnattr_t attr = *attrp; + size_t size = malloc_size(attr); + my_attr = malloc(size); + if (!my_attr) + goto crap; + memcpy(my_attr, attr, size); + } else { + if (posix_spawnattr_init(&my_attr)) + goto crap; + } + + short flags; + if (posix_spawnattr_getflags(&my_attr, &flags)) + goto crap; + psh_log("hook_posix_spawn_generic: path=%s%s%s", + path, + (flags & POSIX_SPAWN_SETEXEC) ? " (exec)" : "", + (flags & POSIX_SPAWN_START_SUSPENDED) ? " (suspend)" : ""); + for (char *const *ap = argv; *ap; ap++) + psh_log(" %s", *ap); + /* This mirrors Substrate's logic with safe mode. I don't really * understand the point of the difference between its 'safe' (which removes * Substrate from DYLD_INSERT_LIBRARIES) and 'quit' (which just skips @@ -129,11 +155,12 @@ static int hook_posix_spawn_generic(__typeof__(posix_spawn) *old, if (!safe_mode) { if (newp != newp_orig) *newp++ = ':'; - const char *dylib_to_add = !strncmp(path, "/usr/libexec/xpcproxy")) + const char *dylib_to_add = !strcmp(path, "/usr/libexec/xpcproxy") ? my_dylib_2 : my_dylib_1; newp = stpcpy(newp, dylib_to_add); } + psh_log("using %s", new); /* no libraries? then just get rid of it */ if (newp == newp_orig) { free(new); @@ -157,34 +184,26 @@ static int hook_posix_spawn_generic(__typeof__(posix_spawn) *old, /* Deal with the dumb __restrict section. A complication is that this * could actually be an exec. */ - if (attrp) { - posix_spawnattr_t attr = *attrp; - size_t size = malloc_size(attr); - my_attr = malloc(size); - if (!my_attr) - goto crap; - memcpy(my_attr, attr, size); - } else { - if (posix_spawnattr_init(&my_attr)) - goto crap; - } - short flags; - if (posix_spawnattr_getflags(&my_attr, &flags)) - goto crap; + /* TODO skip this if Substrate is doing it anyway */ bool was_suspended = flags & POSIX_SPAWN_START_SUSPENDED; flags |= POSIX_SPAWN_START_SUSPENDED; if (posix_spawnattr_setflags(&my_attr, flags)) goto crap; if (flags & POSIX_SPAWN_SETEXEC) { - if (!spawn_unrestrict_me(getpid(), !was_suspended)) + /* make the marker fd; hope you weren't using that */ + if (dup2(2, 255) != 255) { + psh_log("dup2 failure - %s", strerror(errno)); + goto skip; + } + if (fcntl(255, F_SETFD, FD_CLOEXEC)) + goto crap; + psh_log("spawning unrestrict"); + if (!spawn_unrestrict(getpid(), !was_suspended)) goto skip; } - - psh_log("calling old ", path); - for (char *const *ap = argv; *ap; ap++) - psh_log(" %s", *ap); + psh_log("**"); int ret = old(pidp, path, file_actions, &my_attr, argv, envp_to_use); - psh_log("ret=%d", ret); + psh_log("ret=%d pid=%ld", ret, (long) *pidp); if (ret) goto cleanup; /* Since it returned, obviously it was not SETEXEC, so we need to @@ -212,7 +231,7 @@ static int hook_posix_spawn_generic(__typeof__(posix_spawn) *old, free(error); }); #else - spawn_unrestrict_me(pid, !was_suspended); + spawn_unrestrict(pid, !was_suspended); #endif goto cleanup; crap: @@ -227,9 +246,9 @@ cleanup: } static void after_wait_generic(pid_t pid, int stat) { + /* TODO safety */ (void) pid; (void) stat; - } int hook_posix_spawn(pid_t *restrict pid, const char *restrict path, @@ -260,12 +279,28 @@ pid_t hook_waitpid(pid_t pid, int *stat_loc, int options) { return ret; } -void substitute_init(struct shuttle *shuttle, UNUSED size_t nshuttle) { - /* Don't hook twice - that will just mess things up. */ - static int already_initted; - if (__atomic_exchange_n(&already_initted, 1, __ATOMIC_RELAXED)) +void substitute_init(struct shuttle *shuttle, size_t nshuttle) { + /* Just tell them we're done */ + if (nshuttle != 1) { + psh_log("nshuttle = %zd?", nshuttle); return; + } + mach_port_t notify_port = shuttle[0].u.mach.port; + mach_msg_header_t done_hdr; + done_hdr.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MOVE_SEND, 0); + done_hdr.msgh_size = sizeof(done_hdr); + done_hdr.msgh_remote_port = notify_port; + done_hdr.msgh_local_port = 0; + done_hdr.msgh_voucher_port = 0; + done_hdr.msgh_id = 42; + kern_return_t kr = mach_msg_send(&done_hdr); + if (kr) + psh_log("posixspawn-hook: mach_msg_send failed: kr=%x", kr); + /* MOVE deallocated the port */ +} +__attribute__((constructor)) +static void init() { /* Note: I'm using interposing to minimize the chance of conflict with * Substrate. This shouldn't actually be necessary, because MSHookProcess, * at least as of the old version I'm looking at the source code of, blocks @@ -305,30 +340,4 @@ end: if (im) substitute_close_image(im); - mach_port_t notify_port = shuttle[0].u.mach.port; - mach_msg_header_t done_hdr; - done_hdr.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0); - done_hdr.msgh_size = sizeof(done_hdr); - done_hdr.msgh_remote_port = notify_port; - done_hdr.msgh_local_port = 0; - done_hdr.msgh_voucher_port = 0; - done_hdr.msgh_id = 42; - kern_return_t kr = mach_msg_send(&done_hdr); - if (kr) - psh_log("posixspawn-hook: mach_msg_send failed: kr=%x", kr); - /* MOVE deallocated the port */ -} - -__attribute__((constructor)) -static void init() { - if (getenv("TEST_POSIXSPAWN_HOOK")) { - mach_port_t port; - assert(!mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_DEAD_NAME, - &port)); - struct shuttle shuttle = { - .type = SUBSTITUTE_SHUTTLE_MACH_PORT, - .u.mach.port = port - }; - substitute_init(&shuttle, 1); - } } diff --git a/ios-bootstrap/unrestrict.c b/ios-bootstrap/unrestrict.c index e9ab4eb..b77bdf5 100644 --- a/ios-bootstrap/unrestrict.c +++ b/ios-bootstrap/unrestrict.c @@ -6,6 +6,13 @@ #include int main(int argc, char **argv) { + char filename[128]; + sprintf(filename, "/tmp/wtf.%ld", + (long) getpid()); + FILE *fp = fopen(filename, "w"); + if (!fp) + return 1; + #define syslog(a, b...) fprintf(fp, b) if (argc != 3) { syslog(LOG_EMERG, "unrestrict: wrong number of args"); return 1; @@ -34,6 +41,7 @@ int main(int argc, char **argv) { } char *err = NULL; + syslog(LOG_EMERG, "unrestrict: unrestricting %d (%s)", (int) pid, should_resume); int sret = substitute_ios_unrestrict((pid_t) pid, should_resume[0] == '1', &err); if (sret) { syslog(LOG_EMERG, "unrestrict: substitute_ios_unrestrict => %d (%s)", diff --git a/lib/darwin/interpose.c b/lib/darwin/interpose.c index bf9ceb4..419fc65 100644 --- a/lib/darwin/interpose.c +++ b/lib/darwin/interpose.c @@ -123,6 +123,7 @@ static int try_bind_section(void *bind, size_t size, const struct interpose_stat } if (h->old_ptr) *(uintptr_t *) h->old_ptr = old - addend; + offset += stride; } break; } diff --git a/lib/darwin/unrestrict.c b/lib/darwin/unrestrict.c index dd852bc..70b089a 100644 --- a/lib/darwin/unrestrict.c +++ b/lib/darwin/unrestrict.c @@ -7,6 +7,10 @@ #include #include +#define PROC_PIDFDVNODEINFO 1 +#define PROC_PIDFDVNODEINFO_SIZE 176 +int proc_pidfdinfo(int, int, int, void *, int); + static int unrestrict_macho_header(void *header, size_t size, bool *did_modify_p, char **error) { *did_modify_p = false; @@ -43,7 +47,7 @@ static int unrestrict_macho_header(void *header, size_t size, bool *did_modify_p for (uint32_t i = 0; i < sc->nsects; i++, sect++) { if (!strncmp(sect->sectname, "__restrict", 16)) { strcpy(sect->sectname, "\xf0\x9f\x92\xa9"); - *did_modify_p =true; + *did_modify_p = true; } } } @@ -72,23 +76,25 @@ int substitute_ios_unrestrict(pid_t pid, bool should_resume, char **error) { int retries = 0; int wait_us = 1; -setback: while (1) { - /* if calling from unrestrict-me, the process might not have transitioned - * yet. if it has, then TASK_DYLD_INFO will be filled with 0. */ - task_basic_info_data_t tbi; - mach_msg_type_number_t cnt = TASK_BASIC_INFO_COUNT; - - kern_return_t kr = task_info(task, TASK_BASIC_INFO, (void *) &tbi, &cnt); - if (kr || cnt != TASK_BASIC_INFO_COUNT) { - asprintf(error, "task_info: %x", kr); + /* If calling from unrestrict-me, the process might not have + * transitioned yet. We set up a dummy fd 255 in the parent process + * which was marked CLOEXEC, so test if that still exists. + * AFAICT, Substrate's equivalent to this is not actually correct. + * TODO cleanup + */ + char buf[PROC_PIDFDVNODEINFO_SIZE]; + int ret = proc_pidfdinfo(pid, 255, PROC_PIDFDVNODEINFO, buf, sizeof(buf)); + if (ret == -1 && errno == EBADF) { + break; + } else if (ret == -1) { + asprintf(error, "proc_pidfdinfo: %s", strerror(errno)); ret = SUBSTITUTE_ERR_MISC; goto fail; } - if (tbi.user_time.seconds == 0 && tbi.user_time.microseconds == 0) - break; + if (retries++ == 20) { - asprintf(error, "user_time was not 0 after 20 retries"); + asprintf(error, "still in parent process after 20 retries"); ret = SUBSTITUTE_ERR_MISC; goto fail; } @@ -101,6 +107,7 @@ setback: /* alrighty then, let's look at the damage. find the first readable * segment */ +setback:; mach_vm_address_t segm_addr = 0; mach_vm_size_t segm_size = 0; vm_region_basic_info_data_64_t info; @@ -111,6 +118,7 @@ setback: (vm_region_info_t) &info, &info_count, &object_name); if (kr == KERN_INVALID_ADDRESS) { /* nothing! maybe it's not there *yet*? this actually is possible */ + usleep(10); goto setback; } else if (kr) { asprintf(error, "mach_vm_region(%lx): %x", (long) segm_addr, kr); diff --git a/script/gen-deb.sh b/script/gen-deb.sh index 3c73fef..81b3efb 100755 --- a/script/gen-deb.sh +++ b/script/gen-deb.sh @@ -13,7 +13,7 @@ ln -s libsubstitute.0.dylib $debroot/usr/lib/libsubstitute.dylib mkdir -p $debroot/usr/include/substitute cp lib/substitute.h $debroot/usr/include/substitute/ cp substrate/substrate.h $debroot/usr/include/substitute/ -mkdir -p $debroot/Library/Substitute +mkdir -p $debroot/Library/Substitute/DynamicLibraries cp out/{posixspawn-hook.dylib,bundle-loader.dylib,unrestrict,inject-into-launchd} $debroot/Library/Substitute/ cp -a DEBIAN $debroot/ sed "s#{VERSION}#$version#g" DEBIAN/control > $debroot/DEBIAN/control -- cgit v1.2.3