WHUCTF-2025 RE方向全题解#
签到#
打开发现是弗吉尼亚密码,直接解密即可。
WHUCTF{welcome_to_reverse-kep}.exe
ezbase#
发现密文 d2h1Y3RmezZac2U2NF8xc192ZXJ5XzNac3ktZWtlfQ==
猜测base64,发现表都没换
坏了,想简单了,换表了,难度还是太高了(
aBCDEFGHIJKLMNOPQRSTUVWXYZAbcdefghijklmnoqprstuvwxyz0123456789+/
得:
whuctf{6@se64_1s_very_3@sy-eke}.exe
太空逃亡#
主函数是一个巨简单的验证函数,关键的加密逻辑在sub_4015F8。检验得其把用户输入当作一串方向键 W/A/S/D(分别对应上、左、下、右),在一个 16×16 的网格(线性数组 byte_4A3020)上沿直线滑动:每次沿某方向一直前进直到遇到一个非 0的格(障碍/节点);不能越界、不能连续输入相同方向,目标是最终抵达数组中值为 ’!’(33)的格子。
迷宫地图 (0=路径, !=终点, 其他=障碍):
. . . . # . . . . . . . # # # #
. . . # # . . . # # . . . . . .
. . . . . . . . # # . # # . . .
. . . # . # # . # # . # # . . .
. . . # . # # . # # . # . . . .
. . # # . # # . . . . # . . . .
. . . . . # # . . . . # . . . .
# . . . . . . . . . . . . . . .
. . . . . . . . . . . . # . . .
. . . . . . # # # . . . # . . .
. . . . . . . # # # . . . . # #
. . . # . # . # . # . . . . # .
. . . . . . . . . . . . . . # #
# # # # . # . # . # . . . . # .
. # . # . # . # . # . . . . # .
. # . # . # . # . # ! . . . # # plaintext值得注意的是!这是一个冰面迷宫,左右移会直接移到可移动区域的边缘。
写出解密脚本即可:
import hashlib
from collections import deque
def parse_maze_data():
"""解析IDA风格的迷宫数据"""
# 解析重复计数语法
def parse_dup(part):
part = part.strip()
if 'dup(' in part:
count_str, value_str = part.split('dup(')
count_str = count_str.strip()
value_str = value_str.strip(')')
# 处理十六进制计数 (如0Eh, 1Bh等)
if count_str.endswith('h'):
count = int(count_str[:-1], 16)
else:
count = int(count_str)
# 处理十六进制值
if value_str.endswith('h'):
value = int(value_str[:-1], 16)
else:
value = int(value_str)
return [value] * count
else:
# 处理普通值
if part.endswith('h'):
return [int(part[:-1], 16)]
else:
return [int(part)]
# 原始数据
raw_data = [
"4 dup(0), 23h, 7 dup(0), 4 dup(23h), 3 dup(0), 2 dup(23h)",
"3 dup(0), 2 dup(4Fh), 0Eh dup(0), 2 dup(4Fh), 0, 2 dup(50h)",
"6 dup(0), 4Ch, 0, 2 dup(4Fh), 0, 2 dup(4Fh), 0, 2 dup(50h)",
"6 dup(0), 4Ch, 0, 2 dup(4Fh), 0, 2 dup(4Fh), 0, 50h",
"6 dup(0), 2 dup(4Ch), 0, 2 dup(4Fh), 4 dup(0), 50h",
"9 dup(0), 2 dup(4Fh), 4 dup(0), 50h, 4 dup(0), 23h",
"1Bh dup(0), 23h, 9 dup(0), 3 dup(4Dh), 3 dup(0), 23h",
"0Ah dup(0), 3 dup(4Dh), 4 dup(0), 2 dup(45h), 3 dup(0)",
"30h, 0, 4Dh, 0, 4Dh, 0, 4Dh, 4 dup(0), 45h, 0Fh dup(0)",
"2 dup(45h), 3 dup(54h), 49h, 0, 4Dh, 0, 4Dh, 0, 4Dh",
"4 dup(0), 45h, 2 dup(0), 54h, 0, 49h, 0, 4Dh, 0, 4Dh",
"0, 4Dh, 4 dup(0), 45h, 2 dup(0), 54h, 0, 49h, 0, 4Dh",
"0, 4Dh, 0, 4Dh, 21h, 3 dup(0), 2 dup(45h)"
]
maze = []
for line in raw_data:
parts = line.split(',')
for part in parts:
part = part.strip()
if part:
maze.extend(parse_dup(part))
return maze
def solve_maze(maze, start, end):
"""使用BFS算法求解迷宫最短路径"""
rows, cols = 16, 16
directions = [(0, 1, 'D'), (1, 0, 'S'), (0, -1, 'A'), (-1, 0, 'W')]
# 将一维数组转换为二维网格
grid = [[maze[i * cols + j] for j in range(cols)] for i in range(rows)]
# BFS初始化
queue = deque([(start, [])])
visited = set([start])
while queue:
(x, y), path = queue.popleft()
# 到达终点
if (x, y) == end:
return path
# 探索四个方向
for dx, dy, direction in directions:
nx, ny = x + dx, y + dy
# 检查边界和可通行性
if 0 <= nx < rows and 0 <= ny < cols:
if (nx, ny) not in visited and (grid[nx][ny] == 0 or grid[nx][ny] == 0x21):
visited.add((nx, ny))
queue.append(((nx, ny), path + [direction]))
return None # 无解
def visualize_maze(maze):
"""可视化迷宫"""
rows, cols = 16, 16
grid = [[maze[i * cols + j] for j in range(cols)] for i in range(rows)]
print("迷宫地图 (0=路径, !=终点, 其他=障碍):")
for i in range(rows):
for j in range(cols):
if grid[i][j] == 0:
print('.', end=' ')
elif grid[i][j] == 0x21:
print('!', end=' ')
else:
print('#', end=' ')
print()
def main():
# 解析迷宫数据
print("正在解析迷宫数据...")
maze = parse_maze_data()
# 检查迷宫大小
print(f"迷宫大小: {len(maze)} 字节")
# 可视化迷宫
visualize_maze(maze)
# 设置起点和终点
start = (0, 0)
end = (15, 15) # 终点在右下角
print(f"\n起点: {start}")
print(f"终点: {end}")
# 求解迷宫
print("\n正在求解迷宫...")
path = solve_maze(maze, start, end)
if path:
path_str = ''.join(path)
print(f"找到路径! 长度: {len(path)}")
print(f"WASD序列: {path_str}")
# 计算MD5
md5_hash = hashlib.md5(path_str.encode()).hexdigest()
print(f"MD5哈希: {md5_hash}")
# 构造flag
flag = f"WHUCTF{{{md5_hash}}}"
print(f"\nFLAG: {flag}")
else:
print("未找到路径!")
if __name__ == "__main__":
main()pythonTry to find me#
打开IDA一片茫然,啥都找不到。于是Shift + F12查字符串发现了:
strcpy((char *)(v0 + 40), "Software\\flag");
strcpy((char *)(v0 + 84), "flag");c找到了程序逻辑的尾巴:
这里会把解密后的目标字符串写入注册表的HKEY_CURRENT_USER\Software\flag,然后残忍地删掉。
不行,不能让它删掉(悲)
于是我们patch掉这两个RegDeleteKey的调用,再次运行程序。
注:本题的加密是曾经书记出的困难题,所以别想着静调了~
What can you find#
这题可以当作一道misc去做:
> binwalk -e ./what_can_you_find.3dsxbash得一张png,打开有:

这其实是非预期,下面介绍一种比较简洁的预期解法。
首先,我们了解到3DSX是ELF的变体,因此有映射区段并反编译的可能。通过查找Github已有的开源库我们发现了 这个项目 ↗,这里作者其实已经对IDA 7以上的版本做了适配,我进行了少量修改使它符合IDA 9的标准,但不修改也没大问题,可以直接用。放入~/.idapro/loaders即可:

现在,我们就可以像处理一个正常的ELF可执行文件那样去处理它了。先查找字符串可以发现如上的逆天言论,跳转找到主函数:

这里是一个输入密码并显示图片的简单程序,因此我们要从中提取出图片文件。此处像上面非预期一样binwalk即可。
好多文件#
好多文件!!!
但是没关系,观察Plugins发现,83和84单独分离出了自己的dll,猜测83和84是节目效果。
打开后发现是盘子(小)小故事,笑死了。
打开exe文件,太乱了,拖一下列表发现主要函数:
// Hidden C++ exception states: #wind=2
HBRUSH __fastcall WndProc(HWND hWnd, UINT msg, HDC wParam, LPARAM lParam)
{
...something_else...
if ( (unsigned __int16)wParam == 2 )
{
GetWindowTextW(hEdit, buf, 32);
score = j__wtoi(buf);
if ( (unsigned int)score <= 0x64 )
{
std::wstring::wstring(&dllName);
if ( score > 60 )
{
if ( score > 82 )
{
if ( score == 83 )
{
std::wstring::operator=(&dllName, L"Plugin_83.dll");
}
else if ( score == 84 )
{
std::wstring::operator=(&dllName, L"Plugin_84.dll");
}
else
{
std::wstring::operator=(&dllName, L"Plugin_85_100.dll");
}
}
else
{
std::wstring::operator=(&dllName, L"Plugin_61_82.dll");
}
}
else
{
std::wstring::operator=(&dllName, L"Plugin_0_60.dll");
}
v8 = std::wstring::c_str(&dllName);
hMod = LoadLibraryW(v8);
if ( hMod )
{
RunPlugin = (void (__fastcall *)(HWND__ *, int))GetProcAddress(hMod, "RunPlugin");
if ( RunPlugin )
{
RunPlugin_1 = (std::wstring *)RunPlugin;
RunPlugin(hWnd, score);
}
else
{
MessageBoxW(hWnd, &lpText_, &lpCaption_, 0x10u);
}
FreeLibrary(hMod);
std::wstring::~wstring(&dllName);
}
else
{
RunPlugin_1 = std::operator+<wchar_t>(&result, &Left, &dllName);
_Left = RunPlugin_1;
std::operator+<wchar_t>(&v19, RunPlugin_1, Right);
std::wstring::~wstring(&result);
...something_else...
return 0;
}c我们发现了调用dll的关键函数:RunPlugin
分别打开几个Plugin的该函数,发现不同的惊喜:

直接猜XOR,最后结果肯定是whuctf{

不用说了,基础的换表base64,Cyberchef直接梭。

你这是典型的RC4逻辑,RC4思维,RC4素质。
分别解密后拼出完整flag:
whuctf{I_w1ll_give_y0u_an_extr@_po1nt}
hajimi_player#
使用FindCrypt发现了一些AES相关的东西:

询问AI有:
函数 1:
sub_140012E50作用:实现 AES-128 的密钥扩展 (Key Expansion)。
概要说明:
输入:16 字节主密钥(
a1)。输出:176 字节扩展密钥(
a2),即 11 轮 × 16 字节的轮密钥。内部逻辑:
拷贝前 16 字节为初始轮密钥。
每 16 字节为一轮,执行:
RotWord(循环左移 1 字节);SubWord(用 Rijndael S-box 替换每个字节);- 首字节 XOR 上 Rcon(轮常数),并更新 Rcon;
- 按 AES 规则生成新的 16 字节轮密钥。
使用的表:
RijnDael_AES_LONG_14001DBE0(AES S-box)。👉 简单说:它把一个 16 字节的 AES 密钥展开成所有轮用的 176 字节密钥数据。
函数 2:
sub_140013D70作用:执行 AES 的 SubBytes 步骤(S-box 替换)。
概要说明:
- 输入:指向 16 字节数据块的指针(
a1)。- 对每个字节执行:
byte = SBOX[byte];- 直接在原缓冲区中替换(就地操作),返回 16。
- 同样使用表
RijnDael_AES_LONG_14001DBE0(Rijndael S-box)。👉 简单说:它把 16 字节“状态矩阵”中的每个字节都通过 AES S-box 映射,属于 AES 加密的一个核心变换步骤。
查找引用,发现了函数sub_140012CA0:

分析得出这是单块AES的加密实现,用GPT的话说就是:
这是单块 AES-128 加密的驱动函数——把 16 字节明文用 16 字节密钥加密成 16 字节密文(相当于 ECB 的单块操作)。流程就是标准 AES:初始轮密钥异或 → 9 个主轮(SubBytes→ShiftRows→MixColumns→AddRoundKey)→ 最后一轮(SubBytes→ShiftRows→AddRoundKey,不含 MixColumns)。
顺着该函数的线索,观察行移位和列混淆和轮密钥加的相关函数:
- sub_140013A30:ShiftRows
- sub_140013390:MixColumns
- sub_140012C00:AddRoundKey(state XOR round key)
发现全是标准实现。(想到了书记骂盘子的“都多少年了还看不出来AES”)

全部更名后,想起了被PolyEncrypt支配的恐惧。
根据该函数的引用,跳转到了sub_140011990,这是一个登录对话框,输入用户名和密码。
然而,我们发现了奇怪的函数!

根据函数签名,它长得很像加密函数,但又不是我们之前分析的AES_EncryptBlock。导航进去并尝试分析之。发现是一个类TEA实现。这里Trunk和跳转比较多就不贴出完整流程了,给一个重命名表,可自行检验:
sub_140013E00 = TEA_EncryptBlock128_ECB
sub_140013EA0 = TEA_Encrypt64
sub_140011447 = TEA_Trunk_Wrapperc至此登录部分分析完毕,给出用户名和密码的解密脚本(解密需要的静态数据就不贴出了,很容易找到)。
WHUCTF2025_新生赛/RE/hajimi_player via 🐍
❯ cat dec.py
# Requires: pip install pycryptodome
from Crypto.Cipher import AES
import struct
# Constants from .rdata
C1 = bytes.fromhex(
"F2 C1 8F 6E 89 18 46 70 DE E6 76 95 E7 BC 17 CB".replace(" ", ""))
C2 = bytes.fromhex(
"4D D5 DF FB 1A CA 76 E6 1E 22 5F 3E 57 58 F3 EB".replace(" ", ""))
# TEA key words (little-endian 32-bit words as in the binary)
k0 = 0x00000001
k1 = 0x00000002
k2 = 0x00000003
k3 = 0x00000004
KEY_WORDS = (k0, k1, k2, k3)
# TEA parameters observed in the binary
DELTA = 0x0D33B470 # custom delta from disassembly
ROUNDS = 32
def le_bytes_to_u32_pair(b8):
"""Little-endian: first 4 bytes -> v0 (low dword), next 4 -> v1"""
v0 = struct.unpack("<I", b8[0:4])[0]
v1 = struct.unpack("<I", b8[4:8])[0]
return v0, v1
def u32_pair_to_le_bytes(v0, v1):
return struct.pack("<I", v0) + struct.pack("<I", v1)
def tea_decrypt_block64(v0, v1, k):
"""Decrypt single 64-bit block that was encrypted with the variant seen in binary."""
k0, k1, k2, k3 = k
mask32 = 0xFFFFFFFF
# initial sum = delta * rounds mod 2^32
sum_ = (DELTA * ROUNDS) & mask32
# run rounds in reverse
for _ in range(ROUNDS):
# reverse of:
# v5 += (k3 + (v4>>5)) ^ (sum + v4) ^ (k2 + (v4<<4));
# v4 += (k1 + (v5>>5)) ^ (sum + v5) ^ (k0 + (v5<<4));
v1 = (v1 - (((k3 + ((v0 >> 5) & mask32)) ^ ((sum_ + v0) & mask32)
^ ((k2 + ((v0 << 4) & mask32)) & mask32)) & mask32)) & mask32
v0 = (v0 - (((k1 + ((v1 >> 5) & mask32)) ^ ((sum_ + v1) & mask32)
^ ((k0 + ((v1 << 4) & mask32)) & mask32)) & mask32)) & mask32
sum_ = (sum_ - DELTA) & mask32
return v0, v1
def tea_decrypt_128_ecb(cipher16, k):
"""Decrypt 16-byte block that was encrypted as two TEA-64 encryptions (ECB on two halves)."""
assert len(cipher16) == 16
out = bytearray(16)
for i in range(2):
blk = cipher16[i*8:(i+1)*8]
v0, v1 = le_bytes_to_u32_pair(blk)
pv0, pv1 = tea_decrypt_block64(v0, v1, k)
out[i*8:(i+1)*8] = u32_pair_to_le_bytes(pv0, pv1)
return bytes(out)
def aes128_decrypt_block(cipher16, key16):
assert len(cipher16) == 16 and len(key16) == 16
cipher = AES.new(key16, AES.MODE_ECB)
return cipher.decrypt(cipher16)
# Step 1: decrypt username block (TEA variant)
plain_user_block = tea_decrypt_128_ecb(C1, KEY_WORDS)
print("Username block (hex):", plain_user_block.hex())
# remove trailing zero padding to get ASCII username (zero-padded)
username = plain_user_block.rstrip(b"\x00").decode('latin1', errors='replace')
print("Username:", repr(username))
# Step 2: decrypt password block using AES-128 with username-block as key
# The code uses the username block (16 bytes) as AES key directly.
plain_pass_block = aes128_decrypt_block(C2, plain_user_block)
print("Password block (hex):", plain_pass_block.hex())
password = plain_pass_block.rstrip(b"\x00").decode('latin1', errors='replace')
print("Password:", repr(password))
WHUCTF2025_新生赛/RE/hajimi_player via 🐍
❯ python dec.py
Username block (hex): 68616a696d696e616d656c75646f6f6f
Username: 'hajiminameludooo'
Password block (hex): 6d616e626f2331623265266562686a34
Password: 'manbo#1b2e&ebhj4'bash获得用户名和密码。
接下来根据窗口逻辑,顺着指针dwNewLong_捋到函数sub_140012480,发现是一个文件解密器的实现。GPT大人如是说:
快速结论(一句话) sub_140012480 是 Hajimi Player 的窗口过程:用户点击 “select your hajimi file” 会把路径放到全局 FileName;点击 “play” 会读取 GetWindowLongPtr(hWnd, GWLP_USERDATA)(也就是登录时存的 Destination —— username+password 拼接字符串),把它作为“密钥/密钥材料”传给 sub_14001110E(初始化/派生解密上下文),然后调用 sub_140011451 把读到的文件数据用该上下文进行解密/处理,最后把解密后数据指向全局 ::Block/::Size 以供播放。也就是说 Player 在播放前会对选中文件进行解密,解密密钥由窗口的 user-data(登录时的 username+password)来派生/生成。
啊伟大的GPT大人!
解密文件部分主要涉及的是sub_14001110E和sub_140011451,分析得知:
…能得知什么,就一破RC4罢了。
解密之:
# RC4 decrypt for Hajimi Player files
key = (b"hajiminameludooo" + b"manbo#1b2e&ebhj4") # username + password
def rc4_ksa(key: bytes):
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) & 0xFF
S[i], S[j] = S[j], S[i]
return S, 0, 0 # S, i, j
def rc4_prga(S, i, j):
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) & 0xFF]
return K, S, i, j
def rc4_crypt(data: bytes, key: bytes) -> bytes:
S, i, j = rc4_ksa(key)
out = bytearray(len(data))
for n, b in enumerate(data):
k, S, i, j = rc4_prga(S, i, j)
out[n] = b ^ k
return bytes(out)
inp = "video.hajimi" # 改成你的文件名
outp = "output.bin"
with open(inp, "rb") as f:
enc = f.read()
dec = rc4_crypt(enc, key)
with open(outp, "wb") as f:
f.write(dec)
# 粗略看看是否像明文
head = dec[:64]
print("Decrypted head (hex):", head.hex())
try:
print("ASCII head:", head.decode("latin1"))
except:
passpython通过file命令查阅生成的基密文件后发现是个MP4,遂直接mpv播放之。

WHUCTF{h@jimin@meLud0_axiga@xiii}
byd气笑了。
Baby_HeavenGate#
众所周知,Heaven’s Gate是一种在32位程序环境下执行64位代码的混淆技术。
于是,当我们看到这个JUMPOUT时,我们就知道要切到反汇编界面进行查看了。
这里出现了牢门经典的call $+5、retf等汇编指令。
进门发现一个简单的异或:
继续往下分析,又发现了经典牢门:
分析可得,这里会把用户输入送入sub_402000函数中。
当然,如果你实现没有执行某一步骤,你看到的可能不是正经的sub_402000,而是一些奇怪的东西。
其实这里出题人已经通过区段名给了我们提示:这里是Heaven’s Gate跳转到的64位代码处。我们通过IDA的Edit-> Segments -> Edit Segment将x64区段改为64位:
核心的异或解密逻辑就搞定了。
因此,写出解密脚本如下:
# 加密数据
encrypted = [
0x09, 0x2e, 0x27, 0x36, 0x39, 0x01, 0x65, 0x35, 0x71, 0x05,
0x28, 0x20, 0x1d, 0x28, 0x23, 0x32, 0x34, 0x1d, 0x24, 0x61,
0x27, 0x73, 0x05, 0x34, 0x36, 0x26, 0x12
]
# "Heaven's Gate" 密钥 (13个字符)
key = b"Heaven's Gate"
# 第一步:逆向第二步XOR(使用"Heaven's Gate"作为轮转密钥)
def get_key_char(index):
return key[index % len(key)]
# 第二步:逆向XOR_Encrypt
def decrypt_xor_encrypt(encrypted_byte):
for original in range(256):
if encrypted_byte == (original + 39 - 2 * (original & 0x27)) & 0xFF:
return original
return None
# 完整解密
decrypted_step1 = [encrypted[i] ^ get_key_char(i) for i in range(27)]
decrypted_step2 = [decrypt_xor_encrypt(b) for b in decrypted_step1]
flag = ''.join(chr(b) for b in decrypted_step2)
print("解密结果:", flag)python解密结果: flag{Heavens_Gate_mastered}
天才侦探#
打开发现奇怪的setjmp函数:

带着对它的疑惑,先看看别处的逻辑🤔
很轻松地,我们可以看出XOR和XXTEA_Encrypt加密的线索。
然而,XXTEA_Encrypt中部出现了疑似花指令的诡异跳转。切到汇编:
经典的跳转到+1位置,在中间藏一个恶心人的幽灵call。NOP掉即可。
__int64 __fastcall XXTEA_Encrypt(char *Buf1, int n10, char *Can_y0u_catch_me)
{
...something...
if ( n10 <= 1 )
{
if ( n10 < -1 )
{
v20 = -n10;
v10 = 52 / -n10 + 6;
v14 = 1131796 * v10;
v19 = *(_DWORD *)Buf1;
do
{
v8 = (v14 >> 2) & 3;
for ( i = v20 - 1; i; --i )
{
v16 = *(_DWORD *)&Buf1[4 * (i - 1)];
v5 = &Buf1[4 * i];
*(_DWORD *)v5 -= ((v19 ^ v14) + (v16 ^ *(_DWORD *)&Can_y0u_catch_me[4 * (v8 ^ i & 3)]))
^ (((4 * v19) ^ (v16 >> 5)) + ((v19 >> 3) ^ (16 * v16)));
v19 = *(_DWORD *)v5;
}
v17 = *(_DWORD *)&Buf1[4 * v20 - 4];
*(_DWORD *)Buf1 -= (((4 * v19) ^ (v17 >> 5)) + ((v19 >> 3) ^ (16 * v17)))
^ ((v19 ^ v14) + (v17 ^ *(_DWORD *)&Can_y0u_catch_me[4 * v8]));
v19 = *(_DWORD *)Buf1;
v14 -= 1131796;
--v10;
}
while ( v10 );
}
}
else
{
v9 = 52 / n10 + 6;
v13 = 0;
v15 = *(_DWORD *)&Buf1[4 * n10 - 4];
do
{
v13 += 0x114514;
v7 = (v13 >> 2) & 3;
for ( j = 0; j < n10 - 1; ++j )
{
v18 = *(_DWORD *)&Buf1[4 * j + 4];
v3 = &Buf1[4 * j];
*(_DWORD *)v3 += ((v18 ^ v13) + (v15 ^ *(_DWORD *)&Can_y0u_catch_me[4 * (v7 ^ j & 3)]))
^ (((4 * v18) ^ (v15 >> 5)) + ((v18 >> 3) ^ (16 * v15)));
v15 = *(_DWORD *)v3;
}
v4 = &Buf1[4 * n10 - 4];
*(_DWORD *)v4 += ((*(_DWORD *)Buf1 ^ v13) + (v15 ^ *(_DWORD *)&Can_y0u_catch_me[4 * (v7 ^ j & 3)]))
^ (((4 * *(_DWORD *)Buf1) ^ (v15 >> 5)) + ((*(_DWORD *)Buf1 >> 3) ^ (16 * v15)));
v15 = *(_DWORD *)v4;
--v9;
}
while ( v9 );
}
return 0xFFFFFFFF / 0; // 这里会触发除0异常,抛出浮点数异常然后触发longjmp
}c然而,在XXTEA加密的末尾,我们发现了奇怪的除0行为!

这里非常不对劲,除0会引发报错。猜测这里的报错是有意为之,我们找到错误处理函数Function:

一个永真跳转,先执行sub_4015F0,再执行longjmp_w。到这里我们明白了上面的setjmp究竟起何作用:一开始Buf是0,执行XXTEA加密,然后XXTEA加密末尾通过除零触发Function,修改Buf为1并跳到setjmp处,使程序进入XOR_Encrypt的分支。
于是,问题就在于:sub_4015F0()做了什么。

分析各种跳转发现,这里又是一个异或。
于是理清了所有加密步骤,写出解密脚本:
import struct
# 密文数据 (从 Cipher)
cipher_bytes = bytes([
0x8A, 0xFF, 0x4B, 0x28, 0xF8, 0x79, 0x53, 0xA8,
0xFA, 0xE0, 0x9B, 0x21, 0x9D, 0xAB, 0x79, 0x01,
0xCD, 0xD7, 0x49, 0xA5, 0x7A, 0x05, 0x0B, 0x94,
0xF5, 0xD8, 0xBA, 0xD9, 0x4B, 0x26, 0x3E, 0x0C,
0x8F, 0x91, 0x70, 0x41, 0x05, 0xE9, 0x40, 0xD7
])
# sub_4015F0 中使用的 cipher 数组 (从 unk_49F020 提取,按 DWORD 存储)
unk_49F020 = [
0x0000003E, 0x00000034, 0x000000BE, 0x000000D6,
0x00000000, 0x0000008F, 0x000000C8, 0x00000006,
0x00000008, 0x000000E2, 0x000000F9, 0x00000005,
0x000000FD, 0x000000E0, 0x0000003A, 0x000000D0,
0x0000000A, 0x000000C2, 0x000000D7, 0x000000D4,
0x0000000C, 0x0000001E, 0x00000047, 0x0000004D,
0x0000009D, 0x00000020, 0x000000F9, 0x00000095,
0x0000009F, 0x0000000F, 0x000000E8, 0x00000075,
0x000000CA, 0x000000F6, 0x00000096, 0x00000096,
0x0000007B, 0x00000028, 0x0000001B, 0x000000D1
]
# XXTEA 密钥
xxtea_key = b"Can_y0u_catch_me"
# XOR 密钥
xor_key = b"Siesta"
def xxtea_decrypt(v, k):
"""XXTEA 解密算法"""
def MX(sum, y, z, p, e, k):
return ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (k[p & 3 ^ e] ^ z))
n = len(v)
rounds = 6 + 52 // n
sum = (rounds * 0x114514) & 0xFFFFFFFF
y = v[0]
while rounds > 0:
e = (sum >> 2) & 3
for p in range(n - 1, 0, -1):
z = v[p - 1]
v[p] = (v[p] - MX(sum, y, z, p, e, k)) & 0xFFFFFFFF
y = v[p]
z = v[n - 1]
v[0] = (v[0] - MX(sum, y, z, 0, e, k)) & 0xFFFFFFFF
y = v[0]
sum = (sum - 0x114514) & 0xFFFFFFFF
rounds -= 1
return v
def decrypt():
# 步骤1: 逆 XOR_Encrypt (与 "Siesta" 异或)
data = bytearray(cipher_bytes)
for i in range(40):
data[i] ^= xor_key[i % len(xor_key)]
print("Step 1 - 逆 XOR_Encrypt:")
print(' '.join(f'{b:02x}' for b in data))
# 步骤2: 逆 sub_4015F0 ((data ^ cipher[i]) - 1)
for i in range(40):
# 原加密: Input[i] = (Input[i] + 1) ^ cipher[i]
# 逆运算: Input[i] = (data[i] ^ cipher[i]) - 1
data[i] = ((data[i] ^ (unk_49F020[i] & 0xFF)) - 1) & 0xFF
print("\nStep 2 - 逆 sub_4015F0:")
print(' '.join(f'{b:02x}' for b in data))
# 步骤3: XXTEA 解密
# 将字节数组转换为 32 位整数数组(小端序)
v = list(struct.unpack('<10I', bytes(data)))
# 将密钥转换为 32 位整数数组
k = list(struct.unpack('<4I', xxtea_key[:16]))
# 执行 XXTEA 解密
decrypted = xxtea_decrypt(v, k)
# 转换回字节
result = struct.pack('<10I', *decrypted)
print("\nStep 3 - XXTEA 解密:")
print(' '.join(f'{b:02x}' for b in result))
# 尝试解析为字符串
try:
flag = result.decode('ascii', errors='ignore')
print(f"\n解密结果: {flag}")
return flag
except:
print("\n解密结果 (hex):", result.hex())
return result
if __name__ == "__main__":
print("=" * 60)
print("开始解密...")
print("=" * 60)
print()
result = decrypt()python解密结果: WHUCTF{U_ar3_as_pr0f3ssional_as_siesta!}
感想#
本次迎新赛拿到了个总榜第二。当然还是感谢学长们不杀之恩。
第一天下午去了Deepin的WHLUG,没写题,第二天运动会+百团大战+游园会,算是玩爽了,也没怎么写题,我忏悔喵。
最后,逆向工程这个方向,从迎新赛打到moe,从NewStar打到ILoveCTF,甚至在强网杯还干出来一题。再回头看看迎新赛,虽不能说轻舟已过万重山,至少也翻过了一两重吧。
