简单复刻 Coruna macho injector,尝试在越狱环境下不通过 dlopen 把 Mach-O 映射进目标进程。
测试环境:iPhone X-arm64-16.7.16 / Dopamine,目标 SpringBoard。

流程

task_for_pid
    │
    ▼
本地读 Mach-O(segment / _last / LC_DYLD_CHAINED_FIXUPS)
    │
    ▼
mach_vm_allocate → 拷段 → rebase/bind → mach_vm_write
    │
    ▼
mach_vm_protect(__TEXT=RX,数据段=RW)
    │
    ▼
bootstrap → pthread_create_from_mach_thread(_last)

和远程 dlopen 的差别:装载与 fixup 由 injector 完成,dyld 不参与。

Target

pid / 进程名 → task_for_pid → task port

获取到目标进程的 task 后,对目标地址空间可:mach_vm_allocate / mach_vm_read / mach_vm_write / mach_vm_protect / thread_create

权限通常需要 task_for_pid-allow;目标是 SpringBoard 这类 platform 进程还要 com.apple.system-task-ports
远程 thread_set_state 另需 com.apple.private.thread-set-state,否则 injector 会 EXC_GUARD

bind _dlsym、起 pthread 都要目标进程里的真实 VA,不能用 injector 自己的地址。做法是读目标 dyld 镜像表,再扫符号表:

task_info(TASK_DYLD_INFO) → all_image_info_addr
    │
    ▼
dyld_all_image_infos.infoArray[]
    │  每项:imageLoadAddress + imageFilePath
    ▼
find_remote_image(needle)     // 路径 substring,如 "/libdyld.dylib"
    │
    ▼
remote_dlsym(header, "_dlsym")
    │  slide = header - __TEXT.vmaddr
    │  读 LC_SYMTAB + __LINKEDIT → nlist_64
    ▼
runtime = n_value + slide

Payload

只留一个 undefined(_dlsym),其余运行时解析:

构建:-Wl,-fixup_chains-fno-builtin、只导出 _last

injector 在目标 libdyld 里解析 _dlsym,填进 bind 槽即可。

Chained Fixups

现代 Mach-O 用 LC_DYLD_CHAINED_FIXUPS,不再走老式 rebase/bind opcode 流。磁盘上某些 8 字节槽位是编码,加载时要改成运行时指针。

两类:

rebase bind
含义 本镜像内指针 外部符号
编码 bind=0,带 target / next bind=1,带 ordinal / addend / next
结果 按 slide(或 runtimeOffset)算出地址 ordinal → imports → 符号真实地址
[dyld_chained_fixups_header]
    starts_offset  → 每 segment 的 page_start[](链表入口)
    imports_offset → import 数组(ordinal → 名字)
    symbols_offset → 符号名字符串池

seg_info_offset 下标必须与 LC_SEGMENT_64 顺序一致(含 __PAGEZERO)。

preferred_vmin = 非 __PAGEZERO 各 segment 的最小 vmaddr(链接时地址空间起点,dylib 常为 0)
remote_base    = 目标进程里 mach_vm_allocate(VM_FLAGS_ANYWHERE) 返回的实际基址

slide          = remote_base - preferred_vmin

preferred_* 来自文件;remote_base 由内核在目标 VA 里现挑,每次可能不同。

同一 64bit 按 bind 位解释成 rebase 或 bind 结构:

rebase:
  DYLD_CHAINED_PTR_64:        runtime = slide + target          // target = preferred vmaddr
  PTR_64_OFFSET: runtime = remote_base + target    // target = 相对镜像基址偏移

bind:
  ordinal → imports[ordinal].name
         → 目标进程符号地址 + addend

addend:链接器在绑定记录(Binding Record)里携带的“额外偏移”被称为 Addend;

本 demo 的 bind 只接受 _dlsym:在目标 libdyld 里解析一次,写入所有 bind 槽。

同一页内多个待改指针用 next 串起来,不是扫全页:

for each segment:
  for each page:
    next = page_start[page]       // NONE 则跳过
    while true:
      slot = page_va + next
      decode raw → rebase 或 bind → 写回普通 64 位指针
      if next_field == 0: break
      next += next_field * stride // PTR_64: stride = 4

走完后 buffer 里已是「装载后的镜像」,再 mach_vm_write 进目标。

测试日志 rebase=0 bind=1 正常:payload 几乎 PIC,只有一条 _dlsym bind。

远程 pthread

直接 thread_create_last 缺 TLS。用目标里的 _pthread_create_from_mach_thread

1. 分配 code 页 + stack
2. 写 bootstrap:x0=slot, x1=0, x2=_last, x3=arg, x8=pthread_cfm; blr x8
3. thread_create → thread_set_state(pc=boot) → thread_resume
4. bootstrap 创建 pthread,start_routine = _last

pthread_t *slot 必须可写,放在 RW 栈上,不能和 RX bootstrap 同页。

Reference