ptrace (process trace 进程跟踪)
提供一个进程监察和控制另一个进程, 并且可以读取和改变被控制进程的内存和寄存器里面的数据.它就可以用来实现断点调试(修改pc寄存器里的值)和系统调用跟踪.
- ptrace -> iOS系统没有, 但是macOS里有
- 想看具体的参数, 可以创建一个macOS工程
- #import
-> 就可以查看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
- 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是状态标识
- 取值如图
- 那我们拿的时候的计算方式为 ->
- 取值如图
- 可以写一个定时器来定时调用来检测是否被调试
- 如果检测到了
- exit(0);
- 或者上报服务器
- 或者请求控制
- 用sysctl这种检测手段, 那么可延展性很好.
用fishhook破解sysctl
#import
#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
- 如果能断到, 就是有
- 这时候可以bt 来打印堆栈, 能查看到包含ptrace代码的动态库名称
- 如果能断到, 就是有
- 这时候需要找到两个关键值
- 所在的动态库
- 地址上图的72bb44
- image list -> 搜动态库找到动态库的地址 -> 两个地址相减, 就找到偏移了
- 在所在的动态库找到偏移地址
- 找到要破解的可执行文件里面找到对应的framework里的macho -> 用hopper打开
- 搜索上面得到的地址 -> 注意pc寄存器保存的是下一个指令的地址, 所以这里要找上一条指令
- [shift + alt + a]修改上面对应调用ptrace的地方, 改为nop
- nop相当于一个空指令
- 搜索上面得到的地址 -> 注意pc寄存器保存的是下一个指令的地址, 所以这里要找上一条指令
- 修改完后File -> Produce New Executable
- 生成的动态库的macho去替换对应的文件
- 再用MonkeyDev运行, 发现完美破解防护
- 其实MonkeyDev是带有反防护sysctl的代码的, 需要手动打开
-
- 所以这种二进制的修改破解防护, 只要你找到就搞定了 -> 暴力破解
破解sysctl
- 下符号断点
- 发现block_invoke -> 系统调用的 -> 因为之前我们写防护是在dispath中
- 找不到任何的调用代码的痕迹 -> 这里说明用GCD防护很棒
- 这时候用hopper打开工程的macho全局搜索sysctl, 发现找不到
- 这时候就怀疑的动态库, 所以要用hopper打开我们怀疑的动态库的macho来遍历搜索sysctl
- 记住这里我们只是测试, 但是真实的环境中, 这个查找应该很慢
- 运行后崩溃, 是因为这里nop修改sysctl后, assert报错(这给我们什么启示)
防护总结
- 提前执行, 写在自己的私有动态库里
- 写在GCD里面
- 攻防博弈 -> 找到就赢