文章总结: 本文档是吾爱破解2026年春节所有题目的解题方法(WP)分享。内容涵盖了Windows、Android等多个平台的初级及中级题目,详细记录了使用IDA、ResourceHacker等工具进行逆向分析、查壳脱壳、反编译以及编写解密脚本的全过程,并最终给出了各个题目的flag答案。 综合评分: 85 文章分类: CTF,逆向分析,二进制安全,WEB安全,渗透测试
得到 flag:flag{EncrypTIoN_Is_haRd_52p0jIE_2o26_m62Tc4uj78maAq1C}
Android 中级题
通过 Java 层分析确认两件事:
-
assets/hjm_pack.bin是关键资源。
-
输入字符串会进入
NativeBridge.verifyAndDecrypt(),其返回字节后续会被h1.a.S(...)解析。 -
成功判定点是:
h1.a.S((byte[]) obj) == null时走“验证成功”。
拖入 IDA 后确认:
| Java 方法 | Native 偏移 | | — | — | | startSessionBytes | 0x247b0 | | checkRhythm | 0x24da8 | | updateExp | 0x24ea4 | | decryptFrames | 0x2541c | | verifyAndDecrypt | 0x257dc | | setDebugBypass | 0x25c90 |
进入 verifyAndDecrypt 后发现不是直接字符串比较,而是:
- 解包得到目标位图
- 将输入字符串转位图
- 比较位图
进一步分析得到关键函数:
- sub_2dcdc:key 生成
- sub_2ddf8:version=2 解包核心
- 输出改写后的 pack 数据并还原位图读文本
Unicorn 中跑最小链路,直接执行:
复制代码 隐藏代码
sub_2dcdc
sub_2ddf8
拿到改写后的 pack 数据。
分析前 4 个字节:48 4A 4D 31 ,即:HJM1。
解析脚本:
复制代码 隐藏代码
import struct
from pathlib import Path
FILE_PATH = "pack.dec.bin"
defunpack_1bpp(frame: bytes, w: int, h: int) -> bytes:
out = bytearray(w * h)
for i inrange(w * h):
b = frame[i >> 3]
out[i] = (b >> (7 - (i & 7))) & 1
returnbytes(out)
defrender(bits: bytes, w: int, h: int, on="#", off=" "):
for y inrange(h):
row = bits[y * w:(y + 1) * w]
print("".join(on if p else off for p in row))
defmain():
buf = Path(FILE_PATH).read_bytes()
if buf[:4] != b"HJM1":
raise ValueError(f"not HJM1 file, len={len(buf)}")
ver, frames, w, h = struct.unpack("<4I", buf[4:20])
frame_bytes = (w * h + 7) // 8
need = frames * frame_bytes
iflen(buf) < need:
raise ValueError(f"file too short, len={len(buf)}, need={need}")
payload_off = len(buf) - need
frame0 = buf[payload_off:payload_off + frame_bytes]
print(f"HJM1 v{ver}, frames={frames}, w={w}, h={h}, payload_off=0x{payload_off:x}")
bits = unpack_1bpp(frame0, w, h)
render(bits, w, h)
if __name__ == "__main__":
main()
得到 flag:FLAG{HJMWAPJ2026NBLD}。
Web 中级题
先看 verify.js :
- 点击“生成验证码”会调用
wasm_bindgen.gen(uid, voice)。 - 生成后会把 currentHash 赋值为 challenge.h。
- 点击“提交”时执行
checkCode(code, currentHash)。 - checkCode 逻辑是:code 连续做 0x2026 次 SHA-256,再和 currentHash 比较。
- WASM 侧随机输入来自
crypto.getRandomValues,长度固定为 17 字节(可 hook 验证)。
所以核心不是听语音,而是拿到当前轮次的 code,即可构造 flag{code}。
生成验证码:
复制代码 隐藏代码
const challenge = wasm_bindgen.gen(uid, voice)
currentHash = challenge.h
提交:
复制代码 隐藏代码
checkCode(code, currentHash)
关键常量从 wasm.memory.buffer 读取:
- 映射表:abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!。
- 14 字节 key:00 01 01 01 01 01 01 00 01 00 01 00 05 02。
一把梭:
复制代码 隐藏代码
(() => {
const keyBytes = Uint8Array.from([0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x01,0x00,0x01,0x00,0x05,0x02]);
const table = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!";
let lastRand = null;
if (!window.__grv_hooked__) {
const orig = crypto.getRandomValues.bind(crypto);
crypto.getRandomValues = (arr) => {
const ret = orig(arr);
if (arr && arr.length === 17) lastRand = newUint8Array(arr);
return ret;
};
window.__grv_hooked__ = true;
}
functiondecodeFromJ(j) {
let a = 0, f = 1, b = 0, h = 0, c = 0, i = 0, l = 0;
const out = [];
while (true) {
const t0 = j[f - 1];
l = t0;
h = (l | (h << 8));
c = b;
while (true) {
i = (h >> (b = c + 2)) & 63;
out.push(table[i]);
a++;
c -= 6;
if (b > 5) continue;
break;
}
b = c + 8;
const cont = f !== 37;
f += cont ? 1 : 0;
if (!cont) break;
}
if (c !== -8) out.push(table[(l << (-2 - c)) & 63]);
return out.join("");
}
asyncfunctionhmac16(first21) {
const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const sig = await crypto.subtle.sign("HMAC", key, first21);
returnnewUint8Array(sig).slice(0, 16);
}
window.genFlag = async (uid = 2355817, voice = "c") => {
document.getElementById("uid").value = String(uid);
document.getElementById("voice").value = voice;
lastRand = null;
document.getElementById("checkbox-text").click();
awaitnewPromise(r =>setTimeout(r, 1200));
if (!lastRand) thrownewError("No random number captured");
const j = newUint8Array(37);
j[0] = lastRand[0] ^ (uid & 0xff);
j[1] = lastRand[1] ^ ((uid >>> 8) & 0xff);
j[2] = lastRand[2] ^ ((uid >>> 16) & 0xff);
j[3] = lastRand[3] ^ ((uid >>> 24) & 0xff);
j.set(lastRand.slice(0, 8), 4);
j.set(lastRand.slice(8, 16), 12);
j[20] = lastRand[16];
j.set(awaithmac16(j.slice(0, 21)), 21);
const code = decodeFromJ(j);
const flag = `flag{${code}}`;
document.getElementById("verifyInput").value = flag;
return { flag, code };
};
})();
使用:
复制代码 隐藏代码
awaitgenFlag(uid, "c")
Windows 高级题
最折磨我的一题。
一开始我发现有 UPX 壳,想着先自动脱壳试试,结果脱下来了。 我还纳闷,拖进 IDA 发现,原来是控制流混淆等着我呢。
打开字符串果然加密了,但是我在 Imports 中找到了 WideCharToMultiByte 函数,xref 定位到 sub_1400CD490 验证入口。
分析入口得到关键函数:
- sub_1400CF090:把输入 flag 的 hex 文本转字节。
- sub_1400CF270:长度校验(必须是 0x40 字节)。
- sub_1400CF910:核心校验调度(含反调试)。
- sub_1400FD790:目标值生成(混淆太严重)。
- sub_1400D3B20:最终 64 字节比较点。
sub_1400CD490
在 sub_1400CF090 汇编里面明确看到:
- 先把输入的长度除以 2(每两位 hex 组成 1 字节)
- 分支判断 0-9 / a-f / A-F。
- 组合方式为 (high << 4) | low 写入输出缓冲。
复制代码 隐藏代码
loc_1400CF0F5:
shl bpl, 4
or bpl, dl
cmp rax, rdi
jz short loc_1400CF140
sub_1400CF270 虽然有点混淆,但关键点非常直白。长度必须是 0x40 字节,结合前面分析得到:
复制代码 隐藏代码
解码后长度必须是 = 64 字节
输入长度必须是 = 128 个 hex 字符
sub_1400CF910 汇编内可见:
- 多处反调试。
- 目标缓冲生成后进入最终比较调用。
- 比较前明确
mov r8d, 40h,随后call sub_1400D3B20
结论:输入的 flag 必须是 128 个 hex 字符(解码后 64 字节)。
知道这些信息后,就无需再做静态分析,直接上 x64dbg 动态调试。
前面分析知道是有反调试的,但 ScyllaHide 基本能过掉,也不用操心。
直接在最终比较点 sub_1400D3B20 下断点。程序跑起来后输入uid 和错的 flag(128 hex),点击验证 flag,程序断在比较函数。
此时在寄存器 RCX/RDX -> 在转储中跟随,RCX是刚才输入的,RDX是真实的 flag。
比如我的 uid 是:2355817 ,那么对应 flag 就是:06401594023537c80f8cffede100b0fd4cb3bb4c6feb890bc8dabafd24f68d92b16084f2ae4ea6cc3567eea9a91e90c364e7f620407304b820ac44ccb6db6987
注册机:
复制代码 隐藏代码
import frida
import glob
import time
# 你的 uid
uid = "uid"
targets = glob.glob(
"【2026春节】解题领红包之十 {Windows 高级题} 出题老师:Poner.exe")
ifnot targets:
raise SystemExit("target exe not found")
target = targets[0]
js = r'''
const base = Process.enumerateModules()[0].base;
function p(off){ return base.add(off); }
const f_cd490 = new NativeFunction(p(0xCD490), 'uint64', ['pointer','pointer']);
const g_b418 = p(0x2632418);
const g_b419 = p(0x2632419);
let lastY = '';
function toHex(ptr, n){
const u = new Uint8Array(ptr.readByteArray(n)); let s = ''; for (let i = 0; i < u.length; i++) { let h = u[i].toString(16); if (h.length < 2) h = '0' + h; s += h; } return s;}
Interceptor.attach(p(0xD3B20), {
onEnter(args){ lastY = toHex(args[1], 64); }});
Interceptor.attach(p(0x0C1B90), { onLeave(ret){ ret.replace(ptr(0)); } });
Interceptor.attach(p(0x09B30), { onLeave(ret){ ret.replace(ptr(0)); } });
rpc.exports = {
derive(uid){ g_b418.writeU8(0); g_b419.writeU8(0); lastY = ''; const pUid = Memory.allocUtf16String(uid); const pDummy = Memory.allocUtf16String('00'.repeat(64)); f_cd490(pUid, pDummy); return lastY; }, verify(uid, flag){ g_b418.writeU8(0); g_b419.writeU8(0); const pUid = Memory.allocUtf16String(uid); const pFlag = Memory.allocUtf16String(flag); return f_cd490(pUid, pFlag).toString(); }};
'''
pid = frida.spawn([target])
session = frida.attach(pid)
script = session.create_script(js)
script.load()
frida.resume(pid)
time.sleep(1)
api = script.exports_sync
flag = api.derive(uid)
ret = api.verify(uid, flag)
print("uid =", uid)
print("flag =", flag)
print("verify_ret =", ret)
frida.kill(pid)
MCP 中级题
提示词发给 AI :
复制代码 隐藏代码
ctf_request 填的是口令本身,不是 access_token;
audit_log_id 一定要用“被拒访问时返回的完整编号”,别截断别改;
而且只认“同一会话里最近那次拒绝”出来的编号,跨会话或旧编号都不行;
复核这段链路别配太杂,越单一越不容易断;
复核凭据是有时效、且一次性的,失败后要重新触发拒绝再拿新编号;
另外,复核阶段拿到的凭据只是打通流程,最终读密卷还需要后续凭据。
最终跑出 flag:flag{new_year_2026_keep_warm}
-官方论坛
www.52pojie.cn
👆👆👆
公众号设置“星标”,您不会错过新的消息通知
如开放注册、精华文章和周边活动等公告
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:吾爱破解论坛 吾爱pojie 吾爱pojie《吾爱破解 2026 春节所有题 WP》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论