前言
玩APP逆向首先就是要对应用脱壳重签名,为什么要重签名呢?重签名肯定是为了安全,那么它的原理是啥?下面我们先总结一下开发中常用的数据加密方式,然后再分析苹果的签名原理。
RSA非对称加密
上世纪70年代产生的一种加密算法。其加密方式比较特殊,需要两个密钥:公开密钥简称公钥(publickey
)和私有密钥简称私钥(privatekey
)。公钥加密,私钥解密;私钥加密,公钥解密
。这个加密算法就是伟大的RSA
,也称非对称加密算法,它是以三个数学家的名字命名一套加密算法。具体的加密逻辑涉及欧拉定理、费马小定理等数学知识,这里就不做具体分析,感兴趣的可以上网搜一搜,总之学好数理化走遍天下都不怕。下面就总结一下它的特点,以及在电脑上怎么玩。
安全系数高
:由于它的数学特性,几乎不可能根据密文推导出明文加密效率低
:还是由于它的数学特性,如果加密的数据很大,它的效率就很低,所以比较适合加密数据小且关键的数据
,比如key。公钥的提取
:公钥是根据私钥提取的
,所以私钥比公钥长很多
玩一下RSA
由于Mac系统内置OpenSSL
(开源加密库),所以我们可以直接在终端上使用命令来玩RSA,OpenSSL中RSA算法常用指令主要有三个:
genrsa
:生成并输入一个rsa私钥rsautl
:使用RSA密钥进行加密、解密、签名和验证等运算rsa
:处理RSA密钥的格式转换
等问题
先生成RSA
私钥private.pem
,私钥长度1024bit,然后再从私钥中提取RSA公钥public.pem
新建一个message.txt
文件,明文123456;RSA公钥加密,密文为enc.txt
;RSA私钥解密,解密后的明文为des.txt
,cat查看des.txt文件内容与message.txt文件内容一致,解密成功
对称加密
明文通过密钥加密得到密文,密文通过密钥解密得到明文,加密和解密都需要同一个密钥
。
常用算法:
DES
:数据加密标准(用得少,因为强度不够)3DES
:使用3个密钥,对相同的数据执行3次加密,强度增强AES
:高级密码标准。
一般使用高强度的AES
进行数据加解密,而AES数据加解密存在两种模式,ECB和CBC
。
ECB
:电子密码本模式。每一块数据,独立加密
,破坏其中一个密文不影响其他密文。最基本的加密模式,也就是通常理解的加密,相同的明文将永远加密成相同的密文,无初始向量,容易受到密码本重放攻击
,一般情况下很少用。CBC
:密码分组链接模式
。使用一个密钥和一个初始化向量[IV]
对数据执行加密。明文被加密前要与前面的密文进行异或运算后再加密,因此只要选择不同的初始向量,相同的密文加密后会形成不同的密文,这是目前应用最广泛的模式
。CBC加密后的密文是上下文相关的,但明文的错误不会传递到后续分组,但如果一个分组丢失,后面的分组将全部作废(同步错误)。
实例解析
- (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 {
/**
kCCOptionPKCS7Padding | kCCOptionECBMode ECB 的模式
kCCOptionPKCS7Padding CBC 的加密
*/
option = kCCOptionPKCS7Padding | kCCOptionECBMode;
}
// 设置输出缓冲区
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
size_t bufferSize = [data length] + self.blockSize;
void *buffer = malloc(bufferSize);
// 开始加密
size_t encryptedSize = 0;
//加密解密都是它 -- CCCrypt
/**
1.kCCEncrypt 加密/kCCDecrypt解密
2.加密算法 aes/des..
3.加密选项 ECB/CBC
4.KEY 的地址
5.KEY 的长度
6.iv 初始化向量
7.加密的数据(地址)
8.加密的数据长度
9.密文的内存地址
10.密文缓冲区的大小
11.加密结果大小
*/
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];
}
复制代码
分析:这是一个AES加密数据的示例,注释已经很详细,我们看到最主要的就是C函数CCCrypt
,这个函数里面的参数指定了使用哪种对称加密算法(aes、des),以及使用哪种加密模式(cbc、ebc),数据的明文和key同样也作为参数传进去。这就存在一个安全隐患,如果这个CCCrypt
函数被hook
了,数据的明文和key
也就暴露了,所以我们需要对明文和key做保护,这里推荐使用按位异或
进行数据保护,我们知道一个数据A
,按位异或数据B
得到数据C
,同时存在数据C按位异或数据B得到数据A
。所以可以在调用CCCrypt函数前,对明文和key进行一次安慰异或运算再传进去,解密之后得到的数据再按位异或一次得出真实的值,这样可以降低明文数据暴露的风险,如下贴一个按位异或的方法,参数key为想要按位异或的固定值
按位异或
- (NSString *)bitwiseXor:(NSString *)string withKey:(NSString *)key
{
NSData* bytes = [string dataUsingEncoding:NSUTF8StringEncoding];
Byte *myByte = (Byte *)[bytes bytes];
NSData* keyBytes = [key dataUsingEncoding:NSUTF8StringEncoding];
Byte *keyByte = (Byte *)[keyBytes bytes];
int keyIndex = 0;
for (int x = 0; x < [bytes length]; x++)
{ //遍历 异或运算,是位运算
myByte[x] = myByte[x] ^ keyByte[keyIndex];
if (++keyIndex == [keyBytes length])
{
keyIndex = 0;
}
}
NSData *newData = [[NSData alloc] initWithBytes:myByte length:[bytes length]];
NSString *aString = [[NSString alloc] initWithData:newData encoding:NSUTF8StringEncoding];
return aString;
}
复制代码
HASH
Hash,一般翻译做“散列”,也有直接音译为“哈希
”的,就是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
总结特点如下:
算法是公开的
- 对
相同数据
运算,得到的结果是一样
的,所以一般用来做数据识别的。 - 对不同数据运算,如MD5得到的结果默认是128位,32个字符(16进制标识),
无线的数据有限的结果
,就是不管你数据有多长,结果都是128位。 不可逆运算
,无限的数据存在有限的表现形式,所以逆运算没法确定是哪一个。
常用HSAH算法:MD5、SHA1、SHA256
常见用途:
- 用户密码的加密
- 搜索引擎,关键字hash
- 版本识别
疑问?
1、既然Hash不可以逆运算,为什么网络上可以解密,比如md5就可以在网上解密?
注意这不是解密,而是有个变态把常用的数据hash之后的结果保存了下来,当你输入hash密文之后从库里找出对应的明文,正是因为存在安全隐患,hash单纯的对数据加密很不靠谱
。
2、那么在明文hash之前,添加一些随机的字符串一起hash呢,也就是所谓的加盐
,这样是是否靠谱呢?
加盐
确实能够增加破解的难度,但是如果开发者离职带走了盐,这个盐就暴露了,但是如果换盐成本可能更大。所以说加固定的盐也不靠谱,但如果这个盐是动态的就可以解决这个问题了,于是随机盐的HMac
就存在了,它的目的就是让相同的数据,加上随机的盐得到不同的加密数据
。
随机盐HMac
- (NSString *)hmacMD5StringWithKey:(NSString *)key {
const char *keyData = key.UTF8String;
const char *strData = self.UTF8String;
uint8_t buffer[CC_MD5_DIGEST_LENGTH];
CCHmac(kCCHmacAlgMD5, keyData, strlen(keyData), strData, strlen(strData), buffer);
return [self stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH];
}
复制代码
分析:如上是一个Hmac
算法,其本质还是hash(元数据+sault)
,要想hmac真正有意义,那么这个key也就是盐必须是变化的。如果盐是变化的,那么就必须把盐保存起来
,所以这个变化的盐应该是存储在服务器端的。
以用户登录,密码MD5加密保存为例,探索一下思路
首先每个用户都有一个盐
,跟用户唯一绑定保存在表里。用户注册成功后,服务器随机生成唯一的盐跟用户绑定,并且根据这个盐+用户设置的密码
进行hmac运算,将结果存表,并且把盐回传给客户端
。盐的回传可以通过RSA
加解密。客户端保存
服务器返回的盐
,用户登录时,密码+随机盐hmac
运算传给服务器,服务器拿着运算结果比对表中存储的值。- 如果客户端存储的
盐丢失
了,比如换设备或者APP卸载重装了怎么办?客户端登录的时候应该去服务器校验一下是否存在该用户,如果存在该用户,应该对用户进行鉴权
,比如验证短信验证码或者人脸校验,目的是确保是本人操作,确认身份就返回表中的盐
。 - 如果
网络请求被重放
了,也就是攻击者不是要解密或者破坏数据,怎么办?可以使用Hmac哈希值+精确到秒的时间戳
(如2021111635
)做一次hash运算传到后台,后台拿到同样的Hmac哈希值+精确到秒的时间戳(如2021111636
)做一次hash运算进行比对,注意后台的时间戳很可能比客户端的大1秒
,所以如果比对不通过,服务器还要拿上一秒
的时间戳(如2021111635
)再做一次hash比对。
5、解决了防止攻击重放,也解决了防止破解密码,但是如果网络传输的时候其他参数被修改
了怎么办?可以利用RSA算法,对整体的网络数据
报文hash值
做一次签名
,将签名值作为其中一个参数传到后台,后台拿到数据报文hash值做一次验签
就能保证数据的完整性。
APP应用签名原理
苹果开发者都知道证书、描述文件,打包的时候必须有这两个东西,否则就不能打包安装在手机上,那么苹果是怎么实现这套机制的呢?苹果使用了非对称双签名机制
,如下图所示
- 首先开发者
Mac电脑
钥匙串中存在一个公私钥对,比如公钥M,私钥M
,然后向苹果申请证书的时候需要一个CSR
请求文件,这个请求文件中有开发者的信息以及公钥M。 苹果
也存在一个公私钥
对,公钥
在苹果手机
里,私钥
在苹果后台服务器
里,比如私钥A,公钥A
。苹果后台服务器收到CSR请求文件,会用服务器的私钥A对公钥M加密生成一串hash值,同时生成证书
文件保存着公钥M和加密的hash
值。- 开发者拿到这个证书后,
钥匙串
会自动把证书中的公钥M
和Mac电脑中的私钥M
匹配绑定
,毕竟它们本来就是一对。 - 开发者利用
xcode
对APP
进行签名
,其实就是用私钥M
对APP签名,签名后xcode会把证书打包进APP
。 - APP安装进手机里,
手机
里存在公钥A
,用公钥A
解密证书里的Hash值
,比对公钥M,如果比对正确,说明证书是合法的,拿着公钥M
验证APP签名。整个过程就是一个RSA双签名验证机制,Mac电脑一个公私钥对,手机和苹果服务器一个公私钥对
。 - 但是还存在一个问题,如果大家都使用这张证书签名,那么就可以不需要
APPStore
了,所以就必须存在描述文件
对APP的签名进行限制
,比如失效时间、签名应用的个数、证书的类型等等,所以安装进手机里的不仅仅是证书还有描述文件。