前言
我比较菜,很多都是靠ai分析才完成的,毕竟都6202年了。
windows高级题和所有的安卓题我都不会,所以文章中不包含这些题的解法(附件里有cc的解法)。
有条件的可以上cc,今年这些题目都可以全自动解出来。
我朋友用cc解没有给任何工具,都是cc自己想办法解决的,额度至少花了几百刀吧。
附件中是cc最后生成的工具和文档。
安卓初级题
安卓初级题我不会,那就玩嘛,两分钟就出了,参考文章,最后点击宝箱,得到:flag{Wu41_P0j13_2026_Spr1ng_F3st1v4l}
ps:
其实也可以上cc,直接秒杀。
这里上张图:
第一篇windows初级题
首先查壳,无壳(其实初级题一般也不会加壳,主要是为了看编译信息),这里可以断定为c/c++编写的,拖入ida中,f5,复制代码,接下来就交给gemini了。
很快就发现关键函数:
_BYTE *__cdecl sub_401620(int a1)
{
_BYTE *result; // eax
*(_DWORD *)a1 = 758280311;
*(_DWORD *)(a1 + 4) = 1663511336;
*(_DWORD *)(a1 + 8) = 1880974179;
*(_DWORD *)(a1 + 12) = 494170226;
*(_DWORD *)(a1 + 16) = 842146570;
*(_DWORD *)(a1 + 20) = 657202491;
*(_DWORD *)(a1 + 24) = 658185525;
*(_BYTE *)(a1 + 30) = 99;
*(_WORD *)(a1 + 28) = 12323;
result = (_BYTE *)a1;
do
*result++ ^= 0x42u;
while ( result != (_BYTE *)(a1 + 31) );
*(_BYTE *)(a1 + 31) = 0;
return result;
}
这里喂给ds ,注意开深度思考(如果用gemini就得开pro)
提示词:上面的代码加上仔细分析,这个 Flag是什么
很快就得到flag:52pojie!!!_2026_Happy_new_year!
第二篇windows初级题
首先查壳,发现是pyinstaller编译的(其实看图标也能猜个七七八八)。
那么就上pyinstxtractor,可以得到一个crackme_easy.pyc。
我尝试所有pycdc,结果失败了。
法一:
既然是初级题,那就应该很简单,于是我使用notepad4打开,文件编码选择utf-8,这样可以正确看到中文字符串和中文注释,发现了一个敏感数据:
flagz(e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O)�base64� b64decode)
于是我把贴近这的上下文都复制粘贴给gemini,经过它的一通分析,最终得到flag:52p0j!3#2026*H4ppy-N3w-Y34r@@@
ps:
这里的算法是:对e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4Obase64解码,然后异或解密,gemini通过推测(理)猜测的Key: 0x4E 了
法二:
可以看到python314.dll,右键查看详细信息发现是python 3.14.2版本,去官网下载python-3.14.2-embed-amd64.zip。
然后利用该版本python,运行下面的代码:
import dis
import marshal
with open('crackme_easy.pyc', 'rb') as f:
f.read(16) # 跳过文件头
code = marshal.load(f)
dis.dis(code)
得到:
0 RESUME 0
4 LOAD_SMALL_INT 0
LOAD_CONST 1 (None)
IMPORT_NAME 0 (hashlib)
STORE_NAME 0 (hashlib)
5 LOAD_SMALL_INT 0
LOAD_CONST 1 (None)
IMPORT_NAME 1 (base64)
STORE_NAME 1 (base64)
6 LOAD_SMALL_INT 0
LOAD_CONST 1 (None)
IMPORT_NAME 2 (sys)
STORE_NAME 2 (sys)
8 LOAD_CONST 2 (<code object xor_decrypt at 0x000001C336305830, file "crackme_easy.py", line 8>)
MAKE_FUNCTION
STORE_NAME 3 (xor_decrypt)
15 LOAD_CONST 3 (<code object get_encrypted_flag at 0x000001C3362D7AA0, file "crackme_easy.py", line 15>)
MAKE_FUNCTION
STORE_NAME 4 (get_encrypted_flag)
21 LOAD_CONST 4 (<code object generate_flag at 0x000001C336305E30, file "crackme_easy.py", line 21>)
MAKE_FUNCTION
STORE_NAME 5 (generate_flag)
32 LOAD_CONST 5 (<code object calculate_checksum at 0x000001C3362B63A0, file "crackme_easy.py", line 32>)
MAKE_FUNCTION
STORE_NAME 6 (calculate_checksum)
39 LOAD_CONST 6 (<code object hash_string at 0x000001C33633BEB0, file "crackme_easy.py", line 39>)
MAKE_FUNCTION
STORE_NAME 7 (hash_string)
43 LOAD_CONST 7 (<code object verify_flag at 0x000001C3366444E0, file "crackme_easy.py", line 43>)
MAKE_FUNCTION
STORE_NAME 8 (verify_flag)
57 LOAD_CONST 8 (<code object fake_check_1 at 0x000001C3362B3A30, file "crackme_easy.py", line 57>)
MAKE_FUNCTION
STORE_NAME 9 (fake_check_1)
62 LOAD_CONST 9 (<code object fake_check_2 at 0x000001C3364B0430, file "crackme_easy.py", line 62>)
MAKE_FUNCTION
STORE_NAME 10 (fake_check_2)
67 LOAD_CONST 10 (<code object main at 0x000001C336539400, file "crackme_easy.py", line 67>)
MAKE_FUNCTION
STORE_NAME 11 (main)
108 LOAD_NAME 12 (__name__)
LOAD_CONST 11 ('__main__')
COMPARE_OP 88 (bool(==))
POP_JUMP_IF_FALSE 11 (to L3)
NOT_TAKEN
109 NOP
110 L1: LOAD_NAME 11 (main)
PUSH_NULL
CALL 0
POP_TOP
L2: LOAD_CONST 1 (None)
RETURN_VALUE
108 L3: LOAD_CONST 1 (None)
RETURN_VALUE
-- L4: PUSH_EXC_INFO
111 LOAD_NAME 13 (KeyboardInterrupt)
CHECK_EXC_MATCH
POP_JUMP_IF_FALSE 31 (to L6)
NOT_TAKEN
POP_TOP
112 LOAD_NAME 14 (print)
PUSH_NULL
LOAD_CONST 12 ('\n\n[!] Interrupted by user')
CALL 1
POP_TOP
113 LOAD_NAME 2 (sys)
LOAD_ATTR 30 (exit)
PUSH_NULL
LOAD_SMALL_INT 0
CALL 1
POP_TOP
L5: POP_EXCEPT
LOAD_CONST 1 (None)
RETURN_VALUE
111 L6: RERAISE 0
-- L7: COPY 3
POP_EXCEPT
RERAISE 1
ExceptionTable:
L1 to L2 -> L4 [0]
L4 to L5 -> L7 [1] lasti
L6 to L7 -> L7 [1] lasti
Disassembly of <code object xor_decrypt at 0x000001C336305830, file "crackme_easy.py", line 8>:
8 RESUME 0
10 LOAD_GLOBAL 1 (bytearray + NULL)
CALL 0
STORE_FAST 2 (result)
11 LOAD_GLOBAL 3 (enumerate + NULL)
LOAD_FAST_BORROW 0 (data)
CALL 1
GET_ITER
L1: FOR_ITER 42 (to L2)
UNPACK_SEQUENCE 2
STORE_FAST_STORE_FAST 52 (i, byte)
12 LOAD_FAST_BORROW 2 (result)
LOAD_ATTR 5 (append + NULL|self)
LOAD_FAST_BORROW_LOAD_FAST_BORROW 65 (byte, key)
BINARY_OP 12 (^)
LOAD_FAST_BORROW 3 (i)
LOAD_SMALL_INT 255
BINARY_OP 1 (&)
BINARY_OP 12 (^)
CALL 1
POP_TOP
JUMP_BACKWARD 44 (to L1)
11 L2: END_FOR
POP_ITER
13 LOAD_FAST_BORROW 2 (result)
LOAD_ATTR 7 (decode + NULL|self)
LOAD_CONST 1 ('utf-8')
LOAD_CONST 2 ('ignore')
LOAD_CONST 3 (('errors',))
CALL_KW 2
RETURN_VALUE
Disassembly of <code object get_encrypted_flag at 0x000001C3362D7AA0, file "crackme_easy.py", line 15>:
15 RESUME 0
18 LOAD_CONST 1 ('e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O')
STORE_FAST 0 (enc_data)
19 LOAD_GLOBAL 0 (base64)
LOAD_ATTR 2 (b64decode)
PUSH_NULL
LOAD_FAST_BORROW 0 (enc_data)
CALL 1
RETURN_VALUE
Disassembly of <code object generate_flag at 0x000001C336305E30, file "crackme_easy.py", line 21>:
21 RESUME 0
23 LOAD_GLOBAL 1 (get_encrypted_flag + NULL)
CALL 0
STORE_FAST 0 (encrypted)
24 LOAD_SMALL_INT 78
STORE_FAST 1 (key)
26 LOAD_GLOBAL 3 (bytearray + NULL)
CALL 0
STORE_FAST 2 (result)
27 LOAD_GLOBAL 5 (enumerate + NULL)
LOAD_FAST_BORROW 0 (encrypted)
CALL 1
GET_ITER
L1: FOR_ITER 28 (to L2)
UNPACK_SEQUENCE 2
STORE_FAST_STORE_FAST 52 (i, byte)
28 LOAD_FAST_BORROW 2 (result)
LOAD_ATTR 7 (append + NULL|self)
LOAD_FAST_BORROW_LOAD_FAST_BORROW 65 (byte, key)
BINARY_OP 12 (^)
CALL 1
POP_TOP
JUMP_BACKWARD 30 (to L1)
27 L2: END_FOR
POP_ITER
30 LOAD_FAST_BORROW 2 (result)
LOAD_ATTR 9 (decode + NULL|self)
LOAD_CONST 1 ('utf-8')
CALL 1
RETURN_VALUE
Disassembly of <code object calculate_checksum at 0x000001C3362B63A0, file "crackme_easy.py", line 32>:
32 RESUME 0
34 LOAD_SMALL_INT 0
STORE_FAST 1 (total)
35 LOAD_GLOBAL 1 (enumerate + NULL)
LOAD_FAST_BORROW 0 (s)
CALL 1
GET_ITER
L1: FOR_ITER 37 (to L2)
UNPACK_SEQUENCE 2
STORE_FAST_STORE_FAST 35 (i, c)
36 LOAD_FAST_BORROW 1 (total)
LOAD_GLOBAL 3 (ord + NULL)
LOAD_FAST_BORROW 3 (c)
CALL 1
LOAD_FAST_BORROW 2 (i)
LOAD_SMALL_INT 1
BINARY_OP 0 (+)
BINARY_OP 5 (*)
BINARY_OP 13 (+=)
STORE_FAST 1 (total)
JUMP_BACKWARD 39 (to L1)
35 L2: END_FOR
POP_ITER
37 LOAD_FAST_BORROW 1 (total)
RETURN_VALUE
Disassembly of <code object hash_string at 0x000001C33633BEB0, file "crackme_easy.py", line 39>:
39 RESUME 0
41 LOAD_GLOBAL 0 (hashlib)
LOAD_ATTR 2 (sha256)
PUSH_NULL
LOAD_FAST_BORROW 0 (s)
LOAD_ATTR 5 (encode + NULL|self)
CALL 0
CALL 1
LOAD_ATTR 7 (hexdigest + NULL|self)
CALL 0
RETURN_VALUE
Disassembly of <code object verify_flag at 0x000001C3366444E0, file "crackme_easy.py", line 43>:
43 RESUME 0
45 LOAD_GLOBAL 1 (generate_flag + NULL)
CALL 0
STORE_FAST 1 (correct_flag)
47 LOAD_GLOBAL 3 (len + NULL)
LOAD_FAST_BORROW 0 (user_input)
CALL 1
LOAD_GLOBAL 3 (len + NULL)
LOAD_FAST_BORROW 1 (correct_flag)
CALL 1
COMPARE_OP 119 (bool(!=))
POP_JUMP_IF_FALSE 3 (to L1)
NOT_TAKEN
48 LOAD_CONST 1 (False)
RETURN_VALUE
51 L1: LOAD_GLOBAL 5 (range + NULL)
LOAD_GLOBAL 3 (len + NULL)
LOAD_FAST_BORROW 1 (correct_flag)
CALL 1
CALL 1
GET_ITER
L2: FOR_ITER 25 (to L4)
STORE_FAST 2 (i)
52 LOAD_FAST_BORROW_LOAD_FAST_BORROW 2 (user_input, i)
BINARY_OP 26 ([])
LOAD_FAST_BORROW_LOAD_FAST_BORROW 18 (correct_flag, i)
BINARY_OP 26 ([])
COMPARE_OP 119 (bool(!=))
POP_JUMP_IF_TRUE 3 (to L3)
NOT_TAKEN
JUMP_BACKWARD 24 (to L2)
53 L3: POP_TOP
LOAD_CONST 1 (False)
RETURN_VALUE
51 L4: END_FOR
POP_ITER
55 LOAD_CONST 2 (True)
RETURN_VALUE
Disassembly of <code object fake_check_1 at 0x000001C3362B3A30, file "crackme_easy.py", line 57>:
57 RESUME 0
59 LOAD_CONST 1 ('a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890')
STORE_FAST 1 (fake_hash)
60 LOAD_GLOBAL 1 (hash_string + NULL)
LOAD_FAST_BORROW 0 (user_input)
CALL 1
LOAD_FAST_BORROW 1 (fake_hash)
COMPARE_OP 72 (==)
RETURN_VALUE
Disassembly of <code object fake_check_2 at 0x000001C3364B0430, file "crackme_easy.py", line 62>:
62 RESUME 0
64 LOAD_CONST 1 ('1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef')
STORE_FAST 1 (fake_hash)
65 LOAD_GLOBAL 1 (hash_string + NULL)
LOAD_FAST_BORROW 0 (user_input)
CALL 1
LOAD_FAST_BORROW 1 (fake_hash)
COMPARE_OP 72 (==)
RETURN_VALUE
Disassembly of <code object main at 0x000001C336539400, file "crackme_easy.py", line 67>:
67 RESUME 0
69 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 15 ('==================================================')
CALL 1
POP_TOP
70 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 1 (' CrackMe Challenge - Python Edition')
CALL 1
POP_TOP
71 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 15 ('==================================================')
CALL 1
POP_TOP
72 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 2 ('Keywords: 52pojie, 2026, Happy New Year')
CALL 1
POP_TOP
73 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 3 ('Hint: Decompile me if you can!')
CALL 1
POP_TOP
74 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 16 ('--------------------------------------------------')
CALL 1
POP_TOP
76 LOAD_GLOBAL 3 (input + NULL)
LOAD_CONST 4 ('\n[?] Enter the password: ')
CALL 1
LOAD_ATTR 5 (strip + NULL|self)
CALL 0
STORE_FAST 0 (user_input)
79 LOAD_GLOBAL 7 (fake_check_1 + NULL)
LOAD_FAST_BORROW 0 (user_input)
CALL 1
TO_BOOL
POP_JUMP_IF_FALSE 25 (to L1)
NOT_TAKEN
80 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 5 ('\n[!] Nice try, but not quite right...')
CALL 1
POP_TOP
81 LOAD_GLOBAL 3 (input + NULL)
LOAD_CONST 6 ('\nPress Enter to exit...')
CALL 1
POP_TOP
82 LOAD_CONST 7 (None)
RETURN_VALUE
84 L1: LOAD_GLOBAL 9 (fake_check_2 + NULL)
LOAD_FAST_BORROW 0 (user_input)
CALL 1
TO_BOOL
POP_JUMP_IF_FALSE 25 (to L2)
NOT_TAKEN
85 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 8 ("\n[!] You're getting closer...")
CALL 1
POP_TOP
86 LOAD_GLOBAL 3 (input + NULL)
LOAD_CONST 6 ('\nPress Enter to exit...')
CALL 1
POP_TOP
87 LOAD_CONST 7 (None)
RETURN_VALUE
90 L2: LOAD_GLOBAL 11 (verify_flag + NULL)
LOAD_FAST_BORROW 0 (user_input)
CALL 1
TO_BOOL
POP_JUMP_IF_FALSE 108 (to L4)
NOT_TAKEN
91 LOAD_GLOBAL 13 (calculate_checksum + NULL)
LOAD_FAST_BORROW 0 (user_input)
CALL 1
STORE_FAST 1 (checksum)
92 LOAD_GLOBAL 13 (calculate_checksum + NULL)
LOAD_GLOBAL 15 (generate_flag + NULL)
CALL 0
CALL 1
STORE_FAST 2 (expected_checksum)
94 LOAD_FAST_BORROW_LOAD_FAST_BORROW 18 (checksum, expected_checksum)
COMPARE_OP 88 (bool(==))
POP_JUMP_IF_FALSE 60 (to L3)
NOT_TAKEN
95 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 17 ('\n==================================================')
CALL 1
POP_TOP
96 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 9 (' *** SUCCESS! ***')
CALL 1
POP_TOP
97 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 15 ('==================================================')
CALL 1
POP_TOP
98 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 10 ('[+] Congratulations! You cracked it!')
CALL 1
POP_TOP
99 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 11 ('[+] Correct flag: ')
LOAD_FAST_BORROW 0 (user_input)
FORMAT_SIMPLE
BUILD_STRING 2
CALL 1
POP_TOP
JUMP_FORWARD 34 (to L5)
101 L3: LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 12 ('\n[!] Checksum failed!')
CALL 1
POP_TOP
JUMP_FORWARD 22 (to L5)
103 L4: LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 13 ('\n[X] Access Denied!')
CALL 1
POP_TOP
104 LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST 14 ('[X] Wrong password. Keep trying!')
CALL 1
POP_TOP
106 L5: LOAD_GLOBAL 3 (input + NULL)
LOAD_CONST 6 ('\nPress Enter to exit...')
CALL 1
POP_TOP
LOAD_CONST 7 (None)
RETURN_VALUE
通过阅读代码,main->verify_flag->generate_flag->get_encrypted_flag
generate_flag中:
可得知是xor加密,key=78(0x4E),
get_encrypted_flag中:
可发现'e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O',并且后面还有b64decode
那就很明了了。
题外话
这题也可上cc,
第一篇windows中级题
可参考:python逆向之Nuitka,其实坛里也有一篇,但感觉写的不如这篇清晰,流程都是这个流程,记住了就好。
使用到的工具:nuitka-extractor
还是一样,查壳,发现是Nuitka python compiler
那么解包:
nuitka-extractor <file name>
ps:其实也可以运行后去temp目录找onefile开头的文件夹
得到的文件中一眼就看到:crackme_hard.dll
然后先上ResourceHacker提取python的字节码(nuitka会将python的字节码写入在资源段中)
我们先来枚举bin的模块:
import io
import struct
def read_uint32(bio):
return struct.unpack("<I", bio.read(4))[0]
def read_uint16(bio):
return struct.unpack("<H", bio.read(2))[0]
def read_utf8(bio):
bs = b""
while True:
bs += bio.read(1)
if b"\x00" in bs:
break
return bs[:-1].decode("utf-8")
def main():
with open("main.bin", "rb") as f_in:
bs = f_in.read()
bio = io.BytesIO(bs)
hash_ = read_uint32(bio)
size = read_uint32(bio)
print(f"hash: {hex(hash_)}")
print(f"size: {hex(size)}")
while bio.tell() < size:
blob_name = read_utf8(bio)
blob_size = read_uint32(bio)
blob_count = read_uint16(bio)
print(f"name: {blob_name}, size: {hex(blob_size)}, count: {hex(blob_count)}")
bio.seek(bio.tell() + (blob_size - 2))
if __name__ == "__main__":
main()
运行后可以得到:
hash: 0xdcaa9d4e
size: 0x55fdaa
name: .bytecode, size: 0x55f3f1, count: 0x143
name: , size: 0x36e, count: 0x6b
name: __main__, size: 0x62b, count: 0x53
那么可得知__main__是我要解析的了。
接下来就得解析python的字节码了,由于不同版本的nuitka的解析方式也不同,
于是将crackme_hard.dll拖入ida中,可以通过搜索字符串blob->找交叉引用,确定解析函数的位置(别问我怎么知道的,我是看上面的那篇文章学的)。
然后就将那个函数全部复制下来,喂给ds,让它帮你写一个解析代码:(得到的代码第一次可能会出现错误,让ds修正就好了,要告诉ds我们要得到的输出的格式才行)
import io
import struct
from typing import Any, Dict, List, Optional, Union
def read_u8(b) -> int:
return struct.unpack("<B", b.read(1))[0]
def read_u32(b) -> int:
return struct.unpack("<I", b.read(4))[0]
def read_u16(b) -> int:
return struct.unpack("<H", b.read(2))[0]
def read_varint(b) -> int:
"""读取变长整数 (对应C代码中的v73 += v74 * (v75 & 0x7F)逻辑)"""
shift = 0
result = 0
while True:
byte = read_u8(b)
result |= (byte & 0x7F) << shift
if byte < 0x80:
return result
shift += 7
def read_cstring(b) -> bytes:
"""读取C风格字符串 (以\0结尾)"""
s = b""
while True:
c = b.read(1)
if not c or c == b"\x00":
break
s += c
return s
def decode_value(b, depth=0) -> Any:
"""解析一个值 (对应C代码中的sub_31340ECE0函数)"""
try:
tag_byte = read_u8(b)
except:
return None
# 处理非ASCII字符作为原始数据
if tag_byte < 32 or tag_byte >= 127:
# 可能是原始数据,回退一个字节并作为bytes返回
b.seek(b.tell() - 1)
data = b.read(1)
return f"RAW_{data.hex()}"
tag = chr(tag_byte)
# --- 忽略填充符 (对应case '.') ---
if tag == ".":
return None
# --- 字符串类型 ---
# case 'a', 'u': UTF-8字符串,a会被intern
if tag in ("a", "u"):
s = read_cstring(b).decode("utf-8", errors="surrogatepass")
return s
# case 'w': 单字符字符串
if tag == "w":
return b.read(1).decode("utf-8", errors="surrogatepass")
# case 's': interned字符串
if tag == "s":
return read_cstring(b).decode("utf-8", errors="surrogatepass")
# case 'v': 带长度的UTF-8字符串
if tag == "v":
n = read_varint(b)
return b.read(n).decode("utf-8", errors="surrogatepass")
# --- 字节类型 ---
# case 'b': 字节串
if tag == "b":
n = read_varint(b)
data = b.read(n)
# 尝试解码为字符串,失败则返回字节列表
try:
return data.decode("utf-8")
except:
return list(data)
# case 'c': C风格字节串
if tag == "c":
data = read_cstring(b)
try:
return data.decode("utf-8")
except:
return list(data)
# case 'B': bytearray (带长度)
if tag == "B":
n = read_varint(b)
data = b.read(n)
return list(data)
# --- 数字类型 ---
# case 'l', 'q': 整数 (l为正数,q为负数)
if tag in ("l", "q"):
v = read_varint(b)
return v if tag == "l" else -v
# case 'f': 浮点数 (8字节)
if tag == "f":
return struct.unpack("<d", b.read(8))[0]
# case 'j': 复数 (16字节)
if tag == "j":
real = struct.unpack("<d", b.read(8))[0]
imag = struct.unpack("<d", b.read(8))[0]
return complex(real, imag)
# case 'Z': 特殊浮点数常量
if tag == "Z":
subcode = read_u8(b)
# 对应C代码中的switch
constants = [0.0, 1.0, -1.0, 2.0, -2.0, 0.5]
if subcode < len(constants):
return constants[subcode]
return 0.0
# --- 布尔值和None ---
# case 't', 'F', 'n'
if tag in ("t", "F", "n"):
return {
"t": True,
"F": False,
"n": None
}[tag]
# --- 容器类型 ---
# case 'T': 元组
if tag == "T":
n = read_varint(b)
return tuple(decode_value(b, depth+1) for _ in range(n))
# case 'L': 列表
if tag == "L":
n = read_varint(b)
return [decode_value(b, depth+1) for _ in range(n)]
# case 'D': 字典
if tag == "D":
n = read_varint(b)
d = {}
for _ in range(n):
key = decode_value(b, depth+1)
value = decode_value(b, depth+1)
d[key] = value
return d
# case 'P', 'S': 集合/冻结集合
if tag in ("P", "S"):
n = read_varint(b)
items = [decode_value(b, depth+1) for _ in range(n)]
if tag == "P":
return frozenset(items)
else:
return set(items)
# --- 特殊类型 ---
# case ':': 切片
if tag == ":":
start = decode_value(b, depth+1)
stop = decode_value(b, depth+1)
step = decode_value(b, depth+1)
return slice(start, stop, step)
# case ';': range
if tag == ";":
start = decode_value(b, depth+1)
stop = decode_value(b, depth+1)
step = decode_value(b, depth+1)
return range(start, stop, step)
# case 'M': 标记值
if tag == "M":
subcode = read_u8(b)
markers = {
0: None, # Py_None
1: ..., # Ellipsis
2: NotImplemented,
3: "Function",
4: "Generator",
5: "CFunction",
6: "Code",
7: "Module",
10: "Type"
}
return markers.get(subcode, f"Marker_{subcode}")
# case 'Q': 特殊值
if tag == "Q":
subcode = read_u8(b)
if subcode == 0:
return ...
elif subcode == 1:
return NotImplemented
elif subcode == 2:
return ...
else:
return f"Special_{subcode}"
# case 'p': 引用前一个值
if tag == "p":
# 简单实现,实际需要维护引用列表
return "<reference>"
# case 'X': 原始数据块
if tag == "X":
n = read_varint(b)
return b.read(n)
# case 'O': 属性访问
if tag == "O":
name = read_cstring(b).decode()
return f"Attr({name})"
# case 'A': 泛型别名
if tag == "A":
origin = decode_value(b, depth+1)
args = decode_value(b, depth+1)
return f"GenericAlias({origin}, {args})"
# case 'H': 按位OR
if tag == "H":
# 读取第一个对象
first = decode_value(b, depth+1)
if first is None:
return 0
result = first
# 继续读取直到结束
while True:
try:
pos = b.tell()
next_val = decode_value(b, depth+1)
if next_val is None:
break
if isinstance(result, int) and isinstance(next_val, int):
result |= next_val
except:
b.seek(pos)
break
return result
# case 'J': 复数
if tag == "J":
real = decode_value(b, depth+1)
imag = decode_value(b, depth+1)
real_val = real if isinstance(real, (int, float)) else 0
imag_val = imag if isinstance(imag, (int, float)) else 0
return complex(real_val, imag_val)
# case 'g', 'G': 全局对象
if tag in ("g", "G"):
idx = read_varint(b)
return f"Global_{idx}"
# case 'd': 运行时常量
if tag == "d":
idx = read_u8(b)
return f"RuntimeConst_{idx}"
# case 'E': 空
if tag == "E":
# 跳过直到\0
while True:
if b.read(1) == b"\x00":
break
return None
# case 'C': 复杂结构 (从输出中看到)
if tag == "C":
# 复杂结构,可能是code对象
flags = read_u8(b)
# 读取更多数据...
return f"ComplexObject(flags={flags})"
# 处理可能的原始字节
# 回退一个字节,然后作为原始数据读取
b.seek(b.tell() - 1)
# 尝试读取一个整数
try:
val = read_varint(b)
return val
except:
pass
# 作为原始字节返回
data = b.read(1)
return f"RAW_{data.hex()}"
def decode_blob(b, count):
"""解析blob中的对象"""
results = []
for i in range(count):
try:
obj = decode_value(b)
results.append(obj)
except Exception as e:
# 出错时尝试恢复
pos = b.tell()
# 尝试跳过一些字节
b.seek(pos + 1)
results.append(f"<ERROR: {e} at {pos}>")
return results
def main():
import sys
if len(sys.argv) < 2:
print("用法: python script.py <二进制文件>")
return
filename = sys.argv[1]
with open(filename, "rb") as f:
data = f.read()
b = io.BytesIO(data)
# 读取文件头
magic = read_u32(b)
size = read_u32(b)
print(f"magic={hex(magic)} size={size}")
blob_index = 0
while b.tell() < size and b.tell() < len(data):
try:
# 读取blob名称
name_bytes = read_cstring(b)
try:
name = name_bytes.decode('utf-8', errors='ignore')
if not name:
name = "(empty)"
except:
name = name_bytes.hex()
# 读取blob大小和数量
blob_size = read_u32(b)
count = read_u16(b)
print(f"[+] blob {name!r} count={count}")
# 记录当前位置
start_pos = b.tell()
# 如果是__main__ blob,解析里面的对象
if name == "__main__":
objects = decode_blob(b, count)
for i, obj in enumerate(objects):
print(f"{i}: {obj}")
break
else:
# 跳过这个blob
b.seek(start_pos + blob_size - 2)
except Exception as e:
print(f"解析blob时出错: {e}")
break
if __name__ == "__main__":
main()
运行该代码,可得到:
magic=0xdcaa9d4e size=5635498
[+] blob '.bytecode' count=323
[+] blob '(empty)' count=107
[+] blob '__main__' count=83
0: ['dc!a;`b', 'RuntimeConst_17', 'cacg', 'RuntimeConst_47', '\x19e!!(', 'RuntimeConst_14', '\x1fb&', 'RuntimeConst_14', '\x08be#', 'ppp']
1: _parts
2: 81
3: _key
4: 30
5: _total_len
6: 解密单个字符
7: current
8: _decrypt_char
9: 获取指定位置的字符
10: self
11: _get_char_at_position
12: 验证用户输入
13: total
14: 计算校验和
15: aflag
16: checksum
17: 获取目标校验和
18: hashlib
19: sha256
20: encode
21: hexdigest
22: slice(None, 8, None)
23: 16
24: 哈希函数
25: 305419896
26: Global_2
27: RAW_01
28: RAW_81
29: RAW_de
30: RAW_b7
31: RAW_de
32: RAW_02
33: 1380994890
34: hash_input
35: 假检查
36: print
37: ('==================================================',)
38: (' CrackMe Challenge - Binary Edition',)
39: ('Keywords: 52pojie, 2026, Happy New Year',)
40: ('Hint: 1337 5p34k & 5ymb0l5!',)
41: (' Try to decompile this in IDA!',)
42: ('--------------------------------------------------',)
43: CrackMeCore
44:
[?] Enter the password:
45: fake_check
46: ('\n[!] Close, but not quite there...',)
47:
Press Enter to exit...
48: verify
49: get_target_checksum
50: ('\n==================================================',)
51: (' *** SUCCESS! ***',)
52: ('[+] L33T H4X0R!',)
53: [+] Your answer:
54:
[!] Checksum mismatch:
55: !=
56: ('\n[X] Access Denied!',)
57: ('[X] Wrong password!',)
58: 主函数
59: __doc__
60: __file__
61: __cached__
62: __annotations__
63: sys
64: __main__
65: __module__
66: 核心验证类 - 将被编译成二进制
67: __qualname__
68: __init__
69: CrackMeCore.__init__
70: CrackMeCore._decrypt_char
71: CrackMeCore._get_char_at_position
72: CrackMeCore.verify
73: CrackMeCore.checksum
74: CrackMeCore.get_target_checksum
75: main
76: ('\n\n[!] Interrupted',)
77: crackme_hard.py
78: <module>
79: ('self',)
80: ('self', 'part_idx', 'char_idx', 'encrypted_byte')
81: ('self', 'pos', 'current', 'part_idx', 'part')
82: ('self', 's', 'total', 'i', 'c')
然后,将这些输出丢给ds,开深度思考,最后得到密码为 52p0j13@2026~H4ppy_N3w_Y34r!!!
这里我还试了刚出的gemini pro3.1,居然还不如ds深度思考。
cc仍然可解,
番外篇
这不就是熟悉的抓小猫吗,直接玩就行了,得到flag{52pojie_2026_Happy_New_Year!_>w<}。
我一开始还没注意到easy模式没有flag,还傻乎乎的玩easy模式。
后来,我分析了一下,查CatchTheCat.exe壳发现: x64 - Zip Sfx Archive
于是使用360zip国际版,右键CatchTheCat.exe打开
发现里面有main.lua,flag.dat,conf.lua(其实根据lua51.dll也可以判断了是lua写的了)
那这很明显了,flag.dat存的应该就是flag。
打开一看,乱码,说明加密了,那就看lua解密。
观察lua文件大小和命名可知,主要逻辑在main.lua。
打开main.lua,因为win了才显示flag,于是我搜索win,得到以下代码片段:
local function getWinMessage()
local content = nil
if love.filesystem.getInfo("assets/flag.dat") then
content = love.filesystem.read("assets/flag.dat")
end
if not content or currentDifficulty ~= "hard" then
return "You WIN!"
end
local key = "52pojie"
local keyLen = #key
local result = {}
local bit = require("bit")
for i = 1, #content do
local b = string.byte(content, i)
local k = string.byte(key, ((i - 1) % keyLen) + 1)
table.insert(result, string.char(bit.bxor(b, k)))
end
return table.concat(result)
end
不难得知是对flag.dat进行xor,key=52pojie。
不想搜索也可将main.lua喂给ds,提示词写:分析一下win之后的表现,也是可以得到结果的。
第二篇windows中级题
首先查壳,发现是标准upx,用upx -d先脱一下
可先打开cm1.exe看一下,发现直接解密会弹窗。
然后拖入ida中,ctrl+f搜索messagebox,发现MessageBoxW,右键__imp_MessageBoxW查找交叉引用,这样就可以快速定位了。
将那段伪代码喂给ai,找到关键一行:v7 = sub_140008720(Str, v5, v6);
查看sub_140008720:
__int64 __fastcall sub_140008720(char *Str, FILE *Stream, FILE *a3)
{
size_t v6; // rax
__int64 v7; // rdi
char v8; // dl
__int64 result; // rax
__int64 v10; // rdi
unsigned __int64 v11; // r13
char *v12; // rax
char *v13; // r12
unsigned __int64 v14; // rax
_BYTE Buffer[16]; // [rsp+20h] [rbp-858h] BYREF
_BYTE v16[2120]; // [rsp+30h] [rbp-848h] BYREF
sub_140008640(v16);
sub_140008500(v16, "52pojie_2026_", 14);
v6 = strlen(Str);
sub_140008500(v16, Str, v6);
v7 = sub_140008580(v16);
fread(Buffer, 0x10u, 1u, Stream);
v8 = sub_140008310(v16, v7, Buffer);
result = 1;
if ( v8 )
{
fseek(Stream, 0, 2);
v10 = ftell(Stream);
result = 2;
v11 = v10 - 16;
if ( (v10 & 7) == 0 )
{
fseek(Stream, 16, 0);
v12 = (char *)malloc(v10 - 16);
v13 = v12;
if ( v12 )
{
fread(v12, 1u, v10 - 16, Stream);
sub_1400081E0(v16, v13, v10 - 16);
if ( (unsigned __int8)sub_1400082E0(v16) )
{
v14 = (unsigned __int8)v13[v10 - 17];
if ( v11 < v14 )
{
free(v13);
return 5;
}
else
{
fwrite(v13, 1u, v11 - v14, a3);
free(v13);
return 0;
}
}
else
{
free(v13);
return 4;
}
}
else
{
return 3;
}
}
}
return result;
}
一样的喂给ai(这里得把里面的函数伪代码一块喂),得知
sub_140008640(v16);
sub_140008500(v16, "52pojie_2026_", 14);
v6 = strlen(Str);
sub_140008500(v16, Str, v6);
v7 = sub_140008580(v16);
这是对 "52pojie_2026_" + 目标flag 计算 CRC-64,得到64位初始密钥。
下面的大致算法是(这是我二次使用ai概括的结果):
从文件偏移 0x08 处读取 16字节作为初始化向量(IV),使用 AES标准S-Box,提供非线性替换。
解密核心变换(每8字节执行一次)
- 位旋转(循环左移)
- 将当前64位状态 循环左移3位
- 高位移出的部分自动补到低位,保持64位长度
- S-Box 字节替换(8轮迭代)
- 循环执行 8次:
- 取当前值的最高8位作为索引
- 查 AES标准S-Box 进行字节替换
- 将替换结果通过位运算拼回原值
- 作用:在64位维度上实现非线性扩散,打乱数据相关性
- 异或反馈(密钥混合)
- 计算公式:密文数据 ⊕ S-Box变换后的密钥数据 ⊕ 额外密钥数据
- 三部分异或后的结果即为解密后的明文
- 将结果写回内存,完成当前8字节解密
校验与去填充(Verification & Unpadding)
| 步骤 |
说明 |
| CRC32校验 |
计算解密后全文的 CRC32,与文件头 0x04 处存储的CRC值对比。 |
| 去除填充 |
取解密后数据的最后一个字节作为长度 L,删除末尾 L 个字节,得到最终明文 |
整体流程图
用户输入字符串 ──┐
▼
CRC-64("52pojie_2026_" + 输入) ──→ 初始密钥
│
▼
读取文件IV (16字节 @ 0x08)
│
▼
┌─────────────────┐
│ 循环处理每8字节 │
│ 1. 循环左移3位 │
│ 2. S-Box查表×8 │
│ 3. 异或密钥还原 │
└─────────────────┘
│
▼
全文CRC32校验 ──→ 失败则退出
│
▼
去填充(删末尾L字节)
│
▼
最终明文
一句话总结
用CRC-64派生密钥,以"旋转→S-Box混淆→异或还原"为核心轮函数逐8字节解密,最后校验CRC并去填充 —— 这是一个结合哈希派生、类流密码变换和标准校验的自定义加密方案。
文件格式结构
加密文件采用以下格式:
- 0x00-0x03: 魔数
b"CM26"(4字节)
- 0x04-0x07: CRC32校验值(小端序,4字节)
- 0x08-0x0F: 初始64位反馈值(小端序,8字节)
- 0x10-末尾: 密文块(每块8字节)
解密文件得到flag
好,现在知道算法了,那么就可以着手得到flag了。
得到这个flag,肯定不能暴力破解,因为crc64的可能有太多太多了。
因为知道最终得到的是png,而png的文件头已知,所以可用已知明文攻击,直接得到crc64的那个目标哈希值,借助ai得到如下代码(别用网页版,得用cli才行,我使用的是gpt-5.3-codex):
import struct
MASK64 = 0xFFFFFFFFFFFFFFFF
def ror64(value: int, shift: int = 3) -> int:
return ((value >> shift) | ((value & ((1 << shift) - 1)) << (64 - shift))) & MASK64
# AES逆S盒
AES_SBOX_INV = [0] * 256
for i, val in enumerate([
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
]):
AES_SBOX_INV[val] = i
def sbox_mix_64_inv(value: int) -> int:
"""一轮逆运算"""
for _ in range(8):
bottom = value & 0xFF
value = (value >> 8) | (AES_SBOX_INV[bottom] << 56)
return value
def get_crc64_hash(encrypted_data: bytes) -> int:
feedback0 = struct.unpack_from("<Q", encrypted_data, 8)[0]
block1 = struct.unpack_from("<Q", encrypted_data, 0x10)[0]
png_header = 0x0A1A0A0D474E5089 # PNG文件头
# state1 = block1 ^ png_header ^ feedback0
state1 = block1 ^ png_header ^ feedback0
# 逆运算得到rol_state0,再右移3位得到state0
state0 = ror64(sbox_mix_64_inv(state1), 3)
return state0 # 这就是INITIAL_KEY
c=get_crc64_hash(open("flag.png.encrypted", "rb").read())
print(hex(c))
利用该哈希值,可以直接跳过flag的获取,直接按正常流程解密(就把上面得到的crc64哈希值和算法给它):
"""
Decrypt a CM26-encrypted PNG file.
Format:
- 0x00..0x03: magic (b"CM26")
- 0x04..0x07: CRC32 (little-endian) of padded plaintext
- 0x08..0x0F: initial 64-bit feedback value (little-endian)
- 0x10..end : ciphertext blocks (8 bytes each)
"""
from __future__ import annotations
import argparse
import struct
import sys
import zlib
from pathlib import Path
MASK64 = 0xFFFFFFFFFFFFFFFF
INITIAL_KEY = 0x55A4F867BA4475DD
AES_SBOX = [
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
]
def rol64(value: int, shift: int = 3) -> int:
return ((value << shift) & MASK64) | (value >> (64 - shift))
def sbox_mix_64(value: int) -> int:
"""Apply AES S-Box over 8 bytes from high byte to low byte."""
for _ in range(8):
top = (value >> 56) & 0xFF
value = ((value << 8) & MASK64) | AES_SBOX[top]
return value
def pkcs7_unpad(data: bytes, block_size: int = 8) -> bytes:
if not data:
raise ValueError("empty plaintext")
pad_len = data[-1]
if pad_len < 1 or pad_len > block_size:
raise ValueError(f"invalid padding length: {pad_len}")
if data[-pad_len:] != bytes([pad_len]) * pad_len:
raise ValueError("invalid PKCS#7 padding bytes")
return data[:-pad_len]
def decrypt_cm26(data: bytes, key: int = INITIAL_KEY, strict_magic: bool = True) -> bytes:
if len(data) < 0x10:
raise ValueError("input too short")
magic = data[:4]
if strict_magic and magic != b"CM26":
raise ValueError(f"unexpected magic: {magic!r}")
expected_crc = struct.unpack_from("<I", data, 4)[0]
feedback = struct.unpack_from("<Q", data, 8)[0]
ciphertext = data[0x10:]
if len(ciphertext) % 8 != 0:
raise ValueError("ciphertext length is not a multiple of 8")
state = key & MASK64
padded_plain = bytearray()
for i in range(0, len(ciphertext), 8):
block = struct.unpack_from("<Q", ciphertext, i)[0]
state = sbox_mix_64(rol64(state, 3))
plain_qword = block ^ state ^ feedback
padded_plain += struct.pack("<Q", plain_qword)
feedback = block
calc_crc = zlib.crc32(padded_plain) & 0xFFFFFFFF
if calc_crc != expected_crc:
raise ValueError(
f"CRC32 mismatch: expected 0x{expected_crc:08x}, got 0x{calc_crc:08x}"
)
return pkcs7_unpad(bytes(padded_plain), block_size=8)
def main() -> int:
parser = argparse.ArgumentParser(description="Decrypt CM26-encrypted PNG")
parser.add_argument("input", type=Path, help="encrypted file path")
parser.add_argument("output", type=Path, nargs="?", default=Path("flag.png"), help="output PNG path")
parser.add_argument(
"--key",
type=lambda x: int(x, 0),
default=INITIAL_KEY,
help="64-bit initial key (default: 0x55A4F867BA4475DD)",
)
parser.add_argument("--no-magic-check", action="store_true", help="skip CM26 magic validation")
args = parser.parse_args()
encrypted = args.input.read_bytes()
plain = decrypt_cm26(encrypted, key=args.key, strict_magic=not args.no_magic_check)
if not plain.startswith(b"\x89PNG\r\n\x1a\n"):
print("[!] Decrypted data does not start with PNG signature", file=sys.stderr)
args.output.write_bytes(plain)
print(f"[+] Decrypted OK: {args.output} ({len(plain)} bytes)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
这样就得到了一张解密好的图片:
可用notepad4打开看到明文flag:flag{EncrypTIoN_Is_haRd_52p0jIE_2o26_m62Tc4uj78maAq1C}
安卓中级题
我不会,但朋友用cc跑了7小时硬是将算法静态分析出来了。
但奇怪的是正确的flag投喂仍然不对,我用frida调用了setDebugBypass也不行。
web题
js代码简析
先去看一眼js层语音是怎么合成的,发现
- 函数(uid, voice),返回值:{a:音频数据,h:一串hash值}
- 函数checkCode是将输入的不含flag{}的{}中的数据进行0x2026次SHA-256,并与h进行比较。
这就不难猜测,h值是通过验证码文本进行0x2026次SHA-256得到的,a也与验证码文本有关。
还发现了一个关键导入函数wbg.__wbg_getRandomValues_1c61fac11405ffdc,使用了crypto.getRandomValues
于是hook crypto.getRandomValues将值固定
(function() {
const rawCrypto = window.crypto;
const cryptoProxy = new Proxy(rawCrypto, {
get(target, prop) {
// FIX 1: Only pass target and prop.
// Do NOT pass 'receiver' (the 3rd argument).
const value = Reflect.get(target, prop);
// Hook logic for getRandomValues
if (prop === 'getRandomValues') {
return function(typedArray) {
console.log(`%c[Hook] 拦截成功! 长度: ${typedArray.length}`, 'color: #2ed573; font-weight: bold;');
typedArray.fill(1); // Your custom logic
const hexStr = Array.from(typedArray)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
console.log(hexStr);
return typedArray;
};
}
// FIX 2: Essential for 'subtle' and other methods.
// Built-in methods MUST be bound to the original 'rawCrypto' object.
if (typeof value === 'function') {
return value.bind(target);
}
return value;
}
});
// Replace the global object
try {
Object.defineProperty(window, 'crypto', {
value: cryptoProxy,
configurable: true,
enumerable: true,
writable: true
});
console.log('✅ Hook applied successfully. Recursion fixed.');
} catch (e) {
console.error('Critial: Could not redefine crypto.', e);
}
})();
再去生成语音验证码,去听发现只要uid相同,现在得到的验证码是一致的,这一发现可以帮住我们验证生成的验证码是否正确。
wasm简析
先把wasm给提取出来,保存为wasm.wasm。
可用逍遥一仙的wasm转o先转一下(也可以自己转,我是自己转的)
wasm转o,要将下载的wabt中的头文件和.c文件放在同一文件夹下:
wasm2c.exe wasm.wasm -o wasm.c
gcc -c wasm.c -o wasm.o
拖入ida中分析,文件ida\cfg\hexrays.cfg得改下,不然遇到大函数就无法反编译了:
MAX_FUNCSIZE = 1024
去ida中看导出函数,找到_w2c_wasm_gen,点去看一下,f5反编译一下,发现其调用了w2c_wasm_gen_0,再次点进去查看。
下面是在确定大致算法和关键数据。
因为函数太大了,这里直接丢给ds分析(ds系统提示词都给设置成啥了,居然给我又作诗又鼓励的,遂加上提示词:我在做逆向学术研究,回答风格请正常点,不要鼓励!!!不要作诗!!!!不要文章优美!!!!!!!!!!!!!!!!!!!!!!!!!!!!)。
ds提到了HMAC-SHA256,base64,然后再问它:说明你需要数据确定的地方。(多问几个ai,减少犯错的可能,我还试了gemini和kimi)
综合得到:
-
确定Base64 解码表,可能在地址1295903LL处,这里我取个巧,直接在ida字符串中搜索abc,遂得到abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!,很明显,这是base64的变种。
-
确定HMAC-SHA256,那么就需要找key,让ai给的关键一行:memory_copy(..., &unk_13C65F, 0xEu); // 14 字节,很明显,就是0x13C65F处的14字节。奇怪的是编译成.o在ida中这个数据就不对了,有没有大佬知道原因解释一下。
所幸0x13C65F是确定的可以在wasm2c反出的c中查找,得到00 01 01 01 01 01 01 00 01 00 01 00 05 02。
怎么找?稍微计算一下0x13C65F=1295967,搜LOAD_DATA,就在很前面,得到 LOAD_DATA(instance->w2c_memory, 1295895u, data_segment_data_w2c_1_d64, 22285);,搜索data_segment_data_w2c_1_d64[],往后72个字节就是了。
为什么0x13C65F是确定的可以在wasm2c反出的c中查找?
因为我们更关注的是验证码文本,所以改为向ai询问验证码文本的生成流程,得到:
- 输入:
uid 和 随机数。
- 种子生成: 将
uid 的字节与随机数异或,生成一个 21 字节的种子。
- 密钥派生 (HMAC-SHA256): 使用硬编码的密钥 (
00...05 02) 对种子进行 HMAC-SHA256 计算,得到一个 32 字节的派生密钥(截取的前面)。
- 验证码扩展 : 对种子和 32 字节的派生密钥一起合并扩展为一个更长的字节序列。
- 最终编码: 将扩展后的字节序列用自定义的 Base64 字符集 (
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!) 编码成 50 个字符的字符串。这就是最终的 code。
得到代码
其实得到关键数据后就可以偷波懒了,直接将gen的伪代码和关键数据和题目信息一起给gpt-5.3 codex,推理开超高,说要得到验证码文本,不一会儿就出了。
from __future__ import annotations
import argparse
import hashlib
import hmac
import os
from dataclasses import dataclass
# Extracted from gen.c:4318-4321
CUSTOM_B64_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!"
# Extracted from gen.c:338 + gen.c:4323-4336 (14 bytes copied from unk_13C65F)
HMAC_KEY = bytes([
0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x00, 0x01, 0x00, 0x01, 0x00, 0x05, 0x02,
])
@dataclass(frozen=True)
class CodeMaterial:
uid: int
random17: bytes
payload21: bytes
signature16: bytes
payload37: bytes
code: str
def custom_b64_encode_no_padding(data: bytes, alphabet: str = CUSTOM_B64_ALPHABET) -> str:
if len(alphabet) != 64:
raise ValueError("alphabet must contain exactly 64 characters")
out: list[str] = []
acc = 0
bits = 0
for b in data:
acc = ((acc << 8) | b) & 0xFFFFFFFFFFFFFFFF
bits += 8
while bits >= 6:
bits -= 6
out.append(alphabet[(acc >> bits) & 0x3F])
if bits:
out.append(alphabet[(acc << (6 - bits)) & 0x3F])
return "".join(out)
def build_payload21(uid: int, random17: bytes) -> bytes:
if len(random17) != 17:
raise ValueError("random17 must be exactly 17 bytes")
uid_le = (uid & 0xFFFFFFFF).to_bytes(4, "little")
head4 = bytes(uid_le[i] ^ random17[i] for i in range(4))
return head4 + random17
def generate_code_material(uid: int, random17: bytes | None = None) -> CodeMaterial:
if random17 is None:
random17 = os.urandom(17)
payload21 = build_payload21(uid, random17)
# Inferred from gen.c:359-415 (ipad/opad) + gen.c:2997-3014 (SHA-256 rounds).
signature16 = hmac.new(HMAC_KEY, payload21, hashlib.sha256).digest()[:16]
payload37 = payload21 + signature16
code = custom_b64_encode_no_padding(payload37)
return CodeMaterial(
uid=uid & 0xFFFFFFFF,
random17=random17,
payload21=payload21,
signature16=signature16,
payload37=payload37,
code=code,
)
def check_hash(code: str, rounds: int = 0x2026) -> str:
current = code.encode("utf-8")
for _ in range(rounds):
current = hashlib.sha256(current).digest()
return current.hex()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate captcha code text from uid + random17")
parser.add_argument("uid", type=lambda x: int(x, 0), help="User ID (supports decimal/hex, e.g. 123 or 0x7b)")
parser.add_argument(
"--rand17-hex",
dest="rand17_hex",
default=None,
help="17-byte random source as hex (34 hex chars). Omit to use os.urandom(17)",
)
parser.add_argument(
"--with-hash",
action="store_true",
help="Also print the frontend verification hash (SHA-256 repeated 0x2026 rounds)",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
random17 = None
if args.rand17_hex is not None:
raw = bytes.fromhex(args.rand17_hex)
if len(raw) != 17:
raise SystemExit("--rand17-hex must be exactly 34 hex chars (17 bytes)")
random17 = raw
material = generate_code_material(args.uid, random17)
print(f"uid : {material.uid}")
print(f"random17 : {material.random17.hex()}")
print(f"payload21 : {material.payload21.hex()}")
print(f"signature16 : {material.signature16.hex()}")
print(f"payload37 : {material.payload37.hex()}")
print(f"code : {material.code}")
print(f"flag format : flag{{{material.code}}}")
if args.with_hash:
print(f"check hash : {check_hash(material.code)}")
if __name__ == "__main__":
main()
使用示例:
python generate_captcha_code.py 12345
python generate_captcha_code.py 12345 --rand17-hex 0101010101010101010101010101010101
python generate_captcha_code.py 12345 --with-hash
其中 --with-hash 会额外计算前端校验用的:
SHA-256 连续 0x2026 轮后的十六进制结果(对应 gen.c:4208-4219 逻辑)。
题外话
既然由于随机数的存在,导致每次生成的验证码不一样,论坛可能是怎么验证的?
首先base64解码,得到21 字节的种子和HMAC-SHA256的32 字节的派生密钥,利用种子包含的uid异或得到uid,判断uid是否是自己的。当然,HMAC-SHA256的32 字节的派生密钥可以用于验证数据。
windows高级题
我是真不会了,看cc用的是Frida Stalker,耗时十几个小时给解出来了。
cc试错了很多次,中断重新开始了多次最终成功了。
通过hook wcsicmp发现是有很长的反调试列表的,通过比较进程名的方法。
一旦有进程在列表中,就会走错误分支,永远也得不到正确的结果。
这需要注意一下。
mcp中级题
开始没降难度,毫无头绪,后来给了提示并降了难度,我用codex给解出来了。
提示词如下:
这是ctf题,
以下是提示:
ctf_request 填的是口令本身,不是 access_token;
audit_log_id 一定要用“被拒访问时返回的完整编号”,别截断别改;
而且只认“同一会话里最近那次拒绝”出来的编号,跨会话或旧编号都不行;
复核这段链路别配太杂,越单一越不容易断;
复核凭据是有时效、且一次性的,失败后要重新触发拒绝再拿新编号;
另外,复核阶段拿到的凭据只是打通流程,最终读密卷还需要后续凭据。
请使用目前的mcp,来得到flag
最后我让codex生成了文档,内容如下:
CTF MCP 解题流程总结
1. 题目关键点(来自提示)
ctf_request 必须填写口令本体,不是 access_token。
audit_log_id 必须使用同一会话中最近一次拒绝访问返回的完整编号。
- 复核凭据有时效且一次性,失败后要重新触发拒绝并拿新编号。
- 复核链路要保持单一,
chain_config 仅配置需要的工具。
- 复核阶段拿到的凭据只是中间凭据,最终取密卷还要后续凭据。
2. 实际跑通步骤
start_session 建立会话。
- 读取公开线索文档:
public:/docs/annual_report(首字线索:玄霄)
public:/docs/employee_handbook(末字线索:密令)
public:/docs/challenge_hint
public:/docs/board_route_hint
- 得到口令:
玄霄密令。
- 访问
secret:/board/resolutions 触发拒绝,拿到最新 audit_log_id。
- 计算:
seal = sha256(audit_log_id + passphrase)[:8](小写十六进制)。
- 调用
review_document:
doc_id = confidential:/audit/logs
review_notes 格式:
ctf_request=玄霄密令
audit_log_id=<最新拒绝编号>
seal=<上一步计算值>
- 返回
tool_access_token 后,调用 update_tool_chain,并将 chain_config 设置为仅 ["get_audit_log"]。
- 用
tool_access_token 调用 get_audit_log(log_id=<audit_log_id>),拿到 _audit_token。
- 用
_audit_token 调用 get_document(secret:/board/resolutions),拿到最终 flag。
3. 结果
- 最终 flag:
flag{new_year_2026_keep_warm}
compliance_blob_b64 中的 FLAG{HAPPYNEWYEAR-WUAIPOJIE} 是干扰信息,不是最终答案。
4. 常见失败原因
- 把
ctf_request 误填成 access_token。
audit_log_id 不是“同会话最近一次拒绝”的编号。
audit_log_id 复制时被截断或改写。
chain_config 混入了 get_audit_log 以外的工具导致凭据作废。
- 失败后继续用旧凭据(应重新触发拒绝并重跑)。
感受
现在ai发展的真迅猛,很多逆向都可以完全利用ai秒杀了,我们也应学会利用ai。
已经不敢想象在过个几年,ai会发展成什么样子了。
附件
蓝奏云链接