XYCTF 2025

本文最后更新于 2025年4月8日 晚上

实力不行

Crypto

勒索病毒

(感觉这题有点该放逆向里面的)

有一个 task.exe,EP 查一下发现是 Pyinstaller 打包的 python 文件

pyinstxtractor 解包

https://pylingual.io 进行反编译,反编译结果如下

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
# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: task.py
# Bytecode version: 3.8.0rc1+ (3413)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)

"""
Created on Sun Mar 30 18:25:08 2025

@author: Crypto0

import re
import base64
import os
import sys
from gmssl import sm4
from Crypto.Util.Padding import pad
import binascii
from random import shuffle, randrange

N = 49
p = 3
q = 128
d = 3
assert q > (6 * d + 1) * p
R.<x> = ZZ[]
def generate_T(d1, d2):
assert N >= d1 + d2
s = [1] * d1 + [-1] * d2 + [0] * (N - d1 - d2)
shuffle(s)
return R(s)

def invert_mod_prime(f, p):
Rp = R.change_ring(Integers(p)).quotient(x^N - 1)
return R(lift(1 / Rp(f)))

def convolution(f, g):
return (f * g) % (x^N - 1)

def lift_mod(f, q):
return R([((f[i] + q // 2) % q) - q // 2 for i in range(N)])

def poly_mod(f, q):
return R([f[i] % q for i in range(N)])

def invert_mod_pow2(f, q):
assert q.is_power_of(2)
g = invert_mod_prime(f, 2)
while True:
r = lift_mod(convolution(g, f), q)
if r == 1:
return g
g = lift_mod(convolution(g, 2 - r), q)

def generate_message():
return R([randrange(p) - 1 for _ in range(N)])

def generate_key():
while True:
try:
f = generate_T(d + 1, d)
g = generate_T(d, d)
Fp = poly_mod(invert_mod_prime(f, p), p)
Fq = poly_mod(invert_mod_pow2(f, q), q)
break
except:
continue
h = poly_mod(convolution(Fq, g), q)
return h, (f, g)

def encrypt_message(m, h):
e = lift_mod(p * convolution(h, generate_T(d, d)) + m, q)
return e

def save_ntru_keys():
h, secret = generate_key()
with open("pub_key.txt", "w") as f:
f.write(str(h))
m = generate_message()
with open("priv_key.txt", "w") as f:
f.write(str(m))
e = encrypt_message(m, h)
with open("enc.txt", "w") as f:
f.write(str(e))

def terms(poly_str):
terms = []
pattern = r'([+-]?\\s*x\\^?\\d*|[-+]?\\s*\\d+)'
matches = re.finditer(pattern, poly_str.replace(' ', ''))

for match in matches:
term = match.group()
if term == '+x' or term == 'x':
terms.append(1)
elif term == '-x':
terms.append(-1)
elif 'x^' in term:
coeff_part = term.split('x^')[0]
exponent = int(term.split('x^')[1])
if not coeff_part or coeff_part == '+':
coeff = 1
elif coeff_part == '-':
coeff = -1
else:
coeff = int(coeff_part)
terms.append(coeff * exponent)
elif 'x' in term:
coeff_part = term.split('x')[0]
if not coeff_part or coeff_part == '+':
terms.append(1)
elif coeff_part == '-':
terms.append(-1)
else:
terms.append(int(coeff_part))
else:
if term == '+1' or term == '1':
terms.append(0)
terms.append(-0)
return terms

def gen_key(poly_terms):
binary = [0] * 128
for term in poly_terms:
exponent = abs(term)
if term > 0 and exponent <= 127:
binary[127 - exponent] = 1
binary_str = ''.join(map(str, binary))
hex_key = hex(int(binary_str, 2))[2:].upper().zfill(32)
return hex_key

def read_polynomial_from_file(filename):
with open(filename, 'r') as file:
return file.read().strip()


def sm4_encrypt(key, plaintext):
assert len(key) == 16, "SM4 key must be 16 bytes"
cipher = sm4.CryptSM4()
cipher.set_key(key, sm4.SM4_ENCRYPT)
padded_plaintext = pad(plaintext, 16)
return cipher.crypt_ecb(padded_plaintext)

def sm4_encrypt_file(input_path, output_path, key):
with open(input_path, 'rb') as f:
plaintext = f.read()

ciphertext = sm4_encrypt(key, plaintext)

with open(output_path, 'wb') as f:
f.write(ciphertext)

def resource_path(relative_path):
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS
else:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)

def encrypt_directory(directory, sm4_key, extensions=[".txt"]):
if not os.path.exists(directory):
print(f"Directory does not exist: {directory}")
return

for root, _, files in os.walk(directory):
for file in files:
if any(file.endswith(ext) for ext in extensions):
input_path = os.path.join(root, file)
output_path = input_path + ".enc"

try:
sm4_encrypt_file(input_path, output_path, sm4_key)
os.remove(input_path)
print(f"Encrypted: {input_path} -> {output_path}")
except Exception as e:
print(f"Error encrypting {input_path}: {str(e)}")

def main():
try:
save_ntru_keys()
poly_str = read_polynomial_from_file("priv_key.txt")
poly_terms = terms(poly_str)
sm4_key = binascii.unhexlify(poly_terms)
user_name = os.getlogin()
target_dir = os.path.join("C:\\Users", user_name, "Desktop", "test_files")

if not os.path.exists(target_dir):
os.makedirs(target_dir, exist_ok=True)
print(f"Created directory: {target_dir}")
return

txt_files = [f for f in os.listdir(target_dir)
if f.endswith('.txt') and os.path.isfile(os.path.join(target_dir, f))]

if not txt_files:
print("No .txt files found in directory")
return

for txt_file in txt_files:
file_path = os.path.join(target_dir, txt_file)
try:
with open(file_path, 'rb') as f:
test_data = f.read()

ciphertext = sm4_encrypt(sm4_key, test_data)
encrypted_path = file_path + '.enc'

with open(encrypted_path, 'wb') as f:
f.write(ciphertext)
except Exception as e:
print(f"Error processing {txt_file}: {str(e)}")

except Exception as e:
print(f"Fatal error: {str(e)}")

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

在解包后的 task.exe_extracted/res 目录下有两个文件 enc.txtpib_key.txt

enc.txt

1
2
e = 31*x^48 - 14*x^47 + x^46 + 8*x^45 - 9*x^44 - 18*x^43 - 30*x^41 + 14*x^40 + 3*x^39 - 17*x^38 + 22*x^37 + 7*x^36 + 31*x^34 - 30*x^33 - 22*x^32 - 25*x^31 + 31*x^30 - 28*x^29 + 7*x^28 + 23*x^27 - 6*x^26 + 12*x^25 - 6*x^24 + 5*x^23 - 13*x^22 - 10*x^20 + 4*x^19 + 15*x^18 + 23*x^17 + 24*x^16 - 2*x^15 - 8*x^14 - 20*x^13 + 24*x^12 - 23*x^11 - 4*x^10 - 26*x^9 - 14*x^8 + 10*x^7 + 4*x^6 - 4*x^5 - 32*x^4 - 5*x^3 - 31*x^2 + 16*x + 11
-x^48 - x^46 + x^45 + x^43 - x^42 + x^41 + x^40 + x^36 - x^35 + x^34 - x^33 + x^32 - x^30 + x^29 - x^28 - x^27 - x^26 - x^25 - x^23 - x^22 + x^21 + x^20 + x^19 + x^18 - x^17 - x^16 - x^15 - x^14 - x^12 + x^9 - x^7 - x^6 - x^5 - x^4 + x^3 - x + 1

pub_key.txt

1
h = 8*x^48 + 58*x^47 + 18*x^46 + 61*x^45 + 33*x^44 + 21*x^43 + 58*x^42 + 21*x^41 + 5*x^40 + 32*x^39 + 15*x^38 + 40*x^37 + 24*x^36 + 14*x^35 + 40*x^34 + 5*x^33 + x^32 + 48*x^31 + 21*x^30 + 36*x^29 + 42*x^28 + 8*x^27 + 17*x^26 + 54*x^25 + 39*x^24 + 38*x^23 + 14*x^22 + 22*x^21 + 26*x^20 + 22*x^18 + 7*x^17 + 29*x^16 + 53*x^15 + 50*x^14 + 49*x^13 + 21*x^12 + 47*x^11 + 50*x^10 + 32*x^9 + 14*x^8 + 50*x^7 + 18*x^6 + 9*x^5 + 61*x^4 + 10*x^3 + 9*x^2 + 11*x + 47

flag.txt.enc

1
bf0cb5cc6bea6146e9c1f109df953a57daa416d38a8ffba6438e7e599613e01f3b9a53dace4ccd55cd3e55ef88e0b835

enc.txt 的第二个多项式(私钥)提取正项指数(带 + 的指数)

1
-x^48 -x^46 +x^45 +x^43 -x^42 +x^41 +x^40 +x^36 -x^35 +x^34 -x^33 +x^32 -x^30 +x^29 -x^28 -x^27 -x^26 -x^25 -x^23 -x^22 +x^21 +x^20 +x^19 +x^18 -x^17 -x^16 -x^15 -x^14 -x^12 +x^9 -x^7 -x^6 -x^5 -x^4 +x^3 -x +1

正项指数列表

1
key_exponents = [45, 43, 41, 40, 36, 34, 32, 29, 21, 20, 19, 18, 9, 3]

脚本如下:

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
# -*- coding: utf-8 -*-
from gmssl import sm4
from Crypto.Util.Padding import unpad
import binascii

# 私钥多项式中的正项指数
KEY_EXPONENTS = [45, 43, 41, 40, 36, 34, 32, 29, 21, 20, 19, 18, 9, 3]

# 密文(hex格式)
CIPHER_TEXT_HEX = "bf0cb5cc6bea6146e9c1f109df953a57daa416d38a8ffba6438e7e599613e01f3b9a53dace4ccd55cd3e55ef88e0b835"

def generate_sm4_key(exponents):
"""生成 SM4 密钥"""
binary = [0] * 128
for exp in exponents:
if 0 < exp <= 127:
binary[127 - exp] = 1 # 计算二进制位
hex_key = hex(int(''.join(map(str, binary)), 2)[2:].zfill(32)
return hex_key.upper()

def decrypt_flag():
# 生成密钥
sm4_key_hex = generate_sm4_key(KEY_EXPONENTS)
sm4_key = binascii.unhexlify(sm4_key_hex)

# 初始化 SM4 解密器
cipher = sm4.CryptSM4()
cipher.set_key(sm4_key, sm4.SM4_DECRYPT)

# 解密并移除填充
ciphertext = binascii.unhexlify(CIPHER_TEXT_HEX)
decrypted_padded = cipher.crypt_ecb(ciphertext)
decrypted = unpad(decrypted_padded, 16)

return decrypted.decode('utf-8')

if __name__ == "__main__":
flag = decrypt_flag()
print(f"Decrypted Flag: {flag}")

拿到 flag

1
XYCTF{Crypto0_can_n0t_So1ve_it}

Reverse

Dragon

第一次做关于 LLVM 的题,从 0 到 0.1 的配环境www

新装的 Ubuntu24.04.02,用这个传一下文件

1
sudo vmhgfs-fuse .host:/ /mnt/hgfs -o allow_other

拿到 Dragon.bc 文件之后查看文件类型,还能用 Exeinfo PE 看,但是只能得到是关于 LLVM 的,没有具体信息

1
file Dragon.bc

Dragon.bc 文件为 LLVM IR bitcode 文件,LLVM 完整编译流程如下:

1
源文件(.c/.cpp) -> LLVM 前端(clang) -> LLVM IR(.ll/.bc) -> LLVM 中端(opt) -> 优化后 IR -> LLVM 后端(llc) -> 汇编(.s) -> 汇编器 -> 目标文件(.o) -> 链接器 -> 可执行文件

bc 文件转换为 object file

1
llc-17 Dragon.bc -filetype=obj -o Dragon.o

拿出来丢进 ida 分析,找到 main 函数

大概是将 &unk_370 的数据复制到 v7 数组中,然后初始化 Str 数组为0,接着提示用户输入flag,并读取输入到 Str

main 函数中还有一个 calculate_crc64_direct() 函数

根据代码中的 0x42F0E1EBA9EA3693 知道实现的是 CRC64 算法,初始值为 0xFFFFFFFFFFFFFFFF,输入数据每个字节被左移 56 位后与当前 CRC 值异或,然后处理每个位,最高位决定是否异或多项式,处理完所有位后,最终结果取反。CRC 算法有两种处理方式:左移(big-endian)或右移(little-endian),根据函数中 a1[i] << 56 知道这里是左移

找到 &unk_370 的数据

把数据提取出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
47h, 7Bh, 9Fh, 41h, 4Eh, 0E3h, 63h, 0DCh, 0C6h, 0BFh, 0B2h, 0E7h, 0D4h, 0F8h, 1Eh, 3, 9Eh, 0D8h, 5Fh, 62h, 0BCh, 2Fh, 0D6h, 12h, 0E8h, 55h, 57h, 0CCh, 0E1h, 0B6h, 0E8h, 83h, 0CCh, 65h, 0B6h, 2Ah, 0EBh, 0B1h, 7Bh, 0FCh, 6Bh, 0D9h, 62h, 2Ah, 1Bh, 0CAh, 82h, 93h, 87h, 0C3h, 73h, 76h, 0A0h, 0F8h, 0FFh, 0B1h, 0E1h, 5, 8Eh, 38h, 27h, 16h, 0A8h, 0Dh, 0B7h, 0AAh, 0D0h, 0E8h, 1Ah, 0E6h, 0F1h, 9Eh, 45h, 61h, 0F2h, 0E7h, 0D2h, 3Fh, 78h, 92h, 0Bh, 0E6h, 6Fh, 0F5h, 0A1h, 7Ch, 0C9h, 63h, 0ABh, 3Ah, 0B7h, 43h, 0B0h, 0A8h, 0D3h, 9Bh

转为十六进制 ->

0x47, 0x7B, 0x9F, 0x41, 0x4E, 0xE3, 0x63, 0xDC,
0xC6, 0xBF, 0xB2, 0xE7, 0xD4, 0xF8, 0x1E, 0x03,
0x9E, 0xD8, 0x5F, 0x62, 0xBC, 0x2F, 0xD6, 0x12,
0xE8, 0x55, 0x57, 0xCC, 0xE1, 0xB6, 0xE8, 0x83,
0xCC, 0x65, 0xB6, 0x2A, 0xEB, 0xB1, 0x7B, 0xFC,
0x6B, 0xD9, 0x62, 0x2A, 0x1B, 0xCA, 0x82, 0x93,
0x87, 0xC3, 0x73, 0x76, 0xA0, 0xF8, 0xFF, 0xB1,
0xE1, 0x05, 0x8E, 0x38, 0x27, 0x16, 0xA8, 0x0D,
0xB7, 0xAA, 0xD0, 0xE8, 0x1A, 0xE6, 0xF1, 0x9E,
0x45, 0x61, 0xF2, 0xE7, 0xD2, 0x3F, 0x78, 0x92,
0x0B, 0xE6, 0x6F, 0xF5, 0xA1, 0x7C, 0xC9, 0x63,
0xAB, 0x3A, 0xB7, 0x43, 0xB0, 0xA8, 0xD3, 0x9B

正向左移那么逆向就需要右移

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

hex_data = [
0x47, 0x7B, 0x9F, 0x41, 0x4E, 0xE3, 0x63, 0xDC,
0xC6, 0xBF, 0xB2, 0xE7, 0xD4, 0xF8, 0x1E, 0x03,
0x9E, 0xD8, 0x5F, 0x62, 0xBC, 0x2F, 0xD6, 0x12,
0xE8, 0x55, 0x57, 0xCC, 0xE1, 0xB6, 0xE8, 0x83,
0xCC, 0x65, 0xB6, 0x2A, 0xEB, 0xB1, 0x7B, 0xFC,
0x6B, 0xD9, 0x62, 0x2A, 0x1B, 0xCA, 0x82, 0x93,
0x87, 0xC3, 0x73, 0x76, 0xA0, 0xF8, 0xFF, 0xB1,
0xE1, 0x05, 0x8E, 0x38, 0x27, 0x16, 0xA8, 0x0D,
0xB7, 0xAA, 0xD0, 0xE8, 0x1A, 0xE6, 0xF1, 0x9E,
0x45, 0x61, 0xF2, 0xE7, 0xD2, 0x3F, 0x78, 0x92,
0x0B, 0xE6, 0x6F, 0xF5, 0xA1, 0x7C, 0xC9, 0x63,
0xAB, 0x3A, 0xB7, 0x43, 0xB0, 0xA8, 0xD3, 0x9B
]

def calculate_crc64(data):
# CRC64算法实现
crc = 0xFFFFFFFFFFFFFFFF
poly = 0x42F0E1EBA9EA3693
for byte in data:
crc ^= (byte << 56)
for _ in range(8):
if crc & (1 << 63):
crc = ((crc << 1) ^ poly) & 0xFFFFFFFFFFFFFFFF
else:
crc = (crc << 1) & 0xFFFFFFFFFFFFFFFF
return (~crc) & 0xFFFFFFFFFFFFFFFF

def find_flag():
# 转换目标校验值(小端序)
targets = [
int.from_bytes(bytes(hex_data[i:i+8]), 'little')
for i in range(0, 96, 8)
]

# 暴力破解
from tqdm import tqdm # 进度条库

flag = bytearray()
for idx, target in enumerate(targets, 1):
found = False
# 遍历所有可能的2字节组合
for b1, b2 in tqdm(itertools.product(range(256), repeat=2),
desc=f"破解第{idx}个校验值", total=65536):
data = bytes([b1, b2])
if calculate_crc64(data) == target:
flag.extend(data)
print(f"\n[+] 匹配成功:{data}{hex(target)}")
found = True
break
if not found:
print(f"\n[-] 未找到匹配项:{hex(target)}")
return

print("\nflag:")
print("UTF-8:", flag.decode('utf-8', errors='replace'))
print("HEX :", flag.hex())

if __name__ == "__main__":
find_flag()

拿到 flag

1
flag{LLVM_1s_Fun_Ri9h7?}

Moon

这题虽然是 pyd 逆向,但应该是最简单的那一类

shift+f12 搜索字符串,前面有一些文件

往下看能发现一串可疑字符串

1
426b87abd0ceaa3c58761bbb0172606dd8ab064491a2a76af9a93e1ae56fa84206a2f7

跟进定位到 sub_180002550() 函数

把里面关键字符串提取出来:

加密相关:

1
2
3
v9[1] = "426b87abd0ceaa3c58761bbb0172606dd8ab064491a2a76af9a93e1ae56fa84206a2f7";
v80 = "encrypted";
v179 = "xor_crypt";

模块加载:

1
2
3
4
5
6
v14 = "SEED"
v20 = "TARGET_HEX"
v135 = "randint"
v141 = "random" // 使用Python random模块
v146 = "seed"
v152 = "seed_value" // 随机数种子配置

可以看到有 xor 加密,继续看能在 sub_180001370() 函数找到了关于 Xor 的信息,有一个 PyNumber_Xor() ,将 v4v8 异或返回 v5,然后将 v5 添加到 list 中

仔细分析 sub_180001370() 函数中的 v8 来源:

这里通过 PyDict_GetItem_KnownHashoff_18000B618 指向的字典中获取项

1
Item_KnownHash = PyDict_GetItem_KnownHash(*off_18000B618, v11, *(v11 + 24));

通过代码可以发现 v8 的核心来源是 off_18000B618 指向的模块全局字典

然后交叉引用 off_18000B618 发现被 sub_180003010() 函数多次引用

跟进来到 sub_180003010() 函数,在 PyLong_FromLong() 函数中找到了随机数种子 1131796,这个 PyLong_FromLong() 函数是一个构造函数,主要是对数值进行处理

因此可以得到如下信息:

1
2
3
密文:426b87abd0ceaa3c58761bbb0172606dd8ab064491a2a76af9a93e1ae56fa84206a2f7
加密的随机数种子值:1131796
加密方式:异或

所以程序主要逆向逻辑:使用固定的随机数种子 1131796,生成和密文长度一样的密钥然后再和密文异或得到明文

逆向脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import random

# 设置固定种子
random.seed(1131796)

# 密文(64字节HEX字符串)
encrypted_hex = "426b87abd0ceaa3c58761bbb0172606dd8ab064491a2a76af9a93e1ae56fa84206a2f7"
encrypted_bytes = bytes.fromhex(encrypted_hex)

# 生成等长随机密钥
key = [random.randint(0, 255) for _ in range(len(encrypted_bytes))]

# 异或解密
flag_bytes = bytes([encrypted_bytes[i] ^ key[i] for i in range(len(encrypted_bytes))])

# 输出flag
print(flag_bytes.decode())

拿到 flag

1
flag{but_y0u_l00k3d_up_@t_th3_mOOn}

XYCTF 2025
http://example.com/2025/04/08/XYCTF-2025/
作者
butt3rf1y
发布于
2025年4月8日
许可协议