从轮密钥逆推主密钥
首先讨论AES-128
这里用上上篇对称加密算法AES原理及分析里面用到的密钥0123456789abcdef0123456789abcdef生成的10轮子密钥做分析
1 | K00:0123456789abcdef0123456789abcdef |
假如获取到的是K0,那不用说。如果获取到的是K10呢?
8821df9e5e9a36f31255d04529bedb5e,首先我们会到W数组的视图,看W10密钥怎么编排出来的
K10是W40,W41,W42,W43拼起来的,我们已知K10,即已知W40,W41,W42,W43有没有办法求K9?如果可以,那么同理可以逆推到K0,即求得了主密钥
我们不妨先复习一下异或的基本性质,异或作用与比特位上,对应的比特位相同则为0,相异则为1。
因为相同为0,相异为1,那么一个数和自身异或时,因为每个比特位都相同,所以结果为0。
而当某个数和0异或时,自身为0的比特0^0得0,自身为1的比特位1^0得1,这就导致结果和没异或前一样。
除此之外,异或并不看谁先谁后,A ^ B 与 B ^ A 显然无区别,即具有交换律。
看看异或的多角度理解:如何理解「异或」的含义。
下面做变换,左右W42异或
K10中涉及到的四个式子均可以做变化
K10 = 8821df9e5e9a36f31255d04529bedb5e,切成四块
1 | hex(0x8821df9e^0x5e9a36f3) |
所以
W37 = 0xd6bbe96d
W38 = 0x4ccfe6b6
W39 = 0x3beb0b1b
求出了W37,W38,W39,即K9的后大半部分,和真实情况比对后发现一致。
K09:570a707cd6bbe96d4ccfe6b63beb0b1b
那么W36呢?再复习一下g函数吧
首先循环左移一字节,3beb0b1b 变成 eb0b1b3b
然后逐字节S盒替换,得e92bafe2
1 | SBox = [ |
最后是首字节和Rcon中的一个字节异或,这是最后一次变换,即用0x36
1 | rcon = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36] |
即
1 | hex(0xe9^0x36) |
g函数的结果即是0xdf2bafe2
那上面W36=g(W39) xor W40 = 0xdf2bafe2 xor 0x8821df9e
1 | hex(0xdf2bafe2^0x8821df9e) |
所以完整的K9:570a707cd6bbe96d4ccfe6b63beb0b1b,就被我们分析出来了,跟一开始的K表里的K9是一致的。可以继续往上推出K0,获得主密钥。即AES可以依靠轮密钥逆推出主密钥。严肃的说,AES-128可以通过一轮轮密钥逆推出主密钥,AES-192需要一轮半,AES-256需要两轮轮密钥。
接下来顺便下讨论DES的
DES密钥长八字节,在密钥编排时,会舍弃每个字节的最后1比特。因此实际参与密钥编排的是56比特,我们可以验证这一点。
假设密钥是
1 | 11 22 33 44 55 66 77 88 (hex) |
二进制即
00010001 00100010 00110011 01000100 01010101 01100110 01110111 10001000
我们将末尾1比特翻转,即0变1,1变0。如果AES密钥中每个字节的最后一位被“丢弃”,那么翻转后也并不会对程序有任何影响,反之加密结果应该不同。
00010000 00100011 00110010 01000101 01010100 01100111 01110110 10001001
转成十六进制即
1 | 10 23 32 45 54 67 76 89 (hex) |
在Cyberchef中,使用这两个密钥进行DES加密,结果完全一致。
对于DES而言,已知两轮或两轮以上的子密钥,就可逆推出初始密钥,具体代码见 DES 子密钥逆推初始密钥 一文。对于主密钥中无法复原的八比特,随便补什么都行,反正不影响结果。
在实际场景中,当我们需要基于AES/DES的轮密钥恢复主密钥时,我们用 SideChannelMarvels/Stark 这个开源项目,用户体验很好。
在另外一些情况中,目标加密算法可以基于某个轮密钥逆推出全体轮密钥,但没法恢复出主密钥,那怎么办呢——这不就是一开始说的吗
轮密钥所处的内存块可区分吗
首先得考虑一个代码编写的问题,在大多数加密实现中,出于模块化以及效率的考量,密钥编排作为一个整体性的、前置的步骤,在明文的运算前就被完全算出来。以AES -128为例,Key 为 16字节,密钥编排后生成11个16字节的轮密钥,这是我们刚学的。很少有程序会在明文运算中,用到了Ki再去编排生成那16字节,而是像我们的Python代码那样,提前生成所有轮密钥,供后续明文运算时使用。在Android Native开发中而言,即K0-K10紧凑的在一块内存中,装在比如 uint8_t RoundKey[176]这样的数组里。我们将全体轮密钥称为轮密钥群,这么叫比较好听。
那么如果提供给我们一串字节数据,能否判断其为AES的轮密钥群?这里用上一篇某鱼直播软件使用unidbg算法分析 里面出面的轮密钥群做样例,比如如下的176字节
1 | 0000: 30 29 28 27 26 25 24 23 22 21 00 00 00 00 00 00 0)('&%$#"!...... |
这个问题有什么意义?
一个比较小的作用在于,在重度混淆的样本里,我们没法自上而下的分析逻辑。那么如果发现某个参数指向这么一片内存,我们可能需要办法确认——这是不是编排后的轮密钥?
比较取巧的办法是验证图中橙色部分,验证w5是否等于w4^w1,w6是否等于w5^w2等等。
那么比较体系化的办法呢?我们假定上面字节流的最后十六个字节是轮密钥最后一轮,然后逆推出整个轮密钥群,两相对比,如果一致就可确认是否是轮密钥群。
在stark中进行验证,传入的参数中,B833DB3665CDB13BA6991DF165106268是假设的轮密钥(字节流最后十六字节),10为第几轮。
1 | E:\Project\c_project\Stark>aes_keyschedule B833DB3665CDB13BA6991DF165106268 10 |
比对后可确认一致。AES中还有一个特殊之处,第一轮子密钥就是主密钥,所以密钥就是30292827262524232221000000000000。又因为AES”第一轮子密钥就是主密钥“,所以有个更简单和易懂的办法——假设可疑内存块的前十六个字节是主密钥,做密钥编排,对比两者结果是否一致。
现在也有开源的工具可以更快找到密钥,这里推荐龙哥的Unidbg_FindKey