在日常逆向分析工作有遇到过一个白盒AES算法,在网上找到这样一篇还原该白盒算法秘钥的文章:
DFA分析白盒AES算法 ,通过学习该文章,总结了一些心得,在这里分享下。
详细的理论可以看上面那篇blog,我这里只挑一些重点来说明下。
AES算是我们日常开发中最常用一种对称加密算法了,加密过程如下:
主要有这四种操作:
1.S盒字节代换
2.行移位
3.列混淆
4.轮秘钥加
白盒算法是将秘钥混淆到算法中,让攻击者即便能够获取算法的内部细节(能够动态调试),也无法 还原出秘钥的一种算法,常见的白盒算法有:白盒AES,白盒SMS4。
由AES加密算法流程可以看出:第10次轮秘钥加之前是没有列混淆的。如果我们在第九轮列混淆之前构造如下两组数据:
从上图两个状态矩阵可以看出,状态矩阵中只有第一个字节不一样。如果当前状态继续往下推导,可以有如下:
这里的算法分为两个部分:
1.产生这些fault数据的方法:
该算法在DFA 产生Fault数据
该算法主要是通过静态修改二进制文件方式来修改R9的一个字节,从而输出Fault数据的。(这里面具体的实现细节没太弄懂,有兴趣都可以自行去看代码,后面的实战,我主要用IDA动态Patch方式实现输出Fault数据,没有用到这里算法。)
2.拿到这些fault数据后,怎么样还原出白盒AES秘钥
该算法实现是在github上:phoenixAES
下面主要对该算法的学习,解读。
首先该算法的输入一个格式化的文件,每一行分别是输入状态矩阵(16进制字符串表示),输出状态矩阵(16字节128位)
经过分析代码可以发现,其实代码中没有用到输入的值,只需对输出值进行计算,并且第一行的输出值定为golden_ref,即没有做改变的原始正常的值,其他输出值为diff,即在第九轮状态更改一字节后输出的值。
最终算法输出的是还原的第10轮的秘钥,要得到原始AES秘钥,还需逆推一下。
该算法实现中有这样一个列表,这个列表是来判断当前diff是属于哪一类,对应秘钥哪4个位置,比如
拿加密来说,在第九轮列变换之前改变状态矩阵第一列中某一个字节的值,最后的结果只会在(0,7,10,13)的位置上发生改变,也就对应第10轮秘钥的(0,7,10,13)位置上的值。同理改变第二列上的值,只会改变(1,4,11,14)位置上的值。
AES加密列变换阵数值只在:1,2 ,3 中取,
AES解密列变换的数值只在:9, 13, 11, 14中取
这里的列表
_AesMult[1]可以表示(0~255)中分别与1相乘的结果。
_AesMult[9]可以表示(0~255)中分别与9相乘的结果。
这里 j=S(Y0), ibox_j = invSBox(Y0) = Y0
由上述推导公式:diff = S(Y0) + S(Y0+2z) 可以变换得(加号均表示异或):
diff + S(Y0) = S(Y0+2z)
invSBox(diff + S(Y0)) = Y0 + 2z
invSBox(diff + S(Y0)) + Y0 = 2z
candi = [itab[ibox[j ^ diff] ^ ibox_j] for j, ibox_j in enumerate(ibox)]
所以这里这句是返回是S(Y0)从0~255取值时,对应z的取值。
所以后面会对z进行求交集操作,然后反过来根据z的值,去取S(Y0)的取值列表,最后通过多组数据的求解归并得到唯一确定的S(Y0)的值,最后求秘钥时会有一个:
key[Keys[j]]=list(c[index][0][j])[0] ^ Gold[j]
讲了这么多理论算法实现,还是来个具体的示例更容易说明问题,这里选的列子是这篇blog上提的:
LIFE破解白盒AES
它里面提到的解法,是通过用LIEF将Android so转化成Linux上的可执行程序,然后对接上述的DFA
静态产生Fault数据方法得到一组数据,然后调用[phoenixAES]进行秘钥还原。我这边主要是通过IDA 动态Patch来得到一组Fault数据。
里面的APK是SECCON2016 CTF中的一题:SECCON2016 Online CTF-Binary / Crypto500 Obfuscated AES
主要从资源文件arrays中随机取一个字符串传入到native函数 a 进行加密,然后每隔0.01s递归调用一次,所以程序运行后界面上的encrypted_flag一直在变化。
arrays列表:
用IDA打开lib-native.so可以发现该so经过了ollvm混淆了,而且题目也说清楚了这个是一个ollvm混淆的白盒AES算法。
通过IDA静态分析,可以定位到静态注册的JNI函数:Java_kr_repo_h2spice_crypto500_MainActivity_a
简单分析一下可以定位到关键函数: TfcqPqf1lNhu0DC2qGsAAeML0SEmOBYX4jpYUnyT8qYWIlEq
通过frida hook 一下该函数查看该函数的输入输出值:
这里有两点说明下:
1.在hook native层导出函数时用的replace的方式,而不是attach方式是为了能够在函数执行完后再打印一遍输入参数,因为在C中会经常传个指针,在函数中操作的结果也是保存在这个指针中。在本列中TfcqPqf1lNhu0DC2qGsAAeML0SEmOBYX4jpYUnyT8qYWIlEq函数a0参数是加密前java层传下来的数据,a1是经过加密后的数据。
2.这里也hook了Java层的JNI函数,为了让每次传下来的值保持一致,这里我是随机选取一个值:"Gew1cqzKp5K8sejh3FlTZlS/CISCpO81WmZ/oU4SJOk=",这样没隔0.01s都会触发一次调用。
经过上面的hook分析可以得出:
java层传下来的加密数据都是:32字节的数据,所以在C层调用了两次TfcqPqf1lNhu0DC2qGsAAeML0SEmOBYX4jpYUnyT8qYWIlEq函数,分别加密前16字节和后16字节(ECB模式),然后将加密后的数据Base64一下返回给java层,显示到界面。
有了上面分析,结合phoenixAES算法,要还原秘钥,只需要想办法去产生R9 Fault数据,也就是在
调用TfcqPqf1lNhu0DC2qGsAAeML0SEmOBYX4jpYUnyT8qYWIlEq函数内部,某个时间点(刚好在R9列变换前)更改状态矩阵的一个字节的数据,得到加密后的数据,对比输出数据与golden_ref是否刚好只有4字节不同。(如果只有1字节改变,说明patch太晚,如果有16字节不同说明patch太早)。
所以问题就变成如何刚好找到Patch的时间点,由于so被混淆了不能很直观的看到函数执行流程,这里用IDA Python 去打印该函数的核心块的执行过程:
这里我调试是armeabi-v7a的so,其中断点4个位置分别是:
1.OAES函数的开始
2.OAES函数的结尾
3.OAES函数其中一处明显的子函数调用
4.OAES函数其中一处明显的数据处理块的起始地址。
运行完可以明显观察到子函数调用了10次,每次调用之间调用了4次核心处理模块共九组,是不是可以类比AES10轮加密操作,所以我选择在第九组核心操作前进行patch一字节,然后观察到最后的输出结果果然是符合预期的,最后的patch代码如下:
这里在调试时需要用frida hook java层的输入以保持每次输入数据都一样,所以这里的调试步骤为:
1.运行android_server -p12345
2.运行frida_server
3.adb shell am start -D -n kr.repo.h2spice.crypto500/.MainActivity 进入调试模式
4.运行frida hook java 层代码(c层hook注释掉)
5.ida attach
6.jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
7.运行IDA Python patch脚本
注意这里的第4步不能跟第5步互换,不然frida会报错,通过这种方式意外的发现可以hook 到Actity的OnCreate函数,我之前用 frida spawn的方式老是报错。
整理脚本的输出的Fault结果,放入[phoenixAES]中:
还原出K10秘钥为: 040D08DA68001026F3DC0D68897148B4
再调用Key scheduling reversers中的
aes_keyschedule 逆推得到round0秘钥 即AES秘钥。
得到该AES秘钥为:6C2893F21B6185E8567238CB78184945
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-8-22 10:46
被lfyyy编辑
,原因: