cryptohack

【补档】courses通关纪念——2025年4月7日~2025年5月12日。

Introduction to CryptoHack --- 介绍

Finding Flags --- 找flag

看到页面上的flag直接CV提交。

Great Snakes --- 独步crypto的Python

能在页面上下载得一个python3文件,直接运行即得flag。

ASCII --- ASCII编码

页面上给你一串ASCII,用下面的python脚本翻译:

a = [99, 114, 121, 112, 116, 111, 123, 65, 83, 67, 73, 73, 95, 112, 114, 49, 110, 116, 52, 98, 108, 51, 125]
for i in a:		# 循环遍历每个元素
    print(chr(i), end='')		# 用chr()将每个元素转为ASCII字符,中间不换行

运行即得flag。

Hex --- 十六进制

写一个python脚本来翻译这串十六进制:

a = "63727970746f7b596f755f77696c6c5f62655f776f726b696e675f776974685f6865785f737472696e67735f615f6c6f747d"
print(bytes.fromhex(a))		# 用bytes.fromhex()将不包含0x的十六进制字符串转换为字节流

Base64 --- Base64编码

根据题目提示,我们要先HEX解密密文,然后再对其进行base64加密。

python解题脚本:

import base64

a = "72bca9b68fc16ac7beeb8f849dca1d8a783e8acf9679bf9269f7bf"
b = bytes.fromhex(a)		# 先转字节流
print(base64.b64encode(b))		# 再用base64.b64encode()做base64加密(根据题目要求)

Bytes and Big Integers --- 字节流与长整型

要把长整形转换为明文消息,python脚本:

from Crypto.Util.number import *

a = 11515195063862318899931685488813747395775516287289682636499965282714637259206269
print(long_to_bytes(a))		# 将长整形转为字节流

XOR Starter --- 异或基础

按照题目指示写一个手动按位异或脚本:

a = 'label'
b = '0001101'        # 13的二进制形式
c = ''
d = ''

# 字符串转换为二进制
for i in a:
    c = c + bin(ord(i))[2:]
for i in range(0, len(a)):
    d = d + b
print(c)
print(d)

# 逐位异或
e = ''
for i in range(0, len(c)):
    if c[i] == d[i]:
        e = e + '0'
    else:
        e = e + '1'
print(e)

# 二进制转回字符串
f = []
for i in range(0, len(e), 7):
    f.append(int(e[i:i+7], 2))
print(f)
for i in f:
    print(chr(i), end='')

做完题请教了一下AI,发现也可以这样:

a = 'label'
for b in a:
    c = ord(b) ^ 13
    print(chr(c), end='')

这样就能知道自己写的代码到底有多鸡肋了。。。

总之,异或符号 ^ 能将两个十进制整数转成二进制按位异或,然后转回十进制整数。

XOR Properties --- 异或性质

异或有四个性质,暂且这样翻译:

交换、结合、
同一(A ⊕ 0 = A)、
自逆(A ⊕ A = 0)

解题脚本:

from pwn import *

KEY1 = 'a6c8b6733c9b22de7bc0253266a3867df55acde8635e19c73313'
KEY2_KEY1 = '37dcb292030faa90d07eec17e3b1c6d8daf94c35d4c9191a5e1e'
KEY2_KEY3 = 'c1545756687e7573db23aa1c3452a098b71a7fbf0fddddde5fc1'
FLAG_KEY1_KEY3_KEY2 = '04ee9855208a2cd59091d04767ae47963170d1660df7f56f5faf'

# 十六进制转字节
KEY1 = bytes.fromhex(KEY1)
KEY2_KEY3 = bytes.fromhex(KEY2_KEY3)
FLAG_KEY1_KEY3_KEY2 = bytes.fromhex(FLAG_KEY1_KEY3_KEY2)

print(xor(FLAG_KEY1_KEY3_KEY2, KEY1, KEY2_KEY3))

重点还是要解释一下怎么回事。

首先,xor() 来自 pwn 库,你可以往其中填任意个字节流或整型参数来做异或。

其次,这里的解密逻辑就仅仅是:

FLAGKEY1KEY3^KEY2 ^ KEY1 ^ KEY2^KEY3

由交换律、结合律:

FLAG^ (KEY1KEY3KEY2) ^ (KEY1 ^ KEY3^KEY2)

由自逆:

FLAG

Favourite byte --- 爆破天选字符

根据题目提示,用某个字符和给定的字符串做异或然后做hex解码,可能刷出flag。

from pwn import *

a = '73626960647f6b206821204f21254f7d694f7624662065622127234f726927756d'
a = bytes.fromhex(a)		# 得先把十六进制串解成字节流才能做异或

for i in range(1, 128):		# 这里左区间原本填的33,但发现没有,所以索性扩大了范围
    print(xor(a, i))		# 这个xor()函数就是好用

然后在输出结果中Ctrl+F搜“crypto”即可。

You either know, XOR you don't --- 从异或到已知明文攻击

首先给到我们一串密文,这串密文是明文和要猜测的密码异或得到的。

我们已经知道了明文的一部分:crypto{

由于:

明文 ^ 密钥 = 密文

因此两边同异或明文,且根据结合律:

(明文 ^ 明文) ^ 密钥 = 明文 ^ 密文

因此根据自逆性:

0 ^ 密钥 = 明文 ^ 密文

因此根据同一性:

密钥 = 明文 ^ 密文

因此:

密钥 = 部分正确明文 ^ 密文

用这样的脚本来解密钥:

from pwn import *

a = '0e0b213f26041e480b26217f27342e175d0e070a3c5b103e2526217f27342e175d0e077e263451150104'
b = 'crypto{'
a = bytes.fromhex(a)

print(xor(b.encode(), a))

结果:

b'myXORke+y_Q\x0bHOMe$~seG8bGURN\x04DFWg)a|\x1dTM!an\x7f'

猜测密钥:

myXORkey

替换一下变量b就能解密了:

from pwn import *

a = '0e0b213f26041e480b26217f27342e175d0e070a3c5b103e2526217f27342e175d0e077e263451150104'
b = 'myXORkey'
a = bytes.fromhex(a)

print(xor(b.encode(), a))

Modular Arithmetic --- 模运算

Greatest Common Divisor --- 最大公因数

这题的要点是,你需要用辗转相除法来求最大公因数,逻辑大概是这样:

比如,要算 gcd(12, 8):

12 = 1 * 8 + 4

(这个“1”位置上的数字不重要,是 a//b 得来的)

(这个“4”位置上的数字是重要的,是 a%b 得来的)

这时候根据辗转相除法:gcd(12, 8) = gcd(8, 4)

然后继续算 gcd(8, 4):

8 = 2 * 4 + 0

同理,这时候 gcd(8, 4) = gcd(4, 0) = 4

因此,gcd(12, 8) = gcd(4, 0) = 4

根据这个逻辑来写脚本:

def euclid(a, b):
    while(1):
        c = a % b
        if c == 0:
            print(b)
            break
        else:
            a = b
            b = c

a = 66528
b = 52920
if a >= b:
    euclid(a, b)
else:
    euclid(b, a)

每次用辗转相除法都这样写函数显然太麻烦了,经过查询,发现这样写更好:

from gmpy2 import *

a = 66528
b = 52920

print(gcd(a, b))

Extended GCD --- 贝祖等式

这题要手搓贝祖等式(拓展欧几里得算法)的函数太难了,直接用gmpy2库中现成的,脚本:

from gmpy2 import *

p = 26513
q = 32321

print(gcdext(p, q))		# 这个“ext”就是extended,这个函数用来实践贝祖等式

结果:

(mpz(1), mpz(10245), mpz(-8404))

第一个是gcd(p, q),第二个是u,第三个是v。

这里提交v值即可。

Modular Arithmetic 1 --- 模运算基础

这里用简单的取余(模运算的本质)就能完成:

print(11 % 6)		# 输出5
print(8146798528947 % 17)		# 输出4

Modular Arithmetic 2 --- 欧拉定理、费马小定理

这题一堆云里雾里的表达,综合起来让我想起的就是欧拉定理

aΦ(m)1 (mod m)a^{Φ(m)} ≡ 1\ (mod\ m)

如果你的m是个素数:

am11 (mod m)a^{m-1} ≡ 1\ (mod\ m)

两边同乘a能推出费马小定理(这样就不用背了):

ama (mod m)a^{m} ≡ a\ (mod\ m)

其实只要你知道欧拉定理,这题就一眼识破,答案为 1

但这里还是写个python脚本意思一下:

from gmpy2 import *

a = 273246787654
p = 65537
print(powmod(a, p-1, p))		# 算V1的V2次方模p

Modular Inverting --- 乘法逆元

如果你学过信息安全数学基础,你可能知道这玩意儿是在讲乘法逆元

页面上提示我们用费马小定理,能联想到欧拉定理:

aΦ(m)1 (mod m)a^{Φ(m)} ≡ 1\ (mod\ m)

这里13恰好是个素数,所以可以知道:

3121 (mod 13)3^{12} ≡ 1\ (mod\ 13)

所以:

d311 (mod 13)d ≡ 3^{11}\ (mod\ 13)

然后用这个脚本算d:

from gmpy2 import *

print(powmod(3, 11, 13))

很显然,如果你要靠穷举得出逆元,可能也行:

r = 1

while 1:		# 其实这里不用死循环,r注定小于模数13
    if (3 * r) % 13 == 1:
        print(r)
        break
    else:
        r = r + 1

相应地,求乘法逆元也有对应的gmpy2函数,于是可以有这样的脚本能直接算出乘法逆元d:

from gmpy2 import *

print(invert(3, 13))

Quadratic Residues --- 二次剩余

如果你学过信息安全数学基础,你可能知道这玩意儿是在讲二次剩余

这里用一手穷举法:

a = 1       # 穷举值
p = 29
ints = [14, 6, 11]

while 1:
    for i in ints:      # 每次都验证三个参数是否有满足二次剩余的
        if ((a * a) - i) % p == 0:
            print('a = ', a)        # 有就输出数值,并结束程序
            print('i = ', i)
            exit(0)
    a = a + 1       # 没有就自增,继续循环

Legendre Symbol --- 勒让德符号

如果你学过信息安全数学基础,你可能知道这玩意儿是在讲勒让德符号

这里主要记录一些启发我的东西:

首先,根据题目,我们会从ints中找到一个n,关于模p二次剩余,即:勒让德符号模p同余1:

(ap)  ap12  1  mod p(\frac{a}{p})\ ≡\ a^{\frac{p-1}{2}}\ ≡\ 1\ \ mod\ p

因此两边乘a可推:

ap+12  a  mod pa^{\frac{p+1}{2}}\ ≡\ a\ \ mod\ p

然而,二次剩余定义:

x2  a  mod px^2\ ≡\ a\ \ mod\ p

因此:

(ap+14)2  x2  mod p(a^{\frac{p+1}{4}})^2\ ≡\ x^2\ \ mod\ p

因此:

ap+14  x  mod pa^{\frac{p+1}{4}}\ ≡\ x\ \ mod\ p

(并且据此来看,p得满足 4k+3 的形式,否则指数就不是整数了)

python脚本:

from gmpy2 import *

p = 		# 这两个变量数据量太大了,详见网站给的output.txt
ints = 
a = 0

for i in ints:
    if powmod(i, (p-1)//2, p) == 1:		# 如果勒让德符号等于1,就是满足二次剩余,就记到变量a中
        a = i
        break
if p % 4 == 3:		# 用上面推出的二次剩余公式求解
    print(powmod(a, (p+1)//4, p))

Modular Square Root --- Tonelli–Shanks 算法

当 p 满足 4k+1 的形式而不是 4k+3 的形式时,上一题的办法不管用了,于是得用 Tonelli–Shanks 算法 来求解二次剩余。由于这个算法步骤过于繁杂,所以这里直接找AI写了个Exp脚本:

import gmpy2

def tonelli_shanks(a, p):
    if not gmpy2.is_prime(p):
        raise ValueError("p 必须是一个素数")
    if gmpy2.legendre(a, p) != 1:
        raise ValueError("a 不是模 p 的二次剩余,无法求平方根")

    # Step 1:分解 p - 1 为 Q * 2^S
    Q = p - 1
    S = 0
    while Q % 2 == 0:
        Q //= 2
        S += 1

    # Step 2:找到一个模 p 的二次非剩余 z
    for z in range(2, p):
        if gmpy2.legendre(z, p) == -1:
            break

    # Step 3:初始化变量
    c = gmpy2.powmod(z, Q, p)
    R = gmpy2.powmod(a, (Q + 1) // 2, p)
    t = gmpy2.powmod(a, Q, p)
    M = S

    # Step 4:迭代修正
    while t != 1:
        i = 1
        temp = gmpy2.powmod(t, 2, p)
        while temp != 1:
            temp = gmpy2.powmod(temp, 2, p)
            i += 1
            if i == M:
                raise Exception("无法找到平方根")

        b = gmpy2.powmod(c, 2 ** (M - i - 1), p)
        R = (R * b) % p
        t = (t * b * b) % p
        c = (b * b) % p
        M = i

    return R

# ======= 用户输入部分 =======
if __name__ == "__main__":
    print("=== Tonelli–Shanks 算法求模平方根(求解二次剩余) ===")
    a = int(input("请输入 a(被开方数):"))
    p = int(input("请输入 p(模数,需为素数):"))

    try:
        x = tonelli_shanks(a, p)
        print(f"\n满足 x² ≡ a mod p 的")
        print(f"第一个解是:x = {x}")
        print(f"另一个解是:x = {p - x}")
    except Exception as e:
        print("发生错误:", e)

这个算法其实对 p 满足 4k+1 的形式和 4k+3 的形式时都有效,但是如果 p=4k+3 ,还是上一题的办法高效一点。

Chinese Remainder Theorem --- 中国剩余定理

中国剩余定理!!!这我熟啊(可能?),公式:

x  b1M1M11 + b2M2M21 + b3M3M31 +   mod mx\ ≡\ b_1·M_1·M_1^{-1}\ +\ b_2·M_2·M_2^{-1}\ +\ b_3·M_3·M_3^{-1}\ +\ ……\ \ mod\ m

脚本:

from gmpy2 import *

b_n = [2, 3, 5]
m_n = [5, 11, 17]
m = 1
M_n = []
rM_n = []

for i in m_n:		# 求出m
    m = m * i

for i in m_n:		# 求出各个M
    M_n.append(m // i)

for i in range(0, len(m_n)):
    rM_n.append(invert(M_n[i], m_n[i]))

# 中国剩余定理公式
x = 0
for i in range(0, len(m_n)):
    x = x + b_n[i] * M_n[i] * rM_n[i]
x = x % m
print(x)

(二周目做法)如果你用Sage(我当时做到了椭圆曲线章节才懂得用的工具):

from sage.all import *

b_n = [2, 3, 5]
m_n = [5, 11, 17]

print(crt(b_n, m_n))

Adrien's Signs? --- 选择明文攻击

这题要求我们根据加密器和密文反推出明文。那么我们可以通过分析加密器来构造解密器,现在开始分析(这里我把加密器的代码稍稍修改了一下,影响不大,好审一些):

from random import randint		# 依赖

a = 288260533169915		# 四个将用的参数
p = 1007621497415251
flag = b'crypto{????????????????????}'
ciphertext = []
plaintext = ''.join([bin(i)[2:].zfill(8) for i in flag])		# 大概就是,遍历flag变量中的每个字符,转二进制,去掉“0b”,在左侧填充0直到8位。然后把很多个8位拼成一个字符串
for b in plaintext:		# 把二进制字符串的每一位挑出来做处理
    e = randint(1, p)		# 随机生成一个e,大小介于1到1007621497415251之间
    n = pow(a, e, p)		# 计算,使n等于a的e次方模p
    if b == '1':		# 如果b等于1,就不做处理,直接把n拼接到ciphertext末尾成为一个元素
        ciphertext.append(n)
    else:		# 如果b等于0,就让n等于-n%p,再把n拼接到ciphertext末尾成为一个元素
        n = -n % p		# 因为经过前面的处理,n<p,因此这行等效于:n = p - n
        ciphertext.append(n)
print(ciphertext)		# 打出来,output.txt中的内容

所以加密的逻辑是,根据flag的内容来生成一个二进制字符串,然后用随机生成的e按一定方式生成一个n,根据二进制字符串看要不要再做处理,最后拼接输出。

这里其实看了题解,但还是感觉有点费解,只能说是输出个人理解了:

from random import randint
from sympy import *

a = 288260533169915
p = 1007621497415251
with open('./output.txt', 'r') as file:
    temp = file.read()
payload = ''

temp = temp[1:-2]       # 注意,这时候读出的文件是字符串,你要把它处理成列表
n = temp.split(', ')

for i in n:
    print(i)
    try:
        discrete_log(p, int(i), a)      # 离散对数计算函数,计算失败会报错
        payload = payload + '1'
    except:
        payload = payload + '0'

for i in range(0, len(payload), 8):     # 八位二进制翻译为一个字符
    print(chr(int(payload[i:i+8], 2)), end='')

Modular Binomials --- 共模攻击

尝试对这题做一点数学上的解释:

首先我们有:

N=pqc1=(2p+3q)e1 modNc2=(5p+7q)e2 modNN=p⋅q\\ c_1=(2⋅p+3⋅q)^{e1}\ mod  N\\ c_2=(5⋅p+7⋅q)^{e2}\ mod  N

这里在结构上很像RSA公钥加密明文的场景,但明文不一致。先尝试构造以使上下两式明文接近:(这里可以构造使p的系数相等)

(针对式2,两边同乘 5的e1次方 ,然后两边同时取 e2次方,得式4)

(c15e1)e2={5e1(2p+3q)e1}e2={5(2p+3q)}e1e2=(10p+15q)e1e2  modN(c_1·5^{e1})^{e2}=\{5^{e1}·(2⋅p+3⋅q)^{e1}\}^{e2}\\=\{5(2⋅p+3⋅q)\}^{e1·e2}\\=(10·p+15·q)^{e1·e2}\ \ mod  N\\

(针对式3,两边同乘 2的e2次方 ,然后两边同时取 e1次方,得式5)

(c22e2)e1={2e2(5p+7q)e2}e1={2(5p+7q)}e1e2=(10p+14q)e1e2  modN(c_2·2^{e2})^{e1}=\{2^{e2}·(5⋅p+7⋅q)^{e2}\}^{e1}\\=\{2(5⋅p+7⋅q)\}^{e1·e2}\\=(10·p+14·q)^{e1·e2}\ \ mod  N\\

通过刚才的尝试逼近,明文的差值缩小到了一个q!!!

那么,如果让式4和式5相减……不管你次方数e1·e2是多少,只包含p的项由于系数相同总会被消掉,那么结论就是,相减的结果会是q的倍数。从给定的data.txt中,我们可以算出这个“相减的结果”。

由于N的因数有1和它本身,还有p和q。那么这就说明:N和“相减的结果“不仅有公因数1,还有公因数q

但p和N本身可能是“相减的结果“的因数吗?由于题目给的data.txt中的数据都很大,并且没有什么直接的数学证据能说明这一点,所以概率非常非常小。到这里我们就能认为:N和“相减的结果“的最大公因数就是q。(即使不是,也不用紧张——假如实际上最大公因数真的是p,你照样把题做出来;最大公因数不可能是N,因为模N运算消掉了)

总之,对 N和“相减的结果“ 取最大公因数能得到N。根据式1,N//q 能得到p。

数学问题解决了,现在是编程问题了:

from gmpy2 import *

N = 14905562257842714057932724129575002825405393502650869767115942606408600343380327866258982402447992564988466588305174271674657844352454543958847568190372446723549627752274442789184236490768272313187410077124234699854724907039770193680822495470532218905083459730998003622926152590597710213127952141056029516116785229504645179830037937222022291571738973603920664929150436463632305664687903244972880062028301085749434688159905768052041207513149370212313943117665914802379158613359049957688563885391972151218676545972118494969247440489763431359679770422939441710783575668679693678435669541781490217731619224470152467768073
e1 = 12886657667389660800780796462970504910193928992888518978200029826975978624718627799215564700096007849924866627154987365059524315097631111242449314835868137
e2 = 12110586673991788415780355139635579057920926864887110308343229256046868242179445444897790171351302575188607117081580121488253540215781625598048021161675697
c1 = 14010729418703228234352465883041270611113735889838753433295478495763409056136734155612156934673988344882629541204985909650433819205298939877837314145082403528055884752079219150739849992921393509593620449489882380176216648401057401569934043087087362272303101549800941212057354903559653373299153430753882035233354304783275982332995766778499425529570008008029401325668301144188970480975565215953953985078281395545902102245755862663621187438677596628109967066418993851632543137353041712721919291521767262678140115188735994447949166616101182806820741928292882642234238450207472914232596747755261325098225968268926580993051
c2 = 14386997138637978860748278986945098648507142864584111124202580365103793165811666987664851210230009375267398957979494066880296418013345006977654742303441030008490816239306394492168516278328851513359596253775965916326353050138738183351643338294802012193721879700283088378587949921991198231956871429805847767716137817313612304833733918657887480468724409753522369325138502059408241232155633806496752350562284794715321835226991147547651155287812485862794935695241612676255374480132722940682140395725089329445356434489384831036205387293760789976615210310436732813848937666608611803196199865435145094486231635966885932646519

q = gcd(N, powmod((c2 * powmod(2, e2, N)), e1, N) - powmod((c1 * powmod(5, e1, N)), e2, N))     # 这里不用操心谁减谁,求最大公因数结果都一样
p = N//q

print('crypto{', p, ',', q, '}', sep='')        # 加个sep=''以便于复制粘贴直接交flag

Symmetric Cryptography --- 对称密码体制

Keyed Permutations --- 双射

描述:x和y之间存在一一对应的关系,从x可以到y,从y也可以回到x。这是 双射 ,英文 bijection(flag中缺的是这个)。

Resisting Bruteforce --- 抗爆破

目前有一种攻击方式能把 AES-128 的安全级别降低到 126.1 位,叫做:Biclique

Structure of AES --- AES的结构

这题要我们根据 bytes2matrix 函数补全其逆函数 matrix2bytes ,这是题目给的代码文件:

def bytes2matrix(text):
    """ Converts a 16-byte array into a 4x4 matrix.  """
    return [list(text[i:i+4]) for i in range(0, len(text), 4)]

def matrix2bytes(matrix):
    """ Converts a 4x4 matrix into a 16-byte array.  """
    ????		# 你要把这四个问号替换为逆函数的语句

matrix = [
    [99, 114, 121, 112],
    [116, 111, 123, 105],
    [110, 109, 97, 116],
    [114, 105, 120, 125],
]

print(matrix2bytes(matrix))

把四个问号替换为如下内容,运行即可得到flag:

# 遍历矩阵中的每个元素,转为字符后拼接输出
result = ''
for m in matrix:
    for n in m:
        result = result + chr(n)
return result

Round Keys --- 轮密钥

这是轮密钥加。但实施方式和我记忆中的有出入,但就先按题目这么办。

题目给出的加密代码:

state = [
    [206, 243, 61, 34],
    [171, 11, 93, 31],
    [16, 200, 91, 108],
    [150, 3, 194, 51],
]

round_key = [
    [173, 129, 68, 82],
    [223, 100, 38, 109],
    [32, 189, 53, 8],
    [253, 48, 187, 78],
]


def add_round_key(s, k):
    ???		# 我们要补写这部分内容


print(add_round_key(state, round_key))

根据题目介绍,这里只要尝试让矩阵中的元素依次对位异或即可,于是 add_round_key 函数内容如下:

result = ''
for m in range(4):
    for n in range(4):
        result = result + chr(s[m][n] ^ k[m][n])
return result

Confusion through Substitution --- 字节代换

这个是字节代换。字节代换的步骤大概是,对着要做加密的矩阵,挨个元素加密。把元素拿出来后,把它的十六进制值(去掉“0f”)当做字符串,第一个字符作为S盒的行,第二个字符作为S盒的列,据此从S盒中取出新元素替换原本的元素。

这题大概就是希望我们把state矩阵丢到反向S-box中解密一下。

把题目给的代码中的函数 sub_bytes 写为:

def sub_bytes(s, sbox=s_box):		# 虽然这里本质上应该是“inv_sub_bytes”才对
    result = ''
    for m in range(4):		# 循环遍历待处理矩阵中的每个元素
        for n in range(4):
            row_n = int(str(hex(s[m][n]))[2:3], 16)		# 提取十六进制值(去掉“0f”)的第一个字符,将其转换为数字
            col_n = int(str(hex(s[m][n]))[3:4], 16)		# 提取十六进制值(去掉“0f”)的二个字符,将其转换为数字
            place = row_n * 16 + col_n		# 这里的S盒不是二维数组,而是个元组(跟列表差不多的东西),所以要靠这样找到新元素
            result = result + chr(inv_s_box[place])		# 从S盒中取出新元素,转为字符拼接到result后面
    return result

运行即得到flag。

后续完成了 Bringing It All Together 后,对这里的函数有了新理解:

def sub_bytes(s, sbox=inv_s_box):	
    result = ''
    for m in range(len(s)):
        for n in range(len(s[m])):
            s[m][n] = (sbox[s[m][n]])
            result = result + chr(s[m][n])
    return result

Diffusion through Permutation --- 行移位、列混淆

这个是行移位列混淆(本题不需要自行实现列混淆)。

我们要补全 inv_shift_rows 函数,然后让 state 矩阵先做反向列混淆,然后做反向行移位。

我们可以一句现有的 shift_rows 来思考如何完成 inv_shift_rows 函数。在我的印象里,行移位是循环左移动的,但这里python代码因为某些原因,呈现出“循环下移”的形态。总之我们就是能写出它的逆函数即可,写着问号的地方替换为:

s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]

代码末尾还要补代码,运行获得flag:

inv_mix_columns(state)		# 反向列混淆,因为没返回值,直接用函数修改矩阵
inv_shift_rows(state)		# 反向行移位
for m in range(4):		# 依次输出矩阵代表的内容
    for n in range(4):
        print(chr(state[m][n]), end='')

Bringing It All Together --- AES_BOSS关

本关就是把之前出现过的AES解密代码都给搬到这关来,python脚本:(这个脚本也可作为常规AES解密脚本使用)

N_ROUNDS = 10

key = b'\xc3,\\\xa6\xb5\x80^\x0c\xdb\x8d\xa5z*\xb6\xfe\\'
ciphertext = b'\xd1O\x14j\xa4+O\xb6\xa1\xc4\x08B)\x8f\x12\xdd'
s_box = (
    0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
    0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
    0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
    0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
    0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
    0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
    0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
    0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
    0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
    0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
    0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
    0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
    0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
    0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
    0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
    0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)
inv_s_box = (
    0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
    0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
    0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
    0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
    0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
    0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
    0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
    0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
    0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
    0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
    0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
    0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
    0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
    0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
    0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
    0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)


def bytes2matrix(text):
    """ Converts a 16-byte array into a 4x4 matrix.  """
    return [list(text[i:i + 4]) for i in range(0, len(text), 4)]


def matrix2bytes(matrix):
    """ Converts a 4x4 matrix into a 16-byte array.  """
    result = ''
    for m in matrix:
        for n in m:
            result = result + chr(n)
    return result


def inv_shift_rows(s):
    s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
    s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
    s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]


def add_round_key(s, k):		# 理解更新
    for m in range(len(s)):
        for n in range(len(s[m])):
            s[m][n] = s[m][n] ^ k[m][n]


def inv_sub_bytes(s, sbox=inv_s_box):		
    for m in range(len(s)):
        for n in range(len(s[m])):
            s[m][n] = (sbox[s[m][n]])       # 更新了对S盒取值的理解


xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)


def mix_single_column(a):
    # see Sec 4.1.2 in The Design of Rijndael
    t = a[0] ^ a[1] ^ a[2] ^ a[3]
    u = a[0]
    a[0] ^= t ^ xtime(a[0] ^ a[1])
    a[1] ^= t ^ xtime(a[1] ^ a[2])
    a[2] ^= t ^ xtime(a[2] ^ a[3])
    a[3] ^= t ^ xtime(a[3] ^ u)


def mix_columns(s):
    for i in range(4):
        mix_single_column(s[i])


def inv_mix_columns(s):
    # see Sec 4.1.3 in The Design of Rijndael
    for i in range(4):
        u = xtime(xtime(s[i][0] ^ s[i][2]))
        v = xtime(xtime(s[i][1] ^ s[i][3]))
        s[i][0] ^= u
        s[i][1] ^= v
        s[i][2] ^= u
        s[i][3] ^= v
    mix_columns(s)


def expand_key(master_key):
    """
    Expands and returns a list of key matrices for the given master_key.
    """

    # Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants
    r_con = (
        0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
        0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
        0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
        0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
    )

    # Initialize round keys with raw key material.
    key_columns = bytes2matrix(master_key)
    iteration_size = len(master_key) // 4

    # Each iteration has exactly as many columns as the key material.
    i = 1
    while len(key_columns) < (N_ROUNDS + 1) * 4:
        # Copy previous word.
        word = list(key_columns[-1])

        # Perform schedule_core once every "row".
        if len(key_columns) % iteration_size == 0:
            # Circular shift.
            word.append(word.pop(0))
            # Map to S-BOX.
            word = [s_box[b] for b in word]
            # XOR with first byte of R-CON, since the others bytes of R-CON are 0.
            word[0] ^= r_con[i]
            i += 1
        elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
            # Run word through S-box in the fourth iteration when using a
            # 256-bit key.
            word = [s_box[b] for b in word]

        # XOR with equivalent word from previous iteration.
        word = bytes(i ^ j for i, j in zip(word, key_columns[-iteration_size]))
        key_columns.append(word)

    # Group key words in 4x4 byte matrices.
    return [key_columns[4 * i: 4 * (i + 1)] for i in range(len(key_columns) // 4)]


def decrypt(key, ciphertext):
    round_keys = expand_key(key)  # Remember to start from the last round key and work backwards through them when decrypting

    # Convert ciphertext to state matrix
    state = bytes2matrix(ciphertext)

    # Initial add round key step
    add_round_key(state, round_keys[-1])

    for i in range(N_ROUNDS - 1, 0, -1):
        inv_shift_rows(state)
        inv_sub_bytes(state, inv_s_box)
        add_round_key(state, round_keys[i])
        inv_mix_columns(state)

    # Run final round (skips the InvMixColumns step)
    inv_shift_rows(state)
    inv_sub_bytes(state, inv_s_box)
    add_round_key(state, round_keys[0])

    # Convert state matrix to plaintext
    plaintext = matrix2bytes(state)

    return plaintext


print(decrypt(key, ciphertext))

Modes of Operation Starter --- 电码本模式1_简介

跳转页面后能看到的代码:

from Crypto.Cipher import AES


KEY = ?
FLAG = ?


@chal.route('/block_cipher_starter/decrypt/<ciphertext>/')
def decrypt(ciphertext):
    ciphertext = bytes.fromhex(ciphertext)		# 转为字节流,存入ciphertext中

    cipher = AES.new(KEY, AES.MODE_ECB)		# 确定使用电码本模式
    try:
        decrypted = cipher.decrypt(ciphertext)		# 对密文使用AES加密
    # except ValueError as e:		# 报错部分,不管
        # return {"error": str(e)}

    return {"plaintext": decrypted.hex()}		# 输出十六进制加密后的结果


@chal.route('/block_cipher_starter/encrypt_flag/')
def encrypt_flag():
    cipher = AES.new(KEY, AES.MODE_ECB)		# 选择电码本模式
    encrypted = cipher.encrypt(FLAG.encode())		# 把flag先转字节流,然后AES加密

    return {"ciphertext": encrypted.hex()}

通过按页面的加密按钮可知:encrypt_flag 的结果已知且固定,它的来历是,flag用key做AES加密,然后把密文转十六进制输出。

而 decrypt 函数会把输入的内容先做十六进制解密,然后再做电码本解密,然后十六进制加密输出。这几乎就是 encrypt_flag 函数的逆函数,只是最后多了个十六进制加密。

页面中提供了 decrypt 函数的使用按钮,把下面的值填进去,点击加密:

bf791ff22629657bedc29428674f686758cc40d847834a4665a205bf540fbdfb

把这段值填到解密框中解密,按照我们前面分析的,接下来会得到flag的十六进制加密值:

63727970746f7b626c30636b5f633170683372355f3472335f663435375f217d

然后用下面脚本解密即可得到flag:

a = "63727970746f7b626c30636b5f633170683372355f3472335f663435375f217d"

print(bytes.fromhex(a))

Passwords as Keys --- 电码本模式2_密钥爆破

跳转页面后能看到的代码:

from Crypto.Cipher import AES
import hashlib
import random


with open("/usr/share/dict/words") as f:
    words = [w.strip() for w in f.readlines()]		# 掏出密码字典,逐行写到words列表中
keyword = random.choice(words)		# 随便选一条密码

KEY = hashlib.md5(keyword.encode()).digest()		# 把密钥MD5加密
FLAG = ?


@chal.route('/passwords_as_keys/decrypt/<ciphertext>/<password_hash>/')
def decrypt(ciphertext, password_hash):
    # 先把传入的密文和密钥做十六进制解密
    ciphertext = bytes.fromhex(ciphertext)
    key = bytes.fromhex(password_hash)

    # AES解密
    cipher = AES.new(key, AES.MODE_ECB)
    try:
        decrypted = cipher.decrypt(ciphertext)
    # except ValueError as e:
    #     return {"error": str(e)}

    # 将要输出的结果做十六进制加密
    return {"plaintext": decrypted.hex()}


@chal.route('/passwords_as_keys/encrypt_flag/')
def encrypt_flag():		# 还是老样子,拿着key对flag做AES加密,然后再十六进制加密输出结果
    cipher = AES.new(KEY, AES.MODE_ECB)
    encrypted = cipher.encrypt(FLAG.encode())

    return {"ciphertext": encrypted.hex()}

通过重复按页面上的加密按钮,发现 encrypt_flag 函数输出结果仍然固定:

c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66

这段值稍后要填到解密框中解密的,但现在按照我们对代码的分析,还差一个被十六进制加密后的哈希值。

按照题目提示,这段哈希值的背后是一个词条,所以可以考虑爆破,直到输出长得像flag的东西。

第一步,仿照题目要求,结合弱密码字典制作出被十六进制加密后的哈希值的新字典,脚本:

import hashlib
import random


with open("./words.txt", 'r', encoding='utf-8') as f1:		# 这里加载的是网上找的20万行大字典,来自https://raw.githubusercontent.com/eneko/data-repository/master/data/words.txt
    words = [w.strip() for w in f1.readlines()]

with open("./pass.txt", 'w', encoding='utf-8') as f2:       # 自动创建一个新文件写入
    for i in words:
        KEY = hashlib.md5(i.encode()).digest()
        f2.write(KEY.hex() + '\n')
        

这样你就会获得一个新字典 pass.txt 。

因为当时题目提示我去试一下HTTP请求,所以接下来开个抓包软件看一下HTTP请求的结构,这是其中一次访问的请求头:

GET /passwords_as_keys/decrypt/c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66/e10adc3949ba59abbe56e057f20f883e/ HTTP/1.1

相当简单,结构就只是:

GET /passwords_as_keys/decrypt/十六进制加密后的密文/十六进制加密后的哈希值/ HTTP/1.1

那么这样要发HTTP请求就很简单了,爆破性地发请求,写个爬虫把输出结果爬取到,然后十六进制解密开:

import requests
import re


with open("./pass.txt") as f:
    words = [w.strip() for w in f.readlines()]

ciphertext = 'c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66'

for i in words:
    r = requests.get(f'http://aes.cryptohack.org/passwords_as_keys/decrypt/{ciphertext}/{i}/').text      # 发请求
    plaintext = re.search(':"(.*)"', r)     # 返回的结果大概是 {"plaintext":"十六进制加密后的明文"} 这样的,正则表达式匹配截取一下
    print(bytes.fromhex(plaintext[1]))

这个早期的设想失败了,代码有点问题。并且在20万行请求的情况下,我无法很快解出题目。

那我就写题目中提醒的“离线攻击密文”好了,直接使用对方的解密器:

from Crypto.Cipher import AES


def decrypt(ciphertext, password_hash):
    ciphertext = bytes.fromhex(ciphertext)
    key = bytes.fromhex(password_hash)

    cipher = AES.new(key, AES.MODE_ECB)
    try:
        decrypted = cipher.decrypt(ciphertext)
    except ValueError as e:
        return {"error": str(e)}
    return decrypted


with open("./pass.txt") as f:
    words = [w.strip() for w in f.readlines()]

c = 'c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66'

for i in words:
    try:		# 这里原本不是try、except结构的,而是简单粗暴地写“ print(decrypt(c, i)) ”,结果在20万行结果中找不到flag。但经过尝试后我恍然大悟——有些字节如果不解密,它就是你看不懂的样子,像乱码一样,所以还是得decode解码(原本以为这个decode解码没有用来着……)
        print(decrypt(c, i).decode())
    except:
        continue

ECB Oracle --- 电码本模式3_逐字节爆破

跳转页面后能看到的代码:

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad


KEY = ?
FLAG = ?


@chal.route('/ecb_oracle/encrypt/<plaintext>/')
def encrypt(plaintext):		# 我们能输入明文
    plaintext = bytes.fromhex(plaintext)		# 十六进制解密

    padded = pad(plaintext + FLAG.encode(), 16)		# 把我们的plaintext和字节流flag拼接后,分成多个十六字节长的小块,不足十六字节的要补全。16字节一个块在AES中算经典的
    # 接下来进行AES加密,并输出十六进制加密后的密文
    cipher = AES.new(KEY, AES.MODE_ECB)
    try:
        encrypted = cipher.encrypt(padded)
    # except ValueError as e:
        # return {"error": str(e)}

    return {"ciphertext": encrypted.hex()}

这里由于不知道padded长什么样,写个脚本看一下:

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

FLAG = 'crypto{TH1S_1S_A_FAK3_F1A6}'
plaintext = 'ccc'


def encrypt(plaintext):  # 我们能输入明文
    padded = pad(plaintext.encode() + FLAG.encode(), 16)  # 把我们的plaintext和字节流flag拼接后,分成多个十六字节长的小块
    print(padded)


encrypt(plaintext)

#运行输出:
b'ccccrypto{TH1S_1S_A_FAK3_F1A6}\x02\x02'
# 这里的确把plaintext.encode()和FLAG.encode()拼了起来,然后补全十六进制。这里刚好是32个字节。

好的,那么可以这样:我们传入很小的一个任意明文,然后根据前面的经验构造AES电码本解密器,来解密flag。

from Crypto.Cipher import AES

key = ?
ciphertext = ?

def decrypt(ciphertext, key):
    ciphertext = bytes.fromhex(ciphertext)

    cipher = AES.new(key, AES.MODE_ECB)
    try:
        decrypted = cipher.decrypt(ciphertext)
    # except ValueError as e:
        # return {"error": str(e)}
    return decrypted

print(decrypt(ciphertext, key))

解密器写出来了,但是,key是什么!???按照题目提示,这题不是要猜key,所以这样就没用了。

先看网页上的加密器。写一个61(a的十六进制),返回的密文:(64字符,32字节长)

39ccbecdb5524a46b837db50659c76445c4e884ba0ecc2141c276d06247f6b2f

写616161616161,返回的密文:(64字符,32字节长)

ac1c923eb229c3c4d8f0ed1b475003de702bab69dc1be9665a9807fad88ad3d5

写61616161616161,返回的密文:(96字符,48字节长)

3583b98d93fabeac67cf4739802f01bbec103207cd9ee73c84ab8819ebff45053150f4d79d7cc6c1d4b574b1fce84247

传入6个a还是32字节长,但传入7个a就是48字节长了。传入7个a就激活了第三个16字节块,所以传入6个a是刚好达到32字节的。那么flag就是26字节长。

首先,我们知道flag的开头是:crypto{,所以前7个字节是已知的。我们从第8个字节开始猜测。写脚本:

import requests
import re

flag = 'crypto{'

print("这个过程可能会比较缓慢,请耐心等待……")		# 可能得等十几二十分钟
for n in range(8, 0, -1):       # 这里不减少到0是因为,如果减到0,padString就会为空,这样请求会出错(得空数据)
    padString = 'a' * n		# 这串padString是要加在flag前面的,现在两段加起来是15位,而第16位(位置)留给我们来猜测(猜测flag的第8个字符)。
    temp1 = padString.encode().hex()  # 这样子,明文就是很多个a加上flag,加密
    for i in range(33, 127):
        temp2 = (padString + flag + chr(i)).encode().hex()		# 实际处理前15个字节会跟temp1一样,穷举猜第16位(位置)字节(flag的第8个字符)
        r1 = requests.get(f'http://aes.cryptohack.org/ecb_oracle/encrypt/{temp1}/').text
        r1 = re.search(':"(.*)"', r1)[1][0:32]		# 获取返回值的前16字节(第一个16字节块)的hex值
        r2 = requests.get(f'http://aes.cryptohack.org/ecb_oracle/encrypt/{temp2}/').text
        r2 = re.search(':"(.*)"', r2)[1][0:32]		# 获取返回值的前16字节(第一个16字节块)的hex值
        if r1 == r2:
            flag = flag + chr(i)		# 碰撞成功了就加上该字符
            print("当前已知flag:", flag)		# 提示性输出
            break

# 到这里应该已经掌握了15位的flag,接下来要继续猜测另外11位flag
for n in range(16, 6, -1):
    padString = 'a' * n  # 这串padString是要加在flag前面的,现在两段加起来是25位,而第26位(位置)留给我们来猜测(猜测flag的第16个字符)。
    temp1 = padString.encode().hex()  # 这样子,明文就是很多个a加上flag,加密
    for i in range(33, 127):
        temp2 = (padString + flag + chr(i)).encode().hex()  # 实际处理前25个字节会跟temp1一样,穷举猜第26位(位置)字节(flag的第16个字符)
        r1 = requests.get(f'http://aes.cryptohack.org/ecb_oracle/encrypt/{temp1}/').text
        r1 = re.search(':"(.*)"', r1)[1][32:64]  # 获取返回值的第二个16字节块的hex值
        r2 = requests.get(f'http://aes.cryptohack.org/ecb_oracle/encrypt/{temp2}/').text
        r2 = re.search(':"(.*)"', r2)[1][32:64]  # 获取返回值的第二个16字节块的hex值
        if r1 == r2:
            flag = flag + chr(i)  # 碰撞成功了就加上该字符
            print("当前已知flag:", flag)  # 提示性输出
            break
print("\n求得最终flag:", flag)

这个其实有点抽象,用个表格来做示例:(第一行是temp1,加粗部分是服务器自己拼上的flag部分,前面的部分是我们自己传到服务器里的;第二行是temp2,加粗部分是在穷举猜测的字符,前面的部分是我们自己传到服务器里的)

a a a a a a a a c r y p t o { p
a a a a a a a a c r y p t o { p

下一轮,整体左移,同理继续穷举下一位:

a a a a a a a c r y p t o { p 3
a a a a a a a c r y p y o { p 3

第一个十六位的块就只能穷举到下面这个时候,因为你不能一个'a'都不传,或者说是什么都不传,否则服务器会返回空数据给你的!

a c r y p t o { p 3 n 6 u 1 n 5
a c r y p t o { p 3 n 6 u 1 n 5

穷举第二个十六进制块的内容示例:

a a a a a a a a a a a a a a a a
c r y p t o { p 3 n 6 u 1 n 5 _
a a a a a a a a a a a a a a a a
c r y p t o { p 3 n 6 u 1 n 5 _

下一轮,整体左移,同理继续穷举下一位:

a a a a a a a a a a a a a a a c
r y p t o { p 3 n 6 u 1 n 5 _ h
a a a a a a a a a a a a a a a c
r y p t o { p 3 n 6 u 1 n 5 _ h

这样应该就好理解一些了。

ECB CBC WTF --- 密码分组链接模式_分块攻击

跳转页面后能看到的代码:

from Crypto.Cipher import AES


KEY = ?
FLAG = ?


@chal.route('/ecbcbcwtf/decrypt/<ciphertext>/')
def decrypt(ciphertext):
    ciphertext = bytes.fromhex(ciphertext)

    cipher = AES.new(KEY, AES.MODE_ECB)
    try:
        decrypted = cipher.decrypt(ciphertext)
    # except ValueError as e:
        # return {"error": str(e)}

    return {"plaintext": decrypted.hex()}


@chal.route('/ecbcbcwtf/encrypt_flag/')
def encrypt_flag():
    iv = os.urandom(16)		# 随机生成16字节字节流

    cipher = AES.new(KEY, AES.MODE_CBC, iv)		# AES解密flag字节流,但用的是密码分组链接模式
    encrypted = cipher.encrypt(FLAG.encode())
    ciphertext = iv.hex() + encrypted.hex()		# 把这俩东西分别十六进制加密后输出
    return {"ciphertext": ciphertext}

多次按下 加密 按钮,会发现输出结果不一样,展示其中两次密文值:

d089fa542d91d77291fde09957834777eeef402774aa4f0213b3acaeefa974e980068867826b313605e01036af889fd7

25c88d4433364e0e161c2e2f4691bc2944e303cac41cc26fa1e603dd22ae1974c6d4d9f3d1db908ba40c9b626fc10ebd

但分析了代码,发现这只是小问题(这并不是什么大的阻碍)——因为iv的值还是泄露了。

通过页面上给出的 密码分组链接模式 示意图,可总结出如下流程:

截取前32位字符,做从十六进制到字节流的解密,得知iv;

密文的第33到第64位,是第二个16字节块,先做电码本模式的AES解密,然后再十六进制解密为字节流,最后和iv异或,decode获得flag的第一部分;

密文的第65到第96位,先做电码本模式的AES解密,然后再十六进制解密为字节流,最后和 从十六进制转字节流后的密文第33到第64位 异或,decode获得flag的第二部分。

开始执行!

首先,按页面上的 加密 按钮得到一段密文值:

25c88d4433364e0e161c2e2f4691bc2944e303cac41cc26fa1e603dd22ae1974c6d4d9f3d1db908ba40c9b626fc10ebd

然后按照上面提到的流程写脚本:

from pwn import *

c = '25c88d4433364e0e161c2e2f4691bc2944e303cac41cc26fa1e603dd22ae1974c6d4d9f3d1db908ba40c9b626fc10ebd'

c1 = c[0:32]  # 每32字符分一段,得三段密文
c2 = c[32:64]
c3 = c[64:96]
# 处理第一段密文,得到iv
iv = bytes.fromhex(c1)

# 处理第二、三段密文
print("您的 c2 = ", c2, " ,请拿到页面中解密")  # 因为我们没有key,所以不能离线解密,只能到页面上解密
print("您的 c3 = ", c3, " ,请拿到页面中解密")

c4 = input("现在请给我在界面中解密过的c2:")
c5 = input("现在请给我在界面中解密过的c3:")

c4 = bytes.fromhex(c4)
c5 = bytes.fromhex(c5)

c4 = xor(c4, iv).decode()
c5 = xor(bytes.fromhex(c2), c5).decode()

print(c4, c5, sep='')

跳转页面后能看到的代码:

from Crypto.Cipher import AES
import os
from Crypto.Util.Padding import pad, unpad
from datetime import datetime, timedelta


KEY = ?
FLAG = ?


@chal.route('/flipping_cookie/check_admin/<cookie>/<iv>/')
def check_admin(cookie, iv):
    cookie = bytes.fromhex(cookie)		# 对传入的cookie和iv先从十六进制转字节流
    iv = bytes.fromhex(iv)

    try:
        cipher = AES.new(KEY, AES.MODE_CBC, iv)		# 然后做AES解密,用密码分组链接模式
        decrypted = cipher.decrypt(cookie)
        unpadded = unpad(decrypted, 16)
    # except ValueError as e:
        # return {"error": str(e)}

    if b"admin=True" in unpadded.split(b";"):		# 如果admin参数为True就返回flag
        return {"flag": FLAG}
    else:
        return {"error": "Only admin can read the flag"}


@chal.route('/flipping_cookie/get_cookie/')
def get_cookie():
    expires_at = (datetime.today() + timedelta(days=1)).strftime("%s")		# cookie跟时间有关,会很随机
    cookie = f"admin=False;expiry={expires_at}".encode()		# cookie中有个admin参数,可能需要改成True

    iv = os.urandom(16)		# 随机生成16字节字节流
    padded = pad(cookie, 16)		# 把cookie填充到16字节(的倍数)
    cipher = AES.new(KEY, AES.MODE_CBC, iv)		# 接下来做AES加密,用密码分组链接模式
    encrypted = cipher.encrypt(padded)
    ciphertext = iv.hex() + encrypted.hex()		# 把这俩东西分别十六进制加密后输出

    return {"cookie": ciphertext}

多次按下 加密 按钮,会发现输出结果不一样,其中一次的密文值:

976fb2318127ee590d01be63646dbfe712df68f9b009db2c1ba9461295af66f7a1ade5d37f49f38e4f4f76561d229b4c

根据 get_cookie 函数,前32个字符是iv的十六进制值,后64个字符是cookie的十六进制值:

976fb2318127ee590d01be63646dbfe7

12df68f9b009db2c1ba9461295af66f7

a1ade5d37f49f38e4f4f76561d229b4c

这题需要的应该是,先AES解密cookie,篡改完cookie后再AES加密回去。

解密cookie有问题——我们并没有key。我猜测这回又要穷举key。说干就干,上脚本:

from Crypto.Cipher import AES


def decrypt(key, cookie_temp, iv_temp):
    cookie_temp = bytes.fromhex(cookie_temp)  # 对传入的cookie和iv先从十六进制转字节流
    iv_temp = bytes.fromhex(iv_temp)

    cipher = AES.new(key, AES.MODE_CBC, iv_temp)  # 然后做AES解密,用密码分组链接模式
    decrypted = cipher.decrypt(cookie_temp)
    return decrypted


with open("./words.txt") as f:  # 读取字典
    words = [w.strip() for w in f.readlines()]

c = '12df68f9b009db2c1ba9461295af66f7a1ade5d37f49f38e4f4f76561d229b4c'
iv = '976fb2318127ee590d01be63646dbfe7'

for i in words:
    try:
        d = decrypt(i.encode(), c, iv).decode('utf-8', errors='ignore')
        if d.startswith('admin='):
            print(d)
    except:
        continue

没输出任何结果,密钥爆破失败了。

那就换一种方法,看能不能影响某些参数来使解密时呈现出admin=True 。根据页面上密码分组链接模式的示意图,注意到c1解密是这样的:(伪数学公式,仅作示意)(message1代表明文,ciphertext1代表密文)

message1=(AESdecode(ciphertext1,key))ivmessage1 = (AESdecode(ciphertext1, key)) \oplus iv

那么根据异或的性质(自逆性):

message1iv=(AESdecode(ciphertext1,key))message1 \oplus iv = (AESdecode(ciphertext1, key))

那么我们能不能让解出来的明文呈现我们想要的样子?假如我们想要的明文是message1_x,那么应该要找到一个新的iv_x,符合:

message1_xiv_x=(AESdecode(ciphertext1,key))message1\_x \oplus iv\_x = (AESdecode(ciphertext1, key))

如果符合了上式,根据页面上密码分组链接模式的示意图,那么我们不会影响到密文c1,也不会影响到后面的c2,而且还能在不知道key的情况下调包明文。那么我们现在求出iv_x,转十六进制,然后再拿原本的c1和c2的十六进制(点击页面上的 加密 按钮后输出的十六进制串的第33到96位),两者丢进页面的解密器中解密,理论上就能符合条件,输出flag。

那么联立上面两条式子可知:

message1iv=message1_xiv_xmessage1 \oplus iv = message1\_x \oplus iv\_x

根据异或的性质(自逆性):

i_x=message1_xmessage1ivi\_x = message1\_x \oplus message1 \oplus iv

这样就能得到iv_x,用python来算:

from pwn import *

iv = '3b6edf14e8d089114d16da8c3e3414cd'
message1 = 'admin=False;expi'
message1_x = 'admin=True;aaaaa'       # 我构造的明文。注意,分号不能丢,否则admin参数失效
iv_x = xor(bytes.fromhex(iv), message1.encode(), message1_x.encode())       # 根据公式算新的iv

print(iv_x.hex())

然后把iv_x和c1c2的十六进制到页面中解密,就能得到flag。

Symmetry --- 输出反馈模式_简介

这题是输出反馈模式模式的简介,按页面上的 加密 按钮刷出来的十六进制串是不同的。

瞄两眼页面上的代码(到这边已经做得很多了,都感觉很容易识破了),很容易代码的加解密函数几乎就是互逆的,我们只要从十六进制串中分别提取出iv(前32个十六进制字符)和密文的十六进制串(剩下的十六进制字符),放到页面上解密就可以得到flag的十六进制值,然后用下面脚本解密一下十六进制即可得到flag:

a = '63727970746f7b3066625f31355f35796d6d3337723163346c5f2121213131217d'

print(bytes.fromhex(a))

Bean Counter --- 流密码_加密流程分析

跳转页面后能看到的代码:

from Crypto.Cipher import AES


KEY = ?


class StepUpCounter(object):
    def __init__(self, step_up=False):		# 构造函数,类被创建时自动触发
        self.value = os.urandom(16).hex()		# 随机生成16字节长的字符串,并做十六进制加密,根据后面的代码可知,这是流程图中的iv
        self.step = 1
        self.stup = step_up		# 按照参数列表,默认传入False

    def increment(self):		# 由于stup参数为0,那么这个函数相当于几乎什么也没干,只是返回了iv值
        if self.stup:		# 如果这个stup参数为True
            self.newIV = hex(int(self.value, 16) + self.step)
        else:		# 如果stup参数为False(默认情况)
            self.newIV = hex(int(self.value, 16) - self.stup)		# 把iv解成10进制,然后减去stup的值,再加密回十六进制。这里False其实是0,那其实这几行代码相当于什么也没干。
        self.value = self.newIV[2:len(self.newIV)]		# 把新iv的“0x”前缀去掉,覆盖掉旧iv
        return bytes.fromhex(self.value.zfill(32))		# 把新iv补充到32个字符,转化为字节流

    def __repr__(self):		# 对象被直接打印时自动调用
        self.increment()		# 自动调用increment函数,更新iv值
        return self.value		# 返回更新后的iv值



@chal.route('/bean_counter/encrypt/')
def encrypt():
    # 指定AES加密采用电码本模式
    cipher = AES.new(KEY, AES.MODE_ECB)
    # 实例化一个类,保存到ctr变量中,这个对象里默认有一个iv,一个值为1的step参数,一个值为False的stup参数
    ctr = StepUpCounter()

    out = []		# 一个记录结果的列表
    with open("challenge_files/bean_flag.png", 'rb') as f:		# 以二进制形式读取flag图片文件
        block = f.read(16)		# 从文件中读取16字节
        while block:		# 循环地读取16字节,直到全部读完
            keystream = cipher.encrypt(ctr.increment())		# 将调用ctr对象所指向的类中的increment()函数得到的返回值做AES加密
            xored = [a^b for a, b in zip(block, keystream)]		# 逐字节异或。zip函数效果上是burp的攻城锤模式
            out.append(bytes(xored).hex())		# 把异或的结果转16进制,接到out列表末尾
            block = f.read(16)

    return {"encrypted": ''.join(out)}		# 把整个out列表拼接成一个字符串输出

如果这段代码按运行顺序执行(按下 加密 按钮激活encrypt函数后),那么效果上可能是:

实例化一个StepUpCounter类,存到ctr变量中。这时候,这个对象中有三个参数:value参数(iv值)是十六进制加密后的随机十六字节长字符串,step是1,stup在这边是默认值False。

然后存有flag的图片文件被打开,然后循环读取,每次读取十六字节。然后先调用increment函数返回iv值(因为经过分析我们知道increment函数实际上根本没更新iv值),然后把这个iv值做电码本模式的AES加密。然后图片文件流和AES加密后的逐字节异或,并把结果转字节依次输出(实际上不是这样,只是效果一样,但为了理解简洁就这样写)。

攻击思路可能是:

按下 加密 按钮后拿到一段输出密文。

我们先拿开头32个字符(16字节),和随机字符串异或,如果异或出了png文件开头的固定格式的32字符长(目前只是个猜想,如果固定格式真有32字符长才有效)的十六进制串,就算是得到了AES加密后的iv的十六进制串。然后就拿着这个AES加密过后的iv的十六进制串去逐字节异或输出的密文。

最后把异或的结果拼回成一张图片,就能得到flag。

第一步,按下 加密 按钮,随机得到一段密文,把其中包含的十六进制值(太长了,这边就不展示了)拿出来写到一个txt文件中。

第二步,先去验证一下png开头是不是真的有32个字符(16字节)长的十六进制串。拿几张png看一下(我用WinHex看的)……

经过观察,发现前36个字符是固定的,而我们需要的前32个字符(16字节):

89504e470d0a1a0a0000000d49484452

根据异或的自逆性,可以知道如下公式:

16  png16=AESiv密文开头16字节\ \oplus\ png开头固定格式16字节=AES加密后的iv的十六进制串

到这边就可以开始写脚本了:

from pwn import *

p_head = '89504e470d0a1a0a0000000d49484452'
with open('./ciphertext.txt', 'r') as f1:
    ciphertext = f1.read()

encoded_iv = xor(bytes.fromhex(ciphertext[0:32]), bytes.fromhex(p_head))		# 得到AES加密后的iv的十六进制串。惨痛教训,如果用.encode(),异或出来的东西位数会出现变化。所以得是bytes.fromhex()

png_stream = xor(bytes.fromhex(ciphertext), encoded_iv)		# 还原出图片的字节流

with open('./flag.png', 'wb') as f2:		# 把图片的字节流输出为文件
    f2.write(png_stream)


Public-Key Cryptography --- 非对称密码体制

Modular Exponentiation --- 模指数运算

这题就只是想让我们认识一下pow这个函数(但我听说powmod函数更好,我自己一直用的也是这个),拿着题目给的数据,写脚本:

from gmpy2 import *

print(powmod(101, 17, 22663))

Public Keys --- 公钥

这题考的是RSA的加密公式:(其中,c是密文ciphertext,m是明文message,e是公钥指数,N是p和q的乘积)

cme mod Nc≡m^e\ mod\ N

据此与题目中给到的数据写脚本:

from gmpy2 import *

print(powmod(12, 65537, 17*23))

Euler's Totient --- 欧拉函数

RSA中的欧拉函数公式:(如果你学过信息安全数学基础,你会发现其实这甚至算不上公式,本质上还是欧拉函数本身的性质)

φ(N)=(p1)(q1)φ(N)=(p-1)*(q-1)

所以就可以写脚本计算:

p = 857504083339712752489993810777
q = 1029224947942998075080348647219

print((p-1)*(q-1))

Private Keys --- 私钥

RSA中用于求出私钥d的公式:

de1 mod φ(N)d≡e^{−1}\ mod\ φ(N)

于是根据题目中的数据写脚本:

from gmpy2 import *

p = 857504083339712752489993810777
q = 1029224947942998075080348647219
e = 65537

ou = (p-1)*(q-1)

print(powmod(e, -1, ou))

RSA Decryption --- RSA解密

这题考的是RSA的加密公式:(其中,c是密文ciphertext,m是明文message,e是公钥指数,N是p和q的乘积)

mcd mod Nm≡c^d\ mod\ N

这题在题目提示中说明了“要用到上一题的私钥”,d = 121832886702415731577073962957377780195510499965398469843281

于是根据题目中的其他数据写脚本:(题目中提供了e,但用不到)

from gmpy2 import *

N = 882564595536224140639625987659416029426239230804614613279163
c = 77578995801157823671636298847186723593814843845525223303932
d = 121832886702415731577073962957377780195510499965398469843281

print(powmod(c, d, N))

RSA Signatures --- RSA签名

这一题要求我们完成RSA签名,从页面中下载的文件包含N和d。

签名差不多就是,你用你的私钥(注意,因为是签名,所以用的不是公钥了)做加密。这样别人用你公钥解密,如果成功了就能证明这消息是你的,防篡改、抗抵赖。

签名公式:

c2Hash(c1)e mod Nc_2≡Hash(c_1)^e\ mod\ N

根据题目描述和给到的数据写脚本:

from gmpy2 import *
from Crypto.Util.number import *
import hashlib

N = 15216583654836731327639981224133918855895948374072384050848479908982286890731769486609085918857664046075375253168955058743185664390273058074450390236774324903305663479046566232967297765731625328029814055635316002591227570271271445226094919864475407884459980489638001092788574811554149774028950310695112688723853763743238753349782508121985338746755237819373178699343135091783992299561827389745132880022259873387524273298850340648779897909381979714026837172003953221052431217940632552930880000919436507245150726543040714721553361063311954285289857582079880295199632757829525723874753306371990452491305564061051059885803
d = 11175901210643014262548222473449533091378848269490518850474399681690547281665059317155831692300453197335735728459259392366823302405685389586883670043744683993709123180805154631088513521456979317628012721881537154107239389466063136007337120599915456659758559300673444689263854921332185562706707573660658164991098457874495054854491474065039621922972671588299315846306069845169959451250821044417886630346229021305410340100401530146135418806544340908355106582089082980533651095594192031411679866134256418292249592135441145384466261279428795408721990564658703903787956958168449841491667690491585550160457893350536334242689
c_1 = 'crypto{Immut4ble_m3ssag1ng}'

c_1 = hashlib.sha256(c_1.encode()).digest()			# 这题的一个难点在于我们需要找到正确的SHA256加密函数,否则加密不出我们想要的结果

print(powmod(bytes_to_long(c_1), d, N))		# 根据题目要求用bytes_to_long函数

Factoring --- 大数分解

很容易想到要用工具 yafu ,到工具目录下开cmd,输入:

.\yafu-x64.exe factor(510143758735509025530880200653196460532653147)		# 对的,那里写的的确是反斜杠,而不是斜杠

Monoprime --- 单素数

按这样讲,那么n就不是q和p相乘得来的了,就是p或q;欧拉函数,根据定义,也等于 n-1 而已(现在n是素数)。

写RSA解密脚本:

from gmpy2 import *
from Crypto.Util.number import *

n = 171731371218065444125482536302245915415603318380280392385291836472299752747934607246477508507827284075763910264995326010251268493630501989810855418416643352631102434317900028697993224868629935657273062472544675693365930943308086634291936846505861203914449338007760990051788980485462592823446469606824421932591
e = 65537
ct = 161367550346730604451454756189028938964941280347662098798775466019463375610700074840105776873791605070092554650190486030367121011578171525759600774739890458414593857709994072516290998135846956596662071379067305011746842247628316996977338024343628757374524136260758515864509435302781735938531030576289086798942

ou = n-1

d = powmod(e, -1, ou)

m = long_to_bytes(powmod(ct, d, n))

print(m.decode())

Manyprime --- 多素数

这里说是,n可以分解成很多很多个数。yafu照样能分:

.\yafu-x64.exe factor(580642391898843192929563856870897799650883152718761762932292482252152591279871421569162037190419036435041797739880389529593674485555792234900969402019055601781662044515999210032698275981631376651117318677368742867687180140048715627160641771118040372573575479330830092989800730105573700557717146251860588802509310534792310748898504394966263819959963273509119791037525504422606634640173277598774814099540555569257179715908642917355365791447508751401889724095964924513196281345665480688029639999472649549163147599540142367575413885729653166517595719991872223011969856259344396899748662101941230745601719730556631637)

然后写脚本:

from gmpy2 import *
from Crypto.Util.number import *

n = 580642391898843192929563856870897799650883152718761762932292482252152591279871421569162037190419036435041797739880389529593674485555792234900969402019055601781662044515999210032698275981631376651117318677368742867687180140048715627160641771118040372573575479330830092989800730105573700557717146251860588802509310534792310748898504394966263819959963273509119791037525504422606634640173277598774814099540555569257179715908642917355365791447508751401889724095964924513196281345665480688029639999472649549163147599540142367575413885729653166517595719991872223011969856259344396899748662101941230745601719730556631637
e = 65537
ct = 320721490534624434149993723527322977960556510750628354856260732098109692581338409999983376131354918370047625150454728718467998870322344980985635149656977787964380651868131740312053755501594999166365821315043312308622388016666802478485476059625888033017198083472976011719998333985531756978678758897472845358167730221506573817798467100023754709109274265835201757369829744113233607359526441007577850111228850004361838028842815813724076511058179239339760639518034583306154826603816927757236549096339501503316601078891287408682099750164720032975016814187899399273719181407940397071512493967454225665490162619270814464

p = [13099895578757581201		# yafu的分解结果
    , 17174065872156629921
    , 17281246625998849649
    , 14523070016044624039
    , 15824122791679574573
    , 17138336856793050757
    , 10336650220878499841
    , 12132158321859677597
    , 11328768673634243077
    , 16898740504023346457
    , 14178869592193599187
    , 14278240802299816541
    , 16656402470578844539
    , 11530534813954192171
    , 14963354250199553339
    , 14100640260554622013
    , 11473665579512371723
    , 12955403765595949597
    , 11665347949879312361
    , 15364597561881860737
    , 11492065299277279799
    , 9389357739583927789
    , 15669758663523555763
    , 11403460639036243901
    , 13572286589428162097
    , 9282105380008121879
    , 15998365463074268941
    , 9303850685953812323
    , 12973972336777979701
    , 10638241655447339831
    , 11282698189561966721
    , 12834461276877415051]

ou = 1
for i in p:
    ou = ou * (i-1)

d = powmod(e, -1, ou)

m = long_to_bytes(powmod(ct, d, n))

print(m.decode())

Salty --- 超低指数

这题题目提示这题指数很小,并且给到代码供分析:

from Crypto.Util.number import getPrime, inverse, bytes_to_long, long_to_bytes

e = 1
d = -1

while d == -1:
    p = getPrime(512)
    q = getPrime(512)
    phi = (p - 1) * (q - 1)
    d = inverse(e, phi)		# 这里是求e关于phi的乘法逆元。因为e是1,所以这里如果d也是1的话就符合条件

n = p * q

flag = b"XXXXXXXXXXXXXXXXXXXXXXX"
pt = bytes_to_long(flag)
ct = pow(pt, e, n)		# 相当于:ct = pt % n

print(f"n = {n}")
print(f"e = {e}")
print(f"ct = {ct}")

# 这里已经把解密流程透露给我们了
pt = pow(ct, d, n)		# 相当于:pt = ct % n,但由于页面下载到的output.txt文件显示,n比ct大得多,因此可认为:pt = ct
decrypted = long_to_bytes(pt)
assert decrypted == flag

根据上面的分析,脚本如下:

from Crypto.Util.number import *

ct = 44981230718212183604274785925793145442655465025264554046028251311164494127485

print(long_to_bytes(ct).decode())

Modulus Inutilis --- 低指数

下载文件,打开后发现一些数据,其中 e=3

低指数攻击有两种情况,第一种情况是,e小,然后明文m也不大,结果就是明文m的e次方大小还不超过n,就相当于没取余n。于是这时候把密文c开三次方就能拿到明文。

这题运气不错,是上面这种情况,处理起来也比较简单:

from Crypto.Util.number import *
from gmpy2 import *

n = 17258212916191948536348548470938004244269544560039009244721959293554822498047075403658429865201816363311805874117705688359853941515579440852166618074161313773416434156467811969628473425365608002907061241714688204565170146117869742910273064909154666642642308154422770994836108669814632309362483307560217924183202838588431342622551598499747369771295105890359290073146330677383341121242366368309126850094371525078749496850520075015636716490087482193603562501577348571256210991732071282478547626856068209192987351212490642903450263288650415552403935705444809043563866466823492258216747445926536608548665086042098252335883
e = 3
ct = 243251053617903760309941844835411292373350655973075480264001352919865180151222189820473358411037759381328642957324889519192337152355302808400638052620580409813222660643570085177957

m = iroot(ct, e)		# 开方函数
print(long_to_bytes(m[0]).decode())

Working with Fields --- 域

这题先简单介绍了一下抽象代数中的“域”这个概念。然后让你算乘法逆元。算就是了:

from gmpy2 import *

print(invert(209, 991))

Generators of Groups --- 生成元/原根

生成元(或者说是“原根”)g在经过这样多次下面这样的运算后能够遍历有限域的生成子群中所有的非零元素:

gn mod pg^n\ mod\ p

那么我们需要做的是找这个模p的最小原根。用下面的脚本找原根:

from sympy import *

p = 28151

g = primitive_root(p)
print(g)

Computing Public Values --- “公开值”

Diffie-Hellman协议的安全性依赖数学困难问题“离散对数”。

你有一个“公开值“A,它符合下面的公式:

Aga mod pA≡g^a\ mod\ p

其中,a是“私密值”。数学困难问题“离散对数”是,即使你知道A、g和p(这些数应该比较大才可以),要推出a也是困难的。

而这题只是要我们算“公开值”A,脚本:

from gmpy2 import *

g = 2
p = 2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919
a = 972107443837033796245864316200458246846904598488981605856765890478853088246897345487328491037710219222038930943365848626194109830309179393018216763327572120124760140018038673999837643377590434413866611132403979547150659053897355593394492586978400044375465657296027592948349589216415363722668361328689588996541370097559090335137676411595949335857341797148926151694299575970292809805314431447043469447485957669949989090202320234337890323293401862304986599884732815

print(powmod(g, a, p))

Computing Shared Secrets --- 共享密钥

首先得知道共享密钥的公式:

Sgab mod pS≡g^{ab}\ mod\ p

于是可以据此进行进一步推导。

由于:

Aga mod pA≡g^a\ mod\ p

因此:

SgabAb mod pS≡g^{ab}≡A^b\ mod\ p

那么我们就可以根据我们推出的这个式子来算出共享密钥S:

from gmpy2 import *

b = 12019233252903990344598522535774963020395770409445296724034378433497976840167805970589960962221948290951873387728102115996831454482299243226839490999713763440412177965861508773420532266484619126710566414914227560103715336696193210379850575047730388378348266180934946139100479831339835896583443691529372703954589071507717917136906770122077739814262298488662138085608736103418601750861698417340264213867753834679359191427098195887112064503104510489610448294420720
p = 2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919
A = 70249943217595468278554541264975482909289174351516133994495821400710625291840101960595720462672604202133493023241393916394629829526272643847352371534839862030410331485087487331809285533195024369287293217083414424096866925845838641840923193480821332056735592483730921055532222505605661664236182285229504265881752580410194731633895345823963910901731715743835775619780738974844840425579683385344491015955892106904647602049559477279345982530488299847663103078045601

print(powmod(A, b, p))

Deriving Symmetric Keys --- 派生对称密钥

按照我的理解,这个“派生对称密钥”的意思就是,用共享密钥生成用于AES加解密的密钥。

这一题最快的做法就是。用上一题的脚本,改一下数值,生成共享密钥:

from gmpy2 import *

b = 197395083814907028991785772714920885908249341925650951555219049411298436217190605190824934787336279228785809783531814507661385111220639329358048196339626065676869119737979175531770768861808581110311903548567424039264485661330995221907803300824165469977099494284722831845653985392791480264712091293580274947132480402319812110462641143884577706335859190668240694680261160210609506891842793868297672619625924001403035676872189455767944077542198064499486164431451944
p = 2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919
A = 112218739139542908880564359534373424013016249772931962692237907571990334483528877513809272625610512061159061737608547288558662879685086684299624481742865016924065000555267977830144740364467977206555914781236397216033805882207640219686011643468275165718132888489024688846101943642459655423609111976363316080620471928236879737944217503462265615774774318986375878440978819238346077908864116156831874695817477772477121232820827728424890845769152726027520772901423784

print(powmod(A, b, p))

然后和iv、encrypted_flag一起摁到题目提供的 decrypt.py 中解密,直接解出flag。

但是最好还是意思一下,毕竟这种直接送解密器的情况不多见。这是加密器的代码,分析一下:

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import hashlib
import os
from secret import shared_secret

FLAG = b'crypto{????????????????????????????}'


def encrypt_flag(shared_secret: int):
    # Derive AES key from shared secret
    # 下面三句的意思大概是:对共享密钥做sha1加密,并转字节流,然后截取前16位作为key
    sha1 = hashlib.sha1()
    sha1.update(str(shared_secret).encode('ascii'))
    key = sha1.digest()[:16]
    # Encrypt flag
    # 下面三句的意思大概是:随机生成16字节的iv,然后对16字节填充的FLAG做AES加密
    iv = os.urandom(16)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    ciphertext = cipher.encrypt(pad(FLAG, 16))
    # Prepare data to send
    # 下面几句差不多就是,把iv和密文十六进制加密后输出
    data = {}
    data['iv'] = iv.hex()
    data['encrypted_flag'] = ciphertext.hex()
    return data


print(encrypt_flag(shared_secret))

那么如果让我自己写解密器,代码大概长这样:

import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

shared_secret = 1547922466740669851136899009270554812141325611574971428561894811681012510829813498961168330963719034921137405736161582760628870855358912091728546731744381382987669929718448423076919613463237884695314172139247244360699127770351428964026451292014069829877638774839374984158095336977179683450837507011404610904412301992397725594661037513152497857482717626617522302677408930050472100106931529654955968569601928777990379536458959945351084885704041496571582522945310187
iv = '737561146ff8194f45290f5766ed6aba'
ciphertext = '39c99bf2f0c14678d6a5416faef954b5893c316fc3c48622ba1fd6a9fe85f3dc72a29c394cf4bc8aff6a7b21cae8e12c'

def decrypt_flag(shared_secret: int, iv: str, ciphertext: str):
    # 下面三句是,用完全相同的方式生成key
    sha1 = hashlib.sha1()
    sha1.update(str(shared_secret).encode('ascii'))
    key = sha1.digest()[:16]
    # 把iv和密文十六进制解密成字节流以便于做AES解密
    iv = bytes.fromhex(iv)
    ciphertext = bytes.fromhex(ciphertext)
    # 用上面这些东西做AES解密
    c = AES.new(key, AES.MODE_CBC, iv)
    plaintext = c.decrypt(ciphertext)
    return unpad(plaintext, 16).decode('utf-8', errors='ignore')		# 好吧,说实话,这个unpad函数是新学到的,能把之前用pad产生的填充消除掉。但如果这个函数报错了,那直接删掉即可

print(decrypt_flag(shared_secret, iv, ciphertext))

用这个解密器也能解出flag。

Parameter Injection --- 中间人攻击1_参数篡改

打开一台Linux,输入 nc socket.cryptohack.org 13371 ,于是就会刷出:

Intercepted from Alice:
{
"p":"0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff", 
"g": "0x02",
"A":"0x628520027a26e26b1f583b2478efd9d160e882a933fef05c5755b56a45a84357e962a8e1ec9bebfeabe382c47702331274dc206facd85988d719c47774b8e6482c6468fc5bfce2630c501a31f33262109d02107c8554cc3a58c8baff3133cdb7a88de7a6168af21ed83d17049fef7497dbcc6c32c10c7cb40bd58cde8a2d2ba3555a5623765345c6aefde7c29b8b13430316446315f6128d181c31afd8d42bbc9da37e4341b48e2dc9c88870862439759bb62af3b74c207da8630396c8b5f904"
}
Send to Bob:

经过摸索,发现接下来只需要把 Intercepted from Alice 中的内容原封不动地发给Bob即可:

Send to Bob: {"p": "0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff", "g": "0x02", "A": "0x628520027a26e26b1f583b2478efd9d160e882a933fef05c5755b56a45a84357e962a8e1ec9bebfeabe382c47702331274dc206facd85988d719c47774b8e6482c6468fc5bfce2630c501a31f33262109d02107c8554cc3a58c8baff3133cdb7a88de7a6168af21ed83d17049fef7497dbcc6c32c10c7cb40bd58cde8a2d2ba3555a5623765345c6aefde7c29b8b13430316446315f6128d181c31afd8d42bbc9da37e4341b48e2dc9c88870862439759bb62af3b74c207da8630396c8b5f904"}
Intercepted from Bob: {"B": "0x2be5d1fb1338c73764385518a1e6d4a39aaa5f67b4ed7d73407c11b5d7005a73303da64fe7d63772f8244478fe6050f0a372fec85032fdcde24e92e31e5b5d42ba4b7193aac60e5f32b028cf2415dc2b4cb8b3d1807a93e6df222d8bfa55127684c310bb11ee6b585d8d671711a8ffa4723054959f357e1aa8e258cc7156db7a369de5b8393e0bb560773a10cec70e3683616ac82d566dc9acfd2d0eefceb9cab9bf26c3a1452f41db9ca450b69c9d0c45df6332879bd03ea8955a22b8e96129"}

然后如果把 Intercepted from Bob 也原封不动地发回给Alice,就能得到:

Send to Alice: {"B": "0x2be5d1fb1338c73764385518a1e6d4a39aaa5f67b4ed7d73407c11b5d7005a73303da64fe7d63772f8244478fe6050f0a372fec85032fdcde24e92e31e5b5d42ba4b7193aac60e5f32b028cf2415dc2b4cb8b3d1807a93e6df222d8bfa55127684c310bb11ee6b585d8d671711a8ffa4723054959f357e1aa8e258cc7156db7a369de5b8393e0bb560773a10cec70e3683616ac82d566dc9acfd2d0eefceb9cab9bf26c3a1452f41db9ca450b69c9d0c45df6332879bd03ea8955a22b8e96129"}
Intercepted from Alice: {"iv": "936c211ee8a0b1ac178ef35f7dbacb70", "encrypted_flag": "c5c3156c7e65e5201c709e2b5c33d990168ee6e3b8861fb0827d63c4937d6797"}

我们来梳理一下发生了什么事:

首先我们要知道有三条公式存在:

Aga mod pBgb mod pSgabAbBa mod pA≡g^a\ mod\ p\\ B≡g^b\ mod\ p\\ S≡g^{ab}≡A^b≡B^a\ mod\ p

而Alice手上有一个“私密值”a,Bob手上也有一个“私密值”b。这两个值我们是不知道的,也不打算去硬解离散对数问题来爆它。

首先,Alice根据g、p还有a算出一个A,然后把g、p和A传给Bob。到这里,Bob就可以算共享密钥S了;

其次,Bob根据g、p还有b算出一个B,然后把B传给Alice。到这里,Alice就可以算共享密钥S了;

最后,Alice用这个共享密钥S派生出用于AES加解密的对称密钥key,并且应用在通讯中。

那么怎么做能让我们知道共享密钥S呢?诶!我们可能可以让B等于1,然后不管a是多少,最后算出来的S都是1。那我们就要去篡改Bob发给Alice的B:

Send to Alice: {"B": "0x01"}

然后再拿着Alice返回的iv和encrypted_flag,用解密器解密:

import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

shared_secret = 1
iv = '5e47d23057d05a8aefc0be09cf936eda'
ciphertext = 'c58ad37fae3d68ae2fc9c82565b0eaacff531c041f3f6a069df6a32cc68b9a4e'

def decrypt_flag(shared_secret: int, iv: str, ciphertext: str):
    # 下面三句是,用完全相同的方式生成key
    sha1 = hashlib.sha1()
    sha1.update(str(shared_secret).encode('ascii'))
    key = sha1.digest()[:16]
    # 把iv和密文十六进制解密成字节流以便于做AES解密
    iv = bytes.fromhex(iv)
    ciphertext = bytes.fromhex(ciphertext)
    # 用上面这些东西做AES解密
    c = AES.new(key, AES.MODE_CBC, iv)
    plaintext = c.decrypt(ciphertext)
    return plaintext.decode('utf-8', errors='ignore')		# 这里unpad函数不能用,直接删掉即可

print(decrypt_flag(shared_secret, iv, ciphertext))

Export-grade --- 中间人攻击2_参数类型选择

经过摸索,我们这回可以控制他们用什么参数类型。

一开始,Alice会发:

Intercepted from Alice: {"supported": ["DH1536", "DH1024", "DH512", "DH256", "DH128", "DH64"]}
Send to Bob: 

我们可以告诉Bob,Alice只支持一种很拉胯的参数类型:(要不然,Bob会超级喜欢选DH1024,这也挺安全的)

 {"supported": ["DH64"]}

这时候Bob无路可走,只能选这样一个拉胯的参数类型:

Intercepted from Alice: {"supported": ["DH1536", "DH1024", "DH512", "DH256", "DH128", "DH64"]}
Send to Bob: {"supported": ["DH64"]}
Intercepted from Bob: {"chosen": "DH64"}
Send to Alice: 

现在我们复读一下Bob的决定,就能愉快地用DH64来加密了:

Intercepted from Alice: {"supported": ["DH1536", "DH1024", "DH512", "DH256", "DH128", "DH64"]}
Send to Bob: {"supported": ["DH64"]}
Intercepted from Bob: {"chosen": "DH64"}
Send to Alice: {"chosen": "DH64"}        
Intercepted from Alice: {"p": "0xde26ab651b92a129", "g": "0x2", "A": "0x9b01d5f0de577c5e"}
Intercepted from Bob: {"B": "0x91706e1f010c0ce6"}
Intercepted from Alice: {"iv": "f0b5b856820aa64a1ad7a434ecf1a02c", "encrypted_flag": "5244e743e8ee19329ccb9056f9cc3592b54b287cbd5a68c6811ba06524bec214"}

可以看到,选中某种参数类型后,后面的东西就不是我们能控制的了,各种数据直接刷出来。

这边的攻击思路是这样的:由于DH64相比于其他的参数类型显得太拉胯了,因此我们可以尝试直接算这个离散对数问题,脚本:

from sympy import *

p = '0xde26ab651b92a129'
g = 2
B = '0x91706e1f010c0ce6'

p = int(p, 16)
B = int(B, 16)

print(hex(discrete_log(p, B, g)))		# 没错,就这样水灵灵地把Bob的“私密值”b算出来了

然后你拿着“私密值”b还有其他各种各样的数据到解密器中解密,就能获得flag:

import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from gmpy2 import *

b = int('0x19811b879f1ccd66', 16)
p = int('0xde26ab651b92a129', 16)
A = int('0x9b01d5f0de577c5e', 16)
shared_secret = powmod(A, b, p)
iv = 'f0b5b856820aa64a1ad7a434ecf1a02c'
ciphertext = '5244e743e8ee19329ccb9056f9cc3592b54b287cbd5a68c6811ba06524bec214'

def decrypt_flag(shared_secret: int, iv: str, ciphertext: str):
    # 下面三句是,用完全相同的方式生成key
    sha1 = hashlib.sha1()
    sha1.update(str(shared_secret).encode('ascii'))
    key = sha1.digest()[:16]
    # 把iv和密文十六进制解密成字节流以便于做AES解密
    iv = bytes.fromhex(iv)
    ciphertext = bytes.fromhex(ciphertext)
    # 用上面这些东西做AES解密
    c = AES.new(key, AES.MODE_CBC, iv)
    plaintext = c.decrypt(ciphertext)
    return plaintext.decode('utf-8', errors='ignore')

print(decrypt_flag(shared_secret, iv, ciphertext))

Elliptic Curves --- 椭圆曲线

Background Reading --- 背景介绍

那这里就记一些关键点的东西。

ECC依赖于数学困难问题“离散对数”。

Weierstrass 方程,ECC中的经典方程:

Y2X3+aX+b mod pY^2≡X^3+aX+b\ mod\ p

然后,ECC中有个东西,叫 无穷远点 ,符号为 O ,我个人把它当做一种特殊的0。

ECC中的加法最好根据图像理解(顺带一提,ECC的图像一般沿x轴对称)。用图像理解大致就是,把交点当做一个点,把切点当成两个点,在图像上画一条线,三个点相加等于O (如果找不到第三个点,那就两个点相加等于O)。这个加法满足交换律和结合律。

最后,“flag是我们给交换群的名字”。交换群又称“阿贝尔群”,取其英文即构造出flag:

crypto{Abelian}

Point Negation --- 点的取反

这题给了这些东西,让我们去找Q的坐标:

Y2X3+497X+1768 mod 9739P+Q=OP(8045,6936)Y^2≡X^3+497X+1768\ mod\ 9739\\ P+Q=O\\ P(8045,6936)

这里是两个点做ECC加法得到无穷远点O,如果你仔细看了上一题页面中的图片,你就会发现这种情况应该是有一条竖直延伸的线,贯穿了椭圆曲线。

那么我们要的坐标就是 (8045,-6936) 。(实际上,椭圆曲线点也需要表示为“第一象限点”,x值和y值都应该是正数)

但是题目要求我们把“负数”处理掉。那么根据这一题的ECC表达式,模数是9739,那么算出来的y应该也是模9739的,于是掏出计算器:

-6936 + 9739
= 2803

所以我们要的坐标就是 (8045,2803) 。

Point Addition --- 点的加法

前面我们提到了用图像理解ECC加法,而这边要上公式:

P=(x1,y1), Q=(x2,y2), P+Q=(x3,y3)记:P=(x_1,y_1),\ Q=(x_2,y_2),\ P+Q=(x_3,y_3)

{λ(y2y1)(x2x1) mod p,  PQλ(3x12+a)(2y1) mod p,  P=Qx3λ2x1x2 mod py3λ(x1x3)y1 mod p{ \begin{cases} {λ≡\frac{(y_2−y_1)}{(x_2−x_1)}\ mod\ p,\ \ P≠Q}\\ {λ≡\frac{(3x^2_1+a)}{(2y_1)}\ mod\ p,\ \ P=Q} \end{cases} }\\ x_3≡λ^2−x_1−x_2\ mod\ p\\ y_3≡λ(x_1−x_3)−y_1\ mod\ p

这题其实知道了流程后,代码不难写。但是有一个坑点:算 λ 时候,其实那个除法符号不是除法,而是“乘 分母关于模数p的乘法逆元”。

所以可写代码:(哈哈,没错,这是一个支持循环加的脚本)

from gmpy2 import *


def ECC_Round_Addition(x1, y1, p):
    while 1:
        x2 = int(input("请输入x2值:"))		# 输入第二个点
        y2 = int(input("请输入y2值:"))
        if x1 == x2 and y1 == y2:
            a = int(input("现在请额外提供一个数值a:"))
            lam = ((3 * x1 * x1 + a) * invert((2 * y1), p)) % p
        else:
            lam = ((y2 - y1) * invert((x2 - x1), p)) % p
        x3 = (lam * lam - x1 - x2) % p
        y3 = (lam * (x1 - x3) - y1) % p
        print(f"针对 ({x1},{y1})+({x2},{y2}) ,", end='')
        x1 = x3
        y1 = y3
        print(f"计算结果是:( {x1} , {y1} )")


Ini_x1 = int(input("请输入x1值:"))		# 输入第一个点
Ini_y1 = int(input("请输入y1值:"))
Inp_p = int(input("请输入多项式的模数p:"))
ECC_Round_Addition(Ini_x1, Ini_y1, Inp_p)

Scalar Multiplication --- 标量乘法

我把这题的模式称为“循环自增”。就比如,你要求 [7863]P ,这就相当于要让7863个P相加。

上一题脚本不管用——因为你不太可能手敲这么多次,而且直接套用上一题的算法也不够高效。

页面上提供了另一种天才算法,依据这个算法的流程写脚本,运行并输入数据就能得到flag相关的值:

from gmpy2 import *


def ECC_Addition(x1, y1, x2, y2, a, p):     # ECC加法实现函数
    if x1 is None and y1 is None:       # 如果发现传入了无穷远点,就直接返回另外一个点
        return x2, y2
    elif x1 == x2 and y1 == y2:
        lam = ((3 * x1 * x1 + a) * invert((2 * y1), p)) % p
    else:
        lam = ((y2 - y1) * invert((x2 - x1), p)) % p
    x3 = (lam * lam - x1 - x2) % p
    y3 = (lam * (x1 - x3) - y1) % p
    return x3, y3


def scalar_multiplication(Px, Py, n, a, p):     # 用来实现“循环自增”的函数
    Rx, Ry = None, None     # 因为无穷远点在本质上不是(0,0),所以这里就直接用None来代表
    Qx, Qy = Px, Py
    while n > 0:
        if n % 2 == 1:
            Rx, Ry = ECC_Addition(Rx, Ry, Qx, Qy, a, p)
        Qx, Qy = ECC_Addition(Qx, Qy, Qx, Qy, a, p)
        n = n // 2
    return Rx, Ry


int_x = int(input("请输入接受循环自增量P的x值:"))
int_y = int(input("请输入接受循环自增量P的y值:"))
inp_n = int(input("请输入循环自增的叠加量n:"))
inp_a = int(input("请输入多项式中一次方的系数a:"))
inp_p = int(input("请输入多项式的模数p:"))
result_x, result_y = scalar_multiplication(int_x, int_y, inp_n, inp_a, inp_p)
print(f"[{inp_n}]P的计算结果是:( {result_x} , {result_y} )")

Curves and Logs --- 椭圆曲线版DH密钥交换

这题说的是椭圆曲线版DH密钥交换。

流程大概是:

Alice有“私密值”nA,她能根据下面这个公式算出“公开值”QA:

QA=[nA]GQ_A=[n_A]G

同理,Bob可以这样算出“公开值”QB:

QB=[nB]GQ_B=[n_B]G

然后他们交换公开值,双方能通过这样的方式各自算出共享密钥S:

S=[nA]QB=[nB]QA=[nA][nB]GS=[n_A]Q_B=[n_B]Q_A=[n_A][n_B]G

然后题目里竟然直接提供了nB和QA,根据公式,这样S很好算。

然后根据题目要求,还要对结果的x值做sha1加密。

配合上一题“循环自增”的脚本:

from gmpy2 import *
from hashlib import *


def ECC_Addition(x1, y1, x2, y2, a, p):     # ECC加法实现函数
    if x1 is None and y1 is None:       # 如果发现传入了无穷远点,就直接返回另外一个点
        return x2, y2
    elif x1 == x2 and y1 == y2:
        lam = ((3 * x1 * x1 + a) * invert((2 * y1), p)) % p
    else:
        lam = ((y2 - y1) * invert((x2 - x1), p)) % p
    x3 = (lam * lam - x1 - x2) % p
    y3 = (lam * (x1 - x3) - y1) % p
    return x3, y3


def scalar_multiplication(Px, Py, n, a, p):     # 用来实现“循环自增”的函数
    Rx, Ry = None, None     # 因为无穷远点在本质上不是(0,0),所以这里就直接用None来代表
    Qx, Qy = Px, Py
    while n > 0:
        if n % 2 == 1:
            Rx, Ry = ECC_Addition(Rx, Ry, Qx, Qy, a, p)
        Qx, Qy = ECC_Addition(Qx, Qy, Qx, Qy, a, p)
        n = n // 2
    return Rx, Ry


int_x = int(input("请输入接受循环自增量P的x值:"))
int_y = int(input("请输入接受循环自增量P的y值:"))
inp_n = int(input("请输入循环自增的叠加量n:"))
inp_a = int(input("请输入多项式中一次方的系数a:"))
inp_p = int(input("请输入多项式的模数p:"))
result_x, result_y = scalar_multiplication(int_x, int_y, inp_n, inp_a, inp_p)
print(f"[{inp_n}]P的计算结果是:( {result_x} , {result_y} )")

print("crypto{", sha1(str(result_x).encode()).hexdigest(), "}", sep='')		# sha1加密

Efficient Exchange --- 椭圆曲线版DH密钥交换Plus

这一题是,Alice给出了QA的x坐标,你要靠解二次剩余的方式(Tonelli–Shanks 算法)得到QA的两个可能的y坐标。

然后拿着这些还有题目提供的nB来算出共享密钥S:

S=[nB]QAS=[n_B]Q_A

然后以共享密钥的x坐标为解密时的共享密钥数值,使用题目给的解密器解密。

结合题目给的解密脚本、Tonelli–Shanks 算法解二次剩余脚本和ECC循环自增脚本,混合得到脚本:

from gmpy2 import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import hashlib


# 用来解二次剩余的函数
def tonelli_shanks(a, p):
    global z
    if not gmpy2.is_prime(p):
        raise ValueError("p 必须是一个素数")
    if gmpy2.legendre(a, p) != 1:
        raise ValueError("a 不是模 p 的二次剩余,无法求平方根")

    # Step 1:分解 p - 1 为 Q * 2^S
    Q = p - 1
    S = 0
    while Q % 2 == 0:
        Q //= 2
        S += 1

    # Step 2:找到一个模 p 的二次非剩余 z
    for z in range(2, p):
        if gmpy2.legendre(z, p) == -1:
            break

    # Step 3:初始化变量
    c = gmpy2.powmod(z, Q, p)
    R = gmpy2.powmod(a, (Q + 1) // 2, p)
    t = gmpy2.powmod(a, Q, p)
    M = S

    # Step 4:迭代修正
    while t != 1:
        i = 1
        temp = gmpy2.powmod(t, 2, p)
        while temp != 1:
            temp = gmpy2.powmod(temp, 2, p)
            i += 1
            if i == M:
                raise Exception("无法找到平方根")

        b = gmpy2.powmod(c, 2 ** (M - i - 1), p)
        R = (R * b) % p
        t = (t * b * b) % p
        c = (b * b) % p
        M = i
    return R


def ECC_Addition(x1, y1, x2, y2, a, p):     # ECC加法实现函数
    if x1 is None and y1 is None:       # 如果发现传入了无穷远点,就直接返回另外一个点
        return x2, y2
    elif x1 == x2 and y1 == y2:
        lam = ((3 * x1 * x1 + a) * invert((2 * y1), p)) % p
    else:
        lam = ((y2 - y1) * invert((x2 - x1), p)) % p
    x3 = (lam * lam - x1 - x2) % p
    y3 = (lam * (x1 - x3) - y1) % p
    return x3, y3


def scalar_multiplication(Px, Py, n, a, p):     # 用来实现“循环自增”的函数
    Rx, Ry = None, None     # 因为无穷远点在本质上不是(0,0),所以这里就直接用None来代表
    Qx, Qy = Px, Py
    while n > 0:
        if n % 2 == 1:
            Rx, Ry = ECC_Addition(Rx, Ry, Qx, Qy, a, p)
        Qx, Qy = ECC_Addition(Qx, Qy, Qx, Qy, a, p)
        n = n // 2
    return Rx, Ry


# 题目给的两个解密函数
def is_pkcs7_padded(message):
    padding = message[-message[-1]:]
    return all(padding[i] == len(padding) for i in range(0, len(padding)))


def decrypt_flag(shared_secret: int, iv: str, ciphertext: str):
    # Derive AES key from shared secret
    sha1 = hashlib.sha1()
    sha1.update(str(shared_secret).encode('ascii'))
    key = sha1.digest()[:16]
    # Decrypt flag
    ciphertext = bytes.fromhex(ciphertext)
    iv = bytes.fromhex(iv)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)

    if is_pkcs7_padded(plaintext):
        print(unpad(plaintext, 16).decode('ascii'))
    else:
        print(plaintext.decode('ascii'))


print("=== Tonelli–Shanks 算法求模平方根(求解二次剩余) ===")
a = int(input("请输入 x :"))
int_x = a
inp_p = int(input("请输入 p(模数,需为素数):"))
a = (a * a * a + 497 * a + 1768) % inp_p

try:
    x = tonelli_shanks(a, inp_p)
    print(f"\n满足 y² ≡ a mod p 的")
    print(f"第一个解是:y = {x}")
    print(f"另一个解是:y = {inp_p - x}")
except Exception as e:
    print("发生错误:", e)
    exit(0)

int_y1 = x		# 这里直接两种y都尝试使用
int_y2 = inp_p - x
inp_n = int(input("请输入循环自增的叠加量n:"))
inp_a = int(input("请输入多项式中一次方的系数a:"))

iv = 'cd9da9f1c60925922377ea952afc212c'
ciphertext = 'febcbe3a3414a730b125931dccf912d2239f3e969c4334d95ed0ec86f6449ad8'

result_x, result_y = scalar_multiplication(int_x, int_y1, inp_n, inp_a, inp_p)
print(f"[{inp_n}]P的计算结果1是:( {result_x} , {result_y} )")
shared_secret = result_x
decrypt_flag(shared_secret, iv, ciphertext)

result_x, result_y = scalar_multiplication(int_x, int_y2, inp_n, inp_a, inp_p)
print(f"[{inp_n}]P的计算结果2是:( {result_x} , {result_y} )")
shared_secret = result_x
decrypt_flag(shared_secret, iv, ciphertext)

参数填对了就能刷出flag。

这题其实还有种离谱一点的方法。因为模数p不是很大,而共享密钥S的x坐标是一个不大于p的非负整数,所以可以尝试爆破:

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import hashlib


def is_pkcs7_padded(message):
    padding = message[-message[-1]:]
    return all(padding[i] == len(padding) for i in range(0, len(padding)))


def decrypt_flag(shared_secret: int, iv: str, ciphertext: str):
    # Derive AES key from shared secret
    sha1 = hashlib.sha1()
    sha1.update(str(shared_secret).encode('ascii'))
    key = sha1.digest()[:16]
    # Decrypt flag
    ciphertext = bytes.fromhex(ciphertext)
    iv = bytes.fromhex(iv)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)

    if is_pkcs7_padded(plaintext):
        return unpad(plaintext, 16).decode('utf-8', errors='ignore')
    else:
        return plaintext.decode('utf-8', errors='ignore')


iv = 'cd9da9f1c60925922377ea952afc212c'
ciphertext = 'febcbe3a3414a730b125931dccf912d2239f3e969c4334d95ed0ec86f6449ad8'

for shared_secret in range(9739):
    if decrypt_flag(shared_secret, iv, ciphertext).startswith("crypto"):		# 解密结果为“crypto”开头的才输出
        print(decrypt_flag(shared_secret, iv, ciphertext))

Montgomery's Ladder --- 蒙哥马利阶梯算法

这一题的ECC方程不再是“威尔斯特拉斯式”的了,而是“蒙哥马利式”的!!!

因此前面积累的“循环加法”和“循环自增”脚本都不管用了!

主要就是根据页面上的提供的新“循环加法”和“循环自增”流程写出代码。

脚本:

from gmpy2 import *


def tonelli_shanks(a, p):
    global z
    if not gmpy2.is_prime(p):
        raise ValueError("p 必须是一个素数")
    if gmpy2.legendre(a, p) != 1:
        raise ValueError("a 不是模 p 的二次剩余,无法求平方根")

    # Step 1:分解 p - 1 为 Q * 2^S
    Q = p - 1
    S = 0
    while Q % 2 == 0:
        Q //= 2
        S += 1

    # Step 2:找到一个模 p 的二次非剩余 z
    for z in range(2, p):
        if gmpy2.legendre(z, p) == -1:
            break

    # Step 3:初始化变量
    c = gmpy2.powmod(z, Q, p)
    R = gmpy2.powmod(a, (Q + 1) // 2, p)
    t = gmpy2.powmod(a, Q, p)
    M = S

    # Step 4:迭代修正
    while t != 1:
        i = 1
        temp = gmpy2.powmod(t, 2, p)
        while temp != 1:
            temp = gmpy2.powmod(temp, 2, p)
            i += 1
            if i == M:
                raise Exception("无法找到平方根")

        b = gmpy2.powmod(c, 2 ** (M - i - 1), p)
        R = (R * b) % p
        t = (t * b * b) % p
        c = (b * b) % p
        M = i

    return R


def ECC_Addition(x1, y1, x2, y2, A, B, p):     # ECC加法实现函数
    if x1 is None and y1 is None:       # 如果发现传入了无穷远点,就直接返回另外一个点
        return x2, y2
    elif x1 == x2 and y1 == y2:
        afa = ((3 * x1 * x1 + 2 * A * x1 + 1) * invert((2 * B * y1), p)) % p
    else:
        afa = ((y2 - y1) * invert((x2 - x1), p)) % p
    x3 = (B * afa * afa - A - x1 - x2) % p
    y3 = (afa * (x1 - x3) - y1) % p
    return x3, y3


def Montgomery(Px, Py, k, A, B, p):     # 用来实现“循环自增”的函数
    R0x, R0y = Px, Py
    R1x, R1y = ECC_Addition(Px, Py, Px, Py, A, B, p)
    for i in k[1:]:     # 剔除最高位,从高到低逐位分析
        if i == '0':
            temp_R0x, temp_R0y = ECC_Addition(R0x, R0y, R0x, R0y, A, B, p)      # 这里要小心,要认为R0和R1的改变是同时的,于是不能是R0变了之后再改R1
            R1x, R1y = ECC_Addition(R0x, R0y, R1x, R1y, A, B, p)
            R0x, R0y = temp_R0x, temp_R0y
        else:
            R0x, R0y = ECC_Addition(R0x, R0y, R1x, R1y, A, B, p)
            R1x, R1y = ECC_Addition(R1x, R1y, R1x, R1y, A, B, p)
    return R0x, R0y


G_x = 9     # 点G的x坐标
mul_p = 2 ** 255 - 19       # 多项式中的模数p

print("=== Tonelli–Shanks 算法求模平方根(求解二次剩余) ===")
a = ((G_x ** 3) + (486662 * G_x * G_x) + G_x)       # 把x值带入多项式计算

G_y = tonelli_shanks(a, mul_p)
print(f"\n满足 y² ≡ a mod p 的")
print(f"第一个解是:G_y1 = {G_y}")        # 解出两个Y
print(f"另一个解是:G_y2 = {mul_p - G_y}")

inp_k = bin(0x1337c0decafe)[2:]
# inp_n = int('0x1337c0decafe', 16)        # 叠加量
inp_A = 486662      # 多项式中X平方的系数A
inp_B = 1       # 多项式中Y的系数B
result_x, result_y = Montgomery(G_x, G_y, inp_k, inp_A, inp_B, mul_p)
print(f"[{int(inp_k, 2)}]G的计算结果1是:( {result_x} , {result_y} )")

G_y = mul_p - G_y
result_x, result_y = Montgomery(G_x, G_y, inp_k, inp_A, inp_B, mul_p)
print(f"[{int(inp_k, 2)}]G的计算结果2是:( {result_x} , {result_y} )")

Smooth Criminal --- Pohlig-Hellman算法

题目给了一个脚本,还有一份输出文件。显然,这是一道密码分析的题目。

题目给的脚本,分析一下:

from Crypto.Cipher import AES
from Crypto.Util.number import inverse
from Crypto.Util.Padding import pad, unpad
from collections import namedtuple
from random import randint
import hashlib
import os

# Create a simple Point class to represent the affine points.
Point = namedtuple("Point", "x y")

# The point at infinity (origin for the group law).
O = 'Origin'

FLAG = b'crypto{??????????????????????????????}'


def check_point(P: tuple):		# "检查这个点"函数。检查点是否满足某些条件
    if P == O:		# 当这个点是无穷远点或同时满足下面3个条件时,返回True
        return True
    else:
        return (P.y**2 - (P.x**3 + a*P.x + b)) % p == 0 and 0 <= P.x < p and 0 <= P.y < p


def point_inverse(P: tuple):		# 对称点求解函数。如果靠点的x值和ECC方程求Y,通常会得到两个Y。这个函数应该是在知道其中一点的情况下尝试求出另一个点
    if P == O:
        return P
    return Point(P.x, -P.y % p)


def point_addition(P: tuple, Q: tuple):		# 点加法函数
    # based of algo. in ICM
    if P == O:		# 如果P或Q是无穷远点,直接返回Q或P
        return Q
    elif Q == O:
        return P
    elif Q == point_inverse(P):		# 如果Q点和P点关于x轴对称,那就返回无穷远点
        return O
    else:
        if P == Q:		# 很明显,这是“威尔斯特拉斯式”的点加法公式!
            lam = (3*P.x**2 + a)*inverse(2*P.y, p)
            lam %= p
        else:
            lam = (Q.y - P.y) * inverse((Q.x - P.x), p)
            lam %= p
    Rx = (lam**2 - P.x - Q.x) % p
    Ry = (lam*(P.x - Rx) - P.y) % p
    R = Point(Rx, Ry)
    assert check_point(R)		# 算出来的点还得满足某些条件才行
    return R


def double_and_add(P: tuple, n: int):		# 点倍加函数
    # based of algo. in ICM
    Q = P
    R = O
    while n > 0:		# 这里是“威尔斯特拉斯式”的“循环自增”
        if n % 2 == 1:
            R = point_addition(R, Q)
        Q = point_addition(Q, Q)
        n = n // 2
    assert check_point(R)		# 算出来的点还得满足某些条件才行
    return R


def gen_shared_secret(Q: tuple, n: int):		# 共享密钥生成函数
    # Bob's Public key, my secret int
    S = double_and_add(Q, n)		# 这里共享密钥S = [n]Q
    return S.x


def encrypt_flag(shared_secret: int):		# flag加密函数
    # Derive AES key from shared secret
    sha1 = hashlib.sha1()		# 先用sha1加密共享密钥并取出前16位作为密钥
    sha1.update(str(shared_secret).encode('ascii'))
    key = sha1.digest()[:16]
    # Encrypt flag
    iv = os.urandom(16)		# iv是16字节的随机字节流,十六进制加密后输出
    cipher = AES.new(key, AES.MODE_CBC, iv)		# 用CBC模式的AES对16字节填充后的flag进行加密,十六进制加密后输出
    ciphertext = cipher.encrypt(pad(FLAG, 16))
    # Prepare data to send
    data = {}
    data['iv'] = iv.hex()
    data['encrypted_flag'] = ciphertext.hex()
    return data


# Define the curve
p = 310717010502520989590157367261876774703		# 可能是“威尔斯特拉斯式”的ECC方程
a = 2
b = 3

# Generator
g_x = 179210853392303317793440285562762725654		# 这应该是生成点
g_y = 105268671499942631758568591033409611165
G = Point(g_x, g_y)

# My secret int, different every time!!
n = randint(1, p)		# 1到p-1随机生成一个整数

# Send this to Bob!
public = double_and_add(G, n)		# 把 [n]G 的计算结果发给Bob
print(public)

# Bob's public key
b_x = 272640099140026426377756188075937988094		# Bob的公钥对应的点
b_y = 51062462309521034358726608268084433317
B = Point(b_x, b_y)

# Calculate Shared Secret
shared_secret = gen_shared_secret(B, n)		# 生成共享密钥S = [n]B

# Send this to Bob!
ciphertext = encrypt_flag(shared_secret)		# 加密flag并输出
print(ciphertext)

还给了一份输出结果:

Point(x=280810182131414898730378982766101210916, y=291506490768054478159835604632710368904)

{'iv': '07e2628b590095a5e332d397b8a59aa7', 'encrypted_flag': '8220b7c47b36777a737f5ef9caa2814cf20c1c1ef496ec21a9b4833da24a008d0870d3ac3a6ad80065c138a2ed6136af'}

分析一下加密代码的逻辑:

首先你有“威尔斯特拉斯式”的ECC方程、一个生成点G,还有一个随机数n。

然后把 [n]G 算出来发给Bob(有点忘记了这是什么)(这个在后续流程中其实是给我们爆破用的)。

然后拿着Bob的公钥点B,用 [n]B 算出共享密钥S,然后拿着共享密钥加密flag并输出。

那么解密代码的逻辑应该是这样的:

首先根据已知信息分析出n值,然后算出共享密钥S(或者说,用任意手段得到共享密钥S);

然后再配合已知的iv和encrypted_flag解密出flag。

这里通过观察代码可以发现,直接求n属于离散对数问题,理论上会很难。那么可能得靠其他方法来求共享密钥S。这里由于我们知道G、B和[n]G,而我们需要使用的公式是:

S=[n]BS=[n]B

这里可能要再化解一些这个式子。比如:既然G是生成点,那么可不可能是它生成的B呢?如果这样,就有:

B=[m]GB=[m]G

结合起来就是:

S=[n][m]G=[m]([n]G)S=[n][m]G=[m]([n]G)

所以如果我们能找到m,就能求出共享密钥S。这里穷举试试:

from Crypto.Cipher import AES
from Crypto.Util.number import inverse
from Crypto.Util.Padding import pad, unpad
from collections import namedtuple
from random import randint
import hashlib
import os
import time

# Create a simple Point class to represent the affine points.
Point = namedtuple("Point", "x y")

O = 'Origin'


def point_inverse(P: tuple):
    if P == O:
        return P
    return Point(P.x, -P.y % p)


def point_addition(P: tuple, Q: tuple):
    # based of algo. in ICM
    if P == O:
        return Q
    elif Q == O:
        return P
    elif Q == point_inverse(P):
        return O
    else:
        if P == Q:
            lam = (3*P.x**2 + a)*inverse(2*P.y, p)
            lam %= p
        else:
            lam = (Q.y - P.y) * inverse((Q.x - P.x), p)
            lam %= p
    Rx = (lam**2 - P.x - Q.x) % p
    Ry = (lam*(P.x - Rx) - P.y) % p
    R = Point(Rx, Ry)
    return R


# Define the curve
p = 310717010502520989590157367261876774703
a = 2
b = 3

g_x = 179210853392303317793440285562762725654
g_y = 105268671499942631758568591033409611165
G = Point(g_x, g_y)

b_x = 272640099140026426377756188075937988094
b_y = 51062462309521034358726608268084433317
B = Point(b_x, b_y)

m = 2
while 1:
    R = G
    R = point_addition(R, G)
    print(m)
    if R[0] == B[0]:
        print("找到m:", m)
        break
    m = m + 1

这里m穷举到了1000多万了都没出结果,那么这题不大可能是这样做了。

如果这样,出路貌似就只剩下了解这个离散对数问题。。。

这里的离散对数问题可以这样解:使用 Pohlig-Hellman 算法 ,借助对方程的阶的分解来把大的离散对数问题转变成多个小的离散对数问题,然后配合BSBG算法暴力破解离散对数问题,求出n。

Pohlig-Hellman算法 流程:

首先要有ECC表达式,然后算出ECC方程的“阶”,并对其做大素数分解。然后把分解得到的每个素数都存起来(中国剩余定理的p),再使ECC方程的阶依次(地板)除这些分解得到的素数,这样会得到一个值(有点像中国剩余定理的M1=M/m1,M2=M/m2这样的),然后用这个值倍加要做离散对数破解的点,然后做离散对数暴力破解,破解结果(中国剩余定理的b)保存起来。最后把两个结果集拿去做中国剩余定理求解得出n。

接下来向SageMath低头。因为某些算法手搓要求太高,引入其他的库又让Python成分十分复杂,所以……

这里其实参考了别人的脚本来捋明白这个算法是怎么回事,Sage代码也做了参考:(大约1min能完成爆破)

from sage.all import *		# 当你需要用Sage控制台运行放在Sage根目录的.py文件时,你需要有这样一句库依赖语句来激活Sage

p = 310717010502520989590157367261876774703
a = 2
b = 3
ECC_Func = EllipticCurve(GF(p), [a, b])		# 定义ECC方程
ECC_Func_Order = ECC_Func.order()		# 算出ECC方程的阶
order_Factors = factor(ECC_Func_Order)		# 对阶做大素数分解

g_x = 179210853392303317793440285562762725654
g_y = 105268671499942631758568591033409611165
G = ECC_Func(g_x,g_y)		# 定义ECC上的点

b_x = 272640099140026426377756188075937988094
b_y = 51062462309521034358726608268084433317
B = ECC_Func(b_x, b_y)

ng_x = 280810182131414898730378982766101210916
ng_y = 291506490768054478159835604632710368904
NG = ECC_Func(ng_x, ng_y)

b_List = []
p_List = []
for i in order_Factors:
    temp_P = i[0] ** i[1]		# 其实就是,把底数和指数结合起来,相当于这里得到了单个素因子
    p_List.append(temp_P)		# 素因子作为中国剩余定理的p,存起来
    temp_P = ECC_Func_Order // temp_P		# 有点像中国剩余定理的M1=M/m1,M2=M/m2这样的
    b_List.append(discrete_log(temp_P * NG, temp_P * G, operation='+'))		# BSGS算法暴力破解离散对数问题,并将结果作为中国剩余定理的b
n = crt(b_List, p_List)		# 中国剩余定理求解得n
print(n * B)		# 计算共享密钥S

输出结果:

(171172176587165701252669133307091694084 : 188106434727500221954651940996276684440 : 1)

取其x轴坐标为shared_secret,再结合iv和encrypted_flag,写解密脚本,解密后即得flag:

import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

def decrypt_flag(shared_secret: int, iv: str, encrypted_flag: str):
    iv = bytes.fromhex(iv)
    encrypted_flag = bytes.fromhex(encrypted_flag)
    sha1 = hashlib.sha1()
    sha1.update(str(shared_secret).encode('ascii'))
    key = sha1.digest()[:16]
    temp = AES.new(key, AES.MODE_CBC, iv)
    message = unpad(temp.decrypt(encrypted_flag), 16)
    return message

shared_secret = 171172176587165701252669133307091694084
iv = '07e2628b590095a5e332d397b8a59aa7'
encrypted_flag = '8220b7c47b36777a737f5ef9caa2814cf20c1c1ef496ec21a9b4833da24a008d0870d3ac3a6ad80065c138a2ed6136af'

message = decrypt_flag(shared_secret, iv, encrypted_flag)
print(message.decode())

Curveball --- 生成元劫持(依靠标量乘法的结合律)

题目给了一份代码文件:

#!/usr/bin/env python3

import fastecdsa
from fastecdsa.point import Point
from fastecdsa.curve import P256
from utils import listener


FLAG = "crypto{????????????????????????????????????}"
G = P256.G
assert G.x, G.y == [0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296,
                    0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5]     # G是一个生成元,其x和y数值必然是这样的


class Challenge():
    def __init__(self):
        self.before_input = "Welcome to my secure search engine backed by trusted certificate library!\n"
        self.trusted_certs = {
            'www.cryptohack.org': {     # 字典,这三本字典,每本内部还存多个键值对
                "public_key": Point(0xE9E4EBA2737E19663E993CF62DFBA4AF71C703ACA0A01CB003845178A51B859D, 0x179DF068FC5C380641DB2661121E568BB24BF13DE8A8968EF3D98CCF84DAF4A9, curve=P256),
                "curve": "secp256r1",		# 注意这里的曲线类型,在网上找到固定的参数后能得到ECC方程
                "generator": [G.x, G.y]
            },
            'www.bing.com': {
                "public_key": Point(0x3B827FF5E8EA151E6E51F8D0ABF08D90F571914A595891F9998A5BD49DFA3531, 0xAB61705C502CA0F7AA127DEC096B2BBDC9BD3B4281808B3740C320810888592A, curve=P256),
                "curve": "secp256r1",
                "generator": [G.x, G.y]
            },
            'www.gchq.gov.uk': {
                "public_key": Point(0xDEDFC883FEEA09DE903ECCB03C756B382B2302FFA296B03E23EEDF94B9F5AF94, 0x15CEBDD07F7584DBC7B3F4DEBBA0C13ECD2D2D8B750CBF97438AF7357CEA953D, curve=P256),
                "curve": "secp256r1",
                "generator": [G.x, G.y]
            }
        }

    def search_trusted(self, Q):
        for host, cert in self.trusted_certs.items():       # 遍历三本字典的键值对
            if Q == cert['public_key']:     # 子字典中,如果公钥匹配
                return True, host       # 就返回对应地址
        return False, None

    def sign_point(self, g, d):		# 椭圆曲线中的标量乘法运算
        return g * d

    def connection_host(self, packet):
        d = packet['private_key']       # 扒出这本字典的私钥值,赋值到d上
        if abs(d) == 1:     # 如果私钥值的绝对值等于1
            return "Private key is insecure, certificate rejected."
        packet_host = packet['host']        # 下面三段是字典数值读取
        curve = packet['curve']
        x, y = packet['generator']
        g = Point(x, y, curve=P256)
        Q = self.sign_point(g, d)       # 相当于 Q = g * d
        cached, host = self.search_trusted(Q)       # 接下来,传入计算结果点。如果匹配到公钥,就返回host
        if cached:
            return host
        else:       # 如果没匹配到,就添加字典
            self.trusted_certs[packet_host] = {
                "public_key": Q,
                "curve": "secp256r1",
                "generator": G
            }
            return "Site added to trusted connections"

    def bing_it(self, s):       # 可以做到flag读取
        return f"Hey bing! Tell me about {s}"

    #
    # This challenge function is called on your input, which must be JSON
    # encoded
    #
    def challenge(self, your_input):
        host = self.connection_host(your_input)     # 我们需要让connection_host返回"www.bing.com"
        if host == "www.bing.com":
            return self.bing_it(FLAG)
        else:
            return self.bing_it(host)


import builtins; builtins.Challenge = Challenge # hack to enable challenge to be run locally, see https://cryptohack.org/faq/#listener
listener.start_server(port=13382)

经过分析,我们获取到flag的流程大概就是:

我们输入东西,应该会激活challenge函数,然后输入被送到connection_host函数中,我们要想办法让它返回"www.bing.com"。

在connection_host函数中,我们的输入会作为字典名,然后把字典中的私钥值和公钥点拿出来做标量乘法。乘完后,如果其结果等于"www.bing.com"字典中的公钥点,就成功。

而如果打开Ubantu去nc题目中给的 socket.cryptohack.org 13382 ,直接回车,就会发现提示我们输入JSON。那么,这样看来,我们是要输入完整的JSON。

依据代码,需要包含private_key、host、curve、generator四部分,其中,curve随意,host不跟trusted_certs中出现过的一致即可。那么当我们传入这样的JSON的时候,就相当于传入字典了。

按照我们刚才的流程,需要让下面的式子成立:

[我们传入的私钥值] 我们传入的公钥点 == 已知公钥点

按照代码限制:我们传入的私钥值不能为1。否则直接复制"www.bing.com"的公钥点提交直接就能出flag:

[1]public_key=public_key[1]public\_key= public\_key

那么再选个小点的私钥值,等于2。这时候:

[2]g=public_key[2]g= public\_key

这个地方可以在 public_key 的基础上,再乘上2关于模数n的逆元,把私钥值2抵消掉。意思就是,下面的数学算式是成立的:

[2]([21]public_key)([2][21])public_key[1]public_keypublic_key mod n[2]([2^{-1}]public\_key)≡([2][2^{-1}])public\_key≡[1]public\_key≡public\_key\ mod\ n

这就是所谓的“标量乘法的结合律”。

那么,我们需要做的就是,把下面这个点算出来,然后和其他杂七碎八的东西写成JSON形式传入到目标服务器中:

[21]public_key[2^{-1}]public\_key

于是使用Sage来算这个点:

from sage.all import *
from gmpy2 import *

# 网上找到的secp256r1曲线参数,写成ECC方程
p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
E = EllipticCurve(GF(p), [a, b])

# 定义"www.bing.com"的公钥点
target = E(0x3B827FF5E8EA151E6E51F8D0ABF08D90F571914A595891F9998A5BD49DFA3531, 0xAB61705C502CA0F7AA127DEC096B2BBDC9BD3B4281808B3740C320810888592A)

# 算出2关于模数n的乘法逆元,默认输出十进制数值
i = invert(2, n)

# 做标量乘法运算,算出我们要的点
target = i * target

# 构造好JSON格式来传入服务器(你的JSON必须写成一整行来传入到目标的系统中,否则换行符会引发报错)
print(f'{{"private_key": 2,"curve": "secp256r1","host": "666666","generator": [{target[0]},{target[1]}]}}')

然后打开sage终端输入命令:

python ./2.py		# 上面代码我存在sage根目录的2.py中

输出:

{"private_key": 2,"curve": "secp256r1","host": "666666","generator": [15520159875205514130255899098025123715054849599936616868365830290232639266390,35332573964480432986660122673305225849700662492297568815244635356931754804527]}

打开Ubantu终端,依次输入:

nc socket.cryptohack.org 13382		# 然后回车

{"private_key": 2,"curve": "secp256r1","host": "666666","generator": [15520159875205514130255899098025123715054849599936616868365830290232639266390,35332573964480432986660122673305225849700662492297568815244635356931754804527]}		# 回车后得到flag

ProSign 3 --- ECDSA签名

如果打开Ubantu去nc题目中给的 socket.cryptohack.org 13381 ,直接回车,就会发现提示我们输入JSON。那么,这样看来,我们是要输入完整的JSON。

题目给了一份代码文件:

#!/usr/bin/env python3

import hashlib
from Crypto.Util.number import bytes_to_long, long_to_bytes
from ecdsa.ecdsa import Public_key, Private_key, Signature, generator_192
from utils import listener
from datetime import datetime
from random import randrange

FLAG = "crypto{?????????????????????????}"
g = generator_192       # 获取secp192r1的生成元
n = g.order()       # 获取该曲线的阶,一个192位的大素数


class Challenge():
    def __init__(self):
        self.before_input = "Welcome to ProSign 3. You can sign_time or verify.\n"
        secret = randrange(1, n)        # 从 1~(n-1) 选一个随机数
        self.pubkey = Public_key(g, g * secret)     # 公钥和 g 及 secret与g做标量乘法得到的值 有关
        self.privkey = Private_key(self.pubkey, secret)     # 私钥和 公钥 及 secret 有关

    def sha1(self, data):       # sha1加密函数
        sha1_hash = hashlib.sha1()
        sha1_hash.update(data)
        return sha1_hash.digest()

    def sign_time(self):
        now = datetime.now()        # 读取当前时间
        m, n = int(now.strftime("%m")), int(now.strftime("%S"))     # m为分,n为秒。注意!这里n被重置为了1~60以内的一个数!!!
        current = f"{m}:{n}"
        msg = f"Current time is {current}"      # 提示消息msg,包含分和秒的数据
        hsh = self.sha1(msg.encode())       # 对msg做sha1加密
        sig = self.privkey.sign(bytes_to_long(hsh), randrange(1, n))        # 用私钥签名这两个数据(这里有另外一个随机数,记为k)
        return {"msg": msg, "r": hex(sig.r), "s": hex(sig.s)}       # 返回原始消息,还有签名的两个部分

    def verify(self, msg, sig_r, sig_s):
        hsh = bytes_to_long(self.sha1(msg.encode()))        # 对传入的msg做sha1加密
        sig_r = int(sig_r, 16)      # 解出原始的sig_r和sig_s
        sig_s = int(sig_s, 16)
        sig = Signature(sig_r, sig_s)       # 把签名的两部分组合起来,用于下一步验证签名
        if self.pubkey.verifies(hsh, sig):      # 验证签名有效性
            return True
        else:
            return False

    #
    # This challenge function is called on your input, which must be JSON
    # encoded
    #
    def challenge(self, your_input):
        if 'option' not in your_input:      # 我们的输入中必须包含“option”这个字符串,否则报错
            return {"error": "You must send an option to this server"}

        elif your_input['option'] == 'sign_time':       # “option”是一个键。如果它的值是'sign_time',就返回sign_time()函数的返回值
            signature = self.sign_time()
            return signature

        elif your_input['option'] == 'verify':      # 如果“option”键对应的值为'verify'
            # 于是就从我们的输入中读取下面这三个键的值,然后传入verify()函数中
            msg = your_input['msg']
            r = your_input['r']
            s = your_input['s']
            verified = self.verify(msg, r, s)
            if verified:        # 如果verify()函数返回值真,且我们刚才传入的“msg”键对应的返回值为"unlock",就得到flag
                if msg == "unlock":
                    self.exit = True
                    return {"flag": FLAG}
                return {"result": "Message verified"}       # 如果没成功,下面有各种不同的错误提示,可以帮助判断哪一步错了
            else:
                return {"result": "Bad signature"}

        else:
            return {"error": "Decoding fail"}


import builtins; builtins.Challenge = Challenge # hack to enable challenge to be run locally, see https://cryptohack.org/faq/#listener
listener.start_server(port=13381)

经过分析,我们获取到flag的流程大概就是:

我们要输入一段JSON,至少包含一个键:option。当option键的值为“sign_time”时,会输出一段数据;当option的值为“verify”时,还需要输入三个键msg、r和s,其中,msg的值必然为"unlock"。

应该先借助sign_time()函数来推测出一些重要数据,然后爆破出第二个随机数k(可以爆破,因为取值范围不大),然后根据ECDSA签名公式拓展出的随机数secret破解公式破解出第一个随机数secret,据此恢复出公钥和私钥。然后用私钥签名我们需要提交的msg,从中获取到r和s(签名的两部分),整合成一段JSON提交以通过verify()函数的签名验证。

ECDSA签名公式中随机数secret破解公式:

secret=skhr mod nsecret=\frac{s·k-h}{r}\ mod\ n

正式ECDSA签名公式如下:(我们可以利用这边的第一条来爆破k)

{r=(kG).x mod ns=k1(h+dr) mod n{ \begin{cases} {r=(k⋅G).x\ mod\ n}\\ {s=k^{−1}(h+d⋅r)\ mod\ n} \end{cases} }\\

第一步,构造JSON来获取可以推敲的数据。打开Ubantu终端,依次输入:

nc socket.cryptohack.org 13381

{"option":"sign_time"}

返回:

{"msg": "Current time is 5:45", "r": "0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012", "s": "0x47b8ae852c5b33ea9ab1835d2d734d5bbe1a04a45565c8fe"}

据此写python脚本:

import hashlib
from Crypto.Util.number import bytes_to_long, long_to_bytes
from ecdsa.ecdsa import Public_key, Private_key, Signature, generator_192
from gmpy2 import *


def sha1(data):  # 做sha1加密
    sha1_hash = hashlib.sha1()
    sha1_hash.update(data)
    return sha1_hash.digest()


g = generator_192       # 获取secp192r1的生成元
n = g.order()

m1 = "Current time is 5:45"		# 需要手动输入
h1 = bytes_to_long(sha1(m1.encode()))		# 做sha1加密
r1 = int("0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012", 16)		# 随机变化,需要手动输入
s1 = int("0x47b8ae852c5b33ea9ab1835d2d734d5bbe1a04a45565c8fe", 16)		# 随机变化,需要手动输入
m2 = "unlock"
h2 = sha1(m2.encode())       # 做sha1加密

k = 0       # 爆破k
while 1:
    temp = k * g
    if temp.x() == r1:
        break
    else:
        k = k + 1

secret = ((s1 * k - h1) * invert(r1, n)) % n		# secret破解公式
pubkey = Public_key(g, g * secret)		# 按照原加密脚本流程算出公钥和私钥
privkey = Private_key(pubkey, secret)

sig = privkey.sign(bytes_to_long(h2), k)		# 使用私钥对我们的消息进行签名
print(f'{{"option": "verify","msg": "{m2}","r": "{hex(sig.r)}","s": "{hex(sig.s)}"}}')		# 签完后提取出r和s,和其他信息一起写成可以直接提交到服务器中的JSO

打开Ubantu终端,依次输入:

nc socket.cryptohack.org 13381

{"option":"sign_time"}
# 然后服务器会返回一些数据,要把数据复制粘贴到python脚本中使用

{"option": "verify","msg": "unlock","r": "0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012","s": "0x47b8ae8593c4821b43e18461f3cdb065cd69afcbecea8c16"}
# 然后服务器会返回flag

Moving Problems --- MOV攻击

题目给了一份代码文件,是.sage后缀的,直接改成.py后缀就能在PyCharm中查看:

import random
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from sage.all import *      # 我自己加上的一段,看到“sage”后的条件反射

FLAG = b"crypto{??????????????????????????????????????}"

def gen_keypair(G, p):
    n = random.randint(1, (p-1))
    P = n*G		# 就是,让生成元和随机数n(取值范围0~1331169830894825846283645180581,貌似不是很大)做标量乘法
    return n, P

def gen_shared_secret(P, n):
    S = P*n
    return S.xy()[0]

def encrypt_flag(shared_secret: int):
    # Derive AES key from shared secret
    sha1 = hashlib.sha1()
    sha1.update(str(shared_secret).encode('ascii'))
    key = sha1.digest()[:16]		# 先对共享密钥做sha1加密,然后提取出前16个字符
    # Encrypt flag
    iv = os.urandom(16)		# 生成一个iv,这个在output.txt直接提供了
    cipher = AES.new(key, AES.MODE_CBC, iv)
    ciphertext = cipher.encrypt(pad(FLAG, 16))		# 十六位填充flag后用CBC模式的AES加密输出
    # Prepare data to send
    data = {}
    data['iv'] = iv.hex()
    data['encrypted_flag'] = ciphertext.hex()		# flag加密结果再十六进制加密后输出
    return data

# Define Curve params
p = 1331169830894825846283645180581
a = -35
b = 98
E = EllipticCurve(GF(p), [a,b])		# 定义一段ECC方程
G = E.gens()[0]		# ???获取一个生成元

# Generate keypair
n_a, P1 = gen_keypair(G, p)		# ???放入生成元和大素数p,做标量乘法并返回
n_b, P2 = gen_keypair(G, p)

# Calculate shared secret
S1 = gen_shared_secret(P1, n_b)		# 双方都生成共享密钥,公式是[n_a][n_b]G
S2 = gen_shared_secret(P2, n_a)

# Check protocol works
assert S1 == S2		# 双方的共享密钥必然是相同的

flag = encrypt_flag(S1)		# 放入共享密钥加密flag

print(f"Generator: {G}")
print(f"Alice Public key: {P1}")
print(f"Bob Public key: {P2}")
print(f"Encrypted flag: {flag}")

还给了一段输出:

Generator: (479691812266187139164535778017 : 568535594075310466177352868412 : 1)
Alice Public key: (1110072782478160369250829345256 : 800079550745409318906383650948 : 1)
Bob Public key: (1290982289093010194550717223760 : 762857612860564354370535420319 : 1)
Encrypted flag: {'iv': 'eac58c26203c04f68d63dc2c58d79aca', 'encrypted_flag': 'bb9ecbd3662d0671fd222ccb07e27b5500f304e3621a6f8e9c815bc8e4e6ee6ebc718ce9ca115cb4e41acb90dbcabb0d'}

经过分析,我们获取到flag的流程大概就是:

因为n_a貌似不是很大,因此可以从[n_a]G=P1中破解出n_a,然后解出共享密钥。求出共享密钥后处理为key,然后同iv和flag密文做CBC模式的AES解密得到flag。

于是使用sage来解:

import random
import hashlib
# from Crypto.Cipher import AES
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
from sage.all import *      # 我自己加上的一段,看到“sage”后的条件反射

def decrypt_flag(shared_secret: int, iv: str, encrypted_flag: str):
    iv = bytes.fromhex(iv)
    encrypted_flag = bytes.fromhex(encrypted_flag)
    # Derive AES key from shared secret
    sha1 = hashlib.sha1()
    sha1.update(str(shared_secret).encode('ascii'))
    key = sha1.digest()[:16]
    # Encrypt flag
    mode = AES.new(key, AES.MODE_CBC, iv)
    return unpad(mode.decrypt(encrypted_flag), 16)

# Define Curve params
p = 1331169830894825846283645180581
a = -35
b = 98
E = EllipticCurve(GF(p), [a,b])
G = E.gens()[0]

P1 = E(1110072782478160369250829345256, 800079550745409318906383650948)
P2 = E(1290982289093010194550717223760, 762857612860564354370535420319)

n_a = discrete_log(P1, G, operation='+')
S = n_a * P2

iv_ori = 'eac58c26203c04f68d63dc2c58d79aca'
cipher = 'bb9ecbd3662d0671fd222ccb07e27b5500f304e3621a6f8e9c815bc8e4e6ee6ebc718ce9ca115cb4e41acb90dbcabb0d'
flag = decrypt_flag(S.x(), iv_ori, cipher)

print(flag)

失败了,这样爆不出来。。。

需要用更高效的爆破方法,了解到MOV攻击,数学原理非常复杂,从网上找到sage脚本:

import hashlib
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import unpad
from sage.all import *

def decrypt_flag(shared_secret):
    iv = bytes.fromhex('eac58c26203c04f68d63dc2c58d79aca')
    ct = bytes.fromhex('bb9ecbd3662d0671fd222ccb07e27b5500f304e3621a6f8e9c815bc8e4e6ee6ebc718ce9ca115cb4e41acb90dbcabb0d')

    sha1 = hashlib.sha1()
    sha1.update(str(shared_secret).encode('ascii'))
    key = sha1.digest()[:16]
    cipher = AES.new(key, AES.MODE_CBC, iv)

    pt = unpad(cipher.decrypt(ct), 16)
    return pt

p = 1331169830894825846283645180581
a = -35
b = 98

E = EllipticCurve(GF(p), [a, b])
G = E((479691812266187139164535778017, 568535594075310466177352868412))
Alice = E((1110072782478160369250829345256, 800079550745409318906383650948))
Bob = E((1290982289093010194550717223760, 762857612860564354370535420319))

# https://ctftime.org/writeup/33964
order = G.order()
k = 1
while (p**k - 1) % order:
    k += 1
# k = 2

Ee = EllipticCurve(GF(p**k, 'y'), [a, b])
Ge = Ee(G)
Ae = Ee(Alice)

T = Ee.random_point()
M = T.order()
d = gcd(M, G.order())
Q = (M//d)*T

assert G.order() / Q.order() in ZZ

N = G.order()

a = Ge.weil_pairing(Q, N)
b = Ae.weil_pairing(Q, N)

# After a few minutes: na=29618469991922269
na = b.log(a)
assert na*G == Alice

priv_key = (na*Bob).xy()[0]

print(decrypt_flag(priv_key))

打开sage,然后输入命令如:

python 3.py

然后大概等待10分钟能出flag。

从这里可以提炼出MOV攻击脚本:

import hashlib
from sage.all import *

# 下面三个参数需要手动更换填写
p = 1331169830894825846283645180581
a = -35
b = 98

E = EllipticCurve(GF(p), [a, b])

# 下面两个点的参数需要手动更换填写
G = E((479691812266187139164535778017, 568535594075310466177352868412))
resultPoint = E((1110072782478160369250829345256, 800079550745409318906383650948))

order = G.order()
k = 1
while (p**k - 1) % order:
    k += 1
    if k > 10:
        print("MOV攻击不适用")
        exit(0)
# k通常就等于1或2,因为k是所谓“嵌入度”,低嵌入度是MOV攻击有效的原因之一,曲线嵌入度超过10就不适合用MOV攻击了

Ee = EllipticCurve(GF(p**k, 'y'), [a, b])
Ge = Ee(G)
Re = Ee(resultPoint)

T = Ee.random_point()
M = T.order()
d = gcd(M, G.order())
Q = (M//d)*T

assert G.order() / Q.order() in ZZ

N = G.order()

a = Ge.weil_pairing(Q, N)
b = Re.weil_pairing(Q, N)

n = b.log(a)
assert n*G == resultPoint

print(f"MOV攻击结果:[{n}]G = resultPoint")