Parloo杯2025 Reverse wp
逆向ak,但感觉自己代码阅读能力还是很差,耐心也不够,很多地方都是借助AI的
ps:不知道为什么图好像不见了,懒得再找了
encrypted
简单异或,用前缀palu{猜一下异或数字
a='qcoq~Vh{e~bccocH^@Lgt{gtg'
for i in range(len(a)):
print(chr(ord(a[i])^(i+1)),end='')
# palu{PosltionalXOR_sample}
CatchPalu
这一段hook了messageboxa的函数地址,在这段代码之后调用的messageboxa都会变成sub_401360
动态调试看看
这里flag输入要输入附件里面的,也就是palu{P1au_D0nt_Bel1eve},才能进入下面的messageboxa逻辑
进去就可以发现真正的比对函数,这里给硬编码的v8加密,似乎是魔改rc4,但不需要编写代码,直接进函数sub_CE1270,等函数走完看v8内存就行
ps:这里我做题的时候密钥被改变了,我当时手动修改回了forpalu,但这里复现写wp的时候密钥却是正确的,不知道什么情况…
Asymmetric
又是一个go,长得很丑得耐心点看
动态调试慢慢分析,可以发现其实是一个rsa,数据都给了,但是要因式分解,随便找个网站分一下
from Crypto.Util.number import inverse
N_str = "100000000000000106100000000000003093"
e = 65537
C_str = "94846032130173601911230363560972235"
N = int(N_str)
C = int(C_str)
p1 = 3
p2 = 47
p3 = 2287
p4 = 3101092514893
p5 = 100000000000000003
phi_N = (p1 - 1) * (p2 - 1) * (p3 - 1) * (p4 - 1) * (p5 - 1)
d = inverse(e, phi_N)
M = pow(C, d, N)
M_str = str(M)
print("计算出的输入字符串是:", M_str)
# 2279348573780051194351488552157565
a=2279348573780051194351488552157565
print(a.to_bytes(16,'big'))
# palu{3a5Y_R$A}
PaluArray
简单的crackme,只不过函数长得很恶心
先根据字符串定位到主函数逻辑
其实就是要让v13=1145141919810
至于v13的生成逻辑,要看sub_7FF6987E1994
简单来说,就是根据传入的v4,在off_7FF6987E9B48里找索引值
动态调试可以发现,v4其实就是我们输入的字符串,那么先解出来我们输入的字符串是什么
a=[0x50,0x61,0x6C,0x75,0x5F,0x39,0x39,0x36,0x21,0x3F]
b='1145141919810'
for i in b:
print(chr(a[int(i,10)]),end='')
# aa_9a_a?a?!aP
把这个输入就得到flag了
PaluFlat
.com文件,用7zip解压,解压得到一个1g的文件,有点吓人。。。
不过主要逻辑不难
我们的输入经过函数401550加密,再与硬编码的密文比对即可
sub_401550被控制流平坦化混淆,有些难看
但我把这个函数丢给ai它直接就分析出来了…运气还行
enc=[0x54,0x84,0x54,0x44,0xA4,0xB2,0x84,0x54,0x62,0x32,0x8F,0x54,0x62,0xB2,0x54,0x3,0x14,0x80,0x43]
def decrypt(ciphertext):
key1 = "palu"
key2 = "flat"
plaintext = []
for i, c in enumerate(ciphertext):
# 选择密钥
key = key1 if i % 2 == 0 else key2
k = key[i % len(key)]
# 解密步骤
v11 = ~c & 0xFF # 取反(注意处理符号)
v11 = (v11 + 85) & 0xFF # 加 85
v11 = ((v11 << 4) (v11 >> 4)) & 0xFF # 交换高低四位
plaintext.append(chr(v11 ^ ord(k))) # 异或密钥
return ''.join(plaintext)
print(decrypt(enc))
# palu{Fat_N0t_Flat!}
palugogogo
简单的go逆向,我用的ida9版本可以直接显示函数符号,不知道低版本的怎么样,go逆向就是函数长得太丑了很难看,但动态调试分析并不难
这里一开始有个反调试,hook掉返回值过掉就行
具体比对逻辑在下面
输入的flag经过encrypt这个函数加密,再与硬编码的密文比对,其中value与我们输入的无关,它是程序内部自生成的密钥,直接动调拿就可以,值是0x4F
至于这个加密也是耐心动调分析就能出,一开始我测试了一下,发现是逐字节加密,加密逻辑如下
a='palu{asdasdsajkhfjasf}'
value=0x4f
for i in range(len(a)):
print(hex(ord(a[i])+value+(i%5)),end=',')
照着解密就可以
enc=[0xbf,0xb1,0xbd,0xc7,0xce,0x96,0x80,0x98,0x82,0x9a,0x7f,0xaf,0xc1,0xb3,0xbf,0xc4,0xcd]
value=0x4f
for i in range(len(enc)):
print(chr(enc[i]-value-(i%5)),end='')
# palu{G0G0G0_palu}
ParlooChecker
个人感觉最难的一道题,太考验耐心了
apk逆向,但jadx无法分析,转到jeb
主要加密比对逻辑被藏在本地方法parloo里面,解压apk看一下so文件,so文件的导出表只有一个函数,就是oncreat3
函数没任何符号,动调so文件又有些麻烦,只能一个个看了,前面的没什么用,直接看else部分
sub_29390、sub_291D0两个函数进行初始化,这部分使用rc4创建了后面加密要用的密钥,后面有很多函数都不用过多分析,大多起到混淆、分配内存或是格式化字符串作用
sub_28D30里面藏了一个tea
大致逻辑如上,主要调用rc4生成密钥之类的,再调用tea加密原文,大多数函数都没有任何作用
ps:如果不用AI我可能要花更多时间。。。
import struct
import sys
a=[0x99,0xDD,0x56,0xFF,0x6D,0xD9,0x55,0x54,0x42,0x4D,0x79,0x1A,0x34,0xB7,0x81,0x2F]
UNK_10170 = b''
for i in a:
UNK_10170+=i.to_bytes(1)
b=[0x87,0xC1,0x56,0xC0,0x4C,0xF4,0x63,0x4F]
UNK_10180 = b''
for i in b:
UNK_10180+=i.to_bytes(1)
enc=[0xA9,0xB,0x5C,0x1C,0xA3,0x41,0x88,0xCA,0x66,0xD9,0x77,0x1D,0x78,0x3,0x8E,0x7A,0xBA,0x7B,0xD4,0x90,0xCD,0x50,0x7,0x83,0x41,0x4A,0x82,0x9C,0x79,0x1D,0xCC,0x6F,0x9D,0x2F,0x39,0x2D,0xA2,0xDA,0x83,0x1B]
TARGET_CIPHERTEXT = b''
for i in enc:
TARGET_CIPHERTEXT+=i.to_bytes(1)
def rc4_ksa(key):
"""RC4 Key-Scheduling Algorithm (based on sub_2C740)"""S = list(range(256))
j = 0
key_bytes = key
key_len = len(key_bytes)
for i in range(256):
j = (j + S[i] + key_bytes[i % key_len]) % 256
S[i], S[j] = S[j], S[i]
return S
def rc4_prga_sub_293E0(initial_S, input_data):
"""RC4 Pseudo-Random Generation Algorithm (based on sub_293E0 loop)"""S = list(initial_S)
output_data = bytearray(len(input_data))
v8 = 0
v7 = 0
for k in range(len(input_data)):
v8 = (v8 + 1) % 256
v7 = (v7 + S[v8]) % 256
S[v8], S[v7] = S[v7], S[v8]
keystream_index = (S[v7] + S[v8]) % 256
keystream_byte = S[keystream_index]
output_data[k] = keystream_byte ^ input_data[k]
return bytes(output_data)
XTEA_DELTA_CONSTANT = 1640531527 # 0x61C88647
def calculate_encryption_v4_sequence(key_bytes):
"""计算加密轮次中 v4 值的序列。"""v4_sequence = [0]
v4_current = 0
k = [
struct.unpack('<I', key_bytes[i*4 : i*4+4])[0]
for i in range(4)
]
for round_index in range(32):
k2_index = round_index & 3
k2 = k[k2_index]
v4_update_term = (round_index ^ k2) - XTEA_DELTA_CONSTANT
v4_current = (v4_current + v4_update_term) & 0xFFFFFFFF
v4_sequence.append(v4_current)
return v4_sequence
def decrypt_block_round(v6_end, v5_end, v4_start_of_round, v4_end_of_round, round_index, key_bytes):
"""解密定制 XTEA 变种的一轮。"""v6_end = v6_end & 0xFFFFFFFF
v5_end = v5_end & 0xFFFFFFFF
v4_start_of_round = v4_start_of_round & 0xFFFFFFFF
v4_end_of_round = v4_end_of_round & 0xFFFFFFFF
k = [
struct.unpack('<I', key_bytes[i*4 : i*4+4])[0]
for i in range(4)
]
k3_index = (v4_end_of_round >> 11) & 3
k3 = k[k3_index]
intermediate_term2 = v6_end + ((v6_end >> 5) ^ (v6_end << 4))
term2 = (k3 + v4_end_of_round) ^ intermediate_term2
v5_start = (v5_end - term2) & 0xFFFFFFFF
k1_index = v4_start_of_round & 3
k1 = k[k1_index]
intermediate_term1 = v5_start + ((v5_start >> 5) ^ (v5_start << 4))
term1 = (k1 + v4_start_of_round) ^ intermediate_term1
v6_start = (v6_end - term1) & 0xFFFFFFFF
return (v6_start, v5_start)
def cbc_decrypt(ciphertext, key, iv):
"""使用定制 XTEA 变种以 CBC 模式解密密文。"""block_size = 8 # 字节
if len(ciphertext) % block_size != 0:
print(f"错误:密文长度 ({len(ciphertext)}) 不是分组大小 ({block_size}) 的倍数。无法执行 CBC 解密。")
sys.exit(1)
v4_sequence = calculate_encryption_v4_sequence(key)
plaintext_bytes = bytearray()
previous_ciphertext_block = iv
for i in range(0, len(ciphertext), block_size):
current_ciphertext_block = ciphertext[i : i + block_size]
c0, c1 = struct.unpack('<II', current_ciphertext_block)
decrypted_block_state = (c0, c1)
for reverse_round_index in range(31, -1, -1):
v4_start = v4_sequence[reverse_round_index]
v4_end = v4_sequence[reverse_round_index + 1]
decrypted_block_state = decrypt_block_round(
decrypted_block_state[0], decrypted_block_state[1],
v4_start, v4_end,
reverse_round_index,
key
)
p_prime_bytes = struct.pack('<II', decrypted_block_state[0], decrypted_block_state[1])
plaintext_block = bytes([
p_prime_bytes[j] ^ previous_ciphertext_block[j] for j in range(block_size)
])
plaintext_bytes.extend(plaintext_block)
previous_ciphertext_block = current_ciphertext_block
return bytes(plaintext_bytes)
if not UNK_10170 or len(UNK_10170) != 16:
print("错误:请提供 UNK_10170 的 16 字节数据,需从 SO 文件中提取。")
elif not UNK_10180 or len(UNK_10180) != 8:
print("错误:请提供 UNK_10180 的 8 字节数据,需从 SO 文件中提取。")
elif not TARGET_CIPHERTEXT or len(TARGET_CIPHERTEXT) != 40: # <-- 检查长度为 40 字节
print(f"错误:请提供 TARGET_CIPHERTEXT 的 **完整的 40 字节** 数据,需从 SO 文件中提取。")
print(f"(当前提供的长度为 {len(TARGET_CIPHERTEXT)})")
else:
print("占位符数据似乎已提供。尝试解密...")
rc4_key_material = b"DoNotHackMe"
S_box_for_73040 = rc4_ksa(rc4_key_material)
KEY_QWORD_73040 = rc4_prga_sub_293E0(list(S_box_for_73040), UNK_10170)
S_box_for_73050 = rc4_ksa(rc4_key_material)
KEY_QWORD_73050 = rc4_prga_sub_293E0(list(S_box_for_73050), UNK_10180)
print(f"派生出的加密密钥 (qword_73040): {KEY_QWORD_73040.hex()}")
print(f"派生出的 IV (qword_73050): {KEY_QWORD_73050.hex()}")
print(f"目标密文 (40字节): {TARGET_CIPHERTEXT.hex()}")
decrypted_padded_plaintext = cbc_decrypt(TARGET_CIPHERTEXT, KEY_QWORD_73040, KEY_QWORD_73050)
plaintext = decrypted_padded_plaintext.rstrip(b'\x00')
print(f"\n解密后的填充明文 (40 字节): {decrypted_padded_plaintext.hex()}") # <-- 输出提示长度为 40
try:
flag = plaintext.decode('utf-8')
print(f"恢复的 Flag (解码为 UTF-8): {flag}")
except UnicodeDecodeError:
print(f"无法将恢复的明文解码为 UTF-8。")
print(f"恢复的明文字节 (十六进制): {plaintext.hex()}")
except Exception as e:
print(f"在最终处理过程中发生未知错误: {e}")
# palu{thiS_T1Me_it_seeM5_tO_8e_ReAl_te@}
Game
迷宫,bfs求解最短路径
但这道题明显多解,我最开始写的那个代码死活不对(其实也是AI给的)。。。
然后叫ai给我出了一个全解代码(一开始是128个答案,但题目后面给的hint把范围缩小到64个了)
import collections
import itertools
import hashlib
# Keep the original BFS to get distances, but modify it slightly or create a new one
# to just compute the distance grid efficiently.
def bfs_get_dist_grid(grid, start):
""" 使用 BFS 计算从 start 到所有可达点的最短距离,并返回距离网格。 """rows = len(grid)
cols = len(grid[0])
dist = [[float('inf') for _ in range(cols)] for _ in range(rows)] # 使用 inf 表示不可达或未访问
queue = collections.deque([(start[0], start[1])]) # (row, col)
dist[start[0]][start[1]] = 0
dr = [-1, 1, 0, 0] # 方向:上, 下, 左, 右
dc = [0, 0, -1, 1]
# 可行走的字符集合 (保持与之前一致)
walkable_chars = {'0', ' ', 'X', 'Y'}
while queue:
r, c = queue.popleft()
for i in range(4):
nr, nc = r + dr[i], c + dc[i]
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != '#' and dist[nr][nc] == float('inf'):
dist[nr][nc] = dist[r][c] + 1
queue.append((nr, nc))
return dist # 返回完整的距离网格
def get_all_shortest_paths(grid, start, end, dist_grid):
""" 给定距离网格,递归回溯从 start 到 end 的所有最短路径。 """if dist_grid[end[0]][end[1]] == float('inf'):
return [] # 如果终点不可达
all_paths = []
# 递归回溯函数
def collect_paths(current_node, path_so_far):
r, c = current_node
if (r, c) == start:
all_paths.append(list(reversed(path_so_far + [current_node]))) # 到达起点,添加完整路径 (反转)
return
# 探索邻居,只回溯到距离小 1 的邻居 (即在最短路径上)
dr = [-1, 1, 0, 0]
dc = [0, 0, -1, 1]
for i in range(4):
pr, pc = r + dr[i], c + dc[i] # Parent row/col
# 检查边界
if 0 <= pr < len(grid) and 0 <= pc < len(grid[0]):
# 确保是从距离小 1 的点过来的
if dist_grid[r][c] == dist_grid[pr][pc] + 1:
# 检查这个点不是障碍
if grid[pr][pc] != '#':
collect_paths((pr, pc), path_so_far + [current_node]) # 递归回溯
# 从终点开始回溯
collect_paths(end, [])
return all_paths # 返回所有找到的最短路径坐标列表
def coords_to_wasd(path_coords_list):
""" 将坐标路径列表转换为 WASD 字符串。 (与之前相同) """wasd_string = ""
for i in range(len(path_coords_list) - 1):
r1, c1 = path_coords_list[i]
r2, c2 = path_coords_list[i+1]
dr = r2 - r1
dc = c2 - c1
if dr == 1:
wasd_string += 'S' # 向下
elif dr == -1:
wasd_string += 'W' # 向上
elif dc == 1:
wasd_string += 'D' # 向右
elif dc == -1:
wasd_string += 'A' # 向左
return wasd_string
# 您提供的已经替换空格的迷宫地图字符串
maze_string_0_filled = """
##############################0#
#Y#0000000000000000000#0000000X
#0#0#############0###0#0###0##0#
#0#000#000000000#0#000#000#000##
#0#####0#######0###0#0###0###0##
#000#000#00000#0#000#000#0#000##
###0#0###0#####0#0#######0#0####
#0#000#0#00000#0#00000#000#000##
#0#####0#0###0#0#0###0#0#####0##
#000000000#000#0#000#0000000#0##
#0#########0#0#0#############0##
#000#0#00000#0#000#00000000000##
###0#0#0#########0#0#########0##
#0#0#0#00000000000#0#0000000#0##
#0#0#0#############0#0#####0#0##
#000#0000000#0000000#0#000#000##
0X0##0###0###0#0#####0#0########
#000#000#00000#000#0#0#000#000##
###0###0#########0#0#0#0#0#0#0##
#0#0#000#000#000#0#0#0#0#0#0#0##
#0#0###0#0#0#0#0#0#0#0###0#0#0##
#0#000#0#0#000#000#000#000#0#0##
#0###0###0###########0#0#0#0#0##
#000#000#00000000000#0#0#0#0#0##
#0#####0#0#########0#0#0###0#0##
#000#000#000#0000000#0#0#000#0##
###0#0#######0#######0#0#0###0##
#000#000#000#0#00000#0#000#000##
#0#####0#0#0#0#0#####0#####0#0##
#000000000#000#0000000000000#00#
0X0############0X0###########0X
#0##############0#############0#
"""
# --- 解析迷宫字符串到网格 ---
lines = maze_string_0_filled.strip().split('\n')
grid = []
expected_cols = 32
for i, line in enumerate(lines):
if len(line) < expected_cols:
processed_line = line.ljust(expected_cols, '0')
elif len(line) > expected_cols:
processed_line = line[:expected_cols]
else:
processed_line = line
grid.append(list(processed_line))
rows = len(grid)
cols = len(grid[0]) if grid else 0
print(f"解析后的迷宫尺寸: {rows}x{cols}")
# 在网格中找到起始点 'Y' 和所有出口 'X' 的坐标
start_coord = None
exit_coords = []
for r in range(rows):
for c in range(cols):
if grid[r][c] == 'Y':
start_coord = (r, c)
elif grid[r][c] == 'X':
exit_coords.append((r, c))
print(f"在地图中找到的起始点 Y: {start_coord}")
print(f"在地图中找到的出口点 X: {exit_coords} (共 {len(exit_coords)} 个)")
# 在继续之前,验证是否找到一个 Y 和 5 个 X
if start_coord is None:
print("\n错误:未在地图中找到起始点 Y。")
elif len(exit_coords) != 5:
print(f"\n错误:在地图中找到的出口数量不为 5 个,而是 {len(exit_coords)} 个。无法进行计算。")
else:
print("\n成功在地图中找到所有关键点 (1个 Y, 5个 X)。正在计算关键点之间的最短路径距离...")
# 预先计算所有关键点对之间的最短距离
points = [start_coord] + exit_coords
dist_data = {} # 存储 ((p1_r, p1_c), (p2_r, p2_c)): distance
# 为了获取所有路径,我们需要从每个起点运行 BFS 来获取距离网格
# 存储从每个关键点出发的距离网格
dist_grids = {}
for p in points:
dist_grids[p] = bfs_get_dist_grid(grid, p)
# 填充 dist_data 使用计算好的距离网格
for i in range(len(points)):
for j in range(i + 1, len(points)):
p1 = points[i]
p2 = points[j]
# 从 p1 出发的距离网格中查找 p2 的距离
dist = dist_grids[p1][p2[0]][p2[1]]
if dist != float('inf'):
dist_data[(p1, p2)] = dist
dist_data[(p2, p1)] = dist # 距离是双向的
# 找到访问所有出口的最短总路径 (从 Y 开始,遍历所有 X)
min_total_dist = float('inf')
optimal_permutations = [] # 存储所有达到最小距离的出口访问顺序
print("\n正在查找访问所有出口的最短总距离...")
for perm in itertools.permutations(exit_coords):
current_total_dist = 0
current_point = start_coord
valid_permutation = True
# 计算当前排列顺序的总距离
for next_point in perm:
pair = (current_point, next_point)
if pair not in dist_data:
valid_permutation = False
break # 某段不可达
current_total_dist += dist_data[pair]
current_point = next_point
if valid_permutation:
if current_total_dist < min_total_dist:
min_total_dist = current_total_dist
optimal_permutations = [perm] # 找到更短的,清空并记录新的
elif current_total_dist == min_total_dist:
optimal_permutations.append(perm) # 找到同样短的,添加记录
print(f"\n计算出的访问所有出口的最短总路径距离 (从 Y 开始): {min_total_dist} 步。")
print(f"提示的最短路径距离: 290 步。")
if min_total_dist == 290:
print("计算结果与提示相符!正在生成所有最短路径的 WASD 序列...")
all_shortest_wasd_paths = set() # 使用集合存储唯一的 WASD 序列 (大写)
# 现在为每个达到最短总距离的排列组合生成所有可能的路径
for opt_perm in optimal_permutations:
segment_paths_lists = [] # 存储每个路径段的所有最短路径列表
current_point = start_coord
# 对于排列中的每个段 (Y -> X1, X1 -> X2, ...)
for next_point in opt_perm:
# 从当前点的距离网格中获取该段的所有最短路径
segment_dist_grid = dist_grids[current_point]
# 调用新的函数获取所有路径
all_segment_paths = get_all_shortest_paths(grid, current_point, next_point, segment_dist_grid)
if not all_segment_paths:
segment_paths_lists = []
break
segment_paths_lists.append(all_segment_paths)
current_point = next_point
if segment_paths_lists:
# 使用 itertools.product 组合所有路径段的所有可能组合
for path_combination in itertools.product(*segment_paths_lists):
combined_path_coords = []
for i, segment_path in enumerate(path_combination):
if i == 0:
combined_path_coords.extend(segment_path)
else:
combined_path_coords.extend(segment_path[1:])
wasd_path = coords_to_wasd(combined_path_coords) # 生成大写 WASD 路径
all_shortest_wasd_paths.add(wasd_path)
# --- 筛选路径 (最后四步 SS DS), 转小写, 计算 MD5 ---
filtered_md5s = []
filter_suffix_uppercase = "SSDS" # 筛选条件 (大写)
print(f"\n过滤路径:寻找最后四步为 '{filter_suffix_uppercase}' 的路径...")
# 遍历所有找到的最短路径 (大写)
for wasd_path_uppercase in all_shortest_wasd_paths:
# 检查路径是否足够长且以指定的后缀结束
if len(wasd_path_uppercase) >= len(filter_suffix_uppercase) and wasd_path_uppercase.endswith(filter_suffix_uppercase):
# 将符合条件的路径转为小写
wasd_path_lowercase = wasd_path_uppercase.lower()
# 计算小写路径的 MD5 哈希值
md5_hash = hashlib.md5(wasd_path_lowercase.encode('utf-8')).hexdigest()
filtered_md5s.append(md5_hash)
# 排序筛选后的哈希值以便输出顺序稳定
sorted_filtered_md5s = sorted(filtered_md5s)
print(f"\n共找到 {len(sorted_filtered_md5s)} 条长度为 {min_total_dist} 且最后四步为 '{filter_suffix_uppercase}' 的唯一最短路径。")
print("这些路径(转为小写后)的 MD5 哈希值如下:")
for i, md5_hash in enumerate(sorted_filtered_md5s):
print(f"Filtered Path {i+1} MD5: {md5_hash}")
print("\n对应的达到最短总距离的出口访问顺序 (从 Y 出发) 可能有:")
point_names = {start_coord: 'Y'}
for i, coord in enumerate(exit_coords):
point_names[coord] = f'X{i+1}'
for i, opt_perm in enumerate(optimal_permutations):
path_points_sequence = [start_coord] + list(opt_perm)
print(f"顺序 {i+1}: " + " -> ".join([f"{point_names.get(c, str(c))}{c}" for c in path_points_sequence]))
else:
print("计算结果与提示不符,请检查地图、坐标或算法实现。")
print("\n未能找到访问所有出口的有效路径或最短距离不为290。")
一个个试,试到第41个对了
palu{990fd7773f450f1f13bf08a367fe95ea}