这篇文章主要记录一些分组密码相关的趣题
EASY_dfa
最近做到的我觉得相当有意思的一个题,在这里记录一下解题过程
题目来源:川渝网络安全竞赛 2022
题目:
1 | #!/usr/bin/python3 |
代码略有点长,一步一步来,还是从分析加密流程开始:
首先,要过一个proof,为爆破一个十六进制串的末四位哈希,这个本来没什么好讲的,但是仔细看可以发现题目这里锅了:
1 | def proof(): |
可以看到,他直接print了明文串,因此只需要接受明文串,并把末四个十六进制数发送回去即可通过proof。其实这也没什么,不过是一个小失误。但之所以我要特地讲出来,在这篇wp后面会说明原因。
过了proof后,加密正式开始:
- 生成一个8字节的随机密钥key
- 用这个key初始化一个Enc对象C
- 生成一个4字节的随机明文,记作plain,并用Enc对象C进行加密,得到f
- 给出C中的S盒
- 给我们10s时间,可以选择进行以下两种操作:
- 输入”1”,可以自行构造一组明文进行加密,靶机端会返回加密后的密文
- 输入”2”,可以输入一个明文,如果输入的明文被加密后与f相等,则核验通过,发放flag
这是靶机端需完成的任务的逻辑,再来看看Enc对象具体是怎么加密的,也就是encrypt函数:
1 | def encrypt(self, plain): |
- 生成一个S盒,是一个(0,256)之间的随机置换
- 将明文与key的前四个字节作异或,得到T
- 第二行,做完了题也不知道有什么用,估计是出题没删干净
- 将T作S盒置换后,调用l函数,得到更新后的T
- 将T与key的后半段异或,并给出十六进制密文
其中,l函数如下:
1 | def l(self, B: list) -> list: |
你应该可以感觉到,如果能够知道l函数的密文,那么这种级别的加密对于z3来说,求解出明文不是难事。
至此,题目加密流程就梳理结束了,接下来进行到分析环节。
思路一:生日攻击
刚才我特意说,题目的proof锅了,不需要实际爆破,作用就在这里。节省下来的爆破时间给生日攻击提供了一点可能性。
你可以发现,题目实质上就是要求给出正确的4个明文字节。而4个字节一共有2^32次方种可能性,因此我们随便输入四个字节给靶机,也有 $ \frac{1}{2^{32}}$ 的机会成功。而失败了的话,就重新连接靶机,再随便输入四个字节,如此重复。根据生日攻击理论,如果能够反复进行2^32次,我们就有超过50%的几率至少成功攻击一次,那么就能拿到flag。
当然,即使proof锅了,真的要进行2^32次方次的话,可以用tqdm测试一下大概需要600000个小时,仍然是不现实的。但是这确实是个思路,一点办法都没有的时候,可以拼拼运气。
思路二:构造明文
生日攻击只能说有非常微小的成功的机会。真正要做的话,还是得分析一下加密流程,以及如何构造出可用的明文。
首先要注意到,我们只有10s时间,这是个非常重要的信息。经过我用tqdm测试,我们大概最多能构造30-40组明文让服务器加密。并且,考虑到还需要用构造出的明文信息对f进行解密,实际上来说可能最多只能构造10-20组明文。
这个用处挺大的,直接完全排除了中间相遇攻击、建立字典查找等思路。因此现在的主要目标就是:如何用有限的、少量的明文,去泄露Enc对象C的信息,从而解密密文。
你应该能看出来,如果你能获得密钥key的完整8个字节,那么解密是很容易的,只需要对应异或、逆置换、z3解方程就可以解密出明文。
因此,重心放在求解key上也就是自然而然的思路。很容易想到,如果发送四个字节的”\x00”给靶机端,那么再观察encrypt函数:
1 | def encrypt(self, plain): |
那么第一步得到的T其实就是key的前四个字节,在之后也是key的前四个字节进行置换、调用l函数等。
而第二次,考虑发送下面这样的字节串:
1 | b"\x00" * 3 + b"\x01" |
可以发现,第一部得到的T,前面三个字节并没有受到影响,仍然是key的前三个字节,只有最后一字节的最后一比特取了反。因此,到第三步置换时,前三个字节的置换仍然是不变的,变的只有第四个字节。也就是说,经过这两次明文的发送,l函数输入的参数列表只有最后一个发生了改变!
而我们再看l函数加密过程,为了直观我画个图像:
红色部分代表两次构造的明文在调l函数时的不同量,黄色部分代表得到密文,线条表示该线条与另一线条之间的五个部分异或得到下方的密文。
那么你仔细看这个图,你会发现,第二块密文和第三块密文,对于红色块异或的量是相同的!那么我们将两次服务器返回的密文异或,会发生什么呢?在下面的分析过程中,我们把key的前半部分记作key1,后半部分记作key2.
首先,我们把m经置换后的列表记作P(m),经l函数后的列表记作l(P(m)),也就是m经第三步变换后,记作(l(P(m)))。那么两次构造明文到第三步前分别是key1、key1’,则有:
那么就可以消掉key2,即:
而根据刚才的画图分析,密文中间两块对红色的异或利用是相同的,因此我们可以单独分析上式的中间两块,得到:
这一部分一定要结合图多理解理解,我没有想到讲的特别清楚的办法,如果哪里不明白可以与我交流。
而两次不同的红色量分别是(key1)和(key1^1)经置换得到的最后一字节,而我们拥有这个异或值以及S盒,因此可以反查到所有可能的解。一般来说会有1-4组。
那么通过上述方法,我们就获得了key1的一个字节,而类似的,通过构造下面几组明文并发送,可以通过相同的分析得到key1的剩下三个字节:
1 | b"\x00"*2 + b"\x01"*1 +b"\x00"*1 |
而有了key1后,我们就可以发送key1作为明文,那么经第一步异或后得到的T就是四个全零字节,那么也就自然的可以得到第三步置换以及加密后的值,再与服务器返回的密文异或就能得到key2.
得到key2后,解密相对来说就很容易了,可以自行逆序实现。
需要注意的是,有一个地方导致我们可能不成功:
- 每次获得的key1的某字节可能有多解
但是没关系,我们只需要多与靶机交互几次,直到满足key1的四个字节都是正确解即可。这一段可以用下面的代码本地调试一下:
1 | from random import sample, choice |
可以计算出,平均需要180次左右,能获得一次key1的完全正确的解。运气好的话几组就能出,运气不好接近一千组也有可能,不过没事,交互挂着跑就好。
而本地能正确解密的话,我们就能顺利编写靶机代码了:
1 | from Crypto.Util.number import * |
就这一题而言,还是有不少不是很好讲清楚的地方,可能我的做法也并不简洁。如果看了这篇文章你有任何想说的,都欢迎与我交流!
newAES
题目来源:羊城杯 2023 决赛
题目:
1 | #!/usr/bin/env python |
题目代码不短,不过大部分是AES的具体实现,所以不用管。题目真正有用的信息其实就两点:
- 更换了一个AES的S盒
- 给了两组明文,以及AES加密后的对应密文
第一感觉应该是AES的某一部分作了小改动,然后导致产生了漏洞。但是把S盒换成普通的S盒之后,用自己构造的key,将明文分别用这个AES和cyberchef的AES进行加密,结果是完全一样的!说明这个AES真的就是完整、普通的AES加密,没有改动任何地方。
然后就懵了很久,因为好像没听说过AES还能已知明文攻击。但是题目目的其实已经很清楚了:根据这个更换的S盒以及两组明密文对,对AES进行已知明文攻击。
那肯定就是这个S盒设计的有问题,查阅后发现针对S盒确实有一种差分密码分析方式,然后就学习了一波:
简单说明一下我的理解,在这里我把差分分析的相关概念根据题目具体化一下:
差分:两组明文的异或值,就是明文差分;同理,两组密文的异或值,就是密文差分。
差分分析的依据:与密钥异或,不会改变差分
什么叫不会改变差分?我们把AES的行移位、列混淆都先不管,先单独看一下用到S盒的部分,也就是字节代换以及进行代换前的轮密钥加。这样的话,简单画个草图:
那么不会改变差分就体现在下面的式子中:若有
则:
也就是说,轮密钥其实是对差分没有任何影响的,那么我们怎么利用这个事实进行攻击呢?
再回到刚才加密的那个图,可以看到,m1’经S盒代换后变成c1,m2’经S盒代换后变成c2,那么假设c1,c2的密文差分是r,那么我们就拥有了一个S盒的差分对 (t,r) 。
而S盒是一个字节代换操作,因此输入一共就有一个字节种可能,也就是256种,而对应的,所有可能的(m1,m2)明文对也就有256*256种,因此我们可以根据所有的(m1,m2),用已知的S盒求出所有的(c1,c2),从而求出所有明文差分及对应的密文差分,然后建立出一张S盒的差分表:
1 | t = [[0 for i in range(256)] for j in range(256)] |
那你应该要想到,这种差分对于一个设计良好的S盒来说,要显得很随机,因为S盒的字节代换本身是一个非线性操作,如果随机性不够,那么会让这个S盒有较大的概率暴露线性特征。
这么说可能还是有点抽象,直接列出两种S盒的差分表就直观了,简单打印一下前几行看看:
正常S盒的差分表:
1 | [256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
这个题目的S盒的差分表:
1 | [256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
能感受到区别了吧,这个题目的S盒的差分表完全就没有任何随机性,明文差分与密文差分完全是固定的!而这个固定的差分就不会受轮密钥加的影响,因此我们完全不需要密钥,把对应密钥的操作剔除掉即可。
也就是说,我们只需要将已知密文与未知密文求差分,然后将这个差分用AES解密(去除轮密钥加),求出初始的明文差分,然后将这个差分与已知明文再求差分,就能得到未知的明文了。
需要注意的是,由于是对差分进行AES解密,所以用的并不是题目给的S盒,而是由S盒求出的差分S盒。
exp:
1 | from Crypto.Util.number import * |
aes
题目来源:EIS 2019
题目:
1 | #!/usr/bin/env python3 |
靶机提供了如下操作:
- 连接上靶机后,开始计时60s
- 随机生成十六字节作为AES密钥
- 使用AES_CTR模式对flag进行加密,其中Counter由flag的所有字节和生成,并给出flag的加密值
- 可以与服务器进行三十次交互,每一次交互中,可以输入一段明文,并获得其AES_CTR模式的加密值,对应Counter仍然由输入明文的字节和产生
首先就注意到用了一个平时不怎么见的AES_CTR模式,那么问题肯定也出在这里,先了解一波CTR模式是什么:
AES的CTR模式加密解密详解 | Wuman’s Blog (wumansgy.github.io)
其核心如下:
可以看到,他的AES加密是作用于Counter而非明文。也就是说,其加密流程是将被AES加密的Counter与明文异或得到密文。
而这道题显然提供了Counter的额外信息,也就是其initial_value是传入明文的字节和。而flag固定为30个字节,因此其和只有256*30种可能性,完全可以爆破。而当我们取的明文值字节和正好等于flag的字节和时,其AES加密的结果相同,因此有:
1 | flag = flag_enc ^ input ^ input_enc |
至于限时60s以及限30次,完全不重要,因为可以反复连接靶机而flag不会变。当然注意输入明文长度应与flag一致,均为30。
然而,在NSS上复现本题时,爆破完所有范围依然没有结果,此时打印出传回的flag_enc,发现是45字节,说明题目改动了flag与容器镜像的附件,但是下发的附件忘记改了,因此需要扩大范围至256*45。并且由于改动过flag,那么flag头应该考虑”NSSCTF”。同时,考虑到flag均为可见字符,因此复现时还可以进一步缩小一点范围。
exp:
1 | from Crypto.Util.number import * |
3DES
题目来源:暂不明确
题目:
1 | import os |
题目先生成三个4字节的key,并按如下方式将他们扩展到八字节:
- 转为十六进制
- 在每个十六进制字符前面加一个0
- 转为字节流
举个例子,如果生成的四字节key是:
1 | b"1111" |
其十六进制为:
1 | "31313131" |
那么最终expand后得到的八字节就是:
1 | b'\x03\x01\x03\x01\x03\x01\x03\x01' |
讲完这个过程后回头看题目,他将三个key分别用在:
- 第一层的DES加密
- 第二层的异或加密
- 第三层的DES加密
然后给出了9个字节的已知明文头以及密文,要求解出明文。
由于DES加密分组为8字节一组,所以现在我们相当于拥有一组已知的明密文对,而看到这几层加密不难想到是要中间相遇攻击。具体来说,中间相遇的思路是:
- 爆破k1去加密已知明文分组,建立字典
- 爆破k3去解密对应密文分组,查字典
- 如果在字典中,则是可能的k1、k3,可以进行解密来看是否有可见flag来判断是否是正确的密钥对
但是这样没有考虑到k2的影响,事实上由于k2的存在,是没有办法直接用k1的加密值去建立字典的。不过,注意到k2也expand了,所以其对应十六进制的奇数位置的字符都是0,所以这些位置的k1加密值等于没有异或,因此只需要把这些位置的字符存入字典即可。
这样解决了怎么去中间相遇的问题,下一个问题就是复杂度。由于k1、k3都是随机4字节,那么要爆破的话,首先时间复杂度就是2^32往上,并且建立的字典也有2^32项,还要存关键字以及值,所以内存怎么也得用几十G,还得换机器跑。虽然说如果用C++写以及合适的机器跑的话似乎也勉强可以接受,但是还是太臃肿了。
然后突然反应过来这是DES,而DES的密钥看似是64bit,实际上是56bit,因为每个字节的最后一位本来是用于奇偶校验的,并不参与加密解密。因此回看expand后的key就可以少爆8bit,这样一下子就大大减小了时间和空间的压力,几分钟就可以跑出来。
exp:
1 | from Crypto.Cipher import DES |