8950 字
45 分钟
MoeCTF 2025 re

02 逆向工程入门指北#

文件提示:

现在,用 IDA 打开本题的附件,开始我们的第一个挑战吧!

P.S. 附件使用C++编写,有很多正常人看不懂的东西,不过或许 shift+F12 能帮到你?

moectf{open_your_IDA_and_start_reverse_engineering!!}

03 base#

IDA打开,找到main函数

查看sub_140001070函数

是一个 完全遵循Base64编码的标准算法流程

使用strcmp函数,将 sub_140001070 返回的字符串v7与一个硬编码的字符串**“bW9lY3Rme1kwdV9DNG5fRzAwZF9BdF9CNDVlNjQhIX0=“**进行比较。

moectf{Y0u_C4n_G00d_At_B45e64!!}

10 A cup of tea#

搜索main函数无果,shift6+F12搜索字符串

我们看到主函数在sub_1400162E0里,继续点击

v5是密钥,v6是数组,加密逻辑在sub_14001109B里

每一轮使用 delta(这里是 1131796)和 key 进行混淆 ,逆解密

from typing import List, Tuple
DELTA = 1131796 # 0x114514 as seen in the binary (decimal 1131796)
def u32(x: int) -> int:
return x & 0xFFFFFFFF
def tea_decrypt_block(v0: int, v1: int, k: List[int], rounds: int = 32) -> Tuple[int, int]:
v0 = u32(v0)
v1 = u32(v1)
k = [u32(x) for x in k]
sum_ = u32(DELTA * rounds)
for _ in range(rounds):
v1 = u32(v1 - (((v0 >> 5) + k[3]) ^ (sum_ + v0) ^ ((v0 << 4) + k[2])))
v0 = u32(v0 - (((v1 >> 5) + k[1]) ^ (sum_ + v1) ^ ((v1 << 4) + k[0])))
sum_ = u32(sum_ - DELTA)
return v0, v1
def tea_encrypt_block(v0: int, v1: int, k: List[int], rounds: int = 32) -> Tuple[int, int]:
v0 = u32(v0)
v1 = u32(v1)
k = [u32(x) for x in k]
sum_ = 0
for _ in range(rounds):
sum_ = u32(sum_ + DELTA)
v0 = u32(v0 + (((v1 >> 5) + k[1]) ^ (sum_ + v1) ^ ((v1 << 4) + k[0])))
v1 = u32(v1 + (((v0 >> 5) + k[3]) ^ (sum_ + v0) ^ ((v0 << 4) + k[2])))
return v0, v1
def words_to_bytes_le(words: List[int]) -> bytes:
return b"".join(w.to_bytes(4, "little") for w in words)
def main():
key = [289739801, 427884820, 1363251608, 269567252]
v6 = [
2026214571,
578894681,
1193947460,
-229306230,
73202484,
961145356,
-881456792,
358205817,
-554069347,
119347883,
0,
]
v6_u = [u32(x) for x in v6]
ciphertext_words = v6_u[:10]
plaintext_words = []
for i in range(0, len(ciphertext_words), 2):
p0, p1 = tea_decrypt_block(ciphertext_words[i], ciphertext_words[i+1], key)
plaintext_words.extend([p0, p1])
pt_bytes = words_to_bytes_le(plaintext_words)
flag = pt_bytes.split(b"\x00", 1)[0].decode('utf-8', errors='replace')
print("Recovered flag string:", flag)
re_cipher_words = []
for i in range(0, len(plaintext_words), 2):
c0, c1 = tea_encrypt_block(plaintext_words[i], plaintext_words[i+1], key)
re_cipher_words.extend([c0, c1])
print("Verification success?", re_cipher_words == ciphertext_words)
if __name__ == '__main__':
main()

moectf{h3r3_4_cuP_0f_734_f0R_y0U!!!!!!}

11 ezpy#

简单的pyc逆向

通过命令

Terminal window
uncompyle6 ezpy.pyc >ezpy.py

看到生成了ezpy.py

打开ezpy.py文件

实际就是 shift = 10 的凯撒加密, 只要把目标字符串 只要把目标字符串 “wyomdp{I0e_Ux0G_zim}” 逆向 Caesar -10 就行。

def caesar_cipher_decrypt(text, shift):
result = []
for char in text:
if char.isalpha():
if char.islower():
new_char = chr((ord(char) - ord("a") - shift) % 26 + ord("a"))
else:
if char.isupper():
new_char = chr((ord(char) - ord("A") - shift) % 26 + ord("A"))
result.append(new_char)
else:
result.append(char)
return "".join(result)
if __name__ == "__main__":
encrypted = "wyomdp{I0e_Ux0G_zim}"
shift = 114514 % 26 # 实际等效移位量 = 10
flag = caesar_cipher_decrypt(encrypted, shift)
print("解密结果:", flag)

moectf{Y0u_Kn0W_pyc}

13 mazegame#

IDA打开,定位到主函数,是一个迷宫, 需要找到从起始点 **(1, 1)** 到终点 **(32, 15)** 的一条有效路径

迷宫地图在sub_1400010E0函数里

提取出,并按顺序排好

11111111111111111111111111111111111111111111111111111111
10100000000000000010000011011101011111111101011100000111
10111010111111111010111011000001000001000001000101110111
10000010000010000010001011011111111101110111011101110111
10111111111011101110111011010000000000010100010001110111
10100000001000101000100011010101111111011101110101110111
10101011111110111011101011010101000001000000010101110111
10101010000010100000101011110101110101111101111111110111
10111010111010101111101011100101000100000101000101110111
10000010001010001000001011001111011111010101011101110111
11111011101011111011111111101000100000101100101001110111
10001010001000100010000010001010011000100010010011000001
10111010111110101010111011011001011111010101011101011101
10001010001000001010001011000101000100000101000101011101
11101011101111111011101011110101110111111101110101011101
10001000101000001010001011000100010100000101000101011101
10111111101011101110111011011111110101110111011101011101
10001000001000100000001011000100000100010000000101011001
11101011111011111111101011110101111101111111110101011011
10101000000010001000101011010100000001000100010101011011
10101111111110101010101011010111111111010101010101011011
10100000000000100010101011010000000000010001010101011011
10111111111111111110111011011111111111111111011101011011
10000000001111000000000011110111010000111100011111011011
11101111100000011011011111111010110111011101100001011011
11101111111111111011011111111101110111101101100001011011
10001000111111000010000011111010110111011101100001011011
10111010111111111010111011110111010000111101100001010011
10000010000010000010001011111111111111111101100001010111
10111111111011101110111011110001000110001101100001010001
10100000001000101000100011110111011101111101100001011101
10101011111110111011101011110001000101111101100001011101
10101010000010100000101011111101011101111101100001011101
10111010111010101111101011110001000110001101100001011101
10000010001010101000001011111111111111111101100001011101
11111011101011111011111110000000000000001101100001011101
10001010001000100010000011111111111111111100110011011101
10111010111110101010111010010000000011111110001111011101
10001010001000001010001010110111000001111110100101011101
11101011101111111011101000110011001111111100110111011101
10001000101000001010001011111111111111111111110111010001
10111111101011101110111010100001001100000000000011011011
10001000001000100000001011111111111101011101111001011011
10101011111011111111101011000000000001000100010111011011
10101000000010001000101010010111111111111111111111011011
10101111111110101010101010110111111111111111111101011011
10100000000000100010101011100000000000000000000011011011
10111111111111111110011011111111111111111111111011011011
10000011111111111111000010000000000000000000000000011001
11111011111111111111111111111111111111111111111111111101
11111011100001100110110111000000000000000000000111111101
11111011101111011010000111011111111111111111110111111101
11111011100001000010110110000111111111111111110000000001
11111011101111011010110111101111111111111111111111111111
11110000000000011000110000000000000000000000000000000011
11111111111111111111111111111111111111111111111111111111

用BFS算法寻路

from collections import deque
def find_maze_path(maze, start, end):
# maze: 56x56的迷宫地图('0'和'1'的二维列表)
# start: 起点坐标 (x, y)
# end: 终点坐标 (x, y)
rows = len(maze)
cols = len(maze[0])
# 移动方向:上、下、左、右
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] # (dx, dy)
# 队列用于BFS
queue = deque([start])
# 集合用于记录已访问的节点
visited = {start}
# 字典用于回溯路径
parent = {start: None}
while queue:
current_x, current_y = queue.popleft()
current_node = (current_x, current_y)
# 检查是否到达终点
if current_node == end:
return reconstruct_path(parent, start, end)
# 探索所有可能的移动方向
for dx, dy in directions:
next_x, next_y = current_x + dx, current_y + dy
next_node = (next_x, next_y)
# 检查移动是否有效
if 0 <= next_x < cols and 0 <= next_y < rows and \
maze[next_y][next_x] == '0' and next_node not in visited:
visited.add(next_node)
queue.append(next_node)
parent[next_node] = current_node
return None
def reconstruct_path(parent_map, start, end):
path = []
current = end
while current:
path.append(current)
current = parent_map[current]
path.reverse()
instructions = []
for i in range(len(path) - 1):
x1, y1 = path[i]
x2, y2 = path[i+1]
if x2 > x1:
instructions.append('D')
elif x2 < x1:
instructions.append('A')
elif y2 > y1:
instructions.append('S')
elif y2 < y1:
instructions.append('W')
return "".join(instructions)
maze_map_strings = [
"11111111111111111111111111111111111111111111111111111111",
"10100000000000000010000011011101011111111101011100000111",
"10111010111111111010111011000001000001000001000101110111",
"10000010000010000010001011011111111101110111011101110111",
"10111111111011101110111011010000000000010100010001110111",
"10100000001000101000100011010101111111011101110101110111",
"10101011111110111011101011010101000001000000010101110111",
"10101010000010100000101011110101110101111101111111110111",
"10111010111010101111101011100101000100000101000101110111",
"10000010001010001000001011001111011111010101011101110111",
"11111011101011111011111111101000100000101100101001110111",
"10001010001000100010000010001010011000100010010011000001",
"10111010111110101010111011011001011111010101011101011101",
"10001010001000001010001011000101000100000101000101011101",
"11101011101111111011101011110101110111111101110101011101",
"10001000101000001010001011000100010100000101000101011101",
"10111111101011101110111011011111110101110111011101011101",
"10001000001000100000001011000100000100010000000101011001",
"11101011111011111111101011110101111101111111110101011011",
"10101000000010001000101011010100000001000100010101011011",
"10101111111110101010101011010111111111010101010101011011",
"10100000000000100010101011010000000000010001010101011011",
"10111111111111111110111011011111111111111111011101011011",
"10000000001111000000000011110111010000111100011111011011",
"11101111100000011011011111111010110111011101100001011011",
"11101111111111111011011111111101110111101101100001011011",
"10001000111111000010000011111010110111011101100001011011",
"10111010111111111010111011110111010000111101100001010011",
"10000010000010000010001011111111111111111101100001010111",
"10111111111011101110111011110001000110001101100001010001",
"10100000001000101000100011110111011101111101100001011101",
"10101011111110111011101011110001000101111101100001011101",
"10101010000010100000101011111101011101111101100001011101",
"10111010111010101111101011110001000110001101100001011101",
"10000010001010101000001011111111111111111101100001011101",
"11111011101011111011111110000000000000001101100001011101",
"10001010001000100010000011111111111111111100110011011101",
"10111010111110101010111010010000000011111110001111011101",
"10001010001000001010001010110111000001111110100101011101",
"11101011101111111011101000110011001111111100110111011101",
"10001000101000001010001011111111111111111111110111010001",
"10111111101011101110111010100001001100000000000011011011",
"10001000001000100000001011111111111101011101111001011011",
"10101011111011111111101011000000000001000100010111011011",
"10101000000010001000101010010111111111111111111111011011",
"10101111111110101010101010110111111111111111111101011011",
"10100000000000100010101011100000000000000000000011011011",
"10111111111111111110011011111111111111111111111011011011",
"10000011111111111111000010000000000000000000000000011001",
"11111011111111111111111111111111111111111111111111111101",
"11111011100001100110110111000000000000000000000111111101",
"11111011101111011010000111011111111111111111110111111101",
"11111011100001000010110110000111111111111111110000000001",
"11111011101111011010110111101111111111111111111111111111",
"11110000000000011000110000000000000000000000000000000011",
"11111111111111111111111111111111111111111111111111111111",
]
maze_map = [list(row) for row in maze_map_strings]
start_point = (1, 1)
end_point = (32, 15)
path_instructions = find_maze_path(maze_map, start_point, end_point)
if path_instructions:
print("找到了路径!")
print(path_instructions)
else:
print("没有找到路径。")

moectf{SSDDDDWWDDSSDDDDSSDDSSSSDDWWDDWWDDWWWWDDDDSSSSAASSSSAAAASSAASSAAWWAAWWWWAAAASSDDSSAASSDDSSSSAAAASSDDDDDDWWWWDDDDSSDDDDWWDDWWAAWWDDDDSSSSSSSSSSSSAAASSSDDDSSSSAASSSSAAAASSAASSAAWWAAWWWWAAAASSDDSSAASSDDSSSSAAAASSDDDDDDWWWWDDDDSSDDDDWWDDWWAAWWDDDDSSSSSSSSSSSSAAAWAWWWAASSAAWWAASSAAAAAAAAAAWWWWAASSSSSSDDDDSSSSSSDDDDDDDDDWWDDDSSDDWWWDDDSSSDDDDDWWAWWDDDDDDDDDDDDDDDDDDDDSSDDDDDDDDWWWWAWWWWWWWWDWWWWWWWWWWWAAWWDWWWWWWWWWWDWWWWWWAAAASSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSAAAWWAAAAAAAAAAAAAAAAAAAWWWDDDDDDDDWWDDDDDDDDDDWWWAWAAWAWWWWWWWWWWWWWDDWWWWAASSAAWWAASSAAAAAAAAAAWWWWAAWWDDWWAAWWDWWWDWWWWDDDDDDDDDDSSDDDDSSSSDSDSSDDSSAASSAAAAWWAAAASSSSAAAAAAWWDDDDWWWWAAWWAWAASSDSSSDD}

17 Two cup of tea#

IDA打开定位主函数

数组v15是加密后的数据,v10是key,但经过函数处理的才是我们需要的

打开sub_140001070函数

它的作用就是:

生成/混淆出 XXTEA 所需的前半部分 key。

key = [final_low, final_high, 0x12345678, 0x9ABCDEF0];

之后是加密函数sub_1400015E0,是一个改造过的 XXTEA 解密实现,固定迭代 11 轮。

解密脚本

def xxtea_decrypt_uint32(v, key):
n = len(v)
delta = 0x9E3779B9
q = 11 # 题里固定跑 11 轮
sum_ = (q * delta) & 0xFFFFFFFF
while q > 0:
e = (sum_ >> 2) & 3
for p in range(n-1, -1, -1):
z = v[p-1] if p > 0 else v[-1]
y = v[p]
v[p] = (y - (((z >> 5 ^ (v[(p+1) % n] << 2)) + (v[(p+1) % n] >> 3 ^ (z << 4))) ^ ((sum_ ^ v[(p+1) % n]) + (key[(p & 3) ^ e] ^ z)))) & 0xFFFFFFFF
sum_ = (sum_ - delta) & 0xFFFFFFFF
q -= 1
return v
# v15: main 函数里的常量数组(10个 32-bit)
v15 = [
1566723124,
-2044068179,
-1659816037,
-53136879,
1175413710,
-981373336,
-28114771,
167777774,
-1744380997,
-280353208
]
v15 = [x & 0xFFFFFFFF for x in v15]
# key 的两个部分
key_low = 0x12345678
key_high = 0x9ABCDEF0
final_low = 1667592045
final_high = 555837044
key = [final_low, final_high, key_low, key_high]
# 解密
dec = xxtea_decrypt_uint32(v15[:], key)
plain = b''.join(x.to_bytes(4, 'little') for x in dec)
print("解密结果:", plain.decode(errors="ignore"))

moectf{X7e4_And_xx7EA_I5_BeautifuL!!!!!}

19 rusty_sudoku#

shift+F12 查看字符串

点进去看到

通过交叉引用,定位到rusty_sudoku::main函数

分析函数以及在字符串中看到的提示,就是找到一个正确的9*9数独,再回到字符串视图页面,发现有一个未补全的数独,点进去看到完整的数独

使用在线工具

将 369184572185327694274956831632879415897541263541632789756213948918465327423798156 作为程序的输入

moectf{a8c79927d4e830c3fe52e79f410216a0}

01 speed#

IDA定位主函数,发现返回到WinMain函数里

打开该函数,这个函数注册了一个窗口,只显示1秒,就立即销毁,真正的逻辑在WndProc里

打开WndProc函数

加密算法为RC4,密钥 (key): “mylittlepony”

密文

def rc4_crypt(key: bytes, data: bytes) -> bytes:
# simple RC4 (KSA + PRGA)
S = list(range(256))
j = 0
# KSA
for i in range(256):
j = (j + S[i] + key[i % len(key)]) & 0xFF
S[i], S[j] = S[j], S[i]
# PRGA
i = 0
j = 0
out = bytearray(len(data))
for idx in range(len(data)):
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) & 0xFF]
out[idx] = data[idx] ^ K
return bytes(out)
def main():
key = b"mylittlepony"
str_part = (0x07F1B3E885EF9160).to_bytes(8, "little")
v25 = (0x2CD336BCB0464A89).to_bytes(8, "little")
v26 = bytearray((0xEF5FC91642917EE1).to_bytes(8, "little"))
overwrite = (0x739D40A4E356EF5F).to_bytes(8, "little")
v26[6:6+8] = overwrite
cipher_bytes = str_part + v25 + bytes(v26)
plaintext = rc4_crypt(key, cipher_bytes)
print("raw decrypted bytes:", plaintext)
try:
s = plaintext.decode("utf-8")
except UnicodeDecodeError:
s = plaintext.decode("utf-8", errors="ignore")
print("decoded (utf-8, ignoring errors):")
print(s)
if __name__ == "__main__":
main()

moectf{Just_dyn@mic_d3bugg1ng}

04 catch#

定位主函数,

打开solve函数

看到提示,打开sub_114514()函数

发现是根据enc函数进行加密

flag字符串为

enc函数就是一个简单的异或

异或结果为

显然不是我么们的flag,回到刚才的提示flag就藏在项目里,我们查看字符串看到可疑对象

使用解码工具一键解码

Rot13解码: moectf{S4m3_Tr1ck_with_@flower_desuwa}

12 have_fun#

点开WinMain函数,是一个典型的 Win32 GUI 程序入口(WinMain)的反编译版

入口 WinMain 很模板化,实质逻辑都在 sub_140001210

分别查看sub_7FF681721420函数与DialogFunc函数,发现前者是空壳,真正的加密逻辑在DialogFunc里

输入长度为16

对每个字符做异或

把异或后的结果 v23 与目标常量比较

目标常量为[‘G’,‘E’,‘O’,‘I’,’^’,‘L’,‘Q’,‘b’,‘j’,’\’, 0x1E, ‘u’,‘L’, 0x7F,‘D’,‘W’]

解密

# 只需改这里:把 .rdata 里目标串的 16 个字节(低 8 位)填进来
target = [
0x47, 0x45, 0x4F, 0x49, 0x5E, 0x4C, 0x51, 0x62,
0x6A, 0x5C, 0x1E, 0x75, 0x4C, 0x7F, 0x44, 0x57
]
flag = ''.join(chr((b ^ 0x2A) & 0xFF) for b in target)
print(flag) # -> moectf{H@v4_fUn}

moectf{H@v4_fUn}

07 ezandroid#

在AndroidManifest.xml找到android com.example.ezandroid.MainActivity

在源代码里按照路径打开com.example.ezandroid.MainActivity

是一个base64编码,字符串为bW9lY3Rme2FuZHJvaWRfUmV2ZXJzZV9JNV9lYXN5fQ==

moectf{android_Reverse_I5_easy}

18 ezandroid.pro#

用jdax打开这个apk文件

定位到AndroidManifest.xml,在其中找到android

按照此路径在源代码中打开

Java 层只做了长度检查(32)和 check(str) 的调用;真正判定逻辑在 native 库 libezandroidpro.so

把apk后缀名改为zip

在ida中打开libezandroidpro.so

定位到Java_com_example_ezandroidpro_MainActivity_check函数

是一个sm4—ECB加密,密钥是moectf2025!!!!!!

明文4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649C0B971BF2EFBCB160E531A646DF7A6AC0B

moectf{SM4_Android_I5_Funing!!!}

05 upx#

用die打开有upx加壳

用upx脱壳

脱壳完成在ida中打开,定位主函数

字符串长度为35

加密过程

要加密的数据(按顺序)

脚本

def reverse_decrypt(encrypted_data):
decrypted = []
for i in range(len(encrypted_data)):
if i == 0:
# 第一个字符已知是 'm' (moectf的开头)
first_char = ord('m')
decrypted.append(first_char)
else:
# 使用链式XOR解密公式
prev_encrypted = encrypted_data[i-1] # 前一个加密字节
prev_decrypted = decrypted[i-1] # 前一个解密字节
current_char = prev_encrypted ^ prev_decrypted ^ 0x21
decrypted.append(current_char)
return bytes(decrypted)
def encrypt_for_verification(plaintext):
plaintext_bytes = plaintext.encode('ascii')
encrypted = []
for i in range(len(plaintext_bytes) - 1):
# 加密公式: encrypted[i] = (plaintext[i] ^ 0x21) ^ plaintext[i+1]
enc_byte = (plaintext_bytes[i] ^ 0x21) ^ plaintext_bytes[i+1]
encrypted.append(enc_byte)
return encrypted
def solve_moe_exe():
print("=== moe.exe CTF题目解密算法 ===")
print("题目类型: UPX脱壳 + 自定义XOR加密")
print()
complete_encrypted_data = [
0x23, 0x2B, 0x27, 0x36, 0x33, 0x3C, 0x03, 0x48, 0x64, 0x0B, 0x1D, 0x76,
0x7B, 0x10, 0x0B, 0x3A, 0x3F, 0x65, 0x76, 0x29, 0x15, 0x37, 0x1C, 0x0A,
0x08, 0x21, 0x3E, 0x3C, 0x3D, 0x16, 0x0B, 0x24, 0x29, 0x24
]
print(f"加密数据长度: {len(complete_encrypted_data)} 字节")
print("加密数据 (16进制):")
for i in range(0, len(complete_encrypted_data), 16):
chunk = complete_encrypted_data[i:i+16]
hex_str = ' '.join(f'{b:02X}' for b in chunk)
print(f" {hex_str}")
print()
# 执行解密
print("开始解密...")
decrypted_bytes = reverse_decrypt(complete_encrypted_data)
# 转换为字符串
try:
decrypted_str = decrypted_bytes.decode('ascii')
print(f"解密结果: {decrypted_str}")
except UnicodeDecodeError:
print("解密结果包含非ASCII字符,显示原始字节:")
print(decrypted_bytes)
return None
# 检查并修正flag格式
if decrypted_str.startswith('moectf{') and not decrypted_str.endswith('}'):
# 手动添加结尾的'}'
final_flag = decrypted_str + '}'
print(f"修正后的flag: {final_flag}")
else:
final_flag = decrypted_str
# 验证flag格式
if final_flag.startswith('moectf{') and final_flag.endswith('}'):
print("✅ Flag格式正确!")
if 'upx' in final_flag.lower():
print("🎯 包含UPX关键词,符合题目背景!")
return final_flag
else:
print("❌ Flag格式不正确")
return None
def demo_step_by_step():
"""
演示解密过程的每一步
"""
print("\n=== 逐步解密演示 ===")
# 使用前10字节进行演示
demo_data = [0x23, 0x2B, 0x27, 0x36, 0x33, 0x3C, 0x03, 0x48, 0x64, 0x0B]
print("演示数据:", ' '.join(f'{b:02X}' for b in demo_data))
print()
decrypted = []
for i in range(len(demo_data)):
if i == 0:
# 第一个字符
char = ord('m')
decrypted.append(char)
print(f"步骤 {i+1}: 第一个字符已知 = 'm' (0x{char:02X})")
else:
# 后续字符的解密
prev_encrypted = demo_data[i-1]
prev_decrypted = decrypted[i-1]
current_char = prev_encrypted ^ prev_decrypted ^ 0x21
decrypted.append(current_char)
char_display = chr(current_char) if 32 <= current_char <= 126 else f'\\x{current_char:02x}'
print(f"步骤 {i+1}: {prev_encrypted:02X} ^ {prev_decrypted:02X} ^ 21 = {current_char:02X} ('{char_display}')")
# 显示部分结果
partial_result = bytes(decrypted).decode('ascii', errors='ignore')
print(f"\n部分解密结果: {partial_result}")
def main():
"""
主函数 - 执行完整的解密流程
"""
print("moe.exe CTF题目完整解密算法")
print("=" * 50)
# 1. 执行解密
final_flag = solve_moe_exe()
if final_flag:
print(f"\n🏆 最终Flag: {final_flag}")
# 2. 演示解密步骤
demo_step_by_step()
print("\n" + "=" * 50)
print("解密完成!")
print(f"Flag: {final_flag}")
print("题目类型: UPX脱壳 + 自定义XOR加密算法逆向")
print("=" * 50)
return final_flag
else:
print("\n❌ 解密失败")
return None
if __name__ == "__main__":
main()

moectf{Y0u_c4n_unp4ck_It_vvith_upx}

06 ez3#

ida中打开定位到主函数

flag长度为42

真正的加密逻辑在check()

点开

脚本

#!/usr/bin/env python3
# -*- coding: utf-8 -*
MOD = 51966
MUL = 47806
XOR_VAL = 0x114514
LEN = 34
# expected 数组(你给出的值)
a = [
0x0B1B0, 0x5678, 0x7FF2, 0xA332, 0xA0E8, 0x364C, 0x2BD4,
0xC8FE, 0x4A7C, 0x18, 0x2BE4, 0x4144, 0x3BA6, 0xBE8C, 0x8F7E,
0x35F8, 0x61AA, 0x2B4A, 0x6828, 0xB39E, 0xB542, 0x33EC, 0xC7D8,
0x448C, 0x9310, 0x8808, 0xADD4, 0x3CC2, 0x796, 0xC940, 0x4E32,
0x4E2E, 0x924A, 0x5B5C
]
def compute_b(ch: int, i: int, prev_b: int|None) -> int:
tmp = MUL * (ch + i)
if prev_b is not None:
tmp ^= (prev_b ^ XOR_VAL)
return tmp % MOD
# 优先级:'_' 最优,接着 数字,再字母,最后其他可打印字符
def candidate_priority(ch: int):
c = chr(ch)
if c == '_':
return 0
if c.isdigit():
return 1
if c.isalpha():
return 2
if 32 <= ch < 127:
return 3
return 4
def gen_candidates(pos: int, prev_b: int|None, byte_range=range(32,127)):
t = a[pos]
cands = []
for ch in byte_range:
if compute_b(ch, pos, prev_b) == t:
cands.append(ch)
# 按优先级排序(数字优先于字母)
cands.sort(key=lambda x: (candidate_priority(x), x))
return cands
# DFS 回溯,返回若干候选解(按优先级)
def dfs_find(byte_range=range(32,127), max_solutions=20):
solutions = []
stack = [(0, None, [])] # pos, prev_b, bytes_list
while stack and len(solutions) < max_solutions:
pos, prev_b, cur = stack.pop()
if pos == LEN:
solutions.append(bytes(cur))
continue
cands = gen_candidates(pos, prev_b, byte_range)
# 逆序压栈,确保优先级高的先弹出
for ch in reversed(cands):
new_b = compute_b(ch, pos, prev_b)
stack.append((pos + 1, new_b, cur + [ch]))
return solutions
def main():
print("开始回溯求解(数字优先于字母)...")
sols = dfs_find(range(32,127), max_solutions=20)
if not sols:
print("未在可打印 ASCII 找到解,请尝试全字节范围。")
return
for i, s in enumerate(sols):
try:
text = s.decode('ascii')
except:
text = s.decode('latin1')
print(f"[{i}] 候选:{text}")
print(" 完整 flag:", "moectf{" + text + "}")
print("\n建议第一个候选通常为正确结果:")
best = sols[0].decode('ascii', errors='replace')
print("最终 flag:", "moectf{" + best + "}")
if __name__ == "__main__":
main()

moectf{Y0u_Kn0w_z3_S0Iv3r_N0w_a1f2bdce4a9}

16 A simple program#

ida中打开,定位到main函数

输入到str1中,并与str2对比,相等就输出correct,否则输出wrong,但这个主函数并没有什么校验函数,根据题目提示“falg就在主函数里”,查看字符串

上面的moectf是假的flag,下面的数据组就是校验数组,点开这个函数

正如我们所料,对这个数组的数据进行异或,写个脚本

decode_flag.py
ARR = [
0x4E, 0x4C, 0x46, 0x40, 0x57, 0x45, 0x58, 0x7A, 0x13, 0x56,
0x7C, 0x73, 0x17, 0x50, 0x50, 0x66, 0x47, 0x02, 0x02, 0x5E
]
flag = bytes(b ^ 0x23 for b in ARR).decode('ascii')
print(flag) # moectf{Y0u_P4ssEd!!}

moectf{Y0u_P4ssEd!!}

08 flower#

IDA定位到主函数

点击slove函数,有栈堆不平衡

定位到这个位置,发现有花指令,还有函数边界问题

先修改花指令

之后修改函数边界对着冒红行摁u

之后正常反汇编,查看slove函数,找到最主要的一步

encode函数以及enc和key,其中正确的初始key很关键

脚本

brute_recover_flag.py
import string
# enc 常量表(32 个)
enc = [
0x4F, 0x1A, 0x59, 0x1F, 0x5B, 0x1D, 0x5D, 0x6F,
0x7B, 0x47, 0x7E, 0x44, 0x6A, 0x07, 0x59, 0x67,
0x0E, 0x52, 0x08, 0x63, 0x5C, 0x1A, 0x52, 0x1F,
0x20, 0x7B, 0x21, 0x77, 0x70, 0x25, 0x74, 0x2B,
]
PRINTABLE = set(string.printable) - set("\r\n\t\x0b\x0c")
def decode_with_key(enc, key):
# s[i] = enc[i] ^ (key + i)
bs = [(e ^ ((key + i) & 0xFF)) & 0xFF for i, e in enumerate(enc)]
return ''.join(chr(b) for b in bs)
def looks_good(s):
# 1) 全可打印 2) 不含控制字符 3) 基本可读
return all(ch in PRINTABLE for ch in s)
candidates = []
for key in range(256):
inner = decode_with_key(enc, key)
if looks_good(inner):
candidates.append((key, inner))
# 输出候选 key 及解码结果
for key, inner in candidates:
print(f"key=0x{key:02X} -> {inner}")
# 通常只有极少数候选;从可读性判断最佳那个

moectf{f0r3v3r_JuMp_1n_7h3_a$m_a9b35c3c}

14 upx_revenge#

我们只需要在4.24.后面插入UPX!就可以正常脱壳了

脱壳

IDA打开,分析主函数,最主要的在这

去分析sub_7FF6BA531230 sub_7FF6BA531300两个函数

sub_7FF6BA531230 是对标准表进行异或14,sub_7FF6BA531300是调用这个表进行Base64编码

异或后的表

GMLQOJPI^DXHZSBUT[ARCYV-KWEN]@F\\(>JD=MLZSIWPHGYAQBEOU)TNVurqpcjgmlhfnadkbisoetv(zyx,0w.4-2863q1)5?9+7

只需要输入的字符串用这个表进行Base64编码与硬字符串lY7bW=\ck?eyjX7]TZ\}CVbh\tOyTH6>jH7XmFifG]H7对比

脚本

standard_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
encoded = "lY7bW=\\ck?eyjX7]TZ\\}CVbh\\tOyTH6>jH7XmFifG]H7".replace("\\\\", "\\") # 处理转义
# 生成自定义表
custom_table = ''.join(chr(ord(c) ^ 0xE) for c in standard_table)
# 创建映射: char -> index
char_to_index = {custom_table[i]: i for i in range(64)}
# 解码
flag_bytes = []
for i in range(len(encoded) // 4):
group = encoded[i*4:i*4+4]
indices = [char_to_index[c] for c in group]
value = (indices[0] << 18) | (indices[1] << 12) | (indices[2] << 6) | indices[3]
flag_bytes.append(value >> 16 & 0xFF)
flag_bytes.append(value >> 8 & 0xFF)
flag_bytes.append(value & 0xFF)
flag = ''.join(chr(b) for b in flag_bytes)
print("Flag:", flag)

moectf{Y0u_Re4l1y_G00d_4t_Upx!!!}

15 guess#

IDA打开,定位到主函数,无法反编译

定位到140001A5A,有花指令

改成单条无条件跳转,其余全部nop

ok,可以成功反编译

分析函数行为,是一个猜谜游戏,主要的逻辑在这里,看名字知道是RC4算法

点进去,主要逻辑在rc4_ksa和rc4_prga

分别点进去查看,先看ksa,比正常RC4相比密钥要加常数42

再看prga是一个标准的

接下来我们是找key和enc,shift+F12

脚本

from binascii import unhexlify
def rc4_with_plus42(data: bytes, key: bytes) -> bytes:
# === KSA 带 +42 ===
S = list(range(256))
j = 0
for i in range(256):
k = (key[i % len(key)] + 42) & 0xFF
j = (j + S[i] + k) & 0xFF
S[i], S[j] = S[j], S[i]
# === PRGA 标准 ===
i = j = 0
out = bytearray()
for b in data:
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) & 0xFF]
out.append(b ^ K)
return bytes(out)
enc_hex = "464DCF81DE6F2E16BE203F10565CCDBFF18CCD6A45967D20DC558FB76C0CC3AE07D154"
key_str = "moectf2025"
cipher = unhexlify(enc_hex)
plain = rc4_with_plus42(cipher, key_str.encode())
print("moectf{" + plain.decode(errors="ignore") + "}")

moectf{RrRRccCc44$$_w1th_fl0w3r!!_3c6a11b5}

09 2048_master_re#

这道题是一个窗口游戏题,有两种解题思路

一种是HOOK,另一种是直接逆向解密

先说逆向解密,在字符串中找到这个

点开,是一个flag校验函数, 它从 flag.txt 中读取字符串,经过某种加密/变换后,与内置的字节数组 byte_495280 进行比对,若完全一致则返回 0(正确),否则返回 1(错误)。

去查看加密函数sub_401A81

sub_401898:按 小端把明文每 4 字节打包成 uint32_t 数组,长度 Count=(len+3)//4,没有额外写入原始长度;

sub_401530:标准 BTEA/XXTEA,delta = 1050114489 (0x3E9779B9),key 为 16 字节,索引 (p^e)&3;

sub_40195B:把 uint32_t[] 原样拆回字节(每个 dword → 两个 _WORD),最终输出字节数 a3 = 4Count。

脚本

from typing import List
DELTA = 0x3E9779B9
def to_u32_le_blocks(b: bytes) -> List[int]:
pad = (-len(b)) % 4
b += b'\x00' * pad
return [int.from_bytes(b[i:i+4], 'little') for i in range(0, len(b), 4)]
def from_u32_le_blocks(v: List[int]) -> bytes:
return b''.join(x.to_bytes(4, 'little') for x in v)
def btea(v: List[int], n: int, k: List[int]) -> None:
"""XXTEA/BTEA inplace; n>1 加密,n<-1 解密"""
if n > 1:
rounds = 6 + 52 // n
_sum = 0
z = v[n - 1]
for _ in range(rounds):
_sum = (_sum + DELTA) & 0xFFFFFFFF
e = (_sum >> 2) & 3
for p in range(n - 1):
y = v[p + 1]
v[p] = (v[p] + ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
^ ((_sum ^ y) + (k[(p ^ e) & 3] ^ z)))) & 0xFFFFFFFF
z = v[p]
p = n - 1
y = v[0]
v[p] = (v[p] + ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
^ ((_sum ^ y) + (k[(p ^ e) & 3] ^ z)))) & 0xFFFFFFFF
z = v[p]
elif n < -1:
n = -n
rounds = 6 + 52 // n
_sum = (rounds * DELTA) & 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] - ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
^ ((_sum ^ y) + (k[(p ^ e) & 3] ^ z)))) & 0xFFFFFFFF
y = v[p]
p = 0
z = v[n - 1]
v[0] = (v[0] - ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
^ ((_sum ^ y) + (k[(p ^ e) & 3] ^ z)))) & 0xFFFFFFFF
y = v[0]
_sum = (_sum - DELTA) & 0xFFFFFFFF
rounds -= 1
# ==== 数据区 ====
# 全局密文 byte_495280(注意这里只取前 40 字节,其余是 0 padding,不参与校验)
cipher_bytes = bytes([
0x35,0x79,0x77,0xCC,0x1B,0x13,0x41,0x34,
0xF9,0xFF,0x9F,0x91,0xFF,0x5B,0x94,0x78,
0x86,0x2A,0xAF,0xAE,0xD7,0x9E,0x31,0x4D,
0x7A,0xC4,0xA5,0x51,0xD1,0xD9,0x6E,0x44,
0x18,0x52,0x86,0x1B,0x42,0x8A,0xC9,0x63,
])
# key = "2048master2048ma" → 拆成 4 个小端 uint32
key_words = [
int.from_bytes(b"2048", "little"),
int.from_bytes(b"mast", "little"),
int.from_bytes(b"er20", "little"),
int.from_bytes(b"48ma", "little"),
]
# ==== 解密流程 ====
v = to_u32_le_blocks(cipher_bytes)
btea(v, -len(v), key_words)
plain = from_u32_le_blocks(v)
# 原始 flag 长度 = 37
flag = plain[:37].decode(errors="ignore")
print("Flag:", flag)

moectf{@_N1c3_cup_0f_XXL_te4_1n_2O48}

MoeCTF 2025 re
https://blog-5w0.pages.dev/posts/moectf-2025-re/
作者
weixiao
发布于
2025-07-05
许可协议
CC BY-NC-SA 4.0