ptrace (process trace 进程跟踪)
提供一个进程监察和控制另一个进程, 并且可以读取和改变被控制进程的内存和寄存器里面的数据.它就可以用来实现断点调试(修改pc寄存器里的值)和系统调用跟踪.
- ptrace -> iOS系统没有, 但是macOS里有
- 想看具体的参数, 可以创建一个macOS工程
- #import <sys/ptrace.h> -> 就可以查看ptrace参数
- ptrace函数说明 -> ptarce(PT_DENY_ATTACH,0,0,0); -> 拒绝附加进程操作(枚举31)
- arg1: 需要ptrace做的事情
- arg2: 需要操作的进程ID
- arg3(地址)/4(数据): 取决于第一个参数
- 调用了就不能进行debug调试了, 但是用户可以点击正常运行
查看ptrace符号
- 拿到MachO文件
- 查看懒加载和非懒加载符号表
- 发现ptrace在懒加载符号表里很显眼
怎么破解ptrace
- 导入自己定义的ptrace头文件
- InjectCode注入, 用fishhook干掉ptrace
- 运行如果报错 -> 注意插件的运行版本要小于手机的版本, 测试的时候可以都调为iOS11
相应的hook代码
//ptrace声明文件 #import "MyPtraceHeader.h" #import "fishhook.h" //定义一个函数指针!! int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data); //自定义的函数 int my_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data){ if (_request != PT_DENY_ATTACH) {//如果不是拒绝附加保留原始调用! return ptrace_p(_request,_pid,_addr,_data); } return 0; } +(void)load { //交换! struct rebinding ptraceBd; ptraceBd.name = "ptrace"; ptraceBd.replacement = my_ptrace; ptraceBd.replaced = (void *)&ptrace_p; struct rebinding bds[] = {ptraceBd}; rebind_symbols(bds, 1); }
sysctl
- 导入头文件 #import <sys/sysctl.h>
- sysctl参数说明 ->
sysctl(<#int *#>, <#u_int#>, <#void *#>, <#size_t *#>, <#void *#>, <#size_t#>)
- 查询信息数组
- 数组中数据类型的大小
- 接收信息结构体的指针
- 接收信息结构体的大小
判断调试模式的代码
BOOL isDebugger(){ int name[4];//里面放字节码。查询的信息 name[0] = CTL_KERN;//内核查询 name[1] = KERN_PROC;//查询进程 name[2] = KERN_PROC_PID;//传递的参数是进程的ID(PID) name[3] = getpid();//PID的值告诉它! struct kinfo_proc info;//接收进程信息的结构体 size_t info_size = sizeof(info); /** 1、查询信息数组 2、数组中数据类型的大小 3、接受信息结构体的指针 4、接受信息结构体的大小的指针 */ int error = sysctl(name, sizeof(name)/sizeof(*name), &info, &info_size, 0, 0); assert(error == 0);//0 就是没有错误,其他就是错误码! return ((info.kp_proc.p_flag & P_TRACED) != 0); }
& 解析
最后的判断条件
-> info.kp_proc.p_flag & P_TRACED) != 0
- 因为p_flag是状态标识
- 取值如图
image.png
b.那我们拿的时候的计算方式为 ->
image.png
- 可以写一个定时器来定时调用来检测是否被调试
- 如果检测到了
- exit(0);
- 或者上报服务器
- 或者请求控制
- 用sysctl这种检测手段, 那么可延展性很好.
用fishhook破解sysctl
#import <sys/sysctl.h> #import "fishhook.h" @implementation InjectCode +(void)load { rebind_symbols((struct rebinding[1]){{"sysctl",my_sysctl,(void *)&sysctl_p}}, 1); } //原始函数地址 int (*sysctl_p)(int *, u_int, void *, size_t *, void *, size_t); //定义新的函数 int my_sysctl(int *name, u_int namelen, void *info, size_t *infosize, void *newinfo, size_t newinfosize){ if (namelen == 4 && name[0] == CTL_KERN && name[1] == KERN_PROC && name[2] == KERN_PROC_PID && info) { int err = sysctl_p(name,namelen,info,infosize,newinfo,newinfosize); struct kinfo_proc * myinfo = (struct kinfo_proc *)info; if (myinfo->kp_proc.p_flag & P_TRACED) { //使用异或可以取反! myinfo->kp_proc.p_flag ^= P_TRACED; } return err; } return sysctl_p(name,namelen,info,infosize,newinfo,newinfosize); } @end
防fishhook
- 只要比你的rebind_symbols提前执行就OK了
- 将防护代码封装到一个framework里面, 这样这个framework里面的代码永远会比你注入的代码提前执行.
- 我们经常使用的MonKeyDev是带有自动破解ptrace和sysctl的, 但是这种防护手段也是有效的
- 所以只要做了提前执行 -> 你所有的注入手段,都失效了
破解提前注入
修改二进制
- 正常情况, 我想运行别人的包, 但是一运行就闪退, 首先打符号断点, 看看是否有ptrace
- 如果能断到, 就是有
image.png
b.这时候可以bt 来打印堆栈, 能查看到包含ptrace代码的动态库名称
image.png
- 这时候需要找到两个关键值
- 所在的动态库
- 地址上图的72bb44
- image list -> 搜动态库找到动态库的地址 -> 两个地址相减, 就找到偏移了
- 在所在的动态库找到偏移地址
- 找到要破解的可执行文件里面找到对应的framework里的macho -> 用hopper打开
- 搜索上面得到的地址 -> 注意pc寄存器保存的是下一个指令的地址, 所以这里要找上一条指令
image.png
b.[shift + alt + a]修改上面对应调用ptrace的地方, 改为nop
image.png
c.nop相当于一个空指令
- 修改完后File -> Produce New Executable
- 生成的动态库的macho去替换对应的文件
- 再用MonkeyDev运行, 发现完美破解防护
- 其实MonkeyDev是带有反防护sysctl的代码的, 需要手动打开
image.png
c.所以这种二进制的修改破解防护, 只要你找到就搞定了 -> 暴力破解
破解sysctl
- 下符号断点
image.png
- 发现block_invoke -> 系统调用的 -> 因为之前我们写防护是在dispath中
- 找不到任何的调用代码的痕迹 -> 这里说明用GCD防护很棒
- 这时候用hopper打开工程的macho全局搜索sysctl, 发现找不到
- 这时候就怀疑的动态库, 所以要用hopper打开我们怀疑的动态库的macho来遍历搜索sysctl
- 记住这里我们只是测试, 但是真实的环境中, 这个查找应该很慢
5.运行后崩溃, 是因为这里nop修改sysctl后, assert报错(这给我们什么启示)
image.png
防护总结
- 提前执行, 写在自己的私有动态库里
- 写在GCD里面
- 攻防博弈 -> 找到就赢