美文网首页安全防护
28-逆向防护(下)

28-逆向防护(下)

作者: 深圳_你要的昵称 | 来源:发表于2021-08-01 18:47 被阅读0次

    前言

    本篇文章接着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 👉🏻 直接执行汇编代码

    相关文章

      网友评论

        本文标题:28-逆向防护(下)

        本文链接:https://www.haomeiwen.com/subject/dvjxvltx.html