Cryptohack-SYMMETRIC CIPHERS

SYMMETRIC CIPHERS

对称密钥密码是使用相同密钥加密和解密数据的算法。目标是使用短密钥安全有效地发送长消息。

最著名的对称密钥密码是2001年标准化的高级加密标准(AES)。它如此普遍,以至于现代处理器甚至包含了执行AES操作的特殊指令集。这里的第一系列挑战将指导您了解AES的内部工作原理,向您展示其单独的组件如何协同工作,使其成为一个安全的密码。到最后,您将构建自己的AES解密代码!

我们可以将对称密钥密码分为两类,分组密码和流密码。分组密码将明文分解为固定长度的块,并将每个块与密钥一起通过加密函数发送。同时,流密码通过将伪随机密钥流与数据进行异或运算,一次加密一个字节的明文。AES是一种分组密码,但可以使用CTR等操作模式转换为流密码。

分组密码仅指定如何加密和解密单个块,必须使用一种操作模式将密码应用于较长的消息。这就是现实世界的实现经常失败的地方,因为开发人员不理解使用特定模式的微妙含义。在剩下的挑战中,您将攻击各种模式的常见滥用。

How AES WORKS

Keyed Permutations

AES和所有好的分组密码一样,执行“密钥置换”。这意味着它将每个可能的输入块映射到一个唯一的输出块,并使用密钥确定要执行的置换。

“块”只是指固定数量的比特或字节,可以表示任何类型的数据。AES处理一个块并输出另一个块。我们将具体讨论AES的变体,它适用于128位(16字节)块和128位密钥,称为AES-128。

使用相同的密钥,可以反向执行置换,将输出块映射回原始输入块。重要的是,输入和输出块之间有一对一的对应关系,否则我们将无法依赖密文解密回我们开始时的明文。

题目:一对一对应关系的数学术语是什么?

bijection

Resisting Bruteforce

如果分组密码是安全的,那么攻击者就没有办法将AES的输出与比特的随机排列区分开来。此外,没有比简单地强制执行每个可能的密钥更好的方法来撤消排列。这就是为什么学者们认为,如果他们能找到一种比粗暴破解密钥所需步骤更少的攻击,即使这种攻击实际上是不可行的,那么从理论上讲,密码就是“被攻破的”。

强制执行128位密钥空间有多难?有人估计,如果你用整个比特币挖矿网络的力量来对抗AES-128密钥,破解密钥将需要100多倍于宇宙年龄的时间。

事实证明,对AES的攻击比bruteforce好,但只是稍微好一点——它将AES-128的安全级别降低到126.1位,并且已经8年多没有改进了。鉴于128位提供的巨大“安全裕度”,以及尽管进行了广泛的研究,但仍缺乏改进,因此它不被认为是对AES安全的可信风险。但是,是的,从非常狭义的意义上讲,它“打破”了AES。

最后,虽然量子计算机有可能通过Shor的算法完全破解RSA等流行的公钥密码系统,但据认为,通过Grover的算法,它们只能将对称密码系统的安全级别降低一半。这就是为什么人们建议使用AES-256的原因之一,尽管它的性能较差,因为它在量子未来仍然可以提供非常足够的128位安全性。

题目:针对AES的最佳单密钥攻击的名称是什么?

biclique

Structure of AES

为了实现在没有密钥的情况下无法反转的密钥置换,AES对输入应用了大量的ad-hoc混合操作。这与RSA等公钥密码系统形成鲜明对比,后者基于优雅的独立数学问题。AES的优雅程度要低得多,但速度非常快。

在较高层次上,AES-128从“密钥编排”开始,然后在一个状态矩阵上运行10轮。起始状态矩阵就是我们想要加密的明文块,表示为4x4字节矩阵。在10轮的过程中,状态矩阵被多次可逆变换反复修改。

每个转换步骤都有一个明确的目的,基于Claude Shannon在20世纪40年代建立的安全密码的理论性质。在接下来的挑战中,我们将更仔细地研究其中的每一个步骤。

以下是AES加密阶段的概述:

  1. 密钥扩展/密钥编排

    从128位密钥中,导出11个单独的128位“轮密钥”:一个用于每个轮密钥添加步骤。

  2. 初始密钥添加

    轮密钥添加 - 第一轮密钥的字节与状态矩阵的字节进行XOR运算。

  3. 循环 - 此阶段循环10次,包括9个主循环和一个“最后一轮”

    1. 字节代换 - 根据查找表(”S-box”),每个规定的字节被替换为不同的字节。
    2. 行移位 - 状态矩阵的最后三行被转置——在一列、两列或三列上移动。
    3. 列混淆 - 对状态矩阵的列执行矩阵乘法,将每列中的四个字节组合在一起。这在最后一轮中被跳过。
    4. 轮密钥添加 - 当前轮密钥的字节与状态矩阵的字节进行XOR运算。

题目:代码中包括一个bytes2matrix函数,用于将初始明文块转换为状态矩阵。编写一个matrix2bytes函数将该矩阵转换回字节,并将生成的明文作为flag提交。

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# matrix.py
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))

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from Crypto.Util.number import *

def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
message = ''
for i in matrix:
for j in range(4):
message += str(hex(i[j])[2:])
return long_to_bytes(int(message, 16))

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

print(matrix2bytes(matrix))
# b'crypto{inmatrix}'

Round Keys

现在我们将跳过密钥扩展阶段的细节。关键在于,它接收我们的16字节密钥,并从我们的初始密钥中生成11个4x4矩阵,称为“轮密钥”。这些轮密钥允许AES从我们提供的单个密钥中获得额外的里程。

下一个初始密钥添加阶段有一个轮密钥添加步骤。轮密钥添加步骤很简单:它将当前状态矩阵与当前轮密钥进行异或。

image-20241108102447525

轮密钥添加也是每一轮的最后一步。轮密钥添加使AES成为一种“密钥置换”,而不仅仅是一种置换。这是AES中唯一将密钥混合到状态矩阵中的部分,但对于决定发生的置换至关重要。

正如您在之前的挑战中所看到的,如果您知道密钥,XOR是一个很容易反转的操作,但如果您不知道密钥,则很难撤消。现在想象一下,尝试恢复用11个不同密钥XOR的明文,并且在每个XOR操作之间用一系列替换和转置密码严重混淆。AES就是这么做的!我们将在接下来的几次挑战中看到这种联合的有效性。

题目:完成add_round_key函数,然后使用matrix2bytes函数获取下一个flag。

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from Crypto.Util.number import *

def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
message = ''
for i in matrix:
for j in range(4):
message += str(hex(i[j])[2:])
return long_to_bytes(int(message, 16))

def add_round_key(s, k):
new_state = [[0 for col in range(4)] for row in range(4)]
for i in range(4):
for j in range(4):
new_state[i][j] = s[i][j] ^ k[i][j]
return new_state

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],
]
matrix = add_round_key(state, round_key)
print(matrix2bytes(matrix))
# b'crypto{r0undk3y}'

Confusion through Substitution

每个AES轮的第一步是字节替换。这一步包含获取状态矩阵的每个字节,并将其替换为预设16x16查找表中的不同字节。查找表被称为“替换盒”或简称为“S盒,乍一看可能会令人困惑。让我们把它分解一下。

image-20241108105347799

1945年,美国数学家Claude Shannon发表了一篇关于信息论的开创性论文。它将“混淆”确定为安全密码的基本属性。“混淆”意味着密文和密钥之间的关系应该尽可能复杂。只要有一个密文,就不应该有办法了解任何关于密钥的信息。

如果密码的混淆性较差,则可以将密文、密钥和明文之间的关系表示为线性函数。例如,在凯撒密码中,ciphertext = plaintext + key。这是一个显而易见的关系,很容易逆转。更复杂的线性变换可以使用高斯消元等技术来解决。即使是低阶多项式,例如x^4 + 51x^3 + x这样的方程,也可以使用代数方法有效地求解。然而,多项式的次数越高,通常就越难求解——它只能用越来越多的线性函数来近似。

S盒的主要目的是以一种不易被线性函数近似的方式转换输入。S盒的目标是高非线性,虽然AES的S盒并不完美,但它非常接近。S盒中的快速查找是对输入字节执行极其非线性函数的快捷方式。该函数涉及在伽罗瓦域 2**8中取模逆,然后应用仿射变换,该变换已被调整以最大程度地混淆。表达函数的最简单方法是通过以下高次多项式:
$$
f(x) = 05x^{fe} + 09x^{fd} + f9x^{fb} + 25x^{f7} + f4x^{ef} + 01x^{df} + b5x^{bf} + 9fx^{7f} + 63
$$
为了制作S盒,该函数已根据从0x00到0xff的所有输入值以及查找表中的输出进行了计算。

题目:实现sub_bytes函数,通过逆S盒传递状态矩阵,然后将其转换为字节以获得flag。

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from Crypto.Util.number import *

def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
message = ''
for i in matrix:
for j in range(4):
message += str(hex(i[j])[2:])
return long_to_bytes(int(message, 16))

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,
)

state = [
[251, 64, 182, 81],
[146, 168, 33, 80],
[199, 159, 195, 24],
[64, 80, 182, 255],
]

def sub_bytes(s, sbox=s_box):
new_state = [[0 for col in range(4)] for row in range(4)]
for i in range(4):
for j in range(4):
pos = s[i][j]
row = pos >> 4
col = pos - (row << 4)
new_state[i][j] = inv_s_box[row * 16 + col]
return new_state

print(matrix2bytes(sub_bytes(state, sbox=inv_s_box)))
# b'crypto{l1n34rly}'

Diffusion through Permutation

我们已经看到S盒替代是如何造成混淆的。Shannon描述的另一个关键性质是“扩散”。这与输入密码的每个部分如何传播到输出的每个部分有关。
替代本身会产生非线性,但它不会将其分布在整个状态矩阵上。如果没有扩散,同一位置的同一字节在每一轮都会得到相同的转换。这将允许密码分析员分别攻击状态矩阵中的每个字节位置。我们需要通过对状态矩阵进行置乱(以可逆的方式)来交替替换,以便对一个字节应用的替换会影响状态矩阵中的所有其他字节。然后,下一个S盒的每个输入都变成了多个字节的函数,这意味着随着每一轮的进行,系统的代数复杂性都会大大增加。

理想的扩散量会导致明文中一比特位的变化,从而导致密文中一半比特位的统计变化。这一理想的结果被称为雪崩效应

行移位列混淆步骤相结合可以实现这一点。它们协同工作,确保每个字节在两轮内影响状态矩阵中的其他每个字节。

行移位是AES中最简单的转换。它使状态矩阵的第一行保持不变。第二行环绕向左移动一列。第三行移动了两列,第四行移动了三列。维基百科说得很好:“这一步的重要性是避免独立加密列,在这种情况下,AES会退化为四个独立的分组密码。”

image-20241108140107685

该图(和AES规范)显示了以列主符号表示的行移位操作。然而,下面的示例代码使用行主符号表示状态矩阵,因为它在Python中更自然。只要每次访问矩阵时使用相同的符号,最终结果就是相同的。由于访问模式和缓存行为,使用一种符号可以带来更好的性能。

列混淆更复杂。它在Rijndael的伽罗瓦域中在状态矩阵的列和预设矩阵之间执行矩阵乘法。因此,每列的每个字节都会影响结果列的所有字节。实施细节细致入微;这个页面维基百科很好地覆盖了它们。

image-20241108140347816

题目:我们提供了执行列混淆和正向行移位操作的代码。实现inv_shift_rows后,获取状态矩阵,在其上运行inv_mix_columnsinv_shift_rows,然后转换为字节,您将得到您的flag。

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from Crypto.Util.number import *

def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
message = ''
for i in matrix:
for j in range(4):
message += str(hex(i[j])[2:])
return long_to_bytes(int(message, 16))

def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][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[3][3], s[0][3], s[1][3], s[2][3]

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

# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
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)


state = [
[108, 106, 71, 86],
[96, 62, 38, 72],
[42, 184, 92, 209],
[94, 79, 8, 54],
]
inv_mix_columns(state)
inv_shift_rows(state)
print(matrix2bytes(state))
# b'crypto{d1ffUs3R}'

Bringing It All Together

除了KeyExpansion阶段,我们还概述了AES的所有部分。我们已经展示了SubBytes如何提供混淆,ShiftRowsMixColumns如何提供扩散,以及这两个属性如何协同工作,在状态矩阵上反复循环非线性转换。最后,AddRoundKey将密钥种子放入这个替换置换网络中,使密码成为密钥置换。

解密涉及反向执行“AES结构”挑战中描述的步骤,应用反向操作。请注意,仍然需要先运行KeyExpansion,并且将以相反的顺序使用轮密钥。由于XOR具有自逆特性,所以AddRoundKey及其逆是相同的。

题目:我们提供了KeyExpansion的代码,以及由AES-128正确加密的密文。复制到目前为止您编写的所有代码块,并完成实现图中所示步骤的decrypt函数。解密后的明文就是flag。

image-20241115175249455
这些练习中使用的代码取自Bo Zhu的超简单Python AES实现,因此我们在这里复制了许可证。

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
from Crypto.Util.number import *


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. """
message = ''
for i in matrix:
for j in range(4):
message += str(hex(i[j])[2:])
return long_to_bytes(int(message, 16))

def add_round_key(s, k):
new_state = [[0 for col in range(4)] for row in range(4)]
for i in range(4):
for j in range(4):
new_state[i][j] = s[i][j] ^ k[i][j]
return new_state

def sub_bytes(s, sbox=s_box):
new_state = [[0 for col in range(4)] for row in range(4)]
for i in range(4):
for j in range(4):
pos = s[i][j]
row = pos >> 4
col = pos - (row << 4)
new_state[i][j] = inv_s_box[row * 16 + col]
return new_state

def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][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[3][3], s[0][3], s[1][3], s[2][3]

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

# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
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 keywords 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
cipher = bytes2matrix(ciphertext)
# Initial add round key step
# 传入当前cipher状态和当前轮密钥
# 因为N_ROUNDS = 10 倒序 取keys[10]
cipher = add_round_key(cipher, round_keys[10])

for i in range(N_ROUNDS - 1, 0, -1):
# 每一轮的解密 除了最后一轮
inv_shift_rows(cipher)
cipher = sub_bytes(cipher)
cipher = add_round_key(cipher, round_keys[i])
inv_mix_columns(cipher)

# Run final round (skips the InvMixColumns step)
inv_shift_rows(cipher)
cipher = sub_bytes(cipher)
cipher = add_round_key(cipher, round_keys[0])
# Convert state matrix to plaintext
plaintext = matrix2bytes(cipher)
return plaintext

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'

print(decrypt(key, ciphertext))
# b'crypto{MYAES128}'

SYMMETRIC STARTER

image-20241116153012048

Modes of Operation Starter

hint

前面的一组挑战展示了AES如何对单个数据块执行密钥置换。在实际中,我们需要加密比单个块长得多的消息。操作模式描述了如何在较长的消息上使用类似AES的密码。

如果使用不当,所有模式都有严重的弱点。这个挑战将带您进入网站的不同部分,在那里您可以与API交互并利用这些弱点。熟悉界面,并用它来拿下你的下一个flag!

SOURCE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from Crypto.Cipher import AES

KEY = ?
FLAG = ?

@chal.route('/block_cipher_starter/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('/block_cipher_starter/encrypt_flag/')
def encrypt_flag():
cipher = AES.new(KEY, AES.MODE_ECB)
encrypted = cipher.encrypt(FLAG.encode())

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

由starter页点击ENCRYPT_FLAG()的SUBMIT按钮,在OUTPUT中得到json数据:

1
{"ciphertext":"0bf3da3fce245ce6bd6c9a9abaaef49945bbf20ec314ca38b341707c36bd00b9"}

image-20241116141049287

将OUTPUT中的ciphertext扔到DECRYPT(CIPHERTEXT)中,点击SUBMIT按钮,在json中得到解密后的hex值:

1
{"plaintext":"63727970746f7b626c30636b5f633170683372355f3472335f663435375f217d"}

image-20241116141713943

再放入HEX ENCODER/DECODER中,HEX转为TEXT得到flag。

image-20241116141804516

Passwords as Keys

hint

对称密钥算法中的密钥必须是随机字节,而不是密码或其他可预测的数据。随机字节应使用加密安全的伪随机数生成器(CSPRNG)生成。如果密钥在任何方面都是可预测的,那么密码的安全级别就会降低,并且访问密文的攻击者可能会对其进行解密。

仅仅因为密钥看起来像是由随机字节组成的,并不意味着它一定是。在这种情况下,密钥是使用哈希函数从简单密码中导出的,这使得密文是可破解的。

对于此挑战,您可以将HTTP请求编写到端点,或者离线攻击密文。祝你好运!

SOURCE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from Crypto.Cipher import AES
import hashlib
import random

# /usr/share/dict/words from
# https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words
with open("/usr/share/dict/words") as f:
words = [w.strip() for w in f.readlines()]
keyword = random.choice(words)

KEY = hashlib.md5(keyword.encode()).digest()
FLAG = ?

@chal.route('/passwords_as_keys/decrypt/<ciphertext>/<password_hash>/')
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 {"plaintext": decrypted.hex()}

@chal.route('/passwords_as_keys/encrypt_flag/')
def encrypt_flag():
cipher = AES.new(KEY, AES.MODE_ECB)
encrypted = cipher.encrypt(FLAG.encode())

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

同上题一样,通过ENCRYPT_FLAG()获得ciphertext的json数据:

1
{"ciphertext":"c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66"}

通过SOURCE提示得到密码本,本地调用网站函数来解密。

EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from Crypto.Cipher import AES
import hashlib


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

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

# 相对路径读文件
with open("pwd.txt") as f:
words = [w.strip() for w in f.readlines()]
c = "c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66"
for i in words:
# 对密码本的每个数据进行爆破
KEY = hashlib.md5(i.encode()).digest()
m = decrypt(c, KEY)
m = m["plaintext"]
m = bytes.fromhex(m)
if b"crypto{" in m:
print(m)
break
# b'crypto{k3y5__r__n07__p455w0rdz?}'

BLOCK CIPHERS 1

ECB CBC WTF

hint

在这里,您可以在CBC中加密,但只能在ECB中解密。这不应该是一个弱点,因为它们是不同的模式…对吗?

image-20241116155954010

SOURCE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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)

cipher = AES.new(KEY, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(FLAG.encode())
ciphertext = iv.hex() + encrypted.hex()

return {"ciphertext": ciphertext}
WriteUp

同前几题,得到密文:

1
{"ciphertext":"f14003406962ed9760ac5af1a8423677f05c7b58f9f99d8301ab42a138018c419d0af6c6c0a6a526db3eb816e943b1c7"}
EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from Crypto.Util.number import *
import requests

c = "f94d8df428e4641b2f251edee28f483b743f743a243cbafaaa1f3aed475466b0eab960009bf41f6c66d5880e6708b2de"
c = bytes.fromhex(c)
# iv
c1 = hex(bytes_to_long(c[0:16]))[2:]
c2 = hex(bytes_to_long(c[16:32]))[2:]
c3 = hex(bytes_to_long(c[32:48]))[2:]
a1 = requests.get(f'http://aes.cryptohack.org/ecbcbcwtf/decrypt/{c2}')
a2 = requests.get(f'http://aes.cryptohack.org/ecbcbcwtf/decrypt/{c3}')
m1 = a1.json()["plaintext"]
m2 = a2.json()["plaintext"]
m1 = int(m1, 16) ^ int(c1, 16)
m2 = int(m2, 16) ^ int(c2, 16)
print(long_to_bytes(m1) + long_to_bytes(m2))
# b'crypto{3cb_5uck5_4v01d_17_!!!!!}'

ECB ORACLE

hint

ECB是最简单的模式,每个明文块都完全独立加密。在这种情况下,您的输入将被添加到秘密flag之前并加密,仅此而已。我们甚至不提供解密功能。也许当你有一个“ECB的预言者”时,你就不需要填充预言了?

SOURCE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
cipher = AES.new(KEY, AES.MODE_ECB)
try:
encrypted = cipher.encrypt(padded)
except ValueError as e:
return {"error": str(e)}

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

本题为一道典型的ECB加密解密问题,首先,我们既不知道明文也不知道密文,由题目所给的加密算法,先尝试得到加密块大小和flag长度(?是要求的明文,F是ECB模式填充的字节,p是已求出的明文)。

image-20241116201256839

先用一个字符”a”去encrypt返回32个字节的数据(64位16进制),再依次用”aa”,”aaa”,…去encrypt,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests

for i in range(1, 31):
# 0x61 a
m = i*'61'
result = requests.get(f'http://aes.cryptohack.org/ecb_oracle/encrypt/{m}')
try:
a = result.json()["ciphertext"]
print(i, len(a)//2)
except Exception as e:
print(e)
# output:
# 1 32
# 2 32
# ...
# 6 32
# 7 48
# 8 48
# ...
# 22 48
# 23 64
# 24 64
# ...
# 30 64

可见6个”a”的时候返回32个字节的数据,7个”a”的时候返回48个字节的数据,22个”a”的时候返回64个字节的数据,23个”a”的时候返回48个字节的数据。所以块大小就是16字节(22-6),明文(flag)大小就是26个字节(32-6)。接着暴力破解求明文。

aaaaaaaaaaaaaaa? ??????????????? ?????????FFFFFFF

aaaaaaaaaaaaaa?? ??????????????? ????????FFFFFFFF

aaaaaaaaaaaaa??? ??????????????? ???????FFFFFFFFF

aaaaaaaaaaaa???? ??????????????? ?????FFFFFFFFFFF

求第一位:

15个”a”时返回的前16个字节只有最后一个字节未知,可以暴力遍历出第一个字节,也就是用15*”a”+flag第一个字节的加密密文与暴力遍历的加密密文去匹配。2-15位同理。

16-26位:

与上述方法同理。

aaaaaaaaaaaaaaaa ppppppppppppppp? ??????????FFFFFF

aaaaaaaaaaaaaaap ppppppppppppppp? ?????????FFFFFFF

aaaaaaaaaaaaaapp ppppppppppppppp? ????????FFFFFFFF

aaaaaaaaaaaaappp ppppppppppppppp? ???????FFFFFFFFF

aaaaaaaaaaaapppp ppppppppppppppp? ??????FFFFFFFFFF

EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import requests

m = ''
for i in range(15):
t = '61' * (15 - i)
test_result = requests.get(f'http://aes.cryptohack.org/ecb_oracle/encrypt/{t}')
tr = test_result.json()["ciphertext"][:32]
for j in range(32, 127):
t = '61' * (15 - i) + m
t += str(hex(j)[2:])
brute_result = requests.get(f'http://aes.cryptohack.org/ecb_oracle/encrypt/{t}')
print(brute_result)
br = brute_result.json()["ciphertext"][:32]
try:
if tr == br:
m += str(hex(j)[2:])
break
except:
pass
# bytes.fromhex(m) = 'crypto{p3n6u1n5'
m = '63727970746f7b70336e3675316e35'
for i in range(11):
t = '61' * (16 - i)
test_result = requests.get(f'http://aes.cryptohack.org/ecb_oracle/encrypt/{t}')
tr = test_result.json()["ciphertext"][32:64]
for j in range(32, 127):
t = '61' * (16 - i) + m
t += str(hex(j)[2:])
brute_result = requests.get(f'http://aes.cryptohack.org/ecb_oracle/encrypt/{t}')
br = brute_result.json()["ciphertext"][32:64]
try:
if tr == br:
m += str(hex(j)[2:])
break
except:
pass
print(bytes.fromhex(m))
# b'crypto{p3n6u1n5_h473_3cb}'
hint

你可以在我的网站上得到一个cookie,但它不会帮助你获得flag…我想。

image-20241117101904528

SOURCE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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)
iv = bytes.fromhex(iv)

try:
cipher = AES.new(KEY, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(cookie)
unpadded = unpad(decrypted, 16)
except ValueError as e:
return {"error": str(e)}

if b"admin=True" in unpadded.split(b";"):
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 = f"admin=False;expiry={expires_at}".encode()

iv = os.urandom(16)
padded = pad(cookie, 16)
cipher = AES.new(KEY, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(padded)
ciphertext = iv.hex() + encrypted.hex()

return {"cookie": ciphertext}
WriteUp

这是一道典型的CBC字节反转攻击。

image-20241117102006026

题目给出了两个函数check_admin(cookie, iv)get_cookie()

第一个函数将输入的cookieIV进行AES-BCB模式解密,当解密得到的字符串满足以;为分割后存在admin=True的字符串的时候就会把flag输出出来。

1
2
if b"admin=True" in unpadded.split(b";"):
return {"flag": FLAG}

第二个函数会将 cookie这个字符串和一个IV经过AES-BCB模式加密后的结果同加密所用到的IV输出出来。

1
2
3
4
5
expires_at = (datetime.today() + timedelta(days=1)).strftime("%s")
cookie = f"admin=False;expiry={expires_at}".encode()

ciphertext = iv.hex() + encrypted.hex()
return {"cookie": ciphertext}

我们可以通过构造新的IV同原来的密文进行解密从而更改解密后的内容最终拿到flag。
$$
IV⊕Old=T\T⊕IV=Old\T⊕(IV⊕New⊕Old)=Old⊕Old⊕New=New\IV_new=IV⊕New⊕Old\
$$
由上面的推导式
$$
IV_new=IV⊕New⊕Old
$$
所以可得EXP。

EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
from Crypto.Util.number import *

result = requests.get('http://aes.cryptohack.org/flipping_cookie/get_cookie')
cookie = result.json()["cookie"]
cookie_bytes = bytes.fromhex(cookie)
iv = cookie_bytes[:16]
iv_hex = hex(bytes_to_long(iv))
c = cookie_bytes[16:]
c = hex(bytes_to_long(c))[2:]

old = b'admin=False;expi'
new = b'admin=True;00000'
m1 = hex(bytes_to_long(old))
m2 = hex(bytes_to_long(new))
iv_new = int(m1, 16) ^ int(m2, 16) ^ int(iv_hex, 16)
iv_new = hex(iv_new)[2:]
result2 = requests.get(f'http://aes.cryptohack.org/flipping_cookie/check_admin/{c}/{iv_new}')
flag = result2.json()["flag"]
print(flag)
# crypto{4u7h3n71c4710n_15_3553n714l}

本题需要修改的内容刚好在第一组里,如果修改内容在其他组,如第二组,则需要构造新的第一组的密文,构造规则与推导式相同:
$$
新的密文=前一组的密文⊕原来的明文⊕新的明文
$$
只不过可能会需要同时构造新的IV,使新的密文同新的IV解密结果同以前相同。

Lazy CBC

hint

我只是一个懒惰的开发人员,希望我的CBC加密能够正常工作。这些关于初始化向量的讨论是什么?听起来并不重要。

SOURCE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from Crypto.Cipher import AES

KEY = ?
FLAG = ?

@chal.route('/lazy_cbc/encrypt/<plaintext>/')
def encrypt(plaintext):
plaintext = bytes.fromhex(plaintext)
if len(plaintext) % 16 != 0:
return {"error": "Data length must be multiple of 16"}

cipher = AES.new(KEY, AES.MODE_CBC, KEY)
encrypted = cipher.encrypt(plaintext)

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

@chal.route('/lazy_cbc/get_flag/<key>/')
def get_flag(key):
key = bytes.fromhex(key)

if key == KEY:
return {"plaintext": FLAG.encode().hex()}
else:
return {"error": "invalid key"}

@chal.route('/lazy_cbc/receive/<ciphertext>/')
def receive(ciphertext):
ciphertext = bytes.fromhex(ciphertext)
if len(ciphertext) % 16 != 0:
return {"error": "Data length must be multiple of 16"}

cipher = AES.new(KEY, AES.MODE_CBC, KEY)
decrypted = cipher.decrypt(ciphertext)

try:
decrypted.decode() # ensure plaintext is valid ascii
except UnicodeDecodeError:
return {"error": "Invalid plaintext: " + decrypted.hex()}

return {"success": "Your message has been received"}
WriteUp

CBC的生成思路如下:
$$
m_1=decrypt(c_1)\oplus{IV}\m_2=decrypt(c_2)\oplus{m_1}\m_3=decrypt(c_3)\oplus{m_2}
$$
本题给出了密文的生成代码,密文的解密代码以及flag的获得代码,且密文明文都可以由自己设置,我们不妨设如下特殊情况
$$
c_2=0,c_1=c_3\m_1\oplus{m_3}=decrypt(c_1)\oplus{IV}\oplus{decrypt(c_1)}\oplus{m_2}=IV
$$
即可得到IV,在本题中,IV即为所求KEY

EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

# t1 = '61' * 48
# result1 = requests.get(f'http://aes.cryptohack.org/lazy_cbc/encrypt/{t1}')
# c = result1.json()["ciphertext"]
# t2 = c[:32] + '0' * 32 + c[:32]
# result2 = requests.get(f'http://aes.cryptohack.org/lazy_cbc/receive/{t2}')
# m1 = result2.json()
# print(m1)
m1 = '61616161616161616161616161616161a381f13463f0698d5b8c9bc4af21c08e8a67784df1eb484abc01811d2bfbacd7'
key = hex(int(m1[:32], 16) ^ int(m1[64:], 16))[2:]
result3 = requests.get(f'http://aes.cryptohack.org/lazy_cbc/get_flag/{key}')
m = result3.json()["plaintext"]
print(bytes.fromhex(m))
# b'crypto{50m3_p30pl3_d0n7_7h1nk_IV_15_1mp0r74n7_?}'

Triple DES

hint

数据加密标准(DES)是AES的前身,目前仍广泛应用于支付卡行业等一些发展缓慢的领域。这一挑战展示了DES的一个奇怪弱点,而安全分组密码不应该有这个弱点。

SOURCE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from Crypto.Cipher import DES3
from Crypto.Util.Padding import pad

IV = os.urandom(8)
FLAG = ?

def xor(a, b):
# xor 2 bytestrings, repeating the 2nd one if necessary
return bytes(x ^ y for x,y in zip(a, b * (1 + len(a) // len(b))))

@chal.route('/triple_des/encrypt/<key>/<plaintext>/')
def encrypt(key, plaintext):
try:
key = bytes.fromhex(key)
plaintext = bytes.fromhex(plaintext)
plaintext = xor(plaintext, IV)

cipher = DES3.new(key, DES3.MODE_ECB)
ciphertext = cipher.encrypt(plaintext)
ciphertext = xor(ciphertext, IV)

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

except ValueError as e:
return {"error": str(e)}

@chal.route('/triple_des/encrypt_flag/<key>/')
def encrypt_flag(key):
return encrypt(key, pad(FLAG.encode(), 8).hex())
WriteUp

DES Weak key

DES有几个特定的弱密钥和半弱密钥,会使DES的加密模式和解密模式完全相同,本题给出了encrypt_flag(key)encrypt(key, plaintext)两个函数,key使用弱密钥,得到flag的密文,然后用key和flag的密文再一次加密,由于使用了弱密钥,加密与解密一致,对flag的密文用同样的key再加密一次即为解密,得到明文。

EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

key1 = b'\xfe' * 8
key2 = b'\x01' * 8
key3 = b'\xe0' * 4 + b'\xf1' * 4
key4 = b'\x1f' * 4 + b'\x0e' * 4

key = key3 + key4
result1 = requests.get(f'http://aes.cryptohack.org/triple_des/encrypt_flag/{key.hex()}')
c = result1.json()["ciphertext"]
result2 = requests.get(f'http://aes.cryptohack.org/triple_des/encrypt/{key.hex()}/{c}')
m = result2.json()["ciphertext"]
flag = bytes.fromhex(m)
print(flag)
# b'crypto{n0t_4ll_k3ys_4r3_g00d_k3ys}\x06\x06\x06\x06\x06\x06'