Symmetric Ciphers

How AES Works

Keyed Permutations

AES(高级加密标准)像所有优秀的块 cipher(分组密码)一样,执行的是“密钥置换”。这意味着它将每个可能的输入块映射到一个唯一的输出块,而具体使用哪个置换则由密钥决定。

  • “块”只是指固定数量的位或字节,这可能代表任何类型的数据。 AES处理一个块并输出另一个块。我们将专门讨论AES的变体,该变体可在128位(16个字节)块和128位密钥(称为AES-128)上工作。

使用相同的密钥,可以进行反向排列,将输出块映射回原始输入块。输入块和输出块之间必须保持一对一的对应关系,否则我们无法依赖密文解密回我们开始的相同明文。

一对一对应的数学术语是bijection(双射)。

crypto{bijection}

Resisting Bruteforce

​ 如果分组密码是安全的,攻击者就无法区分 AES 的输出和随机的比特排列。此外,除了简单地暴力破解每个可能的密钥之外,应该没有更好的方法来撤销排列。 ​ 这就是为什么学者们认为,如果他们能够找到一种比暴力破解密钥所需的步骤更少的攻击,那么理论上密码就被“破解”了,即使这种攻击实际上是不可行的。

  • 破解 128 位密钥空间有多难?有人估计,如果你将整个比特币挖矿网络的算力用于破解 AES-128 密钥,需要超过宇宙年龄一百倍的时间。

​ 事实证明,有一种针对 AES 的攻击比暴力破解更好,但也只是稍微好一点——它将 AES-128 的安全级别降低到 126.1 位,并且已经超过 8 年没有得到改进。 ​ 鉴于 128 位提供的巨大“安全裕度”,以及尽管进行了大量研究但仍缺乏改进,因此它不被认为对 AES 安全构成可信风险。但是,是的,从非常狭隘的意义上来说, ​ 它“破坏”了 AES。最后,虽然量子计算机有潜力通过Shor算法。 ​ 完全破解 RSA 等流行的公钥密码系统,但人们认为它们只能将通过Grover 算法的对称密码系统的安全级别降低一半。这就是人们推荐使用 AES-256 的原因之一, ​ 尽管它的性能较差,因为它在量子未来仍然可以提供非常足够的 128 位安全性。

针对 AES 的最佳单密钥攻击的名称是biclique

crypto{biclique}

Structure of AES

​ 为了实现一个没有密钥就无法逆推的密钥排列,AES 在输入上应用了大量的临时混合操作。这与基于优雅的单独数学问题的公钥加密系统如 RSA 形成鲜明对比。AES 远没有那么优雅,但它非常快。

​ 在高级别上,AES-128 以“密钥扩展”开始,然后在起始状态上运行 10 轮。起始状态就是我们想要加密的明文块,表示为一个 4x4 字节的矩阵。在 10 轮的过程中,状态矩阵通过一系列可逆变换反复修改。

  • 每个转换步骤都有基于 1940 年代由克劳德·香农确立的安全密码理论性质的明确目的。我们将在接下来的挑战中更详细地探讨这些内容。

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

  1. KeyExpansion or Key Schedule 密钥扩展或密钥调度

​ 从 128 位密钥中,派生出 11 个独立的 128 位“轮密钥”:每个 AddRoundKey 步骤使用一个。

  1. Initial key addition 初始密钥添加

AddRoundKey 轮密钥加

​ 矩阵中的每一个字节都与初始轮密钥做XOR运算

  1. Round 循环加密

此阶段循环 10 次,包括 9 个主轮次和 1 个“最终轮次”

  1. SubBytes 字节替代

    状态的每个字节根据查找表(“S-box”)替换为不同的字节。

  2. ShiftRows 行移位

    将矩阵中的每个横列进行循环式移位

  3. MixColumns 列混淆

    了充分混合矩阵中各个直行的操作。这个步骤使用线性转换来混合每列的四个字节。

  4. AddRoundKey 轮密钥加

    矩阵中的每一个字节都与当前轮密钥做XOR运算

​ 包括一个 bytes2matrix 函数,用于将我们的初始明文块转换为状态矩阵。编写一个 matrix2bytes 函数,将该矩阵转换回字节,并将结果明文作为标志提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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. """
return b''.join(bytes(row) for row in matrix)

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

print(matrix2bytes(matrix))

crypto{inmatrix}

Round Keys

​ 我们将暂时跳过密钥扩展阶段的细节。主要点是它接收我们的 16 字节密钥并生成 11 个 4x4 的矩阵,称为“轮密钥”,这些轮密钥是从我们的初始密钥派生出来的。这些轮密钥使得 AES 能够从我们提供的单个密钥中获得额外的性能。

​ 初始密钥添加阶段,接下来只有一个添加轮密钥步骤。添加轮密钥步骤很简单:它将当前状态与当前轮密钥进行异或运算。

​ AddRoundKey 也出现在每一轮的最后一步。AddRoundKey 是使 AES 成为“密钥置换”而不是仅仅置换的原因。它是 AES 中唯一将密钥混合到状态的部分,但对于确定发生的置换至关重要。

​ 如您在之前的挑战中看到的,如果知道密钥,XOR 是一个容易逆操作的操作,但如果不知道密钥,就很难撤销。现在想象一下尝试恢复被 11 个不同的密钥 XOR 过的明文,并且在每个 XOR 操作之间通过一系列替换和转置密码进行了大量打乱。这正是 AES 所做的!我们将在接下来的几个挑战中看到这种打乱有多有效。

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

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
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 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. """
return b''.join(bytes(row) for row in matrix)

def add_round_key(s, k):
return [[s[i][j] ^ k[i][j] for j in range(4)] for i in range(4)]

print(matrix2bytes(add_round_key(state, round_key)))

crypto{r0undk3y}

Confusion through Substitution

​ 每轮 AES 的第一步是 SubBytes。这涉及到将状态矩阵中的每个字节替换为预设的 16x16 查找表中的不同字节。这个查找表被称为“替换盒”或简称“S-box”,一开始可能会让人感到困惑。让我们来分解一下。

​ 1945 年,美国数学家克劳德·香农发表了一篇关于信息论的突破性论文。它将“混淆”确定为安全密码的一个基本属性。“混淆”意味着密文与密钥之间的关系应该尽可能复杂。仅凭密文,不应有任何方法可以了解密钥。

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

​ S-box 的主要目的是以线性函数难以逼近的方式变换输入。S-box 追求高非线性,尽管 AES 中的 S-box 并不完美,但相当接近。S-box 中的快速查找是执行非常非线性函数的捷径,该函数涉及在伽罗瓦域 2**8 中取模逆,然后应用经过调整以实现最大混乱的仿射变换。表达该函数的最简单方式是通过以下高次多项式:

diagram showing S-Box equation

​ 为了制作 S 盒,该函数已对所有从 0x00 到 0xff 的输入值进行了计算,并将输出放入查找表中。

​ 实现 sub_bytes ,将状态矩阵通过逆 S 盒发送,然后转换为字节以获取标志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
s_box = (...)

inv_s_box = (...)

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

def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
return b''.join(bytes(row) for row in matrix)

def sub_bytes(s, sbox=s_box):
return [[sbox[s[i][j]] for j in range(4)] for i in range(4)]

print(matrix2bytes(sub_bytes(state, sbox=inv_s_box)))

crypto{l1n34rly}

Diffusion through Permutation

​ 我们已经看到 S-box 替换如何提供混淆。香农描述的另一个关键属性是“扩散”。这关系到加密输入的每一部分应该如何扩散到输出的每一部分。

​ 替换本身会产生非线性,但它不会在整个状态上分布。没有扩散,相同位置的字节在每一轮都会应用相同的变换。这将允许密码分析师分别攻击状态矩阵中的每个字节位置。我们需要通过打乱状态(以可逆的方式)交替替换,以便应用于一个字节的替换会影响状态中的所有其他字节。然后每个输入到下一个 S 盒都成为多个字节的函数,这意味着随着每一轮的进行,系统的代数复杂性会极大地增加。

  • 一个理想的扩散量会导致明文中的一个比特变化导致密文统计上有一半的比特发生变化。这种理想的结果被称为雪崩效应。

位移行和混合列步骤结合实现这一点。它们共同工作以确保每个字节在仅两轮内影响状态中的每个其他字节。

ShiftRows 是 AES 中最简单的变换。它保持状态矩阵的第一行不变。第二行向左移动一列,循环。第三行移动两列,第四行移动三列。维基百科表述得很好:“这一步的重要性在于避免列被独立加密,否则 AES 会退化成四个独立的块加密。”

diagram showing ShiftRows
  • 图表(以及 AES 规范)显示了 ShiftRows 操作以列主序表示。然而,下面的示例代码使用行主序表示状态矩阵,因为在 Python 中这更为自然。只要每次访问矩阵时都使用相同的表示法,最终结果就相同。由于访问模式和缓存行为,使用一种类型的表示法可能会导致更好的性能。

MixColumns 较为复杂。它在 Rijndael 的伽罗瓦域中对状态矩阵的列和预设矩阵进行矩阵乘法。因此,每一列的每个单字节都影响结果列的所有字节。实现细节较为复杂;本页和维基百科都很好地涵盖了它们。

diagram showing MixColumns

我们提供了执行 MixColumns 和前向 ShiftRows 操作的代码。在实现 inv_shift_rows 后,取状态,对其运行 inv_mix_columns ,然后运行 inv_shift_rows ,转换为字节,你将得到你的标志。

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
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 matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
return b''.join(bytes(row) for row in matrix)

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

crypto{d1ffUs3R}

Bringing It All Together

​ 除了密钥扩展阶段,我们已经绘制了 AES 的所有组件。我们展示了 SubBytes 如何提供混淆,ShiftRows 和 MixColumns 如何提供扩散,以及这两个属性如何共同工作,在状态上反复循环非线性变换。最后,AddRoundKey 将密钥种入这个替换-置换网络,使加密成为带密钥的置换。

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

​ 我们已提供关键扩展代码,以及经过 AES-128 正确加密的密文。复制您迄今为止编写的所有构建块,并完成实现图中步骤的 decrypt 函数。解密后的明文是标志。

​ 是的,你可以在这项挑战中作弊,但那样有什么乐趣呢?

这些练习中使用的代码来自 Bo Zhu 的超级简单的 Python AES 实现,因此我们在此处重现了许可证。

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
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. """
return b''.join(bytes(row) for row in matrix)

def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]

def sub_bytes(s, sbox=s_box):
for i in range(4):
for j in range(4):
s[i][j] = sbox[s[i][j]]

def inv_sub_bytes(s,inv_sbox=inv_s_box):
for i in range(4):
for j in range(4):
s[i][j] = inv_sbox[s[i][j]]

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 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
s=bytes2matrix(ciphertext)

# Initial add round key step
add_round_key(s, round_keys[N_ROUNDS])

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

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


print(decrypt(key, ciphertext))

crypto{MYAES128}

Symmetric Cryptography

Modes of Operation Starter

之前的挑战集展示了 AES 如何在数据块上执行密钥置换。在实践中,我们需要加密比单个数据块更长的消息。操作模式描述了如何在更长的消息上使用像 AES 这样的加密算法。

所有模式在使用不当时都存在严重缺陷。此类挑战将您带到网站的另一部分,您可以在此处与 API 交互并利用这些缺陷。熟悉界面并使用它来获取您的下一个旗帜!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from Crypto.Cipher import AES
import requests

url='https://aes.cryptohack.org/block_cipher_starter/'

response = requests.get(url+'encrypt_flag/')
enc=response.json()['ciphertext']
print(enc)
response.close()

response = requests.get(url+'decrypt/'+enc)
dec=response.json()['plaintext']
response.close()

print(bytes.fromhex(dec))

crypto{bl0ck_c1ph3r5_4r3_f457_!}

Passwords as Keys

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

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

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

拿到字典爆破即可,请求太慢了,自己写一下aes解密即可

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
from Crypto.Cipher import AES
import requests
import hashlib

url='https://aes.cryptohack.org/passwords_as_keys/'

response = requests.get(url+'encrypt_flag/')
enc=response.json()['ciphertext']
print(enc)
response.close()

response = requests.get("https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words")
date=response.text.split('\n')
response.close()
print(date)

for i in date:
print(i)
KEY = hashlib.md5(i.encode()).digest()
print(KEY)
# response = requests.get(url+'decrypt/'+enc+'/'+KEY.hex())
# dec=response.json()['plaintext']
# response.close()
aes=AES.new(KEY,AES.MODE_ECB)
dec=aes.decrypt(bytes.fromhex(enc))
if b'cry' in dec:
print(dec)
break

crypto{k3y5__r__n07__p455w0rdz?}

Block Ciphers 1

ECB CBC WTF

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

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
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}
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}
CBC

cbc模式加密,ecb解密,得到dec如下图

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

url='https://aes.cryptohack.org/ecbcbcwtf/'

response = requests.get(url+'encrypt_flag/')
enc=response.json()['ciphertext']
print(enc)
response.close()

response = requests.get(url+'decrypt/'+enc[32:])
dec=response.json()['plaintext']
response.close()

iv,enc1,enc2=enc[:32],enc[32:64],enc[64:]
dec1,dec2=dec[:32],dec[32:64]


print(long_to_bytes(int(iv,16)^int(dec1,16))+long_to_bytes(int(enc1,16)^int(dec2,16)))

crypto{3cb_5uck5_4v01d_17_!!!!!}

ECB Oracle

ECB 是最简单的模式,每个明文块完全独立加密。在这种情况下,您的输入被添加到秘密标志之前并加密,就这么多。我们甚至不提供解密函数。也许当您有“ECB oracle”时,您不需要填充预言机?

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()}

先填充一段看看flag长度,发现填充7位的时候,enc长度增加,可知flag长32-6=24位,然后利用填充逐字节爆破即可

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
from Crypto.Cipher import AES
from Crypto.Util.number import *
import requests

url='https://aes.cryptohack.org/ecb_oracle/'

def encry(taxt):
response = requests.get(url+'encrypt/'+taxt)
enc=response.json()['ciphertext']
print(enc)
response.close()
return enc

# for i in range(15):
# enc=encry('ff'*(i+1))
# if enc:
# print(len(enc))

# lenflag = 26

str='01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-='
flag=b'crypto{'
for i in range(19):
enc1=encry('00'*(24-i))
for j in str:
enc2=encry('00'*(24-i)+hex(bytes_to_long(flag+j.encode()))[2:])

if enc1[32:64] == enc2[32:64]:
flag+=j.encode()
print(flag)
break

crypto{p3n6u1n5_h473_3cb}

您可以为我的网站获取一个 cookie,但这不会帮助您读取标志…我想。

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
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}

CBC字节翻转,控制iv即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Util.number import *
import requests
import os

url='https://aes.cryptohack.org/flipping_cookie/'

res=requests.get(url+'get_cookie')
cookie = res.json()['cookie']
print(cookie)
iv=cookie[0:32]
print(iv)
enc=cookie[32:]
print(bytes.fromhex(enc))

m =b'admin=False;expi'
invm=b'admin=True;expir'

new_iv=bytes_to_long(m)^bytes_to_long(invm)^int(iv,16)

res=requests.get(url+'check_admin/'+enc+'/'+hex(new_iv)[2:])
flag=res.json()['flag']
print(flag)

crypto{4u7h3n71c4710n_15_3553n714l}

Lazy CBC

我只是个懒惰的程序员,想让我的 CBC 加密工作。这些关于初始化向量的讨论是什么意思?听起来并不重要。

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
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"}

题目使用iv作key,根据CBC模式,进行如下加解密 第三项与m1异或得到iv

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 AES
from Crypto.Util.number import *
import requests

url='https://aes.cryptohack.org/lazy_cbc/'

m='a'*96

response = requests.get(url+'encrypt/'+m)
enc=response.json()['ciphertext']
print(enc)
response.close()

enc1,enc2,enc3=enc[:32],enc[32:64],enc[64:]

new_m=enc1+'0'*32+enc1
# response = requests.get(url+'receive/'+new_m)
# dec=response.json()
# response.close()
# print(dec)

dec='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa459bbfb2994bbe190b5483264628607150089baf37e464460c0f99b3854d74c6'

iv=int(dec[64:],16)^int('a'*32,16)

response = requests.get(url+'get_flag/'+hex(iv)[2:])
flag=response.json()['plaintext']
response.close()
print(bytes.fromhex(flag))

crypto{50m3_p30pl3_d0n7_7h1nk_IV_15_1mp0r74n7_?}

Triple DES

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

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

​ des弱密钥,题目使用的三重des使用两个密钥,使用key1加密,key2解密key1再次加密。加密两次即可获得flag

在 DES 的计算中,56bit 的密钥最终会被处理为 16 个轮密钥,每一个轮密钥用于 16 轮计算中的一轮,DES 弱密钥会使这 16 个轮密钥完全一致,所以称为弱密钥。

有四个弱密钥是绝对不能使用的:

1
2
3
4
\x01\x01\x01\x01\x01\x01\x01\x01
\xFE\xFE\xFE\xFE\xFE\xFE\xFE\xFE
\xE0\xE0\xE0\xE0\xF1\xF1\xF1\xF1
\x1F\x1F\x1F\x1F\x0E\x0E\x0E\x0E

如果不考虑校验位的密钥,下面几个也是属于弱密钥的:

1
2
3
4
\x00\x00\x00\x00\x00\x00\x00\x00
\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF
\xE1\xE1\xE1\xE1\xF0\xF0\xF0\xF0
\x1E\x1E\x1E\x1E\x0F\x0F\x0F\x0F
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from Crypto.Cipher import AES
from Crypto.Util.number import *
import requests

url='https://aes.cryptohack.org/triple_des/'

key1=b'\x01\x01\x01\x01\x01\x01\x01\x01'
key2=b'\xFE\xFE\xFE\xFE\xFE\xFE\xFE\xFE'
response = requests.get(url+'encrypt_flag/'+(key1+key2).hex())
enc=response.json()['ciphertext']
print(enc)
response.close()

response = requests.get(url+'encrypt/'+(key1+key2).hex()+'/'+enc)
flag=response.json()['ciphertext']
response.close()
print(bytes.fromhex(flag))

crypto{n0t_4ll_k3ys_4r3_g00d_k3ys}

Stream Ciphers

Symmetry

某些分组密码模式,如 OFB、CTR 或 CFB,将分组密码转换为流密码。流密码背后的思想是生成一个伪随机密钥流,然后将其与明文进行异或运算。流密码的一个优点是它们可以处理任意长度的明文,无需填充。

OFB 是一种不为人知的密码模式,在当今使用 CTR 模式时没有实际优势。这个挑战引入了 OFB 的一个不寻常特性。

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
from Crypto.Cipher import AES

KEY = ?
FLAG = ?

@chal.route('/symmetry/encrypt/<plaintext>/<iv>/')
def encrypt(plaintext, iv):
plaintext = bytes.fromhex(plaintext)
iv = bytes.fromhex(iv)
if len(iv) != 16:
return {"error": "IV length must be 16"}

cipher = AES.new(KEY, AES.MODE_OFB, iv)
encrypted = cipher.encrypt(plaintext)
ciphertext = encrypted.hex()

return {"ciphertext": ciphertext}

@chal.route('/symmetry/encrypt_flag/')
def encrypt_flag():
iv = os.urandom(16)

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

return {"ciphertext": ciphertext}
img

根据加密流程可以看出,相同的iv和key会生成相同的异或密钥,已知iv加密两次即可得到密文

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

url='https://aes.cryptohack.org/symmetry/'

response = requests.get(url+'encrypt_flag/')
enc=response.json()['ciphertext']
print(enc)
response.close()

iv=enc[0:32]
enc=enc[32:]
print(iv,enc)

response = requests.get(url+'encrypt/'+enc+'/'+iv)
flag=response.json()['ciphertext']
print(flag)
response.close()

print(bytes.fromhex(flag))

crypto{0fb_15_5ymm37r1c4l_!!!11!}

Bean Counter

我努力让 PyCrypto 的计数模式做我想做的事情,所以我将 ECB 模式自己转换成了 CTR。我的计数器可以向上和向下移动,以迷惑密码分析师!他们根本不可能阅读我的图片。

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

KEY = ?

class StepUpCounter(object):
def __init__(self, step_up=False):
self.value = os.urandom(16).hex()
self.step = 1
self.stup = step_up

def increment(self):
if self.stup:
self.newIV = hex(int(self.value, 16) + self.step)
else:
self.newIV = hex(int(self.value, 16) - self.stup)
self.value = self.newIV[2:len(self.newIV)]
return bytes.fromhex(self.value.zfill(32))

def __repr__(self):
self.increment()
return self.value

@chal.route('/bean_counter/encrypt/')
def encrypt():
cipher = AES.new(KEY, AES.MODE_ECB)
ctr = StepUpCounter()

out = []
with open("challenge_files/bean_flag.png", 'rb') as f:
block = f.read(16)
while block:
keystream = cipher.encrypt(ctr.increment())
xored = [a^b for a, b in zip(block, keystream)]
out.append(bytes(xored).hex())
block = f.read(16)

return {"encrypted": ''.join(out)}
img

仔细看会发现StepUpCounter的实现有问题,会导致if判断一直进else加false导致value不变,png头是已知的,解出密钥异或全部密文即可

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

url='https://aes.cryptohack.org/bean_counter/'

response = requests.get(url+'encrypt/')
enc=response.json()['encrypted']
print(enc)
response.close()

pnghead='89504E470D0A1A0A0000000D49484452'

key=int(pnghead,16)^int(enc[:32],16)
print(key)

with open('flag.png','wb') as f:
for i in range(0,len(enc),32):
flag=hex(int(enc[i:i+32],16)^key)[2:].zfill(32)
print(flag)
f.write(bytes.fromhex(flag))

crypto{hex_bytes_beans}

CTRIME

我们明文可能存在大量冗余,为什么不先压缩一下呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from Crypto.Cipher import AES
from Crypto.Util import Counter
import zlib

KEY = ?
FLAG = ?

@chal.route('/ctrime/encrypt/<plaintext>/')
def encrypt(plaintext):
plaintext = bytes.fromhex(plaintext)

iv = int.from_bytes(os.urandom(16), 'big')
cipher = AES.new(KEY, AES.MODE_CTR, counter=Counter.new(128, initial_value=iv))
encrypted = cipher.encrypt(zlib.compress(plaintext + FLAG.encode()))

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

CRIME(英语:Compression Ratio Info-leak Made Easy,意思为:压缩率使信息容易泄露)CVE-2012-4929

zlib会将两段相邻相同的字节压缩,所以可以根据密文长度逐字节进行爆破,只填充一次会爆破不出E,多填几次即可

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
from Crypto.Util.number import *
import requests

url='https://aes.cryptohack.org/ctrime/'

def en(plaintext):
response = requests.get(url+'encrypt/'+plaintext)
enc=response.json()['ciphertext']
print(enc)
return enc

str='_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!'
flag='crypto{'

nowStr=''
while nowStr!='}':
enclen=len(en((((flag+'}')*2).encode()).hex()))
for i in str:
if len(en((((flag+i)*2).encode()).hex()))<enclen:
nowStr=i
break
flag+=nowStr
print(flag)

print(flag)

crypto{CRIME_571ll_p4y5}

Logon Zero

Logon Zero在使用网络之前,您必须使用我们陈旧的 CFB-8 登录协议通过 Active Directory 进行身份验证。

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
#!/usr/bin/env python3

from Crypto.Cipher import AES
from Crypto.Util.number import bytes_to_long
from os import urandom
from utils import listener

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


class CFB8:
def __init__(self, key):
self.key = key

def encrypt(self, plaintext):
IV = urandom(16)
cipher = AES.new(self.key, AES.MODE_ECB)
ct = b''
state = IV
for i in range(len(plaintext)):
b = cipher.encrypt(state)[0]
c = b ^ plaintext[i]
ct += bytes([c])
state = state[1:] + bytes([c])
return IV + ct

def decrypt(self, ciphertext):
IV = ciphertext[:16]
ct = ciphertext[16:]
cipher = AES.new(self.key, AES.MODE_ECB)
pt = b''
state = IV
for i in range(len(ct)):
b = cipher.encrypt(state)[0]
c = b ^ ct[i]
pt += bytes([c])
state = state[1:] + bytes([ct[i]])
return pt


class Challenge():
def __init__(self):
self.before_input = "Please authenticate to this Domain Controller to proceed\n"
self.password = urandom(20)
self.password_length = len(self.password)
self.cipher = CFB8(urandom(16))

def challenge(self, your_input):
if your_input['option'] == 'authenticate':
if 'password' not in your_input:
return {'msg': 'No password provided.'}
your_password = your_input['password']
if your_password.encode() == self.password:
self.exit = True
return {'msg': 'Welcome admin, flag: ' + FLAG}
else:
return {'msg': 'Wrong password.'}

if your_input['option'] == 'reset_connection':
self.cipher = CFB8(urandom(16))
return {'msg': 'Connection has been reset.'}

if your_input['option'] == 'reset_password':
if 'token' not in your_input:
return {'msg': 'No token provided.'}
token_ct = bytes.fromhex(your_input['token'])
if len(token_ct) < 28:
return {'msg': 'New password should be at least 8-characters long.'}

token = self.cipher.decrypt(token_ct)
new_password = token[:-4]
self.password_length = bytes_to_long(token[-4:])
self.password = new_password[:self.password_length]
return {'msg': 'Password has been correctly reset.'}


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

一个登录系统可以选择修改密码,重置密码,使用密码登录获得flag

观察修改密码的过程,将iv逐位加密后异或c,iv长度不够则后补c,意味着我们输入iv的前一部分和c相等时,密码将是重复的相同字节,爆破即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from Crypto.Util.number import *
from pwn import *
import json
sh=remote("socket.cryptohack.org",13399)

token=b'\x00'*10+b'\x00'*8+b'\x00'*10

sh.recvuntil(b'proceed\n')
sh.sendline(b'{"option": "reset_password", "token": "'+token.hex().encode()+b'"}')
sh.recvuntil(b'reset."}\n')

for i in range(256):
se=json.dumps({"option": "authenticate", "password": (long_to_bytes(i)*8).decode()}).encode()
print(se)
sh.sendline(se)
re=json.loads(sh.recvline().decode())
print(re)
msg=re['msg']
if 'flag' in msg:
print(msg)
break

{‘msg’: ‘Welcome admin, flag: crypto{Zerologon_Windows_CVE-2020-1472}’}

Stream of Consciousness

与我交谈,并从我的加密意识流中听到一句话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from Crypto.Cipher import AES
from Crypto.Util import Counter
import random

KEY = ?
TEXT = ['???', '???', ..., FLAG]

@chal.route('/stream_consciousness/encrypt/')
def encrypt():
random_line = random.choice(TEXT)

cipher = AES.new(KEY, AES.MODE_CTR, counter=Counter.new(128))
encrypted = cipher.encrypt(random_line.encode())

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