今年最后一场认真看题的 CTF 了,周末同时段还有安洵杯,第一天打安洵,第二天来玩 NCTF,拿了两个第一,赢! 两场的 crypto 都 AK 了,安洵的题都是国内赛比较常规的考点,NCTF 的密码题倒还挺好玩的,很有国际赛的感觉,质量很高。
Code Infinite
题目源码附件下载 : Code_Infinite.zip
恢复 Curve 参数
实现了一个基于 Elliptic Curve 的 DH 秘密共享。每次接收我们输入的一个点 作为我们的公钥,然后 server 端将共享密钥值 返回。如果我们能够求解在目标 Elliptic Curve 上的离散对数问题,即可解出服务端的私钥拿到 flag。但是 Elliptic Curve 的参数 都没有给出,于是第一步考虑通过 oracle 拿到 Elliptic Curve 上多组点,将 恢复。
考虑到 Elliptic Curve 的方程 :
拿到 个点 ,其实就等价于拿到了 个线性方程:
消元 得到:
从上述等式我们就能得到模数 的整数倍,对于任意 , ,不断收集这样的 的整数倍,然后 得到最大公因子, 足够大时,大概率就能直接恢复 。得到 之后,直接解模线性方程组即可恢复 。最终得到 :
1 | p = 6277101735386680763835789423207666416083908700390324961279 |
Invalid Curve Attack
恢复曲线后,发现 Elliptic Curve 的阶恰好是一个不等于自身的大素数,在原曲线上求解离散对数是不可行的。发现服务端并没有检查输入的点是否在原曲线 上,存在 Invalid Curve Attack 。
由于 Elliptic Curve 的点加运算、标量乘法都与 的参数 无关,只与 有关,所以可以通过平移曲线 到 上,这样不会改变群的运算,但是 的阶会发生改变,只要 的阶足够光滑,或者有小的素因子,就可以求解(子群上)离散对数,最后再用 CRT 恢复出服务端私钥 。
关于如何寻找光滑的曲线,这题的 比较小,可以直接尝试爆破 的值,然后看阶是否光滑。一般来说阶的最大素因子小于 ,就能很快(几分钟)跑出 DLP 了。当然,实在找不到,就找四个曲线,在各自子群上求 DLP ,只要最终四条曲线上光滑子群阶的 大于 ,也能完全恢复私钥。
其他思路(未验证) :
- 赛后看这题,好像可以尝试选取 进行 singular curve attack,即同态到 上求解,虽然 也不太光滑,但是有限域的 DLP 还是比 ECC 快多了。
- 直接构造特定阶的 Elliptic Curve ,最近看 grhkm 的博客了解到 CM Method 就是构造有限域上特定阶或者迹的曲线的方法,有空可以补上这块,感兴趣的读者可以参考 HackTM Final 上的这题 DDLP (Upsolve)。
EXP
恢复曲线参数
1 | from pwn import remote |
恢复私钥
1 | from pwn import remote |
SteinsGate
题目源码附件下载 : SteinsGate.zip
Padding 算法分析
这题和 hitcon 的 careless padding 出自于同一个篇论文,场景很相似。当初打 hitcon 的 careless padding 时候,exp 太难写了,这题比 hitcon 好写一点。这次刚好补一下之前 hitcon 咕掉的题解。
对 Padding 算法感兴趣的读者可以参考下面两个链接:
- HITCON CTF 2023 - Careless Padding : 官方题解
- 论文 :New Efficient Padding Methods Secure Against Padding Oracle Attacks
首先简单分析一下本题中 Padding 的算法,下图是两个分组情况下的填充格式示意图:
Padding 的字节有 ,其中 分别是倒数第二个分组的第一和第二个字节。 直接加在消息最后,只占一个字节,然后填充 字节直至满足分组长度的整数倍。特别地,当明文消息中 时,上图中 Padding 部分使用的填充字节实际上是 ,以保证 unpad 时不会多截断一个字节。
于是一个简单的 unpad 的流程如下:
取最后一个字节为 ,从填充后消息的末尾开始,依次去掉连续重复的 ,直到碰到第一个不同的字节,记为 ,则 之前的消息均是原始的消息内容,确定消息长度为 。
根据消息长度 ,定位倒数第二个分组,取它的第一第二个字节为 和 ,则正确的填充格式一定满足:
设计这种填充方式,是为了防止诸如 CBC 场景下的 padding oracle 攻击,因为经典场景下的填充格式检查通常只在单个 block 内进行,也就是说在 CBC 分组加密下,我们改变前一个分组的某个字节值,就能恰好对应改变后一个分组的对应字节值,再通过是否满足 padding 格式的信息,我们就能确定最后一个分组的某个字节此时解密为某个特定的值,从而逐字节恢复出最后一个分组的明文,于是在 CBC padding oracle 模式下攻击者就拥有了解密任意分组密文的能力。 而上述填充方式的检查涉及到最后一个分组和倒数第二个分组,因此我们修改倒数第二个分组的密文值,由于雪崩效应,会导致倒数第二个分组的密文解密后的明文发生不可预估的巨大改变,也就说,此时如果我们想通过修改倒数第二个分组的密文,来修改最后一个分组解密得到的 ,则 的变化是完全不可控的,也因此无法从 padding oracle 获取有用信息。
已知明文的 2-block 攻击
考虑下面的密文场景,其中 Block1 密文解密对应的明文是已知的(实际上只要求前两个字节 已知), IV 可以由我们任意修改控制。
显然,这个场景下,我们可以通过 Padding Oracle 泄露出 Block2 的最后两个字节,原理如下:我们不修改 Block1 和 Block2 密文,通过修改 IV 的前面两个字节 去修改 Block1 解密后的 的值,记修改后的值为 ,只有当 的值恰好修改成 或者 ,才会 unpad 成功,该过程最多需要 次交互,在已知明文字符集的情况下,Oracle 的交互次数还能进一步减少。因此通过已知(部分)明文的密文分组,我们可以确定任意一个未知密文分组的最后两个字节的值。
Single Block Attack
我们考虑一下本题中存在的单个密文分组的 Padding Oracle 攻击。
1 | def unpad(msg): |
由于 Python 的语言特性,上面 unpad 过程中的 _X
, _Y
在只有单个分组的情况下(block_count = 1
),变成了本分组的第一、第二个字节。也就说,此时 unpad 是否成功需要检查的信息又回到了单个分组内部,如下图所示(IV 略)
单个 block 的情况下,就是经典的 Padding Oracle 攻击的模式了。此时假设我们通过上节的已知明文攻击,恢复了单个 Block 中最后两个字节 ,此时通过下面的攻击,我们可以解密整个 Block :
解密前两个字节 : 修改 ,修改 的值为 ,直到 unpad 成功。此时有 :
即可成功恢复出 的值。
解密中间字节:固定前两个字节为 ,通过修改 IV 将最后两个字节修改为 ,此时我们通过修改 IV 的倒数第三个字节去修改解密出来的明文的倒数第三个字节,直到 unpad 成功,根据 unpad 的等式即可推导出明文倒数第三个字节的值,以此类推,能够完整解密整个分组。
至此,我们就已经有了在 Padding Oracle 上解密任意密文的能力。
Remarks
实际上,所有与 相关的等式都可能存在两个解,即 的最低比特位是未知,也就是上述攻击中的第二个字节和倒数第一个字节都有 2 种可能。可以通过 padding oracle 确定最后的比特位,也可以通过手动穷举忽略掉这个。
Padding Oracle Attack
回到本题,注意到本题 flag 的未知 byte 都是 hex ,这意味着单个字节只有 16 种可能的取值。给出如下形式的密文:
1 | def chal(): |
通过 CBC 的特性我们知道,给出的 enc
第一个分组解密(ECB)得到的明文是已知的 ,同时第一个密文分组等价于 flag 密文的 IV 值。有了已知明文分组,就能很轻松地进行我们上面阐述地两种攻击,解密任意加密分组了。记我们得到的四个分组如下 ,其中 对应的明文是全 0 字节,基本流程如下:
- 利用诸如 形式的密文,进行已知明文的 2-block 攻击,得到 解密后的最后两个字节。(flag 未知的字节是hex,此步最多需要交互 次)
- 利用诸如 形式的密文,进行单分组攻击,得到 的完整解密结果 。(flag 未知的字节是hex,此步最多需要交互 次)
- 以此类推,解密 。
EXP
flag 是固定的,下面的 exp 没有处理 的低未知比特信息。解密单个 block 的 exp
1 | from pwn import * |
FaultMilestone
题目源码附件下载 : FaultMilestone.zip
Differenti Fault Analysis
这题是很经典的差分故障分析(Differenti Fault Analysis),用的是 DES。之前在 n1ctf 上打过一道基于国密 SM4 的 DFA 题,非预期打的,因为当时分组长度只有 32 比特,可以直接拿 c 写爆破脚本,恢复出轮密钥。这里 DES 的分组长度是 48 比特(密钥长 56 比特),只能用 DFA 打。这题的场景其实就是 : Differential Fault Analysis on DES Middle Rounds 。
分析 encrypt 函数
1 | def encrypt(plain: List[int], sub_keys: List[List[int]],dance=0) -> List[int]: |
在 DES 的第 14 轮加密的时候,引入了一个差分故障,该差分只有单个比特,因此只会影响一个 Sbox,也就是说只会影响 4 个 bit。记 DES 的轮函数为 ,单轮作用如下:
本题中故障差分产生在第 14 轮的右边,也就等价于发生在第十三轮的左边,下图中 (a) 即本题的差分场景:
记差分为 ,未发生故障时密文为 ,其对应的第十六轮输出为 ,发生差分故障后密文为 ,其对应的第十六轮输出为 。由上图中差分的传播路径可知,最终传播到 15 轮末(16 轮起始)左边时,差分的可能取值为
而在第 16 轮,如下差分等式恒成立:
其中 均可从密文 推出,而如果我们已知 ,就能够尝试穷举,从上面的等式中将 可能的值得到。注意到轮函数 中差分的作用是每 6 个比特独立的:
因此,穷举过程只需要 的复杂度。回到本题, 并不是一个确定的取值,而是一个差分集合,但是,该差分只经过了一轮 函数扩散混淆,最多只会影响到扩展的 6 个 bit,在轮密文 输出里最多只会影响到 4 个 bit。因此 至多有 4 个比特不确定,其他均为 0。实际 的差分如下:
1 | { |
将 函数中 permutation 的扩散作用取消 (作用一次 ),可能的差分结果为
1 | [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
只有前 4 个比特是不确定的,后 28 比特完全确定,与我们预期完全相符。因此利用多个故障差分密文对,我们可以恢复出第16 轮轮密钥的绝大多数比特,不使用差分路径的概率去筛选,本题到最后也只有 13 可能的 的取值。
至此,差分故障分析的核心思路就到这里结束了。后面就剩下怎么从 DES 的单次轮密钥恢复出原始密钥比特了。
DES 主密钥恢复
轮密钥生成函数:
1 | def get_sub_key(key: List[int]) -> List[List[int]]: |
DES 的主密钥是 64 位,有效密钥是 56 位,轮密钥是 48 位,而需要解密 DES ,只需要 56 位的有效密钥即可。由上述轮密钥生成过程可以看出,轮密钥由有效密钥进行若干循环移位,最后通过置换(会丢弃 8 个比特)得到,完全是可逆的,因此通过恢复出的 ,我们可以恢复出有效密钥的 48 位,剩下 8 个比特穷举,然后去拿密文解密验证验证即可。
EXP
EXP 以及 Sovler 。
1 | from operator import add |
CalabiYau
题目源码附件下载 :CalabiYau.zip
题解
作者给的代码有 typo ,而且也不能直接跑。整个代码看起来像是 Ring-LWE 的框架。定义有限域 上的多项式商环 ,其中 , 是一个随机生成 2042 比特的大素数。
在 上随机采样二元系数,得到多项式 ,和在 上随机 ,最后公钥如下:
实际上这题和 Ring-LWE 关系不大,这里就直接跳过。来到题目交互部分。第一部分我们能指定一个 seed 来生成 多项式,然后输入一个公钥 ,服务端会返回 ,其中 如下:
其中取整符号对所有系数进行,我们只需要让 ,则 ,即泄露了 alice 的私钥,就能顺利进入三月七的 Party 了(为什么三月七的 Party 都是 Alice 和 Bob 呢,这可太不二次元了,雾)。
之后的场景是一个 Oracle,Bob 私钥公钥信息我们均不知道,但是 Bob 会不断更他的私钥 ,并且把 返回给我们。看到这里,如果对多项式乘法和格比较熟悉,就能发现这其实是一个 Hidden Subset Sum Problem (HSSP)。因为在 上的多项式乘法可以展开成矩阵乘法,即多项式的向量表示为 ,以及,多项式乘法矩阵为
记 ,则 ,本题中 向量固定,每次生成的 都是二元的,即 上的元素都在 上,每个 都对应了集合 的一个子集和问题,但是这里 均未给出,所以是 HSSP 问题。
HSSP 问题的求解方法可以使用正交格攻击,Nguyen-Stern 算法求解,或者 OL+Multivariate 的算法,感兴趣的读者可以参考我另一篇博客:正交格攻击-HSSP 。这里就不再赘述。
EXP
HSSP 的 Solver 直接用的论文 A Polynomial-Time Algorithm for Solving the Hidden Subset Sum Problem 的代码,使用 flatter 用于加速 LLL reduction,论文代码的 recovery binary 函数没有用标准的 NS 算法来写,所以可能恢复不出足够的目标向量,多跑几次就好了。
1 | from pwn import remote, context |
Sign
题目源码附件下载 :sign.zip
题解
真签到题,NTRU 加密,给了公钥,甚至私钥,因此写一个解密函数即可。
1 | import random |