众所周知,安全攻防,是一个互相博弈的过程,知彼知己才能百战不殆,想要做好防御,自然也要知道敌人是怎么进攻的.那么我们在尝试
逆向(以学习为目的) 他人的App的时候,除了先进行必要的砸壳,重签名等步骤后,首先要进行的工作,肯定是通过动态调试的方式,尝试找到切入点.既然是调试,自然会想到LLDB,我们在做普通开发的时候,可以通过Xcode断点的方式,调试自己的App,那我们怎么才能调试别人的App呢?这就需要了解到LLDB的原理了.
LLDB 与 debugserver
Xcode能够通过LLDB调试App,是因为Xcode和iPhone上都有debugserver环境Xcode内的debugserver在DeviceSupport文件夹内,具体路径是/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/,在任意版本路径下的DeveloperDiskImage.dmg文件内的/usr/bin下面,如下图:iPhone内的debugserver需要在越狱环境的手机才能看到,而且需要通过Xcode运行并安装到手机上以后才有,具体路径是/Developer/usr/bin,如图:- 在
越狱环境下,我们可以用终端通过LLDB连接到手机上的debugserver,对越狱手机上的App进行调试 - 通过
Xcode附加进程可以更加方便的调试,甚至能够进行UI调试.
debugserver能够调试App,是通过一个系统函数ptraceptrace即process trace,进程跟踪.debugserver想要附加到其他进程,必须通过ptrace函数来获取系统的允许.ptrace是一个系统函数,此函数能够通过一个进程去监听并控制另一个进程(即被调试的App)ptrace甚至可以读取并修改被控制进程内存和寄存器里的数据- 通过修改
pc寄存器的值,能够实现断点调试
利用ptrace防护debugserver动态调试
- 我们知道了
动态调试的原理是debugserver,而debugserver又使用了ptrace函数,那么显而易见的就要通过ptrace函数下手,进行防护. ptrace函数在mac平台可以直接通过引用头文件#import <sys/ptrace.h>后进行使用,但是在iOS环境中并无法直接引用这个头文件,但实际上ptrace在iOS环境中也是有实现的,所以我们直接自己声明或者创建一个xxx.h头文件,然后将mac端的sys/ptrace.h头文件的内容直接copy过来,即可调用ptrace函数,头文件的实现如下: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中调用- 此时用
Xcode附加调试会直接断开连接
- 在
constructor中调用- 此时用
Xcode附加调试会直接断开连接
- 在
main函数中调用- 此时用
Xcode附加调试会直接断开连接
- 在
didFinishLaunchingWithOptions中调用- 此时用
Xcode附加调试会直接闪退
总结- 由于
load和constructor会在main函数之前执行,所以经过以上验证可知,ptrace在main函数之前调用会停止进程附加,在main函数之后调用会使App直接闪退. - 调用
ptrace只会影响debugserver LLDB调试,不会影响App的正常使用.
- 由于
- 在
利用fishhook干掉ptrace防护
fishhook是Facebook提供的一个动态修改链接machO文件的工具。利用machO文件加载原理,通过修改懒加载表(Lazy Symbol Pointers)和非懒加载表(Non-Lazy Symbol Pointers)这两个表的指针达到hook C函数的目的。- 既然调用
ptrace函数能够防止动态调试,那我们只要将ptrace函数hook掉,就可以废掉ptrace防护 - 通过
framework对目标App进行代码注入,实现ptrace hook: - 增加的
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用法示例:- 通过上图的
isDebugger()函数,就能够获知当前进程是否正在被调试,因为在调用sysctl函数后,我们就能通过结构体指针info接收到我们要查询的信息,下面我们就看一下kinfo_proc结构体,并探究一下如何通过p_flag & P_TRACED来得知当前进程是否被调试:- 首先是
kinfo_proc结构体: - 其中
extern_proc结构体kp_proc是我们需要关注的: - 在
kp_proc结构体中,我们看到有一个p_flag,我们来看一下它都可以设置哪些值,就能清楚的得知它能够做哪些事了: - 通过注释我们可以知道,
extern_proc.p_flag可以配置如图中的许多宏定义的值,这些值是按位运算来设计的,其中我们要找的,正是#define P_TRACED,通过注释可以得知,当p_flag值包含P_TRACED时,我们的进程就正在被Debug!
- 首先是
- 所以,与运算
info.kp_proc.p_flag & P_TRACED即能检测ptrace()函数的调用情况,既然能检测到动态调试,我们就可以随意设计相应的防护逻辑了! - 补充:检测调试的逻辑可以通过定时器的方式来间断性检测,更安全,而且
GCD的代码块能够让函数调用栈更模糊,当对方想要查找我们调用sysctl函数的位置时,将更加的困难,如下:
干掉sysctl
没有绝对安全的防护,sysctl亦然,有经验的逆向人员可以通过符号断点获知当前进程时候有调用sysctl函数,然后简简单单,注入代码,hook掉sysctl,篡改p_flag的值,让检测的逻辑失效,如下:
先发制人,克制fishhook
- 我们发现,无论是
ptrace也好,sysctl也罢,不管是直接干掉被debug的进程,还是对debug行为进行检测并处理,都能够被有经验的逆向人员通过fishhook的方式简简单单就干掉,既然fishhook这么牛,我们何不站在逆向角度来思考,我们是怎么被干掉的?答案就是先发制人. fishhook能够hook到我们的函数,是通过framework代码注入的方式,而framwork在dyld加载过程中被优先加载并执行了load方法,我们的防护函数在执行之前就被fishhook篡改了!- 反制措施也就不言而喻了,我们将
ptrace或者sysctl的防护代码,转移到我们自己的framework中,优先加载并执行,即使逆向人员能够通过fishhook或者monkey工程(其原理也是fishhook)搞事情,但我们早在对方代码注入之前,就已经得知有人的debug我们的进程了.
攻防永无止境
是不是我们将防护代码(ptrace和sysctl)转移到framework中就一定能安全了呢?答案是否定的,攻防永远没有尽头,我们的程序代码和framework最终会被build为mach-O文件,而在逆向领域,有很多能够对二进制文件进行分析和修改的工具,下面就大致介绍一下逆向的思路:
- 通过符号断点发现当前进程正在通过
ptrace或sysctl尝试进行防护 bt指令查看当前函数调用栈,找到当前函数的动态库名称和地址image list指令拿到首地址,两两相减拿到偏移值- 通过
Hooper或IDA等工具分析framework的mach-O文件,通过偏移值找到当前的汇编调用指令 - 现在在你眼前能的,就是类似于
bl ptrace这种特别明显的汇编跳转函数指令! - 将指令修改为
nop,即空指令,不做任何操作,直接废掉ptrace和sysctl!
总结:隐藏很重要,发现即胜利
经过几番博弈,我们发现攻防就像两名狙击手,谁先发现对方的位置,就能想办法先发制人,或者绕过去!所以隐藏也是极其重要的防护手段,藏得好,就能让逆向人员耗费更多的精力,大大提高逆向的难度,我们的程序自然也就更加安全.
而关于如何隐藏自己,我们后续再另起一篇进行探究,攻防无止境,大家共勉!