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<UITouch *> *)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 <Foundation/Foundation.h>
#import <CommonCrypto/CommonCrypto.h>

@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<UITouch *> *)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 <dlfcn.h>

@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<UITouch *> *)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 <sys/syscall.h>

真机运行,直接断开,连符号断点也没断住!连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 👉🏻 直接执行汇编代码
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 注意大量的流程混淆会导致上架失败 准备工作 新建一个工程,并新建一个UserInfo 在ViewControlle...
    木扬音阅读 499评论 0 1
  • 1、MachO 其实MachO是一种文件格式,它包含了可执行文件、动态库、静态库、目标文件、dyld等。对于我们编...
    Miss_QL阅读 464评论 0 3
  • 一: ptrace 作用 ptrace系统调从名字上看是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的...
    ldzSpace阅读 12,405评论 1 26
  • 前言 本篇文章继续讨论App的安全防护的原理,主要讲解关于动态调试的防护。我们知道,App可以被lldb动态调试,...
    深圳_你要的昵称阅读 408评论 0 1
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,159评论 2 7