iOS反调试与反反调试
反调试原理ptraceptrace (Process Trace) 是 Linux(以及其他一些类 Unix 系统,如 macOS)提供的一个极其强大且底层的系统调用。它的
反调试原理ptraceptrace (Process Trace) 是 Linux(以及其他一些类 Unix 系统,如 macOS)提供的一个极其强大且底层的系统调用。它的核心功能是允许一个进程(称为 tracer)观察和控制另一个进程(称为 tracee)的执行,并能检查和修改该进程的内存和寄存器。
核心功能
追踪器的介入 (Tracer/Tracee):
一个进程(如 gdb, strace)调用 ptrace 并指定 PTRACE_ATTACH 或 PTRACE_TRACEME 来开始追踪另一个目标进程。
被追踪的进程 (tracee) 就进入了特殊的状态。每当发生特定事件(尤其是系统调用或信号传递)时,内核会暂停 (SIGSTOP) tracee 的执行,并通知 tracer。
观察和控制执行:
单步执行 (Stepping): Tracer 可以让 tracee 执行一条机器指令 (PTRACE_SINGLESTEP, PTRACE_STEP),然后再次暂停,让 tracer 有机会检查状态。
设置断点: Tracer 可以修改 tracee 的代码(在内存中),通常是通过替换目标地址的指令为一个特殊的陷阱指令(如 int 3)。当 tracee 执行到此处时,会触发一个信号并暂停,交给 tracer 处理。之后 tracer 恢复原指令,并可能继续执行或单步。
拦截系统调用:
Tracer 可以要求在 tracee 即将进入系统调用 (PTRACE_SYSCALL) 或刚从系统调用返回时暂停。
strace 工具就是利用这点来打印出 tracee 进行了哪些系统调用以及参数、返回值。
Tracer 甚至可以修改进入系统调用时的寄存器参数(从而改变系统调用行为)或者修改系统调用的返回值。
检查和修改内存和寄存器:
Tracer 可以读写 tracee 的内存 (PTRACE_PEEKDATA, PTRACE_POKEDATA),包括代码段和数据段。
Tracer 可以读写 tracee 的 CPU 寄存器 (PTRACE_GETREGS, PTRACE_SETREGS, PTRACE_GETFPREGS, PTRACE_SETFPREGS)。
处理信号:
当有信号要发送给 tracee 时,内核会先暂停 tracee,并通过 wait() 通知 tracer。
Tracer 有决定权:
忽略这个信号:不让它传递给 tracee。
将信号传递给 tracee,让它处理。
注入一个自定义的信号给 tracee。
在这个过程中修改信号的行为。
主要应用场景
调试器 (Debuggers): 这是最经典的用途。像 gdb 就是利用 ptrace 来实现设置断点、单步执行、检查变量(内存)、查看寄存器值等核心调试功能。
系统调用追踪器 (System Call Tracers): strace 和 ltrace 这些工具完全依赖 ptrace 来拦截进程调用的系统调用和库函数,并打印相关信息,用于诊断和分析程序行为。
代码插桩 (Instrumentation): 在运行时修改程序代码或注入代码片段,用于性能分析(Profiling)、测试覆盖率、内存检测(如 AddressSanitizer/Valgrind 的部分功能原理)等。
沙箱/安全工具: 创建受限的执行环境。Tracer 可以监控 tracee 的系统调用,并根据安全策略允许或拒绝某些敏感操作(如访问特定文件、进行网络连接)。
进程间控制: 一些特殊场景下需要精细控制另一个进程的执行流。
进程注入: 向另一个运行中的进程注入代码或数据。
重要特点和注意事项
强大而底层: ptrace 给了 tracer 对 tracee 几乎完全的控制权。
复杂性: ptrace 接口相对复杂,涉及信号处理、进程状态、内存布局等多个方面,正确使用并不容易。
安全性: ptrace 能力强大,通常普通用户只能 ptrace 自己拥有的进程(除非有 CAP_SYS_PTRACE 权限或 YAMA 等安全模块被配置)。恶意使用 ptrace 可被用于攻击或监控其他进程。
性能开销: 频繁的 ptrace 操作(如 strace 追踪每个系统调用)会显著降低目标进程的执行速度。
不可靠的信号处理: 使用 ptrace 的进程(tracer)在信号处理上需要极其小心,因为 SIGCHLD 等信号的默认行为会被 ptrace 事件干扰。
竞争条件: 在设计使用 ptrace 的工具时要特别注意并发和竞争条件。
总结: ptrace 是 Linux 系统上一个非常基础的、用于进程间追踪和控制的系统调用。它赋予了调试器 (gdb)、系统调用跟踪器 (strace) 以及其他各种分析、安全和控制工具强大的能力,但同时也非常复杂且需要谨慎使用。理解 ptrace 是深入理解 Linux 下进程执行、调试和系统监控机制的关键。
ptrace 的参数简单介绍ptrace 的原型可以在 macOS 中找到,新建一个 macOS 的应用,或者更加简单一点的命令行程序,就可以导入 sys/ptrace.h 文件查看函数原型并使用它了。如下图:
1234567891011121314151617181920212223242526272829303132333435363738#ifndef _SYS_PTRACE_H_#define _SYS_PTRACE_H_#include
_request: 最重要的参数,指定要对目标进程 pid 执行的具体操作类型。常用的 request 包括:
PT_TRACE_ME:指示本进程希望被其父进程跟踪(通常是调试器启动目标进程时,目标进程主动调用)。
PT_ATTACH: 附着到已运行的 pid 进程上,开始跟踪它。
PT_DETACH: 停止跟踪 pid 目标进程,使其恢复独立运行。
PT_STEP: 设置单步陷阱标志,让目标进程 pid 执行一条指令后暂停。
PT_DENY_ATTACH: 拒绝调试器附加,这正是我们需要传入的参数。
_pid:目标进程的进程 ID。
_addr: 内存地址(常用于 READ/WRITE DATA,系统调用号过滤器等特定操作)。
_data:指向一个数据缓冲区的指针,其含义取决于 request(如要写入的数据,存储读取数据的结构体地址,信号编号等)。
在 iOS 中调用 ptrace 反调试在刚刚的 macOS 应用的例子中我们可以直接导入 sys/ptrace.h 头文件使用 ptrace 函数。但是到了 iOS 环境是无法直接导入 sys/ptrace.h 头文件的,那么自然是无法通过这种方式调用 ptrace 了。但是可以确定的是 iOS 中也的确存在 ptrace 这个函数,只是 iOS 系统没有对外开放。我们可以使用一些绕过限制的方法调用 ptrace。
1. 使用 extern如下图:
这样调用之后,使用 Xcode 调试启动 APP 之后就会被断开调试了,也就意味着我们的 APP 现在无法被 lldb 调试器附加了。尝试附加调试会报以下错误:
123456 ~/ ssh root@localhost -p 2222iPhone8plus:~ root# debugserver localhost:3333 --attach=demoiOSAppdebugserver-@(#)PROGRAM:LLDB PROJECT:lldb-16.0.0 for arm64.Attaching to process demoiOSApp...zsh: segmentation fault debugserver localhost:3333 --attach=demoiOSApp
这种直接使用 ptrace 函数的方式,动态绕过的办法可以使用 fishhook 进行绕过。静态的方法当然是修改汇编指令也可以绕过。以下是使用 fishhook 绕过这种反调试的代码:
12345678910111213141516171819202122232425#import "InjectCode.h"#import "fishhook.h"int (*orig_ptrace)(int _request, pid_t _pid, caddr_t _addr, int _data);int hook_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data) { if (_request == 31) { return 0; } return orig_ptrace(_request, _pid, _addr, _data);}@implementation InjectCode+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ struct rebinding one; one.name = "ptrace"; one.replaced = (void*)&orig_ptrace; one.replacement = hook_ptrace; struct rebinding array[] = {one}; rebind_symbols(array, 1); });}@end
2. 利用 dlsym123456789101112131415161718#import
使用 dlsym 函数动态获取 ptrace 的函数调用。这种方式调用 ptrace 的话,动态的方法就无法使用 fishhook hook ptrace 绕过反调试了,但是可以通过 fishhook hook dlsym 函数的方式绕过,还可以利用 lldb 实现汇编级别的 hook 实现绕过。静态的方法同样是修改汇编指令绕过。
3. 使用 syscall 的方式1234567891011121314#import
使用 syscall 函数,通过传入第一个参数值 26 代表调用的 ptrace 函数,后续的参数就是 ptrace 所需的参数,而 31 正是 PT_DENY_ATTACH。这种方式也无法简单的通过 fishhook 绕过反调试。还可以使用 lldb 进行汇编指令级 hook 实现动态绕过。静态的方式依旧是修改汇编指令。
4. 使用内联汇编的方式123456789101112131415161718192021#import
通过这种方式调用 ptrace,动态的绕过方式使用 fishhook 肯定是不行的,但是可以使用 lldb 汇编级 hook 绕过。而静态的方式当然依旧是分析并修改汇编代码。
以上 4 种方式都是直接或间接的调用 ptrace 函数来实现反调试。iOS 内核已加强对 ptrace 的管控,非越狱环境下调用受限,开发者需依赖私有 API 或漏洞利用。单一 ptrace 易被破解,通常需结合:
sysctl 定期检测:轮询进程状态标志位(如 P_TRACED),发现调试则主动退出或相应的处理。
代码混淆:隐藏反调试逻辑,增加逆向分析难度。
在 macOS/iOS 中,ptrace 的核心价值在于主动阻断调试器附加,但需结合动态加载、系统调用和混淆技术提升可靠性。反调试本质是攻防对抗的持续升级,单一措施不足以保证绝对安全,需多层防护策略。
sysctlsysctl 是一个用于动态配置内核参数的命令行工具同时也是一个可供编程的函数接口,在类 Unix 系统(如 Linux、FreeBSD)及 Apple 生态系统(macOS、iOS)中广泛使用,但不同平台的作用范围和权限限制存在显著差异。以下是具体分析:
类 Unix 系统(Linux/BSD)中的作用sysctl 通过操作 /proc/sys/ 虚拟文件系统实现内核参数的实时调整,无需重启系统。核心功能包括:
动态调优性能
网络优化:调整 TCP 连接队列(net.core.somaxconn)、缓冲区大小(net.ipv4.tcp_rmem)、SYN Flood 防御(net.ipv4.tcp_syncookies=1)。
内存管理:控制 Swap 使用倾向(vm.swappiness=10)、脏页写回策略(vm.dirty_ratio)。
文件系统:设置最大文件句柄数(fs.file-max=2097152)。
功能启停
启用 IP 转发(net.ipv4.ip_forward=1),将主机配置为路由器。
禁用 ICMP 响应(net.ipv4.icmp_echo_ignore_all=1)以提升安全性。
持久化配置
参数写入 /etc/sysctl.conf 或 /etc/sysctl.d/*.conf,通过 sysctl -p 加载。
macOS 中的特殊性与限制macOS 继承 BSD 的 sysctl 实现,但存在平台差异:
配置方式
默认无 /etc/sysctl.conf,需手动创建并加载(sudo sysctl -p)。
部分参数需通过 launchd 持久化(如修改 maxfiles 限制需编辑 /etc/launchd.conf)。
系统完整性保护(SIP)
macOS Big Sur 及以上版本中,修改核心参数(如 kern.ipc 系列)需关闭 SIP,否则操作被拦截。
典型用例
调整 Socket 队列(kern.ipc.somaxconn)、虚拟内存参数(vm.swappiness)。
iOS 中的严格限制iOS 对 sysctl 的访问施加了更严格的安全策略:
权限封锁
多数敏感参数(如 kern.boottime)返回 EPERM 错误,禁止用户态进程读取。
仅允许少数“白名单”参数(如设备型号、CPU 核心数)通过公有 API 访问。
合法使用场景
获取设备运行时间(kern.boottime)用于反欺诈检测(如防止日期篡改),但需 Apple 审核批准。
Apple 建议优先使用公开 API(如 NSProcessInfo),并避免依赖未文档化的参数。
开发风险
使用私有 sysctl 可能导致 App 审核被拒,因违反“禁止访问私有接口”条款。
在 iOS 中使用 sysctl 检测进程是否被调试实现代码如下:
123456789101112131415161718192021222324252627282930313233343536#import
这种通过调用 sysctl 函数查询当前进程的一些信息来判断是否被附加调试的方式通常可以结合定时器,或者子线程 sleep 的方式来保持长期的运行。在这种情况下,检测到被调试的时候不建议直接退出当前进程如调用 exit、kill 等,这样的处理属于简单粗暴的方式。实际使用中可以根据需求制定其他的处理方式。
如何绕过这种检测呢?动态的方式当然同样可以使用 fishhook 进行绕过检测,但是这个 hook 方法的实现和一般的 hook 方法实现不一样,这个 hook 方法中需要修改 info 的结果,使其表示是否被调试的标志位始终为 0。具体的 hook 代码如下:
123456789101112131415161718192021222324252627282930313233343536373839#import "InjectCode.h"#import "fishhook.h"#import
那如何应对 fishhook 这种反反调试呢?当然是不要直接使用 sysctl 这样的函数调用,可以利用 dlsym 动态获取函数实现从而绕过 fishhook,或者使用汇编实现。还有一点值得注意的就是 hook 代码和反 hook 代码的执行顺序问题。
syscall如何反制总的来说,反制的的手段分为动态的 hook 和静态的修改 MachO 两个方面。
库的先后顺序能影响检测的结果。使用函数指针保存 ptrace,sysctl 地址,使用函数指针调用函数。修改 MachO
实战作者的实战环境是 Mac Pro(macOS 15.3.1),iPhone 8 Plus(iOS 16.7.10)使用 palera1n 的 rootful 越狱。
动态绕过反调试这里介绍一个 lldb 插件 xia0LLDB。提供了一个 debugme 命令可以 hook ptrace 和 inlinehook svc 来绕过反调试。但是在我的环境 iPhone8Plus iOS16.7.10 中想要成功利用 debugme 绕过 iOS 端最新版小红书还是没那么简单,出了点问题,我们来分析一下为什么。
小红书这里以作者编写本文时的 iOS 端小红书最新版本(8.88)为例。已经在2026年01月08日的最新版9.16.1验证同样可以绕过反调试防护。我们都知道,小红书是做了 lldb 反调试的。直接启动 APP,然后使用 debugserver 附加调试,肯定是附加不成功的。以下为演示:
APP 在前台时尝试附加:
12345iPhone8plus:~ root# debugserver localhost:3333 --attach=discoverdebugserver-@(#)PROGRAM:LLDB PROJECT:lldb-16.0.0 for arm64.Attaching to process discover...zsh: segmentation fault debugserver localhost:3333 --attach=discover
而就算把 APP 杀死,如上划杀掉进程,或者使用 kill,killall 命令干掉 APP 进程,再次附加调试也是无法成功的,因为此时进程并不存在。如下:
123456iPhone8plus:~ root# debugserver localhost:3333 --attach=discoverdebugserver-@(#)PROGRAM:LLDB PROJECT:lldb-16.0.0 for arm64.Attaching to process discover...error: failed to attach to process named: ""Exiting.
的确存在一种情况,即使做了反调试也可以成功附加的,那就是应用长时间没有打开过了,也不要打开,但是 ps 查看进程依然存在。这个时候可以使用 --attach 附加成功,对的即使做了反调试也能附加成功,只是在附加成功之后 continue 依然会被断开调试:
123(lldb) cProcess 562 resumingProcess 562 exited with status = 45 (0x0000002d)
或许你在网上曾经看到过 backboard 调试(通过 backboard 服务启动应用)时,在应用代码执行前暂停,以便调试器(LLDB)能附加并运行命令。即以下命令
1debugserver *:端口号 -x backboard /应用路径
然而,在 lldb 16.0.0 中,并不存在这个选项了。。。如下:
123456iPhone8plus:~ root#debugserver -x backboard localhost:3333 /private/var/containers/Bundle/Application/AEE569ED-D421-4274-93EA-72456A764135/discover.app/discovererror: invalid TYPE for the --launch=TYPE (-x TYPE) option: 'backboard'Valid values TYPE are: auto Auto-detect the best launch method to use. posix Launch the executable using posix_spawn. fork Launch the executable using fork and exec.
此时,可以使用 debugserver 的 –waitfor 选项来实现在目标应用代码执行前暂停,以便 lldb 能附加并运行命令。就是说 backboard 被 –waitfor 替代了。下面是完整的流程:
确保目标进程不存在:实现的方式有手动上划掉进程,killall -9 进程名,kill pid 等方式
使用 debugserver 的 –waitfor 选项:
我这里使用了 iproxy 进行了端口映射,将电脑的 3333 端口和手机的 3333 端口关联了起来,所以可以使用 localhost:3333 作为参数。
12345iPhone8plus:~ root# debugserver localhost:3333 --waitfor=discoverdebugserver-@(#)PROGRAM:LLDB PROJECT:lldb-16.0.0 for arm64.Waiting to attach to process discover...
手指点击启动目标 APP
进入 lldb 调试:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071 ~/ lldb https://github.com/4ch12dy/xia0LLDB Welcome to xia0LLDB - Python3 Edition ,--. ,--. ,--. ,--. ,------. ,-----.,--. ,--.`--' ,--,--. / \ | | | | | .-. \ | |) /_ \ `' / ,--.' ,-. || () || | | | | | \ :| .-. \ / /. \ | |\ '-' | \ / | '--.| '--.| '--' /| '--' /'--' '--'`--' `--`--' `--' `-----'`-----'`-------' `------'[xia0LLDB] * Version: 3.1[xia0LLDB] + Loading all scripts from /Users/franky/xia0LLDB/src[xia0LLDB] * Finished(lldb) process connect connect://localhost:3333 # 连接到手机建立的 debugserverProcess 3166 stopped* thread #1, stop reason = signal SIGSTOP frame #0: 0x0000000118fb6ed0 dyld`dyld4::PrebuiltLoader::isValid(dyld4::RuntimeState const&) const + 520dyld`dyld4::PrebuiltLoader::isValid:-> 0x118fb6ed0 <+520>: add w22, w22, #0x1 0x118fb6ed4 <+524>: cmp w22, w9 0x118fb6ed8 <+528>: b.lo 0x118fb6e64 ; <+412> 0x118fb6edc <+532>: cbz x8, 0x118fb6f60 ; <+664>Target 0: (discover) stopped.(lldb) croc # 此时处在 dyld`start 方法中,是一个非常早的阶段,很多镜像都没完成加载,CoreFoundation 都没加载,直接使用 debugme 也是会报错的..所以执行这个命令,这个命令里面其实做的事情也很简单,设置了一个 CFBundleGetMainBundle 的断点,然后继续执行会来到断点,然后删掉所有断点,当断点来到 CFBundleGetMainBundle 后,此时所有镜像都加载完成了。后续的 debugme 命令也能成功运行了[*] going to env that can run oc script1 location added to breakpoint 1[+] now you can exe oc(lldb) debugme # 这个命令核心的作用就是对 ptrace 进行 hook,以及对 svc #80 指令进行 hook,这是通过对 APP 镜像文件以及所属的 Framework 的镜像文件的代码段扫描是否存在 svc #80 指令,如果存在就进行 hook。而 hook 的核心实现原理就是新创建一个内存页存放 hook 代码,同时修改原始指令的内存页跳转到 hook 代码中。[*] start patch ptrace funtion to bypass anti debug[+] ptrace funtion patach done[*] start patch svc ins to bypass anti debug[+] use "target list" to get main module:/private/var/containers/Bundle/Application/F55AF2FB-E18C-4B2A-95D1-9630F579F8FD/discover.app/discover[*] app dir:/var/containers/Bundle/Application/F55AF2FB-E18C-4B2A-95D1-9630F579F8FD/discover.app[*] search svc from:/private/var/containers/Bundle/Application/F55AF2FB-E18C-4B2A-95D1-9630F579F8FD/discover.app/discover[*] text start:0x0000000104a48000 end:0x0000000111848630------> 4550367176[*] start hook svc at address:0x10f3917c8[+] hook svc at address:0x10f3917c8 done[*] search svc from:/private/var/containers/Bundle/Application/F55AF2FB-E18C-4B2A-95D1-9630F579F8FD/discover.app/Frameworks/A.framework/A[*] text start:0x0000000118eec6d4 end:0x0000000118eeeb90------>
从打印结果来看,似乎顺利对 discover 的 svc #80 汇编调用进行了 hook,实现了反调试绕过。然后 continue 之后马上就遇到了 signal SIGSYS 信号。。。如下:
1234567891011121314151617181920212223242526(lldb) cProcess 429 resumingProcess 429 stopped* thread #4, queue = 'com.apple.root.user-initiated-qos', stop reason = signal SIGSYS frame #0: 0x000000010663d140 discover`___lldb_unnamed_symbol272407 + 72discover`___lldb_unnamed_symbol272407:-> 0x10663d140 <+72>: mov x8, x0 0x10663d144 <+76>: cmp w8, #0x0 0x10663d148 <+80>: cset w0, eq 0x10663d14c <+84>: ldp x29, x30, [sp, #0x90]Target 0: (discover) stopped.(lldb) rr x0 = 0x000000000000004e x1 = 0x0000000000000000 x2 = 0x0000000000000000 x3 = 0x0000000000000000 x4 = 0x0000000000000000 x5 = 0x0000000000000000 x6 = 0x00000002806ede50 x7 = 0x0000000000000000 x8 = 0x00000001160b20ee "/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/com.apple.MobileGestalt.plist" x9 = 0x0000000000000000 x16 = 0x00000001194c4000 pc = 0x000000010663d140 discover`___lldb_unnamed_symbol272407 + 72 lr = 0x000000010663d110 discover`___lldb_unnamed_symbol272407 + 24 sp = 0x000000016bbe5730
借助 AI 我们很容易知道,SIGSYS 是发生了系统调用异常,也就是系统调用函数出错,传入了一个非法的参数。。。通过读取寄存器 x16 的值可以发现的确不是一个正常的系统调用参数,正常的系统调用参数值在 0~558 左右之内,可以在 syscall.h 头文件查看。那为什么会出现 x16 变成一个巨大的值呢?这就需要对 debugme 进行好好研究一番了。。。实际确实花费了不少时间和功夫。最终发现出问题的地方在对原始指令的修改,跳转到 hook 代码的地方出了一点儿问题。具体代码如下:
123456789101112/* ldr x16, #0x8 br x16 hook_code_addr_1 hook_code_addr_2 *//*50 00 00 58 00 02 1f d6 原作者这里使用 x16 跳转,但是目前在小红书最新版本出现了问题,修改了 x16 的值,导致正常的 svc 调用参数超出范围出现 sigsys 异常51 00 00 58 20 02 1F D6 改用这个 x17 跳转,试试看,没问题!*/uint8_t patch_data[] = {0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1f, 0xd6, (uint8_t)(new_p&0xff), (uint8_t)((new_p>>8*1)&0xff), (uint8_t)((new_p>>8*2)&0xff), (uint8_t)((new_p>>8*3)&0xff), (uint8_t)((new_p>>8*4)&0xff), (uint8_t)((new_p>>8*5)&0xff), (uint8_t)((new_p>>8*6)&0xff), (uint8_t)((new_p>>8*7)&0xff)};int patch_data_size = 4*4;
是的,只需要修改 2 个字节,就可以继续使用 xia0LLDB 绕过小红书最新版的反调试了,但是要能知道这 2 个字节在哪,怎么修改还是需要一些的基础的。修改完 debugme.py 之后再次重复前面的步骤进入 lldb 调试,输入 debugme 之后 c (lldb continue 命令的缩写)就能正常运行了。进入页面之后,如果想查看小红书的视图控制器层次结构可以先 process interrupt 中断进程,然后输入 po [[[[UIApplication sharedApplication] keyWindow] rootViewController] _printHierarchy] 就可以查看了。_printHierarchy 是 UIViewController 的私有方法。
12345678910111213141516171819202122232425262728293031(lldb) process interruptProcess 11823 stopped* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP frame #0: 0x00000001c6dd5030 libsystem_kernel.dylib` mach_msg2_trap + 8libsystem_kernel.dylib`mach_msg2_trap:-> 0x1c6dd5030 <+8>: retlibsystem_kernel.dylib'macx_swapon: 0x1c6dd5034 <+0>: mov x16, #-0x30 ; =-48 0x1c6dd5038 <+4>: svc #0x80 0x1c6dd503c <+8>: retlibsystem_kernel.dylib'macx_swapoff: 0x1c6dd5040 <+0>: mov x16, #-0x31 ; =-49 0x1c6dd5044 <+4>: svc #0x80 0x1c6dd5048 <+8>: retlibsystem_kernel.dylib'thread_get_special_reply_port: 0x1c6dd504c <+0>: mov x16, #-0x32 ; =-50Target 0: (discover) stopped.(lldb) po [[[[UIApplication sharedApplication] keyWindow] rootViewController] _printHierarchy]
从打印的结果来看,小红书的根控制器层次结构还是有点奇怪的,导航控制器嵌入了 TabbarController,然后 TabbarController 的每个子控制器又是导航控制器。。。
这种方式属于通过 lldb 进行汇编级别的动态 hook 绕过反调试。这种动态的方式无法随时在需要的时候附加到目标进程,因为目标进程的反调试代码已经执行了。优点是不需要对 APP MachO 文件进行修改,不需要重新签名打包安装。后面的抖音反反调试是通过静态修改 MachO 文件内容进行的。
支付宝支付宝最新版 10.7.50 直接使用 xia0LLDB 的 debugme 就可以绕过了,甚至连 svc 指令都没有,但是的确做了反调试。就不多说了。
Boss 直聘Boss 直聘的反调试防护是我没有想到的。它的防护手段明显比小红书和抖音等APP要更加高明和厉害。
静态绕过反调试抖音这里同样以作者写作时的抖音最新版本 34.7.0 为例,已经在 2026年01月08日 的最新版 37.3.0 中验证依旧可绕过反调试防护。如果继续使用 xia0LLDB 对抖音进行反调试绕过的话,首先会发现 xia0LLDB 无法读取到 Aweme 和 AwemeCore 两个 MachO 的代码段地址,扫描结果都是 0 到 0。如下:
12345678910111213141516171819202122232425262728293031(lldb) debugme[*] start patch ptrace funtion to bypass anti debug[+] ptrace funtion patach done[*] start patch svc ins to bypass anti debug[+] use "target list" to get main module:/private/var/containers/Bundle/Application/4D2A141E-A8E7-429D-AF16-47DEBE3E01C1/Aweme.app/Aweme[*] app dir:/var/containers/Bundle/Application/4D2A141E-A8E7-429D-AF16-47DEBE3E01C1/Aweme.app[*] search svc from:/private/var/containers/Bundle/Application/4D2A141E-A8E7-429D-AF16-47DEBE3E01C1/Aweme.app/Aweme[*] text start:0x0000000000000000 end:0x0000000000000000------>
这就非常奇怪了,那么先开始分析 MachO 文件,这里可以使用 MachOView 查看 MachO 的 Load Command 发现,要么是不存在 __TEXT,__text 段,要么就是即便存在,但是 size 为 0。这就导致 debugme 根本无法扫描到代码段内存地址范围。但是会发现存在一个额外的 __BD_TEXT,__text 段,这个段里面都是汇编代码,看来抖音是在这里做了一定的防护。首先是 Aweme 的分析
然后是 AwemeCore 的分析:
到此我们可以重新查看 debugme 的扫描代码段的代码 get_text_segment() 方法,新增以下逻辑:
1234567891011121314151617181920212223242526272829303132333435 while(ncmds--) { /* go through all load command to find __TEXT segment*/ struct load_command * lcp = (struct load_command *)((uint8_t*)header + x_offset); x_offset += lcp->cmdsize; if(lcp->cmd == LC_SEGMENT_64) { struct segment_command_64 * curSegment = (struct segment_command_64 *)lcp; struct section_64* curSection = (struct section_64*)((uint8_t*)curSegment + sizeof(struct segment_command_64)); // check current section of segment is __TEXT? if(!strcmp(curSection->segname, "__TEXT") && !strcmp(curSection->sectname, "__text") && curSection->size != 0){ uint64_t memAddr = curSection->addr; textStart = memAddr + (uint64_t)_dyld_get_image_vmaddr_slide(image_index); textEnd = textStart + curSection->size; /* [retStr appendString:@" "]; [retStr appendString:(id)[@(textStart) stringValue]]; [retStr appendString:@" , "]; [retStr appendString:(id)[@(textEnd) stringValue]]; */ break; } /* 针对 Aweme 中 __TEXT,__text 不存在或者即使存在也没有大小的情况这里增加使用 __BD_TEXT,__text 虽然 svc 指令都找到了,但是依旧无法绕过反调试,还需要进一步分析...初步分析是hook代码破坏了原始代码的结构。。。这里还是先注释了吧*/ if (!strcmp(curSection->segname, "__BD_TEXT") && !strcmp(curSection->sectname, "__text") && curSection->size != 0) { uint64_t memAddr = curSection->addr; textStart = memAddr + (uint64_t)_dyld_get_image_vmaddr_slide(image_index); textEnd = textStart + curSection->size; break; } } }
这样就能在使用 debugme 的时候扫描到代码段了,而且扫描出来的 svc 指令调用的地方可真不少。
12345678910111213141516171819202122232425262728(lldb) debugme[*] start patch ptrace funtion to bypass anti debug[+] ptrace funtion patach done[*] start patch svc ins to bypass anti debug[+] use "target list" to get main module:/private/var/containers/Bundle/Application/4D2A141E-A8E7-429D-AF16-47DEBE3E01C1/Aweme.app/Aweme[*] app dir:/var/containers/Bundle/Application/4D2A141E-A8E7-429D-AF16-47DEBE3E01C1/Aweme.app[*] search svc from:/private/var/containers/Bundle/Application/4D2A141E-A8E7-429D-AF16-47DEBE3E01C1/Aweme.app/Aweme[*] text start:0x0000000104d90000 end:0x0000000104d97e08------>
svc 指令地址是扫描出来了,但结果还是无法成功绕过反调试。初步分析是 hook 代码破坏了原始汇编代码的结构,这么多地方(估计100到200个左右)一个个分析起来头都要炸了。。。我们还是另辟蹊径吧。既然这种动态 hook 的方式比较困难,我们就尝试静态分析一番。
首先 Aweme 的大小就很奇怪,只有 178 KB,这显然不是一个正常的应用应该有的大小,打开 hooper 分析一番。可执行文件的 MachO 都存在着入口函数,分析入口函数汇编代码:
直接无条件跳转到 imp___stubs__awemeMain 去了,双击跟进去。
这里的汇编显示,br 到寄存器 x16,而寄存器 x16 又是从 _awemeMain 加载的。那么继续双击 _awemeMain 就发现了它是一个声明在 @rpath/AwemeCore.framework/AwemeCore 的函数。
这下就可以确定了抖音的 APP 包下的 Aweme 只是一个壳,真正的主程序代码全部都写在了一个 AwemeCore 的 Framework 下。那么接下来要做的就是继续分析 AwemeCore 这个文件。但是这个文件太大了,629.9 MB,作者的电脑使用 hopper 完全吃不消这个文件。没关系,反汇编工具那么多,电脑升级不了就换个工具试试,作者这里使用的是 Ghidra。
使用 Ghidra 搜索 _awemeMain,可以看到关联的汇编代码,可以看到一个很熟悉的 svc 指令调用。
参数分别是 x0 = 26, x1 = 31。在 sys/syscall.h 头文件中可以看到 26 是 SYS_ptrace 调用,而 SYS_ptrace 的参数 31 则是 PT_DENY_ATTACH 正是反调试的系统调用。
入口函数就是一个反调试,那么我们就先干掉这个反调试试试看。使用 nop 指令替换 svc 指令,使用方法是选择 svc 指令所在的行,右键 patch instruction 输入 nop 就完成了,如下:
完成替换之后,我们重新导出可执行文件,步骤是 File -> Export Program。格式选择 original file,记得勾选 Export User Byte Modifications 不然修改没有保存。保存之后我们替换 iPhone 上的 AwemeCore,可以备份一下原始的 AwemeCore。还需要注意查看一下修改后的 AwemeCore 的文件权限,Ghidra 保存文件的时候可能是 540 的权限,使用 chmod 755 AwemeCore 提升一下文件权限,否则无法正常启动抖音,启动闪退。
12iPhone8plus:/private/var/containers/Bundle/Application/4D2A141E-A8E7-429D-AF16-47DEBE3E01C1/Aweme.app/Frameworks/AwemeCore.framework root# lsAwemeCore AwemeCoreBackup Info.plist SC_Info/ _CodeSignature/
这个时候我们可以重启手机一下 iPhone,因为有可能 APP 的 Framework 会被缓存下来,导致没有使用我们修改后的 AwemeCore 文件。重启之后,再次使用 debugserver+lldb 远程调试会发现可以成功附加调试了~~~
123456iPhone8plus:~ root# debugserver localhost:3333 --attach=Awemedebugserver-@(#)PROGRAM:LLDB PROJECT:lldb-16.0.0 for arm64.Attaching to process Aweme...Listening to port 3333 for a connection from localhost...
12345678910111213141516171819202122232425262728293031323334 ~/ lldb(lldb) pccProcess 3322 stopped* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP frame #0: 0x00000001c9ca5030 libsystem_kernel.dylib` mach_msg2_trap + 8libsystem_kernel.dylib`mach_msg2_trap:-> 0x1c9ca5030 <+8>: retlibsystem_kernel.dylib'macx_swapon: 0x1c9ca5034 <+0>: mov x16, #-0x30 ; =-48 0x1c9ca5038 <+4>: svc #0x80 0x1c9ca503c <+8>: retlibsystem_kernel.dylib'macx_swapoff: 0x1c9ca5040 <+0>: mov x16, #-0x31 ; =-49 0x1c9ca5044 <+4>: svc #0x80 0x1c9ca5048 <+8>: retlibsystem_kernel.dylib'thread_get_special_reply_port: 0x1c9ca504c <+0>: mov x16, #-0x32 ; =-50Target 0: (Aweme) stopped.(lldb) pvc
到此,我们就通过修改 MachO 文件的汇编指令实现了反调试绕过。其实一般来说,在 iOS 平台,对二进制的修改之后需要重新进行签名才能正常运行,但是经过实践证明,iOS 系统在启动 APP 的时候,应该只会对 APP 包内的第一层文件进行签名验证,而不会递归验证它的子目录,比如我们这里修改了 Framework 下的某个 MachO。如果修改的是 Aweme 的二进制文件,这个毫无疑问哪怕只要是改了一个字节都无法通过签名验证,肯定是无法启动的。 应该是安装了 AppSync Unified 插件让 iOS 签名验证部分失效了