diff options
-rw-r--r-- | Makefile | 10 | ||||
-rw-r--r-- | ios-bootstrap/posixspawn-hook.c | 241 | ||||
-rw-r--r-- | ios-bootstrap/safety-dance/Info.plist | bin | 1478 -> 2551 bytes | |||
-rw-r--r-- | ios-bootstrap/unrestrict-me.c | 32 | ||||
-rw-r--r-- | lib/darwin/inject.c | 8 | ||||
-rw-r--r-- | lib/darwin/mach-decls.h | 31 | ||||
-rw-r--r-- | lib/darwin/stop-other-threads.c | 2 | ||||
-rw-r--r-- | lib/darwin/thread-state.h | 24 | ||||
-rw-r--r-- | lib/darwin/unrestrict.c | 163 | ||||
-rw-r--r-- | lib/substitute-internal.h | 4 | ||||
-rw-r--r-- | lib/substitute.h | 6 |
11 files changed, 484 insertions, 37 deletions
@@ -71,6 +71,7 @@ LIB_OBJS := \ out/darwin/substrate-compat.o \ out/darwin/stop-other-threads.o \ out/darwin/execmem.o \ + out/darwin/unrestrict.o \ out/jump-dis.o \ out/transform-dis.o \ out/hook-functions.o \ @@ -161,10 +162,17 @@ out/safety-dance/SafetyDance.app/SafetyDance: $(SD_OBJS) Makefile ldid -S $@ out/safety-dance/SafetyDance.app/Info.plist: ios-bootstrap/safety-dance/Info.plist Makefile @mkdir -p $(dir $@) - plutil -convert binary1 -o $< $@ + plutil -convert binary1 -o $@ $< cp ios-bootstrap/safety-dance/white.png out/safety-dance/SafetyDance.app/Default.png cp ios-bootstrap/safety-dance/white.png out/safety-dance/SafetyDance.app/Default@2x.png safety-dance: out/safety-dance/SafetyDance.app/SafetyDance out/safety-dance/SafetyDance.app/Info.plist +all: safety-dance + +out/posixspawn-hook.dylib: ios-bootstrap/posixspawn-hook.c out/libsubstitute.dylib + $(CC) -dynamiclib -o $@ $< -Lout -lsubstitute +out/unrestrict-me: ios-bootstrap/unrestrict-me.c out/libsubstitute.dylib + $(CC) -o $@ $< -Lout -lsubstitute +all: out/posixspawn-hook.dylib out/unrestrict-me endif diff --git a/ios-bootstrap/posixspawn-hook.c b/ios-bootstrap/posixspawn-hook.c new file mode 100644 index 0000000..1f1bae1 --- /dev/null +++ b/ios-bootstrap/posixspawn-hook.c @@ -0,0 +1,241 @@ +#include "substitute.h" +#include "substitute-internal.h" +#include <mach/mach.h> +#include <mach-o/dyld.h> +#include <spawn.h> +#include <sys/wait.h> +#include <syslog.h> +#include <malloc/malloc.h> + +extern char ***_NSGetEnviron(void); + +static __typeof__(posix_spawn) *old_posix_spawn, *old_posix_spawnp, + hook_posix_spawn, hook_posix_spawnp; +static __typeof__(wait4) *old_wait4, hook_wait4; +static __typeof__(waitpid) *old_waitpid, hook_waitpid; + +static bool advance(char **strp, const char *template) { + size_t len = strlen(template); + if (!strncmp(*strp, template, len)) { + *strp += len; + return true; + } + return false; +} + +static int hook_posix_spawn_generic(__typeof__(posix_spawn) *old, + pid_t *restrict pid, const char *restrict path, + const posix_spawn_file_actions_t *file_actions, + const posix_spawnattr_t *restrict attrp, + char *const argv[restrict], + char *const envp[restrict]) { + char *new = NULL; + char **new_envp = NULL; + char *const *envp_to_use = envp; + char *const *my_envp = envp ? envp : *_NSGetEnviron(); + posix_spawnattr_t my_attr = NULL; + /* 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 + * directly to the original spawn), but I guess I'll just do the same for + * maximum safety... */ + bool safe_mode = false; + const char *orig_dyld_insert = ""; + static const char my_dylib[] = + "/Library/Substitute/generic-dyld-inserted.dylib"; + size_t env_count = 0; + for (char *const *ep = my_envp; *ep; ep++) { + env_count++; + char *env = *ep; + if (advance(&env, "_MSSafeMode=") || advance(&env, "_SubstituteSafeMode=")) { + if (!strcmp(env, "0") || !strcmp(env, "NO")) + continue; + else if (!strcmp(env, "1") || !strcmp(env, "YES")) + safe_mode = true; + else + goto skip; + } else if (advance(&env, "DYLD_INSERT_LIBRARIES=")) { + orig_dyld_insert = env; + } + } + new = malloc(sizeof("DYLD_INSERT_LIBRARIES=") - 1 + + sizeof(my_dylib) - 1 + + strlen(orig_dyld_insert) + 1); + char *newp_orig = stpcpy(new, "DYLD_INSERT_LIBRARIES="); + char *newp = newp_orig; + const char *p = orig_dyld_insert; + while (*p) { /* W.N.H. */ + const char *next = strchr(p, ':') ?: (p + strlen(p)); + /* append if it isn't a copy of ours */ + if (!(next - p == sizeof(my_dylib) - 1 && + memcmp(next, my_dylib, sizeof(my_dylib) - 1))) { + if (newp != newp_orig) + *newp++ = ':'; + memcpy(newp, p, next - p); + newp += next - p; + } + } + /* append ours if necessary */ + if (!safe_mode) { + if (newp != newp_orig) + *newp++ = ':'; + newp = stpcpy(newp, my_dylib); + } + /* no libraries? then just get rid of it */ + if (newp == newp_orig) { + free(new); + new = NULL; + } + new_envp = malloc(sizeof(char *) * (env_count + 2)); + envp_to_use = new_envp; + char **outp = new_envp; + for (size_t idx = 0; idx < env_count; idx++) { + char *env = envp[idx]; + /* remove *all* D_I_L, including duplicates */ + if (!advance(&env, "DYLD_INSERT_LIBRARIES=")) + *outp++ = env; + } + if (new) + *outp++ = new; + *outp++ = NULL; + + if (safe_mode) + goto skip; + + /* 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; + 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) { + const char *prog = "/Library/Substitute/unrestrict-me"; + char pid[32]; + sprintf(pid, "%ld", (long) getpid()); + const char *should_resume = was_suspended ? "0" : "1"; + const char *argv[] = {prog, pid, should_resume, NULL}; + pid_t prog_pid; + if (old_posix_spawn(&prog_pid, prog, NULL, NULL, (char **) argv, envp)) { + syslog(LOG_EMERG, + "posixspawn-hook: couldn't start unrestrict-me - oh well..."); + goto skip; + } + } + + int ret = old(pid, path, file_actions, &my_attr, argv, envp_to_use); + if (ret) + goto cleanup; + /* Since it returned, obviously it was not SETEXEC, so we need to + * unrestrict ourself. */ + int sret = substitute_ios_unrestrict(*pid, !was_suspended); + if (sret) { + syslog(LOG_EMERG, "posixspawn-hook: substitute_ios_unrestrict => %d", sret); + } + goto cleanup; +crap: + syslog(LOG_EMERG, "posixspawn-hook: weird error - OOM? skipping our stuff"); +skip: + ret = old(pid, path, file_actions, attrp, argv, envp); +cleanup: + free(new_envp); + free(new); + free(my_attr); + return ret; +} + +static void after_wait_generic(pid_t pid, int stat) { + (void) pid; + (void) stat; + +} + +int hook_posix_spawn(pid_t *restrict pid, const char *restrict path, + const posix_spawn_file_actions_t *file_actions, + const posix_spawnattr_t *restrict attrp, + char *const argv[restrict], char *const envp[restrict]) { + return hook_posix_spawn_generic(old_posix_spawn, pid, path, file_actions, + attrp, argv, envp); +} + +int hook_posix_spawnp(pid_t *restrict pid, const char *restrict path, + const posix_spawn_file_actions_t *file_actions, + const posix_spawnattr_t *restrict attrp, + char *const argv[restrict], char *const envp[restrict]) { + return hook_posix_spawn_generic(old_posix_spawnp, pid, path, file_actions, + attrp, argv, envp); +} + +pid_t hook_wait4(pid_t pid, int *stat_loc, int options, struct rusage *rusage) { + pid_t ret = old_wait4(pid, stat_loc, options, rusage); + after_wait_generic(ret, *stat_loc); + return ret; +} + +pid_t hook_waitpid(pid_t pid, int *stat_loc, int options) { + pid_t ret = old_waitpid(pid, stat_loc, options); + after_wait_generic(ret, *stat_loc); + return ret; +} + +void substitute_init(struct shuttle *shuttle, UNUSED size_t nshuttle) { + /* 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 + * until the thread it remotely creates exits, and that thread does + * pthread_join on the 'real' pthread it creates to do the dlopen (unlike + * the equivalent in Substitute - the difference is to decrease dependence + * on pthread internals). substitute_dlopen_in_pid does not, but that's + * what the notify port is for. Meanwhile, the jailbreak I have installed + * properly runs rc.d sequentially, so the injection tools won't do their + * thing at the same time. But just in case any of that doesn't hold up... + */ + + struct substitute_image *im = substitute_open_image(_dyld_get_image_name(0)); + if (!im) { + syslog(LOG_EMERG, "posixspawn-hook: substitute_open_image failed"); + goto end; + } + + static const struct substitute_import_hook hooks[] = { + {"_posix_spawn", hook_posix_spawn, &old_posix_spawn}, + {"_posix_spawnp", hook_posix_spawnp, &old_posix_spawnp}, + {"_waitpid", hook_waitpid, &old_waitpid}, + {"_wait4", hook_wait4, &old_wait4}, + }; + + int err = substitute_interpose_imports(im, hooks, sizeof(hooks)/sizeof(*hooks), 0); + if (err) { + syslog(LOG_EMERG, "posixspawn-hook: substitute_interpose_imports failed: %s", + substitute_strerror(err)); + goto end; + } + +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_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; + if (mach_msg_send(&done_hdr)) /* MOVE deallocates port */ + syslog(LOG_EMERG, "posixspawn-hook: mach_msg_send failed"); +} diff --git a/ios-bootstrap/safety-dance/Info.plist b/ios-bootstrap/safety-dance/Info.plist Binary files differindex fd6a1b3..f2fa520 100644 --- a/ios-bootstrap/safety-dance/Info.plist +++ b/ios-bootstrap/safety-dance/Info.plist diff --git a/ios-bootstrap/unrestrict-me.c b/ios-bootstrap/unrestrict-me.c new file mode 100644 index 0000000..718e9f8 --- /dev/null +++ b/ios-bootstrap/unrestrict-me.c @@ -0,0 +1,32 @@ +#include "substitute.h" +#include "substitute-internal.h" +#include <stdlib.h> +#include <syslog.h> + +int main(int argc, char **argv) { + if (argc != 3) { + syslog(LOG_EMERG, "unrestrict-me: wrong number of args"); + return 1; + } + const char *pids = argv[1]; + char *end; + long pid = strtol(pids, &end, 10); + if (!pids[0] || *end) { + syslog(LOG_EMERG, "unrestrict-me: pid not an integer"); + return 1; + } + + const char *should_resume = argv[2]; + if (strcmp(should_resume, "0") && strcmp(should_resume, "1")) { + syslog(LOG_EMERG, "unrestrict-me: should_resume not 0 or 1"); + return 1; + } + + int sret = substitute_ios_unrestrict((pid_t) pid, should_resume[0] == '1'); + if (sret) { + syslog(LOG_EMERG, "unrestrict-me: substitute_ios_unrestrict => %d", sret); + return 1; + } + + return 0; +} diff --git a/lib/darwin/inject.c b/lib/darwin/inject.c index a74b057..76ecbcb 100644 --- a/lib/darwin/inject.c +++ b/lib/darwin/inject.c @@ -2,7 +2,7 @@ #include "substitute.h" #include "substitute-internal.h" #include "darwin/read.h" -#include "darwin/thread-state.h" +#include "darwin/mach-decls.h" #include <mach/mach.h> #include <mach-o/dyld_images.h> #include <dlfcn.h> @@ -13,12 +13,6 @@ #include <stdio.h> #include <stdbool.h> -kern_return_t mach_vm_read_overwrite(vm_map_t, mach_vm_address_t, mach_vm_size_t, mach_vm_address_t, mach_vm_size_t *); -kern_return_t mach_vm_remap(vm_map_t, mach_vm_address_t *, mach_vm_size_t, mach_vm_offset_t, int, vm_map_t, mach_vm_address_t, boolean_t, vm_prot_t *, vm_prot_t *, vm_inherit_t); -kern_return_t mach_vm_write(vm_map_t, mach_vm_address_t, vm_offset_t, mach_msg_type_number_t); -kern_return_t mach_vm_allocate(vm_map_t, mach_vm_address_t *, mach_vm_size_t, int); -kern_return_t mach_vm_deallocate(vm_map_t, mach_vm_address_t, mach_vm_size_t); - extern const struct dyld_all_image_infos *_dyld_get_all_image_infos(); #define DEFINE_STRUCTS diff --git a/lib/darwin/mach-decls.h b/lib/darwin/mach-decls.h new file mode 100644 index 0000000..29ea908 --- /dev/null +++ b/lib/darwin/mach-decls.h @@ -0,0 +1,31 @@ +#pragma once +#include <stdint.h> + +struct _x86_thread_state_32 { + uint32_t eax, ebx, ecx, edx, edi, esi, ebp, esp; + uint32_t ss, eflags, eip, cs, ds, es, fs, gs; +}; +#define _x86_thread_state_32_flavor 1 +struct _x86_thread_state_64 { + uint64_t rax, rbx, rcx, rdx, rdi, rsi, rbp, rsp; + uint64_t r8, r9, r10, r11, r12, r13, r14, r15; + uint64_t rip, rflags, cs, fs, gs; +}; +#define _x86_thread_state_64_flavor 4 +struct _arm_thread_state_32 { + uint32_t r[13], sp, lr, pc, cpsr; +}; +#define _arm_thread_state_32_flavor 9 +struct _arm_thread_state_64 { + uint64_t x[29], fp, lr, sp, pc; + uint32_t cpsr, pad; +}; +#define _arm_thread_state_64_flavor 6 + +kern_return_t mach_vm_read_overwrite(vm_map_t, mach_vm_address_t, mach_vm_size_t, mach_vm_address_t, mach_vm_size_t *); +kern_return_t mach_vm_remap(vm_map_t, mach_vm_address_t *, mach_vm_size_t, mach_vm_offset_t, int, vm_map_t, mach_vm_address_t, boolean_t, vm_prot_t *, vm_prot_t *, vm_inherit_t); +kern_return_t mach_vm_write(vm_map_t, mach_vm_address_t, vm_offset_t, mach_msg_type_number_t); +kern_return_t mach_vm_allocate(vm_map_t, mach_vm_address_t *, mach_vm_size_t, int); +kern_return_t mach_vm_deallocate(vm_map_t, mach_vm_address_t, mach_vm_size_t); +kern_return_t mach_vm_region(vm_map_t, mach_vm_address_t *, mach_vm_size_t *, vm_region_flavor_t, vm_region_info_t, mach_msg_type_number_t *, mach_port_t *); + diff --git a/lib/darwin/stop-other-threads.c b/lib/darwin/stop-other-threads.c index 1975b47..b35c04c 100644 --- a/lib/darwin/stop-other-threads.c +++ b/lib/darwin/stop-other-threads.c @@ -1,6 +1,6 @@ #include "substitute.h" #include "substitute-internal.h" -#include "darwin/thread-state.h" +#include "darwin/mach-decls.h" #include <pthread.h> #include <mach/mach.h> #include <CoreFoundation/CoreFoundation.h> diff --git a/lib/darwin/thread-state.h b/lib/darwin/thread-state.h deleted file mode 100644 index 0bae7be..0000000 --- a/lib/darwin/thread-state.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#include <stdint.h> - -struct _x86_thread_state_32 { - uint32_t eax, ebx, ecx, edx, edi, esi, ebp, esp; - uint32_t ss, eflags, eip, cs, ds, es, fs, gs; -}; -#define _x86_thread_state_32_flavor 1 -struct _x86_thread_state_64 { - uint64_t rax, rbx, rcx, rdx, rdi, rsi, rbp, rsp; - uint64_t r8, r9, r10, r11, r12, r13, r14, r15; - uint64_t rip, rflags, cs, fs, gs; -}; -#define _x86_thread_state_64_flavor 4 -struct _arm_thread_state_32 { - uint32_t r[13], sp, lr, pc, cpsr; -}; -#define _arm_thread_state_32_flavor 9 -struct _arm_thread_state_64 { - uint64_t x[29], fp, lr, sp, pc; - uint32_t cpsr, pad; -}; -#define _arm_thread_state_64_flavor 6 - diff --git a/lib/darwin/unrestrict.c b/lib/darwin/unrestrict.c new file mode 100644 index 0000000..4fd90da --- /dev/null +++ b/lib/darwin/unrestrict.c @@ -0,0 +1,163 @@ +#include "substitute.h" +#include "substitute-internal.h" +#include "darwin/mach-decls.h" +#include <unistd.h> +#include <mach/vm_region.h> + +static int unrestrict_macho_header(void *header, size_t size, bool *did_modify_p) { + *did_modify_p = false; + struct mach_header *mh = header; + if (mh->magic != MH_MAGIC && mh->magic != MH_MAGIC_64) + return SUBSTITUTE_ERR_MISC; + size_t off = mh->magic == MH_MAGIC_64 ? sizeof(struct mach_header_64) + : sizeof(struct mach_header); + for (uint32_t i = 0; i < mh->ncmds; i++) { + if (off > size || size - off < sizeof(struct load_command)) + break; /* whatever */ + struct load_command *lc = header + off; + if (lc->cmdsize > size - off) + break; + #define CASES(code...) \ + if (lc->cmd == LC_SEGMENT) { \ + typedef struct segment_command segment_command_y; \ + typedef struct section section_y; \ + code \ + } else if (lc->cmd == LC_SEGMENT_64) { \ + typedef struct segment_command_64 segment_command_y; \ + typedef struct section_64 section_y; \ + code \ + } + CASES( + segment_command_y *sc = (void *) lc; + if (lc->cmdsize < sizeof(*sc) || + sc->nsects > (lc->cmdsize - sizeof(*sc)) / sizeof(struct section)) + return SUBSTITUTE_ERR_MISC; + if (!strncmp(sc->segname, "__RESTRICT", 16)) { + section_y *sect = (void *) (sc + 1); + 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; + } + } + } + ) + #undef CASES + + if (off + lc->cmdsize < off) + return SUBSTITUTE_ERR_MISC; + off += lc->cmdsize; + } + return SUBSTITUTE_OK; +} + +EXPORT +int substitute_ios_unrestrict(pid_t pid, bool should_resume) { + mach_port_t task; + kern_return_t kr = task_for_pid(mach_task_self(), pid, &task); + if (kr) + return SUBSTITUTE_ERR_TASK_FOR_PID; + + int ret; + vm_address_t header_addr = 0; + + 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. */ + struct task_dyld_info tdi; + mach_msg_type_number_t cnt = TASK_DYLD_INFO_COUNT; + + kern_return_t kr = task_info(task, TASK_DYLD_INFO, (void *) &tdi, &cnt); + if (kr || cnt != TASK_DYLD_INFO_COUNT) { + ret = SUBSTITUTE_ERR_MISC; + goto fail; + } + if (tdi.all_image_info_size == 0) + break; + if (retries++ == 20) { + ret = SUBSTITUTE_ERR_MISC; + goto fail; + } + wait_us *= 2; + if (wait_us > 100000) + wait_us = 100000; + while (usleep(wait_us)) + ; + } + + /* alrighty then, let's look at the damage. find the first readable + * segment */ + mach_vm_address_t segm_addr = 0; + mach_vm_size_t segm_size = 0; + vm_region_basic_info_data_64_t info; + mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object_name; + while (1) { + kr = mach_vm_region(task, &segm_addr, &segm_size, VM_REGION_BASIC_INFO_64, + (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 */ + goto setback; + } else if (kr) { + ret = SUBSTITUTE_ERR_VM; + goto fail; + } + if (info.protection) + break; + + segm_addr++; + } + + size_t toread = 0x4000; + if (segm_size < toread) + toread = segm_size; + if ((kr = vm_allocate(mach_task_self(), &header_addr, toread, + VM_FLAGS_ANYWHERE))) { + ret = SUBSTITUTE_ERR_MISC; + goto fail; + } + mach_vm_size_t actual = toread; + kr = mach_vm_read_overwrite(task, segm_addr, toread, header_addr, + &actual); + if (kr || actual != toread) { + ret = SUBSTITUTE_ERR_MISC; + goto fail; + } + + bool did_modify; + if ((ret = unrestrict_macho_header((void *) header_addr, toread, + &did_modify))) + goto fail; + + if (did_modify) { + if ((kr = vm_protect(mach_task_self(), header_addr, toread, + FALSE, info.protection))) { + ret = SUBSTITUTE_ERR_VM; + goto fail; + } + vm_prot_t cur, max; + if ((kr = mach_vm_remap(task, &segm_addr, toread, 0, + VM_FLAGS_OVERWRITE, + mach_task_self(), header_addr, FALSE, + &cur, &max, info.inheritance))) { + ret = SUBSTITUTE_ERR_VM; + goto fail; + } + } + + ret = SUBSTITUTE_OK; +fail: + if (should_resume) { + if ((kr = task_resume(task))) + ret = SUBSTITUTE_ERR_MISC; + } + mach_port_deallocate(mach_task_self(), task); + if (header_addr) + vm_deallocate(mach_task_self(), header_addr, toread); + return ret; +} + + diff --git a/lib/substitute-internal.h b/lib/substitute-internal.h index 3fd35bc..b378989 100644 --- a/lib/substitute-internal.h +++ b/lib/substitute-internal.h @@ -63,7 +63,7 @@ enum { * can happen are really complicated and dumb, but generally one solution * is to be root */ SUBSTITUTE_ERR_TASK_FOR_PID = 1000, - + SUBSTITUTE_ERR_TIMEOUT = 1000, SUBSTITUTE_ERR_MISC, }; @@ -85,4 +85,6 @@ struct shuttle { int substitute_dlopen_in_pid(int pid, const char *filename, int options, const struct shuttle *shuttle, size_t nshuttle, char **error); + +int substitute_ios_unrestrict(pid_t pid, bool should_resume); #endif diff --git a/lib/substitute.h b/lib/substitute.h index bdd63e0..8764bcf 100644 --- a/lib/substitute.h +++ b/lib/substitute.h @@ -172,9 +172,9 @@ struct substitute_import_hook { * This can be used to 'hook' functions or even exported variables. Compared * to substitute_hook_functions, it has the following advantages: * - * - Because it does not require the ability to patch executable code; - * accordingly, it can (from a technical rather than policy perspective) be - * used in sandboxed environments like iOS or PaX MPROTECT. + * - It does not require the ability to patch executable code; accordingly, it + * can (from a technical rather than policy perspective) be used in sandboxed + * environments like iOS or PaX MPROTECT. * - On platforms without RELRO or similar, it is thread safe, as the patches * are done using atomic instructions. * - It does not require architecture specific code. |