[CTF秀] CTFshow 第二届月饼杯 Crypto部分 个人writeup

本文使用 Zhihu On VSCode 创作并发布


这次现代密码学部分是我出(tou)的题,所以我就没做,结果比赛结束时每题都个位数的解,可能是大佬都去过中秋节团圆去了……

两道古典密码学不难,不能说送分,但是也是古典密码里面比较基础的东西了……

ctfshow_login

题目描述

这道题基于DSA: y=g^x \bmod p 。我们知道 (g, p, y) 但是不知道 x

题目给了签名和验签的代码。

需要消息中包含特定字符串,并且验签通过,才能得到flag。

我的解答

正如题目所说的,只要用任意一个名字成功登录就能拿到flag,也就是说消息 m 需要有对应子串。但是tricky的地方在于:他并没有对消息的大小直接进行判断,而是直接取消息的末1024位 u 进行计算,并且也没有用哈希函数对消息进行哈希。

所以我们就可以让消息的高位为对应子串,然后去构造消息的末1024位 u 使验签通过。

那么问题来了:如何构造在不知道 x 的情况下构造 (u, r, s) 使得验签通过,也就是

g^u \equiv y^r r^s \ (\bmod p)

解法一

我们可以让 r g y 的因子:也就是令

r=g^i y^j

这样我们就可以把问题化归到 g y 上。代入到验签式中:

g^u \equiv y^r g^{si} y^{sj} \ (\bmod p)

也就是我们想要让

\left\{ \begin{aligned} u \equiv si \ (\bmod (p-1)) \\ y \equiv sj \ (\bmod (p-1)) \end{aligned} \right.

那么我们的构造根据这些变量式的约数也就变得清晰了:

  1. 随机生成 (i, j)

  2. 计算 r=g^i y^j

  3. 计算 s=y^{-1} j \bmod (p-1)

  4. 计算 u=si \bmod (p-1)

这就可以构造出满足要求的 (u, r, s)

解法二

利用二次剩余(勒让德符号)的性质:我们知道由费马小定理,

g^{p-1} \equiv 1 \ (\bmod p)

既然题目让我们构造的数都要在 2 \sim p-2 范围内,所以我们肯定不能直接用 p-1 。但是我们可以用 (p-1)/2 来构造:

g^{(p-1)/2} \equiv \left\{ \begin{aligned} & 1 & g\textrm{是模}p\textrm{的二次剩余} \\ & -1 & g\textrm{是模}p\textrm{的非二次剩余} \end{aligned} \right.

所以我们可以在验签式中令 u=r=s=(p-1)/2 ,然后验签式的三部分都变成1或-1。多拼几次人品使得验签式成立即可。

解法一代码如下:

from Crypto.Util.number import *
from random import randint
g = 7
while (True):
    i, j = randint(2, p-2), randint(2, p-2)
    if GCD(j, p-1) != 1:
        continue
    r = pow(g, i, p) * pow(y, j, p) % p
    s = -r * inverse(j, p-1) % (p-1)
    m = s * i % (p-1)
    m = b'Daniu@CTFshow'.hex() + f'{m:0260x}' 
    print(m)
    print(f'{r:x}')
    print(f'{s:x}')
    break

解法二类似

切记务必一定要简单

题目描述

RSA中 N=pq ,其中 p 为1127位素数, q=13/17 \cdot p + \Delta ,其中 \Delta 为573位数, q 为素数,试求得 N=pq 的分解。

我的解答

其实之后 提示 里面就已经有解法了:费马分解

费马分解的思想:利用恒等式

pq=\left(\frac{p+q}{2}\right)^2 - \left(\frac{p-q}{2}\right)^2

并且注意到正整数 t t+1 各自平方之间的间隔:

(t+1)^2-t^2=2t+1=O(t)

也就是说,当 |p-q| 小的时候,我们有

pq \approx \left(\frac{p+q}{2}\right)^2

而且注意到由于 p q 是奇数所以 (p+q)/2 (p-q)/2 是整数。

所以说我们可以枚举 (p+q)/2 ,这个数必定不小于 \sqrt{pq}=\sqrt{N} 。如果 ((p-q)/2)^2 大概是 (p+q)/2 这个量级,也就是 p-q 的量级大概是 p+q 的平方根量级的话,我们就能在很少次数中的枚举中枚举到。

那么实操也就是让 s=(p+q)/2 \sqrt{N} 开始枚举,然后计算 s^2-N 是否为完全平方数:若是,则开根号求得 |p-q|/2 ,并且解线性方程组得到 (p, q) ;若否,则继续枚举。

回到这题,在这题中并不是 |p-q| 小,而是 |17q-13p| 小。

那么我们也可以利用类似的恒等式:

17 \cdot 13 N = \left(\frac{17q+13p}{2}\right)^2 - \left(\frac{17q-13p}{2}\right)^2

以及类似思路来做。

粗略估算下, (17q+13p)/2 这个数大概是1131位。在枚举的过程中,平方的增长是1131位的增长,而 |17q-13p|/2 是576位,其平方大概是1152位。所以期望枚举21位整数(也就是最多尝试 2^{21} 次)即可完成分解。解题代码如下:

from Crypto.Util.number import *
import gmpy2
e = 65537
N = gmpy2.mpz(N)
h1, ok = gmpy2.iroot(17 * 13 * N, 2)
while True:
    h1 += 1
    h2 = h1 ** 2 - 17 * 13 * N
    h2, ok = gmpy2.iroot(h2, 2)
    if (ok):
        break
print(h1)
q = (h1 + h2) // 17
p = N // q
if (p * q != N):
    q = (h1 + h2) // 13
    p = N // q
print(p * q - N)
d = gmpy2.invert(e, (p-1)*(q-1))
m = gmpy2.powmod(c, d, N)
print(long_to_bytes(m))

张八炫CTFshow三结义

题目描述

RSA中给了 N e 未知但是是91以内的素数。

明文 m 被拆分成了 m_1, m_2, m_3 三部分,密文 c_i=m_i^e \bmod N i=1, 2, 3

另外给出明文三部分之和 s=m_1+m_2+m_3

试求明文。

我的解答

e 的限制知道 e 的取值有限。

这题其实就是考察Grobner基的应用:Grobner基求的是多元多项式的“最大公约数”。特别地,如果这些多元多项式都有共同的根的话,就可以利用Grobner基来求出这些根。

所以这题的本质就是枚举 e ,对理想 (x^e-c_1, y^e-c_2, z^e-c_3, x+y+z-s) 求Grobner基,其结果即为明文。

这里值得注意一下就是题目的生成代码是随机生成的 e ,但是实验在 e 比较大的时候求Grobner基可能会耗时较久。我建议如果十分钟后还没有求出结果的话,把容器destroy一下重新刷新一个 e 就好。求解代码如下:

from tqdm import tqdm
from Crypto.Util.number import *
cs = 
P.<x, y, z> = PolynomialRing(Zmod(N))
e_list = [i for i in range(1, 91) if isPrime(i)]
for e in tqdm(e_list):
    G = [x+y+z-s, x^e-cs[0], y^e-cs[1], z^e-cs[2]]
    B = Ideal(G).groebner_basis()
    if (len(B) != 1):
        print(B)

沐沐子倒拔垂杨柳

题目描述

AES-CBC中, IV=key ,并且给了加密和解密的Oracle。

试求key。

我的解答

首先我们看看CBC的加密式和解密式。加密式:

c_1 = \mathrm{Enc}(m_1 \oplus IV)

c_2 = \mathrm{Enc}(m_2 \oplus c_1)

解密式:

m'_1 = \mathrm{Dec}(c'_1) \oplus IV

我们要求的也就是 IV ,肯定是利用解密式所暴露的这个异或的 IV 来求。

在加密式中,我们除了 IV ,其他的值都是知道的。所以我们可以利用第二个式子来构造:假设我们在解密式中令 c'_1=c_2 ,便有:

\begin{align} m'_1 &= \mathrm{Dec}(\mathrm{Enc}(m_2 \oplus c_1)) \\ &=m_2 \oplus c_1 \oplus IV \end{align}

所以我们就可以算出 IV=m'_1 \oplus m_2 \oplus c_1 。求解代码如下:

import binascii
m = '0' * 64
print(m)
m = bytes.fromhex(m)
c = ''
c = bytes.fromhex(c)
def xor(a, b):
    return bytes(x ^ y for x, y in zip(a, b))
c1 = c[:16]
c2 = c[16:32]
m2 = m[16:32]
print(c2.hex())
c_ = ''
c_ = bytes.fromhex(c_)
c_1 = c_[:16]
mm = xor(xor(c1, m2), c_1)
print(mm)

我的木头啊!!!

题目描述

古典密码,只给了一串密文。

我的解答

题目给的密文中, ctfshow 这些字母出现的位置似乎有规律。

联想到题目描述中所提到的栅栏,怀疑是栅栏加密。

但是用 枚举工具 试了一下,发现并没有东西。

所以想到可能是W型栅栏。

直接去网上搜现成的代码,解得

ctfshow{626173653136_MJQXGZJTGI_YmFzZTY0_qzTiEHgB_@UX=h}

发现flag格式归位了,但是flag内容似乎都是编码,看上去第一个是hex(base16)编码,第二个是base32编码……

所以解到这里还没完,要把这些base编码都给解开才算完。这里推荐 解码工具 ,会发现下划线分隔的每部分依次是base16、base32、base64、base58、base85编码,都解开就能得到最后的flag。

解W型栅栏代码如下:

def generate_w(string, n): 
    '''将字符排列成w型'''
    array = [['.']*len(string) for i in range(n)] #生成初始矩阵
    row = 0
    upflag = False
    for col in range(len(string)): #在矩阵上按w型画出string
        array[row][col] = string[col]
        if row == n-1:
            upflag = True
        if row == 0:
            upflag = False
        if upflag:
            row -= 1
        else:
            row += 1
    return array
def encode(string, n):
    '''加密'''
    array = generate_w(string, n)
    msg = []
    for row in range(n): #将每行的字符连起来
        for col in range(len(string)):
            if array[row][col] != '.':
                msg.append(array[row][col])
    return array, msg
def decode(string, n):
    '''解密'''
    array = generate_w(string, n)
    sub = 0
    for row in range(n): #将w型字符按行的顺序依次替换为string
        for col in range(len(string)):
            if array[row][col] != '.':
                array[row][col] = string[sub]
                sub += 1
    msg = []
    for col in range(len(string)): #以列的顺序依次连接各字符
        for row in range(n):
            if array[row][col] != '.':
                msg.append(array[row][col])
    return array, msg
def crack_cipher(string):
    '''破解密码'''
    for n in range(2,len(string)): #遍历所有可能的栏数
        print(str(n)+'栏:'+''.join(decode(string, n)[1]))
if __name__ == "__main__":
    string = "c6_I_@t216MG_0q_Uf673JTYYzBXs{31QJmTTg=hw63XZFZiHho5GzE}"
    n = 5 #栏数
    #若不知道栏数,则遍历所有可能
    crack_cipher(string)
    # #若知道栏数
    # array,msg = decode(string, n)