iOS APP安全保护

代码下载:https://github.com/ZhangJingHao/ZJHAppSafeGuard.git

一、字符串加密

很多时候,可执行文件中的字符串信息,对破解者来说,非常关键,是破解的捷径之一。为了加大破解、逆向难度,可以考虑对字符串进行加密。字符串的加密技术有很多种,可以根据自己的需要自行制定算法

这里举一个简单的例子,对每个字符进行异或处理,需要使用字符串时,对异或过的字符再进行一次异或,就可以获得原字符

1、加密前

直接在源代码里面编写字符串,使用hopper可直接找到该串

字符串加密前.png

2、加密后

混淆后的代码已经看不到硬编码了。
参考链接:https://www.jianshu.com/p/49e98b8a05fd

字符串加密后.png

3、封装加密方法

对加密方法进行封装,提高代码的可读性
参考链接:https://github.com/CoderMJLee/MJCodeObfuscation

字符串封装加密方法.png

二、代码混淆

1、混淆说明

iOS程序可以通过class-dump、Hopper、IDA等获取类名、方法名、以及分析程序的执行逻辑。如果进行代码混淆,可以加大别人的分析难度

可通过宏定义的方式对程序进行混淆,例

#ifndef MJCodeObfuscation_h
#define MJCodeObfuscation_h

// 混淆类名
#define ZJHPerson CsPTLrkanhBbQAPL

// 混淆属性
#define zjh_name HrZLzcgSoPhwMBwW

// 混淆方法名
#define zjh_test _DU_TJLoLaiRpXAv
#define zjh_run KmJHtapxjqnqLtGp

// 可以使用正常的方法名替换原有方法名,减少特殊字符的使用
// 避免大量使用特殊字符,导致审核被拒,如
#define showAlertView dismissSheetView

#endif

2、混淆前后效果对比

代码混淆前后对比.png

3、注意点

  • 不能混淆系统方法
  • 不能混淆init开头的等初始化方法
  • 混淆属性时需要额外注意set方法
  • 如果xib、storyboard中用到了混淆的内容,需要手动修正
  • 可以考虑把需要混淆的符号都加上前缀,跟系统自带的符号进行区分
  • 混淆过多可能会被AppStore拒绝上架,需要说明用途
  • 可以使用正常的方法名替换原有方法名,减少特殊字符的使用
  • 建议给需要混淆的符号加上了一个特定的前缀

三、网络数据加密

1、对称加密

需要对加密和解密使用相同密钥的加密算法。由于其速度快,对称性加密通常在消息发送方需要加密大量数据时使用。对称性加密也称为密钥加密。常用的非对称加密有DES和AES。

优点:对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。

缺点:在数据传送前,发送方和接收方必须商定好秘钥,然后使双方都能保存好秘钥。其次如果一方的秘钥被泄露,那么加密信息也就不安全了.

可以用混淆敏感字符串的方式对密钥进行存储,原理同本文第一条。

// DES + Base64 数据加密
- (void)DESRequestWithData:(NSData *)dictData {
    NSString *dictStr = [[NSString alloc] initWithData:dictData
                                              encoding:NSUTF8StringEncoding];
    
    // 混淆 DES 的 Key
    NSString *keyStr = [_WKCodeConfused getApiSecretKey];
    // 3DES + Base64 数据加密处理
    NSString *encryptStr = [DES3Util encryptUseDES:dictStr key:keyStr];
    
    NSData *encryptData = [encryptStr dataUsingEncoding:NSUTF8StringEncoding];
    [self requestWithData:encryptData];
}
明文传输.png

DES+Base64加密.png

Base64编码原理:https://blog.csdn.net/wo541075754/article/details/81734770

2、非对称加密

非对称加密算法需要两个密钥来进行加密和解密,这两个秘钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)。

优点:与对称加密相比,其安全性更好:对称加密的通信双方使用相同的秘钥,如果一方的秘钥遭泄露,那么整个通信就会被破解。而非对称加密使用一对秘钥,一个用来加密,一个用来解密,而且公钥是公开的,秘钥是自己保存的,不需要像对称加密那样在通信之前要先同步秘钥。

缺点:非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。

// RSA 加密
- (void)RSARequestWithData:(NSData *)data {
    AMRSACryptor *rsaObj = [[AMRSACryptor alloc] init];
    
    // 公钥文件路径
    NSString *publicPath = [[NSBundle mainBundle] pathForResource:@"rsacert" ofType:@"der"];
    [rsaObj loadPublicKey:publicPath];
    // 使用公钥加密数据
    NSData *encryptData = [rsaObj RSAEncryptData:data];
    [self requestWithData:encryptData];

    // 私钥文件路径
    NSString *privatePath = [[NSBundle mainBundle] pathForResource:@"p" ofType:@"p12"];
    [rsaObj loadPrivateKey:privatePath password:@"123456"];
    // 使用私钥解密
    NSData *decryptData = [rsaObj RSADecryptData:encryptData];
    NSDictionary *dictionary =[NSJSONSerialization JSONObjectWithData:decryptData options:NSJSONReadingMutableLeaves error:nil];
    NSLog(@"dictionary : %@", dictionary);
}

RSA加密.png

3、传输data加密

在传输 Data 的时候可以对其加密,首先将data转换为byte, 然后对byte进行操作,比如将所有字符与 0xA8 异或。这样比人截取data,看到的是一推乱码

// 传输 Data 加密
- (void)transmitRequestWithData:(NSData *)data {
    
    // 首先将data转换为byte, 然后对byte进行操作
    NSUInteger lengthTemp = [data length];
    char *bytesTemp = malloc(lengthTemp);
    [data getBytes:bytesTemp length:lengthTemp];
    
    // 比如将所有字符与 0xA8 异或
    for (int i=0; i<lengthTemp; i++) {
        bytesTemp[i]^=0xA8;
    }
    
    NSData *encryptData = [[NSData alloc] initWithBytes:bytesTemp length:lengthTemp];
    
    [self requestWithData:encryptData];
}
传输data加密.png

iOS常用加密算法汇总:ZJHSecurityTool

4、SSL Pinning

SSL Pinning(又叫Certificate Pinning)可以理解为证书绑定。在一些应用场景中,客户端和服务器之间的通信是事先约定好的,既服务器地址和证书是预先知道的,这种情况常见于CS(Client-Server)架构的应用中。这样的话在客户端事先保存好一份服务器的证书(含公钥),每次请求服务器的时候,将服务器返回的证书与客户端保存的证书进行对比,如果证书不符,说明受到中间人攻击,马上可以中断请求。这样的话中间人就无法伪造证书进行攻击了。

在我们AFNetworking中,可以这样使用:

+ (AFHTTPSessionManager *)manager
{
    static AFHTTPSessionManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        manager =  [[AFHTTPSessionManager alloc] initWithSessionConfiguration:config];

        AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey withPinnedCertificates:[AFSecurityPolicy certificatesInBundle:[NSBundle mainBundle]]];
        manager.securityPolicy = securityPolicy;
    });
    return manager;
}

效果下入:

使用SLL Pinning后.png

参考链接:https://www.jianshu.com/p/2c5c8bc55f54

5、小结

对普通请求、返回数据,可使用DES对称加密的方式,密钥使用敏感字符串混淆的方式存储。

对于重要数据,使用RSA进行数字签名,起到防篡改作;对于比较敏感的数据,如用户信息(登陆、注册等),客户端发送使用RSA加密,服务器返回使用DES(AES)加密;

首先移动端给服务器传递通过RSA公钥加密后的数据,参数包括DES的密钥(密钥是随机生成的八位字符串) 和 相关参数信息,服务器通过私钥解密信息数据,里面包括DES密钥和 相关参数信息,服务器再通过此DES密钥加密返回数据给移动端,移动端通过此DES密钥进行解密获取数据。(注:DES的密钥每一次都要重新随机生成,也就是一个密钥只完成这一次的数据传递)

RSA+DES 网络数据加密.png

四、插件校验

1、常规校验方式

客服端发起获取插件信息请求,服务端返回插件url地址和对应zip的MD5值,客户端根据url下载zip文件,下载完成后,对zip文件进行md5取值,与服务器返回的md5值对比校验。

常规校验方式.png

2、RSA + MD5 校验

RSA+MD5校验.png
  1. 服务端计算出脚本文件的 MD5 值,作为这个文件的数字签名。
  2. 服务端通过私钥加密第 1 步算出的 MD5 值,得到一个加密后的 MD5 值。
  3. 把脚本文件和加密后的 MD5 值一起下发给客户端。
  4. 客户端拿到加密后的 MD5 值,通过保存在客户端的公钥解密。
  5. 客户端计算脚本文件的 MD5 值。
  6. 对比第 4/5 步的两个 MD5 值(分别是客户端和服务端计算出来的 MD5 值),若相等则通过校验。

只要通过校验,就能确保脚本在传输的过程中没有被篡改,因为第三方若要篡改脚本文件,必须计算出新的脚本文件 MD5 并用私钥加密,客户端公钥才能解密出这个 MD5 值,而在服务端未泄露的情况下第三方是拿不到私钥的。

这种方案安全性跟 HTTPS 一致,但不像 HTTPS 一样部署麻烦,一套代码即可通用。对于它的缺点:数据内容泄露,其实在传输过程中不泄露,保存在本地同样会泄露,若对此在意,可以对脚本文件再加一层简单的对称加密。这个方案优点多缺点少,推荐使用,目前 JSPatch 平台就是使用这个方案。

参考链接:http://blog.cnbang.net/tech/2879/

代码实现:


// 判断zip文件是否合法
- (BOOL)checkIsValidatedPatchAtPath:(NSString *)zipPath {
    if (!zipPath.length) {
        return NO;
    }
    
    // 根据文件头,判断文件是否为 zip 类型
    NSFileHandle *fh = [NSFileHandle fileHandleForReadingAtPath:zipPath];
    NSData *data = [fh readDataOfLength:4];
    BOOL isZipFile = NO;
    if ([data length] == 4) {
        const char *bytes = [data bytes];
        // ZIP Archive (zip),文件头:504B0304
        isZipFile = (bytes[0] == 'P' && bytes[1] == 'K' && bytes[2] == 3 && bytes[3] == 4);
    }
    if (!isZipFile) {
        return NO;
    }
    
    id res = nil;
    NSDictionary *dict = nil;
    NSString *md5 = nil;
    OZZipFile *unzipFile = nil;

    @try {
        unzipFile=
        [[OZZipFile alloc] initWithFileName:zipPath mode:OZZipFileModeUnzip];
        
        // 取出 patch.key 中 md5值
        if ([unzipFile locateFileInZip:@"app/patch.key"]) {
            OZFileInZipInfo *info = [unzipFile getCurrentFileInZipInfo];
            OZZipReadStream *readStream = [unzipFile readCurrentFileInZip];
            NSUInteger infoLength = [[NSNumber numberWithUnsignedLongLong:info.length] unsignedIntegerValue];
            NSMutableData *keyData = [[NSMutableData alloc] initWithLength:infoLength];
            [readStream readDataWithBuffer:keyData];
            if (keyData) {
                // RSA公钥解析md5值
                dict = [keyData wk_decodeKeyData];
            }
        }
        
        // 取出 main.js 文件数据,生成文件的 md5值
        if ([unzipFile locateFileInZip:@"app/main.js"]) {
            OZFileInZipInfo *info = [unzipFile getCurrentFileInZipInfo];
            OZZipReadStream *readStream = [unzipFile readCurrentFileInZip];
            NSUInteger infoLength = [[NSNumber numberWithUnsignedLongLong:info.length] unsignedIntegerValue];
            NSMutableData *data = [[NSMutableData alloc] initWithLength:infoLength];
            [readStream readDataWithBuffer:data];
            [readStream finishedReading];
            if (data) {
                NSString *tmpPath =
                [NSTemporaryDirectory() stringByAppendingPathComponent:@"tmp"];
                if ([data writeToFile:tmpPath atomically:YES]) {
                    WKFileHash *fileHash =
                    [WKFileHash hashForFile:tmpPath types:WKFileHashTypeMD5];
                    md5 = fileHash.md5String;
                }
            }
        }
        
        [unzipFile close];
        
        // 校验 md5 值是否相等
        if ([dict isKindOfClass:[NSDictionary class]] &&
            [md5 isKindOfClass:[NSString class]] && md5.length) {
            if ([dict[ @"md5" ] isEqualToString:md5]) {
                res = dict;
            }
        }
    }
    @catch (NSException *exception) {
        [unzipFile close];
        return res;
    }
    
    NSLog(@"patch.key : %@", res);
    return res != nil;
}

3、小结

  • 可以使用二者结合的方式,来保证文件包的安全
  • 网络请求时需使用数据加密请求,防止被人同时篡改md5值和zip文件地址
  • 解析判断的代码逻辑,需代码混淆,防止别人解析出操作逻辑

五、反调试

1、利用ptrace防护debugserver

为了方便应用软件的开发和调试,UNX的早期版本就提供了一种对运行中的进程进行跟踪和控制的手段,那就是系统调用 ptrace。通过 ptrace,可以对另一个进程实现调试跟踪。同时 ptrace 提供了一个非常有用的参数,那就是 PT_DENY_ATTACH,这个参数用于告诉系统阻止调试器依附。所以,最常用的反调试方案就是通过调用 ptrace 来实现反调试。

把ptrace.h导入工程。ptrace头文件不能直接导入app工程,可以新建命令行工程,然后#import <sys/ptrace.h>进入到ptrace.h,把内容全部复制到自己工程中新建的header文件MyPtrace.h中,那么自己的工程想调用ptrace就可以导入MyPtrace.h直接进行调用.

- (void)viewDidLoad {
    [super viewDidLoad];

    ptrace(PT_DENY_ATTACH, 0, 0, 0);
}

2、反ptrace ,让别人的ptrace失效

就是如果别人的的app进行了ptrace防护,那么你怎么让他的ptrace不起作用,进行调试他的app呢?
由于ptrace是系统函数,那么我们可以用fishhook来hook住ptrace函数,然后让他的app调用我们自己的ptrace函数

  • 注入动态库meryinDylib
  • 在meryinDylib中hook住ptrace函数

3、针对2,要想别人hook自己的app的ptrace失效

思路:别人hook ptrace的时候,自己的ptrace已经调用
想要自己函数调用在最之前:自己写一个framework库
在库中写入ptrace(PT_DENY_ATTACH, 0, 0, 0);

库加载顺序:
自己写的库>别人注入的库
自己的库加载顺序:按照 Link Binary Libraries的顺序加载

4、针对3,要想别人hook自己的app的ptrace失效

思路:别人hook ptrace的时候,自己的ptrace已经调用
想要自己函数调用在最之前:自己写一个framework库
在库中写入ptrace(PT_DENY_ATTACH, 0, 0, 0);

库加载顺序:
自己写的库>别人注入的库
自己的库加载顺序:按照 Link Binary Libraries的顺序加载

参考链接:https://www.jianshu.com/p/ebdfb0a25c85

六、反注入

反注入主要还是一种注入检测机制,即检测当前有没有其他模块注入。曾经出现过一种主
动防止注入的方法,但该方法现在已经没有用了。

1、restrict防注入

当旧版的dyld检测到存在 __RESTRICT,__restrict 这样的 section 时, DYLD_INSERT_LIBRARIES
环境变量会被忽略,导致注入失败。因此,在 Xcode 的编译设置选项 “Other Linker flags” 中加
上 -Wl,-sectcreate,__RESTRICT,__restrict,/dev/null 参数,就能达到反注入的效果。

在新版的 dyld 及 iOS10 的测试中发现,该方法已经没有用了,dyld 已经不检测这个 section
了,而且 opool 带 unrestrct 的功能。所以,这个方法现在已经没有实际的用处了。

2、注入检测

从另一个角度来分析,可以通过注入检测的方式来判断有没有进行注入。和调试检测一样,
具体的应对措施由我们自己制定。注入检测可以判断加载模块中有没有一些不在正常加载列表
中的模块,使用 _dyld_get_image_name 获取模块名,然后进行对比,具体如下

int AMCheckInjector() {
    int count = _dyld_image_count();
    
    if(count>0) {
        for (int i = 0; i < count; 1++) {
            const char* dyld = _dyld_get_image_name(i);
            // 或者发现其他不在白名单内的库是以load command方式注入的
            if(strstr(dyld,"DynamicLibraries")) {
                return 1;
            }
        }
    }
    return 0;
}

参考链接:
https://blog.csdn.net/sysprogram/article/details/76691496
https://www.jianshu.com/p/f664b1da8458

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

推荐阅读更多精彩内容