diff --git a/ios/main.mm b/ios/main.mm index c111208aba..d91300302b 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -3,12 +3,16 @@ #import #import #import +#import +#import #import +#import #import #import #import #import #import +#import #import #import "AppDelegate.h" @@ -22,84 +26,261 @@ #include "Common/StringUtils.h" #include "Common/Profiler/Profiler.h" #include "Core/Config.h" +#include "Common/Log.h" #include "UI/DarwinFileSystemServices.h" +struct cs_blob_index { + uint32_t type; + uint32_t offset; +}; + +struct cs_superblob { + uint32_t magic; + uint32_t length; + uint32_t count; + struct cs_blob_index index[]; +}; + +struct cs_entitlements { + uint32_t magic; + uint32_t length; + char entitlements[]; +}; + static int (*csops)(pid_t pid, unsigned int ops, void * useraddr, size_t usersize); static boolean_t (*exc_server)(mach_msg_header_t *, mach_msg_header_t *); static int (*ptrace)(int request, pid_t pid, caddr_t addr, int data); #define CS_OPS_STATUS 0 /* return status */ +#define CS_KILL 0x00000200 /* kill process if it becomes invalid */ #define CS_DEBUGGED 0x10000000 /* process is currently or has previously been debugged and allowed to run with invalid pages */ -#define PT_ATTACHEXC 14 /* attach to running process with signal exception */ -#define PT_DETACH 11 /* stop tracing a process */ +#define PT_TRACE_ME 0 /* child declares it's being traced */ +#define PT_SIGEXC 12 /* signals as exceptions for current_proc */ #define ptrace(a, b, c, d) syscall(SYS_ptrace, a, b, c, d) -bool get_debugged() { - int flags; - int rv = csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags)); - if (rv==0 && flags&CS_DEBUGGED) return true; - - pid_t pid = fork(); - if (pid > 0) { - int st,rv,i=0; - do { - usleep(500); - rv = waitpid(pid, &st, 0); - } while (rv<0 && i++<10); - if (rv<0) fprintf(stderr, "Unable to wait for child?\n"); - } else if (pid == 0) { - pid_t ppid = getppid(); - int rv = ptrace(PT_ATTACHEXC, ppid, 0, 0); - if (rv) { - perror("Unable to attach to process"); - exit(1); - } - for (int i=0; i<100; i++) { - usleep(1000); - errno = 0; - rv = ptrace(PT_DETACH, ppid, 0, 0); - if (rv==0) break; - } - if (rv) { - perror("Unable to detach from process"); - exit(1); - } - exit(0); - } else { - perror("Unable to fork"); - } - - rv = csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags)); - if (rv==0 && flags&CS_DEBUGGED) return true; - - return false; -} - -kern_return_t catch_exception_raise(mach_port_t exception_port, - mach_port_t thread, - mach_port_t task, - exception_type_t exception, - exception_data_t code, - mach_msg_type_number_t code_count) { - return KERN_FAILURE; -} - -void *exception_handler(void *argument) { - auto port = *reinterpret_cast(argument); +static void *exception_handler(void *argument) { + mach_port_t port = *(mach_port_t *)argument; mach_msg_server(exc_server, 2048, port, 0); return NULL; } +static NSDictionary *parse_entitlements(const void *entitlements, size_t length) { + char *copy = (char *)malloc(length); + memcpy(copy, entitlements, length); + + // strip out psychic paper entitlement hiding + if (@available(iOS 13.5, *)) { + } else { + static const char *needle = ""; + char *found = strnstr(copy, needle, length); + if (found) { + memset(found, ' ', strlen(needle)); + } + } + NSData *data = [NSData dataWithBytes:copy length:length]; + free(copy); + + return [NSPropertyListSerialization propertyListWithData:data + options:NSPropertyListImmutable + format:nil + error:nil]; +} + +static NSDictionary *app_entitlements(void) { + // Inspired by codesign.c in Darwin sources for Security.framework + + // Find our mach-o header + Dl_info dl_info; + if (dladdr((const void *)app_entitlements, &dl_info) == 0) + return nil; + if (dl_info.dli_fbase == NULL) + return nil; + char *base = (char *)dl_info.dli_fbase; + struct mach_header_64 *header = (struct mach_header_64 *)dl_info.dli_fbase; + if (header->magic != MH_MAGIC_64) + return nil; + + // Simulator executables have fake entitlements in the code signature. The real entitlements can be found in an __entitlements section. + size_t entitlements_size; + uint8_t *entitlements_data = getsectiondata(header, "__TEXT", "__entitlements", &entitlements_size); + if (entitlements_data != NULL) { + NSData *data = [NSData dataWithBytesNoCopy:entitlements_data + length:entitlements_size + freeWhenDone:NO]; + return [NSPropertyListSerialization propertyListWithData:data + options:NSPropertyListImmutable + format:nil + error:nil]; + } + + // Find the LC_CODE_SIGNATURE + struct load_command *lc = (struct load_command *) (base + sizeof(*header)); + struct linkedit_data_command *cs_lc = NULL; + for (uint32_t i = 0; i < header->ncmds; i++) { + if (lc->cmd == LC_CODE_SIGNATURE) { + cs_lc = (struct linkedit_data_command *) lc; + break; + } + lc = (struct load_command *) ((char *) lc + lc->cmdsize); + } + if (cs_lc == NULL) + return nil; + + // Read the code signature off disk, as it's apparently not loaded into memory + NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:NSBundle.mainBundle.executableURL error:nil]; + if (fileHandle == nil) + return nil; + [fileHandle seekToFileOffset:cs_lc->dataoff]; + NSData *csData = [fileHandle readDataOfLength:cs_lc->datasize]; + [fileHandle closeFile]; + const struct cs_superblob *cs = (const struct cs_superblob *)csData.bytes; + if (ntohl(cs->magic) != 0xfade0cc0) + return nil; + + // Find the entitlements in the code signature + for (uint32_t i = 0; i < ntohl(cs->count); i++) { + struct cs_entitlements *ents = (struct cs_entitlements *) ((char *) cs + ntohl(cs->index[i].offset)); + if (ntohl(ents->magic) == 0xfade7171) { + return parse_entitlements(ents->entitlements, ntohl(ents->length) - offsetof(struct cs_entitlements, entitlements)); + } + } + return nil; +} + +static NSDictionary *cached_app_entitlements(void) { + static NSDictionary *entitlements = nil; + if (!entitlements) { + entitlements = app_entitlements(); + } + return entitlements; +} + +bool jb_has_container(void) { + NSDictionary *entitlements = cached_app_entitlements(); + return ![entitlements[@"com.apple.private.security.no-sandbox"] boolValue]; +} + +extern const char *environ[]; + +static char *childArgv[] = {NULL, "debugme", NULL}; + +bool jb_spawn_ptrace_child(int argc, char **argv) { + int ret; pid_t pid; + + if (argc > 1 && strcmp(argv[1], childArgv[1]) == 0) { + ret = ptrace(PT_TRACE_ME, 0, NULL, 0); + NSLog(@"child: ptrace(PT_TRACE_ME) %d", ret); + exit(ret); + } + if (jb_has_container()) { + return false; + } + childArgv[0] = argv[0]; + if ((ret = posix_spawnp(&pid, argv[0], NULL, NULL, (char *const *)childArgv, NULL)) != 0) { + return false; + } + return true; +} + +bool jb_has_jit_entitlement(void) { + NSDictionary *entitlements = cached_app_entitlements(); + return [entitlements[@"dynamic-codesigning"] boolValue]; +} + +bool jb_has_cs_disabled(void) { + int flags; + return !csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags)) && (flags & ~CS_KILL) == flags; +} + +#define _COMM_PAGE_START_ADDRESS (0x0000000FFFFFC000ULL) /* In TTBR0 */ +#define _COMM_PAGE_APRR_SUPPORT (_COMM_PAGE_START_ADDRESS+0x10C) + +static bool is_device_A12_or_newer(void) { + // devices without APRR are definitely < A12 + char aprr_support = *(volatile char *)_COMM_PAGE_APRR_SUPPORT; + if (aprr_support == 0) { + return false; + } + // we still have A11 devices that support APRR + struct utsname systemInfo; + if (uname(&systemInfo) != 0) { + return false; + } + // iPhone 8, 8 Plus, and iPhone X + if (strncmp("iPhone10,", systemInfo.machine, 9) == 0) { + return false; + } else { + return true; + } +} + +bool jb_has_cs_execseg_allow_unsigned(void) { + NSDictionary *entitlements = cached_app_entitlements(); + if (@available(iOS 14.2, *)) { + if (@available(iOS 14.4, *)) { + return false; // iOS 14.4 broke it again + } + // technically we need to check the Code Directory and make sure + // CS_EXECSEG_ALLOW_UNSIGNED is set but we assume that it is properly + // signed, which should reflect the get-task-allow entitlement + return is_device_A12_or_newer() && [entitlements[@"get-task-allow"] boolValue]; + } else { + return false; + } +} + +static bool jb_has_debugger_attached(void) { + int flags; + return !csops(getpid(), CS_OPS_STATUS, &flags, sizeof(flags)) && flags & CS_DEBUGGED; +} + +bool jb_enable_ptrace_hack(void) { + bool debugged = jb_has_debugger_attached(); + + // Thanks to this comment: https://news.ycombinator.com/item?id=18431524 + // We use this hack to allow mmap with PROT_EXEC (which usually requires the + // dynamic-codesigning entitlement) by tricking the process into thinking + // that Xcode is debugging it. We abuse the fact that JIT is needed to + // debug the process. + if (ptrace(PT_TRACE_ME, 0, NULL, 0) < 0) { + return false; + } + + // ptracing ourselves confuses the kernel and will cause bad things to + // happen to the system (hangs…) if an exception or signal occurs. Setup + // some "safety nets" so we can cause the process to exit in a somewhat sane + // state. We only need to do this if the debugger isn't attached. (It'll do + // this itself, and if we do it we'll interfere with its normal operation + // anyways.) + if (!debugged) { + // First, ensure that signals are delivered as Mach software exceptions… + ptrace(PT_SIGEXC, 0, NULL, 0); + + // …then ensure that this exception goes through our exception handler. + // I think it's OK to just watch for EXC_SOFTWARE because the other + // exceptions (e.g. EXC_BAD_ACCESS, EXC_BAD_INSTRUCTION, and friends) + // will end up being delivered as signals anyways, and we can get them + // once they're resent as a software exception. + mach_port_t port = MACH_PORT_NULL; + mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port); + mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND); + task_set_exception_ports(mach_task_self(), EXC_MASK_SOFTWARE, port, EXCEPTION_DEFAULT, THREAD_STATE_NONE); + pthread_t thread; + pthread_create(&thread, NULL, exception_handler, (void *)&port); + } + + return true; +} + float g_safeInsetLeft = 0.0; float g_safeInsetRight = 0.0; float g_safeInsetTop = 0.0; float g_safeInsetBottom = 0.0; // We no longer need to judge if jit is usable or not by according to the ios version. -/* -static bool g_jitAvailable = false; -static int g_iosVersionMinor; -*/ +static bool g_jitAvailable = true; +//static int g_iosVersionMinor; + static int g_iosVersionMajor; static std::string version; @@ -169,7 +350,7 @@ bool System_GetPropertyBool(SystemProperty prop) { return false; #endif case SYSPROP_CAN_JIT: - return get_debugged(); + return g_jitAvailable; #ifndef HTTPS_NOT_AVAILABLE case SYSPROP_SUPPORTS_HTTPS: return true; @@ -309,9 +490,22 @@ int main(int argc, char *argv[]) g_iosVersionMajor = 14; } -/* - g_jitAvailable = get_debugged(); + if (jb_spawn_ptrace_child(argc, argv)) { + INFO_LOG(SYSTEM, "JIT: ptrace() child spawn trick\n"); + } else if (jb_has_jit_entitlement()) { + INFO_LOG(SYSTEM, "JIT: found entitlement\n"); + } else if (jb_has_cs_disabled()) { + INFO_LOG(SYSTEM, "JIT: CS_KILL disabled\n"); + } else if (jb_has_cs_execseg_allow_unsigned()) { + INFO_LOG(SYSTEM, "JIT: CS_EXECSEG_ALLOW_UNSIGNED set\n"); + } else if (jb_enable_ptrace_hack()) { + INFO_LOG(SYSTEM, "JIT: ptrace() hack supported\n"); + } else { + INFO_LOG(SYSTEM, "JIT: ptrace() hack failed\n"); + g_jitAvailable = false; + } +/* if (g_iosVersionMajor > 14 || (g_iosVersionMajor == 14 && g_iosVersionMinor >= 4)) { g_jitAvailable = false; } else {