2025 第二届"Parloo 杯"CTF 应急响应挑战赛 RE wp

PositionalXOR

根据题目描述可知加密算法就是xor

PositionalXOR

附件里就一个bin文件,010editor打开,数据提出来直接写脚本解密

PositionalXOR-1

1
2
3
4
5
enc = "qcoq~Vh{e~bccocH^@Lgt{gt|g"
enc_list = list(enc)
for i in range(len(enc_list)):
print(chr(ord(enc_list[i]) ^ (i + 1)), end="")
#palu{PosltionalXOR_sample}

PaluArray

UPX壳,并且好像有的标志位被删了,直接x64dbg手动脱壳,然后ida分析,根据提示字符串定位到程序的主要逻辑

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
// Hidden C++ exception states: #wind=2
struct CWnd *__fastcall sub_7FF61DC51DD8(CDialog *a1)
{
struct CWnd *result; // rax
struct CWnd *v3; // rdi
_QWORD *v4; // rax
void **v5; // rax
__int64 v6; // rax
void *v7; // rcx
_BYTE v8[8]; // [rsp+20h] [rbp-E0h] BYREF
_BYTE v9[40]; // [rsp+28h] [rbp-D8h] BYREF
void *Block; // [rsp+50h] [rbp-B0h] BYREF
char v11; // [rsp+58h] [rbp-A8h] BYREF
__int64 v12; // [rsp+E0h] [rbp-20h] BYREF
_BYTE v13[8]; // [rsp+E8h] [rbp-18h] BYREF
void *v14[3]; // [rsp+F0h] [rbp-10h] BYREF
unsigned __int64 n0xF; // [rsp+108h] [rbp+8h]

CDialog::OnOK(a1);
result = CWnd::GetDlgItem(a1, 1000);
v3 = result;
if ( result )
{
ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v12);
CWnd::GetWindowTextW(v3, &v12);
v4 = ATL::CSimpleStringT<wchar_t,1>::CSimpleStringT<wchar_t,1>(v8, &v12);
sub_7FF61DC51994(v13, v4);
if ( ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::Compare(v13, a1145141919810) )// "1145141919810"
{
CWnd::MessageBoxW(a1, aFailed, 0, 0); // "Failed"
}
else
{
CWnd::MessageBoxW(a1, aSuccess, 0, 0); // "Success"
v5 = sub_7FF61DC51F6C(&Block, v12);
sub_7FF61DC52118(v14, *v5);
if ( Block != &v11 )
free(Block);
v6 = sub_7FF61DC52244(v9, v14);
sub_7FF61DC51A48(v6);
if ( n0xF > 0xF )
{
v7 = v14[0];
if ( n0xF + 1 >= 0x1000 )
{
v7 = *(v14[0] - 1);
if ( (v14[0] - v7 - 8) > 0x1F )
invalid_parameter_noinfo_noreturn();
}
operator delete[](v7);
}
}
ATL::CSimpleStringT<wchar_t,1>::~CSimpleStringT<wchar_t,1>(v13);
return ATL::CSimpleStringT<wchar_t,1>::~CSimpleStringT<wchar_t,1>(&v12);
}
return result;
}

这里就是处理输入密文的函数,sub_7FF61DC51994函数对字符串的每个字符进行查找,在一个确定的table中

PaluArray

然后再看sub_7FF61DC51A48函数,有MD5,但好像是生成flag的逻辑,那么应该就是将上面解密出来的字符串输入进去,就会输出flag

解密脚本

1
2
3
4
table = "Palu_996!?"
index = list("1145141919810")
for i in range(13):
print(table[ord(index[i]) - ord('0')], end="")

得到:aa_9a_a?a?!aP

然后运行程序输入

PaluArray-1

PaluGOGOGO

go语言,ida打开分析

主要加密逻辑

PaluGOGOGO

红色部分是密文,绿色部分获取的value会传入到main_complexEncrypt参与flag的加密

PaluGOGOGO-1

加密逻辑如图,value就是在外面传进来的值,n10_4是index

解密脚本

1
2
3
4
enc = [0xbf,0xb1,0xbd,0xc7,0xce,0x96,0x80,0x98,0x82,0x9a,0x7f,0xaf,0xc1,0xb3,0xbf,0xc4,0xcd]
for i in range(len(enc)):
print(chr(enc[i] - 0x4f - (i % 5)), end="")
#palu{G0G0G0_palu}

PaluFlat

ida打开附件 发现密文

未命名-2

加密算法是sub_401550 跟进一下

未命名-3

主要逻辑就是 根据奇数偶数选择对应的密钥 输入跟密钥循环异或 半字节交换 减0x55 取反

直接对着写逆向脚本即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
key1 = b"flat"  
key2 = b"palu"
enc = [0x54, 0x84, 0x54, 0x44, 0xA4, 0xB2, 0x84, 0x54, 0x62, 0x32,
0x8F, 0x54, 0x62, 0xB2, 0x54, 0x03, 0x14, 0x80, 0x43]
flag = []
for i, c in enumerate(enc):
byte = ~c & 0xFF
byte = (byte + 85) % 256
byte = ((byte << 4) | (byte >> 4)) & 0xFF
if i % 2 == 0:
key = key2
else:
key = key1
k = key[i % len(key)]
flag.append(byte ^ k)
print(bytes(flag))
#b'palu{Fat_N0t_Flat!}'

Asymmetric

ida打开附件 发现65537 第一时间想到RSA

re1-3

密文在下边

re1-4

yafu分解一下n

re1-5

直接写脚本解rsa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import libnum  
from Crypto.Util.number import inverse
import gmpy2
from Crypto.Util.number import long_to_bytes
p = 3
q = 47
r = 2287
s = 3101092514893
m = 100000000000000003
n = 100000000000000106100000000000003093
phi = (p - 1) * (q - 1) * (r - 1) * (s - 1) * (m - 1)
e = 65537
d = inverse(e, phi)

c = 94846032130173601911230363560972235

m = pow(c, d, n)
print(long_to_bytes(m))
#b'palu{3a5Y_R$A}'

CatchPalu

ida打开附件 常规花 nop掉 进入main函数 有个hook函数 跟进

密文密钥如下

CatchPalu

跟进下面的函数 发现是个魔改的RC4

CatchPalu-1

写脚本解密即可

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
def KSA(key):  
S = list(range(256))
v5 = 0
for _ in range(3):
for k in range(256):
v5 = (key[k % len(key)] + v5 + S[k]) % 233
S[k], S[v5] = S[v5], S[k]
return S

def PRGA(S):
i, j = 0, 0
while True:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
yield K

def RC4(key, ciphertext):
cipher_bytes = bytes(ciphertext)
S = KSA(key)
keystream = PRGA(S)
return bytes([c ^ next(keystream) for c in cipher_bytes])

key = b'forpalu'
enc = [13, 176, 191, 10, 141, 47, 2, 56, 111, 25, 174, 153, 25, 199,
110, 247, 79, 203, 144, 78, 85, 142, 209, 16, 192]

flag = RC4(key, enc)
print(flag.decode())
#palu{G00d_P1au_Kn0w_H00K}

帕鲁迷宫

无语题 python的exe 3.11 解包反编译发现往下走才会给出完整地图 还有出口 直接手动走一下 得到

帕鲁迷宫

那么地图如下

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
################################  
#Y# # X#
# # ############# ### # ### ## #
# # # # # # # ##
# ##### ####### ### # ### ### ##
# # # # # # # # ##
### # ### ##### # ####### # ####
# # # # # # # # ##
# ##### # ### # # ### # ##### ##
# # # # # # ##
# ######### # # ############# ##
# # # # # # ##
### # # ######### # ######### ##
# # # # # # # ##
# # # ############# # ##### # ##
# # # # # # ##
#X ## ### ### # ##### # ########
# # # # # # # # ##
### ### ######### # # # # # # ##
# # # # # # # # # # # # ##
# # ### # # # # # # # ### # # ##
# # # # # # # # # # ##
# ### ### ########### # # # # ##
# # # # # # # # ##
# ##### # ######### # # ### # ##
# # # # # # # # ##
### # ####### ####### # # ### ##
# # # # # # # # ##
# ##### # # # # ##### ##### # ##
# # # # #
#X ############ X ########### X#
################################

搓个bfs跑一下最短路径

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
from collections import deque  

def parse_map(map_str):
map_data = map_str.strip().split('\n')
rows = len(map_data)
cols = len(map_data[0]) if rows > 0 else 0
start = None
exits = []
for y in range(rows):
for x in range(cols):
if map_data[y][x] == 'Y':
start = (y, x)
elif map_data[y][x] == 'X':
exits.append((y, x))
return map_data, start, exits


def bfs(start, targets, map_data):
rows = len(map_data)
cols = len(map_data[0])
visited = {}
queue = deque([(start[0], start[1])])
visited[(start[0], start[1])] = (None, None, 0)
directions = [(-1, 0, 'w'), (1, 0, 's'), (0, -1, 'a'), (0, 1, 'd')]
found_targets = []

while queue:
y, x = queue.popleft()
current_dist = visited[(y, x)][2]

if (y, x) in targets:
found_targets.append((y, x, current_dist))

for dy, dx, move in directions:
ny, nx = y + dy, x + dx
if 0 <= ny < rows and 0 <= nx < cols:
if map_data[ny][nx] != '#' and (ny, nx) not in visited:
visited[(ny, nx)] = (y, x, current_dist + 1)
queue.append((ny, nx))

if not found_targets:
return None, None
target = min(found_targets, key=lambda x: x[2])
path = []
current = (target[0], target[1])
while True:
prev_info = visited.get(current)
if prev_info is None or prev_info[0] is None:
break
py, px = prev_info[0], prev_info[1]
dy = current[0] - py
dx = current[1] - px
if dy == 1:
move = 's'
elif dy == -1:
move = 'w'
elif dx == 1:
move = 'd'
else:
move = 'a'
path.append(move)
current = (py, px)
path.reverse()
return ''.join(path), (target[0], target[1])


def find_shortest_path(map_str):
map_data, start, exits = parse_map(map_str)
current_pos = start
remaining_exits = set(exits)
total_path = []
path_details = [] # 存储每个阶段的路径详情

while remaining_exits:
path, exit_pos = bfs(current_pos, remaining_exits, map_data)
if not path:
break
total_path.append(path)

path_details.append({
'from': current_pos,
'to': exit_pos,
'path': path,
'length': len(path)
}) current_pos = exit_pos
remaining_exits.remove(exit_pos)

# 打印每个阶段的路径
for i, segment in enumerate(path_details):
print(f"阶段 {i + 1}: 从 {segment['from']} 到 {segment['to']}")
print(f"路径: {segment['path']}")
print(f"长度: {segment['length']}\n")

full_path = ''.join(total_path)
return full_path


map_str = """
################################
#Y# # X#
# # ############# ### # ### ## #
# # # # # # # ##
# ##### ####### ### # ### ### ##
# # # # # # # # ##
### # ### ##### # ####### # ####
# # # # # # # # ##
# ##### # ### # # ### # ##### ##
# # # # # # ##
# ######### # # ############# ##
# # # # # # ##
### # # ######### # ######### ##
# # # # # # # ##
# # # ############# # ##### # ##
# # # # # # ##
#X ## ### ### # ##### # ########
# # # # # # # # ##
### ### ######### # # # # # # ##
# # # # # # # # # # # # ##
# # ### # # # # # # # ### # # ##
# # # # # # # # # # ##
# ### ### ########### # # # # ##
# # # # # # # # ##
# ##### # ######### # # ### # ##
# # # # # # # # ##
### # ####### ####### # # ### ##
# # # # # # # # ##
# ##### # # # # ##### ##### # ##
# # # # #
#X ############ X ########### X#
################################
"""

path = find_shortest_path(map_str)
print(f"完整路径: {path}")
print(f"总长度: {len(path)}")
print(f"最后四步: {path[-4:] if len(path) >= 4 else path}")

跑出来不对 猜测有多解 经过手扣发现有的地方路径不唯一 具体如下

1
ssssddssddwwddwwddddddddssssssssddssaaaaaaaaaawwddddwwddwwaaaassaaaaaaaassddssssa(as,sa)(sd,ds)dssssddssddssaassddssaaaaa(as,sa)(wd,dw)dddddddwwddssddwwwwddddddwwaaaaaaaaaawwwwddssddwwddssddwwwwaawwddddwwwwddddddddddwwwwaawwddwwaawwdddaaassddssaassddssssssssaawwaaaaaassssssssssssssssaaaa(as,sa)(wd,dw)ddddddddddwwddss(ds,sd)

直接写脚本算一下符合条件的每一个的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
import re  
import hashlib
from itertools import product

path_str = (
"ssssddssddwwddwwddddddddssssssssddssaaaaaaaaaawwddddwwddwwaaaassaaaaaaaassddssssa(as,sa)(sd,ds)dssssddssddssaassddssaaaaa(as,sa)(wd,dw)dddddddwwddssddwwwwddddddwwaaaaaaaaaawwwwddssddwwddssddwwwwaawwddddwwwwddddddddddwwwwaawwddwwaawwdddaaassddssaassddssssssssaawwaaaaaassssssssssssssssaaaa(as,sa)(wd,dw)ddddddddddwwddss(ds,sd)"
)

pattern = re.compile(r'\(([^()]+)\)')
segments = []
last_index = 0

for match in pattern.finditer(path_str):
segments.append([path_str[last_index:match.start()]])
options = match.group(1).split(',')
segments.append(options)
last_index = match.end()

segments.append([path_str[last_index:]])

combinations = [''.join(p) for p in product(*segments)]

result = []
for path in combinations:
if path.endswith('ssds'):
md5_hash = hashlib.md5(path.encode()).hexdigest()
result.append((path, f'palu{{{md5_hash}}}'))

for path, md5 in result:
print(f"{md5}:\n{path}\n")

print(f"共匹配路径数量: {len(result)}")

ssssddssddwwddwwddddddddssssssssddssaaaaaaaaaawwddddwwddwwaaaassaaaaaaaassddssssasadsdssssddssddssaassddssaaaaasadwdddddddwwddssddwwwwddddddwwaaaaaaaaaawwwwddssddwwddssddwwwwaawwddddwwwwddddddddddwwwwaawwddwwaawwdddaaassddssaassddssssssssaawwaaaaaassssssssssssssssaaaasadwddddddddddwwddssds

flag:palu{990fd7773f450f1f13bf08a367fe95ea}

Checker

安卓 主要逻辑在so 扔ida里

安卓

跟进一下encrypto函数

安卓-1

加密主要是一个cbc模式的xtea 跟进一下初始化key的函数

安卓-2

主要是对key跟iv进行一个rc4的加密 rc4的key是DoNotHackMe

安卓-3

先解一下key跟iv

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
def KSA(key):  
S = list(range(256))
v5 = 0
for k in range(256):
v5 = (key[k % len(key)] + v5 + S[k]) % 256
S[k], S[v5] = S[v5], S[k]
return S


def PRGA(S):
i, j = 0, 0
while True:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
yield K


def RC4(key, ciphertext):
cipher_bytes = bytes(ciphertext)
S = KSA(key)
keystream = PRGA(S)
return bytes([c ^ next(keystream) for c in cipher_bytes])


key = b'DoNotHackMe'
key_enc = [0x99, 0xDD, 0x56, 0xFF, 0x6D, 0xD9, 0x55, 0x54, 0x42, 0x4D,
0x79, 0x1A, 0x34, 0xB7, 0x81, 0x2F]
iv_enc = [ 0x87, 0xC1, 0x56, 0xC0, 0x4C, 0xF4, 0x63, 0x4F]
key_xtea = RC4(key, key_enc)
iv = RC4(key, iv_enc)
print(' '.join(hex(x) for x in key_xtea))
print(' '.join(hex(y) for y in iv))

得到key:52756e74696d65537472696e67457874

​ iv:4c696e4b48405348

在解密cbc-xtea即可

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


def xtea_decrypt_block(cipher_block, key):
v5, v4 = struct.unpack('<2I', cipher_block)
key = struct.unpack('<4I', key)

sum_values = []
current_sum = 0
for i in range(32):
sum_values.append(current_sum)
key_idx = i % 4
current_sum += (key[key_idx] ^ i) - 0x61C88647

for i in reversed(range(32)):
sum_after = sum_values[i + 1] if i < 31 else current_sum

key_idx = (sum_after >> 11) & 3
key_val = key[key_idx]
v4 -= (((v5 << 4) ^ (v5 >> 5)) + v5) ^ (sum_after + key_val)
v4 &= 0xFFFFFFFF

sum_before = sum_values[i]
key_idx = sum_before & 3
key_val = key[key_idx]
v5 -= (((v4 << 4) ^ (v4 >> 5)) + v4) ^ (sum_before + key_val)
v5 &= 0xFFFFFFFF

return struct.pack('<2I', v5, v4)


def cbc_decrypt(ciphertext, key, iv):
blocks = [ciphertext[i:i + 8] for i in range(0, len(ciphertext), 8)]
prev_cipher = iv
plaintext = bytearray()

for block in blocks:
decrypted = xtea_decrypt_block(block, key)

plain_block = bytes([d ^ p for d, p in zip(decrypted, prev_cipher)])
plaintext.extend(plain_block)

prev_cipher = block

return bytes(plaintext)

if __name__ == "__main__":
encrypted_data = bytes.fromhex("A90B5C1CA34188CA66D9771D78038E7ABA7BD490CD500783414A829C791DCC6F9D2F392DA2DA831B")
secret_key = bytes.fromhex("52756e74696d65537472696e67457874")
initialization_vector = bytes.fromhex("4c696e4b48405348")

try:
decrypted = cbc_decrypt(encrypted_data, secret_key, initialization_vector)
print(f"解密结果(HEX): {decrypted.hex()}")
print(f"ASCII表示: {decrypted.decode('utf-8', errors='replace')}")
except Exception as e:
print(f"解密失败: {str(e)}")
#palu{thiS_T1Me_it_seeM5_tO_8e_ReAl_te@}