前言
本篇文章继续讨论App的安全防护
的原理,主要讲解关于动态调试
的防护。我们知道,App可以被lldb
动态调试,因为App被设备中的debugserver
附加进程,它会跟踪我们的应用进程(trace process)
,我们可以利用这点,动态的修改App进程中的数据,达到我们想要的结果,而这一过程利用的就是ptrace
函数。
一、ptrace防护
接着上篇文章26-越狱防护的末尾,我们知道👇🏻
-
ptrace(process trace)
其实是系统内核函数
,系统提供一个进程监察和控制
另一个进程,并且还能读取和修改
被控制的进程的内存和寄存器
里的数据,因此它可以决定应用能否被debugserver
附加。 - 如果我们在项目中,调用
ptrace
函数,将程序设置为拒绝附加
,即可对lldb动态调试
进行有效的防护
。
1.1 防护代码示例
- 搭建App项目
antiDebug
- 导入
MyPtraceHeader.h
头文件👇🏻
/*
* Copyright (c) 2000-2005 Apple Computer, Inc. All rights reserved.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. The rights granted to you under the License
* may not be used to create, or enable the creation or redistribution of,
* unlawful or unlicensed copies of an Apple operating system, or to
* circumvent, violate, or enable the circumvention or violation of, any
* terms of an Apple operating system software license agreement.
*
* Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_END@
*/
/* Copyright (c) 1995 NeXT Computer, Inc. All Rights Reserved */
/*-
* Copyright (c) 1984, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* @(#)ptrace.h 8.2 (Berkeley) 1/4/94
*/
#ifndef _SYS_PTRACE_H_
#define _SYS_PTRACE_H_
#include <sys/appleapiopts.h>
#include <sys/cdefs.h>
enum {
ePtAttachDeprecated __deprecated_enum_msg("PT_ATTACH is deprecated. See PT_ATTACHEXC") = 10
};
#define PT_TRACE_ME 0 /* child declares it's being traced */
#define PT_READ_I 1 /* read word in child's I space */
#define PT_READ_D 2 /* read word in child's D space */
#define PT_READ_U 3 /* read word in child's user structure */
#define PT_WRITE_I 4 /* write word in child's I space */
#define PT_WRITE_D 5 /* write word in child's D space */
#define PT_WRITE_U 6 /* write word in child's user structure */
#define PT_CONTINUE 7 /* continue the child */
#define PT_KILL 8 /* kill the child process */
#define PT_STEP 9 /* single step the child */
#define PT_ATTACH ePtAttachDeprecated /* trace some running process */
#define PT_DETACH 11 /* stop tracing a process */
#define PT_SIGEXC 12 /* signals as exceptions for current_proc */
#define PT_THUPDATE 13 /* signal for thread# */
#define PT_ATTACHEXC 14 /* attach to running process with signal exception */
#define PT_FORCEQUOTA 30 /* Enforce quota for root */
#define PT_DENY_ATTACH 31
#define PT_FIRSTMACH 32 /* for machine-specific requests */
__BEGIN_DECLS
int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
__END_DECLS
#endif /* !_SYS_PTRACE_H_ */
- 打开ViewController.m文件,写入以下代码👇🏻
#import "ViewController.h"
#import "MyPtraceHeader.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
@end
- Xcode运行项目,启动后立即退出。使用
ptrace
设置为拒绝附加
,只能手动启动
App。
也就是说,用户在使用App
时一切正常,不会有任何影响。一旦被debugserver
附加,就会闪退
。
越狱情况
以上是非越狱
手机,如果在越狱
机上进行debugserver
附加呢?
- 找到
antiDebug
进程👇🏻
ps -A | grep antiDebug
-
手动
对App进行debugserver
附加
debugserver localhost:12346 -a 15289
-------------------------
debugserver-@(#)PROGRAM:LLDB PROJECT:lldb-900.3.87
for arm64.
Attaching to process 15289...
Segmentation fault: 11
附加失败,无论以何种方式,都会被ptrace
函数阻止。
1.2 破解ptrace 防护
ptrace
是系统内核函数
,被开发者所熟知
。ptrace
的防护痕迹也很明显
👉🏻 手动
运行程序正常
,Xcode
运行程序闪退
。
因此,我们在逆向
一款App
时,遇到上述情况,第一时间就会想到ptrace防护
。那么怎么破解这个呢?
- 从MachO符号角度思考,由于
ptrace
是系统函数,需要间接符号表
,我们可以试探性的下一个ptrace的符号断点
👇🏻
- 运行,
ptrace
的断点命中👇🏻
上图验证了我们的想法,现在确定了对方的防护手段,想要破解并非难事。
- 针对
antiDebug
项目,模拟应用重签名,注入动态库
- 创建
Inject
动态库,创建InjectCode类
- 在
Inject
动态库中,导入fishhook
,导入MyPtraceHeader.h
头文件 - 打开
InjectCode.m
文件,写入以下代码👇🏻
#import "InjectCode.h"
#import "MyPtraceHeader.h"
#import "fishhook.h"
@implementation InjectCode
+(void)load{
struct rebinding reb;
reb.name="ptrace";
reb.replacement=my_ptrace;
reb.replaced=(void *)&sys_ptrace;
struct rebinding rebs[]={reb};
rebind_symbols(rebs, 1);
}
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);
}
@end
在ptrace_my
函数中,如果是PT_DENY_ATTACH
枚举值,直接return返回
。如果是其他类型,系统有特定的作用,需要执行ptrace原始函数
。
- 运行项目,进入lldb动态调试,
ptrace
破解成功!🍺🍺🍺🍺🍺
二、sysctl
由于系统并没有公开ptrace
函数,使用的时候需额外手动引入头文件
,该头文件还不是系统公开的,这点就比较麻烦,那有没有别的函数可以直接使用进行防护呢?当然有,接下来,介绍另一个系统内核函数sysctl
。
2.1 sysctl定义
首先来看看该函数的定义 👇🏻
⚠️注意:使用前需引入系统的头文件 👉🏻
#import <sys/sysctl.h>
int sysctl(int *, u_int, void *, size_t *, void *, size_t);
明显可见有6个参数,分别如下👇🏻
- 查询信息的数组,给它的指针
- 数组中元素的数据类型的大小
- 接收信息结构体的指针
- 接收信息结构体的大小的指针
- 第5个和第6个参数 👉🏻 直接写
0
就行
接下来我们来解释下这几个参数具体的含义。
第一个参数int *
因为是int *
类型,所以该数组的元素是int类型
,代表字节码
的意思,存储的就是调用方需要查询的信息
。例如可以这么写👇🏻
int name[4];//里面放字节码。查询的信息
name[0] = CTL_KERN;//内核查询
name[1] = KERN_PROC;//查询进程
name[2] = KERN_PROC_PID;//传递的参数是进程的ID
name[3] = getpid();//PID的值
第二个参数u_int
u_int
是无符号整型,可以这么计算 👉🏻 sizeof(name)/sizeof(*name)
第三个参数void *
代表一个接收信息的结构体指针,一般使用kinfo_proc
结构体👇🏻
struct kinfo_proc {
struct extern_proc kp_proc; /* proc structure */
struct eproc {
struct proc *e_paddr; /* address of proc */
struct session *e_sess; /* session pointer */
struct _pcred e_pcred; /* process credentials */
struct _ucred e_ucred; /* current credentials */
struct vmspace e_vm; /* address space */
pid_t e_ppid; /* parent process id */
pid_t e_pgid; /* process group id */
short e_jobc; /* job control counter */
dev_t e_tdev; /* controlling tty dev */
pid_t e_tpgid; /* tty process group id */
struct session *e_tsess; /* tty session pointer */
#define WMESGLEN 7
char e_wmesg[WMESGLEN + 1]; /* wchan message */
segsz_t e_xsize; /* text size */
short e_xrssize; /* text rss */
short e_xccount; /* text references */
short e_xswrss;
int32_t e_flag;
#define EPROC_CTTY 0x01 /* controlling tty vnode active */
#define EPROC_SLEADER 0x02 /* session leader */
#define COMAPT_MAXLOGNAME 12
char e_login[COMAPT_MAXLOGNAME]; /* short setlogin() name */
int32_t e_spare[4];
} kp_eproc;
};
其中,我们需要重点关注一个flag
参数,在extern_proc
结构体中👇🏻
有哪些定义呢?往下翻到155行👇🏻
上图红框处的P_TRACED
,看注释就知道,是debug调试跟踪进程信息的,这个就是我们想要的。而且它的值是0x800
,其它的类似0x100,0x200,0x400
,一看就知道是按照byte(位)
定义的枚举
类型,可以多种状态叠加。
使用的时候采用&与操作符
。例如👇🏻
struct kinfo_proc info;//接受查询结果的结构体
(info.kp_proc.p_flag & P_TRACED) != 0 //判断结果中是否包含了P_TRACED状态,即当前App是否正在被第三方动态调试
第四个参数size_t *
size_t info_size = sizeof(info);
⚠️注意:是
size_t *
,所以使用的时候是&info_size
。
完整的使用示例
BOOL isDebugger(){
int name[4];//里面放字节码。查询的信息
name[0] = CTL_KERN;//内核查询
name[1] = KERN_PROC;//查询进程
name[2] = KERN_PROC_PID;//传递的参数是进程的ID
name[3] = getpid();//PID的值
struct kinfo_proc info;//接受查询结果的结构体
size_t info_size = sizeof(info);
if(sysctl(name, 4, &info, &info_size, 0, 0)){
NSLog(@"查询失败");
return NO;
}
//看info.kp_proc.p_flag 的第12位。如果为1,表示调试状态。
//(info.kp_proc.p_flag & P_TRACED)
return ((info.kp_proc.p_flag & P_TRACED) != 0);
}
调用处👇🏻
- (void)viewDidLoad {
[super viewDidLoad];
if (isDebugger()) {
NSLog(@"检测到有调试!");
} else {
NSLog(@"没有调试!");
}
}
进阶版
上面在viewDidLoad
中,只能执行一次,我们更希望定时检查
,所以可以这么改,采用dispatch_source_t
的定时器方式👇🏻
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();
}
运行👇🏻
当然,也不用一直开启定时器检查,在App启动的时候
检查一段时间即可!
与ptrace的不同
-
ptrace
的特点:- 重签名(Xcode)运行之后闪退!
- 手动打开正常运行!
-
sysctl
👉🏻 可扩展性更强,检测到调试后,可以做自己想做的事。
2.2 破解sysctl
因为sysctl
是系统的内核函数,所以我们很自然就想到 👉🏻 fishHook
,因此这么做👇🏻
- 创建一个动态库
inject.framework
,再创建一个类InjectCode
,专门用来hooksysctl
👇🏻
- 实现hook
sysctl
👇🏻
#import "InjectCode.h"
#import "fishhook.h"
#import <sys/sysctl.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)*infosize == sizeof(struct kinfo_proc)) {
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) != 0) {
//使用异或可以取反
myinfo->kp_proc.p_flag ^= P_TRACED;
}
return err;
}
return sysctl_p(name,namelen,info,infosize,newInfo,newInfoSize);
}
+(void)load
{
//交换
rebind_symbols((struct rebinding[1]){{"sysctl",my_sysctl,(void *)&sysctl_p}}, 1);
}
@end
run👇🏻
三、破解
现在我们知道,使用ptrace
和sysctl
2种方式都能检测到当前App是否被调试,那有没有方法能破解
这些检测呢?当然可以。
3.1 动态注入破解
首先,我们要清楚,之所以能检测到被调试,是因为我们在动态库
中使用fishHook
,替换了sysctl
的方式实现,核心在于时机
👉🏻 动态库的load
方法肯定比主工程的早
执行! 只要我们的破解比这个更早
,是不是就能实现了?接下来我们用代码来验证👇🏻
- 同样的,新建一个工程
antiDebug
,在工程中新建动态库antiDebug.framework
- 引入
ptrace
和sysctl
- 引入头文件
MyPtraceHeader.h
- 新建类
antiDebugCode
- 引入头文件
#import "antiDebugCode.h"
#import <sys/sysctl.h>
#import "MyPtraceHeader.h"
#import <mach-o/dyld.h>
#import <mach-o/loader.h>
@implementation antiDebugCode
//检测是否存在调试
BOOL isDebugger(){
int name[4];//里面放字节码。查询的信息
name[0] = CTL_KERN;//内核查询
name[1] = KERN_PROC;//查询进程
name[2] = KERN_PROC_PID;//传递的参数是进程的ID
name[3] = getpid();//PID的值
struct kinfo_proc info;//接受查询结果的结构体
size_t info_size = sizeof(info);
if(sysctl(name, 4, &info, &info_size, 0, 0)){
NSLog(@"查询失败");
return NO;
}
//看info.kp_proc.p_flag 的第12位。如果为1,表示调试状态。
//(info.kp_proc.p_flag & P_TRACED)
return ((info.kp_proc.p_flag & P_TRACED) != 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, ^{
printf("检查INSERT:%s\n",getenv("DYLD_INSERT_LIBREARIES"));
if (isDebugger()) {
// NSLog(@"调试状态!!");
}else{
// NSLog(@"正常!");
}
});
dispatch_resume(timer);
}
+(void)load
{
debugCheck();
//不允许调试附加!
// ptrace(PT_DENY_ATTACH, 0, 0, 0);
//检查Cycript
int count = _dyld_image_count();
for (int i = 0; i < count; i++) {
printf("%s\n",_dyld_get_image_name(i));
}
}
- 新建一个
MonkeyApp
工程MonkeyDemo
,使用上面的antiDebug
工程生成的app包进行重签名,例如👇🏻
- 打开MonkeyApp中
sysctl
的检测功能👇🏻
- run 👇🏻
上图可见,sysctl
的检测功能已被破解!🍺🍺🍺🍺🍺🍺
⚠️ 综上所述,所有的防护手段的关键点在于 👉🏻
提前执行!
3.2 静态破解
接着上面的例子继续,既然App中已经提前预防了ptrace
和sysctl
,那么我们所有的想以注入
的方式去修改代码的这条路肯定就走不通
了,不论是动态库注入
还是静态库注入
,因为你的时机点不可能比App中的还早,是吧!
那有没有别的方式去破防呢?当然也有 👉🏻 静态破解!
我们尝试直接修改它的Mach-O文件(即修改二进制
)。
3.2.1 ptrace静态破解
接下来我们示例演示一下,如何静态调试App中使用的ptrace
检测。
- 首先,我们把上述工程
antiDebug
中的ptrace检测的注释放开👇🏻
- 重新编译生成新的app包
001--antiDebug
,当做我们要破解的App👇🏻
- 新建Monkey工程,取名
test
,重签名app包001--antiDebug
👇🏻
此时,直接run,肯定无法动态调试,会直接崩溃(每次finish running 就会自动断开调试)👇🏻
- 此时我们猜测,是否是ptrace防护了?于是添加符号断点👇🏻
再次run,果然断住了👇🏻
根据调用栈信息,是在[antiDebugCode load]
之中调用的,因为我们本地电脑有antiDebug
的工程源码,并且源码工程并没有去符号
,所以能查看到。真实的情况下,是没有源码的,我们可以通过lldb指令
来查看 👉🏻 bt
查看调用栈👇🏻
一样能找到[antiDebugCode load]
, 是在动态库antiDebug
中,还有动态库的地址0x000000010431fd20
,这个是虚拟地址,包含了偏移量的地址,那如何得到偏移前的地址呢?👉🏻 image list
指令得到首地址来计算!👇🏻
得到了首地址0x0000000104318000
,然后相减得到偏移量0x7D20
。
- 使用hopper打开
antiDebug.framework
的Mach-O二进制文件,搜索偏移量0x7D20
👇🏻
⚠️ 注意:将
antiDebug
拷贝出来,方便查看。
找到了!
- 接着修改该汇编指令 👉🏻
option + A
👇🏻
修改成nop
空指令的意思。
- 导出生成新的二进制👇🏻 (需要将Hopper升级为正版)
替换原有的antiDebug.framework
的Mach-O二进制文件,再次runtest工程
,就不会断开调试了!
以上就是通过修改Mach-O文件的方式,静态暴力
破解ptrace
。
3.2.2 sysctl静态破解
接下来就轮到sysctl
了,还是一样,先打符号断点,看看👇🏻
bt
查看调用栈信息👇🏻
上图可见,在gcd的block之中,,看不到任何的调用的触发点等信息,无法继续往下深究了。
换一种思路,从原始App包入手,Hopper看看Mach-O文件中,搜索sysctl
👇🏻
直接搜索,在真实的工程中这是一个很耗时的过程,因为我们是demo,所以很快就能知道。
主工程
找不到就找动态库
👇🏻
上图就找到了,我们继续往下翻,找到debugCheck
函数👇🏻
继续往下翻,找到___debugCheck_block_invoke
,就是调用sysctl
的地方👇🏻
⚠️ 注意:但是此时还是在
GCD的block
之中,我们还是无法确定App
之中是哪个方法调用的sysctl
。
3.3 破解防护与block
上述sysctl
破解过程中,我们也发现了个好处👇🏻
防护代码写到GCD的block
中执行,即使第三方破解能拿到地址
,也只是block_invoke
调用处的地址,并不是真正的调用的地址,仍然无法继续断点跟进,难以破防,除非你对GCD的源码流程了如指掌。
总结
-
ptrace
- 可阻止
App
被debugserver
附加 - 在iOS系统中,无法直接使用,需要
导入头文件
-
ptrace
函数的定义
◦int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
- 破解
ptrace
◦ 使用ptrace符号断点
试探
◦ 使用fishhook
对ptrace
函数HOOK
◦ 是PT_DENY_ATTACH
枚举值,直接返回。其他类型,执行原始函数
- 可阻止
-
sysctl
- 使用前需引入系统的头文件 👉🏻
#import <sys/sysctl.h>
-
sysctl
函数 👉🏻int sysctl(int *, u_int, void *, size_t *, void *, size_t);
◦ 查询信息的数组,给它的指针
◦ 数组中元素的数据类型的大小
◦ 接收信息结构体的指针 👉🏻kinfo_proc
结构体指针,其中重点关注p_flag
参数,在extern_proc
结构体中
◦ 接收信息结构体的大小的指针
◦ 第5个和第6个参数 👉🏻 直接写0就行 - 与ptrace的不同
◦ptrace
特点 👉🏻重签名
(Xcode)运行之后闪退
!手动打开
正常运行!
◦sysctl
👉🏻可扩展性
更强,检测到调试后,可以做自己想做的事。 - 破解
sysctl
◦ 同样在framework
中使用fishhook
进行方法交换
◦ 判断查询信息的数组
中各个元素的条件是否是追踪当前进程
◦kinfo_proc
结构体的判断 👉🏻(p_flag & P_TRACED) != 0
则p_flag ^= P_TRACED
异或取反
- 使用前需引入系统的头文件 👉🏻
- 破解
- 动态注入破解 👉🏻 在
framework
中新建类,在load
方法中进行ptrace
和sysctl
的防护 - 静态破解
-
ptrace
静态破解
◦ 符号断点得到ptrace
偏移后的地址,image list
得到库的首地址,计算偏移地址offsetAddress
◦ 使用hopper
打开Mach-O
二进制文件,搜索offsetAddress
,并将其对应的汇编指令修改为nop
空指令,导出新的Mach-O
,替换原有的 -
sysctl
静态破解 👉🏻 如果在GCD的Block中执行,则很难破解,因为block_invoke
的地址即使知道,但不清楚GCD底层的调用逻辑
-
- 动态注入破解 👉🏻 在