吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 222|回复: 8
上一主题 下一主题
收起左侧

[Android 原创] 2026春节红包解题-安卓中级

[复制链接]
跳转到指定楼层
楼主
WXjzc 发表于 2026-3-4 09:58 回帖奖励
本帖最后由 WXjzc 于 2026-3-4 10:11 编辑

AI

先大致看了一下,流程太复杂,涉及多种算法,opencode+codex+mcp直接梭哈,目录中包含了一些提取出来的数据(这是因为之前用trae cn解的,ai太蠢了啥都不会,实际上这些数据mcp都能拿到。。)

最终耗时19分钟得出结果

接下来我问他详细的流程、

好的,我按“程序真实执行顺序”把这题的解密流程完整讲一遍。

调用链

Java 层入口是 NativeBridge.verifyAndDecrypt(byte[] bArr, String str),在 Q0.z 里被调用。
bArr 来自资源文件 assets/hjm_pack.bin(Q0.y 里 getAssets().open("hjm_pack.bin"))。
JNI 注册在 JNI_OnLoad,verifyAndDecrypt 对应 native 函数就是 sub_257DC。
同一组 JNI 里还有 startSessionBytes -> sub_247B0、setDebugBypass -> sub_25C90,这两个会影响 sub_257DC 的密钥状态。
hjm_pack.bin 结构

包头前 24 字节是 6 个小端 u32,你这个文件解析出来是:HJM1、mode=2、frameCount=1、width=64、height=64、extra=120。
payload_size = frameCount * width * height / 8 = 1 * 64 * 64 / 8 = 512。
整包长度 52 + payload_size = 564,和文件长度一致。
所以 offset 52 开始的 512 字节就是“最终要和输入字符串渲染图比对的位图数据”。
sub_257DC 在做什么

先校验参数和头:bArr、inputStr 非空,长度足够,magic 必须是 HJM1。
调 sub_25EF8 做环境/状态采样,更新全局状态(dword_5EA4C、dword_5EA50、qword_5EA30 等)。
根据包头里的 mode 分两条路:mode=1 或 mode=2,本题是 mode=2。
mode=2 下会先做权限/反作弊门控,再调用 sub_2DDF8 解密 bArr+52 的 payload。
然后把 inputStr 交给 sub_2E5FC 渲染成位图缓冲区(尺寸 width*height/8)。
最后执行 memcmp(bArr+52, rendered_bitmap, width*height/8),完全相等才返回原始 bArr,否则返回空数组。
mode=2 真正解密细节(sub_2DDF8)

先再次做结构校验:magic/mode/尺寸/payload 长度是否自洽。
取头部 24..39 和全局 qword_5EA30,通过 sub_2E170 派生出一对 64 位种子。
payload 按 256 字节分块解密,每块会构造 32 字节临时上下文,再两次 sub_25CA8(..., 0x1357)、sub_25CA8(..., 0x2468) 得到 16 字节块密钥。
再用头部 40..51 作为 nonce 基础,混入“块序号 + 全局状态”生成计数器前缀,交给 sub_2D678 做流式异或解密。
sub_2D678 实质是 AES-CTR(S-box/Rcon 就是你目录里的 1694D.bin / 16a4d.bin)。
若 frameCount > 1 还会用 sub_2E2E8 生成置换表重排帧;本题 frameCount=1,这步等价于不重排。
输入字符串如何被渲染(sub_2E5FC)

字体表是 5x7 点阵,字符映射来自 byte_16A58(你目录里的 char_map.txt 就是这个表的人类可读版)。
每字符占 6x8 布局单元(5x7 字形 + 1 列/1 行间隔),会自动按宽度换行并居中布局。
64 宽时每行最多 10 个字符。
函数会把字符串转小写去匹配字模(字库里大写和小写都有),逐 bit 写入位图缓冲区。
任何一个字形找不到,渲染就失败,最终 verifyAndDecrypt 返回空。
本题如何落地拿到 flag

关键是先得到正确解密后的 payload 位图,再把位图按 char_map.txt 做 OCR。
本题 mode=2,若走 setDebugBypass(true) 路径(sub_25C90 设置 byte_5EB88=1),sub_257DC 会用 sub_2DCDC() 生成验证密钥并允许解密。
用 APK 里的节奏初始表 [0,250,500,750](startSession)可复现初始全局状态,解出 payload 后按 5x7 字库还原文本。
还原结果就是:FLAG{HJMWAPJ2026NBLD}。
如果你愿意,我下一步可以给你一版“极简 Python 复现脚本思路”(只保留必要函数:sub_25CA8、sub_2D248、sub_2DDF8 中的 CTR 部分、5x7 OCR)。

之后让生成完整的解密脚本

#!/usr/bin/env python3
"""
完整解密脚本(基于 sub_257DC / sub_2DDF8 / sub_2E5FC 逆向逻辑)

默认使用当前目录文件:
- hjm_pack.bin
- 1694D.bin       (AES S-Box)
- 16a4d.bin       (AES Rcon)
- char_map.txt    (5x7 字模)

运行:
    python solve.py
"""

from __future__ import annotations

import argparse
import struct
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple

MASK64 = (1 << 64) - 1

# 常量来自 so 内部
CONST_MAGIC = 0x1A8CBC5B802E097C
CONST_MAGIC_ALT = 0x802E097C1A8CBC5B
CONST_GOLDEN_NEG = 0x61C8864680B583EB

def u64(x: int) -> int:
    return x & MASK64

def ror(x: int, n: int) -> int:
    x &= MASK64
    return ((x >> n) | ((x << (64 - n)) & MASK64)) & MASK64

def read_u64_le(b: bytes, off: int) -> int:
    return struct.unpack_from("<Q", b, off)[0]

def read_u32_le(b: bytes, off: int) -> int:
    return struct.unpack_from("<I", b, off)[0]

@dataclass
class HjmHeader:
    magic: bytes
    mode: int
    frame_count: int
    width: int
    height: int
    extra: int

    @property
    def frame_bytes(self) -> int:
        return (self.width * self.height) >> 3

    @property
    def payload_bytes(self) -> int:
        return self.frame_count * self.frame_bytes

def parse_hjm_header(buf: bytes) -> HjmHeader:
    if len(buf) < 24:
        raise ValueError("buffer too small (<24)")
    return HjmHeader(
        magic=buf[0:4],
        mode=read_u32_le(buf, 4),
        frame_count=read_u32_le(buf, 8),
        width=read_u32_le(buf, 12),
        height=read_u32_le(buf, 16),
        extra=read_u32_le(buf, 20),
    )

# -------------------------------
# hash / mix primitives
# -------------------------------

def sub_2D248(data: bytes, a3: int, a4: int) -> int:
    """对应 sub_2D248(SipHash 风格混淆)"""
    v4 = u64(a3 ^ 0x736F6D6570736575)
    v5 = u64(a4 ^ 0x646F72616E646F6D)
    v6 = u64(a3 ^ 0x6C7967656E657261)
    v7 = u64(a4 ^ 0x7465646279746573)

    n = len(data)
    n8 = n & ~7
    i = 0

    while i < n8:
        m = read_u64_le(data, i)
        i += 8

        v11 = u64(v4 + v5)
        v12 = u64(v11 ^ ror(v5, 51))
        v13 = u64(m ^ v7)
        v14 = u64(v13 + v6)
        v15 = u64(v12 + v14)
        v16 = u64(v14 ^ ror(v13, 48))
        v17 = u64(v15 ^ ror(v12, 47))
        v18 = u64(ror(v11, 32) + v16)
        v19 = u64(v18 + v17)
        v20 = u64(v18 ^ ror(v16, 43))
        v21 = u64(v19 ^ ror(v17, 51))
        v22 = ror(v19, 32)
        v23 = u64(v20 + ror(v15, 32))
        v24 = u64(v21 + v23)
        v25 = u64(v23 ^ ror(v20, 48))
        v5 = u64(v24 ^ ror(v21, 47))
        v26 = u64(v22 + v25)
        v7 = u64(v26 ^ ror(v25, 43))
        v6 = ror(v24, 32)
        v4 = u64(v26 ^ m)

    v27 = u64(n << 56)
    tail = data[n8:]
    rem = n & 7
    if rem >= 7:
        v27 |= tail[6] << 48
    if rem >= 6:
        v27 |= tail[5] << 40
    if rem >= 5:
        v27 |= tail[4] << 32
    if rem >= 4:
        v27 |= tail[3] << 24
    if rem >= 3:
        v27 |= tail[2] << 16
    if rem >= 2:
        v27 |= tail[1] << 8
    if rem >= 1:
        v27 |= tail[0]

    v28 = u64(v4 + v5)
    v29 = u64(v27 ^ v7)
    v30 = u64(v28 ^ ror(v5, 51))
    v31 = u64(v29 + v6)
    v32 = ror(v28, 32)
    v33 = u64(v31 ^ ror(v29, 48))
    v34 = u64(v30 + v31)
    v35 = u64(v34 ^ ror(v30, 47))
    v36 = u64(v32 + v33)
    v37 = u64(v36 + v35)
    v38 = u64(v36 ^ ror(v33, 43))
    v39 = u64(v37 ^ ror(v35, 51))
    v40 = u64(v38 + ror(v34, 32))
    v41 = u64(v40 ^ ror(v38, 48))
    v42 = u64(v39 + v40)
    v43 = u64(v42 ^ ror(v39, 47))
    v44 = u64(ror(v37, 32) + v41)
    v45 = u64((v44 ^ v27) + v43)
    v46 = u64(v44 ^ ror(v41, 43))
    v47 = u64(v45 ^ ror(v43, 51))
    v48 = u64(v46 + (ror(v42, 32) ^ 0xFF))
    v49 = u64(v48 ^ ror(v46, 48))
    v50 = u64(v47 + v48)
    v51 = u64(v50 ^ ror(v47, 47))
    v52 = u64(ror(v45, 32) + v49)
    v53 = u64(v52 + v51)
    v54 = u64(v52 ^ ror(v49, 43))
    v55 = u64(v53 ^ ror(v51, 51))
    v56 = u64(v54 + ror(v50, 32))
    v57 = u64(v56 ^ ror(v54, 48))
    v58 = u64(v55 + v56)
    v59 = u64(v58 ^ ror(v55, 47))
    v60 = u64(ror(v53, 32) + v57)
    v61 = u64(v60 + v59)
    v62 = u64(v60 ^ ror(v57, 43))
    v63 = u64(v61 ^ ror(v59, 51))
    v64 = u64(v62 + ror(v58, 32))
    v65 = u64(v64 ^ ror(v62, 48))
    v66 = u64(v63 + v64)
    v67 = u64(v66 ^ ror(v63, 47))
    v68 = u64(ror(v61, 32) + v65)
    v69 = u64((v68 + v67) ^ ror(v67, 51))
    v70 = u64(v68 ^ ror(v65, 43))
    v71 = u64(v70 + ror(v66, 32))

    return u64(
        ror(v69, 47)
        ^ ror(u64(v69 + v71), 32)
        ^ ror(u64(v71 ^ ror(v70, 48)), 43)
        ^ u64(v69 + v71)
    )

def sub_25CA8(data: bytes, seed: int) -> int:
    """对应 sub_25CA8(64-bit 混淆 hash)"""
    C_M = 0x3D4D51C2D82B14B1
    C1 = 0x9E3779B185EBCA87

    n = len(data)
    i = 0
    r0 = r1 = r2 = r3 = 0

    if n < 0x20:
        v18 = u64(seed + 0x27D4EB2F165667C5)
    else:
        v4 = u64(seed - C_M)
        v5 = u64(seed + 0x60EA27EEADC0B5D6)
        v6 = u64(seed + 0x61C8864E7A143579)
        a3 = u64(seed)

        while i <= n - 32:
            x0 = read_u64_le(data, i)
            x1 = read_u64_le(data, i + 8)
            x2 = read_u64_le(data, i + 16)
            x3 = read_u64_le(data, i + 24)
            i += 32

            t2 = u64(a3 - C_M * x2)
            t3 = u64(v6 - C_M * x3)
            r0 = ror(u64(v5 - C_M * x0), 33)
            r1 = ror(u64(v4 - C_M * x1), 33)
            r2 = ror(t2, 33)
            r3 = ror(t3, 33)

            v5 = u64(C1 * r0)
            v4 = u64(C1 * r1)
            a3 = u64(C1 * r2)
            v6 = u64(C1 * r3)

        m0 = u64(
            ((0x3C6EF3630BD7950E * r0) | (v5 >> 63))
            + 0x1BBCD8C2F5E54380 * r1
            + (v4 >> 57)
            + 0x779B185EBCA87000 * r2
            + (a3 >> 52)
            - 0x1939E850D5E40000 * r3
            + (v6 >> 46)
        )

        h0 = u64(C1 * ror(u64(0xDEF35B010F796CA9 * r0), 33))
        h1 = u64(C1 * ror(u64(0xDEF35B010F796CA9 * r1), 33))
        h2 = u64(C1 * ror(u64(0xDEF35B010F796CA9 * r2), 33))
        h3 = u64(C1 * ror(u64(0xDEF35B010F796CA9 * r3), 33))

        x = u64(m0 ^ h0)
        x = u64(0x85EBCA77C2B2AE63 - 0x61C8864E7A143579 * x)
        x = u64(x ^ h1)
        x = u64(0x85EBCA77C2B2AE63 - 0x61C8864E7A143579 * x)
        x = u64(x ^ h2)
        x = u64(0x85EBCA77C2B2AE63 - 0x61C8864E7A143579 * x)
        x = u64(x ^ h3)
        v18 = u64(0x85EBCA77C2B2AE63 - 0x61C8864E7A143579 * x)

    v19 = u64(v18 + n)

    while i + 8 <= n:
        v21 = read_u64_le(data, i)
        i += 8
        t = u64(C1 * ror(u64(0xC2B2AE3D27D4EB4F * v21), 33))
        v19 = u64(0x85EBCA77C2B2AE63 - 0x61C8864E7A143579 * ror(u64(t ^ v19), 37))

    if i + 4 <= n:
        v23 = u64(C1 * read_u32_le(data, i)) ^ v19
        v19 = u64(0x165667B19E3779F9 - 0x3D4D51C2D82B14B1 * ror(v23, 41))
        i += 4

    while i < n:
        b = data[i]
        i += 1
        v19 = u64(C1 * ror(u64(0x27D4EB2F165667C5 * b) ^ v19, 53))

    z = u64(0xC2B2AE3D27D4EB4F * (v19 ^ (v19 >> 33)))
    v26 = u64(0x165667B19E3779F9 * (z ^ (z >> 29)))
    return u64(v26 ^ ((v26 >> 32) & 0xFFFFFFFF))

def splitmix64(x: int) -> int:
    z = u64(x)
    z = u64(0xBF58476D1CE4E5B9 * (z ^ (z >> 30)))
    z = u64(0x94D049BB133111EB * (z ^ (z >> 27)))
    return u64(z ^ (z >> 31))

def mix_pair(result: int, key0: int, key1: int, c_a: int, c_b: int) -> Tuple[int, int]:
    C1 = 0x9E3779B185EBCA87
    C2 = 0xC2B2AE3D27D4EB4F
    K = 0x7A1435883D4D519D
    A = 0x165667B19E3779F9

    v7 = u64(C1 * ror(u64(C2 * result), 33))
    v8 = u64(C1 * ror(u64(C2 * key0), 33))
    v9 = u64(v8 ^ u64(C1 * ror(u64(v7 ^ c_a), 37) - K))
    v10 = u64(v8 ^ u64(C1 * ror(u64(v7 ^ c_b), 37) - K))
    v11 = u64(C1 * ror(u64(C2 * key1), 33))
    v12 = u64(v11 ^ u64(C1 * ror(v10, 37) - K))
    v13 = ror(u64(v11 ^ u64(C1 * ror(v9, 37) - K)), 37)
    v14 = ror(v12, 37)

    t0 = u64(C1 * v13 - K)
    t1 = u64(C1 * v14 - K)
    v15 = u64(C2 * (t0 ^ (t0 >> 33)))
    v16 = u64(C2 * (t1 ^ (t1 >> 33)))

    o0 = u64(A * (v15 ^ (v15 >> 29)))
    o1 = u64(A * (v16 ^ (v16 >> 29)))
    return u64(o0 ^ (o0 >> 32)), u64(o1 ^ (o1 >> 32))

def sub_2E170(a1: int, a2: int, key16: bytes) -> Tuple[int, int, int]:
    """对应 sub_2E170,返回 (hash, out0, out1)"""
    inbuf = struct.pack("<QQ", u64(a1), u64(a2))
    h = sub_2D248(inbuf, u64(a2 ^ CONST_MAGIC), u64(a2 ^ CONST_MAGIC_ALT))
    k0 = read_u64_le(key16, 0)
    k1 = read_u64_le(key16, 8)
    o0, o1 = mix_pair(h, k0, k1, 0x27D4EB2F165726CC, 0x27D4EB2F165728BB)
    return h, o0, o1

def sub_2DCDC(qword_5EA30: int, beat_count: int) -> int:
    """对应 sub_2DCDC(debug bypass 路径用的 key)"""
    if beat_count < 1:
        return 0

    t = u64(qword_5EA30 ^ CONST_MAGIC)
    v3 = u64(
        0xBF58476D1CE4E5B9
        * (u64(t - CONST_GOLDEN_NEG) ^ (u64(t - CONST_GOLDEN_NEG) >> 30))
    )
    result = u64(0x94D049BB133111EB * (v3 ^ (v3 >> 27)))
    result = u64(result ^ (result >> 31))

    a3 = u64(qword_5EA30 ^ CONST_MAGIC)
    a4 = u64(qword_5EA30 ^ CONST_MAGIC_ALT)
    for i in range(12):
        buf = struct.pack("<QIIII", result, i % beat_count, 0, i, 2)
        result = sub_2D248(buf, a3, a4)
    return result

def derive_qword_5EA30(
    qword_5EA28: int, v25: int, v26: int, byte_5EA54: int
) -> Tuple[int, int, int]:
    """对应 sub_257DC/sub_2541C 里 qword_5EA30 计算段"""
    if v25 < 4 and byte_5EA54 != 1:
        return 0, u64(qword_5EA28), byte_5EA54

    if v25 >= 4:
        byte_5EA54 = 1

    v27 = u64(qword_5EA28)
    v28 = u64((v25 ^ (u64(v26 & 0xFFFFFFFF) << 32) ^ CONST_MAGIC) - CONST_GOLDEN_NEG)
    t = u64(0xBF58476D1CE4E5B9 * (v28 ^ (v28 >> 30)))
    v29 = u64(0x94D049BB133111EB * (t ^ (t >> 27)))
    v30 = u64(v29 ^ (v29 >> 31))

    if v30 != 0:
        v31 = (v30 << 64) | v29
        temp = u64((v31 >> 35) ^ v30)
        v27 = u64(v27 ^ temp)

    return 1, v27, byte_5EA54

# -------------------------------
# AES-CTR (sub_2D678)
# -------------------------------

def aes_expand_key_128(key16: bytes, sbox: List[int], rcon: List[int]) -> bytes:
    w = list(key16)
    i = 16
    while i < 176:
        temp = w[i - 4 : i]
        if i % 16 == 0:
            temp = temp[1:] + temp[:1]
            temp = [sbox[b] for b in temp]
            temp[0] ^= rcon[i // 16]
        for j in range(4):
            w.append(w[i - 16 + j] ^ temp[j])
        i += 4
    return bytes(w)

def _xtime(x: int) -> int:
    x <<= 1
    if x & 0x100:
        x ^= 0x11B
    return x & 0xFF

def _mul2(x: int) -> int:
    return _xtime(x)

def _mul3(x: int) -> int:
    return _xtime(x) ^ x

def _shift_rows(state: List[int]) -> List[int]:
    return [
        state[0],
        state[5],
        state[10],
        state[15],
        state[4],
        state[9],
        state[14],
        state[3],
        state[8],
        state[13],
        state[2],
        state[7],
        state[12],
        state[1],
        state[6],
        state[11],
    ]

def _mix_columns(state: List[int]) -> List[int]:
    out = state[:]
    for c in range(4):
        i = c * 4
        a0, a1, a2, a3 = state[i], state[i + 1], state[i + 2], state[i + 3]
        out[i] = _mul2(a0) ^ _mul3(a1) ^ a2 ^ a3
        out[i + 1] = a0 ^ _mul2(a1) ^ _mul3(a2) ^ a3
        out[i + 2] = a0 ^ a1 ^ _mul2(a2) ^ _mul3(a3)
        out[i + 3] = _mul3(a0) ^ a1 ^ a2 ^ _mul2(a3)
    return out

def aes_encrypt_block(block16: bytes, round_keys176: bytes, sbox: List[int]) -> bytes:
    state = [b for b in block16]

    rk = round_keys176[0:16]
    state = [state[i] ^ rk[i] for i in range(16)]

    for rnd in range(1, 10):
        state = [sbox[b] for b in state]
        state = _shift_rows(state)
        state = _mix_columns(state)
        rk = round_keys176[rnd * 16 : (rnd + 1) * 16]
        state = [state[i] ^ rk[i] for i in range(16)]

    state = [sbox[b] for b in state]
    state = _shift_rows(state)
    rk = round_keys176[160:176]
    state = [state[i] ^ rk[i] for i in range(16)]
    return bytes(state)

def aes_ctr_xor(
    data: bytes, round_keys: bytes, nonce12: bytes, sbox: List[int]
) -> bytes:
    out = bytearray(data)
    for off in range(0, len(out), 16):
        ctr = nonce12 + struct.pack("<I", off // 16)
        ks = aes_encrypt_block(ctr, round_keys, sbox)
        blen = min(16, len(out) - off)
        for i in range(blen):
            out[off + i] ^= ks[i]
    return bytes(out)

# -------------------------------
# sub_2E2E8 frame permutation
# -------------------------------

def build_perm(count: int, seed: int) -> List[int]:
    arr = list(range(count))
    x = u64(seed - CONST_GOLDEN_NEG)
    for i in range(count - 1, 0, -1):
        j = splitmix64(x) % (i + 1)
        arr[i], arr[j] = arr[j], arr[i]
        x = u64(x - CONST_GOLDEN_NEG)
    return arr

# -------------------------------
# mode=2 decrypt (sub_2DDF8)
# -------------------------------

def decrypt_mode2(
    buf: bytes, verify_key: int, qword_5EA30: int, sbox: List[int], rcon: List[int]
) -> bytes:
    hdr = parse_hjm_header(buf)
    if hdr.magic != b"HJM1":
        raise ValueError("bad magic")
    if hdr.mode != 2:
        raise ValueError("this script only handles mode=2")
    if hdr.frame_count <= 0 or hdr.width <= 0 or hdr.height <= 0:
        raise ValueError("invalid dimensions")
    if hdr.frame_bytes < 1:
        raise ValueError("invalid frame_bytes")
    if 52 + hdr.payload_bytes > len(buf):
        raise ValueError("payload out of range")
    if verify_key == 0:
        raise ValueError("verify_key must be non-zero")

    out = bytearray(buf)
    payload_off = 52
    payload_len = hdr.payload_bytes

    key_24_40 = bytes(out[24:40])
    nonce_40_52 = bytes(out[40:52])

    _, seed0, seed1 = sub_2E170(verify_key, qword_5EA30, key_24_40)
    seed16 = struct.pack("<QQ", seed0, seed1)
    mix32 = (qword_5EA30 ^ (qword_5EA30 >> 32)) & 0xFFFFFFFF

    remain = payload_len
    cur_off = payload_off
    chunk_idx = 0
    while remain > 0:
        chunk_len = min(256, remain)

        ctx = bytearray(32)
        ctx[0:16] = seed16
        struct.pack_into(
            "<II", ctx, 16, chunk_idx & 0xFFFFFFFF, qword_5EA30 & 0xFFFFFFFF
        )
        struct.pack_into("<Q", ctx, 24, u64(((chunk_idx << 32) & MASK64) ^ qword_5EA30))

        k0 = sub_25CA8(bytes(ctx), 0x1357)
        k1 = sub_25CA8(bytes(ctx), 0x2468)
        key16 = struct.pack("<QQ", k0, k1)
        round_keys = aes_expand_key_128(key16, sbox, rcon)

        nonce = bytearray(nonce_40_52)
        bi = chunk_idx & 0xFFFFFFFF
        nonce[0] ^= bi & 0xFF
        nonce[1] ^= (bi >> 8) & 0xFF
        nonce[2] ^= (bi >> 16) & 0xFF
        nonce[3] ^= (bi >> 24) & 0xFF

        nonce[4] ^= mix32 & 0xFF
        nonce[5] ^= (mix32 >> 8) & 0xFF
        nonce[6] ^= (mix32 >> 16) & 0xFF
        nonce[7] ^= (mix32 >> 24) & 0xFF

        dec = aes_ctr_xor(
            bytes(out[cur_off : cur_off + chunk_len]), round_keys, bytes(nonce), sbox
        )
        out[cur_off : cur_off + chunk_len] = dec

        cur_off += chunk_len
        remain -= chunk_len
        chunk_idx += 1

    # frame_count > 1 时按 sub_2E2E8 做重排
    if hdr.frame_count > 1:
        perm_seed = sub_25CA8(seed16 + key_24_40, 0xDEAD)
        perm = build_perm(hdr.frame_count, perm_seed ^ qword_5EA30)

        fb = hdr.frame_bytes
        src = bytes(out[payload_off : payload_off + payload_len])
        dst = bytearray(payload_len)
        for src_idx in range(hdr.frame_count):
            dst_idx = perm[src_idx]
            dst[dst_idx * fb : (dst_idx + 1) * fb] = src[
                src_idx * fb : (src_idx + 1) * fb
            ]
        out[payload_off : payload_off + payload_len] = dst

    return bytes(out)

# -------------------------------
# OCR (char_map.txt)
# -------------------------------

def load_char_map(path: Path) -> Dict[str, List[int]]:
    mp: Dict[str, List[int]] = {}
    for line in path.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line:
            continue
        k, v = line.split(":")
        k = k.strip()
        v = v.strip()
        rows = [int(v[i : i + 2], 16) for i in range(0, len(v), 2)]
        mp[k] = rows
    return mp

def payload_to_bits(payload: bytes, w: int, h: int) -> List[List[int]]:
    bits = [[0] * w for _ in range(h)]
    for y in range(h):
        for x in range(w):
            p = y * w + x
            bits[y][x] = (payload[p >> 3] >> (7 - (p & 7))) & 1
    return bits

def decode_bitmap_text(
    payload: bytes, w: int, h: int, cmap: Dict[str, List[int]]
) -> Optional[str]:
    bits = payload_to_bits(payload, w, h)

    sig2ch: Dict[Tuple[Tuple[int, ...], ...], str] = {}
    for ch, rows in cmap.items():
        sig: List[Tuple[int, ...]] = []
        for r in rows:
            # bit4..bit0 -> x=0..4
            sig.append(tuple(1 if (r & (1 << (4 - c))) else 0 for c in range(5)))
        sig2ch[tuple(sig)] = ch

    cands: List[Tuple[int, int, int, str]] = []

    # sub_2E5FC 布局规则:每行最多 10 字符;单元 6x8;字符 5x7
    for n in range(1, 81):
        k = (n + 9) // 10
        if k < 1 or k > 8:
            continue
        line_lens = [10] * (k - 1) + [n - 10 * (k - 1)]
        y0 = (65 - 8 * k) // 2

        ok = True
        chars: List[str] = []
        used = [[0] * w for _ in range(h)]

        for li, L in enumerate(line_lens):
            x0 = (65 - 6 * L) // 2
            y = y0 + 8 * li
            for j in range(L):
                x = x0 + 6 * j
                sig: List[Tuple[int, ...]] = []

                for ry in range(7):
                    yy = y + ry
                    if yy < 0 or yy >= h:
                        ok = False
                        break

                    row: List[int] = []
                    for rx in range(5):
                        xx = x + rx
                        if xx < 0 or xx >= w:
                            ok = False
                            break
                        b = bits[yy][xx]
                        row.append(b)
                        if b:
                            used[yy][xx] = 1

                    if not ok:
                        break
                    sig.append(tuple(row))

                if not ok:
                    break

                ch = sig2ch.get(tuple(sig))
                if ch is None:
                    ok = False
                    break
                chars.append(ch)

            if not ok:
                break

        if not ok:
            continue

        stray = 0
        on = 0
        for yy in range(h):
            for xx in range(w):
                if bits[yy][xx]:
                    on += 1
                    if not used[yy][xx]:
                        stray += 1

        cands.append((stray, -on, n, "".join(chars)))

    if not cands:
        return None

    cands.sort()
    return cands[0][3]

def try_decode_once(
    pack: bytes,
    beatmap: List[int],
    qword_5EA30: int,
    verify_key: int,
    sbox: List[int],
    rcon: List[int],
    cmap: Dict[str, List[int]],
) -> Optional[str]:
    dec = decrypt_mode2(pack, verify_key, qword_5EA30, sbox, rcon)
    hdr = parse_hjm_header(dec)
    payload = dec[52 : 52 + hdr.payload_bytes]
    return decode_bitmap_text(payload, hdr.width, hdr.height, cmap)

def main() -> None:
    base = Path(__file__).resolve().parent

    parser = argparse.ArgumentParser(description="hjm_pack.bin 完整解密 + OCR")
    parser.add_argument("--pack", default=str(base / "hjm_pack.bin"))
    parser.add_argument("--sbox", default=str(base / "1694D.bin"))
    parser.add_argument("--rcon", default=str(base / "16a4d.bin"))
    parser.add_argument("--char-map", default=str(base / "char_map.txt"))
    parser.add_argument(
        "--beatmap", default="0,250,500,750", help="startSession 节奏点,逗号分隔"
    )
    args = parser.parse_args()

    pack = Path(args.pack).read_bytes()
    sbox = list(Path(args.sbox).read_bytes())
    rcon = list(Path(args.rcon).read_bytes())
    while len(rcon) < 16:
        rcon.append(0)
    cmap = load_char_map(Path(args.char_map))

    beatmap = [int(x.strip()) for x in args.beatmap.split(",") if x.strip()]
    beatmap_bytes = b"".join(struct.pack("<I", x & 0xFFFFFFFF) for x in beatmap)

    hdr = parse_hjm_header(pack)
    print(
        f"[+] header: magic={hdr.magic!r} mode={hdr.mode} frames={hdr.frame_count} size={hdr.width}x{hdr.height}"
    )

    # 对应 startSessionBytes 初始化得到 qword_5EA28
    qword_5EA28 = sub_25CA8(beatmap_bytes, CONST_MAGIC)
    print(f"[+] qword_5EA28 = 0x{qword_5EA28:016x}")

    # 首次 verify 常见路径:qword_5EA30 保持 qword_5EA28
    qword_5EA30 = qword_5EA28
    beat_count = len(beatmap)
    verify_key = sub_2DCDC(qword_5EA30, beat_count)
    print(f"[+] qword_5EA30 = 0x{qword_5EA30:016x}")
    print(f"[+] verify_key(sub_2DCDC) = 0x{verify_key:016x}")

    text = None
    try:
        text = try_decode_once(pack, beatmap, qword_5EA30, verify_key, sbox, rcon, cmap)
    except Exception as e:
        print(f"[!] direct path failed: {e}")

    # 如果直接路径失败,做一次小范围状态回溯(完整但保守)
    if not text or not text.startswith("FLAG{"):
        print("[+] fallback: brute runtime-state (hi/low/xor) ...")
        found = None
        for hi in range(0, 13):
            for low in range(0, 256):
                v25 = min(12, hi + hi)
                v26 = low
                _, q30, _ = derive_qword_5EA30(
                    qword_5EA28, v25, v26, 1 if hi >= 4 else 0
                )
                base_key = sub_2DCDC(q30, beat_count)

                for use_xor in (0, 1):
                    a2 = u64(base_key ^ (0xA5A5A5A5A5A5A5A5 if use_xor else 0))
                    try:
                        t = try_decode_once(pack, beatmap, q30, a2, sbox, rcon, cmap)
                    except Exception:
                        continue
                    if t and t.startswith("FLAG{"):
                        found = (t, q30, a2, hi, low, use_xor)
                        break
                if found:
                    break
            if found:
                break

        if found:
            text, q30, a2, hi, low, use_xor = found
            print(
                f"[+] fallback matched: hi={hi} low={low} xor={use_xor} "
                f"q30=0x{q30:016x} key=0x{a2:016x}"
            )

    if text:
        print(f"[+] decoded text: {text}")
    else:
        print("[-] failed to decode text")

if __name__ == "__main__":
    main()

下面是char_map.txt

0: 0e11131519110e
1: 040c040404040e
2: 0e11010204081f
3: 0e11010601110e
4: 02060a121f0202
5: 1f101e0101110e
6: 0e10101e11110e
7: 1f010204080808
8: 0e11110e11110e
9: 0e11110f01020c
A: 0e11111f111111
B: 1e11111e11111e
C: 0e11101010110e
D: 1e11111111111e
E: 1f10101e10101f
F: 1f101e10101010
G: 0e11101711110e
H: 11111f11111111
I: 0e04040404040e
J: 0702020202120c
K: 11121418141211
L: 1010101010101f
M: 111b1511111111
N: 11191513111111
O: 0e11111111110e
P: 1e11111e101010
Q: 0e11111115120d
R: 1e11111e141211
S: 0f10100e01011e
T: 1f040404040404
U: 1111111111110e
V: 11111111110a04
W: 11111115151b11
X: 11110a040a1111
Y: 11110a04040404
Z: 1f01020408101f
a: 00000e010f110f
b: 10101e1111111e
c: 00000e1110110e
d: 01010f1111110f
e: 00000e111f100e
f: 06081e08080808
g: 00000f11110f01
h: 10101e11111111
i: 04000c0404040e
j: 0200060202120c
k: 10101214181412
l: 0c04040404040e
m: 00001a15151515
n: 00001e11111111
o: 00000e1111110e
p: 00001e11111e10
q: 00000f11110f01
r: 00001619101010
s: 00000f100e011e
t: 08081e08080806
u: 0000111111110f
v: 00001111110a04
w: 00001111151b11
x: 0000110a040a11
y: 00001111110f01
z: 00001f0204081f
{: 06040408040406
}: 0c04040204040c

秒出flag

HOOK

拿到flag兴冲冲投喂,结果不对,但是提交结果是正确的,说明肯定有检测

这时注意到这里,也就是说要调用一下setDebugBypass(true)

hook之后依然不正确,但是能看到sub_2DCDC已经被调用了

function setDebugBypass(enabled: boolean) {
  let module = Process.findModuleByName("libhajimi.so");
  let addr = module?.base.add(0x5eb88);
  addr?.writeS8(enabled ? 1 : 0);
}

function hook_sub2DCDC() {
  let module = Process.findModuleByName("libhajimi.so");
  let addr = module?.base.add(0x2dcdc);
  Interceptor.attach(addr!, {
    onEnter: function (args) {
      console.log("bypass");
    },
    onLeave: function (retval) {},
  });
}

setDebugBypass(true);
hook_sub2DCDC();

这时候又向上找,看到了一个检测函数

这里面老老长一串,是检测函数,而且这个函数在其他地方也有调用

在java层一开始就有个调用,所以是一开始就检测了,我们可以spawn的时候hook so加载,在加载时把检测函数返回0

import { onSoLoad } from "../../utils";

function setDebugBypass(module: Module, enabled: boolean) {
  let addr = module!.base.add(0x5eb88);
  addr.writeS8(enabled ? 1 : 0);
}

function hook_sub25EF8(module: Module) {
  let addr = module!.base.add(0x25ef8);
  Interceptor.attach(addr!, {
    onEnter: function (args) {},
    onLeave: function (retval) {
      console.log(retval.toInt32());
      retval.replace(ptr(0));
      console.log(retval.toInt32());
    },
  });
}

onSoLoad("libhajimi.so", (module: Module) => {
  setDebugBypass(module, true);
  hook_sub25EF8(module);
});

这样就验证成功了

但是又看到前面AI说的,最后就是通过memcmp比较,比较的内容是已经解密好的,所以只要hook它,我们完全可以任意输入就拿到flag,省去了什么对照等等多余的分析步骤了

这里需要hook此处调用的memcmp,不然就太多了不好判断,hook到直接把第二个参数赋值给第一个参数即可

import { onSoLoad } from "../../utils";

function setDebugBypass(module: Module, enabled: boolean) {
  let addr = module!.base.add(0x5eb88);
  addr.writeS8(enabled ? 1 : 0);
}

function hook_sub25EF8(module: Module) {
  let addr = module!.base.add(0x25ef8);
  Interceptor.attach(addr!, {
    onEnter: function (args) {},
    onLeave: function (retval) {
      console.log(retval.toInt32());
      retval.replace(ptr(0));
      console.log(retval.toInt32());
    },
  });
}

function hook_memcmp(module: Module) {
  let addr = module!.base.add(0x25b24);
  Interceptor.attach(addr!, {
    onEnter: function (args) {
      args[0] = args[1];
    },
    onLeave: function (retval) {},
  });
}

onSoLoad("libhajimi.so", (module: Module) => {
  setDebugBypass(module, true);
  hook_sub25EF8(module);
  hook_memcmp(module);
});

此时任意输入就能拿到flag

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
cattie 发表于 2026-3-4 10:00
格式炸了,建议用markdown重新排一下版 w
3#
 楼主| WXjzc 发表于 2026-3-4 10:00 |楼主
cattie 发表于 2026-3-4 10:00
格式炸了,建议用markdown重新排一下版 w

我点的保存草稿,不知道怎么发布了

点评

草稿功能有点问题,如果触发了审核会发出来的,建议不要使用这个功能。 现在直接编辑帖子就行了。  详情 回复 发表于 2026-3-4 10:08
4#
cattie 发表于 2026-3-4 10:08
WXjzc 发表于 2026-3-4 10:00
我点的保存草稿,不知道怎么发布了

草稿功能有点问题,如果触发了审核会发出来的,建议不要使用这个功能。

现在直接编辑帖子就行了。
5#
神奇的人鱼 发表于 2026-3-4 11:16
看起来大佬用的MCP,能告诉一下用的什么MCP嘛
6#
zhuhuan 发表于 2026-3-4 11:57
为什么我是这样

QQ20260304-115729.jpg (258.53 KB, 下载次数: 0)

QQ20260304-115729.jpg
7#
GoldenSpirit 发表于 2026-3-4 12:33
相当精彩
8#
 楼主| WXjzc 发表于 2026-3-4 14:17 |楼主
神奇的人鱼 发表于 2026-3-4 11:16
看起来大佬用的MCP,能告诉一下用的什么MCP嘛

ida-pro-mcp
9#
 楼主| WXjzc 发表于 2026-3-4 14:20 |楼主
zhuhuan 发表于 2026-3-4 11:57
为什么我是这样

没有正确hook掉检测函数,导致解密失败,显示的就是乱码
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-3-4 15:21

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表