文章总结: 本文分享了2026年春节活动中大部分题目的解题方法,主要包括安卓初级题和两篇Windows初级题。其中,Windows初级题的解法涵盖了使用IDAPro分析C/C++代码、借助AI工具(如Gemini)进行逆向分析,以及通过pyinstxtractor工具处理PyInstaller编译的Python程序等多种技巧。 综合评分: 85 文章分类: CTF,逆向分析,WEB安全,安全工具,实战经验
利用该哈希值,可以直接跳过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,
]
defrol64(value: int, shift: int = 3) -> int:
return ((value << shift) & MASK64) | (value >> (64 - shift))
defsbox_mix_64(value: int) -> int:
"""Apply AES S-Box over 8 bytes from high byte to low byte."""
for _ inrange(8):
top = (value >> 56) & 0xFF
value = ((value << 8) & MASK64) | AES_SBOX[top]
return value
defpkcs7_unpad(data: bytes, block_size: int = 8) -> bytes:
ifnot data:
raise ValueError("empty plaintext")
pad_len = data[-1]
if pad_len < 1or 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]
defdecrypt_cm26(data: bytes, key: int = INITIAL_KEY, strict_magic: bool = True) -> bytes:
iflen(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:]
iflen(ciphertext) % 8 != 0:
raise ValueError("ciphertext length is not a multiple of 8")
state = key & MASK64
padded_plain = bytearray()
for i inrange(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)
defmain() -> 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)
ifnot 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)")
return0
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 = newProxy(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') {
returnfunction(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)
classCodeMaterial:
uid: int
random17: bytes
payload21: bytes
signature16: bytes
payload37: bytes
code: str
defcustom_b64_encode_no_padding(data: bytes, alphabet: str = CUSTOM_B64_ALPHABET) -> str:
iflen(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)
defbuild_payload21(uid: int, random17: bytes) -> bytes:
iflen(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 inrange(4))
return head4 + random17
defgenerate_code_material(uid: int, random17: bytes | None = None) -> CodeMaterial:
if random17 isNone:
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,
)
defcheck_hash(code: str, rounds: int = 0x2026) -> str:
current = code.encode("utf-8")
for _ inrange(rounds):
current = hashlib.sha256(current).digest()
return current.hex()
defparse_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()
defmain() -> None:
args = parse_args()
random17 = None
if args.rand17_hex isnotNone:
raw = bytes.fromhex(args.rand17_hex)
iflen(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:
-
ctf_request=玄霄密令 -
audit_log_id=<最新拒绝编号> -
seal=<上一步计算值> -
doc_id = confidential:/audit/logs -
review_notes格式:
- 返回
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会发展成什么样子了。
附件
https://wwavz.lanzout.com/ivS7W3jidv8d
-官方论坛
www.52pojie.cn
👆👆👆
公众号设置“星标”,您不会错过新的消息通知
如开放注册、精华文章和周边活动等公告
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:吾爱破解论坛 吾爱pojie 吾爱pojie《【2026春节】解题领红包大部分题题解》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论