0x00 前言
上文中,我们介绍了有关Unicorn
的使用,为了避免只造轮子不开车的现象出现,我们就用Unicorn
来亲手攻击一个AES
白盒。
我选取了CHES 2016
竞赛中的AES
白盒,这个白盒非常白,甚至给了源码,代码和程序可以在"https://github.com/SideChannelMarvels/Deadpool/tree/master/wbs_aes_ches2016" [1]中找到。文章中还包含里Writeup
,但是其中使用的工具很老了,还是python2
的代码,并且使用的执行引擎是PIN
,如果是ARM
的安卓APP
里的白盒就没办法了。
所有计算均在——有限域GF(2^8)
在进行白盒破解之前,我们看一下错误注入的原理是什么。
对于AES128来说,错误注入的目标在第九轮的MixColumns计算之前,第九轮的MixColumns计算之前的数据假设是这个样子的:
假设我们的错误正好命中了第一个字节,则数据流变成了:
之后,数据流会依次进入:
MixColumns
AddRoundKey K9
SubBytes
ShiftRows
AddRoundKey K10
中间的过程就不写了,有兴趣的通过可以自己推一下,如果熟悉AES的计算过程,不难推算。"https://blog.quarkslab.com/differential-fault-analysis-on-white-box-aes-implementations.html"[2]该文有详细的推导过程。
最后,AddRoundKey *K*10
结束之后输出,应该是这个样子的:
如果成功错误注入的话,会变成这个样子(其中的+号表示异或):
以第一个字节为例,我们设:
之后把O
和O’
进行Xor
计算:
得到:
设:
原式变为:
把剩下3个字节补齐,得到:
四个Y的取值都是0-255,遍历四个Y,就可以得到Z的一个取值范围 。得到Z的值范围了之后,可以对应一组Y。(再说一次,乘法和加法都在GF(2^8)上)。
之后通过关系公式:
推导出一组K10(0,7,10,13)
密钥的值。
这只是错误出现在第一个Byte的情况,通过多组错误输出,可以唯一的推导所有的K10
,之后通过密钥扩展算法,推导出AES
的密钥。
这个地方有点绕,我们举个例子:
假设O13 ^O'13 = 0x55,我们尝试求一下:
sbox=(
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16)
a=[]
for y3 in range(256):
#假设O13 ^ O'13 = 0x55
a.append(sbox.index(0x55 ^ sbox[i]) ^ y3)
print(a)
计算得到了256个数,但是由于其中有的数是重复的,所以可以缩小Z的取值范围。拿到了Z之后,就可以推导出对应的Y,由于O是已知的,所以可以推出密钥或者缩小密钥的范围。
那么2Z
和3Z
怎么计算?
这里的2,3与Z的关系是有限域中的乘法,所以计算求Z的时候,并不是简单的除一下就行了,而是域中的计算。需要涉及到生成多项式0x11B
求逆元的计算。(推荐一本书,密码编码学与网络安全—原理与实践-第六版,写的很好。)
0x002 实践操作
我没有像"https://bbs.pediy.com/thread-254042.htm"[3]一样用frida
和idapython
去做,这样不太灵活。我使用了unicorn
引擎,一般来说,白盒AES算法都会被封装在一个或者连续的几个函数中,这样对于Unicorn
是十分方便的。
我的目标没有选择上文中的wbDES
,它太老了,而且DES的使用越来越少,我也没有使用参看文献中的基于OLLVM
的实现,因为OLLVM
混淆之后的AES也不是严格意义上的白盒,我选用了CHES 2016 CTF上的一道题。
首先对程序使用GDB
进行分析,发现程序的main
函数主要是获取输入和输出,加密过程chow_aes3_encrypt_wb
函数中,在chow_aes3_encrypt_wb
下断点。
(注:我没有使用github中提供的Shared object文件,而是使用源码,在makefile中添加了-no-pie参数,重新编译了一个executable文件)
之后在函数的执行结束的位置下断点。
发现程序使用RSI
和RDI
进行传参,RSI
保存的指针是输出缓冲区,RDI
保存的指针是函数输出数据的位置。
之后开始栈空间的构建,具体构建的方法和调试请参考上一篇文章。
有错误注入攻击,一定需要能量分析攻击,在针对芯片的攻击中,SPA和DPA可以提供攻击位置信息,针对白盒的攻击也差不多,白盒的实现是针对查找表实现的,所以我们首先需要打印出来所有内存读的位置,这也是为了后续的攻击做准备。在hook中添加筛选条件:
if access == UC_MEM_READ and size == 1 and address<0xb0000000:
其中,size==1表示每次读取的大小是一个字节,因为AES是以字节为单位进行计算的;address<0xb0000000是为了排除程序在操作栈中数据时的误触发。
(左边数值是地址值的十进制表示,实际是从0x61a800(6400000)到0x6acfc0(7000000))这个图可以看出,查找表随着时间是从高地址向低地址分布的。
接下来,我们开始hook并错误注入,我们需要注入的是第九轮,也就是说,在时间上是比较靠后的位置,在这个过程中,我们需要不停的改变注入的位置,通过分析错误输出,来了解是否注入对了地方,如果正好输出了四个错误,并且位置符合,就是注入对了地方;如果错误的位置过多,表示注入的靠前了,如果错误的位置只有一个,说明注入的位置靠后了。
我们拿到了符合条件的错误输出值:
628caf41f9a2f7a51c57b9e23e137365
628cf341f961f7a5c157b9e23e137366
628c2f41f91ff7a5b557b9e23e13730e
628c1541f9caf7a56e57b9e23e1373b8
628c6e41f9b0f7a5d857b9e23e137323
628c1b41f961f7a5c457b9e23e1373aa
628c3d41f93ff7a57857b9e23e13730e
628c1e41f902f7a5bb57b9e23e13732b
628c8c41f9e4f7a5a757b9e23e137319
628ca341f948f7a56057b9e23e1373a2
628cc241f950f7a50f57b9e23e137319
628cbc41f9aef7a58157b9e23e13735a
628c4e41f9a1f7a50057b9e23e1373e3
628ccf41f914f7a57f57b9e23e137317
628cbc41f9aef7a58157b9e23e13735a
628caf4af9a286a51cf9b9e2d7137365
628cafd9f9a22aa51c3bb9e205137365
628cafd9f9a22aa51c3bb9e205137365
628cafd9f9a22aa51c3bb9e205137365
628caf85f9a27aa51cb4b9e2d4137365
628cafc2f9a245a51ce9b9e2f4137365
628cafd9f9a22aa51c3bb9e205137365
f88caf41f9a2f7441c5782e23ef47365
6c8caf41f9a2f7c71c57c6e23e297365
a68caf41f9a2f7781c57e6e23eb97365
828caf41f9a2f7391c5739e23ef27365
d48caf41f9a2f7931c57f7e23e8b7365
3d8caf41f9a2f7061c5736e23ee87365
6236af4122a2f7a51c57b9b83e138f65
62faaf41bba2f7a51c57b9483e133365
6236af4122a2f7a51c57b9b83e138f65
6296af41b6a2f7a51c57b9f93e138a65
6272af41b3a2f7a51c57b9493e13bb65
62f4af41ada2f7a51c57b9ef3e13bd65
6272af41b3a2f7a51c57b9493e13bb65
62c5af4167a2f7a51c57b93e3e133f65
拿到足够多的错误输出后,我使用了工具https://github.com/SideChannelMarvels/JeanGrey/tree/master/phoenixAES[4]进行上述的数学计算推导过程
得到第10轮密钥之后,使用工具aes_keyschedule
推算出AES-128
的密钥。
0x003 参考
[1]https://github.com/SideChannelMarvels/Deadpool/tree/master/wbs_aes_ches2016
[2]https://blog.quarkslab.com/differential-fault-analysis-on-white-box-aes-implementations.html
[3]https://bbs.pediy.com/thread-254042.htm
[4]https://github.com/SideChannelMarvels/JeanGrey/tree/master/phoenixAES