吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1373|回复: 17
上一主题 下一主题
收起左侧

[CTF] 【2026春节】解题领红包 【2-9】WP 通杀

[复制链接]
跳转到指定楼层
楼主
jingtai123 发表于 2026-3-4 23:17 回帖奖励
本帖最后由 jingtai123 于 2026-3-4 23:25 编辑

书接上回

【2026春节】解题领红包10.Windows 高级题 11.MCP 中级题
https://www.52pojie.cn/thread-2094516-1-1.html

继续感慨 难得通杀 借了AI的大势了

AI的发展太离谱了。不学习AI 就像发明了汽车马夫不学开车一样。

本篇内容大部分由AI生成。

2026春节解题领红包之二 - Windows初级题

题目概述

这是一道逆向分析题目,需要分析给定的汇编代码,找到并解密flag。

一开始走了爆破路线,后来AI分析直接秒了- -!


方法一:直接分析汇编解密

分析过程

  1. 定位解密函数

    在汇编代码的 00521620 - 00521681 地址处找到了关键的解密函数。

  2. 分析加密数据初始化

    该函数首先在堆栈上初始化加密数据:

    0052162C | C702 7770322D            | mov dword ptr ds:[edx],2D327077
    00521635 | C742 04 282B2763         | mov dword ptr ds:[edx+4],63272B28
    0052163C | C742 08 63631D70         | mov dword ptr ds:[edx+8],701D6363
    ...
  3. 分析解密算法

    核心解密逻辑使用异或操作:

    00521670 | 8030 42                  | xor byte ptr ds:[eax],42    ; 密钥是 0x42
    00521673 | 83C0 01                  | add eax,1
    00521676 | 39C8                     | cmp eax,ecx
    00521678 | 75 F6                    | jne 00521670

    解密算法:对每个字节执行 byte ^ 0x42

  4. 提取加密数据

    从汇编代码中提取完整的加密数据:

    encrypted = [
       0x77, 0x70, 0x32, 0x2D,
       0x28, 0x2B, 0x27, 0x63,
       0x63, 0x63, 0x1D, 0x70,
       0x72, 0x70, 0x74, 0x1D,
       0x0A, 0x23, 0x32, 0x32,
       0x3B, 0x1D, 0x2C, 0x27,
       0x35, 0x1D, 0x3B, 0x27,
       0x23, 0x30, 0x63
    ]

解密结果

52pojie!!!_2026_Happy_new_year!

方法二:Checksum 约束 + 多线程爆破验证

分析过程

在逆向分析过程中,我们发现了程序使用了 Checksum 验证机制:

def get_checksum(text):
    return sum(ord(c) * (i + 1) for i, c in enumerate(text))

已知条件:

  • 目标长度:31
  • 目标 Checksum:44709
  • 关键词列表:["52pojie", "2026", "Happy_new_year", "_", "!"]

核心算法

采用 DFS(深度优先搜索) 配合 多线程验证 的策略:

  1. DFS 遍历:使用关键词进行深度优先搜索,生成所有可能的组合
  2. Checksum 剪枝:在 DFS 过程中实时计算 Checksum,如果超过目标值则立即剪枝
  3. 数学优化:利用公式 add_checksum = block.base_weight + (current_len * block.char_sum) 进行 O(1) 级别的快速计算
# 预处理每个关键词的基础信息
blocks_info = []
for b in WORD_BLOCKS:
    base_weight = sum(ord(c) * (i + 1) for i, c in enumerate(b))
    char_sum = sum(ord(c) for c in b)
    blocks_info.append({
        "str": b,
        "len": len(b),
        "base_weight": base_weight,
        "char_sum": char_sum
    })

def dfs(current_str, current_len, current_checksum):
    # 剪枝:Checksum 超标
    if current_checksum > TARGET_CHECKSUM:
        return
    # 剪枝:长度达标
    if current_len == TARGET_TOTAL_LENGTH:
        if current_checksum == TARGET_CHECKSUM:
            task_queue.put(current_str)
        return
    # 剪枝:长度超标
    if current_len > TARGET_TOTAL_LENGTH:
        return
    for block in blocks_info:
        add_checksum = block["base_weight"] + (current_len * block["char_sum"])
        dfs(current_str + block["str"], current_len + block["len"], current_checksum + add_checksum)
  1. 多线程验证:开启多个线程,从队列中获取候选 flag 并调用 EXE 程序进行验证,一旦找到正确答案立即通知所有线程停止
NUM_THREADS = 8
def worker_thread(thread_id):
    while not found_event.is_set():
        try:
            flag = task_queue.get(timeout=0.5)
        except queue.Empty:
            if dfs_done_event.is_set():
                break
            continue

        output = test_flag(flag)
        if "SUCCESS" in output or "Congratulations" in output:
            found_event.set()

验证结果

对解密结果进行验证:

  • 字符串:52pojie!!!_2026_Happy_new_year!
  • 长度:31
  • 计算出的 Checksum:44709

验证通过! 完全匹配目标值。


最终 Flag

52pojie!!!_2026_Happy_new_year!

解题脚本

1. 汇编解密脚本 (solve.py)

# 从汇编代码中提取加密数据
encrypted = [
    0x77, 0x70, 0x32, 0x2D,
    0x28, 0x2B, 0x27, 0x63,
    0x63, 0x63, 0x1D, 0x70,
    0x72, 0x70, 0x74, 0x1D,
    0x0A, 0x23, 0x32, 0x32,
    0x3B, 0x1D, 0x2C, 0x27,
    0x35, 0x1D, 0x3B, 0x27,
    0x23, 0x30, 0x63
]

# 密钥是 0x42
key = 0x42

# 解密
decrypted = bytes([b ^ key for b in encrypted])

print("解密结果:")
print(decrypted.decode('utf-8', errors='ignore'))

2. Checksum 验证脚本 (myz3.py)

import subprocess
import time
import threading
import queue

WORD_BLOCKS = [
    "52pojie",
    "2026",
    "Happy_new_year",
    "_",
    "!"
]

TARGET_TOTAL_LENGTH = 31
TARGET_CHECKSUM = 44709
NUM_THREADS = 8

task_queue = queue.Queue()
found_event = threading.Event()
dfs_done_event = threading.Event()
print_lock = threading.Lock()

def test_flag(flag):
    exe_path = r'【2026春节 解题领红包之二 {Windows 初级题} 出题老师:云在天.exe'
    p = subprocess.Popen(
        [exe_path],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        cwd=r'C:\Users\jingt\Desktop\52pojie\第一题'
    )
    output, _ = p.communicate(input=flag + '\n', timeout=3)
    return output

def worker_thread(thread_id):
    while not found_event.is_set():
        try:
            flag = task_queue.get(timeout=0.5)
        except queue.Empty:
            if dfs_done_event.is_set():
                break
            continue

        output = test_flag(flag)

        if "SUCCESS" in output or "Congratulations" in output:
            with print_lock:
                print(f"\n[+] 线程 {thread_id} 破案了!真正的 Flag 是:{flag}")
            found_event.set()
        else:
            with print_lock:
                print(f"    [-] 线程 {thread_id} 验证失败: {flag}")

        task_queue.task_done()

# 预处理关键词信息
blocks_info = []
for b in WORD_BLOCKS:
    base_weight = sum(ord(c) * (i + 1) for i, c in enumerate(b))
    char_sum = sum(ord(c) for c in b)
    blocks_info.append({
        "str": b,
        "len": len(b),
        "base_weight": base_weight,
        "char_sum": char_sum
    })

def dfs(current_str, current_len, current_checksum):
    if found_event.is_set():
        return
    if current_checksum > TARGET_CHECKSUM:
        return
    if current_len == TARGET_TOTAL_LENGTH:
        if current_checksum == TARGET_CHECKSUM:
            task_queue.put(current_str)
        return
    if current_len > TARGET_TOTAL_LENGTH:
        return
    for block in blocks_info:
        add_checksum = block["base_weight"] + (current_len * block["char_sum"])
        dfs(current_str + block["str"], current_len + block["len"], current_checksum + add_checksum)

# 启动多线程
threads = []
for i in range(NUM_THREADS):
    t = threading.Thread(target=worker_thread, args=(i+1,))
    t.daemon = True
    t.start()
    threads.append(t)

dfs("", 0, 0)
dfs_done_event.set()

for t in threads:
    t.join()

解题思路总结

  1. 快速定位解密函数:在汇编中寻找典型的解密特征(XOR操作、循环处理、字符串操作)
  2. 分析算法细节:确定加密数据、密钥和算法
  3. 复现解密过程:用脚本实现相同的算法
  4. 辅助验证:通过Checksum等约束条件验证结果正确性
  5. 多线程加速:使用多线程配合EXE验证,快速定位正确答案

2026春节解题领红包之三{Android_初级题}_出题老师_正己

直接拼图

一开始分析半天,实在不想装APK,然后除了jntm 啥也没分析出来,于是虚拟机安装拼图直接答案了!白浪费时间 - -!


【2026春节】解题领红包之四 {Windows 初级题} 出题老师:云在天

解题思路

这是一个使用 PyInstaller 打包的 Python CrackMe 程序。与其从汇编代码层面进行分析,更高效的方法是直接提取和分析 PyInstaller 打包的 pyc 文件。

解题步骤

1. 识别程序结构

首先观察到程序是一个典型的 PyInstaller 打包的 Windows 可执行文件,其特征包括:

  • 包含解压缩和库加载的启动代码
  • 使用 Python 解释器动态加载模块
  • 有清晰的 PyInstaller 特征(如 PYZ.pyz 文件)

2. 提取打包文件

使用 PyInstaller 自带的提取工具或 pyinstxtractor.py 提取程序内容,可以获得:

  • crackme_easy.pyc - 主要程序逻辑的编译文件
  • PYZ.pyz - Python 归档文件
  • 各种系统库和 Python 依赖库

3. 分析 pyc 文件

虽然 uncompyle6 不支持 Python 3.14,但我们可以使用 Python 内置的 dis 模块直接分析字节码:

import marshal
import dis

pyc_path = "crackme_easy.pyc"
with open(pyc_path, 'rb') as f:
    f.read(16)  # 跳过 pyc 头部
    code_obj = marshal.load(f)

dis.dis(code_obj)

4. 关键函数详细分析

4.1 get_encrypted_flag() - 获取加密数据
def get_encrypted_flag():
    enc_data = 'e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O'
    return base64.b64decode(enc_data)

分析: 这个函数简单地返回一个 base64 编码的加密字符串,没有任何复杂逻辑。


4.2 generate_flag() - 动态生成真正的 Flag
def generate_flag():
    encrypted = get_encrypted_flag()
    key = 78  # 解密密钥
    result = bytearray()
    for i, byte in enumerate(encrypted):
        result.append(byte ^ key)  # XOR 解密
    return result.decode('utf-8')

分析:

  1. 调用 get_encrypted_flag() 获取加密数据
  2. 使用密钥 78 进行逐字节 XOR 解密
  3. 将解密后的字节数组转换为 UTF-8 字符串返回

4.3 verify_flag() - 验证输入是否正确
def verify_flag(user_input):
    correct_flag = generate_flag()
    # 先比较长度
    if len(user_input) != len(correct_flag):
        return False
    # 逐字符比较
    for i in range(len(correct_flag)):
        if user_input[i] != correct_flag[i]:
            return False
    return True

分析:

  1. 先动态生成正确的 flag
  2. 检查输入长度是否匹配
  3. 逐字符比较输入与正确 flag,只要有一个字符不匹配就返回 False
  4. 全部匹配才返回 True

4.4 fake_check_1()fake_check_2() - 假检查函数
def fake_check_1(user_input):
    fake_hash = 'a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890'
    return hash_string(user_input) == fake_hash

def fake_check_2(user_input):
    fake_hash = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
    return hash_string(user_input) == fake_hash

分析:

  • 这两个函数都是用于迷惑的假检查
  • 它们会计算用户输入的 SHA256 哈希值,与预定义的假哈希值比较
  • 即使匹配了这些假哈希,程序也会提示"还不对"或"接近了",然后直接退出
  • 真正的验证在 verify_flag() 中进行

4.5 主流程控制

主函数 main() 的流程:

  1. 显示欢迎界面
  2. 接收用户输入
  3. 先运行 fake_check_1(),如果匹配就提示"Nice try"并退出
  4. 再运行 fake_check_2(),如果匹配就提示"You're getting closer"并退出
  5. 最后才运行真正的 verify_flag() 进行验证
  6. 验证通过后还要计算并比较校验和,双重保险

5. 解密密码

分析 generate_flag() 函数的逻辑:

  1. get_encrypted_flag() 获取 base64 编码的加密数据:e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O
  2. 使用密钥 78 进行 XOR 解密
  3. 解密后得到完整的密码

完整解密代码

import base64

def get_encrypted_flag():
    enc_data = 'e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O'
    return base64.b64decode(enc_data)

def generate_flag():
    encrypted = get_encrypted_flag()
    key = 78
    result = bytearray()
    for i, byte in enumerate(encrypted):
        result.append(byte ^ key)
    return result.decode('utf-8')

flag = generate_flag()
print("真实密码/Flag:", flag)

最终答案

52p0j!3#2026*H4ppy-N3w-Y34r@@@

【2026春节】解题领红包之五 {Windows 中级题} 出题老师:云在天

论坛里的思路

公众号里吹了一波234用AI秒了,5不行,有位IP美国的大哥留言:5论坛上有,搜nuitka就有。。。emmm图省事,照猫画虎。

解出内容

刚看了大家的 不知道为啥我这解出来是float 17 47 14 14给我坑老半天- -!我还在ascii表找对应关系猜了半天...最后咋整出来的我都忘了- -!熬夜干的人麻了

[
  "[b'dc!a;`b', '<PREDEFINED_FLOAT_INDEX_17>', b'cacg', '<PREDEFINED_FLOAT_INDEX_47>', b'\\x19e!!(', '<PREDEFINED_FLOAT_INDEX_14>', b'\\x1fb&', '<PREDEFINED_FLOAT_INDEX_14>', b'\\x08be#', b'ppp']",
  "_parts",
  "81",
  "_key",
  "30",
  "_total_len",
  "解密单个字符",
  "current",
  "_decrypt_char",
  "获取指定位置的字符",
  "self",
  "_get_char_at_position",
  "验证用户输入",
  "total",
  "计算校验和",
  "flag\u0000achecksum\u0000u获取目标校验和\u0000ahashlib\u0000asha256\u0000aencode\u0000ahexdigest\u0000:nl\bnl\u0010u哈希函数\u0000l�",
  "<UNHANDLED_TYPE_0xAC>",
  "<UNHANDLED_TYPE_0xD1>",
  "<UNHANDLED_TYPE_0x91>",
  "<UNHANDLED_TYPE_0x01>",
  "<UNHANDLED_TYPE_0x67>",
  "<UNHANDLED_TYPE_0x02>",
  "<UNHANDLED_TYPE_0x01>",
  "<UNHANDLED_TYPE_0x81>",
  "<UNHANDLED_TYPE_0xDE>",
  "<UNHANDLED_TYPE_0xB7>",
  "<UNHANDLED_TYPE_0xDE>",
  "<UNHANDLED_TYPE_0x02>",
  "1380994890",
  "hash_input",
  "假检查",
  "print",
  "('==================================================',)",
  "('   CrackMe Challenge - Binary Edition',)",
  "('Keywords: 52pojie, 2026, Happy New Year',)",
  "('Hint: 1337 5p34k & 5ymb0l5!',)",
  "('      Try to decompile this in IDA!',)",
  "('--------------------------------------------------',)",
  "CrackMeCore",
  "\n[?] Enter the password: ",
  "fake_check",
  "('\\n[!] Close, but not quite there...',)",
  "\nPress Enter to exit...",
  "verify",
  "get_target_checksum",
  "('\\n==================================================',)",
  "('        *** SUCCESS! ***',)",
  "('[+] L33T H4X0R!',)",
  "[+] Your answer: ",
  "\n[!] Checksum mismatch: ",
  " != ",
  "('\\n[X] Access Denied!',)",
  "('[X] Wrong password!',)",
  "主函数",
  "__doc__",
  "__file__",
  "__cached__",
  "__annotations__",
  "sys",
  "__main__",
  "__module__",
  "核心验证类 - 将被编译成二进制",
  "__qualname__",
  "__init__",
  "CrackMeCore.__init__",
  "CrackMeCore._decrypt_char",
  "CrackMeCore._get_char_at_position",
  "CrackMeCore.verify",
  "CrackMeCore.checksum",
  "CrackMeCore.get_target_checksum",
  "main",
  "('\\n\\n[!] Interrupted',)",
  "crackme_hard.py",
  "<module>",
  "('self',)",
  "('self', 'part_idx', 'char_idx', 'encrypted_byte')",
  "('self', 'pos', 'current', 'part_idx', 'part')",
  "('self', 's', 'total', 'i', 'c')",
  "('user_input', 'fake_hashes', 'user_hash')",
  "('self', 'flag', 'i')",
  "('s',)",
  "('core', 'user_input', 'cs', 'target')",
  "('self', 'user_input', 'i', 'expected')"
]

剩下的就交给ai了。


【春节】解题领红包之六 {番外篇 初级题} 出题老师:Coxxs

直接游戏就出来了...

显示easy一遍过,再是hard三遍过,直接flag了= =!


【春节】解题领红包之七 {Windows 中级题} 出题老师:爱飞的猫

1. 题目文件与分析目标

附件核心文件:

  • CM1.exe / CM1_unpacked.exe(程序)
  • flag.png.encrypted(密文)

本题要求不是爆破,而是从程序逻辑推导出密码。


2. 样本初步分析

先看 CM1_unpacked.exe 基本信息:

  • PE64(0x8664
  • ImageBase = 0x140000000
  • 主要导入:USER32/GDI32/KERNEL32/msvcrt
  • 存在对话框消息处理流程(典型 Win32 GUI CrackMe)

3. 按钮点击分支定位

在对话框过程函数 0x140007AD0(消息分发)中:

  • WM_COMMAND (0x111)
  • LOWORD(wParam) == 11 时走“解密按钮”分支

该分支关键流程:

  1. GetDlgItemTextA 读取 3 个输入框(输入路径、输出路径、密码)
  2. 调用核心函数 sub_140008720 做校验与解密
  3. 若返回值为 0:提示成功
  4. 若返回值非 0:拼接 ERR-%03d,弹框报错

即:

  • ERR-xxxxxx 就是 sub_140008720 的返回码

4. 核心函数 sub_140008720 还原

函数原型可抽象为:

int decrypt_and_check(const char* password, FILE* fin, FILE* fout);

主流程(伪代码):

int decrypt_and_check(pw, fin, fout) {
    ctx = init_crc64_ctx();
    update_crc64(ctx, CONST_14_BYTES, 14);
    update_crc64(ctx, pw, strlen(pw));
    seed = finalize_crc64(ctx);  // 64-bit

    read header(16 bytes);
    if (!parse_header_magic_and_init_crc32(header)) return 1;

    file_len = ftell(fin);
    data_len = file_len - 16;
    if (file_len % 8 != 0) return 2;

    rewind to data start;
    buf = malloc(data_len);
    if (!buf) return 3;

    fread(buf, data_len, 1, fin);
    decrypt_blocks(buf, data_len, seed, header.iv);

    if (!check_crc32(buf, data_len, header.stored_crc32)) {
        free(buf);
        return 4;   // ERR-004
    }

    pad = buf[data_len - 1];
    if (pad > data_len) {
        free(buf);
        return 5;
    }

    fwrite(buf, data_len - pad, 1, fout);
    free(buf);
    return 0;
}

错误码映射很清晰:

  • 1:头部不合法(magic 不对)
  • 2:长度非 8 对齐
  • 3:内存申请失败
  • 4:完整性校验失败(本题关键)
  • 5:padding 异常

5. ERR-004 的验证逻辑(关键)

ERR-004 对应函数 sub_1400082E0,其核心是:

bool ok = (~crc32_state == stored_crc32_from_header);

其中 crc32_state 来自对“解密后数据(含 padding)”逐块更新。

也就是:

  • 密码错 -> 解密流错 -> 明文错 -> CRC32 不匹配 -> 返回 4 -> 弹 ERR-004

这正是题目要求定位的验证点。


6. 文件格式与解密算法还原

6.1 密文文件头格式(16 字节)

flag.png.encrypted 前 16 字节:

  • [0:4] magic:"CM26"(小端 dword 为 0x36324D43
  • [4:8] stored_crc32(小端)
  • [8:16] iv(8 字节)

样本实测:

  • stored_crc32 = 0x8D8445A2
  • iv = f5 69 73 60 01 cb 35 bc

6.2 口令到种子(seed)生成

程序用 64 位 CRC(多项式 0xC96C5795D7870F42):

  1. 初始化状态为 0xFFFFFFFFFFFFFFFF
  2. 先喂固定 14 字节常量(位于 .rdata,偏移 0xA828
    FB A1 FF FF 22 9D FF FF A3 A2 FF FF 83 A2
  3. 再喂密码明文字节
  4. 做末尾 4 字节长度扰动并取反,得到 64 位 seed

6.3 分组解密

每块 8 字节,使用链式异或:

P[i] = C[i] XOR Prev XOR F(state)
Prev = (i==0 ? IV : C[i-1])
state = F(state) 每块更新一次

其中 F 为:

  1. state = rol64(state, 3)
  2. state 的每个字节做 S-Box 替换

该 S-Box 位于 .rdata 0xA270,首字节 63 7C 77 7B ...,即 AES S-Box。


7. 逆推出正确密码

因为目标文件是 PNG,明文前 8 字节固定为:

89 50 4E 47 0D 0A 1A 0A

又有:

P0 = C0 XOR IV XOR F(seed)

所以可直接求出首块密钥流:

F(seed) = C0 XOR IV XOR PNG_SIG

实算得到:

  • F(seed) = 87 e4 26 b5 27 2e cc 95

再逆 F(逆 S-Box + ror64(3))可得初始 seed

  • seed = 0x55A4F867BA4475DD

用该 seed 对整文件解密后:

  • 明文前缀正确为 PNG 文件头
  • 计算 CRC32 恰好等于 0x8D8445A2
  • 校验通过,不再触发 ERR-004

随后在解出的 PNG tEXt 块中读到口令。


8. 最终答案

正确解密密码(flag)为:

flag{EncrypTIoN_Is_haRd_52p0jIE_2o26_m62Tc4uj78maAq1C}

9. 可复现实验脚本(Python)

以下脚本可直接验证思路:从已知 PNG 头反推 seed,解密并提取 flag 文本。

from pathlib import Path
import pefile, struct, zlib, re

enc = Path("question/flag.png.encrypted").read_bytes()
head, body = enc[:16], enc[16:]

magic = head[:4]
stored_crc = int.from_bytes(head[4:8], "little")
iv = head[8:16]
assert magic == b"CM26"

# 从程序里取 AES S-box
pe = pefile.PE("CM1_unpacked.exe")
exe = Path("CM1_unpacked.exe").read_bytes()
sbox_off = pe.get_offset_from_rva(0xA270)
sbox = exe[sbox_off:sbox_off + 256]

inv = [0] * 256
for i, v in enumerate(sbox):
    inv[v] = i

MASK = (1 << 64) - 1

def F(x: int) -> int:
    x = ((x << 3) & MASK) | (x >> 61)  # rol64(x,3)
    out = 0
    for i in range(8):
        out |= sbox[(x >> (8 * i)) & 0xFF] << (8 * i)
    return out

def Finv(y: int) -> int:
    b = bytearray(8)
    for i in range(8):
        b[i] = inv[(y >> (8 * i)) & 0xFF]
    x = int.from_bytes(b, "little")
    return ((x >> 3) | ((x & 0x7) << 61)) & MASK  # ror64(x,3)

# 已知 PNG 头反推 seed
png_sig = bytes.fromhex("89504e470d0a1a0a")
c0 = body[:8]
ks0 = bytes(a ^ b ^ c for a, b, c in zip(c0, iv, png_sig))
seed = Finv(int.from_bytes(ks0, "little"))
print(f"[+] seed = 0x{seed:016X}")

# 解密
state = seed
prev = iv
plain = bytearray()
for i in range(0, len(body), 8):
    c = body[i:i+8]
    state = F(state)
    ks = state.to_bytes(8, "little")
    p = bytes(c[j] ^ prev[j] ^ ks[j] for j in range(8))
    plain.extend(p)
    prev = c

calc_crc = zlib.crc32(plain) & 0xFFFFFFFF
print(f"[+] stored_crc = 0x{stored_crc:08X}, calc_crc = 0x{calc_crc:08X}")
assert calc_crc == stored_crc

# 去除 padding 并解析 PNG 文本块
pad = plain[-1]
png = bytes(plain[:-pad])
Path("question/decrypted_recovered.png").write_bytes(png)

pos = 8
while pos + 8 <= len(png):
    ln = struct.unpack(">I", png[pos:pos+4])[0]
    typ = png[pos+4:pos+8]
    data = png[pos+8:pos+8+ln]
    if typ in (b"tEXt", b"iTXt", b"zTXt"):
        m = re.search(rb"flag\\{[^}]+\\}", data)
        if m:
            print("[+] flag =", m.group().decode())
            break
    pos += ln + 12

【2026春节】解题领红包之八(Android 中级)

0x00 题目信息与最终结论

题目:【2026春节】解题领红包之八 {Android 中级题} 出题老师:正己.apk

最终可通过输入(flag):

FLAG{HJMWAPJ2026NBLD}

关键点:

  1. verifyAndDecrypt(packBytes, trim(input)) 最终是 位图渲染后 memcmp
  2. packBytes 来自 assets/hjm_pack.bin
  3. 存在 native 作弊开关 setDebugBypass(true)(写全局 5d140=1),会切换到 debug key 分支。
  4. 在 debug 分支下,可稳定还原出正确输入:FLAG{HJMWAPJ2026NBLD}

0x01 Java/Compose 层入口与按钮事件全分支

1. 主界面按钮

主界面有两个按钮:

  • 接着奏乐接着舞
  • 投喂 flag

对应逻辑位于 Q0.vQ0.w,并由 Q0.N/Q0.h 组合函数绑定。

2. 按钮 接着奏乐接着舞 分支(Q0.v.case 0

  1. 若当前 GameState.cheatTriggered == true:直接 return(点击被锁死)。
  2. 否则计算当前点击落点(基于 0/250/500/750ms beatmap)。
  3. 调用 native:
    • checkRhythm(...)
    • updateExp(...)
  4. 分支:
    • updateExp >= 0 && checkRhythm != -7:正常更新分数(Perfect/Good/Miss/None)。
    • 否则:进入作弊态 score=Cheat("嘻嘻"), cheatTriggered=true

3. 按钮 投喂 flag 分支

  • 仅设置 dialogVisible=true,弹出输入框。

4. 弹窗按钮分支

  • 验证:走协程 Q0.A,调用 native verifyAndDecrypt(packBytes, trim(input))
  • 取消:仅关闭弹窗。
  • 点击外部 dismiss:仅关闭弹窗。

5. 协程 Q0.A 校验流程

  1. packBytes 缓存为空:先读取 assets/hjm_pack.bin
  2. NativeBridge.verifyAndDecrypt(packBytes, trim(input))
  3. 若解出结构为空:显示 Flag 不正确
  4. 否则显示 验证成功,并更新层级状态。
  5. 异常:显示 验证出错

0x02 Native 入口与关键函数定位

目标 so:_apk/lib/x86_64/libhajimi.so

关键 native 函数地址:

  • verifyAndDecrypt0x24850
  • setDebugBypass0x24ca0
  • 24fc0(环境/状态采样函数):0x24fc0
  • 2e680(mode2 解包):0x2e680
  • 2efd0(输入字符串 -> 64x64 位图):0x2efd0
  • 2e570(debug key 生成):0x2e570

全局变量:

  • 5d140:debug bypass 标志(setDebugBypass 写入)
  • 5cff0 / 5cff8:运行态 key 与 gate
  • 5cfe8:参与解包的状态种子
  • 5d004/5d008/5d00c24fc0 更新的状态字段

0x03 verifyAndDecrypt(packBytes, trim(input)) 内部逻辑

verifyAndDecrypt 关键流程(mode=2):

  1. jbyteArray packBytes 读入本地缓冲。
  2. 检查头:HJM1 + mode==2 + 宽高/帧参数合法。
  3. 24fc0,并基于返回值更新状态,最终更新 5cfe8
  4. 进入 mode2 分支(0x24b8b)。
  5. 选 key:
    • 如果 5d140 != 0(debug bypass 开启)=> key 来自 2e570()
    • 否则 => key 来自 5cff0(并受 r13 条件影响是否 ^0xA5..)。
  6. 2e680 对 pack payload 解包。
  7. 2efd0(input, 64, 64, out, 512) 生成输入位图。
  8. memcmp(decrypted_payload, rendered_input, 512):相等即验证成功。

这意味着题目的核心本质是:

求一个 input,使 render(input) 与解包目标位图完全一致。


0x04 packBytes 的确认

Java 侧 Q0.y 明确:

  • context.getAssets().open("hjm_pack.bin")

因此 verifyAndDecrypt 第一个参数就是:

  • assets/hjm_pack.bin

文件结构确认:

  • 文件头 HJM1
  • mode = 2
  • payload 长度对应 64x64 bit(512 bytes)

0x05 作弊模式如何开启

native 里有导出接口:

  • setDebugBypass(boolean)(函数体在 0x24ca0

逻辑非常直接:

  • dl == 1 时写 byte [0x5d140] = 1

一旦 5d140=1verifyAndDecrypt mode2 分支会走 debug key 路径(2e570),从而解出另一份目标位图。

实战可用两种方式:

  1. 运行时 hook 调 NativeBridge.INSTANCE.setDebugBypass(true)
  2. 直接内存改写 5d140=1

0x06 求解策略与过程

我没有直接硬啃所有 C++ STL/环境代码,而是采用“可复现仿真 + 逐层验证”的策略。

1. 先验证渲染器 2efd0

构造调用 2efd0(input,64,64,buf,512),确认它输出是稀疏文本位图(例如输入 AAAAAA 只点亮少量位)。

2. 跑 verifyAndDecrypt 抓 memcmp 两侧缓冲

在仿真中拦截 memcmp

  • 参数1:解包后目标位图
  • 参数2:2efd0(input) 渲染位图

3. 比较普通分支 vs debug 分支

  • 普通分支(5d140=0)下目标位图高熵,非文本感。
  • debug 分支(5d140=1)下目标位图变成明显文本位图(总置位约 314)。

4. 用 2efd0 做反求

把“位图距离(XOR bitcount)”作为目标函数,对候选字符串做贪心/爬山优化,长度扫描后在 N=21 收敛到 0 距离。

收敛结果:

  • FLAG{HJMWAPJ2026NBLD}

验证:

  • render(flag) 与 debug 分支解包目标 memcmp 完全相等(bit 差异=0)。

0x07 关键复现命令

1. 查看最终分析结论

Q0.A verifyAndDecrypt(packBytes, trim(input)) 逆向分析(含作弊模式与可通过输入)
1. packBytes 来源
  • Java 协程 Q0.A -> Q0.yassets/hjm_pack.bin 读取字节数组。
  • verifyAndDecrypt 的第一个参数就是这个 packBytes
  • 文件头校验:HJM10x314D4A48),并且模式字段为 2
2. Native 主流程(0x24850

verifyAndDecrypt(JNIEnv*, ..., jbyteArray packBytes, jstring input) 关键流程:

  1. 读取 packBytes 到本地缓冲,校验头部。
  2. 24fc0 更新状态:5d004/5d008/5d00c,并据此更新 5cfe8
  3. 分支按 mode:本题 mode=2,走 0x24b8b 分支。
  4. 关键开关:
    • 5d140 != 0(debug bypass)时,key 来自 2e570()
    • 否则 key 来自 5cff0(并可能按 r13 决定是否 ^0xA5...)。
  5. 2e680 用 key+5cfe8 解包 hjm_pack.bin 的 payload(64x64 bit,512 bytes)。
  6. 2efd0(input,64,64,buf,512) 生成输入位图。
  7. memcmp(decrypted_payload, rendered_input, 512):相等即验证成功。
3. 作弊模式如何开启

Native 里有显式接口:

  • setDebugBypass(boolean)0x24ca0
  • dl==1 时写全局 5d140=1

5d140=1 时,verifyAndDecrypt 强制走 debug key 路径(2e570),这是可稳定复现的“作弊模式”验证分支。

说明:Java 反编译代码里未发现普通 UI 直接调用点;实战可通过 Frida/Hook 直接调用该 native 方法或直接改写 5d140 达到同效果。

4. 真实可通过输入(Flag)

debug bypass=1 分支下,解包目标位图与渲染器 2efd0 逐字符匹配,得到 唯一完全匹配输入

FLAG{HJMWAPJ2026NBLD}

验证结果:

  • memcmp 差异位数 = 0(完全一致)
  • 即可返回“验证成功”分支。
5. 结论
  • packBytesassets/hjm_pack.bin
  • 本题关键是 mode=2 下的位图比较链路:2e680(解包) + 2efd0(渲染) + memcmp
  • 开启作弊模式(setDebugBypass(true))后,正确 flag:

FLAG{HJMWAPJ2026NBLD}

按钮点击事件逻辑分析
1. 分析范围与入口
  • App 入口:MainActivity.onCreate 仅加载 Compose 根内容(Q0.d.b),无多 Activity 按钮分流。
  • 主界面核心在 Q0.N.a(...),主按钮容器在 Q0.N.c(...) -> Q0.h
  • 弹窗(输入 flag)在 Q0.N.a(...) 里通过 androidx.compose.material3.n.b(...) 构建。
2. 按钮总览(全部可点击按钮)
  1. 主界面按钮A:接着奏乐接着舞
  2. 主界面按钮B:投喂 flag
  3. 弹窗按钮A:验证
  4. 弹窗按钮B:取消

补充:弹窗还有 onDismissRequest(点外部/返回键关闭),虽然不是实体按钮,但属于点击/关闭交互分支。


3. 主界面双按钮绑定关系
3.1 绑定证据
  • Q0.N.c(p23, p24, ...) 将两个回调传入 Q0.hv9_1(p23, ..., p24, ...)
  • Q0.h 中第1个 E.a(this.j, ..., new Q0.g(..., 0, ...)),第2个 E.a(this.m, ..., new Q0.g(..., 1, ...))
  • Q0.g 文案分支:
    • case 0 => "接着奏乐接着舞"
    • case 1 => "投喂 flag"
  • Q0.x 实际传参:
    • 第1个回调是 new Q0.v(..., q=0)(节奏点击逻辑)
    • 第2个回调是 new Q0.w(..., j=0)(打开弹窗)
3.2 对应结论
  1. 接着奏乐接着舞 -> Q0.v.case 0
  2. 投喂 flag -> Q0.w.case 0

4. 每个按钮点击逻辑与完整分支
4.1 按钮A:接着奏乐接着舞Q0.v.case 0

入口:Q0.v.o()case 0

分支树
  1. gameState.cheatTriggered == true

    • 直接返回,不再处理点击(已进入作弊态后,节奏点击被锁死)。
  2. 否则继续节奏判定

    • 取当前纳秒时间,计算本次点击相位。
    • 根据节奏点数组(0/250/500/750ms)选最近点位索引。
    • 组装扰动参数后调用 Native:
    • checkRhythm(...)
    • updateExp(...)
  3. Native 返回后分支

    • updateExp >= 0checkRhythm != -7
    • 正常更新 GameState(exp + score,cheat=false)
    • score 映射到 Q0.QNone/Perfect/Good/Miss
    • 否则:
    • 进入作弊分支:GameState(exp保持原值, score=Cheat("嘻嘻"), cheat=true)
可见结果
  • 正常:分数文本在 就绪/完美/良好/失误 间变化。
  • 作弊:分数变 嘻嘻,并触发作弊态展示。

4.2 按钮B:投喂 flagQ0.w.case 0

入口:Q0.w.o()case 0

分支树
  1. 无条件执行:dialogVisible = true
  2. Q0.N.a(...) 检测到 dialogVisible 后显示验证弹窗

4.3 弹窗按钮A:验证Q0.B.o() -> 协程 Q0.A.g()

入口:Q0.C 组合的 TextButton 点击回调是 Q0.B

第1层分支(点击瞬间)
  1. 输入为空(trim后为空

    • 状态文本设为:请先输入 flag
    • 不发起校验协程
  2. 输入非空

    • dialogVisible = false(先关弹窗)
    • 状态文本设为:验证中...
    • 启动协程 Q0.A
第2层分支(协程 Q0.A
  1. 若缓存密文包字节为空

    • 先从 assets/hjm_pack.bin 读取并缓存,再继续校验
  2. 调用 Native 校验

    • verifyAndDecrypt(packBytes, trim(input))
  3. 解包判定

    • h1.a.S(decryptBytes) == null
    • 状态文本:Flag 不正确
    • 否则
    • 保存解包得到的真实层级对象(P
    • 解析输入文本对应层级 Q0.N.j(...)
    • 解析成功:用解析出的层级
    • 解析失败:回退到 a==100 的层级(lv100
    • 状态文本:验证成功
  4. 异常分支(协程中任意异常)

    • 记录日志 verify flag failed
    • 状态文本:验证出错

4.4 弹窗按钮B:取消Q0.w.default 即 j=2)

入口:Q0.D.case 0 中构建 new Q0.w(..., 2)

分支树
  1. 无条件执行:dialogVisible = false
  2. 关闭弹窗,不触发任何校验

4.5 弹窗外部关闭(非按钮,但属于点击关闭分支)

入口:Q0.N.a(...) 弹窗 onDismissRequest = new Q0.w(..., 1)

分支树
  1. 无条件执行:dialogVisible = false
  2. 与“取消”一样只关闭弹窗

5. 结果文本/颜色分支(按钮后效)

Q0.N.i(...) 根据状态文本分支颜色:

  1. 验证成功 -> 成功色
  2. Flag 不正确 -> 失败色
  3. 验证出错 -> 错误色
  4. 其他/空 -> 默认色

这部分不是点击入口,但属于按钮点击后可见分支结果。


6. 完整性结论(按钮是否遗漏)

通过调用点反查可确认未遗漏:

  1. new Q0.v(...) 仅在主界面按钮绑定处出现一次(对应 接着奏乐接着舞)。
  2. new Q0.w(...) 仅出现三种 case:
    • j=0:打开弹窗(投喂 flag
    • j=1:弹窗 onDismissRequest
    • j=2:弹窗 取消
  3. 弹窗 验证 回调唯一入口是 Q0.B -> Q0.A

结论:本 APK 当前界面的所有按钮点击事件及其逻辑分支已全部覆盖。

2. 运行仿真脚本

ai脚本:

import struct
from pathlib import Path
import lief
from unicorn import Uc, UcError, UC_ARCH_X86, UC_MODE_64, UC_HOOK_CODE, UC_PROT_ALL
from unicorn.x86_const import *

PAGE = 0x1000
MASK64 = (1<<64)-1

def p64(x):
    return struct.pack('<Q', x & MASK64)

def u64(b):
    return struct.unpack('<Q', b)[0]

def i64(x):
    return x if x < (1<<63) else x - (1<<64)

def i32(x):
    x &= 0xffffffff
    return x if x < 0x80000000 else x - 0x100000000

class EmuQ8:
    def __init__(self, so_path):
        self.so_path = so_path
        self.bin = lief.parse(str(so_path))
        self.mu = Uc(UC_ARCH_X86, UC_MODE_64)
        self.mapped_pages = set()

        self.HEAP_BASE = 0x10000000
        self.HEAP_SIZE = 0x08000000
        self.STACK_BASE = 0x20000000
        self.STACK_SIZE = 0x02000000
        self.TLS_BASE = 0x30000000
        self.TLS_SIZE = 0x1000
        self.STOP_ADDR = 0x40000000
        self.STUB_BASE = 0x41000000
        self.ENV_PTR = 0x50000000
        self.JNI_TABLE = 0x50001000

        self.heap_cur = self.HEAP_BASE + 0x1000
        self.handle_cur = 0x60000000
        self.load_images = []

        self.jbyte_arrays = {}
        self.jstrings = {}
        self.jstring_cptr = {}

        self.memcmp_records = []
        self.key_records = []
        self.in_verify = False
        self.unknown_imports = {}

        self.ret24_vals = [0, 0]
        self.ret24_idx = 0
        self.popcnt_map = {
            0x298C6: (UC_X86_REG_R14, UC_X86_REG_RBP),
            0x29DB8: (UC_X86_REG_R8, UC_X86_REG_R14),
            0x2A129: (UC_X86_REG_RAX, UC_X86_REG_R14),
            0x2A838: (UC_X86_REG_RSI, UC_X86_REG_R14),
            0x2B34D: (UC_X86_REG_RCX, UC_X86_REG_RBP),
            0x2B572: (UC_X86_REG_RAX, UC_X86_REG_RBP),
            0x2B778: (UC_X86_REG_RSI, UC_X86_REG_R14),
            0x2B92F: (UC_X86_REG_RAX, UC_X86_REG_R12),
            0x2BA68: (UC_X86_REG_RSI, UC_X86_REG_R12),
            0x2BFA7: (UC_X86_REG_R12, UC_X86_REG_RBX),
            0x2C2F7: (UC_X86_REG_RAX, UC_X86_REG_RBX),
            0x2C4A3: (UC_X86_REG_RBX, UC_X86_REG_R14),
            0x2C7EB: (UC_X86_REG_RAX, UC_X86_REG_R14),
            0x2CA58: (UC_X86_REG_RSI, UC_X86_REG_R14),
            0x2CDCD: (UC_X86_REG_R8, UC_X86_REG_RDI),
            0x2D069: (UC_X86_REG_RAX, UC_X86_REG_RBP),
            0x2D0BE: (UC_X86_REG_R9, UC_X86_REG_RBP),
            0x2D388: (UC_X86_REG_RDI, UC_X86_REG_R15),
            0x2D718: (UC_X86_REG_RSI, UC_X86_REG_R12),
        }

        self._map_segments()
        self._map_runtime_regions()
        self._build_import_map()
        self._build_jni_table()
        # Restrict code hooks to hot stub ranges to avoid per-instruction overhead.
        self.mu.hook_add(UC_HOOK_CODE, self._hook_code, begin=self.STOP_ADDR, end=self.STOP_ADDR)
        self.mu.hook_add(UC_HOOK_CODE, self._hook_code, begin=0x55C70, end=0x56570)
        self.mu.hook_add(UC_HOOK_CODE, self._hook_code, begin=self.STUB_BASE, end=self.STUB_BASE + 0x2000)
        self.mu.hook_add(UC_HOOK_CODE, self._hook_code, begin=0x24FC0, end=0x24FC0)
        self.mu.hook_add(UC_HOOK_CODE, self._hook_code, begin=0x2EFD0, end=0x2EFD0)
        self.mu.hook_add(UC_HOOK_CODE, self._hook_code, begin=0x2E680, end=0x2E680)
        for a in self.popcnt_map:
            self.mu.hook_add(UC_HOOK_CODE, self._hook_code, begin=a, end=a)

    def _align_down(self, x):
        return x & ~(PAGE-1)

    def _align_up(self, x):
        return (x + PAGE - 1) & ~(PAGE-1)

    def _map(self, addr, size, perms=UC_PROT_ALL):
        a = self._align_down(addr)
        b = self._align_up(addr + size)
        total = b - a
        try:
            self.mu.mem_map(a, total, perms)
            for p in range(a, b, PAGE):
                self.mapped_pages.add(p)
            return
        except UcError:
            pass

        for p in range(a, b, PAGE):
            if p in self.mapped_pages:
                continue
            self.mu.mem_map(p, PAGE, perms)
            self.mapped_pages.add(p)

    def _map_segments(self):
        raw = Path(self.so_path).read_bytes()
        for s in self.bin.segments:
            if s.type != lief.ELF.Segment.TYPE.LOAD:
                continue
            va = s.virtual_address
            vsz = s.virtual_size
            fsz = s.physical_size
            off = s.file_offset
            self._map(va, vsz, UC_PROT_ALL)
            if fsz > 0:
                self.mu.mem_write(va, raw[off:off+fsz])
            img = bytearray(vsz)
            if fsz > 0:
                img[:fsz] = raw[off:off+fsz]
            self.load_images.append((va, bytes(img)))

    def _map_runtime_regions(self):
        self._map(self.HEAP_BASE, self.HEAP_SIZE, UC_PROT_ALL)
        self._map(self.STACK_BASE, self.STACK_SIZE, UC_PROT_ALL)
        self._map(self.TLS_BASE, self.TLS_SIZE, UC_PROT_ALL)
        self._map(self.STOP_ADDR, PAGE, UC_PROT_ALL)
        self._map(self.ENV_PTR, 0x4000, UC_PROT_ALL)
        self._map(self.STUB_BASE, 0x4000, UC_PROT_ALL)
        self._map(0x0, PAGE, UC_PROT_ALL)
        self.mu.mem_write(self.STOP_ADDR, b"\xCC")

        # Unicorn build in this environment doesn't expose FS_BASE register;
        # keep fs:[0x28] canary in low memory where segmented read resolves.
        self.mu.mem_write(0x28, p64(0x1122334455667788))

    def reset_state(self):
        # Restore original loadable image (including zeroed bss tail).
        for va, img in self.load_images:
            self.mu.mem_write(va, img)

        # Reset runtime arenas and handles.
        self.heap_cur = self.HEAP_BASE + 0x1000
        self.handle_cur = 0x60000000
        self.jbyte_arrays.clear()
        self.jstrings.clear()
        self.jstring_cptr.clear()
        self.memcmp_records.clear()
        self.key_records.clear()
        self.in_verify = False
        self.ret24_idx = 0
        self.unknown_imports.clear()

        # Restore canary and JNI header pointers.
        self.mu.mem_write(0x28, p64(0x1122334455667788))
        self.mu.mem_write(self.ENV_PTR, p64(self.JNI_TABLE))

    def _build_import_map(self):
        self.imp = {}
        rels = self.bin.pltgot_relocations
        for i, r in enumerate(rels):
            addr = 0x55c70 + i * 0x10
            name = r.symbol.name if r.has_symbol else f'idx_{i}'
            self.imp[addr] = name

        self.handlers = {
            0x55ca0: self._h_free,
            0x55cb0: self._h_stack_fail,
            0x55ce0: self._h_malloc,
            0x55cf0: self._h_memset,
            0x55d00: self._h_memcmp,
            0x55e00: self._h_memcpy,
            0x55e10: self._h_memmove,
            0x55e60: self._h_strlen,
            0x55d70: self._h_memchr,
            0x55ec0: self._h_strlen_chk,
            0x55d90: self._h_guard_acquire,
            0x55da0: self._h_guard_release,
            0x55e40: self._h_access,
            0x55db0: self._h_clock_gettime,
            0x55dc0: self._h_readlink,
            0x55de0: self._h_syscall,
            0x55df0: self._h_system_property_get,
            0x55f60: self._h_next_prime,
        }

        # Internal function stubs
        self.handlers[0x24fc0] = self._h_24fc0
        self.handlers[0x2efd0] = self._h_2efd0

    def _build_jni_table(self):
        OFF = {
            'GetStringUTFChars': 0x548,
            'ReleaseStringUTFChars': 0x550,
            'GetArrayLength': 0x558,
            'NewByteArray': 0x580,
            'GetByteArrayRegion': 0x640,
            'SetByteArrayRegion': 0x680,
        }
        self.jni_stub = {}
        idx = 1
        for k, off in OFF.items():
            addr = self.STUB_BASE + idx * 0x100
            idx += 1
            self.jni_stub[addr] = k
            self.handlers[addr] = getattr(self, f'_jni_{k}')
            self.mu.mem_write(self.JNI_TABLE + off, p64(addr))

        self.mu.mem_write(self.ENV_PTR, p64(self.JNI_TABLE))

    def _rd(self, addr, size):
        return self.mu.mem_read(addr, size)

    def _wr(self, addr, data):
        self.mu.mem_write(addr, data)

    def _rd_u64(self, addr):
        return u64(self._rd(addr, 8))

    def _wr_u64(self, addr, v):
        self._wr(addr, p64(v))

    def alloc(self, size):
        size = max(1, size)
        size = (size + 0xF) & ~0xF
        p = self.heap_cur
        self.heap_cur += size
        if self.heap_cur >= self.HEAP_BASE + self.HEAP_SIZE:
            raise RuntimeError('heap exhausted')
        self._wr(p, b"\x00" * size)
        return p

    def new_jbyte_array(self, b: bytes):
        h = self.handle_cur
        self.handle_cur += 8
        self.jbyte_arrays[h] = bytearray(b)
        return h

    def new_jstring(self, s: str):
        h = self.handle_cur
        self.handle_cur += 8
        self.jstrings[h] = s
        self.jstring_cptr[h] = 0
        return h

    def _ret(self):
        rsp = self.mu.reg_read(UC_X86_REG_RSP)
        ra = self._rd_u64(rsp)
        self.mu.reg_write(UC_X86_REG_RSP, rsp + 8)
        self.mu.reg_write(UC_X86_REG_RIP, ra)

    def _hook_code(self, uc, address, size, _user):
        if address == self.STOP_ADDR:
            uc.emu_stop()
            return

        if address in self.popcnt_map:
            dst, src = self.popcnt_map[address]
            v = uc.reg_read(src) & MASK64
            uc.reg_write(dst, v.bit_count())
            uc.reg_write(UC_X86_REG_RIP, address + 5)
            return

        if address == 0x2e680 and self.in_verify:
            key = uc.reg_read(UC_X86_REG_RSI) & MASK64
            self.key_records.append(key)

        h = self.handlers.get(address)
        if h is not None:
            h()
            return

        # Unknown import in PLT range: fail fast for visibility
        if 0x55c70 <= address < 0x56570:
            name = self.imp.get(address, 'unknown')
            self.unknown_imports[(address, name)] = self.unknown_imports.get((address, name), 0) + 1
            self.mu.reg_write(UC_X86_REG_RAX, 0)
            self._ret()

    # ===== JNI handlers =====
    def _jni_GetArrayLength(self):
        h = self.mu.reg_read(UC_X86_REG_RSI)
        arr = self.jbyte_arrays.get(h)
        self.mu.reg_write(UC_X86_REG_RAX, len(arr) if arr is not None else 0)
        self._ret()

    def _jni_GetByteArrayRegion(self):
        h = self.mu.reg_read(UC_X86_REG_RSI)
        start = self.mu.reg_read(UC_X86_REG_RDX) & 0xffffffff
        n = self.mu.reg_read(UC_X86_REG_RCX) & 0xffffffff
        dst = self.mu.reg_read(UC_X86_REG_R8)
        arr = self.jbyte_arrays.get(h, bytearray())
        chunk = bytes(arr[start:start+n])
        if len(chunk) < n:
            chunk += b"\x00" * (n - len(chunk))
        self._wr(dst, chunk)
        self._ret()

    def _jni_NewByteArray(self):
        n = self.mu.reg_read(UC_X86_REG_RSI) & 0xffffffff
        h = self.new_jbyte_array(bytes(n))
        self.mu.reg_write(UC_X86_REG_RAX, h)
        self._ret()

    def _jni_SetByteArrayRegion(self):
        h = self.mu.reg_read(UC_X86_REG_RSI)
        start = self.mu.reg_read(UC_X86_REG_RDX) & 0xffffffff
        n = self.mu.reg_read(UC_X86_REG_RCX) & 0xffffffff
        src = self.mu.reg_read(UC_X86_REG_R8)
        arr = self.jbyte_arrays.get(h)
        if arr is None:
            self._ret()
            return
        data = bytes(self._rd(src, n))
        end = start + n
        if end > len(arr):
            arr.extend(b"\x00" * (end - len(arr)))
        arr[start:end] = data
        self._ret()

    def _jni_GetStringUTFChars(self):
        h = self.mu.reg_read(UC_X86_REG_RSI)
        s = self.jstrings.get(h, "")
        p = self.jstring_cptr.get(h, 0)
        if p == 0:
            b = s.encode('utf-8') + b"\x00"
            p = self.alloc(len(b))
            self._wr(p, b)
            self.jstring_cptr[h] = p
        self.mu.reg_write(UC_X86_REG_RAX, p)
        self._ret()

    def _jni_ReleaseStringUTFChars(self):
        self._ret()

    # ===== Import/internal stubs =====
    def _h_stack_fail(self):
        raise RuntimeError('__stack_chk_fail')

    def _h_malloc(self):
        n = self.mu.reg_read(UC_X86_REG_RDI)
        self.mu.reg_write(UC_X86_REG_RAX, self.alloc(n))
        self._ret()

    def _h_free(self):
        self._ret()

    def _h_memset(self):
        dst = self.mu.reg_read(UC_X86_REG_RDI)
        c = self.mu.reg_read(UC_X86_REG_RSI) & 0xff
        n = self.mu.reg_read(UC_X86_REG_RDX)
        self._wr(dst, bytes([c]) * n)
        self.mu.reg_write(UC_X86_REG_RAX, dst)
        self._ret()

    def _h_memcpy(self):
        dst = self.mu.reg_read(UC_X86_REG_RDI)
        src = self.mu.reg_read(UC_X86_REG_RSI)
        n = self.mu.reg_read(UC_X86_REG_RDX)
        self._wr(dst, bytes(self._rd(src, n)))
        self.mu.reg_write(UC_X86_REG_RAX, dst)
        self._ret()

    def _h_memmove(self):
        dst = self.mu.reg_read(UC_X86_REG_RDI)
        src = self.mu.reg_read(UC_X86_REG_RSI)
        n = self.mu.reg_read(UC_X86_REG_RDX)
        tmp = bytes(self._rd(src, n))
        self._wr(dst, tmp)
        self.mu.reg_write(UC_X86_REG_RAX, dst)
        self._ret()

    def _h_memcmp(self):
        a = self.mu.reg_read(UC_X86_REG_RDI)
        b = self.mu.reg_read(UC_X86_REG_RSI)
        n = self.mu.reg_read(UC_X86_REG_RDX)
        ba = bytes(self._rd(a, n))
        bb = bytes(self._rd(b, n))
        self.memcmp_records.append((a, b, n, ba, bb))
        rv = 0
        if ba != bb:
            for x, y in zip(ba, bb):
                if x != y:
                    rv = -1 if x < y else 1
                    break
        self.mu.reg_write(UC_X86_REG_RAX, rv & MASK64)
        self._ret()

    def _h_strlen(self):
        p = self.mu.reg_read(UC_X86_REG_RDI)
        n = 0
        while True:
            ch = self._rd(p+n, 1)[0]
            if ch == 0:
                break
            n += 1
        self.mu.reg_write(UC_X86_REG_RAX, n)
        self._ret()

    def _h_memchr(self):
        p = self.mu.reg_read(UC_X86_REG_RDI)
        c = self.mu.reg_read(UC_X86_REG_RSI) & 0xff
        n = self.mu.reg_read(UC_X86_REG_RDX)
        data = bytes(self._rd(p, n))
        i = data.find(bytes([c]))
        self.mu.reg_write(UC_X86_REG_RAX, 0 if i < 0 else p + i)
        self._ret()

    def _h_strlen_chk(self):
        self._h_strlen()

    def _h_guard_acquire(self):
        self.mu.reg_write(UC_X86_REG_RAX, 1)
        self._ret()

    def _h_guard_release(self):
        self._ret()

    def _h_access(self):
        self.mu.reg_write(UC_X86_REG_RAX, 0)
        self._ret()

    def _h_clock_gettime(self):
        # int clock_gettime(clockid_t clk_id, struct timespec *tp)
        tp = self.mu.reg_read(UC_X86_REG_RSI)
        self._wr_u64(tp, 1700000000)
        self._wr_u64(tp + 8, 123456789)
        self.mu.reg_write(UC_X86_REG_RAX, 0)
        self._ret()

    def _h_readlink(self):
        # ssize_t readlink(const char *path, char *buf, size_t bufsiz)
        buf = self.mu.reg_read(UC_X86_REG_RSI)
        n = self.mu.reg_read(UC_X86_REG_RDX)
        s = b"/system/bin/app_process64"
        m = min(len(s), n)
        self._wr(buf, s[:m])
        self.mu.reg_write(UC_X86_REG_RAX, m)
        self._ret()

    def _h_syscall(self):
        self.mu.reg_write(UC_X86_REG_RAX, -1 & MASK64)
        self._ret()

    def _h_system_property_get(self):
        # int __system_property_get(const char* name, char* value)
        name_p = self.mu.reg_read(UC_X86_REG_RDI)
        out_p = self.mu.reg_read(UC_X86_REG_RSI)
        name = self._read_cstr(name_p)
        props = {
            'ro.debuggable': '0',
            'ro.secure': '1',
            'ro.build.tags': 'release-keys',
            'ro.build.type': 'user',
        }
        v = props.get(name, '')
        b = v.encode() + b"\x00"
        self._wr(out_p, b)
        self.mu.reg_write(UC_X86_REG_RAX, len(v))
        self._ret()

    def _h_next_prime(self):
        n = self.mu.reg_read(UC_X86_REG_RDI)
        if n <= 2:
            p = 2
        else:
            p = n if (n & 1) else n + 1
            while True:
                ok = True
                d = 3
                while d * d <= p:
                    if p % d == 0:
                        ok = False
                        break
                    d += 2
                if ok:
                    break
                p += 2
        self.mu.reg_write(UC_X86_REG_RAX, p & MASK64)
        self._ret()

    def _h_24fc0(self):
        if self.ret24_idx < len(self.ret24_vals):
            v = self.ret24_vals[self.ret24_idx]
        else:
            v = self.ret24_vals[-1]
        self.ret24_idx += 1
        self.mu.reg_write(UC_X86_REG_RAX, v & MASK64)
        self._ret()

    def _h_2efd0(self):
        # int render(const char* in, int w, int h, uint8_t* out, size_t outlen)
        out = self.mu.reg_read(UC_X86_REG_RCX)
        outlen = self.mu.reg_read(UC_X86_REG_R8)
        # deterministic dummy bitmap
        self._wr(out, b"\x00" * outlen)
        self.mu.reg_write(UC_X86_REG_RAX, 1)
        self._ret()

    def _read_cstr(self, p, limit=0x1000):
        bs = bytearray()
        for i in range(limit):
            c = self._rd(p+i, 1)[0]
            if c == 0:
                break
            bs.append(c)
        return bs.decode('utf-8', errors='ignore')

    def call(self, fn, args, timeout=1_000_000):
        regs = [UC_X86_REG_RDI, UC_X86_REG_RSI, UC_X86_REG_RDX, UC_X86_REG_RCX, UC_X86_REG_R8, UC_X86_REG_R9]
        for r, v in zip(regs, args):
            self.mu.reg_write(r, v & MASK64)

        rsp = self.STACK_BASE + self.STACK_SIZE - 0x100
        rsp &= ~0xF
        rsp -= 8
        self._wr_u64(rsp, self.STOP_ADDR)
        self.mu.reg_write(UC_X86_REG_RSP, rsp)
        self.mu.reg_write(UC_X86_REG_RIP, fn)

        try:
            self.mu.emu_start(fn, self.STOP_ADDR, count=timeout)
        except UcError as e:
            rip = self.mu.reg_read(UC_X86_REG_RIP)
            rsp2 = self.mu.reg_read(UC_X86_REG_RSP)
            raise RuntimeError(f'emu error {e} rip={hex(rip)} rsp={hex(rsp2)} fn={hex(fn)}') from e
        return self.mu.reg_read(UC_X86_REG_RAX)

    def g_u64(self, addr):
        return self._rd_u64(addr)

    def g_u32(self, addr):
        return struct.unpack('<I', self._rd(addr, 4))[0]

    def g_u8(self, addr):
        return self._rd(addr, 1)[0]

    def render_text(self, s: str, w=64, h=64):
        # Directly call 2efd0 with C-string input and fresh heap space.
        self.heap_cur = self.HEAP_BASE + 0x1000
        b = s.encode('utf-8') + b"\x00"
        in_ptr = self.alloc(len(b))
        self._wr(in_ptr, b)
        out_len = (w * h) // 8
        out_ptr = self.alloc(out_len)
        self._wr(out_ptr, b"\x00" * out_len)
        rv = self.call(0x2EFD0, [in_ptr, w, h, out_ptr, out_len], timeout=2_000_000)
        data = bytes(self._rd(out_ptr, out_len))
        return rv & 0xFF, data

def java_noise(idx, now_ns):
    x = (((idx & 0xffffffff) << 32) ^ (now_ns & MASK64)) & MASK64
    x ^= ((x << 13) & MASK64)
    x ^= (x >> 7)
    x ^= ((x << 17) & MASK64)
    return x & 0xffffffff

def run_case(
    ret_start,
    ret_verify,
    clicks=1000,
    input_text='AAAAAA',
    do_clicks=False,
    key_override=0x661606DD8316E47D,
    gate_override=1,
    emu=None,
    debug_bypass=None,
):
    if emu is None:
        emu = EmuQ8('_apk/lib/x86_64/libhajimi.so')
    else:
        emu.reset_state()
    emu.ret24_vals = [ret_start, ret_verify]

    # prepare startSession args
    beats = [0, 250, 500, 750]
    b = b''.join(struct.pack('<I', x) for x in beats)
    beat_arr = emu.new_jbyte_array(b)

    t0 = 1_700_000_000_000_000_000
    emu.call(0x238a0, [emu.ENV_PTR, 0, t0, beat_arr, 1000])

    exp = 0
    last_score = 0
    if do_clicks:
        for i in range(clicks):
            now = t0 + i * 250_000_000
            idx = i & 3
            noise = java_noise(idx, now)
            over50 = 1 if exp >= 50 else 0
            score = emu.call(0x23e50, [emu.ENV_PTR, 0, now, idx, noise, over50]) & 0xffffffff
            last_score = i32(score)
            exp_raw = emu.call(0x23f60, [emu.ENV_PTR, 0, score, idx, noise])
            exp = i64(exp_raw & MASK64)
    else:
        emu._wr_u64(0x5cff0, key_override)
        emu._wr(0x5cff8, bytes([gate_override & 0xff]))

    key = emu.g_u64(0x5cff0)
    gate = emu.g_u8(0x5cff8)

    pack = Path('_apk/assets/hjm_pack.bin').read_bytes()
    pack_arr = emu.new_jbyte_array(pack)
    jstr = emu.new_jstring(input_text)

    if debug_bypass is not None:
        emu._wr(0x5d140, bytes([debug_bypass & 0xFF]))

    emu.in_verify = True
    rv = emu.call(0x24850, [emu.ENV_PTR, 0, pack_arr, jstr])
    emu.in_verify = False

    out_target = None
    out_render = None
    cmp_len = None
    if emu.memcmp_records:
        a, b, n, ba, bb = emu.memcmp_records[-1]
        out_target = ba
        out_render = bb
        cmp_len = n

    return {
        'ret': rv,
        'exp': exp,
        'gate': gate,
        'key': key,
        'last_score': last_score,
        'memcmp_len': cmp_len,
        'target': out_target,
        'render': out_render,
        'key_records': emu.key_records,
        'g_5cfe0': emu.g_u64(0x5cfe0),
        'g_5cfe8': emu.g_u64(0x5cfe8),
        'g_5d004': emu.g_u32(0x5d004),
        'g_5d008': emu.g_u32(0x5d008),
        'g_5d00c': emu.g_u8(0x5d00c),
    }

if __name__ == '__main__':
    r = run_case(0, 0, do_clicks=False)
    print('exp', r['exp'], 'gate', r['gate'], 'key', hex(r['key']))
    print('5cfe0', hex(r['g_5cfe0']), '5cfe8', hex(r['g_5cfe8']))
    print('5d004', hex(r['g_5d004']), '5d008', r['g_5d008'], '5d00c', r['g_5d00c'])
    print('memcmp_len', r['memcmp_len'], 'key_records', [hex(x) for x in r['key_records']])
    if r['target']:
        ones = sum(bin(x).count('1') for x in r['target'])
        print('target ones', ones)
        Path('_analysis/verify_target_repro.bin').write_bytes(r['target'])
        print('wrote _analysis/verify_target_repro.bin')

说明:

  • 支持调用 verifyAndDecrypt 路径并抓取 memcmp
  • 支持 debug_bypass=1 分支验证。
  • 支持直接调用 2efd0 渲染位图。

0x08 最终答案

作弊模式开启后(setDebugBypass(true)),正确 flag:

FLAG{HJMWAPJ2026NBLD}


【春节】解题领红包之九 {Web 中级题} 出题老师:Coxxs

0x00 题目与目标

题目是一个“语音验证码”页面:

  • 前端入口:Q9/index.html
  • 逻辑脚本:Q9/assets/verify.js
  • Wasm 载体:Q9/assets/verify.wasm.js(内嵌 base64 wasm)

页面校验逻辑是:输入 flag{...},取中间 code,做 0x2026 次 SHA-256,和 challenge 哈希比较。

目标:还原 wasm_bindgen.gen(uid, voice) 中 50 位 code 的生成算法,复现出 flag{50位code}


0x01 入口 JavaScript 分析

先看 Q9/assets/verify.js

  1. wasm_bindgen.gen(uid, voice) 返回对象:
    • a:音频 wav bytes
    • h:校验哈希(hex)
  2. checkCode(code, expectedHash)
    • code0x2026 次 SHA-256
    • 结果 hex 与 expectedHash 比较

因此本题本质是:从 wasm 里拿到真实 code 生成逻辑,而不是识别语音。


0x02 Wasm 静态定位

把 wasm 转 WAT(本地已导出为 Q9/wasm.txt),在 func $gen 里定位关键块。

1) 随机数与初始缓冲

Q9/wasm.txt:14219 开始:

  • var3+80 处申请 17 字节
  • 调用 crypto.getRandomValues 填充 17 字节随机数

2) 37 字节工作缓冲 var9

Q9/wasm.txt:14579call $func77 申请 37 字节(var9)。

前 21 字节构造:

  • var9[0..3] = random[0..3] XOR uid_le[0..3]
    • 对应 14586~14617
  • var9[4..20] = random[0..16]
    • 对应 14620~14637(连续 copy)

3) HMAC 密钥提取(题目要求的 14 字节)

Q9/wasm.txt:14701~14703

  • 从内存偏移 1295967 拷贝 14 字节
  • 作为 HMAC key

提取结果:

  • hex: 0001010101010100010001000502
  • bytes: [0,1,1,1,1,1,1,0,1,0,1,0,5,2]

4) HMAC 过程

Q9/wasm.txt:14759~14887

  • 64 字节块先 XOR 0x36(ipad)
  • 后 XOR 0x6A(把 ipad 变 opad,等价 key^0x5c)
  • 调用 func9(SHA-256 压缩流程)

最终 digest 的前 16 字节写入 var9[21..36]15519 一带写回)。

即:

var9 = (random[0..3]^uid_le) + random17 + HMAC_SHA256(key, first21)[:16]


0x03 50 位 code 生成算法

关键循环在 Q9/wasm.txt:15627~15741

  • 字符表查表基址:i32.load8_u offset=1295903
  • 表内容:

abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!

  • var9 的 37 字节做 6-bit 拆分:
    • 37 * 8 = 296 bit
    • 296 / 6 = 49 余 2
    • 补齐后得到 50 个 6-bit 索引
  • 每个索引查字符表,得到 50 位 code

这一步本质是“自定义字符表的 base64 风格编码”。


0x04 语音拼接流程(题目重点)

语音拼接入口在 Q9/wasm.txt:16335 附近,核心是反复 call $func34 扩容并 memory.copy

整体拼接顺序:

  1. 前缀音频(按 voice 选择固定片段)

    • voice='c'offset=1048577, len=76762
    • voice='y'offset=1211415, len=84480
    • voice='e'offset=1125339, len=86076
    • 对应 16318~16354
  2. flag{ 五个字符逐个转语音并拼接

    • 字符来源常量区 1295898 开始(UTF-8 读取循环)
    • 对应 16389~16593
  3. 50 位 code 每个字符转语音并拼接

    • 从前面生成的 50 字符数组读取
    • 对应 16594~16805
  4. } 字符转语音并拼接

    • 直接写入 codepoint 125
    • 对应 16807~16921
  5. 封装 WAV 头 + data 段

    • 写入 RIFF/WAVE/fmt /data 常量
    • 对应 16989 之后

结论:你的判断是对的——音频就是

"flag{" + 50位code + "}"

逐字符 TTS 后拼接。


0x05 Python 还原与复现

已在 Q9/算法.py 完整实现:

  • 固定密钥(14 字节)
  • 37 字节构造
  • wasm 同款 6-bit 提取
  • 0x2026 次 SHA-256 校验哈希

关键函数:

  • build_37_bytes(...)
  • wasm_extract_50_chars(...)
  • hash_2026(...)

运行示例(固定演示随机数 00..10):

python Q9/算法.py --uid 551842 --voice c --demo-seed0

输出:

  • code50=OMOkaWabaGmebqyhcaKkcWWndG8qSSAL1wczlV4Z9PEiq5cP!q
  • flag{OMOkaWabaGmebqyhcaKkcWWndG8qSSAL1wczlV4Z9PEiq5cP!q}
  • hash_2026=1aa787ab510dae05976a5553df8fd2506e821e09061d38de4d66646678143c8e

和 wasm 生成的 challenge 哈希一致。


0x06 关键结论

  1. code 的本体只由 uid + random17 + 固定14字节密钥 决定。
  2. voice 仅影响语音素材选择,不影响 50 位 code 与哈希。
  3. 题目不是音频识别题,核心是 wasm 算法还原题。
  4. flag 结构固定为:

flag{<50 chars from [a-zA-Z0-9?!]>}


0x07 附:可复现单次 challenge 的方法

若你想复现“某一次页面生成出来的那条语音”对应 flag:

  1. Hook 当次 crypto.getRandomValues 取到 17 字节随机数。
  2. 拿到当次 uid
  3. 运行:
python Q9/算法.py --uid <uid> --voice c --rand-hex <17字节hex>

即可得到当次正确 flag{...}

Q9/算法.py

import argparse
import hashlib
import hmac
import os

KEY_14 = bytes([0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 5, 2])
CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789?!"

def build_37_bytes(uid: int, random17: bytes) -> bytes:
    if len(random17) != 17:
        raise ValueError("random17 must be exactly 17 bytes")

    uid_le = uid.to_bytes(4, "little", signed=False)
    first4 = bytes(
        [
            random17[0] ^ uid_le[0],
            random17[1] ^ uid_le[1],
            random17[2] ^ uid_le[2],
            random17[3] ^ uid_le[3],
        ]
    )
    first21 = first4 + random17

    digest = hmac.new(KEY_14, first21, hashlib.sha256).digest()
    return first21 + digest[:16]

def wasm_extract_50_chars(src37: bytes) -> str:
    if len(src37) != 37:
        raise ValueError("src37 must be exactly 37 bytes")

    out = []
    var0 = 0
    var5 = 1
    var1 = 0
    var4_idx = 0
    var7 = 0
    var11 = 0

    while True:
        b = src37[var4_idx]
        var11 = b
        var4_idx = var5
        var7 = (b | ((var7 << 8) & 0xFFFFFFFF)) & 0xFFFFFFFF
        var2 = var1

        while True:
            var1 = var2 + 2
            idx = (var7 >> var1) & 0x3F
            out.append(CHARSET[idx])
            var0 += 1
            var2 -= 6
            if var1 <= 5:
                break

        var1 = var2 + 8
        if var5 == 37:
            break
        var5 += 1

    if var2 != -8:
        idx = (var11 << (-2 - var2)) & 0x3F
        out.append(CHARSET[idx])

    if len(out) != 50:
        raise RuntimeError(f"unexpected output length: {len(out)}")
    return "".join(out)

def hash_2026(code: str) -> str:
    cur = code.encode("ascii")
    for _ in range(0x2026):
        cur = hashlib.sha256(cur).digest()
    return cur.hex()

def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--uid", type=int, default=551842)
    parser.add_argument("--voice", default="c")
    parser.add_argument(
        "--rand-hex",
        default=None,
        help="17-byte random in hex; if omitted, uses os.urandom(17)",
    )
    parser.add_argument(
        "--demo-seed0",
        action="store_true",
        help="use fixed random 00..10 (matches JS hook demo)",
    )
    args = parser.parse_args()

    if args.demo_seed0:
        random17 = bytes(range(17))
    elif args.rand_hex is None:
        random17 = os.urandom(17)
    else:
        random17 = bytes.fromhex(args.rand_hex)

    src37 = build_37_bytes(args.uid, random17)
    code50 = wasm_extract_50_chars(src37)
    h = hash_2026(code50)

    print(f"uid={args.uid}, voice='{args.voice}'")
    print(f"random17={random17.hex()}")
    print(f"code50={code50}")
    print(f"flag{{{code50}}}")
    print(f"hash_2026={h}")

if __name__ == "__main__":
    main()

免费评分

参与人数 3吾爱币 +3 热心值 +3 收起 理由
sn954321 + 1 + 1 我很赞同!
唐小样儿 + 1 + 1 我很赞同!
Command + 1 + 1 圈小猫的HARD难度居然还能过吗?? 不会玩啊

查看全部评分

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

推荐
ZhangYixiSuccee 发表于 2026-3-5 14:22
我和大佬解出来的一样,也是'<PREDEFINED_FLOAT_INDEX_17><PREDEFINED_FLOAT_INDEX_47><PREDEFINED_FLOAT_INDEX_14><PREDEFINED_FLOAT_INDEX_14>,我知道长度是30,但是这几个数字不知道怎么处理了,然后就卡住了,其实就当数字处理就好了,太可惜了
沙发
abuiamei 发表于 2026-3-5 08:23
3#
 楼主| jingtai123 发表于 2026-3-5 08:54 |楼主
@Command 先刷一个初始点靠边的开局,沿着小猫最近出逃路线的最远点开始布局,隔点补齐
4#
uniboy 发表于 2026-3-5 08:58
膜拜大佬
5#
 楼主| jingtai123 发表于 2026-3-5 10:07 |楼主
附张图@Command

23b2bc40-116c-415a-b64a-cc471fb87443.png (60.28 KB, 下载次数: 0)

23b2bc40-116c-415a-b64a-cc471fb87443.png
6#
 楼主| jingtai123 发表于 2026-3-5 11:59 |楼主
再来一个

ScreenShot_2026-03-05_115828_378.png (74.36 KB, 下载次数: 0)

ScreenShot_2026-03-05_115828_378.png
7#
 楼主| jingtai123 发表于 2026-3-5 12:02 |楼主
太容易了

ScreenShot_2026-03-05_120155_503.png (79.89 KB, 下载次数: 0)

ScreenShot_2026-03-05_120155_503.png
8#
 楼主| jingtai123 发表于 2026-3-5 12:05 |楼主
继续发个

ScreenShot_2026-03-05_120456_068.png (75.26 KB, 下载次数: 0)

ScreenShot_2026-03-05_120456_068.png
9#
 楼主| jingtai123 发表于 2026-3-5 12:12 |楼主
继续发继续发

ScreenShot_2026-03-05_121200_796.png (61.43 KB, 下载次数: 0)

ScreenShot_2026-03-05_121200_796.png
10#
 楼主| jingtai123 发表于 2026-3-5 12:18 |楼主

继续发继续发

ScreenShot_2026-03-05_121752_899.png (76.28 KB, 下载次数: 0)

ScreenShot_2026-03-05_121752_899.png
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-3-10 02:37

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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