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