XYCTF2025 Reverse 方向复现

CrackMe

ida 打开看到 WinMain 函数,同时注意到有一个 TlsCallback_0 函数

1
2
3
4
5
6
>VOID NTAPI TlsCallback(PVOID DllHandle, DWORD dwReason, PVOID Reserved) {
//DllHandle:对于 exe 是进程句柄,对于 dll 是模块句柄;dwReason:回调原因;Reserved:保留参数,一般用不到
if (dwReason == DLL_PROCESS_ATTACH) {
MessageBoxA(NULL, "TlsCallback Triggered!", "Info", 0);
}
>}

点进 WinMain,有反调试,先打断点运行起来用 ScyllaHide 去一下反调;很多传入了多个参数的 sub 函数点进去看发现没有什么重要信息,找到 sub_xxxAC00(v39),点进去后同样注意到 v6 = sub_7FF7AD959470;

sub_xxx8770 里面像是一个消息处理器,根据不同的参数值分发不同的处理逻辑
而 Tlscallback 这边,进入 sub_xxx2370



switch 下有五个 case,分别对应不同的参数,case0 里面带点进 sub_xxxF280 挺复杂的,留到后面看,case1、case2、case3 没什么东西,case4 和 case5 有可疑的函数

case4 的 sub_xxxD010 里面有个异或

1
2
3
4
data=[0xDD,0xD7,0xDa,0xDC,0xC0]
for i in range(len(data)):
data[i] ^= 0xBB
print(chr(data[i]), end='')

解出来是 flag{,由此可知此处是校验 flag 的格式
case5 的 sub_xxxC850 里看到了 CRC 校验

爆破出输入

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
def generate_crc32_table():
table = []
for i in range(256):
x = i
for _ in range(8):
if x & 1:
x = (x >> 1) ^ 0xEDB88321
else:
x = x >> 1
table.append(x)
return table


data = [
0x46A95BAD, 0x1CAC84B6, 0xA67CB2B2,
0x32188937, 0x4872D39F, 0xF2A2E59B, 0x011B94D2
]

crc_table = generate_crc32_table()

result_bytes = []
for val in data:

target_crc = (~(val ^ 0xB0E0E879)) & 0xFFFFFFFF

found_idx = None
for idx, crc_val in enumerate(crc_table):
if crc_val == target_crc:
found_idx = idx
break

if found_idx is not None:
input_byte = found_idx ^ 0x79
result_bytes.append(input_byte)
else:
result_bytes.append(0xFF) # 未找到标记

printable_bytes = [b for b in result_bytes if 32 <= b <= 126]
flag_part = ''.join(chr(b) for b in printable_bytes)
print(f" {flag_part}")
print(f" {result_bytes}")
# moshui_
# [109, 111, 115, 104, 117, 105, 95]

所以这里得到部分flag moshui_
case0 的 sub_xxxF280

里面依旧有 crc ,不过这里的 crc 不是进行校验,而是使用输入的前 5 个字节(“flag{“)加上 {} 内部的前 7 个字节(v7 数组里面)生成了两个四字节的密钥(v6[0]和v6[2]),再加上另外连个直接赋值的即可得到完整密钥

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
def build_crc32_table():
table = []
for i in range(256):
crc = i
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0xEDB88321
else:
crc >>= 1
table.append(crc)
return table

def crc_variant(data, table):
result = 0xFFFFFFFF
for b in data:
result ^= table[(b ^ (result & 0xFF))]
result = ~result & 0xFFFFFFFF
return result

data = bytes([0x66, 0x6C, 0x61, 0x67, 0x7B, 0x5C, 0x2F, 0xD0, 0xEC, 0x82, 0x0E, 0x67])

part1 = data[:5]
part2 = data[5:]

crc_table = build_crc32_table()

key0 = crc_variant(part1, crc_table)
key1 = 0x12345678
key2 = crc_variant(part2, crc_table)
key3 = 0x89ABCDEF

key = [key0, key1, key2, key3]
print([hex(k) for k in key])

# ['0x42b2986c', '0x12345678', '0xe40ecf0a', '0x89abcdef']

密钥 :0x42b2986c,0x12345678,0xe40ecf0a,0x89abcdef
同时在里面发现sub_xxx1500
sub_7FF7AD961500(v6, *(_QWORD *)(v5 + 8) + 12LL, 16LL, v8);
分析后可知 a1 传入的是原始密钥

sub_xxx1FF0 :将128位原始密钥扩展为52个16位轮密钥(符合IDEA标准)

sub_xxx1640 :IDEA 加密

分组长度:64 位
密钥长度:128 位
共 8 轮 + 1 次输出变换
每轮用到了三种不同的运算:
模 2¹⁶ 加法(模加)
模 2¹⁶ + 1 乘法(模乘)
按位异或 XOR

主要特点包括使用模乘(模65537)和模加(模65536)运算,以及大量的异或操作

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
__int64 __fastcall sub_7FF7AD961640(
unsigned __int16 *a1, // 输入块(4×16位)
_WORD *a2, // 输出块(4×16位)
unsigned __int16 *a3 // 轮密钥数组
)
// 输入块字节序转换(小端→主机序)
v4 = (*a1 << 8) | (*a1 >> 8); // 字1
v7 = (a1[1] << 8) | (a1[1] >> 8); // 字2
v12 = (a1[2] << 8) | (a1[2] >> 8); // 字3
v18 = (a1[3] << 8) | (a1[3] >> 8); // 字4

// 8轮混淆操作(每轮用6个子密钥)
v24 = 8; // 轮计数器
do {
// 模乘操作 (mod 0x10001) + 加法/异或
v5 = modmul(*a3++, v4); // 轮密钥与v4模乘
v8 = *a3++ + v7; // 轮密钥与v7模加
v13 = *a3++ + v12; // 轮密钥与v12模加
v19 = modmul(*a3++, v18); // 轮密钥与v18模乘
v14 = v5 ^ v13;
v15 = modmul(*a3++, v14); // 轮密钥与v14模乘
v9 = v15 + (v19 ^ v8);
v10 = modmul(*a3++, v9); // 轮密钥与v9模乘
v16 = v10 + v15;
// 状态更新
v4 = v10 ^ v5;
v7 = v13 ^ v10; // 下一轮v7
v12 = v8 ^ v16; // 下一轮v12
v18 = v16 ^ v19; // 下一轮v18
} while (--v24);

// 最终轮(4个子密钥)
v6 = modmul(*a3++, v4);
v17 = *a3++ + v12;
v11 = *a3++ + v7;
v20 = modmul(*a3, v18);

// 输出字节序转换(主机序→小端)
*a2 = (v6 << 8) | (v6 >> 8);
a2[1] = (v17 << 8) | (v17 >> 8);
a2[2] = (v11 << 8) | (v11 >> 8);
a2[3] = (v20 << 8) | (v20 >> 8);

密文在 v7 数组中:

解出第二部分:

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
sub_key = [0] * 52
inv_sub_key = [0] * 52

def plus(a, b):
return (a + b) & 0xFFFF

def times(a, b):
a = a if a != 0 else 0x10000
b = b if b != 0 else 0x10000
r = (a * b) % 0x10001
return r if r != 0x10000 else 0

def xor(a, b):
return a ^ b

def inv_plus(a):
return (-a) & 0xFFFF

def inv_times(a):
if a == 0: return 0
return pow(a, -1, 0x10001)

def subkeys_get(keys_input):
for i in range(8):
sub_key[i] = keys_input[i]

main_key = 0
for i in range(8):
main_key = (main_key << 16) | keys_input[i]

mask128 = (1 << 128) - 1

key_offset = 8
for _ in range(5):
main_key = ((main_key << 25) | (main_key >> 103)) & mask128
for j in range(8):
sub_key[key_offset + j] = (main_key >> (112 - 16 * j)) & 0xFFFF
key_offset += 8

main_key = ((main_key << 25) | (main_key >> 103)) & mask128
for i in range(4):
sub_key[48 + i] = (main_key >> (112 - 16 * i)) & 0xFFFF

def inv_subkeys_get(sub_key_list):
for i in range(6, 48, 6):
inv_sub_key[i] = inv_times(sub_key_list[48 - i])
inv_sub_key[i + 1] = inv_plus(sub_key_list[50 - i])
inv_sub_key[i + 2] = inv_plus(sub_key_list[49 - i])
inv_sub_key[i + 3] = inv_times(sub_key_list[51 - i])
for i in range(0, 48, 6):
inv_sub_key[i + 4] = sub_key_list[46 - i]
inv_sub_key[i + 5] = sub_key_list[47 - i]
inv_sub_key[0] = inv_times(sub_key_list[48])
inv_sub_key[1] = inv_plus(sub_key_list[49])
inv_sub_key[2] = inv_plus(sub_key_list[50])
inv_sub_key[3] = inv_times(sub_key_list[51])
inv_sub_key[48] = inv_times(sub_key_list[0])
inv_sub_key[49] = inv_plus(sub_key_list[1])
inv_sub_key[50] = inv_plus(sub_key_list[2])
inv_sub_key[51] = inv_times(sub_key_list[3])

def dencrypt_final(cipher_block):
i1, i2, i3, i4 = (cipher_block >> 48)&0xFFFF, (cipher_block >> 32)&0xFFFF, (cipher_block >> 16)&0xFFFF, cipher_block&0xFFFF

for i in range(0, 48, 6):
t1, t2, t3, t4 = times(i1, inv_sub_key[i]), plus(i2, inv_sub_key[i+1]), plus(i3, inv_sub_key[i+2]), times(i4, inv_sub_key[i+3])
t5, t6 = xor(t1, t3), xor(t2, t4)
t7 = times(t5, inv_sub_key[i+4])
t8 = plus(t6, t7)
t9 = times(t8, inv_sub_key[i+5])
t10 = plus(t7, t9)

i1 = xor(t1, t9)
i2 = xor(t3, t9)
i3 = xor(t2, t10)
i4 = xor(t4, t10)

y1 = times(i1, inv_sub_key[48])
y2 = plus(i3, inv_sub_key[49]) # 注意:这里I3和I2是交换使用的,这是非标准部分
y3 = plus(i2, inv_sub_key[50]) # 注意:这里I3和I2是交换使用的,这是非标准部分
y4 = times(i4, inv_sub_key[51])

return (y1 << 48) | (y2 << 32) | (y3 << 16) | y4

if __name__ == "__main__":
part2 = bytes([0x5C, 0x2F, 0xD0, 0xEC, 0x82, 0x0E, 0x67, 0x57, 0x6A, 0x9F, 0x91, 0xF6, 0x95, 0xA4, 0xAC, 0x90])
key_data = [0x6C98B242, 0x78563412, 0x3E6A6D0D, 0xEFCDAB89] # 端序转换
keys_input = [val for k in key_data for val in [(k >> 16) & 0xFFFF, k & 0xFFFF]]

cipher1 = int.from_bytes(part2[0:8], 'big')
cipher2 = int.from_bytes(part2[8:16], 'big')

subkeys_get(keys_input)
inv_subkeys_get(sub_key)

plain1 = dencrypt_final(cipher1)
plain2 = dencrypt_final(cipher2)

p1_bytes, p2_bytes = plain1.to_bytes(8, 'big'), plain2.to_bytes(8, 'big')

result_bytes = p1_bytes + p2_bytes

hex_output = ' '.join(f'{b:02X}' for b in result_bytes)
print(f"Decrypted bytes (hex): {hex_output}")
try:
str_output = result_bytes.decode('utf-8')
print(f"Decrypted string : {str_output}")
except UnicodeDecodeError:
print(f"Decrypted string (raw) : {result_bytes!r}")

# Decrypted bytes (hex): 62 75 69 6C 64 5F 74 68 69 73 5F 62 6C 6F 63 6B
# Decrypted string : build_this_block

所以第一部分的 moshui_ 和第二部分的 build_this_block 拼接起来就是完整的 flag{moshui_build_this_block}

Dragon

附件是一个 .bc 文件,用 file Dragon.bc 查看文件类型
得到Dragon.bc: LLVM IR bitcode
继续往下编译 clang Dragon.bc -o Dragon
然后 ida 打开编译好的 Dragon 文件,main 函数
读取输入后进入一个加密函数(CRC 64),最后又字符串比较,动调提取出密文

1
2
3
4
5
6
7
8
9
10
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]

密文有 96 字节 -> 12 个 64 位整数,每两个字符为一组,生成一个 64 位的 CRC 值,与密文对比
因此可以得出输入是 24 个字符,需要爆破 12 组的 2 字符组合
加密函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 __fastcall sub_7FF6EA4B1000(char *a1, unsigned __int64 n2)
{
unsigned __int64 n8; // [rsp+0h] [rbp-28h]
unsigned __int64 n2_1; // [rsp+8h] [rbp-20h]
__int64 v5; // [rsp+10h] [rbp-18h]

v5 = -1; //0xFFFFFFFFFFFFFFFF
for ( n2_1 = 0; n2_1 < n2; ++n2_1 ) // n2=2
{
v5 ^= (unsigned __int64)(unsigned __int8)a1[n2_1] << 56;
for ( n8 = 0; n8 < 8; ++n8 ) //每个字节处理 8 位
{
if ( v5 >= 0 ) //相当于 v5 & (1 << 63) == 0
v5 *= 2;
else
v5 = (2 * v5) ^ 0x42F0E1EBA9EA3693LL;
}
}
return ~v5;
}

解密脚本:

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
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]
# print(len(data)) # 96
target =[
int.from_bytes(bytes(data[i:i+8]),'little',)
for i in range(0,96,8)
]

def crc64(enc):
crc=0xFFFFFFFFFFFFFFFF
for byte in enc:
crc ^= (byte << 56)
crc &= 0xFFFFFFFFFFFFFFFF
for _ in range(8):
if crc &(1 << 63):
crc = (crc << 1) ^ 0x42F0E1EBA9EA3693
else:
crc <<= 1
crc &= 0xFFFFFFFFFFFFFFFF
return crc ^ 0xFFFFFFFFFFFFFFFF #取反
#爆破
flag=[]
for i in target:
found =False
for j in range(0x10000):
byte0 = j & 0xFF #低字节
byte1 = (j >> 8) & 0xFF #高字节
temp = bytes([byte0,byte1])

crc_val = crc64(temp)
if crc_val == i:
flag.append(temp)
found = True
break

final =b''.join(flag).decode('ascii')

print(f"Flag is : {final}")

flag 为flag{LLVM_1s_Fun_Ri9h7?}

WARMUP

附件是一个 .vbs 文件,是一种包含用Visual Basic Scripting Edition编写的脚本代码的文本文件,不需要编译,直接由Windows Script Host (WSH)解释执行
在网上看到相关处理方式是:用 vscode 或者 其他文本编辑器打开,把开头的 Execute 改为 wscript.echo
直接运行即可得到源代码

Execute 是用来执行代码
wscript.echo 的作用是将一个或多个字符串作为输出打印到控制台(或者在图形界面中弹出一个对话框)

运行后果然看到了源代码

不难看出这是一个标准的 rc4 ,
密文是 “90df4407ee093d309098d85a42be57a2979f1e51463a31e8d15e2fac4e84ea0df622a55c4ddfb535ef3e51e8b2528b826d5347e165912e99118333151273cc3fa8b2b3b413cf2bdb1e8c9c52865efc095a8dd89b3b3cfbb200bbadbf4a6cd4”
密钥是
“rc4key”
解密脚本

def rc4_decrypt(ciphertext_hex, key_string):
 
    try:
        ciphertext = bytes.fromhex(ciphertext_hex)
    except ValueError:
        return "WRONG FORMAT"

    S = list(range(256))
    key_length = len(key_string)
    T = [ord(key_string[i % key_length]) for i in range(256)]
    
    j = 0
    for i in range(256):
        j = (j + S[i] + T[i]) % 256
        S[i], S[j] = S[j], S[i] 
        
    key_stream = []
    i = 0
    j = 0
    for byte in ciphertext:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]  
        t = (S[i] + S[j]) % 256
        key_stream.append(S[t])
        
    plaintext_bytes = bytearray()
    for i in range(len(ciphertext)):
        plaintext_bytes.append(ciphertext[i] ^ key_stream[i])
        
  
    return plaintext_bytes.decode('utf-8', errors='ignore')

ciphertext_given = "90df4407ee093d309098d85a42be57a2979f1e51463a31e8d15e2fac4e84ea0df622a55c4ddfb535ef3e51e8b2528b826d5347e165912e99118333151273cc3fa8b2b3b413cf2bdb1e8c9c52865efc095a8dd89b3b3cfbb200bbadbf4a6cd4"

key = "rc4key"

decrypted_text = rc4_decrypt(ciphertext_given, key)

print(f"Flag is: {decrypted_text}")

# Flag is: flag{We1c0me_t0_XYCTF_2025_reverse_ch@lleng3_by_th3_w@y_p3cd0wn's_chall_is_r3@lly_gr3@t_&_fuN!}

题目中说 flag为flag{}包含的内容进行md5加密并用XYCTF{}包含

所以最终的 flag 为 XYCTF{5f9f46c147645dd1e2c8044325d4f93c}


XYCTF2025 Reverse 方向复现
http://example.com/2025/07/20/XYCTF2025/
作者
Eleven
发布于
2025年7月20日
许可协议