23、iOS逆向防护

一、ptrace

1.1 、ptrace概述

ptrace: process trace,提供一个进程监察和控制另一个进程.并且可以读取和改变被控制进程的内存和寄存器里面的数据.它就可以用来实现断点调试和系统调用跟踪.

App可以被lldb动态调试,因为App被设备中的debugserver附加,它会跟踪我们的应用进程(process trace), 而这一过程利用的就是ptrace 函数.

ptrace是系统内核函数,它可以决定应用能否被debugserver附加.如果我们在项目中,调用ptrace函数,将程序设置为拒绝附加,即可对lldb动态调试进行有效的防护.

ptrace在iOS系统中存在,但是无法直接使用,需要导入头文件.但是在Mac OS系统下,存在于

  • ptrace函数的定义:
int ptrace(int _request, pid _pid, caddr_t _addr, int _data);
    • request: 请求ptrace执行的操作
    • pid: 目标进程的ID
    • addr: 目标进程的地址值,和request参数有关
    • data: 根据request的不同而变化.如果需要向目标进程中写入数据,data存放的是需要写入的数据.如果从目标进程中读数据,data将存放返回的数据.

1.2 ptrace防护应用

  • 搭建App项目AntiDebug
  • 导入MyPtraceHeader.h头文件.
  • 写入调用代码
#import "ViewController.h"
#import "MyPtraceHeader.h"
@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
    • 使用Xcode运行项目,启动后立即退出.使用ptrace设置为拒绝附加,只能手动启动App,也就是说,用户在使用App时,不会有任何影响.一旦被debugserver附加,就会闪退.
  • 如果在越狱环境,手动对App进行debugserver附加呢?
    • 找到AntiDebug进程,手动对App进行debugserver附加
Holothurian6P:~ root# ps -A | grep AntiDebug
48329 ??         0:00.23 /var/containers/Bundle/Application/3D3083F3-295D-4F55-83DB-40887FF4FF46/AntiDebug.app/AntiDebug
48336 ttys000    0:00.00 grep AntiDebug
Holothurian6P:~ root# debugserver localhost:22 -a 48329
debugserver-@(#)PROGRAM:LLDB  PROJECT:lldb-900.3.87
 for arm64.
Attaching to process 48329...
Segmentation fault: 11
    • 同样附加失败,无论以何种方式,都会被ptrace函数阻止.

1.3 ptrace防护破解

  • ptrace是系统内核函数,被开发者所熟知.ptrace的防护痕迹也很明显,手动运行程序正常,Xcode运行程序闪退
  • 我们在逆向一款App时,遇到上述情况,第一时间就会想到ptrace防护
  • 由于ptrace是系统函数,需要间接符号表,我们可以尝试下ptrace的符号断点

    • ptrace的断点命中,确定了对方的防护手段,接下来尝试破解
  • 采用AntiDebug项目,模拟应用重签名,注入动态库
  • 创建 Inject动态库,创建 InjectCode类
  • 在Inject动态类中,导入fishhook,导入MyPtraceHeader.h头文件
#import "InjectCode.h"
#import "MyPtraceHeader.h"
#import "fishhook.h"

@implementation InjectCode

int (*sys_ptrace)(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 0;
    }
    return sys_ptrace(_request,_pid,_addr,_data);
}
+ (void)load {
    struct rebinding rebPtrace;
    rebPtrace.name = "ptrace";
    rebPtrace.replacement = my_ptrace;
    rebPtrace.replaced    = (void *)&sys_ptrace;
    struct rebinding rebs[] = {rebPtrace};
    rebind_symbols(rebs, 1);
}

@end
  • 在my_ptrace函数中,如果是 PT_DENY_ATTACH 宏定义值,直接返回,如果是其他类型,系统由特定的作用,需要执行ptrace原始函数.
  • 运行项目,进入lldb动态调试,ptrace防护破解成功

二、sysctl

sysctl可以用来检测进程状态,判断当前是否是debug状态,sysctl可以自己控制接下来要干什么事情.

int sysctl(int *, u_int, void *, size_t *, void *, size_t);

2.1 sysctl使用

    • 参数1:   查询信息数组
    • 参数2: 数组中数据类型的大小
    • 参数3: 接收信息结构体指针
    • 参数4: 接收信息结构体的大小
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);
    //info.kp_proc.p_flag; 是当前的进程标记值
    //P_TRACED 是正在跟踪的调试进程
    bool ret = (info.kp_proc.p_flag & P_TRACED) != 0;
    return ret;
}
    • 调用及日志输出
- (void)viewDidLoad {
    [super viewDidLoad];
    if (isDebugger()) {
        NSLog(@"检测到有调试");
    } else {
        NSLog(@"程序正常运行");
    }
}
    • 运行项目后:日志输出
2023-04-16 20:54:02.344892+0800 SysctlDemo[1938:89970] 检测到有调试

2.2 sysctl调用

  • 在正常的反调试项目中,只检测一次是明显不合理的,我们需要定时监测,当前项目是否被调试
  • 因此需要添加一个计时器
static dispatch_source_t timer;
void debugCheck() {
    timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        if (isDebugger()) {
            NSLog(@"检测到有调试");
        } else {
            NSLog(@"程序正常运行");
        }
    });
    dispatch_resume(timer);
}
- (void)viewDidLoad {
    [super viewDidLoad];
    debugCheck();
}
    • 当检测到有调试行为后,可以直接exit(0)退出程序
    • 当手动启动App后,控制台输出: 程序正常运行. 达到目的

2.3 sysctl破解

  • 因为是系统函数,那么存在间接符号表,考虑使用fishhook来HOOK破解.在原SysctlDemo直接添加动态库,模拟被HOOK情况.在Inject中,实现sysctl破解
#import "InjectCode.h"
#import <sys/sysctl.h>
#import "fishhook.h"

@implementation InjectCode

//原始函数地址
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);
}

+ (void)load {
    struct rebinding sysctlBind;
    sysctlBind.name = "sysctl";
    sysctlBind.replacement = my_sysctl;
    sysctlBind.replaced    = (void *)&sysctl_p;
    struct rebinding rbs[] = {sysctlBind};
    rebind_symbols(rbs, 1);
//    rebind_symbols((struct rebinding[1]){
   
   {"sysctl",my_sysctl,(void *)&sysctl_p}}, 1);
}

@end
  • hook结果
2023-04-16 21:50:44.629585+0800 SysctlDemo[2115:102944] 程序正常运行
2023-04-16 21:50:45.629685+0800 SysctlDemo[2115:102944] 程序正常运行
2023-04-16 21:50:46.629694+0800 SysctlDemo[2115:102944] 程序正常运行
2023-04-16 21:50:47.629471+0800 SysctlDemo[2115:102944] 程序正常运行

三、sysctl&ptrace再防护(防止Hook)

  • 利用库的加载顺序,在别人Hook前,让防护代码先执行.就可以直线防护手段
  • 将VC中的防护去除,创建一个动态库,将VC中做的防护操作放入其中.
  • 这个时候运行项目,程序中断、但是手动运行项目查看控制台手机日志,运行正常.

#import "MyPtraceHeader.h" #import <sys/sysctl.h> @implementation AntiCode + (void)load { debugCheck(); ptrace(PT_DENY_ATTACH, 0, 0, 0); } static dispatch_source_t timer; void debugCheck() { timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0)); dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC); dispatch_source_set_event_handler(timer, ^{ if (isDebugger()) { NSLog(@"检测到有调试"); // exit(0); } else { NSLog(@"程序正常运行"); } }); dispatch_resume(timer); } 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); //info.kp_proc.p_flag; 是当前的进程标记值 //P_TRACED 是正在跟踪的调试进程 return (info.kp_proc.p_flag & P_TRACED) != 0; } @end

四、修改二进制手段破解ptrace

  • 当目标App采用库加载的方式进行了反调试,那么就需要我们通过更高级的手段去破解.
  • 采用修改二进制文件的方式,来针对这种情况.

具体操作步骤为:

1、创建一个MonkeyDev的Demo、然后将刚才的AntiHook的包做为目标App放入TargetApp中

2、下一个ptrace符号断点.可以通过当前bt显示出调用栈.找到调用ptrace的库及方法

3、取出AntiCode这个动态库,利用Hopper查看二进制文件,找到 AntiCode动态库,调用ptrace的地方

4、修改二进制文件中此行,选中后 Alt+a

    • 1、将该处直接修改为 nop

    • 2、将该处修改为直接跳转到下一句汇编执行代码

5、导出新的二进制文件 (需要破解版Hopper或付费版)

File --> Produce New Executable

6、覆盖之前.app文件里面的MachO,重新运行就可以.

五、dlopen+dlsym

  • 为了隐藏在MachO中明显的ptrace字符串常量,我们采用异或一个固定Key字符,再根据指针指向字符串初始值,再次异或,就能得到原来的字符串
  • 通过dlopen拿到句柄函数 handle
  • 定义ptrace函数指针: (*ptrace_p)
  • 通过dlsym 拿到ptrace函数指针 ptrace_p
  • 调用所需函数: ptrace_p
    #import <dlfcn.h>
    #define KEY 0xAC
    //使用一个char数组拼接一个ptrace字符串 (此拼接方式可以让逆向的人在使用工具查看汇编时无法直接看到此字符串)
    unsigned char funcName[] = {
        (KEY ^ 'p'),
        (KEY ^ 't'),
        (KEY ^ 'r'),
        (KEY ^ 'a'),
        (KEY ^ 'c'),
        (KEY ^ 'e'),
        (KEY ^ '\0'),
    };
    unsigned char * p = funcName;
    //再次异或之后恢复原本的值
    while (((*p) ^= KEY) != '\0') p++;
    //通过dlopen拿到句柄
    void * handle = dlopen("/usr/lib/system/libsystem_kernel.dylib", RTLD_LAZY);
    //定义函数指针
    int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);
    //如果拿到句柄
    if (handle) {
        //通过dlsym拿到函数指针
        ptrace_p = dlsym(handle, (const char *)funcName);
        //如果拿到函数指针
        if (ptrace_p) {
            //调用所需函数
            ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
        }
    }
  • 使用dlopen+dlsym方式调用,lldb对ptrace下断点还是可以断住的. 这样就可以通过汇编跟符号的方式,找到跳转函数,对其修改.

六、 利用syscall调用系统函数

  • 编写调用代码(中可以查找系统函数编号)
/**
参数一:参数是函数编号
其它参数:给参数一的函数提供参数
*/
syscall(SYS_ptrace,PT_DENY_ATTACH,0,0);
//上边等价于
//syscall(26,31,0,0,0);
  • syscall方式调用,lldb对ptrace下断点是无法断住的。但是syscall本身可以被符号断点断住.

七、 最终手段: 利用汇编调用系统函数

中可以查找系统函数编号

  • 根据上一章syscall的直接调用值 syscall(26,31,0,0,0), 联想到汇编调用. volatile代表不优化此汇编代码
asm volatile(
    "mov x0,#26\n"
    "mov x1,#31\n"
    "mov x2,#0\n"
    "mov x3,#0\n"
    "mov x16,#0\n"//这里就是syscall的函数编号
    "svc #0x80\n"//这条指令就是触发中断(系统级别的跳转)
);
  • 再回想ptrace的函数调用:  
ptrace(PT_DENY_ATTACH, 0, 0, 0);
  •  也就是 ptrace(31, 0, 0, 0); 那么汇编代码为(CPU架构是arm64)
asm volatile(
     "mov x0,#31\n"//参数1
     "mov x1,#0\n"//参数2
     "mov x2,#0\n"//参数3
     "mov x3,#0\n"//参数4
     "mov x16,#26\n"//中断根据x16 里面的值,跳转ptrace
     "svc #0x80\n"//这条指令就是触发中断去找x16执行(系统级别的跳转!)
     );

八、exit(0)的汇编

  • 当检测到当前为调试环境后,采用exit(0)退出程序,但是这段代码想隐藏起来,不那么直观的通过二进制就被查找到.所以可以考虑汇编实现;
  • 根据arm64和32位架构的区别,汇编上进行区分
#ifdef __arm64__
    asm(
        "mov x0,#0\n"
        "mov x16,#1\n"//这里相当于 Sys_exit,调用exit函数
        "svc #0x80\n"    
    );
#endif
#ifdef __arm__
    asm(
        "mov r0,#0\n"
        "mov r16,#1\n" //这里相当于 Sys_exit
        "svc #80\n"    
    );
#endif

九、补充

        防护: 通过对App包中的embeded.描述文件中TeamID进行唯一值判定、也是一种防护手段;

        破解思路: 找到指定TeamID、在汇编中修改TeamID值即可.

十、总结

  • 反调试
    • ptrace(31,0,0,0) ; 拒绝进程附加
    • 该函数防护的特点
      • 程序被Xcode安装运行直接闪退
      • 终端附加会失败
      • 用户正常启动能运行
  • 破解ptrace
    • 通过fishhook函数勾住ptrace
    • 判断参数1: 如果是拒绝就直接return
  • 反调试sysctl
    • 这个函数里面去检查进程状态
    • sysctl(查询信息的数组,数组大小,接受信息结构体指针,结构体大小的指针,和3、4参数一样)
    • 在接受的结构体中,kp_proc属性中有一个标记 p_flag;
    • 查看二进制位第12位(P_TRACED)是否为1(为1代表有调试器附加).
  • 破解sysctl
    • 通过fishhook
    • 取出标记,然后异或取反.进行修改标记,达到破解效果
  • 提前执行防护代码
    • 将防护代码用Framework库的形式优先执行.
  • 静态修改: 二进制修改
    • hopper、IDA
    • 找到关键函数执行代码.直接修改汇编,改变流程:
      • 跳过ptrace执行函数,b 函数地址 ;
      • 直接去掉: nop
  • dlopen+dlsym
    • 隐藏ptrace的字符串函数调用,在二进制中不可见.
    • 但是ptrace本身还是会被符号断点断住
  • syscall
    • syscall(26,31,0,0,0);
    • lldb对ptrace下断点是无法断住的。但是syscall本身可以被符号断点断住.
  • 利用汇编调用系统函数
    • syscall(26,31,0,0,0) 汇编形式
    • ptrace(31, 0, 0, 0) 汇编形式
  • exit(0)的汇编
    • arm64
    • arm32

猜你喜欢

转载自blog.csdn.net/SharkToping/article/details/130349039