27-逆向防护(上)

前言

本篇文章继续讨论App的安全防护的原理,主要讲解关于动态调试的防护。我们知道,App可以被lldb动态调试,因为App被设备中的debugserver附加进程,它会跟踪我们的应用进程(trace process),我们可以利用这点,动态的修改App进程中的数据,达到我们想要的结果,而这一过程利用的就是ptrace函数。

一、ptrace防护

接着上篇文章26-越狱防护的末尾,我们知道

  1. ptrace(process trace)其实是系统内核函数,系统提供一个进程监察和控制另一个进程,并且还能读取和修改被控制的进程的内存和寄存器里的数据,因此它可以决定应用能否被debugserver附加。
  2. 如果我们在项目中,调用ptrace函数,将程序设置为拒绝附加,即可对lldb动态调试进行有效的防护

1.1 防护代码示例

  1. 搭建App项目antiDebug
  2. 导入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 
#include 

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_ */

  1. 打开ViewController.m文件,写入以下代码
#import "ViewController.h"
#import "MyPtraceHeader.h"

@implementation ViewController

- (void)viewDidLoad {
   [super viewDidLoad];
   ptrace(PT_DENY_ATTACH, 0, 0, 0);
}

@end
  1. Xcode运行项目,启动后立即退出。使用ptrace设置为拒绝附加,只能手动启动App。
    也就是说,用户在使用App时一切正常,不会有任何影响。一旦被debugserver附加,就会闪退
越狱情况

以上是非越狱手机,如果在越狱机上进行debugserver附加呢?

  1. 找到antiDebug进程
ps -A | grep antiDebug
  1. 手动对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防护。那么怎么破解这个呢?

  1. 从MachO符号角度思考,由于ptrace是系统函数,需要间接符号表,我们可以试探性的下一个ptrace的符号断点
  1. 运行,ptrace的断点命中

上图验证了我们的想法,现在确定了对方的防护手段,想要破解并非难事。

  1. 针对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原始函数

  1. 运行项目,进入lldb动态调试,ptrace破解成功!

二、sysctl

由于系统并没有公开ptrace函数,使用的时候需额外手动引入头文件,该头文件还不是系统公开的,这点就比较麻烦,那有没有别的函数可以直接使用进行防护呢?当然有,接下来,介绍另一个系统内核函数sysctl

2.1 sysctl定义

首先来看看该函数的定义

⚠️注意:使用前需引入系统的头文件 #import

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

明显可见有6个参数,分别如下

  1. 查询信息的数组,给它的指针
  2. 数组中元素的数据类型的大小
  3. 接收信息结构体的指针
  4. 接收信息结构体的大小的指针
  5. 第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的特点:
    1. 重签名(Xcode)运行之后闪退!
    2. 手动打开正常运行!
  • sysctl 可扩展性更强,检测到调试后,可以做自己想做的事。

2.2 破解sysctl

因为sysctl是系统的内核函数,所以我们很自然就想到 fishHook,因此这么做

  1. 创建一个动态库inject.framework,再创建一个类InjectCode,专门用来hooksysctl
  1. 实现hooksysctl
#import "InjectCode.h"
#import "fishhook.h"
#import 


@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

三、破解

现在我们知道,使用ptracesysctl2种方式都能检测到当前App是否被调试,那有没有方法能破解这些检测呢?当然可以。

3.1 动态注入破解

首先,我们要清楚,之所以能检测到被调试,是因为我们在动态库中使用fishHook,替换了sysctl的方式实现,核心在于时机 动态库的load方法肯定比主工程的执行! 只要我们的破解比这个更早,是不是就能实现了?接下来我们用代码来验证

  1. 同样的,新建一个工程antiDebug,在工程中新建动态库antiDebug.framework
  2. 引入ptracesysctl
    • 引入头文件MyPtraceHeader.h
    • 新建类antiDebugCode
#import "antiDebugCode.h"
#import 
#import "MyPtraceHeader.h"
#import 
#import 

@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));
    }
    
}
  1. 新建一个MonkeyApp工程MonkeyDemo,使用上面的antiDebug工程生成的app包进行重签名,例如
  1. 打开MonkeyApp中sysctl的检测功能
  1. run

上图可见,sysctl的检测功能已被破解!

⚠️ 综上所述,所有的防护手段的关键点在于 提前执行!

3.2 静态破解

接着上面的例子继续,既然App中已经提前预防了ptracesysctl,那么我们所有的想以注入的方式去修改代码的这条路肯定就走不通了,不论是动态库注入还是静态库注入,因为你的时机点不可能比App中的还早,是吧!

那有没有别的方式去破防呢?当然也有 静态破解!我们尝试直接修改它的Mach-O文件(即修改二进制)。

3.2.1 ptrace静态破解

接下来我们示例演示一下,如何静态调试App中使用的ptrace检测。

  1. 首先,我们把上述工程antiDebug中的ptrace检测的注释放开
  1. 重新编译生成新的app包001--antiDebug,当做我们要破解的App
  1. 新建Monkey工程,取名test,重签名app包001--antiDebug

此时,直接run,肯定无法动态调试,会直接崩溃(每次finish running 就会自动断开调试)

  1. 此时我们猜测,是否是ptrace防护了?于是添加符号断点

再次run,果然断住了

根据调用栈信息,是在[antiDebugCode load] 之中调用的,因为我们本地电脑有antiDebug的工程源码,并且源码工程并没有去符号,所以能查看到。真实的情况下,是没有源码的,我们可以通过lldb指令来查看 bt查看调用栈

一样能找到[antiDebugCode load], 是在动态库antiDebug中,还有动态库的地址0x000000010431fd20,这个是虚拟地址,包含了偏移量的地址,那如何得到偏移前的地址呢? image list指令得到首地址来计算!

得到了首地址0x0000000104318000,然后相减得到偏移量0x7D20

  1. 使用hopper打开antiDebug.framework的Mach-O二进制文件,搜索偏移量0x7D20

⚠️ 注意:将antiDebug拷贝出来,方便查看。

找到了!

  1. 接着修改该汇编指令 option + A

修改成nop空指令的意思。

  1. 导出生成新的二进制 (需要将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
    • 可阻止Appdebugserver附加
    • 在iOS系统中,无法直接使用,需要导入头文件
    • ptrace函数的定义
      int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
    • 破解ptrace
      ◦ 使用ptrace符号断点试探
      ◦ 使用fishhookptrace函数HOOK
      ◦ 是PT_DENY_ATTACH枚举值,直接返回。其他类型,执行原始函数
  • sysctl
    • 使用前需引入系统的头文件 #import
    • 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) != 0p_flag ^= P_TRACED异或取反
  • 破解
    • 动态注入破解 在framework中新建类,在load方法中进行ptracesysctl的防护
    • 静态破解
      • ptrace静态破解
        ◦ 符号断点得到ptrace偏移后的地址,image list得到库的首地址,计算偏移地址offsetAddress
        ◦ 使用hopper打开Mach-O二进制文件,搜索offsetAddress,并将其对应的汇编指令修改为nop空指令,导出新的Mach-O,替换原有的
      • sysctl静态破解 如果在GCD的Block中执行,则很难破解,因为block_invoke的地址即使知道,但不清楚GCD底层的调用逻辑

你可能感兴趣的:(27-逆向防护(上))