CryptoCTF 2023 medium分类 团队解题writeup 之二
这次 medium 分类题量是最多的,内容方面是最丰富的,两极分化比较严重。
有脑筋急转弯的,有考察 SageMath API 使用的,有 MISC 题目混进来的……
总之 medium 分类最难的比 tough 还要更难……
Roldy
题目描述
题目的加密基于 ECB 模式的分组加密,以及基于 pyope 包中的 OPE 加密算法。
与远程终端交互,可以知道 flag 的密文,也能进行任意次输入明文,得到加密结果的操作。
试求 flag。
我的解答
首先搜一波 OPE 是什么:
https://crypto.stackexchange.com/questions/37375/order-preserving-encryption-ope-and-leakage
简单来说,OPE 就是 Order-Preserving Encryption(保序加密)的缩写。
接着具体看到 pyope 包的页面:
https://pypi.org/project/pyope/
其中有:
1 | assert cipher.encrypt(1000) < cipher.encrypt(2000) < cipher.encrypt(3000) |
也就是说,该加密函数是单调递增的。
那么就很简单了:可以根据该加密函数单调递增的性质,对明文进行二分,然后根据密文的值是否不小于目标值来决定选哪个子区间。
然后需要注意的一个点:我在实战的时候把输入限定到了可见字符,因为它是要求直接输明文的。这就导致我当时撸了一个类似“向下对齐”的函数。因为我就怕二分的时候分到了一个字符串里面带回车之类的,然后服务器读的时候就把后面的内容给截断了。(实际上好像并没有什么必要?)
然后还有就是服务器在实际操作的时候不是辣么的稳定。有的时候二分分着分着就会突然连接断开。这个时候,我们需要把每一块的至今的二分结果(子区间)存下来。这样,我们就能在下次 run 脚本的时候,恢复之前的子区间,继续二分。
最后就是最后一块有可能不满 16bytes,那么就需要在“向下对齐”的函数里面限定,特殊处理一下。如果观察到 block_size=16
的时候,二分查找分不出来,那么就改 block_size
为 15……不过,好像它 flag 设计的就是正好填满 4 块,所以实战的时候并没有用到这个 trick。
代码如下,其中调整 gao_block
的参数以对指定 block_id
的密文进行二分:
1 | from pwn import * |
得到 flag 为:
1 | CCTF{Boldyreva_5ymMe7rIC_0rD3r_pRe5Erv!n9_3nCryp7i0n!_LStfig9TM} |
Bertrand
题目描述
该问题是关于一个对图像进行加密的算法。
值得关注的是,后面有一句
1 | assert len(key) == 3 |
而且稍微关注一下 encrypt
函数,就知道 msg
和 key
都是 str
类型,且一开始用了 ord
转成 ASCII 码,因此还可以大胆猜测 msg
和 key
都是可见字符。
所以就是可以对 key
进行爆破。
然后具体看到 encrypt
加密算法,该算法分为两部分:第一部分将 msg
打乱,第二部分将打乱后的 msg
按照一定顺序填入一个 $n \times n$ 的图像中。
第一部分:打乱 msg
分为如下若干步,迭代 len(key) = 3
次
w
:key
的长度h
: 取为n**2 // w + 1
,这样就保证w * h > n**2
arr
: 弄一个w
行h
列的的二维数组,将_msg 内容 按列优先 依次填入arr
中,剩下的元素填None
_conf
: 对_key
中的(k_ascii, index)
排序_marshal
:arr[index]
一行一行的,也就是说按照_key
中 ascii 从小到大的索引,对arr
的行进行重排_msg
: 将_marshal
展平为一维数组,并过滤掉None
,最后用key
中内容进行循环模 256 加的加密。
下面是上述第 3 步的解释:如果我们取 w = 3
,h = 4
,n = 3
,msg = 'wuhudasim'
,那么 arr
如下图,其中 /
表示 None
:
然后按行打乱得到 _marshal
,譬如打乱成
然后展平、过滤掉 None
,就得到了 udihamwus
,然后与 key
中内容进行循环模 256 加的加密。
第二部分:将密文填入图像
这里定义了一个 sox
函数,用于控制图像上的 $(x, y)$ 坐标演进。通过代码审计,或是简单的调试,可以看到:
- 这是一个类似于蛇形的、尝试将 $2^k \times 2^k$ 的坐标铺满的坐标生成方式,其中 $k$ 为满足 $2^k \ge n$ 的最小 $k$
- 对于同奇偶的 $n_1, n_2$,如果 $n_1 < n_2$,那么用 $n_1$ 生成的坐标序列是用 $n_2$ 坐标序列的子序列。
性质一的直观展示如下:
性质二的验证代码如下:
1 | def sox(n, d): |
之后还会根据 sum(key) % 4
的值来对图像进行旋转操作。
分析
所以说,因为之前生成的图像是 $n \times n$ 的,所以就可以通过图像的大小得到 $n$ 的大小。
读进来发现 $n = 256$。
代入第一部分的分析,$w = 3, h = 21846$,这样因为 $256 ^ 2 \equiv 1 \pmod{3}$,所以的话在填 arr
的时候,第 0 行就会填多一个,第 1 行和第 2 行都是填的 None
。所以在逆过来,也就是从被压扁的一维数组升到二维数组的时候,要保证把 arr
的行换回去的时候,多出来的东西还是在第 0 行就行了。
另外,对于换位置这种变换,我们可以仅把明文序号代进去,得到加密之后的序号,然后反推即可。
代码如下:
1 | import cv2 |
然后再的时候,会发现运行得非常慢。。。所以在不另谋他法的前提下,需要把上面的代码尝试改成 C++。首先,原生 C++没有图像操作的 API,所以把像素值(包括所有旋转,往回转的)写到文件中,再在 C++中直接操作。同时,由于计算填入图像的坐标比较耗时(需要用到这些坐标,以把二维的图像展成一维的密文)所以也采取先预计算并存文件,再在 C++里面读取的策略。预处理如下:
1 | import pickle |
C++完全从 python 的求解代码改过来。可以采取多线程的技巧。当然,先要在单进程下把程序调通,不然如果连求解的正确性都不能保证的话,那就痛苦了:
1 |
|
编译开启 -O3
优化,链接 pthread
:
1 | g++ solve.cpp -O3 -lpthread -o solve |
在 256 核服务器上也需要跑个几分钟才能跑出结果来。得到 flag 为:
1 | CCTF{3nCrypTioN_8Y_c0lUmn4R_7rAnSp05it!On!} |
当然还有从算法中着手优化的方法:由于整个算法一路下来是线性的,所以可以跟踪密钥的加密情况。
具体来说,就是可以假设一个 _key
的顺序,就可以得到加密步骤中所有的位置重排。
然后在加每轮密钥的时候,记录加的是哪个密钥;在位置重排的时候,和上面的一样,把 _msg
的索引做重排,然后此时密钥的位置也需要跟着 _msg
的索引进行重排。
然后经过加密流程以后,就可以根据 CCTF{
的前缀,去建立 $GF(2^8)$ 下的线性方程组,解出密钥。最后根据解出的密钥对密文进行解密。
但是这样做的话,可能有一个坑点:$GF(2^8)$ 下的线性方程组可能会有 2 的系数,这样解出来的密钥是不唯一的。也就是说,sagemath 的 solve_right
方法只能提供一个解,并且对于 $GF(2^8)$,sagemath 是没有实现 right_kernel
的。不过幸亏我们的系数只会走到 2,所以解出来的密钥可以保证模 $2^7$ 的值是没问题的。所以我们就只需要解出一个密钥之后,枚举一下高位就行了。
因为这里我们用 sagemath 做,所以只需要把 sox
函数所生成的路径持久化为 pickle,在代码中再载入就行了。代码如下:
1 | from PIL import Image |
Insights
题目描述
RSA 中,$p$ 和 $q$ 为 1024 位素数,且高 71 位已知。$N=pq$,$N$ 已知。
解密指数 $d$ 为素数,大概为 $N^{0.2919}$
此外还知道加密指数 $e$ 和密文 $c$,求明文 $m$。
我的解答
解密指数给得非常微妙,正好摸到了 Boneh-Durfee 理论界的顶端。所以一开始会去想莽试一波 Boneh-Durfee。
但是显而易见,这个理论界,就和国足理论上夺取大力神杯一样,太理论了。所以还是需要借助 $p$ 和 $q$ 高位来进行解题。
进一步将高位已知的事实进行形式化:记 $p = p_h + p_l$,$q = q_h + q_l$
那么复习一下,Boneh-Durfee 是利用公钥与私钥之间的关系式:
其中 $s = p + q$
将上式模 $e$ 分析,并记 $A = (N+1) / 2, x = 2k, y=s/2$ 就有:
然后利用 Coppersmith 方法解上式。
类似地,在公钥和私钥的关系式中,将我们已知高位的条件代入进去,可以知道:
所以可以类似地,记 $A = (N+1)/2 - (p_h + q_h)/2, x = 2k, y=(p_l + q_l)/2$ 就有:
就可以用 Coppersmith 方法解,而且问题形式跟上面的一模一样。
然后代码直接根据 RSA-and-LLL-attacks 里面的代码改就行。需要注意里面的默认 $m=4$ 规约出的系数不能保证方程在 $\mathbb{Z}^2$ 上有解,所以需要再调大一点点:
1 | from __future__ import print_function |
解得私钥 $d$ 之后解密:
1 | from Crypto.Util.number import * |
得到明文:
1 | CCTF{RSA_N3w_rEc0rd5_4Nd_nEw_!nSi9h75!} |