Skip to content

Berd's Red Envelope 2021 WriteUp

Published: at 23:47

摸鱼的 2020 最后几小时

Web 手的 Misc/Crypto 修行

在结束之前一直保持密码保护 已经结束力(

ToC

开始

由于 <a> 设置了 pointer-events: none; 因此无法点击。复制链接进入第一题

一只方熊猫

下载后发现图片无法打开:

使用 010Editor 打开后提示 CRC 错误:

随便找了个爆破图片宽高的脚本:

1
# -*- coding: utf8 -*-
2
3
import os
4
import binascii
5
import struct
6
misc = open("panda.png","rb").read()
7
8
# 爆破宽
9
for i in range(1024):
10
data = misc[12:16] + struct.pack('>i',i)+ misc[20:29] #IHDR数据
11
crc32 = binascii.crc32(data) & 0xffffffff
12
if crc32 == 0x4A920BA4: #IHDR块的crc32值
13
print("w")
14
print(i)
15
print("hex:"+hex(i))
16
17
# 爆破高
18
for i in range(1024):
19
data = misc[12:20] + struct.pack('>i',i)+ misc[24:29]
20
crc32 = binascii.crc32(data) & 0xffffffff
21
if crc32 == 0x4A920BA4:
22
print("h")
23
print(i)
24
print("hex:"+hex(i))

拿脚本跑一下,得到图片的真正 height

修改原文件,得到 flag

根据 UUID 进入下一题

🤝

给出的链接是 http://arealexistingdomain/flag.html,第一想法就是在 /etc/hosts 里加个域名(

然后 curl 一下就出来了(

根据 UUID 进入下一题

粗心的小明

题中给出的代码如下:

1
const fs = require("fs");
2
const uuid = require("uuid");
3
const crypto = require("crypto");
4
5
let red_envelope_2021 = uuid.v4();
6
7
let key = crypto.scryptSync("xiaomingSecureKey2021", "xiaomingSuperSalt", 32);
8
let cipher = crypto.createCipheriv("aes-256-cfb", key, crypto.randomBytes(16));
9
10
let json = JSON.stringify({
11
red_envelope_2021,
12
});
13
14
fs.writeFile(
15
"red_envelope_encrypted.hex",
16
cipher.update(json, "utf8", "hex") + cipher.final("hex"),
17
function (err) {
18
if (err) {
19
console.error(err);
20
} else {
21
console.info("Red envelope stored successfully!");
22
}
23
}
24
);

给出的 hex 如下:

1
d365895fbcdbd3a29b1bf00307429fd07d53ba3c0553b8789867d4aee3b8c3bbb0e5a8fd582a9696aabbdc1e373f97efac2529d588320800449553f6

加密使用的是 aes-256-cfb,以 IV 作为初始文本,加密(encrypt_block)得到一个块(block_encrypted),再将 block_encrypted 和明文异或(⊕)得到密文(prev_ciphertext)。之后,再以 prev_ciphertext 为初始文本,加密得到下一个 block_encrypted,再与下一段明文异或……以此类推,如下图所示[1]:

而文本的前半段是确定的,为 {"red_envelope_2021":,这已经超过了一个块的大小(16),因此可以通过第一个块得到 IV。简单步骤如下:

  1. 将原文转换为 hex7b227265645f656e76656c6f70655f32
  2. 从密文中摘取第一块:d365895fbcdbd3a29b1bf00307429fd0
  3. 异或得到 AES 加密后的内容:a847fb3ad884b6cced7e9c6c7727c0e2
  4. Node 的运行结果中得到 32 位密钥:03da3479601ff722a6472329347a161bf8a712ead1d04b5560414be1dead3566
  5. 解密即可

解密使用了 boppreh/aesAES 实现[2],修改后的脚本如下:

1
#!/usr/bin/env python3
2
3
s_box = (
4
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
5
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
6
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
7
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
8
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
9
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
10
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
11
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
12
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
13
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
14
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
15
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
16
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
17
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
18
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
19
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
20
)
21
22
inv_s_box = (
23
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
24
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
25
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
26
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
27
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
28
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
29
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
30
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
31
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
32
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
33
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
34
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
35
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
36
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
37
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
38
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
39
)
40
41
42
def sub_bytes(s):
43
for i in range(4):
44
for j in range(4):
45
s[i][j] = s_box[s[i][j]]
46
47
48
def inv_sub_bytes(s):
49
for i in range(4):
50
for j in range(4):
51
s[i][j] = inv_s_box[s[i][j]]
52
53
54
def shift_rows(s):
55
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
56
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
57
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
58
59
60
def inv_shift_rows(s):
61
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
62
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
63
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
64
65
def add_round_key(s, k):
66
for i in range(4):
67
for j in range(4):
68
s[i][j] ^= k[i][j]
69
70
71
# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
72
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
73
74
75
def mix_single_column(a):
76
# see Sec 4.1.2 in The Design of Rijndael
77
t = a[0] ^ a[1] ^ a[2] ^ a[3]
78
u = a[0]
79
a[0] ^= t ^ xtime(a[0] ^ a[1])
80
a[1] ^= t ^ xtime(a[1] ^ a[2])
81
a[2] ^= t ^ xtime(a[2] ^ a[3])
82
a[3] ^= t ^ xtime(a[3] ^ u)
83
84
85
def mix_columns(s):
86
for i in range(4):
87
mix_single_column(s[i])
88
89
90
def inv_mix_columns(s):
91
# see Sec 4.1.3 in The Design of Rijndael
92
for i in range(4):
93
u = xtime(xtime(s[i][0] ^ s[i][2]))
94
v = xtime(xtime(s[i][1] ^ s[i][3]))
95
s[i][0] ^= u
96
s[i][1] ^= v
97
s[i][2] ^= u
98
s[i][3] ^= v
99
100
mix_columns(s)
101
102
103
r_con = (
104
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
105
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
106
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
107
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
108
)
109
110
111
def bytes2matrix(text):
112
""" Converts a 16-byte array into a 4x4 matrix. """
113
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
114
115
def matrix2bytes(matrix):
116
""" Converts a 4x4 matrix into a 16-byte array. """
117
return bytes(sum(matrix, []))
118
119
def xor_bytes(a, b):
120
""" Returns a new byte array with the elements xor'ed. """
121
return bytes(i^j for i, j in zip(a, b))
122
123
def inc_bytes(a):
124
""" Returns a new byte array with the value increment by 1 """
125
out = list(a)
126
for i in reversed(range(len(out))):
127
if out[i] == 0xFF:
128
out[i] = 0
129
else:
130
out[i] += 1
131
break
132
return bytes(out)
133
134
def pad(plaintext):
135
"""
136
Pads the given plaintext with PKCS#7 padding to a multiple of 16 bytes.
137
Note that if the plaintext size is a multiple of 16,
138
a whole block will be added.
139
"""
140
padding_len = 16 - (len(plaintext) % 16)
141
padding = bytes([padding_len] * padding_len)
142
return plaintext + padding
143
144
def unpad(plaintext):
145
"""
146
Removes a PKCS#7 padding, returning the unpadded text and ensuring the
147
padding was correct.
148
"""
149
padding_len = plaintext[-1]
150
assert padding_len > 0
151
message, padding = plaintext[:-padding_len], plaintext[-padding_len:]
152
assert all(p == padding_len for p in padding)
153
return message
154
155
def split_blocks(message, block_size=16, require_padding=True):
156
assert len(message) % block_size == 0 or not require_padding
157
return [message[i:i+16] for i in range(0, len(message), block_size)]
158
159
160
class AES:
161
rounds_by_key_size = {16: 10, 24: 12, 32: 14}
162
def __init__(self, master_key):
163
"""
164
Initializes the object with a given key.
165
"""
166
assert len(master_key) in AES.rounds_by_key_size
167
self.n_rounds = AES.rounds_by_key_size[len(master_key)]
168
self._key_matrices = self._expand_key(master_key)
169
170
def _expand_key(self, master_key):
171
"""
172
Expands and returns a list of key matrices for the given master_key.
173
"""
174
# Initialize round keys with raw key material.
175
key_columns = bytes2matrix(master_key)
176
iteration_size = len(master_key) // 4
177
178
# Each iteration has exactly as many columns as the key material.
179
columns_per_iteration = len(key_columns)
180
i = 1
181
while len(key_columns) < (self.n_rounds + 1) * 4:
182
# Copy previous word.
183
word = list(key_columns[-1])
184
185
# Perform schedule_core once every "row".
186
if len(key_columns) % iteration_size == 0:
187
# Circular shift.
188
word.append(word.pop(0))
189
# Map to S-BOX.
190
word = [s_box[b] for b in word]
191
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
192
word[0] ^= r_con[i]
193
i += 1
194
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
195
# Run word through S-box in the fourth iteration when using a
196
# 256-bit key.
197
word = [s_box[b] for b in word]
198
199
# XOR with equivalent word from previous iteration.
200
word = xor_bytes(word, key_columns[-iteration_size])
201
key_columns.append(word)
202
203
# Group key words in 4x4 byte matrices.
204
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
205
206
def encrypt_block(self, plaintext):
207
"""
208
Encrypts a single block of 16 byte long plaintext.
209
"""
210
assert len(plaintext) == 16
211
212
plain_state = bytes2matrix(plaintext)
213
214
add_round_key(plain_state, self._key_matrices[0])
215
216
for i in range(1, self.n_rounds):
217
sub_bytes(plain_state)
218
shift_rows(plain_state)
219
mix_columns(plain_state)
220
add_round_key(plain_state, self._key_matrices[i])
221
222
sub_bytes(plain_state)
223
shift_rows(plain_state)
224
add_round_key(plain_state, self._key_matrices[-1])
225
226
return matrix2bytes(plain_state)
227
228
def decrypt_block(self, ciphertext):
229
"""
230
Decrypts a single block of 16 byte long ciphertext.
231
"""
232
assert len(ciphertext) == 16
233
234
cipher_state = bytes2matrix(ciphertext)
235
236
add_round_key(cipher_state, self._key_matrices[-1])
237
inv_shift_rows(cipher_state)
238
inv_sub_bytes(cipher_state)
239
240
for i in range(self.n_rounds - 1, 0, -1):
241
add_round_key(cipher_state, self._key_matrices[i])
242
inv_mix_columns(cipher_state)
243
inv_shift_rows(cipher_state)
244
inv_sub_bytes(cipher_state)
245
246
add_round_key(cipher_state, self._key_matrices[0])
247
248
return matrix2bytes(cipher_state)
249
250
if __name__ == '__main__':
251
from base64 import b64encode
252
from binascii import unhexlify
253
ret=AES(unhexlify('03da3479601ff722a6472329347a161bf8a712ead1d04b5560414be1dead3566')).decrypt_block(unhexlify('a847fb3ad884b6cced7e9c6c7727c0e2'))
254
print(b64encode(ret))

得到 IVbase64Jle9TPezMnv770Y5b+Wc1g==,最后使用 CyberChef 解密:

得到下一题的地址

太 🆒 啦

找了好久,结果 Esolang Wiki 还真有全名就叫这个的语言[3](

题面如下:

1
🆕6️⃣🍿🍔🌭🥪🌮🍆🥑💬🍔💬🌭💬🥪💬🌮💬🍆💬🥑🛑🆕🔟🤔🖇🙃🍖🍟🥔🌽🥩🥕🌶️🌯🥥❓🆓⚖️🆓✖️🆓➗🙃🔢1️⃣8️⃣4️⃣7️⃣🔢🛑4️⃣🛑🔢2️⃣4️⃣8️⃣🔢🛑🆓🍿🌶️🥥🌽🥕🌽🥔🍿🌽🍖🌽🥩🌯🍟🛑🆓💬🔤🚫📥️🔛🔤🛑🛑💬🔤💻🔑🔑🔑🔑🔑🔑🔤🤔🎛️🔤9️⃣❤️9️⃣4️⃣🔤🔤1️⃣9️⃣7️⃣4️⃣🔤🔤4️⃣💜💛7️⃣🔤🔤➖🔤🔤🧡7️⃣9️⃣9️⃣🔤🔤8️⃣❤️4️⃣💛🔤🔤💚6️⃣0️⃣💛🔤🔤2️⃣5️⃣💙1️⃣🔤🔤2️⃣3️⃣7️⃣2️⃣🔤

提示

到一半给出了提示:

1
❤️ A 🧡 B 💛 C 💚 D 💙 E 💜 F

于是下面分析的内容就将这些红心全都替换成对应的文本了。

🍿

square-cool 仓库[4] 的描述,🆕 是定义函数,后面跟随一个数字 n,表示参数的数量,然后是函数名 emoji,最后的 nemoji 表示参数名,函数以 🛑 结尾。于是开头的一一部分可以翻译成下面的形式:

1
# 🍿: Print six arguments
2
🆕6️🍿
3
4
🍔🌭🥪🌮🍆🥑
5
6
💬🍔
7
💬🌭
8
💬🥪
9
💬🌮
10
💬🍆
11
💬🥑
12
13
🛑

其中 💬 是内置的函数,表示输出。这个函数的功能也就是连续输出对应的 6 个参数。

🤔

同样是一个函数,翻译如下:

1
# 🤔:
2
🆕🔟🤔
3
🖇 # modifier
4
🙃 # input
5
🍖 # p1
6
🍟 # p2
7
🥔 # p3
8
🌽 # p4
9
🥩 # p5
10
🥕 # p6
11
🌶️ # p7
12
🌯 # p8
13
🥥 # p9
14
❓ # Returns the second argument if the first is true, otherwise returns the third argument.
15
# input / 1847 * 4 == 2️4️8️
16
🆓
17
⚖️
18
🆓
19
✖️
20
🆓
21
➗🙃🔢1️8️4️7️🔢
22
🛑
23
4️
24
🛑
25
🔢2️4️8️🔢
26
🛑
27
28
🆓
29
# 🍿(p7, p9, -, p6, -, p3)
30
🍿🌶️🥥🌽🥕🌽🥔
31
# 🍿(-, p1, -, p5, p8, p2)
32
🍿🌽🍖🌽🥩🌯🍟
33
🛑
34
35
🆓
36
# print('🚫📥️🔛')
37
💬🔤🚫📥️🔛🔤
38
🛑
39
40
🛑

其中 🆓 的功能类似作用域开始,并以 🛑 结束。

最后

1
# main
2
# print('💻🔑🔑🔑🔑🔑🔑')
3
💬🔤💻🔑🔑🔑🔑🔑🔑🔤
4
5
# 🤔(stdin, '9️A9️4️', '1️9️7️4️', '4️FC7️', '-', 'B7️9️9️', '8️A4️C', 'D6️0️C', '2️5️E1️', '2️3️7️2️')
6
# d60c2372-8a4c-4fc7-9a94-b79925e11974
7
🤔🎛️
8
🔤9️A9️4️🔤
9
🔤1️9️7️4️🔤
10
🔤4️FC7️🔤
11
🔤➖🔤
12
🔤B7️9️9️🔤
13
🔤8️A4️C🔤
14
🔤D6️0️C🔤
15
🔤2️5️E1️🔤
16
🔤2️3️7️2️🔤

最后就是调用了,其中 🎛️ 表示从 stdin 读入一个整数,但在实际分析中并没有起到作用。

最后得到 UUIDd60c2372-8a4c-4fc7-9a94-b79925e11974,进入终章

Happy 2021

游戏结束,新年快乐!あけましておめでとうございます!(笑)

参考

  1. https://ctf-wiki.github.io/ctf-wiki/crypto/blockcipher/mode/cfb-zh/
  2. https://github.com/boppreh/aes
  3. https://esolangs.org/wiki/%F0%9F%86%92
  4. https://gitlab.com/fogity/squared-cool