溺的文档
ReZygisk · 第 2 篇 / 共 4 篇

ReZygisk 注入机制:monitor 与 ptracer

2026-06-30 · 阅读 2

本文是 ReZygisk 源码解读系列第 2 篇。全系列:

  • 00 总览与整体架构
  • 01 守护进程 zygiskd 与 Root 适配
  • 02 注入机制:monitor 监控与 ptracer 注入(本篇)
  • 03 注入体 injector 与反检测基石
  • 04 外围:IPC 协议、模块脚本、WebUI 与构建

代码引用格式 文件:行号。本篇代码位于 loader/src/ptracer/


一、本篇定位

Zygisk 的本质是:在 Android 的孵化器进程 zygote 里插入自己的代码,这样之后由 zygote fork 出来的每一个 APP / system_server 进程都天然带上模块逻辑。

问题是——怎么把代码插进 zygote?ReZygisk 选择的是 ptrace 路线(与基于 LD_PRELOAD/riru 的方案不同):

  1. 一个常驻的 monitor 进程用 ptrace 追踪 init(pid 1),坐等 init fork+exec 出 zygote。
  2. 一旦发现 zygote 启动,monitor 立刻 fork+exec 出一个 ptracerzygisk-ptrace trace <pid>)去 attach 那个 zygote。
  3. ptracer 在 zygote 里远程完成"分配内存 → 加载 libzygisk.so → 调用其入口",全程绕过系统 linker(用自定义加载器 CSOLoader),以规避基于 linker 的检测。

本篇就讲这两个角色。对应文件:

文件 角色
ptracer/main.c 二进制 zygisk-ptrace 的入口,分发 monitor/trace/ctl/info 子命令
ptracer/monitor.c monitor 主体:追踪 init、捕获 zygote、派发 ptracer、状态上报
ptracer/monitor.h monitor 与 ptracer 共享的命令枚举与声明
ptracer/ptracer.c ptracer 主体:trace_zygote() 远程注入
ptracer/remote_csoloader.c/h 远程 ELF 加载器(CSOLoader 的远程化)
ptracer/utils.c/h 远程过程调用原语(读写内存/寄存器、远程 call/syscall、跨架构抽象)

二、整体注入链路

先看一次完整注入从头到尾发生了什么:

sequenceDiagram autonumber participant Init as init (pid 1) participant Mon as monitor participant PT as ptracer (zygisk-ptrace trace) participant Zy as zygote (app_process) participant D as zygiskd participant Lib as libzygisk.so Mon->>Init: PTRACE_SEIZE(1, TRACEFORK) Init->>Zy: fork + execve(app_process64) Init-->>Mon: PTRACE_EVENT_FORK / EXEC 通知 Mon->>Mon: get_program(pid) == /system/bin/app_process64? Mon->>D: ensure_daemon_created() 拉起 zygiskd64 Mon->>Zy: 让 zygote 停住 (SIGSTOP) Mon->>PT: fork_dont_care + execl(zygisk-ptrace trace pid) PT->>Zy: PTRACE_SEIZE + 等待停顿 PT->>Zy: 破坏 AT_ENTRY → CONT → 等 SIGSEGV (停在 linker 初始化后) PT->>Zy: 远程 mmap + CSOLoader 加载 libzygisk.so PT->>Zy: remote_call entry(base, size, tango=0) Zy->>Lib: entry() 安装 hook Lib->>D: ZygoteInjected 上报 D-->>Mon: ZYGOTE64_INJECTED (回流) PT->>Zy: 恢复寄存器 + PTRACE_DETACH Zy->>Zy: 从原入口继续,照常 fork APP

注意第 ⑱ 步:"注入完成"不是 ptracer 通知的,而是注入体 libzygisk.so 在 zygote 内通过 zygiskd 回流给 monitor 的。ptracer 的职责到"恢复寄存器+detach"就结束了。


三、monitor:追踪 init,捕获 zygote

3.1 启动:claim init tracer

init_monitormonitor.c:992-1051)是 zygisk-ptrace monitor 的落点。启动序列:

void init_monitor() {
  prepare_environment();          // 读取 module.prop 模板,准备状态写回
  claim_init_tracer();            // PTRACE_SEIZE init
  monitor_events_init();          // epoll_create
  rezygiskd_listener_init();      // 创建 init_monitor datagram socket
  monitor_events_register_event(rezygiskd_listener_callback, monitor_sock_fd, EPOLLIN|EPOLLET);
  sigchld_listener_init();        // signalfd 捕获 SIGCHLD
  monitor_events_register_event(sigchld_listener_callback, sigchld_signal_fd, EPOLLIN|EPOLLET);
  monitor_events_loop();          // 主循环
  ...
}

claim_init_tracermonitor.c:544-562)是关键的第一步:

static bool claim_init_tracer() {
  if (ptrace(PTRACE_SEIZE, 1, 0, PTRACE_O_TRACEFORK) == -1) {
    if (errno == EPERM) {
      // 已有另一个进程在追踪 init(例如重复启动了 ReZygisk)
      update_status("❌ Multiple Zygisks functioning");
    } else PLOGE("failed to seize init");
    return false;
  }
  return true;
}
  • PTRACE_SEIZE 而非 PTRACE_ATTACH:SEIZE 不会向被追踪者发停止信号,对 init 这种关键进程更安全。
  • PTRACE_O_TRACEFORK:让 init 每次 fork 都通知 monitor——zygote 正是 init 的子进程。
  • EPERM 处理:一个进程只能被一个 tracer 追踪,若已被占用说明重复运行,直接退出,避免冲突。

3.2 双事件源的 epoll 循环

monitor 用 epoll 同时监听两个事件源(monitor.c:117-146):

flowchart TB L["monitor_events_loop<br/>(epoll_wait)"] --> E1["init_monitor socket<br/>rezygiskd_listener_callback"] L --> E2["signalfd (SIGCHLD)<br/>sigchld_listener_callback"] E1 --> C1["控制命令 START/STOP/EXIT<br/>+ daemon 状态上报"] E2 --> C2["init/子进程的 ptrace 停顿事件<br/>→ 捕获 zygote → 派发 ptracer"]
  • signalfdmonitor.c:564-585):把 SIGCHLD 转成可 epoll 的 fd。被 ptrace 追踪的进程发生停顿时,tracer 会收到 SIGCHLD,于是这里统一处理所有 ptrace 事件。
  • init_monitor datagram socketmonitor.c:150-173):路径 <rezygiskd_path>/init_monitor,既接收用户控制命令(ctl),也接收 zygiskd 的状态上报。

3.3 controller socket:命令与状态上报

rezygiskd_listener_callbackmonitor.c:175-396)处理 init_monitor socket 上收到的字节命令。命令枚举见 monitor.h

命令 来源 含义
START 1 用户 ctl start 恢复/开始追踪 init
STOP 2 用户 ctl stop 停止追踪(PTRACE_INTERRUPT init)
EXIT 3 用户 ctl exit 退出 monitor
ZYGOTE64_INJECTED 4 zygiskd 64 位 zygote 注入完成
ZYGOTE32_INJECTED 5 zygiskd 32 位 zygote 注入完成
DAEMON64_SET_INFO 6 zygiskd 上报 root 实现 + 模块清单(64)
DAEMON32_SET_INFO 7 zygiskd 同上(32)
DAEMON64_SET_ERROR_INFO 8 zygiskd 上报错误信息(64)
DAEMON32_SET_ERROR_INFO 9 zygiskd 同上(32)

这正好与 01 篇里 constants.hLP_SELECT 编码对应:ZYGOTE_INJECTED = LP_SELECT(5,4)(32 位发 5、64 位发 4),DAEMON_SET_INFO = LP_SELECT(7,6)DAEMON_SET_ERROR_INFO = LP_SELECT(9,8)。zygiskd 用 unix_datagram_sendto 发,monitor 在这里收。

SET_INFO 分支(monitor.c:240-351)会读取 root 实现名、模块数、各模块名,存进 environment_information64/32,随后 update_status 把它们写进 state.json(供 WebUI 读取,见 04 篇)。

3.4 捕获 zygote:PTRACE_EVENT_EXEC 匹配程序名

真正的"捕获"在 sigchld_listener_callbackmonitor.c:587-804)。它 waitpid 收割所有 ptrace 停顿,对 init 的子进程做这样的处理:

flowchart TB W["waitpid 收到停顿"] --> P{"pid == 1 (init)?"} P -->|是| FORK["PTRACE_EVENT_FORK<br/>记录新 fork 的子进程"] P -->|否| NEW{"新出现的子进程?"} NEW -->|是| OPT["PTRACE_SETOPTIONS TRACEEXEC<br/>PTRACE_CONT"] NEW -->|否| EXEC{"PTRACE_EVENT_EXEC?"} EXEC -->|是| GP["get_program(pid)"] GP --> M{"program 匹配?"} M -->|"/system/bin/app_process64"| I64["PRE_INJECT(64)"] M -->|"/system/bin/app_process32"| I32["PRE_INJECT(32)"] M -->|"/system_ext/bin/tango_translator"| IT["PRE_INJECT_TANGO"] M -->|其它| SKIP["放行"] I64 & I32 & IT --> HO["handoff: fork+exec ptracer"]

捕获逻辑要点:

  • init fork 出子进程时(PTRACE_EVENT_FORK),monitor 把子进程纳入追踪并设 PTRACE_O_TRACEEXECmonitor.c:687-692),这样子进程 execve 时会再次停下。
  • 子进程 execve 触发 PTRACE_EVENT_EXEC,monitor 用 get_program(pid)(读 /proc/<pid>/exe)拿到它 exec 的程序路径(monitor.c:694-702)。
  • 三个匹配目标(monitor.c:481-535):
    • /system/bin/app_process64 → 64 位 zygote
    • /system/bin/app_process32 → 32 位 zygote
    • /system_ext/bin/tango_translator → Tango(AArch64 上运行 ARM32 的翻译器,需特殊注入)

3.5 PRE_INJECT:拉起 daemon + 防过度重启

PRE_INJECT 是个宏(monitor.c:485-509),在确认要注入前做两件事:

#define PRE_INJECT(abi, is_64) \
  if (strcmp(program, APP_PROCESS_##abi) == 0) { \
    tracer = "./bin/zygisk-ptrace" #abi; \
    if (should_stop_inject##abi()) {            /* 防过度重启 */ \
      tracing_state = STOPPING; monitor_stop_reason = "Zygote crashed"; \
      ptrace(PTRACE_INTERRUPT, 1, 0, 0); break; \
    } \
    if (!ensure_daemon_created(is_64)) {         /* 确保 zygiskd 在跑 */ \
      tracing_state = STOPPING; monitor_stop_reason = "ReZygiskd not running"; \
      ptrace(PTRACE_INTERRUPT, 1, 0, 0); break; \
    } \
  }
  • 防过度重启monitor.c:405-423CREATE_ZYGOTE_START_COUNTER 宏):用 CLOCK_MONOTONIC 记录上次 zygote 启动时刻,若 30 秒内连续启动达到 MAX_RETRY_COUNT(5)次,判定 zygote 在崩溃循环,停止注入——否则会陷入"注入→崩溃→重启→再注入"的死循环把设备拖垮。
  • ensure_daemon_createdmonitor.c:428-459):fork+execl ./bin/zygiskd32/64,把对应位宽的守护进程拉起来(这就是 01 篇里 zygiskd 的来源)。

3.6 handoff:把注入交给独立的 ptracer 进程

确认要注入后(monitor.c:707-779),monitor 并不亲自注入,而是:

// 先让目标停稳(非 tango 路径)
kill(pid, SIGSTOP);
ptrace(PTRACE_CONT, pid, 0, 0);
waitpid(pid, &sigchld_status, __WALL);
ptrace(PTRACE_DETACH, pid, 0, SIGSTOP);   // monitor 自己 detach,让 ptracer 来接管

// fork 出一个"不用 reap"的子进程去 exec ptracer
int p = fork_dont_care();
if (p == 0) {
  execl(tracer, basename(tracer), "trace", pid_str, [--restart] [--tango], NULL);
  ...
}

为什么要交给独立进程?因为一个进程只能被一个 tracer 追踪。monitor 要继续追踪 init,就不能同时追踪 zygote,于是 monitor 先 detach,再 fork 出专门的 ptracer 去 SEIZE zygote。fork_dont_care()utils.c:343-357,双重 fork)让 ptracer 成为 init 的孩子,monitor 无需 waitpid 它。

--restart / --tango 的传递(monitor.c:752-766):

  • --tango:目标是 tango_translator 时附加。
  • --restart:仅在"非首次"启动时附加(count_zygote > 1),用于通知 zygiskd 重置 companion(zygote 重启了,旧 companion 连接失效)。

3.7 状态机与状态上报

monitor 维护一个 4 态状态机(monitor.c:38-45):TRACING / STOPPING / STOPPED / EXITINGupdate_statusmonitor.c:838-961)把当前状态写到两个地方:

  1. module.prop 的 description:用 emoji 拼出形如 [Monitor: ✅, ReZygisk 64-bit: ✅, ReZygisk 32-bit: ✅] 的状态串(monitor.c:818-836WRITE_STATUS_ABI 宏),这就是用户在 root 管理器模块列表里看到的描述。
  2. /data/adb/rezygisk/state.json:结构化状态(root 实现、monitor 状态、daemon 32/64 运行情况、模块清单、zygote 注入标志),供 WebUI 读取(monitor.c:879-956)。

3.8 ctl 命令

zygisk-ptrace ctl <start|stop|exit>main.c:38-58)通过 send_control_commandmonitor.c:1053-1072)向 init_monitor socket 发一个命令字节,由 3.3 节的 callback 处理。这给了用户运行时开关注入的能力(无需重启)。


四、ptracer:trace_zygote 远程注入

zygisk-ptrace trace <pid> [--restart] [--tango]main.c:15-37)最终调用 trace_zygote(pid, is_tango)ptracer.c:453)。

一个时序细节(main.c:24-35):非 tango 的重启会rezygiskd_zygote_restart() 再 trace;tango 因注入窗口极窄,必须先尽快 trace,所以把 restart 通知推迟到 trace 成功之后

下面以标准路径(非 tango,对应 inject_on_mainptracer.c:232-442)讲解,这是理解 ReZygisk 注入的核心。

4.1 SEIZE 与内核版本判定

trace_zygote 先按内核版本决定 SEIZE 选项(ptracer.c:463-500):内核 ≥ 3.8 才支持带选项的 SEIZE。标准路径用 PTRACE_O_EXITKILL | PTRACE_O_TRACESECCOMP,然后等待目标进入第一个停顿(期望 SIGSTOP + PTRACE_EVENT_STOP,即 monitor 交接前让它停住的那个点)。

4.2 精确停顿:破坏 AT_ENTRY,等一个 SIGSEGV

这是整个注入最巧妙的技巧。注入需要满足两个矛盾的条件:

  • linker 必须已经初始化完成(否则远程没法做符号解析、内存分配等);
  • 但 zygote 的业务逻辑还没开始跑(否则注入晚了,错过 fork)。

ReZygisk 的解法(ptracer.c:232-353):

flowchart TB A["读 zygote 栈顶<br/>KernelArgumentBlock"] --> B["遍历 auxv 找 AT_ENTRY<br/>记下入口地址 entry_addr<br/>及其在内存中的位置"] B --> C["把 AT_ENTRY 改写成<br/>一个无效地址 break_addr"] C --> D["PTRACE_CONT 放行"] D --> E["zygote 跑完 linker 初始化<br/>跳向(被破坏的)入口"] E --> F["触发 SIGSEGV<br/>停在 break_addr"] F --> G["校验 REG_IP == break_addr<br/>✓ 确认停在伪造入口"] G --> H["此刻: linker 已就绪<br/>业务未跑 → 可以注入了"] H --> I["把真实 entry_addr 写回 auxv<br/>备份寄存器"]

核心代码(ptracer.c:329-330):

// 把入口地址改成一个必定 fault 的无效地址(保留最低位以兼容 ARM thumb 判定)
uintptr_t break_addr = (uintptr_t)((intptr_t)(-0x0F & ~1) | (intptr_t)((uintptr_t)entry_addr & 1));
write_proc(pid, (uintptr_t)addr_of_entry_addr, &break_addr, sizeof(break_addr));

KernelArgumentBlock 的解析(ptracer.c:232-319):从 REG_SP(栈顶)开始,依次读 argcargv → 跳过 envp → 定位 auxv,在 auxv 里找 AT_ENTRY(程序真正入口)及其内存地址。所有读取走 read_procprocess_vm_readv)。

这个手法的优雅之处:不需要硬件断点、跨架构通用——让进程自己跑到入口再因为地址非法而崩,崩点就是我们要的注入时机。

4.3 远程加载 libzygisk.so(CSOLoader)

停稳后,调用 remote_csoloader_load_and_resolve_entryptracer.c:378)在 zygote 内加载注入体。这是 ReZygisk 防检测的关键,单独在第六节展开。它返回 libzygisk 在目标进程的基址 remote_base、大小 remote_size、以及入口符号 entry 的地址 injector_entry

4.4 远程调用 entry()

// ptracer.c:390-395
long args[3] = { remote_base, remote_size, 0 /* tango_flag */ };
remote_call(pid, &regs, injector_entry, (uintptr_t)libc_return_addr, args, 3);

remote_call 怎么在目标进程里"调用一个函数"?见第五节。这里关键是怎么知道调用结束了——ReZygisk 用了一个统一技巧:给函数一个必定崩溃的返回地址libc_return_addr,取自 libc 一个不可执行段的起始)。函数返回时跳到这个地址 → 触发 SIGSEGV → 控制回到 ptracer。

// ptracer.c:401-403 成败判定
// 停下来时 REG_IP 恰好等于 libc_return_addr → 说明 entry() 正常返回
if (停在 libc_return_addr) { /* 成功 */ }
else { /* entry() 内部崩了 */ }

4.5 恢复现场,detach

entry() 成功后,把备份的寄存器恢复(REG_IP 设回真实 entry_addr),set_regsptracer.c:427-431)。随后 trace_zygote 经过一串 CONT/WAIT 握手后 PTRACE_DETACHptracer.c:566-614)。zygote 像什么都没发生一样从入口继续执行——但它的地址空间里已经多了一个 libzygisk.so,且 JNI 方法表已被 hook(hook 在 entry() 里完成,见 03 篇)。


五、远程过程调用原语(utils.c)

trace_zygote 依赖一组"在别的进程里操作内存、寄存器、调用函数"的原语。它们是理解 ptracer 的基础。

5.1 跨架构寄存器抽象

utils.h:18-39 把四种架构(aarch64 / arm / x86_64 / i386)的寄存器统一成 4 个宏:

含义
REG_SP 栈指针
REG_IP 指令指针(PC)
REG_RET 返回值寄存器
REG_SYSNR 系统调用号寄存器

整个 ptracer 的跨架构能力都建立在这层抽象上。

5.2 内存与寄存器读写

函数 机制
read_proc / write_procutils.c:26-62 process_vm_readv / process_vm_writev 批量读写远程内存
get_regs / set_regsutils.c:64-120 x86 用 PTRACE_GETREGS;ARM 系用 PTRACE_GETREGSET, NT_PRSTATUS(iovec)

5.3 远程符号解析:本地求偏移 + 远程基址

find_func_addrutils.c:174-215)解决"目标进程里某个 libc 函数在哪"。它利用一个事实:ptracer 自己也加载了同名的系统库(libc/linker),于是可以在本进程解析符号偏移,再换算到目标的加载基址:

// utils.c:209 跨进程符号定位的核心等式
uintptr_t addr = (uintptr_t)(sym - local_base) + (uintptr_t)remote_base;

本地偏移用 elf_utilElfImg_create + getSymbAddress 求出(见 03 篇)。这避免了在目标进程里跑 dlsym(既慢又留痕)。

5.4 remote_call:构造调用栈

remote_callutils.c:225-341)按架构 ABI 装配寄存器和栈:

flowchart LR A["align_stack<br/>16 字节对齐"] --> B["按 ABI 传参"] B --> C1["x86_64: rdi/rsi/rdx/rcx/r8/r9 + 压栈"] B --> C2["aarch64: x0-x7 + 压栈<br/>x30(lr)=return_addr"] B --> C3["arm: r0-r3 + 压栈<br/>lr=return_addr + CPSR Thumb 位"] C1 & C2 & C3 --> D["REG_IP = func_addr"] D --> E["set_regs → PTRACE_CONT → wait"] E --> F{"停在 return_addr<br/>(SIGSEGV)?"} F -->|是| G["返回 REG_RET"] F -->|否| H["报错返回 0"]

关键设计(与 4.4 呼应):把返回地址设成一个会 fault 的地址,靠"停在该地址的 SIGSEGV"判定调用正常结束并取回返回值(utils.c:325-332)。

5.5 remote_syscall:裸 syscall gadget

remote_call 调用的是用户态函数,而加载库需要的 mmap/mprotect系统调用则用 remote_syscallutils.c:891-1013)实现。它不调用 libc 的封装,而是:

  1. find_syscall_gadgetutils.c:359-448)在目标可执行内存里搜索一条 syscall 指令(aarch64 svc #0、x86_64 syscall、i386 int 0x80、arm 的 ARM/Thumb 变体),优先在 [vdso] 里找
  2. 把系统调用号和参数装进寄存器,REG_IP 指向这条 gadget。
  3. 两次 PTRACE_SYSCALL(进入 + 退出 syscall-stop)确保系统调用真正执行完,取 REG_RET,最后恢复原寄存器。

为什么用裸 gadget 而非跳进 libc 的 mmap?因为新版 Android 启用了 IBT(间接分支跟踪)/ GCS(受保护影子栈),"跳进 libc 函数中间"会被 CPU 拦截。直接复用进程里已有的 syscall 指令则不受影响。aarch64 上还会清 pstateBTYPE 位以让单步 vDSO 的 svc 被接受(utils.c:915-916)。


六、CSOLoader:绕过系统 linker 的远程加载

remote_csoloader.c 是 README 中提到的 CSOLoader("SOTA Linux custom linker")的远程化部分。它在目标进程里完成 libzygisk.so 的加载,完全不经过系统 linker / dlopen

6.1 为什么要自己写加载器

系统 dlopen 会把新库登记进 linker 的内部链表(soinfo),并使其出现在 dl_iterate_phdr/proc/self/maps 的可识别项里。很多 root 检测正是遍历 linker 的库列表或扫描 maps 找可疑映射来发现注入。CSOLoader 自己 mmap 的映射不进入 linker 列表,从根本上躲开这类检测。

6.2 加载步骤

remote_csoloader_load_and_resolve_entryremote_csoloader.c:684-1106)的流程:

flowchart TB A["本地 open + 解析 ELF<br/>compute_load_layout (PT_LOAD 范围)"] --> B["find_syscall_gadget"] B --> C["把 .so 路径写到目标栈上"] C --> D["远程 openat 打开 .so"] D --> E["远程 mmap(PROT_NONE)<br/>占位整段地址空间<br/>(LP64: 要求 >= 4GiB 高地址)"] E --> F["逐 PT_LOAD 段远程映射<br/>(MAP_FIXED, 文件部分+bss 清零)"] F --> G["远程 close fd"] G --> H["解析动态信息<br/>(SYMTAB/STRTAB/REL/GNU_HASH)"] H --> I["apply_relocations<br/>逐架构重定位"] I --> J["逐段 mprotect 为最终权限"] J --> K["解析入口符号 'entry'<br/>→ remote_base + 偏移"]

几个防检测细节:

  • 高地址放置remote_csoloader.c:793-837):LP64 上要求映射基址 >= 0x100000000(4GiB+),远离目标进程自然产生的 VMA,降低被"相邻可疑映射"启发式发现的概率。
  • 裸 syscall:所有 mmap/mprotect/openat/close 都走 remote_syscall(同 5.5 的理由)。
  • 抹除栈上路径串remote_csoloader.c:764-791):把临时写到目标栈上的 .so 路径字符串用 0 覆盖,不留痕迹。
  • 重定位时解析 dl* 到 linker 内部符号remote_csoloader.c:483-499):对未定义的 dlopen/dlsym 等,直接解析 /system/bin/linker(64) 导出的 __dl_dlopen/__dl_dlsym,既可用又不走正常符号绑定。

重定位实现按架构分(remote_csoloader.c:514-649):aarch64 处理 R_AARCH64_RELATIVE/GLOB_DAT/JUMP_SLOT/ABS64,x86_64 处理 R_X86_64_*,arm/i386 类似。入口符号约定为 entryremote_csoloader.c:1080-1090),正对应注入体 injector/entry.cvoid entry(void*, size_t, int)


七、Tango 路径:ARM32-on-ARM64 的特殊注入

Tango 是让 AArch64-only 设备运行 32 位 ARM 程序的二进制翻译器。注入 Tango 需要完全不同的策略(inject_tangoptracer.c:27-229,仅 ARM32 编译),因为:

  • Tango 在用户态拦截 BKPT/UDF 断点指令,会吞掉 SIGTRAP,标准的断点式停顿失效;
  • Tango 维护内存侧状态,不能简单用 set_regs 回退。

ReZygisk 的解法是 GOT hook

flowchart TB A["监视 app_process32 的<br/>__libc_init GOT 槽<br/>(tango_wait_linker_ready)"] --> B["等 linker 解析该槽<br/>(值从初值变为非零)"] B --> C["在可执行段 padding 处<br/>写一段 Thumb trampoline"] C --> D["trampoline: mprotect(RWX)<br/>→ entry(base,size,tango=1)<br/>→ kill(getpid, SIGTRAP)"] D --> E["把 __libc_init GOT 槽<br/>改写为 trampoline 地址 (GOT hook)"] E --> F["PTRACE_CONT, 等目标走到<br/>__libc_init → 跳进 trampoline"] F --> G["entry 执行后 kill(SIGTRAP)<br/>(走内核信号路径绕过 Tango 拦截)"] G --> H["捕获 SIGTRAP → 清理 trampoline<br/>恢复 GOT 原值"]

要点:

  • ptrace_poke_u32utils.c:765-788)用 PTRACE_PEEKDATA/POKEDATA 做非对齐写,可绕过 RELRO(只读 GOT 页通过 ptrace 仍可写)。
  • kill(getpid(), SIGTRAP)(而非断点指令)走内核正常信号投递路径,绕过 Tango 对断点的拦截。
  • 清理阶段用一段 tail-call stub 让目标自然恢复执行(因不能用 set_regs)。

八、防检测设计小结

把本篇散落的防检测点汇总,这是 ReZygisk 相对系统 Zygisk / 旧方案的核心价值:

手段 位置 防的是什么
ptrace SEIZE init(不发停止信号) monitor.c:545 对 init 干扰最小,避免异常
破坏 AT_ENTRY 精确停顿 ptracer.c:329 无需硬件断点,跨架构、低痕迹
CSOLoader 自加载(不进 linker 列表) remote_csoloader.c 遍历 linker 库列表的检测
高地址放置映射 remote_csoloader.c:793 "相邻可疑映射"启发式
裸 syscall gadget(优先 vdso) utils.c:359 IBT/GCS CPU 缓解;libc hook 检测
抹除栈上路径串 remote_csoloader.c:764 内存扫描残留字符串
Tango GOT hook + kill(SIGTRAP) ptracer.c:27 Tango 吞断点信号
ptrace 痕迹清理(seccomp event) injector/ptrace_clear.c(见 03 篇) 注入后残留的 ptrace event 信息

九、小结

  1. monitorPTRACE_SEIZE 追踪 init,靠 epoll(signalfd + 控制 socket)双事件循环,在 PTRACE_EVENT_EXEC 时按程序名(app_process64/32tango_translator)捕获 zygote,拉起 zygiskd 并 fork+exec 出独立的 ptracer,自身则继续守着 init。
  2. ptracertrace_zygote 用"破坏 AT_ENTRY 等 SIGSEGV"精确停在 linker 就绪、业务未跑之时;再用 CSOLoader 远程加载 libzygisk.so(绕过系统 linker、高地址放置、裸 syscall),最后 remote_callentry() 并恢复现场 detach。
  3. 远程调用的统一技巧是"给一个必定 fault 的返回地址,靠 SIGSEGV 收口";系统调用则复用进程内已有的 syscall 指令(gadget),规避现代 CPU 缓解。
  4. 注入完成的确认是注入体经 zygiskd 回流给 monitor 的 ZYGOTE*_INJECTED,而非 ptracer 直接发出。

下一篇(03)进入被注入的 libzygisk.so 内部:它如何 hook JNI 方法、如何驱动模块的 preAppSpecialize/postAppSpecialize 生命周期,以及 ELF 解析、痕迹清理等反检测基石。

评论

  • 还没有评论,来说点什么吧。

无需注册或登录,填个昵称即可评论。