28-逆向防护(下)

前言

本篇文章接着27-逆向防护(上),继续探讨逆向防护的知识点,首先给大家介绍最常用的混淆,然后重点介绍 如何防护fishhook,这整个过程中,如何一步步地优化我们的防护方案。

一、混淆

相信大家对混淆很熟悉,网上有很多现成的脚本可以实现代码混淆的相关功能,当然也很实用,这里不做说明。接下来给大家重点讲解下混淆需要注意的点。

1.1 核心的类名、方法名称的混淆

通常情况下,OC的项目中,我们混淆的方式通常会采用

脚本混淆 统一将类名、方法名用一串随机字符串替换

但是会有个问题,我们创建类的时候,类名文件名其实是一样的,此时如果采用脚本对核心的类名进行混淆的话,可能会将文件名也一起混淆了,这不是我们想要的,那有没有别的方式对核心类名和方法名称进行混淆呢?当然有

利用语法特性 针对OC工程项目,在pch头文件中使用宏定义混淆

宏定义混淆示例
  1. 新建演示工程UserInfoDemo,新建示例Model类UserInfo,添加以下代码
@interface UserInfo : NSObject
-(BOOL)isVipWithAccount:(NSString *)account;
@end

@implementation UserInfo

-(BOOL)isVipWithAccount:(NSString *)account{
    if ([account isEqualToString:@"hank"]) {
        return YES;
    }
    return NO;
}

@end

调用的代码在ViewController.m

#import "ViewController.h"
#import "UserInfo.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    if ([[[UserInfo alloc] init] isVipWithAccount:@"hank123"]) {
        NSLog(@"是VIP");
    }else{
        NSLog(@"不是VIP");
    }
}

@end
  1. 尝试动态调试

我们就当做没有源代码,如何定位到UserInfo类,和它的isVipWithAccount这些核心的名称?

首先我们知道,UserInfoisVipWithAccount方法,通常在类似按钮点击这种情况下触发调用,那么我们可以针对按钮点击的事件下符号断点,在本例中对touchesBegan下符号断点

真机运行,触发断点

这是在系统底层UIKitCore中触发的,继续点击走断点

来到[ViewController touchesBegan:withEvent:]这层,就是页面上触发的时机了,我们看汇编,首地址是0x100f25e28,然后image list查看工程的首地址

工程的首地址是0x0000000100f20000,由此计算得到偏移地址是0x100f25e28 - 0x0000000100f20000 = 0x5E28

根据偏移地址0x5E28,hopper搜索Mach-O二进制文件

类名、方法名称还有传递的入参hank123一目了然!完全明文,对于破解方来说,一下子就定位到了!

宏定义混淆

接下来,我们使用宏定义混淆

  1. 新建pch头文件PrefixHeader.pch,并且配置路径
  1. PrefixHeader.pch中,添加代码,开始混淆
#ifndef PrefixHeader_pch
#define PrefixHeader_pch

#define UserInfo CJKD2534
#define isVipWithAccount  KKLDIU34235

#endif /* PrefixHeader_pch */
  1. 重新编译项目,可以观察到

类名和方法名称全部变色了!包括调用的地方也是

  1. 在以同样的方式动态调试查看偏移地址0x5E28

类名、方法名称都被替换了! 此时想要定位到核心类名和方法名,难度就大了!
尝试符号断点,查看调用栈,也是宏定义替换后的结果,一脸懵逼,头疼!
由此可见,宏定义混淆相对于脚本混淆的优点在于

代码不需要改变,项目无污染,轻量级!

1.2 常量的混淆

细心的你会发现,入参值hank123仍然可以看到,在我们的开发场景中,也存在一些敏感信息,需要作为入参传递,但是不想被破解,那么如何解决呢?第一时间想到的就是加密,接下来我们采用AES对称加密算法,解决下面的示例

  1. 首先AES/DES对称加密的算法代码如下
  • EncryptionTools.h
#import 
#import 

@interface EncryptionTools : NSObject

+ (instancetype)sharedEncryptionTools;

/**
 @constant   kCCAlgorithmAES     高级加密标准,128位(默认)
 @constant   kCCAlgorithmDES     数据加密标准
 */
@property (nonatomic, assign) uint32_t algorithm;

/**
 *  加密字符串并返回base64编码字符串
 *
 *  @param string    要加密的字符串
 *  @param keyString 加密密钥
 *  @param iv        初始化向量(8个字节)
 *
 *  @return 返回加密后的base64编码字符串
 */
- (NSString *)encryptString:(NSString *)string keyString:(NSString *)keyString iv:(NSData *)iv;

- (NSString *)encryptString:(NSString *)string;

/**
 *  解密字符串
 *
 *  @param string    加密并base64编码后的字符串
 *  @param keyString 解密密钥
 *  @param iv        初始化向量(8个字节)
 *
 *  @return 返回解密后的字符串
 */
- (NSString *)decryptString:(NSString *)string keyString:(NSString *)keyString iv:(NSData *)iv;

- (NSString *)decryptString:(NSString *)string;

@end
  • EncryptionTools.m
#import "EncryptionTools.h"


@interface EncryptionTools()
@property (nonatomic, assign) int keySize;
@property (nonatomic, assign) int blockSize;
@property (nonatomic, copy, readwrite) NSString *key;
@end

@implementation EncryptionTools

+ (instancetype)sharedEncryptionTools {
    static EncryptionTools *instance;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
        instance.algorithm = kCCAlgorithmAES;
    });
    
    return instance;
}

- (void)setAlgorithm:(uint32_t)algorithm {
    _algorithm = algorithm;
    switch (algorithm) {
        case kCCAlgorithmAES:
        self.keySize = kCCKeySizeAES128;
        self.blockSize = kCCBlockSizeAES128;
        break;
        case kCCAlgorithmDES:
            self.keySize = kCCKeySizeDES;
            self.blockSize = kCCBlockSizeDES;
        break;
        default:
        break;
    }
}

- (NSString *)encryptString:(NSString *)string {
    // 生成>=24位的key
    if (self.key == nil || self.key.length == 0) {
        NSMutableString *randomString = [NSMutableString stringWithCapacity:24];
        for (int i = 0; i < 24; i++) {
            [randomString appendFormat: @"%C", [kRandomAlphabet characterAtIndex:arc4random_uniform((u_int32_t)[kRandomAlphabet length])]];
        }
        self.key = randomString;
        NSLog(@"=-=-DES.key = %@", self.key);
    }
    
    NSString *ivStr = @"00000000";
    return [self encryptString:string keyString:self.key iv:[ivStr dataUsingEncoding:NSUTF8StringEncoding]];
}
    
- (NSString *)encryptString:(NSString *)string keyString:(NSString *)keyString iv:(NSData *)iv {
    // 设置秘钥
    NSData *keyData = [keyString dataUsingEncoding:NSUTF8StringEncoding];
    uint8_t cKey[self.keySize];
    bzero(cKey, sizeof(cKey));
    [keyData getBytes:cKey length:self.keySize];
    
    // 设置iv
    uint8_t cIv[self.blockSize];
    bzero(cIv, self.blockSize);
    int option = 0;
    if (iv) {
        [iv getBytes:cIv length:self.blockSize];
        option = kCCOptionPKCS7Padding;
    } else {
        option = kCCOptionPKCS7Padding | kCCOptionECBMode;
    }
    
    // 设置输出缓冲区
    NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
    size_t bufferSize = [data length] + self.blockSize;
    void *buffer = malloc(bufferSize);
    
    // 开始加密
    size_t encryptedSize = 0;
    //加密解密都是它 -- CCCrypt
    CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
                                          self.algorithm,
                                          option,
                                          cKey,
                                          self.keySize,
                                          cIv,
                                          [data bytes],
                                          [data length],
                                          buffer,
                                          bufferSize,
                                          &encryptedSize);
    
    NSData *result = nil;
    if (cryptStatus == kCCSuccess) {
        result = [NSData dataWithBytesNoCopy:buffer length:encryptedSize];
    } else {
        free(buffer);
        NSLog(@"[错误] 加密失败|状态编码: %d", cryptStatus);
    }
    
    return [result base64EncodedStringWithOptions:0];
}

- (NSString *)decryptString:(NSString *)string {
    NSString *ivStr = @"00000000";
    return [self decryptString:string keyString:self.key iv:[ivStr dataUsingEncoding:NSUTF8StringEncoding]];
}
    
- (NSString *)decryptString:(NSString *)string keyString:(NSString *)keyString iv:(NSData *)iv {
    
    // 设置秘钥
    NSData *keyData = [keyString dataUsingEncoding:NSUTF8StringEncoding];
    uint8_t cKey[self.keySize];
    bzero(cKey, sizeof(cKey));
    [keyData getBytes:cKey length:self.keySize];
    
    // 设置iv
    uint8_t cIv[self.blockSize];
    bzero(cIv, self.blockSize);
    int option = 0;
    if (iv) {
        [iv getBytes:cIv length:self.blockSize];
        option = kCCOptionPKCS7Padding;
    } else {
        option = kCCOptionPKCS7Padding | kCCOptionECBMode;
    }
    
    // 设置输出缓冲区
    NSData *data = [[NSData alloc] initWithBase64EncodedString:string options:0];
    size_t bufferSize = [data length] + self.blockSize;
    void *buffer = malloc(bufferSize);
    
    // 开始解密
    size_t decryptedSize = 0;
    CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
                                          self.algorithm,
                                          option,
                                          cKey,
                                          self.keySize,
                                          cIv,
                                          [data bytes],
                                          [data length],
                                          buffer,
                                          bufferSize,
                                          &decryptedSize);
    
    NSData *result = nil;
    if (cryptStatus == kCCSuccess) {
        result = [NSData dataWithBytesNoCopy:buffer length:decryptedSize];
    } else {
        free(buffer);
        NSLog(@"[错误] 解密失败|状态编码: %d", cryptStatus);
    }
    
    return [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding];
}

@end
  1. 我们使用上面的加密类EncryptionTools进行加密,在UserInfo类中添加发送信息的方法,对发送的信息进行加密
@interface UserInfo : NSObject
-(BOOL)isVipWithAccount:(NSString *)account;
-(void)sendWithUserInfo:(NSString *)info;
@end

NSString * const AES_KEY = @"IU**YD#$%()*";
@implementation UserInfo

-(BOOL)isVipWithAccount:(NSString *)account{
    if ([account isEqualToString:@"hank"]) {
        return YES;
    }
    return NO;
}

//给服务器一些敏感的信息
-(void)sendWithUserInfo:(NSString *)info{
    NSLog(@"加密之后%@",[[EncryptionTools sharedEncryptionTools] encryptString:info keyString:AES_KEY iv:nil]);
}

@end

调用的地方,将判断账户hank123改为hank,满足发送信息的条件

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    
    UserInfo * user = [[UserInfo alloc] init];
    if ([user isVipWithAccount:@"hank"]) {
        [user sendWithUserInfo:@"some msg"];
        NSLog(@"是VIP");
    }else{
        NSLog(@"不是VIP");
    }
}
  1. 再增加3个宏定义,混淆加密的类名EncryptionTools和方法名encryptString keyString
#define EncryptionTools  KKLDIU32035
#define encryptString  KOIE76875
#define keyString  JUIIYT8776
  1. 运行查看Mach-O文件

上图可见,我们用Hopper查看Mach-O文件,在方法sendWithUserInfo中,看到了加密的密钥信息,这点就很危险了,这个密钥信息就是项目的核心代码,当然是不能暴露出来的。

  1. 接下来,我们想办法隐藏这个密钥信息,通过下面的方式
#define KEY 0xAC
static NSString * AES_KEYINFO(){
    //这种方式能够让这些字符串不进入常量区。
    unsigned char key[] = {
        (KEY ^ 'I'),
        (KEY ^ 'U'),
        (KEY ^ '&'),
        (KEY ^ '*'),
        (KEY ^ '('),
        (KEY ^ '$'),
        (KEY ^ '%'),
        (KEY ^ ')'),
        (KEY ^ '\0')
    };
    unsigned char * p = key;
    while (((*p) ^= KEY) != '\0') p++;
    
    return [NSString stringWithUTF8String:(const char *)key];
}

以上通过以字符为单位,遍历^异或固定地址KEY的方式,生成了密钥keyString。然后调用密钥的地方这么写

//给服务器一些敏感的信息
-(void)sendWithUserInfo:(NSString *)info{
    NSLog(@"加密之后%@",[[EncryptionTools sharedEncryptionTools] encryptString:info keyString:AES_KEYINFO() iv:nil]);
}

将之前的AES_KEY改为AES_KEYINFO()

  1. 再次查看Mach-O文件

sendWithUserInfo中就能看到AES_KEYINFO,继续跟进查看

AES_KEYINFO中的汇编,就看不到密钥信息了,证明我们成功的将之前的AES_KEY从常量区移除了!

小结

上述示例中,我们发现:模拟网络请求,对敏感数据进行对称加密时,存在漏洞

对称加密的密钥key,可以在寄存器中读取!

我们通过符号断点 + Mach-O动态调试+静态分析的方式,根据调用栈查汇编代码的执行流程,最终能查找到对称加密的密钥key,这点就是灾难了,必须解决。

解决措施

  1. 混淆方法名称,类名称
  2. 函数替换全局常量定义的方式
  3. 通过^异或计算的方式 移除常量区

⚠️ 注意:大量的流程的混淆,会导致无法上线

如果你大量混淆了很多流程的代码,苹果在审核的时候就能检测到,会导致你App上线失败!所以我们平时只能对一些很关键的流程做混淆。

二、fishhook的防护

上篇27-逆向防护(上)中对ptrace防护做了介绍,并且通过fishhok的方式可以破解ptrace防护,我们接着这个点继续深究,如何做到 破解你的fishhok防护,让ptrace继续有效?

2.1 dlopen函数

之前的ptrace示例逻辑是这样

  1. fishhook的代码
  1. 调用的代码

这样真机运行调试起来不会断开

  1. 我们改下调用的代码
#import "ViewController.h"
#import "MyPtraceHeader.h"
#import 

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //通过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);
    
    ptrace_p = dlsym(handle, "ptrace");
    if (ptrace_p) {
        ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
    }
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"正常运行!!!");
}


@end

通过dlopen的方式,通过ptrace所在的动态库的地址拿到句柄,然后dlsym的方式通过ptrace字符串构造ptrace方法并调用,达到防护的目的。

  1. 能否做到防护fishhook呢? 通过MachOView查看,Lazy表里没有ptrace符号

所以fishhook失效,真机一运行,会自动断开!

2.2 破解dlopen

接下来,我们以破解者的身份,看看如何破解dlopen

  1. Hopper查看,能否找到"ptrace"字符串

全局搜索

能找到,那么就能静态修改 可以nop,也可以将字符串改成别的值,这样就能绕过prace的调用,达到破解的目的。那么如何避免呢?接着往下看。

  1. 利用上面的常量的混淆所使用的^(异或)地址的方式,提前改掉"ptrace"字符串,这样破解方者则无法找到"ptrace"字符串,代码如下
- (void)viewDidLoad {
    [super viewDidLoad];
    
    //拼接一个 ptrace
    unsigned char funcStr[] = {
        ('a' ^ 'p'),
        ('a' ^ 't'),
        ('a' ^ 'r'),
        ('a' ^ 'a'),
        ('a' ^ 'c'),
        ('a' ^ 'e'),
        ('a' ^ '\0'),
    };
    unsigned char * p = funcStr;
    while (((*p) ^= 'a') != '\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);
    
    ptrace_p = dlsym(handle, (const char *)funcStr);
    if (ptrace_p) {
        ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
    }
}

继续Hopper查看搜索ptrace,就找不到了!

2.3 破解你的破解

既然上面能防护dlopen的破解,那接下来再破解你的这个防护!

  1. ptrace符号断点

真机运行断住,查看调用栈

那么调用的地址是0x1041ca5b0

  1. 通过image list获取首地址,计算偏移地址

首地址是0x00000001041c4000,那么偏移地址 0x1041ca5b0 - 0x00000001041c4000 = 0x65B0

  1. Hopper搜索0x65B0

以上就是对抗fishhook

2.4 对抗完美方案

以上2.3中是通过下ptrace符号断点,找地址,再根据地址,在Mach-O里面分析,找到ptrace的调用指令,直接nop一下,达到破解目的。但是这样的方案并不是完美的,接下来我们看看完美的方案

使符号断点失效,破解方无从下手!

GCD 尝试

在研究完美方案之前,我们尝试用GCD,在block中调用对抗代码,看看是什么效果。

  • 尝试一:在dispatch_after的block中执行
- (void)viewDidLoad {
    [super viewDidLoad];
    
    //拼接一个 ptrace
    unsigned char funcStr[] = {
        ('a' ^ 'p'),
        ('a' ^ 't'),
        ('a' ^ 'r'),
        ('a' ^ 'a'),
        ('a' ^ 'c'),
        ('a' ^ 'e'),
        ('a' ^ '\0'),
    };
    unsigned char * p = funcStr;
    while (((*p) ^= 'a') != '\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);
    
    ptrace_p = dlsym(handle, (const char *)funcStr);
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (ptrace_p) {
            ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
        }
    });
}

下符号断点ptrace,可以看到

上图可见 可以确定是在_block_invoke中执行的ptrace

接着查看Mach-O
偏移地址是0x100982560 - 首地址 0x000000010097c000 = 0x6560

一样可以定位到这个blr跳转指令,所以也可以修改为nop指令,达到绕开prace的目的,结论 dispatch_after方式无效!

  • 尝试二:改为全局队列中执行
- (void)viewDidLoad {
    [super viewDidLoad];
    
    antyDebug();
}

void antyDebug () {
    //拼接一个 ptrace
    unsigned char funcStr[] = {
        ('a' ^ 'p'),
        ('a' ^ 't'),
        ('a' ^ 'r'),
        ('a' ^ 'a'),
        ('a' ^ 'c'),
        ('a' ^ 'e'),
        ('a' ^ '\0'),
    };
    unsigned char * p = funcStr;
    while (((*p) ^= 'a') != '\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);
    
    ptrace_p = dlsym(handle, (const char *)funcStr);
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),dispatch_get_global_queue(0, 0), ^{
        if (ptrace_p) {
            ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
        }
    });
}

我们再去个本地符号, buildSetting中去符号化,strip

断点一样可以定位到

既然能定位地址,所以还是一样可以利用hopper修改地址所对应的汇编指令,仍然无效!

  • 尝试三:在dispatch_source_t中的block执行
    既然dispatch_after不行,我们换用定时器dispatch_source_t看看
- (void)viewDidLoad {
    [super viewDidLoad];
    
    antyDebug();
}


static dispatch_source_t timer;

void antyDebug () {
    //拼接一个 ptrace
    unsigned char funcStr[] = {
        ('a' ^ 'p'),
        ('a' ^ 't'),
        ('a' ^ 'r'),
        ('a' ^ 'a'),
        ('a' ^ 'c'),
        ('a' ^ 'e'),
        ('a' ^ '\0'),
    };
    unsigned char * p = funcStr;
    while (((*p) ^= 'a') != '\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);
    
    ptrace_p = dlsym(handle, (const char *)funcStr);
    
    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 (ptrace_p) {
            ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
        }
    });
    dispatch_resume(timer);
}

run,仍然可以定位到block_invoke的地址

  • 尝试四:仍然使用dispatch_after
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        antyDebug();
    });
    
}

void antyDebug () {
    //拼接一个 ptrace
    unsigned char funcStr[] = {
        ('a' ^ 'p'),
        ('a' ^ 't'),
        ('a' ^ 'r'),
        ('a' ^ 'a'),
        ('a' ^ 'c'),
        ('a' ^ 'e'),
        ('a' ^ '\0'),
    };
    unsigned char * p = funcStr;
    while (((*p) ^= 'a') != '\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);
    
    ptrace_p = dlsym(handle, (const char *)funcStr);
    if (ptrace_p) {
        ptrace_p(PT_DENY_ATTACH, 0, 0, 0 );
    }
}

但是strip去符号得注意

  1. HankHook动态库是去debug调试符号
  1. 对主工程是去所有符号

再次运行

调用栈中就无法找到block_invoke的地址了!

但是,注意一个细节,XCode会过滤调用栈的一些信息

取消这个选择的过滤,看看

仍然能定位到调用的地址!

结论:GCD的Block执行对抗代码 有待研究!

使符号断点失效

以上我们通过GCD的尝试,最终以失败告终。但是,我们从中也得到一个启示

GCD的Block无效,是因为我们下了ptrace符号断点,这个符号断点一直能断住,就能锁定地址!

顺着该思路,那能否不触发符号断点?当然能

执行syscall 不会触发符号断点!

代码很简单,就一句

需引入头文件#import

真机运行,直接断开,连符号断点也没断住!连fishhook都没法hook住!

syscall
    /**
     1、编号,你要调用哪个系统函数
     2、后面都是参数!
     */
    syscall(26,31,0,0,0);

  • 第一个参数26的意思

26就是ptrace,所以真机一运行就断开,和ptrace的特点一模一样!唯一不同的是 不会触发符号断点

  • 能否hooksyscall
    首先查看syscall是否在间接符号表中,因为fishhook就是hook间接符号表中的符号

上图可见,有符号,那么就能使用fishhook hooksyscall,那怎么防住fishhook呢?上面我们讲过,将syscall使用dlopen dlsym移除常量区,可以做到防护fishhook,这里就无限套娃了。

汇编模式

我们不想通过移除常量区去防护fishhook,说白了,就是syscall能做到防止fishhook防护ptrace,但是无法防护自己!那有没有别的方式?

使用汇编代码执行syscall

- (void)viewDidLoad {
    [super viewDidLoad];

//    ptrace(PT_DENY_ATTACH, 0, 0, 0);
    
    //syscall
    /**
     1、编号,你要调用哪个系统函数
     2、后面都是参数!
     */
//    syscall(26,31,0,0,0);
    
    
    //相当于是调用syscall
    asm volatile(
                 "mov x0,#26\n"
                 "mov x1,#31\n"
                 "mov x2,#0\n"
                 "mov x3,#0\n"
                 "mov x4,#0\n"
                 "mov x16,#0\n"//这里就是syscall的编号
                 "svc #0x80\n"//这条指令就是触发中断(系统级别的跳转!)
                 );
}

既然汇编能执行syscall,同样,也能直接执行ptrace

    //下面就是直接调用ptrace
    asm volatile(
                 "mov x0,#31\n"
                 "mov x1,#0\n"
                 "mov x2,#0\n"
                 "mov x3,#0\n"
                 "mov x16,#26\n"//这里26就是ptrace
                 "svc #0x80\n"//这条指令就是触发中断(系统级别的跳转!)
                 );

这种汇编的模式,想要破解的话,就是只能全局搜索svc指令,通过上下文分析汇编代码,得出它所执行的功能。所以,没有绝对的防护!

总结

  • 混淆
    • 可使用脚本进行统一的混淆
    • 关键类名、方法名称的混淆 宏定义混淆
    • 常量的混淆 移除常量区
      ◦ char数组遍历
      ◦ ^异或固定地址
  • fishhook的防护
    • dlopen 传入ptrace所在动态库的地址,得到句柄
    • dlsym 通过句柄ptrace字符串,得到ptrace的函数调用指针,直接调用
    • 破解dlopen 常量的混淆方式破解
    • 防护破解dlopen
      ◦ 下ptrace符号断点,计算出偏移地址
      ◦ 根据偏移地址,Hopper检索出汇编指令,改为nop ,但并不完美
    • 完美对抗fishhook
      GCD + strip去符号 有待研究!
      符号断点失效
      • syscall 第一个参数26代表SYS_ptrace1代表SYS_exit
      • 防护syscall 直接执行汇编代码

你可能感兴趣的:(28-逆向防护(下))