iOS动态调试防护

1,732 阅读8分钟

cyberpunk2077.png 众所周知,安全攻防,是一个互相博弈的过程,知彼知己才能百战不殆,想要做好防御,自然也要知道敌人是怎么进攻的.那么我们在尝试逆向(以学习为目的) 他人的App的时候,除了先进行必要的砸壳,重签名等步骤后,首先要进行的工作,肯定是通过动态调试的方式,尝试找到切入点.既然是调试,自然会想到LLDB,我们在做普通开发的时候,可以通过Xcode断点的方式,调试自己的App,那我们怎么才能调试别人的App呢?这就需要了解到LLDB的原理了.

LLDBdebugserver

  • Xcode能够通过LLDB调试App,是因为XcodeiPhone上都有debugserver环境
    • Xcode内的debugserverDeviceSupport文件夹内,具体路径是/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/,在任意版本路径下的DeveloperDiskImage.dmg文件内的/usr/bin下面,如下图:
      • debugserverOnMac.png
    • iPhone内的debugserver需要在越狱环境的手机才能看到,而且需要通过Xcode运行并安装到手机上以后才有,具体路径是/Developer/usr/bin,如图:
      • debugserveroniphone.png
    • 越狱环境下,我们可以用终端通过LLDB连接到手机上的debugserver,对越狱手机上的App进行调试
    • 通过Xcode附加进程可以更加方便的调试,甚至能够进行UI调试.
  • debugserver能够调试App,是通过一个系统函数ptrace
    • ptraceprocess trace,进程跟踪.
    • debugserver想要附加到其他进程,必须通过ptrace函数来获取系统的允许.
    • ptrace是一个系统函数,此函数能够通过一个进程去监听并控制另一个进程(即被调试的App)
    • ptrace甚至可以读取并修改被控制进程内存寄存器里的数据
    • 通过修改pc寄存器的值,能够实现断点调试

利用ptrace防护debugserver动态调试

  • 我们知道了动态调试的原理是debugserver,而debugserver又使用了ptrace函数,那么显而易见的就要通过ptrace函数下手,进行防护.
  • ptrace函数在mac平台可以直接通过引用头文件#import <sys/ptrace.h>后进行使用,但是在iOS环境中并无法直接引用这个头文件,但实际上ptraceiOS环境中也是有实现的,所以我们直接自己声明或者创建一个xxx.h头文件,然后将mac端的sys/ptrace.h头文件的内容直接copy过来,即可调用ptrace函数,头文件的实现如下:
    • ptraceHeader.png
  • ptrace函数说明
    • 函数声明:
    int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
    
    • _request:需要ptrace做的事情,值可以查看头文件内的宏定义
      • #define PT_DENY_ATTACH  31表示拒绝附加
    • _pid:要操作的进程id, 0代表当前进程
    • _addr & _data:地址和数据,根据参数_request决定是否需要传递,不需要默认传0
  • ptrace函数的使用
    • load中调用
      • load.png
      • 此时用Xcode附加调试会直接断开连接
    • constructor中调用
      • constructor.png
      • 此时用Xcode附加调试会直接断开连接
    • main函数中调用
      • main.png
      • 此时用Xcode附加调试会直接断开连接
    • didFinishLaunchingWithOptions中调用
      • finish.png
      • 此时用Xcode附加调试会直接闪退
    • 总结
      • 由于loadconstructor会在main函数之前执行,所以经过以上验证可知,ptracemain函数之前调用会停止进程附加,在main函数之后调用会使App直接闪退.
      • 调用ptrace只会影响debugserver LLDB调试,不会影响App的正常使用.

利用fishhook干掉ptrace防护

  • fishhookFacebook提供的一个动态修改链接machO文件的工具。利用machO文件加载原理,通过修改懒加载表(Lazy Symbol Pointers)和非懒加载表(Non-Lazy Symbol Pointers)这两个表的指针达到hook C函数的目的。
  • 既然调用ptrace函数能够防止动态调试,那我们只要将ptrace函数hook掉,就可以废掉ptrace防护
  • 通过framework对目标App进行代码注入,实现ptrace hook:
    • antiPtrace.png
  • 增加的framework会被dyld一并加载,在load方法中通过fishhook替换掉ptrace函数,即可使ptrace失效.

利用sysctl进行防护

fishhook竟然如此轻松的就将ptrace防护废掉了,那有没有什么方式能够躲过fishhook呢?答案就是sysctl.
sysctl是在usr/include > sys > sysctl.h中的一个函数,能够根据传递的参数去访问部分进程信息,通过sysctl函数能够获取到当前是否正在调用ptrace,也就能够得知是否有人正在debug我们的程序,sysctl的函数声明和参数释义如下:

  • int sysctl(int *, u_int, void *, size_t *, void *, size_t);
    • int *  查询信息的数组
    • u_int    数组中数据类型的大小
    • void *   接收信息的结构体的指针
    • size_t * 接收信息的结构体的大小
    • void *   默认传0,依据前面的参数决定传什么
    • size_t * 默认传0,依据前面的参数决定传什么
  • sysctl用法示例:
    • sysctl.png
  • 通过上图的isDebugger()函数,就能够获知当前进程是否正在被调试,因为在调用sysctl函数后,我们就能通过结构体指针info接收到我们要查询的信息,下面我们就看一下kinfo_proc结构体,并探究一下如何通过p_flag & P_TRACED来得知当前进程是否被调试:
    • 首先是kinfo_proc结构体:
      • kp_proc.png
    • 其中extern_proc结构体kp_proc是我们需要关注的:
      • p_flag.png
    • kp_proc结构体中,我们看到有一个p_flag,我们来看一下它都可以设置哪些值,就能清楚的得知它能够做哪些事了:
      • p_traced.png
    • 通过注释我们可以知道,extern_proc.p_flag可以配置如图中的许多宏定义的值,这些值是按位运算来设计的,其中我们要找的,正是#define P_TRACED,通过注释可以得知,当p_flag值包含P_TRACED时,我们的进程就正在被Debug!
  • 所以,与运算info.kp_proc.p_flag & P_TRACED即能检测ptrace()函数的调用情况,既然能检测到动态调试,我们就可以随意设计相应的防护逻辑了!
  • 补充:检测调试的逻辑可以通过定时器的方式来间断性检测,更安全,而且GCD的代码块能够让函数调用栈更模糊,当对方想要查找我们调用sysctl函数的位置时,将更加的困难,如下:
    • gcdsysctl.png

干掉sysctl

没有绝对安全的防护,sysctl亦然,有经验的逆向人员可以通过符号断点获知当前进程时候有调用sysctl函数,然后简简单单,注入代码,hooksysctl,篡改p_flag的值,让检测的逻辑失效,如下:

  • anti-sysctl.png

先发制人,克制fishhook

  • 我们发现,无论是ptrace也好,sysctl也罢,不管是直接干掉被debug的进程,还是对debug行为进行检测并处理,都能够被有经验的逆向人员通过fishhook的方式简简单单就干掉,既然fishhook这么牛,我们何不站在逆向角度来思考,我们是怎么被干掉的?答案就是先发制人.
  • fishhook能够hook到我们的函数,是通过framework代码注入的方式,而framworkdyld加载过程中被优先加载并执行了load方法,我们的防护函数在执行之前就被fishhook篡改了!
  • 反制措施也就不言而喻了,我们将ptrace或者sysctl的防护代码,转移到我们自己的framework中,优先加载并执行,即使逆向人员能够通过fishhook或者monkey工程(其原理也是fishhook)搞事情,但我们早在对方代码注入之前,就已经得知有人的debug我们的进程了.

攻防永无止境

是不是我们将防护代码(ptracesysctl)转移到framework中就一定能安全了呢?答案是否定的,攻防永远没有尽头,我们的程序代码framework最终会被buildmach-O文件,而在逆向领域,有很多能够对二进制文件进行分析和修改的工具,下面就大致介绍一下逆向的思路:

  • 通过符号断点发现当前进程正在通过ptracesysctl尝试进行防护
  • bt指令查看当前函数调用栈,找到当前函数的动态库名称和地址
  • image list指令拿到首地址,两两相减拿到偏移值
  • 通过HooperIDA等工具分析frameworkmach-O文件,通过偏移值找到当前的汇编调用指令
  • 现在在你眼前能的,就是类似于bl ptrace这种特别明显的汇编跳转函数指令!
  • 将指令修改为nop,即空指令,不做任何操作,直接废掉ptracesysctl!

总结:隐藏很重要,发现即胜利

经过几番博弈,我们发现攻防就像两名狙击手,谁先发现对方的位置,就能想办法先发制人,或者绕过去!所以隐藏也是极其重要的防护手段,藏得好,就能让逆向人员耗费更多的精力,大大提高逆向的难度,我们的程序自然也就更加安全.
而关于如何隐藏自己,我们后续再另起一篇进行探究,攻防无止境,大家共勉!