代码下载:https://github.com/ZhangJingHao/ZJHAppSafeGuard.git
一、字符串加密
很多时候,可执行文件中的字符串信息,对破解者来说,非常关键,是破解的捷径之一。为了加大破解、逆向难度,可以考虑对字符串进行加密。字符串的加密技术有很多种,可以根据自己的需要自行制定算法
这里举一个简单的例子,对每个字符进行异或处理,需要使用字符串时,对异或过的字符再进行一次异或,就可以获得原字符
1、加密前
直接在源代码里面编写字符串,使用hopper可直接找到该串
2、加密后
混淆后的代码已经看不到硬编码了。
参考链接://www.greatytc.com/p/49e98b8a05fd
3、封装加密方法
对加密方法进行封装,提高代码的可读性
参考链接:https://github.com/CoderMJLee/MJCodeObfuscation
二、代码混淆
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、混淆前后效果对比
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];
}
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);
}
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];
}
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;
}
效果下入:
参考链接://www.greatytc.com/p/2c5c8bc55f54
5、小结
对普通请求、返回数据,可使用DES对称加密的方式,密钥使用敏感字符串混淆的方式存储。
对于重要数据,使用RSA进行数字签名,起到防篡改作;对于比较敏感的数据,如用户信息(登陆、注册等),客户端发送使用RSA加密,服务器返回使用DES(AES)加密;
首先移动端给服务器传递通过RSA公钥加密后的数据,参数包括DES的密钥(密钥是随机生成的八位字符串) 和 相关参数信息,服务器通过私钥解密信息数据,里面包括DES密钥和 相关参数信息,服务器再通过此DES密钥加密返回数据给移动端,移动端通过此DES密钥进行解密获取数据。(注:DES的密钥每一次都要重新随机生成,也就是一个密钥只完成这一次的数据传递)
四、插件校验
1、常规校验方式
客服端发起获取插件信息请求,服务端返回插件url地址和对应zip的MD5值,客户端根据url下载zip文件,下载完成后,对zip文件进行md5取值,与服务器返回的md5值对比校验。
2、RSA + MD5 校验
- 服务端计算出脚本文件的 MD5 值,作为这个文件的数字签名。
- 服务端通过私钥加密第 1 步算出的 MD5 值,得到一个加密后的 MD5 值。
- 把脚本文件和加密后的 MD5 值一起下发给客户端。
- 客户端拿到加密后的 MD5 值,通过保存在客户端的公钥解密。
- 客户端计算脚本文件的 MD5 值。
- 对比第 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的顺序加载
参考链接://www.greatytc.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
//www.greatytc.com/p/f664b1da8458