AI做题思路
[quote]# 完整对话记录
题目: chu8
分类: reverse
模型: claude-opus-4-6-think
创建时间: 2026-02-24 14:00:17
消息总数: 294
👤 用户 [14:00:17]
请分析并解决这道 reverse 类型的 CTF 题目:chu8
题目描述: 安卓逆向下载地址:
https://down.52pojie.cn/9Ax3NVYO.7z | PassWord:lDiCOaBh13Ov
👤 用户 [14:00:17]
[System — Mandatory Planning Phase]
Before executing ANY tools, you MUST first create a structured solving plan:
Step 1: Read the challenge description carefully — identify category, key constraints, and attack surface
Step 2: If attachments exist, download and examine them FIRST (file type, checksec, strings) before planning
Step 3: Based on gathered info, use todolist (action: reset) to create 3-5 concrete, ORDERED solving steps
Step 4: Rank steps from most-likely-to-succeed to least-likely
Step 5: Only THEN begin executing the first step
🧠 Planning Quality Checklist:
- Each step should be a TESTABLE hypothesis (e.g., 'Test for SQL injection on /login param'), not vague (e.g., 'Try web attacks')
- Include WHAT tool to use and WHAT to look for in each step
- First step should always be information gathering (read source, analyze binary, fetch target)
- Have at least one backup approach from a different attack angle
Previous solving ideas for this challenge:
No ideas yet. Use action 'add' to brainstorm approaches.
⚠️ Review these ideas carefully:
- Skip approaches marked as 'failed' — they already proved unsuccessful
- Prioritize 'pending' ideas — they haven't been tried yet
- Build your plan around untried approaches
Do NOT skip planning. A structured approach prevents wasting rounds on random attempts.
🤖 助手 - 第 1 轮 [14:00:24]
<details>
<summary>💭 思考过程</summary>
Let me analyze this challenge. It's an Android reverse engineering challenge called "chu8" from 52pojie. I need to download the 7z file, extract it, and then analyze the APK.
Let me start by downloading and examining the file.
</details>
好的,这是一道安卓逆向题。让我先下载附件并分析。
⬇️ 下载文件: https://down.52pojie.cn/9Ax3NVYO.7z
📋 download_file 输出 [14:00:24]
Downloaded: https://down.52pojie.cn/9Ax3NVYO.7z
Saved to: D:\AI\AICTF\workdir\52pojie\chu8\chu8.7z
Size: 2955939 bytes
Content-Type: application/x-7z-compressed
👤 用户 [14:00:26]
<historical_experience>
⚠️ 严禁将以下案例中的具体值(XOR key、checksum、flag 字符串、密码、偏移量)直接用于当前题目。仅用于识别技术方向和分析思路。
以下是从历史解题中提取的技术模式,仅供思路启发,必须结合当前题目独立分析,不要照搬历史解法:
[cases] chu7: UPX-packed PE32+ with CRC64 key derivation and custom stream cipher using AES S-box (relevance: 0.47)
Windows GUI program CM1.exe encrypted flag.png using a custom algorithm. Key insight: UPX packing concealed real code; after unpacking, found hardcoded key prefix '52pojie2026' combined with user password to derive CRC64 hash (0xC96C5795D7870F42 polynomial), which initializes a stream cipher using AES S-box for keystream generation. PNG magic bytes (89504e47...) serve as known plaintext to recover stream cipher key 0x55a4f867ba4475dd via XOR. Solution: Unpack binary, extract CRC64 polynomial and S-box at 0x14000A270, implement stream cipher logic (ROL3 + S-box substitution + XOR with IV), use known plaintext attack to recover CRC64 state, decrypt file, verify CRC32 checksum, and extract flag from PNG tEXt chunk metadata.
[cases] 初二题目 PE32 XOR password recovery (relevance: 0.41)
Windows PE32 reverse challenge where hardcoded DWORDs were XORed to recover password. Binary had Chinese filename causing path issues. Solution extracted 6 DWORD values from binary and XORed them sequentially.
[cases] Windows PE32 crackme with XOR-obfuscated password and weighted checksum validation (relevance: 0.41)
52pojie Spring Festival CTF 'Day 2' challenge: PE32 console app with fake flag hint, 31-char password requirement, XOR-deobfuscation (key 0x42), and weighted checksum (sum of position*value = 44709). Key insight: ignore fake flag, reverse sub_401620's DWORD buffer XOR logic.
[cases] reverse1: Simple character replacement validation in PE x64 binary (relevance: 0.40)
Challenge: PE x64 Windows executable that validates user input against a hardcoded flag. Key insight: The flag string is stored in the binary's data section and can be extracted directly via IDA Pro. The validation logic replaces all 'o' characters with '0' before comparison. Solution: Use IDA Pro headless to decompile main function, identify the global string variable (Str2), extract its value, apply the character replacement transformation, and wrap with flag{} format.
[cases] SUCTF2018 single dog - AAEncode in MISC image (relevance: 0.38)
Misc image steganography challenge. Used binwalk to extract embedded zip+txt from image file. Hidden content was AAEncoded, decoded to get flag. Key insight: check hex for file inclusion anomalies, use binwalk for extraction.
</historical_experience>
🤖 助手 - 第 2 轮 [14:00:31]
<details>
<summary>💭 思考过程</summary>
Let me extract the 7z file and analyze the contents.
</details>
📦 解压: D:\AI\AICTF\workdir\52pojie\chu8\chu8.7z
📋 extract_archive 输出 [14:00:31]
$ 7z x D:\AI\AICTF\workdir\52pojie\chu8\chu8.7z -> D:\AI\AICTF\workdir\52pojie\chu8\extracted
7-Zip 24.06 (x64) : Copyright (c) 1999-2024 Igor Pavlov : 2024-05-26
Scanning the drive for archives:
1 file, 2955939 bytes (2887 KiB)
Extracting archive: D:\AI\AICTF\workdir\52pojie\chu8\chu8.7z
--
Path = D:\AI\AICTF\workdir\52pojie\chu8\chu8.7z
Type = 7z
Physical Size = 2955939
Headers Size = 275
Method = LZMA2:6m 7zAES
Solid = -
Blocks = 1
Everything is Ok
Size: 4459647
Compressed: 2955939
Extracted 1 files:
【2026春节】解题领红包之八 {Android 中级题} 出题老师:正己.apk
🤖 助手 - 第 3 轮 [14:00:37]
<details>
<summary>💭 思考过程</summary>
Good, extracted an APK file. Let me examine it and create a plan.
</details>
📋 查找结果 [14:00:37]
D:\AI\AICTF\workdir\52pojie\chu8\extracted
D:\AI\AICTF\workdir\52pojie\chu8\extracted\【2026春节】解题领红包之八 {Android 中级题} 出题老师:正己.apk
🤖 助手 - 第 4 轮 [14:00:41]
🔨 执行命令
$ copy "D:\AI\AICTF\workdir\52pojie\chu8\extracted\【2026春节】解题领红包之八 {Android 中级题} 出题老师:正己.apk" "D:\AI\AICTF\workdir\52pojie\chu8\chu8.apk"
📋 命令输出 [14:00:42]
🤖 助手 - 第 5 轮 [14:00:46]
🔨 执行命令
$ file "D:\AI\AICTF\workdir\52pojie\chu8\chu8.apk"
📋 命令输出 [14:00:47]
D:\AI\AICTF\workdir\52pojie\chu8\chu8.apk: Zip archive data, at least v0.0 to extract, compression method=deflate
🤖 助手 - 第 6 轮 [14:00:53]
好,这是一个 APK 文件(Android 应用)。让我先解压看看结构,然后用 jadx 反编译。
🔨 执行命令
$ mkdir "D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped" && cd /d "D:\AI\AICTF\workdir\52pojie\chu8" && 7z x chu8.apk -oapk_unzipped -y 2>&1 | head -30
📋 命令输出 [14:00:54]
Directory: D:\AI\AICTF\workdir\52pojie\chu8
[32;1mMode [0m[32;1m LastWriteTime[0m [32;1;3m Length[0m[32;1m Name[0m
[32;1m---- [0m [32;1m -------------[0m [32;1m ------[0m [32;1m----[0m
d---- 2026/2/24 14:00 [44;1mapk_unzipped[0m
[31;1mSet-Location: [31;1mA positional parameter cannot be found that accepts argument 'D:\AI\AICTF\workdir\52pojie\chu8'.[0m
[exit code: exit status 1]
🤖 助手 - 第 7 轮 [14:00:58]
📦 解压: D:\AI\AICTF\workdir\52pojie\chu8\chu8.apk
📋 extract_archive 输出 [14:00:58]
$ 7z x D:\AI\AICTF\workdir\52pojie\chu8\chu8.apk -> D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped (fallback)
7-Zip 24.06 (x64) : Copyright (c) 1999-2024 Igor Pavlov : 2024-05-26
Scanning the drive for archives:
1 file, 4459647 bytes (4356 KiB)
Extracting archive: D:\AI\AICTF\workdir\52pojie\chu8\chu8.apk
--
Path = D:\AI\AICTF\workdir\52pojie\chu8\chu8.apk
Type = zip
Physical Size = 4459647
Everything is Ok
Files: 161
Size: 5290531
Compressed: 4459647
Extracted 50 files:
AndroidManifest.xml
DebugProbesKt.bin
META-INF
META-INF\androidx.activity_activity-compo
... [truncated 3011 bytes] ...
META-INF\androidx.profileinstaller_profileinstaller.version
META-INF\androidx.savedstate_savedstate-ktx.version
META-INF\androidx.savedstate_savedstate.version
META-INF\androidx.startup_startup-runtime.version
META-INF\androidx.tracing_tracing.version
META-INF\androidx.versionedparcelable_versionedparcelable.version
META-INF\com
META-INF\com\android
META-INF\com\android\build
META-INF\com\android\build\gradle
META-INF\com\android\build\gradle\app-metadata.properties
META-INF\kotlinx_coroutines_android.version
META-INF\kotlinx_coroutines_core.version
... (truncated)
🤖 助手 - 第 8 轮 [14:01:03]
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped (模式: *.dex)
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped (模式: *.so)
📋 查找结果 [14:01:03]
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\classes.dex
📋 查找结果 [14:01:03]
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\arm64-v8a\libhajimi.so
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\armeabi-v7a\libhajimi.so
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86\libhajimi.so
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so
🤖 助手 - 第 9 轮 [14:01:11]
<details>
<summary>💭 思考过程</summary>
This is an Android reverse engineering challenge. It has:
- A
classes.dex file (Java/Kotlin code)
- A native library
libhajimi.so for multiple architectures
Let me first decompile the DEX to understand the Java/Kotlin layer, then analyze the native library. Let me use jadx for the DEX and IDA for the SO.
</details>
有 DEX 和 native SO 库。让我先用 jadx 反编译 DEX,再用 IDA 分析 SO。
🔨 执行命令
$ where jadx 2>nul || echo "jadx not found"
🔨 执行命令
$ file "D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so"
📋 命令输出 [14:01:12]
📋 命令输出 [14:01:12]
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=270f490cb71fbaa3b51330dd72f8d9bc7738ffe0, stripped
🤖 助手 - 第 10 轮 [14:01:18]
🔨 工具调用: todolist
- action:
reset
- tasks:
[1. 用jadx反编译APK,分析Java/Kotlin层逻辑,找到验证入口和native方法调用 2. 用IDA分析libhajimi.so (x86_64),找到JNI函数和核心验证逻辑 3. 分析加密/验证算法,提取密钥和常量 4. 编写脚本逆向算法,恢复flag 5. 提交flag]
📋 todolist 输出 [14:01:18]
TodoList reset with 5 task(s):
⬜ 1. 用jadx反编译APK,分析Java/Kotlin层逻辑,找到验证入口和native方法调用
⬜ 2. 用IDA分析libhajimi.so (x86_64),找到JNI函数和核心验证逻辑
⬜ 3. 分析加密/验证算法,提取密钥和常量
⬜ 4. 编写脚本逆向算法,恢复flag
⬜ 5. 提交flag
🤖 助手 - 第 11 轮 [14:01:23]
🔨 工具调用: todolist
- action:
update
- id:
1
- status:
in_progress
📋 todolist 输出 [14:01:23]
Updated task #1. Current TodoList:
🔄 1. 用jadx反编译APK,分析Java/Kotlin层逻辑,找到验证入口和native方法调用
⬜ 2. 用IDA分析libhajimi.so (x86_64),找到JNI函数和核心验证逻辑
⬜ 3. 分析加密/验证算法,提取密钥和常量
⬜ 4. 编写脚本逆向算法,恢复flag
⬜ 5. 提交flag
🤖 助手 - 第 12 轮 [14:01:28]
🔨 执行命令
$ jadx -d "D:\AI\AICTF\workdir\52pojie\chu8\jadx_out" "D:\AI\AICTF\workdir\52pojie\chu8\chu8.apk" --no-res 2>&1 | tail -10
📋 命令输出 [14:01:29]
[31;1mjadx: [31;1mThe term 'jadx' is not recognized as a name of a cmdlet, function, script file, or executable program.[0m
[31;1m[31;1mCheck the spelling of the name, or if a path was included, verify that the path is correct and try again.[0m
[exit code: exit status 1]
🤖 助手 - 第 13 轮 [14:01:35]
🔨 执行命令
$ dir "D:\AI\AICTF\Tools" 2>nul && dir "D:\AI\AICTF\Tools\jadx*" 2>nul
📋 命令输出 [14:01:35]
Directory: D:\AI\AICTF\Tools
[32;1mMode [0m[32;1m LastWriteTime[0m [32;1;3m Length[0m[32;1m Name[0m
[32;1m---- [0m [32;1m -------------[0m [32;1m ------[0m [32;1m----[0m
d---- 2026/2/14 9:19 [44;1mIDA[0m
d---- 2026/2/19 14:46 [44;1mjadx[0m
d---- 2026/2/19 14:53 [44;1mphp[0m
-a--- 2026/2/19 14:40 25926183 apktool_2.12.1.jar
-a--- 2026/2/24 13:49 524288 [31;1mphp.zip[0m
-a--- 2025/5/6 10:19 599040 [32;1mupx.exe[0m
-a--- 2026/2/11 16:46 5927424 [32;1mwsrx.exe[0m
d---- 2026/2/19 14:46 [44;1mjadx[0m
🤖 助手 - 第 14 轮 [14:01:39]
🔨 执行命令
$ dir "D:\AI\AICTF\Tools\jadx" 2>nul
📋 命令输出 [14:01:40]
Directory: D:\AI\AICTF\Tools\jadx
[32;1mMode [0m[32;1m LastWriteTime[0m [32;1;3m Length[0m[32;1m Name[0m
[32;1m---- [0m [32;1m -------------[0m [32;1m ------[0m [32;1m----[0m
d---- 2026/2/19 14:46 [44;1mbin[0m
d---- 2026/2/19 14:46 [44;1mlib[0m
----- 1980/2/1 0:00 11357 LICENSE
----- 1980/2/1 0:00 17870 README.md
🤖 助手 - 第 15 轮 [14:01:44]
🔨 执行命令
$ "D:\AI\AICTF\Tools\jadx\bin\jadx.bat" -d "D:\AI\AICTF\workdir\52pojie\chu8\jadx_out" "D:\AI\AICTF\workdir\52pojie\chu8\chu8.apk" --no-res 2>&1 | tail -10
📋 命令输出 [14:01:45]
[31;1mParserError: [0m
[31;1m[36;1mLine |[0m
[31;1m[36;1m[36;1m 1 | [0m … ext.Encoding]::UTF8; "D:\AI\AICTF\Tools\jadx\bin\jadx.bat" [36;1m-d[0m "D:\AI\ …[0m
[31;1m[36;1m[36;1m[0m[36;1m[0m[36;1m | [31;1m ~~[0m
[31;1m[36;1m[36;1m[0m[36;1m[0m[36;1m[31;1m[31;1m[36;1m | [31;1mUnexpected token '-d' in expression or statement.[0m
[exit code: exit status 1]
👤 用户 [14:01:45]
[Progress Checkpoint - Round 16/200]
Briefly assess your progress:
- What have you accomplished so far?
- Are you making progress toward the flag?
Continue with your current approach if it shows promise.
🤖 助手 - 第 16 轮 [14:01:53]
<details>
<summary>💭 思考过程</summary>
Let me assess and continue. I've downloaded and extracted the APK, found it has a native library libhajimi.so. I need to use jadx to decompile the Java layer and IDA to analyze the SO. The jadx command is failing due to PowerShell parsing issues with the -d flag. Let me fix the command.
</details>
Progress: Downloaded APK, extracted it, found libhajimi.so native library and classes.dex. Jadx command failing due to PowerShell parsing. Let me fix that and continue.
🔨 执行命令
$ cmd /c "D:\AI\AICTF\Tools\jadx\bin\jadx.bat" -d "D:\AI\AICTF\workdir\52pojie\chu8\jadx_out" "D:\AI\AICTF\workdir\52pojie\chu8\chu8.apk" --no-res 2>&1
📋 命令输出 [14:01:59]
INFO - loading ...
INFO - processing ...
INFO - progress: 0 of 1394 (0%)
INFO - progress: 242 of 1394 (17%)
INFO - progress: 451 of 1394 (32%)
INFO - progress: 706 of 1394 (50%)
INFO - progress: 936 of 1394 (67%)
INFO - progress: 1262 of 1394 (90%)
ERROR - finished with errors, count: 16
[exit code: exit status 1]
👤 用户 [14:01:59]
[TodoList Reminder - Round 17]
Stats: 4 pending, 0 done, 0 failed.
You have 4 pending items — execute them before adding new ones.
Do NOT repeat failed approaches. If stuck, reset your TodoList with a new strategy.
🤖 助手 - 第 17 轮 [14:02:05]
<details>
<summary>💭 思考过程</summary>
jadx completed with some errors but should have output. Let me find the relevant source files.
</details>
jadx 反编译完成了(有些错误但应该有输出)。让我找到关键的 Java/Kotlin 源码。
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out (模式: *.java)
📋 查找结果 [14:02:05]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\AbstractC0154a.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\a.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\b.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\c.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\d.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\e.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\f.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\g.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\h.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\i.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\C0155a.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\C0156b.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\a.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\b.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\c.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\d.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\e.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\f.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\AbstractC0254a.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\AbstractC0257d.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\AbstractC0258e.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\AbstractC0259f.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\C0256c.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\a.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\b.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\c.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\d.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\e.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\f.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\g.java
🤖 助手 - 第 18 轮 [14:02:10]
🔨 工具调用: strings_search
- case_insensitive:
true
- max_matches:
50
- paths:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources
- pattern:
hajimi|native|loadLibrary|System\.load|check|verify|flag|52pojie
<details>
<summary>📋 strings_search 输出 [14:02:10](较长,点击展开)</summary>
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\a.java:11:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\a.java:57:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\a.java:66:> case NativeBridge.SCORE_PERFECT /* 2 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\b.java:17:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\b.java:74:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\b.java:115:> case NativeBridge.SCORE_PERFECT /* 2 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\a.java:4:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\a.java:35:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\C0\C0261a.java:39:> sb.append(", configFlags=");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\e.java:4:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\e.java:145:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\D0\e.java:26:> return a(i2, 0) ? "Button" : a(i2, 1) ? "Checkbox" : a(i2, 2) ? "Switch" : a(i2, 3) ? "RadioButton" : a(i2, 4) ? "Tab" : a(i2, 5) ? "Image" : a(i2, 6) ? "DropdownList" : "Unknown";
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\D0\n.java:3:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\D0\n.java:70:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\D0\n.java:74:> case NativeBridge.SCORE_PERFECT /* 2 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\C0\d.java:919:> throw new IllegalStateException("Check failed.".toString());
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E\n.java:3:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E\n.java:24:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E\n.java:27:> case NativeBridge.SCORE_PERFECT /* 2 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\g.java:488:> extractedText.flags = (n1.k.a0(str2, '\n', 0, false, 2) >= 0 ? 1 : 0) ^ 1;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E\l.java:57:> throw new IllegalStateException("Check failed.".toString());
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0014a.java:130:> F0.e.a(accessibilityNodeInfo).remove("androidx.view.accessibility.AccessibilityNodeInfoCompat.SPANS_FLAGS_KEY");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0014a.java:174:> fVar.b("androidx.view.accessibility.AccessibilityNodeInfoCompat.SPANS_FLAGS_KEY").add(Integer.valueOf(spanned.getSpanFlags(clickableSpan2)));
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0014a.java:245:> public final void sendAccessibilityEventUnchecked(View view, AccessibilityEvent accessibilityEvent) {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0014a.java:246:> this.f309a.f312a.sendAccessibilityEventUnchecked(view, accessibilityEvent);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0021h.java:17:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0021h.java:144:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0021h.java:147:> case NativeBridge.SCORE_PERFECT /* 2 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0021h.java:315:> if (view.isInEditMode() || view.onCheckIsTextEditor()) {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0021h.java:377:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\b0.java:15:> window.clearFlags(134217728);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\b0.java:16:> window.addFlags(Integer.MIN_VALUE);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\a0.java:23:> window.clearFlags(67108864);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\a0.java:24:> window.addFlags(Integer.MIN_VALUE);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\F0\f.java:151:> ArrayList arrayListB3 = b("androidx.view.accessibility.AccessibilityNodeInfoCompat.SPANS_FLAGS_KEY");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\F0\t.java:4:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\F0\t.java:107:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\F0\t.java:119:> case NativeBridge.SCORE_PERFECT /* 2 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\F0\u.java:5:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\F0\u.java:135:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\F0\u.java:148:> case NativeBridge.SCORE_PERFECT /* 2 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\G0\q.java:33:> f1.h.h("nativeCanvas");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\G0\q.java:43:> f1.h.h("nativeCanvas");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\G0\q.java:54:> f1.h.h("nativeCanvas");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\G0\q.java:64:> f1.h.h("nativeCanvas");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\G0\q.java:74:> f1.h.h("nativeCanvas");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\G0\q.java:85:> f1.h.h("nativeCanvas");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\G0\q.java:96:> f1.h.h("nativeCanvas");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\G0\q.java:108:> f1.h.h("nativeCanvas");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\G0\q.java:120:> f1.h.h("nativeCanvas");
[50 matches in 19/282 file(s)] [truncated at 50 matches]
</details>
🤖 助手 - 第 19 轮 [14:02:16]
关键类是 NativeBridge。让我查看它。
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources (模式: NativeBridge*)
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources (模式: *wuaipojie*)
📋 查找结果 [14:02:16]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java
📋 查找结果 [14:02:16]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2
🤖 助手 - 第 20 轮 [14:02:21]
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2 (模式: *)
📋 查找结果 [14:02:21]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\MainActivity.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\R.java
🤖 助手 - 第 21 轮 [14:02:27]
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\MainActivity.java
📋 文件内容 [14:02:27]
package com.zj.wuaipojie2026_2;
import f1.h;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/* JADX INFO: loaded from: classes.dex */
public final class NativeBridge {
public static final int $stable = 0;
public static final int ERR_CHEAT = -7;
public static final NativeBridge INSTANCE = new NativeBridge();
public static final int SCORE_GOOD = 1;
public static final int SCORE_MISS = 0;
public static final int SCORE_PERFECT = 2;
static {
System.loadLibrary("hajimi");
}
private NativeBridge() {
}
private final native void startSessionBytes(long j2, byte[] bArr, int i2);
public final native int checkRhythm(long j2, int i2, long j3, int i3);
public final native byte[] decryptFrames(byte[] bArr, long j2);
public final native void setDebugBypass(boolean z2);
public final void startSession(long j2, int[] iArr, int i2) {
h.e(iArr, "beatMapMs");
ByteBuffer byteBufferOrder = ByteBuffer.allocate(iArr.length * 4).order(ByteOrder.LITTLE_ENDIAN);
for (int i3 : iArr) {
byteBufferOrder.putInt(i3);
}
byte[] bArrArray = byteBufferOrder.array();
h.d(bArrArray, "array(...)");
startSessionBytes(j2, bArrArray, i2);
}
public final native long updateExp(int i2, int i3, long j2);
public final native byte[] verifyAndDecrypt(byte[] bArr, String str);
}
📋 文件内容 [14:02:27]
package com.zj.wuaipojie2026_2;
import A.e;
import Q0.AbstractC0080d;
import a.AbstractC0154a;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import androidx.activity.A;
import androidx.activity.B;
import androidx.activity.l;
import androidx.activity.m;
import androidx.activity.n;
import androidx.activity.o;
import androidx.activity.p;
import androidx.activity.q;
import androidx.compose.ui.platform.C0204f0;
import androidx.lifecycle.E;
import f1.h;
import h1.a;
/* JADX INFO: loaded from: classes.dex */
public final class MainActivity extends l {
@Override // androidx.activity.l, w0.AbstractActivityC0756a, android.app.Activity
public final void onCreate(Bundle bundle) {
super.onCreate(bundle);
int i2 = m.f1921a;
A a2 = A.f1870j;
B b2 = new B(0, 0, a2);
B b3 = new B(m.f1921a, m.f1922b, a2);
View decorView = getWindow().getDecorView();
h.d(decorView, "window.decorView");
Resources resources = decorView.getResources();
h.d(resources, "view.resources");
boolean zBooleanValue = ((Boolean) a2.d0(resources)).booleanValue();
Resources resources2 = decorView.getResources();
h.d(resources2, "view.resources");
boolean zBooleanValue2 = ((Boolean) a2.d0(resources2)).booleanValue();
int i3 = Build.VERSION.SDK_INT;
q pVar = i3 >= 29 ? new p() : i3 >= 26 ? new o() : new n();
Window window = getWindow();
h.d(window, "window");
pVar.a(b2, b3, window, decorView, zBooleanValue, zBooleanValue2);
e eVar = AbstractC0080d.f1133b;
ViewGroup.LayoutParams layoutParams = AbstractC0154a.f1861a;
View childAt = ((ViewGroup) getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);
C0204f0 c0204f0 = childAt instanceof C0204f0 ? (C0204f0) childAt : null;
if (c0204f0 != null) {
c0204f0.setParentCompositionContext(null);
c0204f0.setContent(eVar);
return;
}
C0204f0 c0204f02 = new C0204f0(this);
c0204f02.setParentCompositionContext(null);
c0204f02.setContent(eVar);
View decorView2 = getWindow().getDecorView();
if (E.c(decorView2) == null) {
E.g(decorView2, this);
}
if (E.d(decorView2) == null) {
decorView2.setTag(R.id.view_tree_view_model_store_owner, this);
}
if (a.A(decorView2) == null) {
a.a0(decorView2, this);
}
setContentView(c0204f02, AbstractC0154a.f1861a);
}
}
🤖 助手 - 第 22 轮 [14:02:37]
很好!NativeBridge 类加载了 libhajimi.so,有几个关键 native 方法:
checkRhythm - 检查节奏
decryptFrames - 解密帧
verifyAndDecrypt - 验证并解密
updateExp - 更新经验值
startSessionBytes - 开始会话
setDebugBypass - 调试绕过
这看起来是一个音乐节奏游戏。让我看看 Compose UI 层怎么调用这些方法的。
🔨 工具调用: strings_search
- case_insensitive:
true
- max_matches:
100
- paths:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources
- pattern:
verifyAndDecrypt|decryptFrames|checkRhythm|updateExp|startSession|setDebugBypass
📋 strings_search 输出 [14:02:37]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:23:> private final native void startSessionBytes(long j2, byte[] bArr, int i2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:25:> public final native int checkRhythm(long j2, int i2, long j3, int i3);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:27:> public final native byte[] decryptFrames(byte[] bArr, long j2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:29:> public final native void setDebugBypass(boolean z2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:31:> public final void startSession(long j2, int[] iArr, int i2) {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:39:> startSessionBytes(j2, bArrArray, i2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:42:> public final native long updateExp(int i2, int i3, long j2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:44:> public final native byte[] verifyAndDecrypt(byte[] bArr, String str);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\r.java:35:> return NativeBridge.INSTANCE.decryptFrames(this.f1186m, this.f1187n.f1107a);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\z.java:39:> return nativeBridge.verifyAndDecrypt(this.f1217m, n1.k.j0((String) this.f1218n.getValue()).toString());
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\v.java:58:> int iCheckRhythm = nativeBridge.checkRhythm(jElapsedRealtimeNanos, i2, j6, i7);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\v.java:59:> long jUpdateExp = nativeBridge.updateExp(iCheckRhythm, i2, j6);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\v.java:60:> if (jUpdateExp < 0 || iCheckRhythm == -7) {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\v.java:64:> Q q2 = iCheckRhythm != 0 ? iCheckRhythm != 1 ? iCheckRhythm != 2 ? Q.None : Q.Perfect : Q.Good : Q.Miss;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\v.java:66:> o2 = new O(jUpdateExp, q2, false);
[15 matches in 4/2191 file(s)]
🤖 助手 - 第 23 轮 [14:02:43]
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\r.java
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\z.java
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\v.java
📋 文件内容 [14:02:43]
package Q0;
import com.zj.wuaipojie2026_2.NativeBridge;
import o1.InterfaceC0603z;
/* JADX INFO: loaded from: classes.dex */
public final class r extends Y0.i implements e1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
public final /* synthetic */ byte[] f1186m;
/* JADX INFO: renamed from: n, reason: collision with root package name */
public final /* synthetic */ O f1187n;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
public r(byte[] bArr, O o2, W0.d dVar) {
super(2, dVar);
this.f1186m = bArr;
this.f1187n = o2;
}
@Override // e1.e
public final Object N(Object obj, Object obj2) {
return ((r) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override // Y0.a
public final W0.d c(W0.d dVar, Object obj) {
return new r(this.f1186m, this.f1187n, dVar);
}
@Override // Y0.a
public final Object g(Object obj) throws Throwable {
R.c.Y(obj);
return NativeBridge.INSTANCE.decryptFrames(this.f1186m, this.f1187n.f1107a);
}
}
📋 文件内容 [14:02:43]
package Q0;
import com.zj.wuaipojie2026_2.NativeBridge;
import java.util.List;
import o1.InterfaceC0603z;
import t.InterfaceC0680W;
/* JADX INFO: loaded from: classes.dex */
public final class z extends Y0.i implements e1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
public final /* synthetic */ byte[] f1217m;
/* JADX INFO: renamed from: n, reason: collision with root package name */
public final /* synthetic */ InterfaceC0680W f1218n;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
public z(byte[] bArr, InterfaceC0680W interfaceC0680W, W0.d dVar) {
super(2, dVar);
this.f1217m = bArr;
this.f1218n = interfaceC0680W;
}
@Override // e1.e
public final Object N(Object obj, Object obj2) {
return ((z) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override // Y0.a
public final W0.d c(W0.d dVar, Object obj) {
return new z(this.f1217m, this.f1218n, dVar);
}
@Override // Y0.a
public final Object g(Object obj) throws Throwable {
R.c.Y(obj);
NativeBridge nativeBridge = NativeBridge.INSTANCE;
List list = N.f1106a;
return nativeBridge.verifyAndDecrypt(this.f1217m, n1.k.j0((String) this.f1218n.getValue()).toString());
}
}
<details>
<summary>📋 文件内容 [14:02:43](较长,点击展开)</summary>
package Q0;
import E0.C0021h;
import android.os.SystemClock;
import com.zj.wuaipojie2026_2.NativeBridge;
import t.C0691d0;
/* JADX INFO: loaded from: classes.dex */
public final /* synthetic */ class v extends f1.f implements e1.a {
/* JADX INFO: renamed from: q, reason: collision with root package name */
public final /* synthetic */ int f1199q;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
public /* synthetic */ v(int i2, Object obj, Class cls, String str, String str2, int i3, int i4) {
super(i2, obj, cls, str, str2, i3);
this.f1199q = i4;
}
@Override // e1.a
public final Object o() {
int i2;
O o2;
switch (this.f1199q) {
case 0:
C0082f c0082f = (C0082f) this.f3686j;
if (!((O) c0082f.f1144f.getValue()).f1109c) {
long jElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
long j2 = (jElapsedRealtimeNanos - c0082f.f1143e) / 1000000;
C0021h c0021h = c0082f.f1141c;
if (j2 < 0) {
c0021h.getClass();
i2 = 0;
} else {
c0021h.getClass();
int i3 = (int) (j2 % ((long) 1000));
int[] iArr = (int[]) c0021h.f318b;
int length = iArr.length;
int i4 = Integer.MAX_VALUE;
int i5 = 0;
for (int i6 = 0; i6 < length; i6++) {
int iAbs = Math.abs(i3 - iArr[i6]);
int iMin = Math.min(iAbs, 1000 - iAbs);
if (iMin < i4) {
i5 = i6;
i4 = iMin;
}
}
i2 = i5;
}
long j3 = (((long) i2) << 32) ^ jElapsedRealtimeNanos;
long j4 = j3 ^ (j3 << 13);
long j5 = j4 ^ (j4 >> 7);
long j6 = j5 ^ (j5 << 17);
C0691d0 c0691d0 = c0082f.f1144f;
int i7 = ((O) c0691d0.getValue()).f1107a >= 50 ? 1 : 0;
NativeBridge nativeBridge = NativeBridge.INSTANCE;
int iCheckRhythm = nativeBridge.checkRhythm(jElapsedRealtimeNanos, i2, j6, i7);
long jUpdateExp = nativeBridge.updateExp(iCheckRhythm, i2, j6);
if (jUpdateExp < 0 || iCheckRhythm == -7) {
o2 = new O(((O) c0691d0.getValue()).f1107a, Q.Cheat, true);
} else {
O o3 = (O) c0691d0.getValue();
Q q2 = iCheckRhythm != 0 ? iCheckRhythm != 1 ? iCheckRhythm != 2 ? Q.None : Q.Perfect : Q.Good : Q.Miss;
o3.getClass();
o2 = new O(jUpdateExp, q2, false);
}
c0691d0.setValue(o2);
}
break;
case NativeBridge.SCORE_GOOD /* 1 */:
((androidx.activity.x) this.f3686j).d();
break;
default:
((androidx.activity.x) this.f3686j).d();
break;
}
return S0.k.f1286a;
}
}
</details>
🤖 助手 - 第 24 轮 [14:02:49]
🔨 工具调用: todolist
- action:
update
- id:
1
- status:
done
- result:
找到NativeBridge类,加载libhajimi.so,关键native方法:checkRhythm, updateExp, decryptFrames, verifyAndDecrypt, startSessionBytes, setDebugBypass。这是音乐节奏游戏,需要分析SO中的验证逻辑。
📋 todolist 输出 [14:02:49]
Updated task #1. Current TodoList:
✅ 1. 用jadx反编译APK,分析Java/Kotlin层逻辑,找到验证入口和native方法调用 → 找到NativeBridge类,加载libhajimi.so,关键native方法:checkRhythm, updateExp, decryptFrames, verifyAndDecrypt, startSessionBytes, setDebugBypass。这是音乐节奏游戏,需要分析SO中的验证逻辑。
⬜ 2. 用IDA分析libhajimi.so (x86_64),找到JNI函数和核心验证逻辑
⬜ 3. 分析加密/验证算法,提取密钥和常量
⬜ 4. 编写脚本逆向算法,恢复flag
⬜ 5. 提交flag
👤 用户 [14:02:49]
[TodoList Reminder - Round 25]
Stats: 4 pending, 1 done, 0 failed.
You have 4 pending items — execute them before adding new ones.
Do NOT repeat failed approaches. If stuck, reset your TodoList with a new strategy.
🤖 助手 - 第 25 轮 [14:02:56]
<details>
<summary>💭 思考过程</summary>
Let me now analyze the SO file with IDA. I'll use the x86_64 version since it's easier to analyze.
</details>
现在用 IDA 分析 SO 文件。这是核心逆向工作,让我委派给 reverse 子 agent。
🔨 工具调用: todolist
- action:
update
- id:
2
- status:
in_progress
📋 todolist 输出 [14:02:56]
Updated task #2. Current TodoList:
✅ 1. 用jadx反编译APK,分析Java/Kotlin层逻辑,找到验证入口和native方法调用 → 找到NativeBridge类,加载libhajimi.so,关键native方法:checkRhythm, updateExp, decryptFrames, verifyAndDecrypt, startSessionBytes, setDebugBypass。这是音乐节奏游戏,需要分析SO中的验证逻辑。
🔄 2. 用IDA分析libhajimi.so (x86_64),找到JNI函数和核心验证逻辑
⬜ 3. 分析加密/验证算法,提取密钥和常量
⬜ 4. 编写脚本逆向算法,恢复flag
⬜ 5. 提交flag
🤖 助手 - 第 26 轮 [14:03:02]
Let me also look for encrypted data in the APK (assets, raw resources) that might be the flag ciphertext, while I start IDA analysis.
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped (模式: *)
<details>
<summary>📋 查找结果 [14:03:02](较长,点击展开)</summary>
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\AndroidManifest.xml
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\DebugProbesKt.bin
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.activity_activity-compose.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.activity_activity-ktx.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.activity_activity.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.annotation_annotation-experimental.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.arch.core_core-runtime.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.autofill_autofill.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.animation_animation-core.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.animation_animation.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.foundation_foundation-layout.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.foundation_foundation.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.material3_material3.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.material_material-icons-core.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.material_material-ripple.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.runtime_runtime-saveable.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.runtime_runtime.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.ui_ui-geometry.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.ui_ui-graphics.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.ui_ui-text.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.ui_ui-tooling-preview.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.ui_ui-unit.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.ui_ui-util.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.compose.ui_ui.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.core_core-ktx.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.core_core.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.customview_customview-poolingcontainer.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.emoji2_emoji2.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.interpolator_interpolator.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.lifecycle_lifecycle-livedata-core.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.lifecycle_lifecycle-process.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.lifecycle_lifecycle-runtime-ktx.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.lifecycle_lifecycle-runtime.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.lifecycle_lifecycle-viewmodel-ktx.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.lifecycle_lifecycle-viewmodel-savedstate.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.lifecycle_lifecycle-viewmodel.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.profileinstaller_profileinstaller.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.savedstate_savedstate-ktx.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.savedstate_savedstate.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.startup_startup-runtime.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.tracing_tracing.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.versionedparcelable_versionedparcelable.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\com
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\com\android
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\com\android\build
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\com\android\build\gradle
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\com\android\build\gradle\app-metadata.properties
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\kotlinx_coroutines_android.version
</details>
🤖 助手 - 第 27 轮 [14:03:08]
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped (模式: *.bin)
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped (模式: *.dat)
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped (模式: *.enc)
📋 查找结果 [14:03:08]
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\DebugProbesKt.bin
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin
📋 查找结果 [14:03:08]
No files found.
📋 查找结果 [14:03:08]
No files found.
🤖 助手 - 第 28 轮 [14:03:15]
assets/hjm_pack.bin 看起来很关键!让我检查它。
🔨 执行命令
$ file "D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin"
🐍 执行 Python 脚本
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin", "rb") as f:
data = f.read()
print(f"Size: {len(data)} bytes")
print(f"First 64 bytes hex: {data[:64].hex()}")
print(f"First 64 bytes: {data[:64]}")
print(f"Last 32 bytes hex: {data[-32:].hex()}")
📋 命令输出 [14:03:16]
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin: data
📋 Python 输出 [14:03:16]
Size: 564 bytes
First 64 bytes hex: 484a4d3102000000010000004000000040000000780000000f1e2d3c4b5a69788796a5b4c3d2e1f0112233445566778899aabbcc9641cd80feda2af5fb95dd2c
First 64 bytes: b'HJM1\x02\x00\x00\x00\x01\x00\x00\x00@\x00\x00\x00@\x00\x00\x00x\x00\x00\x00\x0f\x1e-<KZix\x87\x96\xa5\xb4\xc3\xd2\xe1\xf0\x11"3DUfw\x88\x99\xaa\xbb\xcc\x96A\xcd\x80\xfe\xda*\xf5\xfb\x95\xdd,'
Last 32 bytes hex: d4c4d122d0f243d6770260c018b66ed6db8a1b903acbaa215ca816690497ccc1
🤖 助手 - 第 29 轮 [14:03:21]
HJM1 是自定义格式头。现在让我开 IDA 分析 SO 文件,这是核心。
🔨 工具调用: mcp_ida-pro-headless_idalib_open
- input_path:
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so
📋 mcp_ida-pro-headless_idalib_open 输出 [14:03:23]
{
"success": true,
"session": {
"session_id": "2769f686",
"input_path": "D:\\AI\\AICTF\\workdir\\52pojie\\chu8\\apk_unzipped\\lib\\x86_64\\libhajimi.so",
"filename": "libhajimi.so",
"created_at": "2026-02-24T14:03:23.380456",
"last_accessed": "2026-02-24T14:03:23.380456",
"is_analyzing": false,
"metadata": {}
},
"message": "Binary opened successfully: libhajimi.so"
}
🤖 助手 - 第 30 轮 [14:03:28]
🔨 工具调用: mcp_ida-pro-headless_list_funcs
- queries:
{"filter": "Java_*", "count": 50}
📋 mcp_ida-pro-headless_list_funcs 输出 [14:03:28]
[
{
"data": [],
"next_offset": null
}
]
👤 用户 [14:03:28]
[Progress Checkpoint - Round 31/200]
Briefly assess your progress:
- What have you accomplished so far?
- Are you making progress toward the flag?
Continue with your current approach if it shows promise.
🤖 助手 - 第 31 轮 [14:03:36]
<details>
<summary>💭 思考过程</summary>
No JNI functions with standard naming. The SO likely uses dynamic registration. Let me look at JNI_OnLoad and exported functions.
</details>
No standard JNI naming — likely uses dynamic registration. Let me find JNI_OnLoad and exports.
🔨 工具调用: mcp_ida-pro-headless_list_funcs
- queries:
{"filter": "*JNI*", "count": 50}
🔨 工具调用: mcp_ida-pro-headless_list_funcs
- queries:
{"filter": "*OnLoad*", "count": 50}
📋 mcp_ida-pro-headless_list_funcs 输出 [14:03:36]
[
{
"data": [
{
"addr": "0x237c0",
"name": "JNI_OnLoad",
"size": "0x8e"
}
],
"next_offset": null
}
]
📋 mcp_ida-pro-headless_list_funcs 输出 [14:03:36]
[
{
"data": [
{
"addr": "0x237c0",
"name": "JNI_OnLoad",
"size": "0x8e"
}
],
"next_offset": null
}
]
🤖 助手 - 第 32 轮 [14:03:41]
🔨 工具调用: mcp_ida-pro-headless_decompile
📋 mcp_ida-pro-headless_decompile 输出 [14:03:41]
{
"addr": "0x237c0",
"code": "__int64 __fastcall JNI_OnLoad(__int64 a1)\n{\n unsigned int v1; // ebx\n __int64 v2; // rax\n _QWORD v4[3]; // [rsp+0h] [rbp-18h] BYREF\n\n v4[1] = __readfsqword(0x28u); /*0x237ce*/\n v4[0] = 0; /*0x237d3*/\n v1 = -1; /*0x237e9*/\n if ( !(*(unsigned int (__fastcall **)(__int64, _QWORD *, __int64))(*(_QWORD *)a1 + 48LL))(a1, v4, 65542) )\n {\n v2 = (*(__int64 (__fastcall **)(_QWORD, const char *))(*(_QWORD *)v4[0] + 48LL))( /*0x23800*/\n v4[0],\n \"com/zj/wuaipojie2026_2/NativeBridge\");\n if ( v2 )\n return (*(unsigned int (__fastcall **)(_QWORD, __int64, char **, __int64))(*(_QWORD *)v4[0] + 1720LL))(\n v4[0],\n v2,\n off_5CCA0,\n 6) != 0\n ? -1\n : 0x10006;\n }\n return v1; /*0x23843*/\n}"
}
👤 用户 [14:03:41]
[TodoList Reminder - Round 33]
Stats: 3 pending, 1 done, 0 failed.
You have 3 pending items — execute them before adding new ones.
Do NOT repeat failed approaches. If stuck, reset your TodoList with a new strategy.
🤖 助手 - 第 33 轮 [14:03:47]
动态注册表在 off_5CCA0,注册了 6 个方法。让我读取这个表。
🔨 工具调用: mcp_ida-pro-headless_get_bytes
- regions:
map[addr:0x5CCA0 size:192]
📋 mcp_ida-pro-headless_get_bytes 输出 [14:03:47]
[
{
"addr": "0x5CCA0",
"data": "0x81 0x67 0x1 0x0 0x0 0x0 0x0 0x0 0xda 0x58 0x1 0x0 0x0 0x0 0x0 0x0 0xa0 0x38 0x2 0x0 0x0 0x0 0x0 0x0 0xb1 0x5c 0x1 0x0 0x0 0x0 0x0 0x0 0x3e 0x4e 0x1 0x0 0x0 0x0 0x0 0x0 0x50 0x3e 0x2 0x0 0x0 0x0 0x0 0x0 0xca 0x56 0x1 0x0 0x0 0x0 0x0 0x0 0x20 0x4d 0x1 0x0 0x0 0x0 0x0 0x0 0x60 0x3f 0x2 0x0 0x0 0x0 0x0 0x0 0xd4 0x6d 0x1 0x0 0x0 0x0 0x0 0x0 0x4 0x5e 0x1 0x0 0x0 0x0 0x0 0x0 0x20 0x45 0x2 0x0 0x0 0x0 0x0 0x0 0x9a 0x55 0x1 0x0 0x0 0x0 0x0 0x0 0xab 0x55 0x1 0x0 0x0 0x0 0x0 0x0 0x50 0x48 0x2 0x0 0x0 0x0 0x0 0x0 0xc7 0x54 0x1 0x0 0x0 0x0 0x0 0x0 0xe2 0x6d 0x1 0x0 0x0 0x0 0x0 0x0 0xa0 0x4c 0x2 0x0 0x0 0x0 0x0 0x0 0x0 0x11 0x5 0x0 0x0 0x0 0x0 0x0 0x90 0x92 0x3 0x0 0x0 0x0 0x0 0x0 0xc0 0x93 0x3 0x0 0x0 0x0 0x0 0x0 0xae 0x67 0x1 0x0 0x0 0x0 0x0 0x0 0x10 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x8 0x0 0x0 0x0 0x0 0x0 0x0 0x0"
}
]
🤖 助手 - 第 34 轮 [14:03:58]
🐍 执行 Python 脚本
import struct
data = bytes([0x81,0x67,0x1,0x0,0x0,0x0,0x0,0x0, 0xda,0x58,0x1,0x0,0x0,0x0,0x0,0x0, 0xa0,0x38,0x2,0x0,0x0,0x0,0x0,0x0,
0xb1,0x5c,0x1,0x0,0x0,0x0,0x0,0x0, 0x3e,0x4e,0x1,0x0,0x0,0x0,0x0,0x0, 0x50,0x3e,0x2,0x0,0x0,0x0,0x0,0x0,
0xca,0x56,0x1,0x0,0x0,0x0,0x0,0x0, 0x20,0x4d,0x1,0x0,0x0,0x0,0x0,0x0, 0x60,0x3f,0x2,0x0,0x0,0x0,0x0,0x0,
0xd4,0x6d,0x1,0x0,0x0,0x0,0x0,0x0, 0x04,0x5e,0x1,0x0,0x0,0x0,0x0,0x0, 0x20,0x45,0x2,0x0,0x0,0x0,0x0,0x0,
0x9a,0x55,0x1,0x0,0x0,0x0,0x0,0x0, 0xab,0x55,0x1,0x0,0x0,0x0,0x0,0x0, 0x50,0x48,0x2,0x0,0x0,0x0,0x0,0x0,
0xc7,0x54,0x1,0x0,0x0,0x0,0x0,0x0, 0xe2,0x6d,0x1,0x0,0x0,0x0,0x0,0x0, 0xa0,0x4c,0x2,0x0,0x0,0x0,0x0,0x0])
# JNINativeMethod: { const char* name, const char* signature, void* fnPtr } - 3 pointers each
for i in range(6):
off = i * 24
name_ptr = struct.unpack_from('<Q', data, off)[0]
sig_ptr = struct.unpack_from('<Q', data, off+8)[0]
fn_ptr = struct.unpack_from('<Q', data, off+16)[0]
print(f"Method {i}: name=0x{name_ptr:x}, sig=0x{sig_ptr:x}, fn=0x{fn_ptr:x}")
📋 Python 输出 [14:03:58]
Method 0: name=0x16781, sig=0x158da, fn=0x238a0
Method 1: name=0x15cb1, sig=0x14e3e, fn=0x23e50
Method 2: name=0x156ca, sig=0x14d20, fn=0x23f60
Method 3: name=0x16dd4, sig=0x15e04, fn=0x24520
Method 4: name=0x1559a, sig=0x155ab, fn=0x24850
Method 5: name=0x154c7, sig=0x16de2, fn=0x24ca0
🤖 助手 - 第 35 轮 [14:04:04]
🔨 工具调用: mcp_ida-pro-headless_get_string
- addrs:
["0x16781", "0x158da", "0x15cb1", "0x14e3e", "0x156ca", "0x14d20", "0x16dd4", "0x15e04", "0x1559a", "0x155ab", "0x154c7", "0x16de2"]
📋 mcp_ida-pro-headless_get_string 输出 [14:04:04]
[
{
"addr": "0x16781",
"value": "startSessionBytes"
},
{
"addr": "0x158da",
"value": "(J[BI)V"
},
{
"addr": "0x15cb1",
"value": "checkRhythm"
},
{
"addr": "0x14e3e",
"value": "(JIJI)I"
},
{
"addr": "0x156ca",
"value": "updateExp"
},
{
"addr": "0x14d20",
"value": "(IIJ)J"
},
{
"addr": "0x16dd4",
"value": "decryptFrames"
},
{
"addr": "0x15e04",
"value": "([BJ)[B"
},
{
"addr": "0x1559a",
"value": "verifyAndDecrypt"
},
{
"addr": "0x155ab",
"value": "([BLjava/lang/String;)[B"
},
{
"addr": "0x154c7",
"value": "setDebugBypass"
},
{
"addr": "0x16de2",
"value": "(Z)V"
}
]
🤖 助手 - 第 36 轮 [14:04:11]
现在我有了完整的 JNI 方法映射。让我反编译所有关键函数:
🔨 工具调用: mcp_ida-pro-headless_rename
- batch:
map[func:[map[addr:0x238a0 name:startSessionBytes] map[addr:0x23e50 name:checkRhythm] map[addr:0x23f60 name:updateExp] map[addr:0x24520 name:decryptFrames] map[addr:0x24850 name:verifyAndDecrypt] map[addr:0x24ca0 name:setDebugBypass]]]
📋 mcp_ida-pro-headless_rename 输出 [14:04:11]
{
"func": [
{
"addr": "0x238a0",
"name": "startSessionBytes",
"ok": true,
"error": null,
"dir": "vibe",
"dir_error": null
},
{
"addr": "0x23e50",
"name": "checkRhythm",
"ok": true,
"error": null,
"dir": "vibe",
"dir_error": null
},
{
"addr": "0x23f60",
"name": "updateExp",
"ok": true,
"error": null,
"dir": "vibe",
"dir_error": null
},
{
"addr": "0x24520",
"name": "decryptFrames",
"ok": true,
"error": null,
"dir": "vibe",
"dir_error": null
},
{
"addr": "0x24850",
"name": "verifyAndDecrypt",
"ok": true,
"error": null,
"dir": "vibe",
"dir_error": null
},
{
"addr": "0x24ca0",
"name": "setDebugBypass",
"ok": true,
"error": null,
"dir": "vibe",
"dir_error": null
}
]
}
👤 用户 []
[Context compacted — previous conversation summary follows]
→ Tool call: mcp_ida-pro-headless_decompile({"addr": "0x24850"})
"addr": "0x24850",
"code": "int64 fastcall verifyAndDecrypt(int64 a1, int64 a2, int64 a3, int64 a4)\n{\n int64 v4; // rax\n int64 v5; // rax\n int64 v6; // rax\n int64 v7; // rax\n int64 v8; // rax\n int64 v9; // rax\n int64 v10; // rax\n int64 v11; // rax\n int64 v12; // rax\n int64 v13; // rax\n int64 v14; // rax\n int64 v15; // rax\n int64 v16; // rax\n int64 v17; // rax\n int64 v18; // rax\n int64 v19; // rax\n int64 v20; // rax\n int64 v21; // rax\n int64 v22; // rax\n int64 v23; // rax\n int64 v24; // rax\n int64 v25; // rax\n int64 v26; // rax\n int64 v27; // rax\n int64 v28; // rax\n int64 v29; // rax\n int64 v30; // rax\n int64 v31; // rax\n int64 v32; // rax\n int64 v33; // rax\n int64 v34; // rax\n int64 v35; // rax\n int64 v36; // rax\n int64 v37; // rax\n int64 v38; // rax\n int64 v39; // rax\n int64 v40; // rax\n int64 v41; // rax\n int64 v42; // rax\n int64 v43; // rax\n int64 v44; // rax\n int64 v45; // rax\n int64 v46; // rax\n int64 v47; // rax\n int64 v48; // rax\n int64 v49; // rax\n int64 v50; // rax\n int64 v51; // rax\n int64 v52; // rax\n int64 v53; // rax\n int64 v54; // rax\n int64 v55; // rax\n int64 v56; // rax\n int64 v57; // rax\n int64 v58; // rax\n int64 v59; // rax\n int64 v60; // rax\n int64 v61; // rax\n int64 v62; // rax\n int64 v63; // rax\n int64 v64; // rax\n int64 v65; // rax\n int64 v66; // rax\n int64 v67; // rax\n int64 v68; // rax\n int64 v69; // rax\n int64 v70; // rax\n int64 v71; // rax\n int64 v72; // rax\n int64 v73; // rax\n int64 v74; // rax\n int64 v75; // rax\n int64 v76; // rax\n int64 v77; // rax\n int64 v78; // rax\n int64 v79; // rax\n int64 v80; // rax\n int64 v81; // rax\n int64 v82; // rax\n int64 v83; // rax\n int64 v84; // rax\n int64 v85; // rax\n int64 v86; // rax\n int64 v87; // rax\n int64 v88; // rax\n int64 v89; // rax\n int64 v90; // rax\n int64 v91; // rax\n int64 v92; // rax\n int64 v93; // rax\n int64 v94; // rax\n int64 v95; // rax\n int64 v96; // rax\n int64 v97; // rax\n int64 v98; // rax\n int64 v99; // rax\n int64 v100; // rax\n int64 v101; // rax\n int64 v102; // rax\n int64 v103; // rax\n int64 v104; // rax\n int64 v105; // rax\n int64 v106; // rax\n int64 v107; // rax\n int64 v108; // rax\n int64 v109; // rax\n int64 v110; // rax\n int64 v111; // rax\n int64 v112; // rax\n int64 v113; // rax\n int64 v114; // rax\n int64 v115; // rax\n int64 v116; // rax\n int64 v117; // rax\n int64 v118; // rax\n int64 v119; // rax\n int64 v120; // rax\n int64 v121; // rax\n int64 v122; // rax\n int64 v123; // rax\n int64 v124; // rax\n int64 v125; // rax\n int64 v126; // rax\n int64 v127; // rax\n int64 v128; // rax\n int64 v129; // rax\n int64 v130; // rax\n int64 v131; // rax\n int64 v132; // rax\n int64 v133; // rax\n int64 v134; // rax\n int64 v135; // rax\n int64 v136; // rax\n int64 v137; // rax\n int64 v138; // rax\n int64 v139; // rax\n int64 v140; // rax\n int64 v141; // rax\n int64 v141; // rax\n int64 v142; // rax\n int64 v143; // rax\n int64 v144; // rax\n int64 v145; // rax\n int64 v146; // rax\n int64 v147; // rax\n int64 v148; // rax\n int64 v149; // rax\n int64 v150; // rax\n int64 v151; // rax\n int64 v152; // rax\n int64 v153; // rax\n int64 v154; // rax\n int64 v155; // rax\n int64 v156; // rax\n int64 v157; // rax\n int64 v158; // rax\n int64 v159; // rax\n int64 v160; // rax\n int64 v161; // rax\n int64 v162; // rax\n int64 v163; // rax\n int64 v164; // rax\n int64 v165; // rax\n int64 v166; // rax\n int64 v167; // rax\n int64 v168; // rax\n int64 v169; // rax\n int64 v170; // rax\n int64 v171; // rax\n int64 v172; // rax\n int64 v173; // rax\n int64 v174; // rax\n int64 v175; // rax\n int64 v176; // rax\n int64 v176; // rax\n int64 v177; // rax\n int64 v178; // rax\n int64 v179; // rax\n int64 v180; // rax\n int64 v181; // rax\n int64 v182; // rax\n int64 v183; // rax\n int64 v184; // rax\n int64 v185; // rax\n int64 v186; // rax\n int64 v187; // rax\n int64 v188; // rax\n int64 v189; // rax\n int64 v190; // rax\n int64 v191; // rax\n int64 v192; // rax\n int64 v193; // rax\n int64 v194; // rax\n int64 v195; // rax\n int64 v196; // rax\n int64 v197; // rax\n int64 v198; // rax\n int64 v199; // rax\n int64 v200; // rax\n int64 v201; // rax\n int64 v202; // rax\n int64 v203; // rax\n int64 v204; // rax\n int64 v205; // rax\n int64 v206; // rax\n int64 v207; // rax\n int64 v208; // rax\n int64 v209; // rax\n int64 v210; // rax\n int64 v211; // rax\n int64 v212; // rax\n int64 v213; // rax\n int64 v214; // rax\n int64 v215; // rax\n int64 v216; // rax\n int64 v217; // rax\n int64 v218; // rax\n int64 v219; // rax\n int64 v220; // rax\n int64 v221; // rax\n int64 v222; // rax\n int64 v223; // rax\n int64 v224; // rax\n int64 v225; // rax\n int64 v226; // rax\n int64 v227; // rax\n int64 v228; // rax\n int64 v229; // rax\n int64 v230; // rax\n int64 v231; // rax\n int64 v232; // rax\n int64 v233; // rax\n int64 v234; // rax\n int64 v235; // rax\n int64 v236; // rax\n int64 v237; // rax\n int64 v238; // rax\n int64 v239; // rax\n int64 v240; // rax\n int64 v241; // rax\n int64 v242; // rax\n int64 v243; // rax\n int64 v244; // rax\n int64 v245; // rax\n int64 v246; // rax\n int64 v247; // rax\n int64 v248; // rax\n int64 v249; // rax\n int64 v250; // rax\n int64 v251; // rax\n int64 v252; // rax\n int64 v253; // rax\n int64 v254; // rax\n int64 v255; // rax\n int64 v256; // rax\n int64 v257; // rax\n int64 v258; // rax\n int64 v259; // rax\n int64 v260; // rax\n int64 v261; // rax\n int64 v262; // rax\n int64 v263; // rax\n int64 v264; // rax\n int64 v265; // rax\n int64 v266; // rax\n int64 v267; // rax\n int64 v268; // rax\n int64 v269; // rax\n int64 v270; // rax\n int64 v271; // rax\n int64 v272; // rax\n int64 v273; // rax\n int64 v274; // rax\n int64 v275; // rax\n int64 v276; // rax\n int64 v276; // rax\n int64 v277; // rax\n int64 v278; // rax\n int64 v279; // rax\n int64 v280; // rax\n int64 v281; // rax\n int64 v282; // rax\n int64 v283; // rax\n int64 v284; // rax\n int64 v285; // rax\n int64 v286; // rax\n int64 v287; // rax\n int64 v288; // rax\n int64 v289; // rax\n int64 v290; // rax\n int64 v291; // rax\n int64 v292; // rax\n int64 v293; // rax\n int64 v294; // rax\n int64 v295; // rax\n int64 v296; // rax\n int64 v297; // rax\n int64 v298; // rax\n int64 v299; // rax\n int64 v300; // rax\n int64 v301; // rax\n int64 v302; // rax\n int64 v303; // rax\n int64 v304; // rax\n int64 v305; // rax\n int64 v306; // rax\n int64 v307; // rax\n int64 v308; // rax\n int64 v309; // rax\n int64 v310; // rax\n int64 v311; // rax\n int64 v312; // rax\n int64 v313; // rax\n int64 v314; // rax\n int64 v315; // rax\n int64 v316; // rax\n int64 v317; // rax\n int64 v318; // rax\n int64 v319; // rax\n int64 v320; // rax\n int64 v321; // rax\n int64 v322; // rax\n int64 v323; // rax\n int64 v324; // rax\n int64 v325; // rax\n int64 v326; // rax\n int64 v327; // rax\n int64 v328; // rax\n int64 v329; // rax\n int64 v330; // rax\n int64 v331; // rax\n int64 v332; // rax\n int64 v333; // rax\n int64 v334; // rax\n int64 v335; // rax\n int64 v336; // rax\n int64 v337; // rax\n int64 v338; // rax\n int64 v339; // rax\n int64 v340; // rax\n int64 v341; // rax\n int64 v342; // rax\n int64 v343; // rax\n int64 v344; // rax\n int64 v345; // rax\n int64 v346; // rax\n int64 v347; // rax\n int64 v348; // rax\n int64 v349; // rax\n int64 v350; // rax\n int64 v351; // rax\n int64 v352; // rax\n int64 v353; // rax\n int64 v354; // rax\n int64 v355; // rax\n int64 v356; // rax\n int64 v357; // rax\n int64 v358; // rax\n int64 v359; // rax\n int64 v360; // rax\n int64 v361; // rax\n int64 v362; // rax\n int64 v363; // rax\n int64 v364; // rax\n int64 v365; // rax\n int64 v366; // rax\n int64 v367; // rax\n int64 v368; // rax\n int64 v369; // rax\n int64 v370; // rax\n int64 v371; // rax\n int64 v372; // rax\n int64 v373; // rax\n int64 v374; // rax\n int64 v375; // rax\n int64 v376; // rax\n int64 v377; // rax\n int64 v378; // rax\n int64 v379; // rax\n int64 v380; // rax\n int64 v381; // rax\n int64 v382; // rax\n int64 v383; // rax\n int64 v384; // rax\n int64 v385; // rax\n int64 v386; // rax\n int64 v387; // rax\n int64 v388; // rax\n int64 v389; // rax\n int64 v390; // rax\n int64 v391; // rax\n int64 v392; // rax\n int64 v393; // rax\n int64 v394; // rax\n int64 v395; // rax\n int64 v396; // rax\n int64 v397; // rax\n int64 v398; // rax\n int64 v399; // rax\n int64 v400; // rax\n int64 v401; // rax\n int64 v402; // rax\n int64 v403; // rax\n int64 v404; // rax\n int64 v405; // rax\n int64 v406; // rax\n int64 v407; // rax\n int64 v408; // rax\n int64 v409; // rax\n int64 v410; // rax\n int64 v411; // rax\n int64 v412; // rax\n int64 v413; // rax\n int64 v414; // rax\n int64 v415; // rax\n int64 v416; // rax\n int64 v417; // rax\n int64 v418; // rax\n int64 v419; // rax\n int64 v420; // rax\n int64 v421; // rax\n int64 v422; // rax\n int64 v423; // rax\n int64 v424; // rax\n int64 v425; // rax\n int64 v426; // rax\n int64 v427; // rax\n int64 v428; // rax\n int64 v429; // rax\n int64 v430; // rax\n int64 v431; // rax\n int64 v432; // rax\n int64 v433; // rax\n int64 v434; // rax\n int64 v435; // rax\n int64 v436; // rax\n int64 v437; // rax\n int64 v438; // rax\n int64 v439; // rax\n int64 v440; // rax\n int64 v441; // rax\n int64 v442; // rax\n int64 v443; // rax\n int64 v444; // rax\n int64 v445; // rax\n int64 v446; // rax\n int64 v447; // rax\n int64 v448; // rax\n int64 v449; // rax\n int64 v450; // rax\n int64 v451; // rax\n int64 v452; // rax\n int64 v453; // rax\n int64 v454; // rax\n int64 v455; // rax\n int64 v456; // rax\n int64 v457; // rax\n int64 v458; // rax\n int64 v459; // rax\n int64 v460; // rax\n int64 v461; // rax\n int64 v462; // rax\n int64 v463; // rax\n int64 v464; // rax\n int64 v465; // rax\n int64 v466; // rax\n int64 v467; // rax\n int64 v468; // rax\n int64 v469; // rax\n int64 v470; // rax\n int64 v471; // rax\n int64 v472; // rax\n int64 v473; // rax\n int64 v474; // rax\n int64 v475; // rax\n int64 v476; // rax\n int64 v477; // rax\n int64 v478; // rax\n int64 v479; // rax\n int64 v480; // rax\n int64 v481; // rax\n int64 v482; // rax\n int64 v483; // rax\n int64 v484; // rax\n int64 v485; // rax\n int64 v486; // rax\n int64 v487; // rax\n int64 v488; // rax\n int64 v489; // rax\n int64 v490; // rax\n int64 v491; // rax\n int64 v492; // rax\n int64 v493; // rax\n int64 v494; // rax\n int64 v495; // rax\n int64 v496; // rax\n int64 v497; // rax\n int64 v498; // rax\n int64 v499; // rax\n int64 v500; // rax\n int64 v501; // rax\n int64 v502; // rax\n int64 v503; // rax\n int64 v504; // rax\n int64 v505; // rax\n int64 v506; // rax\n int64 v507; // rax\n int64 v508; // rax\n int64 v509; // rax\n int64 v510; // rax\n int64 v511; // rax\n int64 v512; // rax\n int64 v513; // rax\n int64 v514; // rax\n int64 v515; // rax\n int64 v516; // rax\n int64 v517; // rax\n int64 v518; // rax\n int64 v519; // rax\n int64 v520; // rax\n int64 v521; // rax\n int64 v522; // rax\n int64 v523; // rax\n int64 v524; // rax\n int64 v525; // rax\n int64 v526; // rax\n int64 v527; // rax\n int64 v528; // rax\n int64 v529; // rax\n int64 v530; // rax\n int64 v531; // rax\n int64 v532; // rax\n int64 v533; // rax\n int64 v534; // rax\n int64 v535; // rax\n int64 v536; // rax\n int64 v537; // rax\n int64 v538; // rax\n int64 v539; // rax\n int64 v540; // rax\n int64 v541; // rax\n int64 v542; // rax\n int64 v543; // rax\n int64 v544; // rax\n int64 v545; // rax\n int64 v546; // rax\n int64 v547; // rax\n int64 v548; // rax\n int64 v549; // rax\n int64 v550; // rax\n int64 v551; // rax\n int64 v552; // rax\n int64 v553; // rax\n int64 v554; // rax\n int64 v555; // rax\n int64 v556; // rax\n int64 v557; // rax\n int64 v558; // rax\n int64 v559; // rax\n int64 v560; // rax\n int64 v561; // rax\n int64 v562; // rax\n int64 v563; // rax\n int64 v564; // rax\n int64 v565; // rax\n int64 v566; // rax\n int64 v567; // rax\n int64 v568; // rax\n int64 v569; // rax\n int64 v570; // rax\n int64 v571; // rax\n int64 v572; // rax\n int64 v573; // rax\n int64 v574; // rax\n int64 v575; // rax\n int64 v576; // rax\n int64 v577; // rax\n int64 v578; // rax\n int64 v579; // rax\n int64 v580; // rax\n int64 v581; // rax\n int64 v582; // rax\n int64 v583; // rax\n int64 v584; // rax\n int64 v585; // rax\n int64 v586; // rax\n int64 v587; // rax\n int64 v588; // rax\n int64 v589; // rax\n int64 v590; // rax\n int64 v591; // rax\n int64 v592; // rax\n int64 v593; // rax\n int64 v594; // rax\n int64 v595; // rax\n int64 v596; // rax\n int64 v597; // rax\n int64 v598; // rax\n int64 v599; // rax\n int64 v600; // rax\n int64 v601; // rax\n int64 v602; // rax\n int64 v603; // rax\n int64 v604; // rax\n int64 v605; // rax\n int64 v606; // rax\n int64 v607; // rax\n int64 v608; // rax\n int64 v609; // rax\n int64 v610; // rax\n int64 v611; // rax\n int64 v612; // rax\n int64 v613; // rax\n int64 v614; // rax\n int64 v615; // rax\n int64 v616; // rax\n int64 v617; // rax\n int64 v618; // rax\n int64 v619; // rax\n int64 v620; // rax\n int64 v621; // rax\n int64 v622; // r
🤖 助手 - 第 37 轮 [14:04:17]
🔨 工具调用: mcp_ida-pro-headless_decompile
🔨 工具调用: mcp_ida-pro-headless_decompile
📋 mcp_ida-pro-headless_decompile 输出 [14:04:17]
{
"addr": "0x24850",
"code": "__int64 __fastcall verifyAndDecrypt(__int64 *a1, __int64 a2, __int64 a3, __int64 a4)\n{\n __int64 v5; // rax\n int v8; // eax\n unsigned int v9; // ebp\n unsigned __int64 v10; // rbx\n char *v11; // r12\n size_t v12; // rdx\n char *v13; // rbx\n __int64 v14; // rax\n __int64 v15; // r15\n __int64 v17; // rax\n __int64 v18; // rcx\n __int64 v19; // rsi\n __int64 v20; // rdx\n __int64 v21; // rax\n unsigned __int64 v22; // rcx\n char v23; // r13\n __int128 v24; // rt0\n int v25; // r12d\n int v26; // eax\n __int64 v27; // rdi\n __int64 v28;
... [truncated 7497 bytes] ...
a1, 0, v29, v35); /*0x24bcf*/\nLABEL_7:\n v15 = v14; /*0x24903*/\nLABEL_8:\n operator delete(v13); /*0x24906*/\n return v15; /*0x24933*/\n }\n }\n }\n }\n v14 = (*(__int64 (__fastcall **)(__int64 *, _QWORD, __int64))(*a1 + 1408))(a1, 0, v20); /*0x24bbc*/\n goto LABEL_7; /*0x24bc2*/\n }\n v5 = *a1; /*0x24934*/\n }\n return (*(__int64 (__fastcall **)(__int64 *, _QWORD, __int64, unsigned __int64))(v5 + 1408))( /*0x24925*/\n a1,\n 0,\n a3,\n __readfsqword(0x28u));\n}"
}
📋 mcp_ida-pro-headless_decompile 输出 [14:04:17]
{
"addr": "0x24520",
"code": "__int64 __fastcall decryptFrames(__int64 a1, __int64 a2, __int64 a3, __int64 a4)\n{\n int v7; // eax\n __int64 v8; // rdx\n unsigned int v9; // ebp\n size_t v10; // r12\n _DWORD *v11; // rbx\n __int64 v12; // rax\n __int64 v13; // r15\n __int64 v15; // rax\n __int64 v16; // rcx\n __int64 v17; // rsi\n __int64 v18; // rdx\n __int64 v19; // rax\n unsigned __int64 v20; // rcx\n char v21; // r13\n __int128 v22; // rt0\n int v23; // eax\n __int64 v24; // rdi\n __int64 v25; // rax\n unsigned __int64 v26; // rsi\n __int64 v27; // [rsp+0h] [rbp-68h
... [truncated 5236 bytes] ...
ll **)(__int64, _QWORD))(*(_QWORD *)a1 + 1408LL))(a1, 0); /*0x24814*/\n goto LABEL_5; /*0x2481a*/\n }\nLABEL_35:\n v13 = (*(__int64 (__fastcall **)(__int64, _QWORD))(*(_QWORD *)a1 + 1408LL))(a1, v9); /*0x247e0*/\n (*(void (__fastcall **)(__int64, __int64, _QWORD, _QWORD, _DWORD *))(*(_QWORD *)a1 + 1664LL))(a1, v13, 0, v9, v11); /*0x24801*/\n goto LABEL_6; /*0x24807*/\n }\n return (*(__int64 (__fastcall **)(__int64, _QWORD, __int64, unsigned __int64))(*(_QWORD *)a1 + 1408LL))( /*0x245e0*/\n a1,\n 0,\n v8,\n __readfsqword(0x28u));\n}"
}
🤖 助手 - 第 38 轮 [14:04:25]
这很复杂。让我也看看其他关键子函数和 startSessionBytes、checkRhythm、updateExp。
🔨 工具调用: mcp_ida-pro-headless_decompile
🔨 工具调用: mcp_ida-pro-headless_decompile
🔨 工具调用: mcp_ida-pro-headless_decompile
📋 mcp_ida-pro-headless_decompile 输出 [14:04:25]
{
"addr": "0x238a0",
"code": "unsigned __int64 __fastcall startSessionBytes(__int64 a1, __int64 a2, __int64 a3, __int64 a4, int a5)\n{\n int v9; // eax\n unsigned int v10; // ecx\n unsigned int v11; // ecx\n unsigned int v12; // ebx\n __int64 v13; // r14\n unsigned __int64 v14; // rcx\n __int64 v15; // rsi\n __int64 v16; // rdi\n unsigned __int64 v17; // rdx\n __int64 v18; // rax\n _QWORD *v19; // rax\n __int64 v20; // rdx\n __int64 v21; // rsi\n void **v22; // r12\n __int64 v23; // rax\n __int64 v24; // rcx\n void **v25; // rbx\n __int64 v26; // rdx\n __int64 v27; // ra
... [truncated 9105 bytes] ...
/*0x23e0a*/\n v45,\n 40,\n (0x94D049BB133111EBLL\n * ((0xBF58476D1CE4E5B9LL * (v42 ^ (v42 >> 30))) ^ ((0xBF58476D1CE4E5B9LL * (v42 ^ (v42 >> 30))) >> 27)))\n ^ ((0x94D049BB133111EBLL\n * ((0xBF58476D1CE4E5B9LL * (v42 ^ (v42 >> 30))) ^ ((0xBF58476D1CE4E5B9LL * (v42 ^ (v42 >> 30))) >> 27))) >> 31),\n (0x94D049BB133111EBLL * (v43 ^ (v43 >> 27))) ^ ((0x94D049BB133111EBLL * (v43 ^ (v43 >> 27))) >> 31));\n byte_5D05C = 1; /*0x23e11*/\n return __readfsqword(0x28u); /*0x23e2b*/\n}"
}
📋 mcp_ida-pro-headless_decompile 输出 [14:04:25]
{
"addr": "0x23e50",
"code": "__int64 __fastcall checkRhythm(__int64 a1, __int64 a2, __int64 a3, int a4, __int64 a5, int a6)\n{\n __int64 result; // rax\n unsigned __int64 v7; // rdx\n unsigned __int64 v8; // rsi\n unsigned int v9; // eax\n int v10; // esi\n int v11; // eax\n int v12; // edi\n char v13; // r8\n int v14; // ecx\n int v15; // r10d\n\n result = 0; /*0x23e57*/\n if ( dword_5CFD0 > 0 && dword_5CFD4 > 0 ) /*0x23e6a*/\n {\n v7 = (a3 - qword_5CFD8) / 0xF4240uLL; /*0x23e87*/\n if ( (v7 & 0x1FFF00000000LL) != 0 ) /*0x23e98*/\n v8 = v7 % (unsigned int)dword_5C
... [truncated 1749 bytes] ...
e7*/\n v14 = 45; /*0x23eed*/\n if ( a6 == 1 ) /*0x23ef2*/\n v14 = 15; /*0x23ef2*/\n v15 = 90; /*0x23efb*/\n if ( a6 == 1 ) /*0x23f01*/\n v15 = 30; /*0x23f01*/\n if ( v12 / ((32 * (a6 != 1)) | 0x10) < 15 ) /*0x23f16*/\n v13 = v12 / ((32 * (a6 != 1)) | 0x10); /*0x23f16*/\n dword_5CFFC = ((((unsigned int)~v10 >> 27) | (unsigned __int8)((_BYTE)a6 << 6)) & 0x50) + (v13 & 0xF) + 16; /*0x23f35*/\n byte_5D000 = 1; /*0x23f3b*/\n result = 2; /*0x23f4c*/\n if ( v12 > v14 ) /*0x23f51*/\n return v12 <= v15; /*0x23f51*/\n }\n return result; /*0x23f54*/\n}"
}
📋 mcp_ida-pro-headless_decompile 输出 [14:04:25]
{
"addr": "0x23f60",
"code": "__int64 __fastcall updateExp(__int64 a1, __int64 a2, int a3, int a4, __int64 a5)\n{\n __int64 v5; // r15\n __int64 v6; // rbp\n __int64 v7; // r13\n __int64 v8; // r14\n __int64 v9; // rbx\n unsigned __int64 v10; // r12\n __int64 v11; // rax\n unsigned __int64 v12; // rdx\n unsigned __int64 v13; // rax\n unsigned __int64 v14; // rax\n unsigned __int64 v15; // rbx\n unsigned __int64 v16; // rbx\n unsigned __int64 v17; // rcx\n __int64 v18; // r9\n bool v19; // bp\n __int64 v20; // rdx\n unsigned int v21; // edi\n __int64 v22; // r13\n __int64
... [truncated 9466 bytes] ...
97CLL)\n - 0x61C8864680B583EBLL) >> 30))) >> 27))) >> 31);\n dword_5D038 = v21; /*0x2444c*/\n qword_5D040 = v18; /*0x24452*/\n qword_5D048 = v31; /*0x24459*/\n dword_5D050 = v32; /*0x24460*/\n dword_5D058 = v35; /*0x2446b*/\n v44 = qword_5D020; /*0x24472*/\n v45 = v43; /*0x2447c*/\n v46 = v21; /*0x24480*/\n v47 = v18; /*0x24484*/\n v48 = v31; /*0x24489*/\n v49 = v32; /*0x2448e*/\n v50 = v35; /*0x24492*/\n qword_5D028 = sub_2DAF0(&v44, 40, v42, v41); /*0x244b0*/\n }\n }\n return v5; /*0x244ca*/\n}"
}
🤖 助手 - 第 39 轮 [14:04:36]
这个逻辑非常复杂。让我看看关键的子函数 sub_2DDC0、sub_2DF40、sub_2E680、sub_24FC0 和 sub_24CB0。
🔨 工具调用: mcp_ida-pro-headless_decompile
🔨 工具调用: mcp_ida-pro-headless_decompile
🔨 工具调用: mcp_ida-pro-headless_decompile
📋 mcp_ida-pro-headless_decompile 输出 [14:04:36]
{
"addr": "0x2DDC0",
"code": "unsigned __int64 __fastcall sub_2DDC0(__int64 a1, _QWORD *a2, __int64 a3, unsigned __int64 *a4)\n{\n __int64 v6; // rax\n unsigned __int64 v7; // rsi\n unsigned __int64 v8; // r8\n unsigned __int64 v9; // rax\n __int64 v10; // rdx\n __int64 v11; // rax\n unsigned __int64 v12; // rax\n unsigned __int64 v13; // rcx\n _QWORD v15[2]; // [rsp+0h] [rbp-38h] BYREF\n int v16; // [rsp+10h] [rbp-28h]\n unsigned __int64 v17; // [rsp+20h] [rbp-18h]\n\n v17 = __readfsqword(0x28u); /*0x2ddd6*/\n v15[0] = a1; /*0x2dddb*/\n v15[1] = *(_QWORD *)a3; /*0x2dde2*/\n
... [truncated 2398 bytes] ...
((0x85EBCA77C2B2AE63LL - 0x61C8864E7A143579LL * v11) >> 33))) >> 29));\n v13 = 0x165667B19E3779F9LL /*0x2df05*/\n * ((0xC2B2AE3D27D4EB4FLL\n * ((0x85EBCA77C2B2AE63LL - 0x61C8864E7A143579LL * v10)\n ^ ((0x85EBCA77C2B2AE63LL - 0x61C8864E7A143579LL * v10) >> 33)))\n ^ ((0xC2B2AE3D27D4EB4FLL\n * ((0x85EBCA77C2B2AE63LL - 0x61C8864E7A143579LL * v10)\n ^ ((0x85EBCA77C2B2AE63LL - 0x61C8864E7A143579LL * v10) >> 33))) >> 29));\n *a4 = v12 ^ HIDWORD(v12); /*0x2df13*/\n a4[1] = v13 ^ HIDWORD(v13); /*0x2df16*/\n return __readfsqword(0x28u); /*0x2df2a*/\n}"
}
📋 mcp_ida-pro-headless_decompile 输出 [14:04:36]
{
"addr": "0x2DF40",
"code": "unsigned __int64 __fastcall sub_2DF40(__int64 a1, unsigned __int64 a2, const __m128i *a3, _BYTE *a4)\n{\n int v4; // eax\n unsigned int v5; // edx\n __int64 v6; // r14\n __m128i inserted; // xmm0\n __int64 v8; // rbx\n int v9; // kr00_4\n int v10; // ecx\n __m128i v11; // xmm0\n __m128i si128; // xmm1\n __m128i v13; // xmm2\n __int64 v14; // r9\n unsigned __int8 v15; // r9\n unsigned __int8 v16; // bl\n unsigned __int8 v17; // r12\n __int64 v18; // rdi\n unsigned __int8 v19; // r14\n unsigned __int8 v20; // dl\n unsigned __int8 v21; // cl\n
... [truncated 12218 bytes] ...
if ( v44 == v45 ) /*0x2e50c*/\n goto LABEL_7; /*0x2e50c*/\n if ( (v44 & 8) != 0 ) /*0x2e514*/\n goto LABEL_17; /*0x2e514*/\n }\n else\n {\n v45 = 0; /*0x2e496*/\n }\n do /*0x2e53f*/\n {\nLABEL_25:\n *(_BYTE *)(v66 + a1 + v45) ^= v68.m128i_u8[v45]; /*0x2e530*/\n ++v45; /*0x2e539*/\n }\n while ( v44 != v45 ); /*0x2e53f*/\nLABEL_7:\n v10 = v67 + 1; /*0x2e100*/\n v14 = v44 + v66; /*0x2e107*/\n }\n while ( v44 + v66 < a2 ); /*0x2e113*/\n }\n return __readfsqword(0x28u); /*0x2e559*/\n}"
}
📋 mcp_ida-pro-headless_decompile 输出 [14:04:36]
{
"addr": "0x2E680",
"code": "__int64 __fastcall sub_2E680(_QWORD *a1, __int64 a2)\n{\n _QWORD *v2; // r12\n _DWORD *v3; // r14\n unsigned __int64 v4; // rax\n __int64 v5; // rdx\n __int64 v6; // rbp\n int v7; // ecx\n int v8; // edi\n unsigned int v9; // edi\n size_t v10; // rdi\n unsigned __int64 v11; // rbx\n unsigned __int64 v12; // r13\n void **v13; // r14\n __int64 v14; // rax\n __int64 v15; // r15\n __int64 v16; // rax\n unsigned __int64 v17; // rbp\n void **v18; // r13\n _QWORD *v19; // r14\n void *v20; // r12\n void *v21; // rax\n __int64 v22; // rax\n __int6
... [truncated 7703 bytes] ...
ptr[1] = ptr[0]; /*0x2ea54*/\n operator delete(ptr[0]); /*0x2ea59*/\n }\n return (unsigned int)v6; /*0x2ea5e*/\n }\n }\n if ( !src[0] || (src[1] = src[0], operator delete(src[0]), (v25 = ptr[0]) != 0) ) /*0x2ea7d*/\n {\n ptr[1] = v25; /*0x2ea7f*/\n operator delete(v25); /*0x2ea84*/\n }\n goto LABEL_27; /*0x2ea84*/\n }\n }\n }\n }\n }\n }\n return (unsigned int)v6; /*0x2eaa0*/\n}"
}
👤 用户 [14:04:36]
[System — Repetitive Pattern Notice]
⚠️ Tool 'mcp_ida-pro-headless_decompile' called 8 times in last 12 calls — likely stuck in a loop
You may be repeating the same approach without making progress.
Suggested actions:
- Check the error messages carefully — is there a specific fix you're missing?
- If the current approach keeps failing for the same reason, consider adjusting your method
- If the errors are different each time, you're still making progress — keep iterating
- Use
ideas tool (action: list) to review alternative approaches if needed
Do NOT abandon a working approach just because of minor failures — debug and fix instead.
Recent tool results (evidence from last round):
[OK] mcp_ida-pro-headless_decompile({"addr": "0x2DDC0"}) -> {
"addr": "0x2DDC0",
"code": "unsigned int64 fastcall sub_2DDC0(int64 a1, _QWORD a2, int64 a3, unsigned int64 a4)\n{\n int64 v6; // rax\n unsigned int64 v7; // rsi\n unsigned int64 v8; // r8\n unsigned int64 v9; ...
[OK] mcp_ida-pro-headless_decompile({"addr": "0x2DF40"}) -> {
"addr": "0x2DF40",
"code": "unsigned int64 fastcall sub_2DF40(int64 a1, unsigned int64 a2, const m128i a3, _BYTE a4)\n{\n int v4; // eax\n unsigned int v5; // edx\n int64 v6; // r14\n m128i inserted; // xmm0\n i...
[OK] mcp_ida-pro-headless_decompile({"addr": "0x2E680"}) -> {
"addr": "0x2E680",
"code": "int64 fastcall sub_2E680(_QWORD *a1, int64 a2)\n{\n _QWORD v2; // r12\n _DWORD v3; // r14\n unsigned int64 v4; // rax\n int64 v5; // rdx\n __int64 v6; // rbp\n int v7; // ecx\n int v8; //...
🤖 助手 - 第 40 轮 [14:05:45]
<details>
<summary>💭 思考过程</summary>
I've gathered a lot of decompiled code. Let me analyze what I've found so far and understand the overall structure before decompiling more functions. Let me think about what I know:
- The APK has a
hjm_pack.bin file in assets with header "HJM1" (0x484A4D31)
- The SO has 6 JNI methods registered dynamically
sub_2DF40 is clearly AES encryption - it uses S-box (byte_17430), MixColumns operations, 10 rounds - this is AES-128 in CTR mode (XOR with keystream)
sub_2DDC0 derives a key using xxHash-like operations
sub_2E680 is the main decryption function that processes the HJM1 format
The key insight from decryptFrames and verifyAndDecrypt:
- They check
*v11 != 827148872 which is 0x31_4D_4A_48 = "HJM1" in little-endian
v11[1] (offset 4) is the type: 1 or 2
- For type 1: uses
sub_2DDC0 (key derivation) + sub_2DF40 (AES decryption)
- For type 2: more complex verification involving
sub_2E680
Let me look at the HJM1 file format more carefully:
Offset 0: "HJM1" (magic)
Offset 4: type (uint32) - value is 2
Offset 8: field2 (uint32) - value is 1
Offset 12: field3 (uint32) - 0x40 = 64
Offset 16: field4 (uint32) - 0x40 = 64
Offset 20: field5 (uint32) - 0x78 = 120
Offset 24: 16 bytes - key/IV material
Offset 40: 12 bytes - more key material
Offset 52: encrypted data
Wait, let me re-read the hex:
484a4d31 02000000 01000000 40000000 40000000 78000000
- Magic: HJM1
- Type: 2
- field[2]: 1
- field[3]: 0x40 = 64
- field[4]: 0x40 = 64
- field[5]: 0x78 = 120
Then starting at offset 24:
0f1e2d3c 4b5a6978 8796a5b4 c3d2e1f0
This is 16 bytes at offset 24 (v3+6 = offset 24 bytes)
Then at offset 40:
11223344 5566778899aabbcc
This is 12 bytes at offset 40 (v3+10 = offset 40 bytes)
Then at offset 52:
The encrypted data starts.
Now in verifyAndDecrypt:
- For type 2 (v26 == 2):
- It checks
byte_5CFF8 || byte_5D140 or v23 & 1 (anti-cheat flags)
- It needs
v27 >= 999 (exp >= 999)
- It calls
sub_2E680 which is
... [thinking truncated, was 7414 chars] ...
pted data and the current exp value. For type 2 data, it needs exp >= 999.
The key question is: what is qword_5CFF0? It's set in updateExp when certain conditions are met. Let me trace through updateExp more carefully.
In updateExp:
- When
a3 == 2 (PERFECT hit) and conditions are met:
v25 = v39 + 1 (increment perfect counter)
- If
v32 >= 0xC (12 perfects) and v37 >= 0x3E7 (exp >= 999):
byte_5CFF8 = 1 (flag set)
qword_5CFF0 = v31 (key stored)
So the key qword_5CFF0 is set when you get 12+ perfect hits and exp >= 999.
The value v31 at that point is derived from the game state through complex hash operations. This means the key depends on the exact sequence of game inputs.
Hmm, but there must be a deterministic way to compute this. The game has a fixed beat map, and if you hit all notes perfectly, the state should be deterministic.
Let me look at what beat map data is used. In startSessionBytes, the beat map is copied to dword_5CDD0. The
</details>
👤 用户 [14:06:41]
请继续解题。之前的尝试被中断了,请基于已有的分析继续完成解题,找到 flag 并提交。
👤 用户 [14:06:41]
[System — Mandatory Planning Phase]
Before executing ANY tools, you MUST first create a structured solving plan:
Step 1: Read the challenge description carefully — identify category, key constraints, and attack surface
Step 2: If attachments exist, download and examine them FIRST (file type, checksec, strings) before planning
Step 3: Based on gathered info, use todolist (action: reset) to create 3-5 concrete, ORDERED solving steps
Step 4: Rank steps from most-likely-to-succeed to least-likely
Step 5: Only THEN begin executing the first step
🧠 Planning Quality Checklist:
- Each step should be a TESTABLE hypothesis (e.g., 'Test for SQL injection on /login param'), not vague (e.g., 'Try web attacks')
- Include WHAT tool to use and WHAT to look for in each step
- First step should always be information gathering (read source, analyze binary, fetch target)
- Have at least one backup approach from a different attack angle
Previous solving ideas for this challenge:
🔄 [386ab2cd-ecc0-40dd-8909-86213a0e3f3c] Extract and analyze APK file structure to identify obfuscation, encryption, or anti-analysis techniq → APK extraction complete - 1394 classes decompiled, obfuscation patterns detected in NativeBridge and MainActivity, anti-analysis techniques identified
🔄 [227240ce-0eae-4068-a78a-85084825d9ae] Decompile APK using apktool or jadx to examine source code and identify vulnerability vectors → jadx decompilation successful - source code examination ongoing, vulnerability vectors in JNI calls and obfuscated class references identified
🔄 [d68e4418-0810-414c-9b85-9626c3306d73] Analyze NativeBridge native method calls to identify JNI exploitation vectors or native code vulnera → JNI_OnLoad entry point confirmed at 0x237c0 - native method call chain analysis in progress for exploitation vectors
🔄 [3feefef8-9e1e-422e-9b9b-7b60e43a4f5d] Examine MainActivity imports and obfuscated class references (f1.h, Q0.AbstractC0080d, etc.) to map → Obfuscation mapping underway - f1.h, Q0.AbstractC0080d class references correlated with native function calls
🔄 [d112ede0-8135-40cb-a480-c00601aaa18b] Reverse engineer NativeBridge JNI calls to locate native library (.so) and analyze for buffer overfl → Native library analysis initiated - JNI_OnLoad at 0x237c0 disassembly proceeding to identify buffer overflow or memory corruption vectors
🔄 [9f0076f9-2570-4ff7-8ada-419bd017c95c] Disassemble JNI_OnLoad at 0x237c0 to identify native library loading mechanism and potential ROP gad → JNI_OnLoad disassembly initiated - function at 0x237c0 (size 0x8e) identified, native library loading mechanism analysis underway
⏳ [642a6e58-cb27-4ee4-b907-936b98b5807f] Analyze renamed functions (startSessionBytes, checkRhyt...) for cryptographic or session management
📊 Stats: 1 pending, 6 testing, 0 failed, 0 verified, 0 skipped (active: 7/10)
🎯 ACTION REQUIRED: You have pending ideas. Pick one and EXECUTE it now instead of adding more ideas!
⚠️ Review these ideas carefully:
- Skip approaches marked as 'failed' — they already proved unsuccessful
- Prioritize 'pending' ideas — they haven't been tried yet
- Build your plan around untried approaches
Do NOT skip planning. A structured approach prevents wasting rounds on random attempts.
🤖 助手 - 第 41 轮 [14:08:13]
<details>
<summary>💭 思考过程</summary>
Let me take stock of what I've learned so far and create a focused plan to solve this challenge.
Key findings:
- APK with
libhajimi.so native library
NativeBridge class with methods: startSessionBytes, checkRhythm, updateExp, decryptFrames, verifyAndDecrypt, setDebugBypass
hjm_pack.bin asset file with "HJM1" magic header (564 bytes)
- The SO has AES-like encryption (S-box at byte_17430, key expansion, MixColumns operations in sub_2DF40)
sub_2DF40 is clearly AES-128 CTR mode encryption/decryption
sub_2E680 handles the HJM1 format parsing and decryption
verifyAndDecrypt takes byte[] and String, processes HJM1 data
decryptFrames takes byte[] and long, also processes HJM1 data
The game flow seems to be:
- Start a rhythm game session
- Player hits notes, accumulating experience/score
- When score is high enough, decryptFrames or verifyAndDecrypt is called to decrypt the flag
The key insight is that the encryption uses AES with keys derived from game state. I need to understand how to bypass the game logic and directly decrypt the data.
Let me focus on understanding the HJM1 file format and the decryption path. The sub_2E680 function checks:
- Magic: 0x314D4A48 = "HJM1" (827148872)
- Type field at offset 4 must be 2
- Several other fields at offsets 8, 12, 16
For verifyAndDecrypt:
- It reads the HJM1 data
- Calls sub_24FC0 (anti-debug check?)
- For type==1: uses sub_2DDC0 (key derivation) and sub_2DF40 (AES decrypt) with key=0x5A5A5A5D (999 ^ 0x5A5A5A5A)
- For type==2: calls sub_2E680 with a key derived from game state
Let me look more carefully at the HJM1 file structure and figure out the decryption approach.
Actually, let me re-read the decompiled code more carefully.
In decryptFrames:
- Checks HJM1 magic (827148872 = 0x314D4A48)
- v23 = v11[1] (type field at offset 4)
- If type == 1 and size > 0x34:
- v24 = a4 (exp value) ^ 0x5A5A5A5A (if v21 is true, i.e., anti-debug passed)
- sub_2DDC0(v24, v11+6, v11+10, v29) - ke
... [thinking truncated, was 12175 chars] ...
y have IDA open. Let me just decompile the remaining key functions and then write a solution script.
Let me focus on:
- setDebugBypass - how to enable debug mode
- sub_2E570 - how the key is generated in debug mode
- sub_2EB00 - key derivation used in sub_2E680
- sub_24CB0 - hash function used throughout
Then I can write a Python script to decrypt hjm_pack.bin.
Let me also check sub_2EFD0 which validates the string input in verifyAndDecrypt.
Actually, I think the most efficient approach is to spawn a reverse sub-agent with all the decompiled code and let it figure out the decryption. But let me first get the remaining decompilations.
Let me be more strategic. The key functions I still need:
- setDebugBypass (0x24CA0) - small, should be quick
- sub_2E570 - key generation for debug mode
- sub_24CB0 - hash function (used everywhere)
- sub_2EB00 - key derivation for type 2
- sub_2DAF0 - another hash function used in startSession
Let me get these and then write the solution.
</details>
好的,让我基于已有分析制定聚焦计划。我已经知道:
hjm_pack.bin 是 HJM1 格式,type=2,包含 64x64 位图(512字节加密数据)
- 解密路径在
sub_2E680,需要一个密钥
- 有
setDebugBypass 和 sub_2E570 可能提供简化路径
🔨 工具调用: todolist
- action:
reset
- tasks:
[1. 反编译setDebugBypass和sub_2E570,理解debug bypass路径的密钥生成 2. 反编译sub_24CB0(哈希)和sub_2EB00(密钥派生),理解完整解密链 3. 提取AES S-box和所有常量,编写Python解密脚本 4. 解密hjm_pack.bin得到64x64位图,识别flag(可能是QR码) 5. 提交flag]
📋 todolist 输出 [14:08:13]
TodoList reset with 5 task(s):
⬜ 1. 反编译setDebugBypass和sub_2E570,理解debug bypass路径的密钥生成
⬜ 2. 反编译sub_24CB0(哈希)和sub_2EB00(密钥派生),理解完整解密链
⬜ 3. 提取AES S-box和所有常量,编写Python解密脚本
⬜ 4. 解密hjm_pack.bin得到64x64位图,识别flag(可能是QR码)
⬜ 5. 提交flag
👤 用户 [14:08:13]
<historical_experience>
⚠️ 严禁将以下案例中的具体值(XOR key、checksum、flag 字符串、密码、偏移量)直接用于当前题目。仅用于识别技术方向和分析思路。
以下是从历史解题中提取的技术模式,仅供思路启发,必须结合当前题目独立分析,不要照搬历史解法:
[cases] chu7: UPX-packed PE32+ with CRC64 key derivation and custom stream cipher using AES S-box (relevance: 0.47)
Windows GUI program CM1.exe encrypted flag.png using a custom algorithm. Key insight: UPX packing concealed real code; after unpacking, found hardcoded key prefix '52pojie2026' combined with user password to derive CRC64 hash (0xC96C5795D7870F42 polynomial), which initializes a stream cipher using AES S-box for keystream generation. PNG magic bytes (89504e47...) serve as known plaintext to recover stream cipher key 0x55a4f867ba4475dd via XOR. Solution: Unpack binary, extract CRC64 polynomial and S-box at 0x14000A270, implement stream cipher logic (ROL3 + S-box substitution + XOR with IV), use known plaintext attack to recover CRC64 state, decrypt file, verify CRC32 checksum, and extract flag from PNG tEXt chunk metadata.
[cases] 初二题目 PE32 XOR password recovery (relevance: 0.41)
Windows PE32 reverse challenge where hardcoded DWORDs were XORed to recover password. Binary had Chinese filename causing path issues. Solution extracted 6 DWORD values from binary and XORed them sequentially.
[cases] Windows PE32 crackme with XOR-obfuscated password and weighted checksum validation (relevance: 0.41)
52pojie Spring Festival CTF 'Day 2' challenge: PE32 console app with fake flag hint, 31-char password requirement, XOR-deobfuscation (key 0x42), and weighted checksum (sum of position*value = 44709). Key insight: ignore fake flag, reverse sub_401620's DWORD buffer XOR logic.
[cases] reverse1: Simple character replacement validation in PE x64 binary (relevance: 0.40)
Challenge: PE x64 Windows executable that validates user input against a hardcoded flag. Key insight: The flag string is stored in the binary's data section and can be extracted directly via IDA Pro. The validation logic replaces all 'o' characters with '0' before comparison. Solution: Use IDA Pro headless to decompile main function, identify the global string variable (Str2), extract its value, apply the character replacement transformation, and wrap with flag{} format.
[cases] SUCTF2018 single dog - AAEncode in MISC image (relevance: 0.38)
Misc image steganography challenge. Used binwalk to extract embedded zip+txt from image file. Hidden content was AAEncoded, decoded to get flag. Key insight: check hex for file inclusion anomalies, use binwalk for extraction.
</historical_experience>
🤖 助手 - 第 42 轮 [14:08:23]
<details>
<summary>💭 思考过程</summary>
Let me focus on the key functions. I need to understand the debug bypass path and the decryption chain. Let me start by decompiling setDebugBypass and sub_2E570.
</details>
🔨 工具调用: todolist
- action:
update
- id:
1
- status:
in_progress
📋 todolist 输出 [14:08:23]
Updated task #1. Current TodoList:
🔄 1. 反编译setDebugBypass和sub_2E570,理解debug bypass路径的密钥生成
⬜ 2. 反编译sub_24CB0(哈希)和sub_2EB00(密钥派生),理解完整解密链
⬜ 3. 提取AES S-box和所有常量,编写Python解密脚本
⬜ 4. 解密hjm_pack.bin得到64x64位图,识别flag(可能是QR码)
⬜ 5. 提交flag
🤖 助手 - 第 43 轮 [14:08:28]
🔨 工具调用: mcp_ida-pro-headless_decompile
🔨 工具调用: mcp_ida-pro-headless_decompile
📋 mcp_ida-pro-headless_decompile 输出 [14:08:28]
{
"addr": "0x24ca0",
"code": "void __fastcall setDebugBypass(__int64 a1, __int64 a2, char a3)\n{\n byte_5D140 = a3 == 1; /*0x24ca3*/\n}"
}
📋 mcp_ida-pro-headless_decompile 输出 [14:08:28]
{
"addr": "0x2E570",
"code": "__int64 sub_2E570()\n{\n int v0; // ebp\n int v1; // ebx\n __int64 v2; // r14\n unsigned __int64 v3; // rsi\n __int64 v4; // rcx\n unsigned __int64 v5; // r15\n __int64 v7; // [rsp+0h] [rbp-48h] BYREF\n int v8; // [rsp+8h] [rbp-40h]\n int v9; // [rsp+Ch] [rbp-3Ch]\n int v10; // [rsp+10h] [rbp-38h]\n int v11; // [rsp+14h] [rbp-34h]\n unsigned __int64 v12; // [rsp+18h] [rbp-30h]\n\n v12 = __readfsqword(0x28u); /*0x2e585*/\n v0 = dword_5CFD0; /*0x2e58a*/\n v1 = 0; /*0x2e590*/\n if ( dword_5CFD0 <= 0 ) /*0x2e594*/\n return 0; /*0x2e650*/\n v2 =
... [truncated 1474 bytes] ...
)))\n ^ ((0xBF58476D1CE4E5B9LL\n * (((qword_5CFE8 ^ 0x1A8CBC5B802E097CLL) - 0x61C8864680B583EBLL)\n ^ (((qword_5CFE8 ^ 0x1A8CBC5B802E097CuLL) - 0x61C8864680B583EBLL) >> 30))) >> 27));\n v4 = v3 ^ (v3 >> 31); /*0x2e5f2*/\n v5 = qword_5CFE8 ^ 0x802E097C1A8CBC5BLL; /*0x2e5ff*/\n do /*0x2e64c*/\n {\n v7 = v4; /*0x2e615*/\n v8 = v1 % v0; /*0x2e619*/\n v9 = 0; /*0x2e61d*/\n v10 = v1; /*0x2e625*/\n v11 = 2; /*0x2e629*/\n v4 = sub_2DAF0(&v7, 24, v2, v5); /*0x2e644*/\n ++v1; /*0x2e647*/\n }\n while ( v1 != 12 ); /*0x2e64c*/\n return v4; /*0x2e665*/\n}"
}
🤖 助手 - 第 44 轮 [14:08:36]
setDebugBypass 只是设置 byte_5D140 = 1。当 byte_5D140 为 true 时,decryptFrames 和 verifyAndDecrypt 会调用 sub_2E570 来生成密钥。这是一个确定性路径!
现在让我看 sub_2DAF0(哈希函数)和 sub_24CB0、sub_2EB00:
🔨 工具调用: mcp_ida-pro-headless_decompile
🔨 工具调用: mcp_ida-pro-headless_decompile
📋 mcp_ida-pro-headless_decompile 输出 [14:08:36]
{
"addr": "0x2DAF0",
"code": "__int64 __fastcall sub_2DAF0(unsigned __int8 *a1, __int64 a2, __int64 a3, __int64 a4)\n{\n __int64 v4; // r9\n __int64 v5; // r8\n __int64 v6; // rax\n __int64 v7; // rdx\n unsigned __int64 v8; // r10\n __int64 v9; // r11\n __int64 v10; // r9\n __int64 v11; // rbx\n __int64 v12; // r8\n __int64 v13; // rdx\n __int64 v14; // rax\n __int64 v15; // rdx\n __int64 v16; // r9\n __int64 v17; // rax\n __int64 v18; // r8\n __int64 v19; // rdx\n __int64 v20; // r9\n __int64 v21; // r8\n __int64 v22; // rax\n __int64 v23; // rdx\n __int64 v24; // r9\
... [truncated 5807 bytes] ...
^ __ROL8__(v55, 21); /*0x2dcea*/\n v60 = v58 + v56; /*0x2dced*/\n v61 = v60 ^ __ROL8__(v58, 13); /*0x2dcf4*/\n v62 = v59 + __ROL8__(v57, 32); /*0x2dcfb*/\n v63 = v62 ^ __ROL8__(v59, 16); /*0x2dd02*/\n v64 = v63 + __ROL8__(v60, 32); /*0x2dd05*/\n v65 = v61 + v62; /*0x2dd0c*/\n v66 = v65 ^ __ROL8__(v61, 17); /*0x2dd13*/\n v67 = v64 ^ __ROL8__(v63, 21); /*0x2dd1a*/\n v68 = v67 + __ROL8__(v65, 32); /*0x2dd24*/\n v69 = (v66 + v64) ^ __ROL8__(v66, 13); /*0x2dd32*/\n return (v69 + v68) ^ __ROL8__(v68 ^ __ROL8__(v67, 16), 21) ^ __ROL8__(v69, 17) ^ __ROL8__(v69 + v68, 32); /*0x2dd49*/\n}"
}
📋 mcp_ida-pro-headless_decompile 输出 [14:08:36]
{
"addr": "0x24CB0",
"code": "unsigned __int64 __fastcall sub_24CB0(unsigned __int8 *a1, unsigned __int64 a2, unsigned __int64 a3)\n{\n unsigned __int64 v3; // r9\n unsigned __int64 v4; // rbp\n unsigned __int64 v5; // r13\n unsigned __int64 v6; // r12\n __int64 v7; // r15\n __int64 v8; // r14\n __int64 v9; // rbx\n __int64 v10; // r11\n unsigned __int64 v11; // r11\n unsigned __int64 v12; // r11\n unsigned __int64 v13; // rsi\n unsigned __int8 *v14; // r10\n bool v15; // cc\n unsigned __int64 v16; // rdi\n unsigned __int64 v17; // r10\n __int64 v18; // r11\n unsigned __in
... [truncated 4974 bytes] ...
t8 *)v16), 11))\n ^ (0x27D4EB2F165667C5LL * *(unsigned __int8 *)(v16 + 1)),\n 11))\n ^ (0x27D4EB2F165667C5LL * *(unsigned __int8 *)(v16 + 2)),\n 11))\n ^ (0x27D4EB2F165667C5LL * *(unsigned __int8 *)(v16 + 3)),\n 11);\n v16 += 4LL; /*0x24f7d*/\n }\n while ( v16 < v3 ); /*0x24f84*/\n }\nLABEL_18:\n v20 = 0x165667B19E3779F9LL /*0x24f86*/\n * ((0xC2B2AE3D27D4EB4FLL * (v13 ^ (v13 >> 33))) ^ ((0xC2B2AE3D27D4EB4FLL * (v13 ^ (v13 >> 33))) >> 29));\n return v20 ^ HIDWORD(v20); /*0x24fac*/\n}"
}
🤖 助手 - 第 45 轮 [14:08:44]
sub_2DAF0 是 SipHash-2-4!sub_24CB0 是 XXHash64。这些都是已知的哈希算法。
现在让我看 sub_2EB00(密钥派生)和 sub_2EC80(排列生成):
🔨 工具调用: mcp_ida-pro-headless_decompile
🔨 工具调用: mcp_ida-pro-headless_decompile
📋 mcp_ida-pro-headless_decompile 输出 [14:08:44]
{
"addr": "0x2EB00",
"code": "unsigned __int64 __fastcall sub_2EB00(__int64 a1, __int64 a2, _QWORD *a3, unsigned __int64 *a4)\n{\n __int64 v6; // rax\n unsigned __int64 v7; // rsi\n unsigned __int64 v8; // r8\n unsigned __int64 v9; // rax\n __int64 v10; // rdx\n __int64 v11; // rax\n unsigned __int64 v12; // rax\n unsigned __int64 v13; // rcx\n _QWORD v15[5]; // [rsp+0h] [rbp-28h] BYREF\n\n v15[2] = __readfsqword(0x28u); /*0x2eb16*/\n v15[0] = a1; /*0x2eb35*/\n v15[1] = a2; /*0x2eb39*/\n v6 = sub_2DAF0((unsigned __int8 *)v15, 16, a2 ^ 0x1A8CBC5B802E097CLL, a2 ^ 0x802E097C1A8CB
... [truncated 2294 bytes] ...
((0x85EBCA77C2B2AE63LL - 0x61C8864E7A143579LL * v11) >> 33))) >> 29));\n v13 = 0x165667B19E3779F9LL /*0x2ec41*/\n * ((0xC2B2AE3D27D4EB4FLL\n * ((0x85EBCA77C2B2AE63LL - 0x61C8864E7A143579LL * v10)\n ^ ((0x85EBCA77C2B2AE63LL - 0x61C8864E7A143579LL * v10) >> 33)))\n ^ ((0xC2B2AE3D27D4EB4FLL\n * ((0x85EBCA77C2B2AE63LL - 0x61C8864E7A143579LL * v10)\n ^ ((0x85EBCA77C2B2AE63LL - 0x61C8864E7A143579LL * v10) >> 33))) >> 29));\n *a4 = v12 ^ HIDWORD(v12); /*0x2ec4f*/\n a4[1] = v13 ^ HIDWORD(v13); /*0x2ec52*/\n return __readfsqword(0x28u); /*0x2ec66*/\n}"
}
📋 mcp_ida-pro-headless_decompile 输出 [14:08:44]
{
"addr": "0x2EC80",
"code": "__int64 __fastcall sub_2EC80(int a1, __int64 a2, __int64 *a3)\n{\n __int64 result; // rax\n unsigned __int64 v6; // rcx\n __int64 v7; // rdx\n __m128i si128; // xmm0\n __int64 v9; // rdi\n __m128i v10; // xmm1\n __m128i v11; // xmm2\n __int64 v12; // rdi\n unsigned __int64 v13; // rcx\n unsigned __int64 v14; // rbx\n __int64 v15; // r10\n unsigned __int64 v16; // rdx\n\n result = *a3; /*0x2ec8f*/\n v6 = (a3[1] - *a3) >> 2; /*0x2ec99*/\n if ( v6 >= a1 ) /*0x2eca0*/\n {\n if ( v6 > a1 ) /*0x2ecaf*/\n {\n result += 4LL * a1; /*0x2ecb1*/
... [truncated 2890 bytes] ...
*)(v12 + 4 * v15) = *(_DWORD *)(v12 + 4 * v16); /*0x2edab*/\n *(_DWORD *)(v12 + 4 * v16) = result; /*0x2edaf*/\n --v13; /*0x2edb2*/\n v14 -= 0x61C8864680B583EBLL; /*0x2edb5*/\n }\n while ( v13 > 2 ); /*0x2edbc*/\n }\n return result; /*0x2edbc*/\n }\n }\n else\n {\n v7 = 0; /*0x2eccb*/\n }\n do /*0x2ed39*/\n {\n *(_DWORD *)(result + 4 * v7) = v7; /*0x2ed30*/\n ++v7; /*0x2ed33*/\n }\n while ( a1 != v7 ); /*0x2ed39*/\n goto LABEL_12; /*0x2ed39*/\n }\n return result; /*0x2edbe*/\n}"
}
🤖 助手 - 第 46 轮 [14:08:52]
现在让我看 sub_2DD50(分配内存)和 sub_2EFD0(验证密码):
🔨 工具调用: mcp_ida-pro-headless_decompile
🔨 工具调用: mcp_ida-pro-headless_decompile
📋 mcp_ida-pro-headless_decompile 输出 [14:08:52]
{
"addr": "0x2DD50",
"code": "void __fastcall sub_2DD50(__int64 a1, signed __int64 a2)\n{\n char *v2; // rax\n char *v3; // r15\n\n *(_OWORD *)a1 = 0; /*0x2dd58*/\n *(_QWORD *)(a1 + 16) = 0; /*0x2dd5b*/\n if ( a2 ) /*0x2dd66*/\n {\n if ( a2 < 0 ) /*0x2dd6b*/\n sub_2EAF0(a1); /*0x2dd9f*/\n v2 = (char *)operator new(a2); /*0x2dd73*/\n *(_QWORD *)a1 = v2; /*0x2dd78*/\n v3 = &v2[a2]; /*0x2dd7e*/\n *(_QWORD *)(a1 + 16) = &v2[a2]; /*0x2dd81*/\n memset(v2, 0, a2); /*0x2dd8d*/\n *(_QWORD *)(a1 + 8) = v3; /*0x2dd92*/\n }\n}"
}
📋 mcp_ida-pro-headless_decompile 输出 [14:08:52]
{
"addr": "0x2EFD0",
"code": "__int64 __fastcall sub_2EFD0(char *s, int a2, int a3, void *a4, size_t a5)\n{\n char *v5; // r13\n char *v6; // rbp\n size_t v7; // rax\n size_t v8; // r12\n size_t v9; // rax\n size_t v10; // r15\n size_t v11; // rbx\n __int64 v12; // r9\n __int64 v13; // r10\n size_t v14; // rdi\n char *v15; // r14\n char *v16; // rsi\n unsigned __int64 *v17; // r11\n unsigned __int64 v18; // r12\n __int64 v19; // rax\n _BYTE *v20; // rcx\n unsigned __int64 v21; // rdx\n __int64 v22; // r8\n __int128 v23; // xmm1\n char v24; // al\n unsigned __int64 v25;
... [truncated 12861 bytes] ...
/*0x2f6fb*/\n }\n }\n LODWORD(v6) = 0; /*0x2f334*/\nLABEL_101:\n if ( v27 ) /*0x2f70d*/\n {\n v49 = v27; /*0x2f70f*/\n if ( v28 != v27 ) /*0x2f715*/\n {\n do /*0x2f726*/\n {\n v50 = v28 - 3; /*0x2f728*/\n if ( (*(_BYTE *)(v28 - 3) & 1) != 0 ) /*0x2f730*/\n operator delete(*(v28 - 1)); /*0x2f736*/\n v28 = v50; /*0x2f720*/\n }\n while ( v50 != v27 ); /*0x2f726*/\n v49 = (void **)ptr[0]; /*0x2f73d*/\n }\n ptr[1] = v27; /*0x2f745*/\n operator delete(v49); /*0x2f74d*/\n }\n return (unsigned int)v6; /*0x2f767*/\n}"
}
👤 用户 [14:08:52]
[System — Repetitive Pattern Notice]
⚠️ Tool 'mcp_ida-pro-headless_decompile' called 8 times in last 10 calls — likely stuck in a loop
You may be repeating the same approach without making progress.
Suggested actions:
- Check the error messages carefully — is there a specific fix you're missing?
- If the current approach keeps failing for the same reason, consider adjusting your method
- If the errors are different each time, you're still making progress — keep iterating
- Use
ideas tool (action: list) to review alternative approaches if needed
Do NOT abandon a working approach just because of minor failures — debug and fix instead.
Recent tool results (evidence from last round):
[OK] mcp_ida-pro-headless_decompile({"addr": "0x2DD50"}) -> {
"addr": "0x2DD50",
"code": "void __fastcall sub_2DD50(int64 a1, signed int64 a2)\n{\n char v2; // rax\n char v3; // r15\n\n (_OWORD )a1 = 0; /0x2dd58/\n (_QWORD )(a1 + 16) = 0; /0x2dd5b/\n if ( a2 ) /0x2dd66/\n {...
[OK] mcp_ida-pro-headless_decompile({"addr": "0x2EFD0"}) -> {
"addr": "0x2EFD0",
"code": "int64 fastcall sub_2EFD0(char s, int a2, int a3, void a4, size_t a5)\n{\n char v5; // r13\n char v6; // rbp\n size_t v7; // rax\n size_t v8; // r12\n size_t v9; // rax\n size_t v10; // r15\n ...
🤖 助手 - 第 47 轮 [14:10:06]
<details>
<summary>💭 思考过程</summary>
Now I have a good understanding of the code. Let me summarize what I've learned and write a Python decryption script.
Key findings:
sub_2DAF0 = SipHash-2-4
sub_24CB0 = XXHash64
sub_2DF40 = AES-128 CTR mode encryption/decryption (with S-box at byte_17430)
sub_2DDC0 = Key derivation using SipHash + XXHash
sub_2E680 = Main decryption function for type=2 frames
sub_2EB00 = Another key derivation function
sub_2EC80 = Fisher-Yates shuffle (permutation generation)
sub_2EFD0 = Text rendering to bitmap (converts string to bitmap using font data at byte_17540)
The hjm_pack.bin file structure:
- Offset 0: magic "HJM1" (0x314D4A48 = 827148872)
- Offset 4: type (1 or 2)
- Offset 8: field2 (number of frames/rows)
- Offset 12: field3 (height)
- Offset 16: field4 (width)
- Offset 20: padding/reserved
- Offset 24: key material (16 bytes for type=1, or used differently for type=2)
- Offset 40: IV (12 bytes)
- Offset 52: encrypted data
For type=2 in sub_2E680:
- It checks magic, type=2, and fields
- Calls
sub_2EB00(a2, qword_5CFE8, v3+6, &v42) to derive AES key
- Then in a loop, derives per-block keys using
sub_24CB0 (XXHash64)
- Calls
sub_2DF40 (AES CTR) to decrypt each 256-byte block
- If field2 > 1, generates a permutation using
sub_2EC80 and rearranges rows
The key question is: what is a2 (the key parameter) passed to sub_2E680?
Looking at verifyAndDecrypt (0x24850):
- For type=2, when
byte_5D140 (debug bypass) is set:
- Calls
sub_2E570 to generate key
- XORs with 0xA5A5A5A5A5A5A5A5
- Passes to
sub_2E680
And sub_2E570 generates a deterministic key based on qword_5CFE8 and dword_5CFD0.
But wait - qword_5CFE8 and dword_5CFD0 are set in startSessionBytes based on the session data. These are state variables that depend on the rhythm game state.
Actually, let me re-read the flow more carefully.
In sub_2E680:
sub_2EB00(a2, qword_5CFE8, v3 + 6, &v42); // derive key from a2, qword_5CFE8, a
... [thinking truncated, was 5084 chars] ...
XHash64 of session data
qword_5CFE0 = v18;
qword_5CFE8 = v18; // Initially set to same value
And then in decryptFrames/verifyAndDecrypt, qword_5CFE8 gets updated based on game state.
For the hjm_pack.bin file, it seems like it's a standalone encrypted file. The question is: what state was used to encrypt it?
Let me look at this from a different angle. Maybe the file is meant to be decrypted with a specific known state. Let me check if there's a "session" file or if the state is embedded.
Actually, let me re-read the challenge. It's an Android reverse engineering challenge. The APK contains:
- Native library (libhjm.so)
- hjm_pack.bin (encrypted data)
- Java code
The hjm_pack.bin is likely encrypted with a key that can be derived from the code. Let me look at how the APK uses the native library.
Let me check the Java code to see how the native methods are called.
Actually, I should look at the APK's Java code to understand the flow. Let me check what files we have.
</details>
好的,我已经收集了足够的反编译信息。现在该停止反编译,开始写解密脚本了。让我先检查 hjm_pack.bin 的文件头和提取关键常量。
🔨 工具调用: mcp_ida-pro-headless_get_bytes
- regions:
[map[addr:0x17430 size:256] map[addr:0x17530 size:16]]
📋 mcp_ida-pro-headless_get_bytes 输出 [14:10:06]
[
{
"addr": "0x17430",
"data": "0x63 0x7c 0x77 0x7b 0xf2 0x6b 0x6f 0xc5 0x30 0x1 0x67 0x2b 0xfe 0xd7 0xab 0x76 0xca 0x82 0xc9 0x7d 0xfa 0x59 0x47 0xf0 0xad 0xd4 0xa2 0xaf 0x9c 0xa4 0x72 0xc0 0xb7 0xfd 0x93 0x26 0x36 0x3f 0xf7 0xcc 0x34 0xa5 0xe5 0xf1 0x71 0xd8 0x31 0x15 0x4 0xc7 0x23 0xc3 0x18 0x96 0x5 0x9a 0x7 0x12 0x80 0xe2 0xeb 0x27 0xb2 0x75 0x9 0x83 0x2c 0x1a 0x1b 0x6e 0x5a 0xa0 0x52 0x3b 0xd6 0xb3 0x29 0xe3 0x2f 0x84 0x53 0xd1 0x0 0xed 0x20 0xfc 0xb1 0x5b 0x6a 0xcb 0xbe 0x39 0x4a 0x4c 0x58 0xcf 0xd0 0xef 0xaa 0xfb 0x43 0x4d 0x33 0x85 0x45 0xf9 0x2 0x7f 0x50 0x3c 0x9f 0xa8 0x51
... [truncated 1429 bytes] ...
xdb 0xe0 0x32 0x3a 0xa 0x49 0x6 0x24 0x5c 0xc2 0xd3 0xac 0x62 0x91 0x95 0xe4 0x79 0xe7 0xc8 0x37 0x6d 0x8d 0xd5 0x4e 0xa9 0x6c 0x56 0xf4 0xea 0x65 0x7a 0xae 0x8 0xba 0x78 0x25 0x2e 0x1c 0xa6 0xb4 0xc6 0xe8 0xdd 0x74 0x1f 0x4b 0xbd 0x8b 0x8a 0x70 0x3e 0xb5 0x66 0x48 0x3 0xf6 0xe 0x61 0x35 0x57 0xb9 0x86 0xc1 0x1d 0x9e 0xe1 0xf8 0x98 0x11 0x69 0xd9 0x8e 0x94 0x9b 0x1e 0x87 0xe9 0xce 0x55 0x28 0xdf 0x8c 0xa1 0x89 0xd 0xbf 0xe6 0x42 0x68 0x41 0x99 0x2d 0xf 0xb0 0x54 0xbb 0x16"
},
{
"addr": "0x17530",
"data": "0x0 0x1 0x2 0x4 0x8 0x10 0x20 0x40 0x80 0x1b 0x36 0x0 0x0 0x0 0x0 0x0"
}
]
👤 用户 [14:10:06]
[System — Repetitive Pattern Notice]
⚠️ Tool 'mcp_ida-pro-headless_decompile' called 8 times in last 11 calls — likely stuck in a loop
You may be repeating the same approach without making progress.
Suggested actions:
- Check the error messages carefully — is there a specific fix you're missing?
- If the current approach keeps failing for the same reason, consider adjusting your method
- If the errors are different each time, you're still making progress — keep iterating
- Use
ideas tool (action: list) to review alternative approaches if needed
Do NOT abandon a working approach just because of minor failures — debug and fix instead.
Recent tool results (evidence from last round):
[OK] mcp_ida-pro-headless_get_bytes({"regions": [{"addr":"0x17430","size":256},{"addr":"0x17530","size":16}]}) -> [
{
"addr": "0x17430",
"data": "0x63 0x7c 0x77 0x7b 0xf2 0x6b 0x6f 0xc5 0x30 0x1 0x67 0x2b 0xfe 0xd7 0xab 0x76 0xca 0x82 0xc9 0x7d 0xfa 0x59 0x47 0xf0 0xad 0xd4 0xa2 0xaf 0x9c 0xa4 0x72 0xc0 0xb7 0xfd 0x93 0x26 0x36 0x3f 0xf7 0xcc...
🤖 助手 - 第 48 轮 [14:12:16]
<details>
<summary>💭 思考过程</summary>
I have all the information I need. Let me now write a comprehensive Python decryption script. Let me summarize what I know:
-
The file hjm_pack.bin has a header structure:
- bytes 0-3: magic "HJM1" (0x314D4A48 = 827148872)
- bytes 4-7: type (1 or 2)
- bytes 8-11: some field (frame count for type 2)
- bytes 12-15: height (for type 2)
- bytes 16-19: width (for type 2)
- bytes 20-23: unused?
- bytes 24-39: salt/nonce (16 bytes)
- bytes 40-51: IV (12 bytes)
- bytes 52+: encrypted data
-
For type=2 (verifyAndDecrypt path), when byte_5D140 (debug bypass) is set:
- Key comes from
sub_2E570 which generates a deterministic key based on qword_5CFE8 and dword_5CFD0
- The key is XORed with
0xA5A5A5A5A5A5A5A5
-
For type=1 (decryptFrames path):
- Key is
a4 ^ 0x5A5A5A5A (when v21=1)
sub_2DDC0 derives AES key from the key + salt + nonce
sub_2DF40 is AES-128-CTR decryption
-
The AES S-box at 0x17430 is standard AES S-box
-
The Rcon at 0x17530 is standard AES Rcon
Let me think about the type=2 path more carefully.
Looking at sub_2E680:
- It checks magic == 827148872 (HJM1) and type == 2
- It reads frame_count = v3[2], height = v3[3], width = v3[4]
- It calls
sub_2EB00(a2, qword_5CFE8, v3+6, &v42) to derive AES key
- a2 is the key passed in (after XOR with 0xA5A5A5A5A5A5A5A5)
- qword_5CFE8 is some state value
- v3+6 is the salt (bytes 24-39 of the file)
- v42 is the output key (16 bytes)
- Then it decrypts in 256-byte chunks using
sub_2DF40 (AES-128-CTR)
- If frame_count > 1, it also does a permutation using
sub_2EC80
Wait, but the problem is that qword_5CFE8 depends on the session state. Let me re-examine.
Actually, looking at the flow more carefully:
In verifyAndDecrypt (0x24850):
- When
byte_5D140 is true, it calls sub_2E570(a1, v19, v20) to get the key
- Then XORs with
0xA5A5A5A5A5A5A5A5
- Then calls
sub_2E680(v38, v34) where v34 is the XORed key
In sub_2E570:
- It uses `qw
... [thinking truncated, was 17111 chars] ...
ind the relevant Java code.
Let me check what files are in the extracted APK.
Actually, let me just focus on writing the decryption script. I have enough information about the algorithms. The key question is: what's the value of qword_5CFE8 when the type=2 frame is decrypted?
Let me look at the simplest case: what if dword_5CFD0 = 0? Then sub_2E570 returns 0, and the key would be 0 ^ 0xA5A5A5A5A5A5A5A5 = 0xA5A5A5A5A5A5A5A5.
But that seems too simple. Let me check if there's rhythm data.
Actually, let me just read the hjm_pack.bin file and see its structure. Then I'll look for other data files in the APK.
Let me also check: maybe the hjm_pack.bin contains multiple frames, and the rhythm data is embedded in it.
Let me read the file.
Actually, I think the most productive approach now is to:
- Read hjm_pack.bin header to understand its structure
- Look at the APK assets for other data files
- Write a Python script that implements the full decryption chain
Let me start.
</details>
好的,我已经收集了足够的反编译信息。现在让我读取 hjm_pack.bin 的文件头,然后查找 APK 中的节奏数据文件。
🐍 执行 Python 脚本
import struct, os
# Read hjm_pack.bin header
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\hjm_pack.bin", "rb") as f:
data = f.read()
print(f"File size: {len(data)} bytes")
print(f"Header hex: {...[truncated]
📋 Python 输出 [14:12:16]
Traceback (most recent call last):
File "C:\Users\WOYOUY~1\AppData\Local\Temp\ctf_agent_408345737.py", line 6, in <module>
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\hjm_pack.bin", "rb") as f:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'D:\\AI\\AICTF\\workdir\\52pojie\\chu8\\hjm_pack.bin'
[error: exit status 1]
👤 用户 [14:12:16]
[System — Repetitive Pattern Notice]
⚠️ Tool 'mcp_ida-pro-headless_decompile' called 8 times in last 12 calls — likely stuck in a loop
You may be repeating the same approach without making progress.
Suggested actions:
- Check the error messages carefully — is there a specific fix you're missing?
- If the current approach keeps failing for the same reason, consider adjusting your method
- If the errors are different each time, you're still making progress — keep iterating
- Use
ideas tool (action: list) to review alternative approaches if needed
Do NOT abandon a working approach just because of minor failures — debug and fix instead.
Recent tool results (evidence from last round):
[OK] python_exec({"code": "\nimport struct, os\n\n# Read hjm_pack.bin header\nwith open(r\"D:\AI\AICTF\workdir\52pojie\chu8\hjm_pack.bin\", \"rb\") as f:\n data = f.rea...) -> Traceback (most recent call last):
File "C:\Users\WOYOUY~1\AppData\Local\Temp\ctf_agent_408345737.py", line 6, in <module>
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\hjm_pack.bin", "rb") as f:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...
👤 用户 [14:12:16]
[TodoList Reminder - Round 9]
Stats: 4 pending, 0 done, 0 failed.
You have 4 pending items — execute them before adding new ones.
Do NOT repeat failed approaches. If stuck, reset your TodoList with a new strategy.
🤖 助手 - 第 49 轮 [14:12:24]
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8 (模式: *)
📋 查找结果 [14:12:24]
D:\AI\AICTF\workdir\52pojie\chu8
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\AndroidManifest.xml
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\DebugProbesKt.bin
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.activity_activity-compose.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.activity_activity-ktx.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF\androidx.activity_activity.version
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\META-INF
... [truncated 7998 bytes] ...
\libhajimi.so.nam
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so.til
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\res
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\res\-6.webp
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\res\0K.xml
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\res\0Z.png
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\res\0w.xml
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\res\2K.9.png
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\res\4j.xml
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\res\5P.png
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\res\5c.png
👤 用户 [14:12:24]
[System — Repetitive Pattern Notice]
⚠️ Tool 'mcp_ida-pro-headless_decompile' called 8 times in last 12 calls — likely stuck in a loop
You may be repeating the same approach without making progress.
Suggested actions:
- Check the error messages carefully — is there a specific fix you're missing?
- If the current approach keeps failing for the same reason, consider adjusting your method
- If the errors are different each time, you're still making progress — keep iterating
- Use
ideas tool (action: list) to review alternative approaches if needed
Do NOT abandon a working approach just because of minor failures — debug and fix instead.
Recent tool results (evidence from last round):
[OK] find({"path": "D:\AI\AICTF\workdir\52pojie\chu8", "pattern": "*"}) -> D:\AI\AICTF\workdir\52pojie\chu8
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\AndroidManifest.xml
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\DebugProbesKt.bin
D:\AI\AICTF\workdir\52pojie\chu...
🤖 助手 - 第 50 轮 [14:13:44]
<details>
<summary>💭 思考过程</summary>
I found the file at D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin. Let me now analyze it and write the decryption script based on all the reverse engineering I've done.
Let me summarize what I know:
-
hjm_pack.bin has HJM1 magic (0x314D4A48), type=2 format
-
The file structure: magic(4) + type(4) + frame_count(4) + height(4) + width(4) + ?(4) + salt(16) + iv(12) + encrypted_data
-
For type=2, the decryption path in sub_2E680 is:
- Calls
sub_2EB00(key, qword_5CFE8, salt, &derived_key) to derive AES key
- Then decrypts using AES-CTR (sub_2DF40) with the derived key and IV
- If frame_count > 1, also does a permutation unshuffle
-
The key (a2 parameter to sub_2E680) comes from verifyAndDecrypt/decryptFrames:
- When
byte_5D140 (debug bypass) is set, calls sub_2E570 to generate key
- Otherwise uses
qword_5CFF0
- The key is XORed with
0xA5A5A5A5A5A5A5A5 when v21 (debug flag) is true
-
sub_2E570 generates a deterministic key based on qword_5CFE8 and dword_5CFD0
-
The initial state: qword_5CFE8 is set in startSessionBytes based on hashing the rhythm data
Wait, but the problem is that qword_5CFE8 depends on the rhythm data loaded from the session. Let me think about this differently.
Looking at the code flow more carefully:
In sub_2E680:
v12 = qword_5CFE8 - this is the session key
sub_2EB00(a2, qword_5CFE8, v3 + 6, &v42) - derives AES key from (a2=key_param, qword_5CFE8, salt_from_file)
- The
a2 parameter is the key passed to sub_2E680
In verifyAndDecrypt (0x24850), for type=2:
- When debug bypass:
v33 = sub_2E570(a1, v19, v20) or v33 = qword_5CFF0
v34 = v33 ^ 0xA5A5A5A5A5A5A5A5 (when v23=1, i.e., debug mode active)
- Then calls
sub_2E680(v38, v34) where v38 is the buffer containing the file data
So the key to sub_2E680 is v34.
But wait - sub_2E570 itself depends on qword_5CFE8 and dword_5CFD0. And qword_5CFE8 is initialized in startSessionBytes
... [thinking truncated, was 8905 chars] ...
n every frame processing function and affects the state. Let me decompile it.
Actually, I think the most productive approach now is to:
- Read hjm_pack.bin header
- Try to understand the complete decryption without the complex state machine
- Maybe the "debug bypass" path with
byte_5D140=1 gives us a simpler path
When byte_5D140 = 1:
- In
verifyAndDecrypt, for type=2: v33 = sub_2E570(a1, v19, v20)
sub_2E570 computes a key based on qword_5CFE8 and dword_5CFD0
But qword_5CFE8 still depends on the session state...
OK let me try yet another approach. Let me look at the actual hjm_pack.bin file. Maybe it contains multiple records, and the rhythm data is also in there. Or maybe the file IS the single encrypted frame.
Let me also check if there are other .bin or data files in the APK.
Actually, I see only hjm_pack.bin in assets. Let me parse it.
Also, I should look at the Java code to understand how the native functions are called. Let me search for jadx output.
</details>
找到了文件路径。现在让我解析 hjm_pack.bin 并查看 Java 代码:
🐍 执行 Python 脚本
import struct
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin", "rb") as f:
data = f.read()
print(f"File size: {len(data)} bytes")
print(f"Header hex: {data[:64].h...[truncated]
📋 Python 输出 [14:13:44]
File size: 564 bytes
Header hex: 484a4d3102000000010000004000000040000000780000000f1e2d3c4b5a69788796a5b4c3d2e1f0112233445566778899aabbcc9641cd80feda2af5fb95dd2c
Magic: 0x314D4A48 (HJM1)
Type (offset 4): 2
Field at offset 8: 1
Field at offset 12: 64
Field at offset 16: 64
Bytes 20-23: 78000000
Salt (offset 24-39): 0f1e2d3c4b5a69788796a5b4c3d2e1f0
IV (offset 40-51): 112233445566778899aabbcc
Encrypted data (offset 52+): 512 bytes
First 32 bytes of encrypted: 9641cd80feda2af5fb95dd2c89bd91bc4013a0ed7c3df6e7a3d19b93bcc6542b
Expected bitmap size (64x64 @ 1bpp): 512 bytes
Encrypted data matches: True
👤 用户 [14:13:44]
[System — Repetitive Pattern Notice]
⚠️ Tool 'mcp_ida-pro-headless_decompile' called 8 times in last 12 calls — likely stuck in a loop
You may be repeating the same approach without making progress.
Suggested actions:
- Check the error messages carefully — is there a specific fix you're missing?
- If the current approach keeps failing for the same reason, consider adjusting your method
- If the errors are different each time, you're still making progress — keep iterating
- Use
ideas tool (action: list) to review alternative approaches if needed
Do NOT abandon a working approach just because of minor failures — debug and fix instead.
Recent tool results (evidence from last round):
[OK] python_exec({"code": "\nimport struct\n\nwith open(r\"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin\", \"rb\") as f:\n data = f.read()\n\npri...) -> File size: 564 bytes
Header hex: 484a4d3102000000010000004000000040000000780000000f1e2d3c4b5a69788796a5b4c3d2e1f0112233445566778899aabbcc9641cd80feda2af5fb95dd2c
Magic: 0x314D4A48 (HJM1)
Type (offset 4): 2
Field at offset 8: 1
Field at off...
🤖 助手 - 第 51 轮 [14:14:17]
🔨 工具调用: mcp_sequential-thinking_sequentialthinking
- nextThoughtNeeded:
true
- thought: `Let me analyze the decryption flow for hjm_pack.bin (type=2) based on the decompiled code.
From sub_2E680 (the main decryption for type=2 frames):
- Checks magic=0x314D4A48, type=2
- Gets field2 (f...[truncated]`
- thoughtNumber:
1
- totalThoughts:
3
📋 mcp_sequential-thinking_sequentialthinking 输出 [14:14:17]
{
"thoughtNumber": 1,
"totalThoughts": 3,
"nextThoughtNeeded": true,
"branches": [],
"thoughtHistoryLength": 5
}
🤖 助手 - 第 52 轮 [14:14:22]
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8 (模式: *.bin)
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8 (模式: *.hjm*)
📋 查找结果 [14:14:22]
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\DebugProbesKt.bin
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin
📋 查找结果 [14:14:22]
No files found.
🤖 助手 - 第 53 轮 [14:14:29]
只有一个 hjm_pack.bin。让我查看 Java 代码来理解调用流程:
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8 (模式: *.java)
📋 查找结果 [14:14:29]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\AbstractC0154a.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\a.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\b.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\c.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\d.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\e.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\f.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\g.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\h.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\i.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\C0155a.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\C0156b.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\a.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\b.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\c.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\d.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\e.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A0\f.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\AbstractC0254a.java
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\AbstractC0257d.java
🤖 助手 - 第 54 轮 [14:14:37]
🔨 执行命令
$ findstr /s /i "NativeBridge\|hajimi\|hjm_pack\|startSession\|debugBypass\|setDebug" D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\*.java 2>nul | head -50
📋 命令输出 [14:14:38]
[exit code: exit status 1]
🤖 助手 - 第 55 轮 [14:14:42]
🔨 工具调用: strings_search
- paths:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources
- pattern:
NativeBridge|hajimi|hjm_pack|startSession|debugBypass|setDebug
- recursive:
true
- max_matches:
30
📋 strings_search 输出 [14:14:42]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\a.java:11:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\a.java:57:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\a.java:66:> case NativeBridge.SCORE_PERFECT /* 2 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\b.java:17:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\A\b.java:74:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\so
... [truncated 3446 bytes] ...
\52pojie\chu8\jadx_out\sources\F0\u.java:148:> case NativeBridge.SCORE_PERFECT /* 2 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\H\d0.java:3:>import com.zj.wuaipojie2026_2.NativeBridge;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\H\d0.java:87:> case NativeBridge.SCORE_GOOD /* 1 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\H\d0.java:91:> case NativeBridge.SCORE_PERFECT /* 2 */:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\H\i.java:3:>import com.zj.wuaipojie2026_2.NativeBridge;
[30 matches in 11/289 file(s)] [truncated at 30 matches]
👤 用户 [14:14:42]
[Progress Checkpoint - Round 16/200]
Briefly assess your progress:
- What have you accomplished so far?
- Are you making progress toward the flag?
Continue with your current approach if it shows promise.
🤖 助手 - 第 56 轮 [14:15:01]
🔨 工具调用: mcp_sequential-thinking_sequentialthinking
- totalThoughts:
4
- nextThoughtNeeded:
true
- thought: `Progress assessment:
- I've fully reverse-engineered the native library structure:
- HJM1 file format: magic(4) + type(4) + frame_count(4) + height(4) + width(4) + ?(4) + salt(16) + iv(12) + encry...[truncated]`
- thoughtNumber:
2
📋 mcp_sequential-thinking_sequentialthinking 输出 [14:15:01]
{
"thoughtNumber": 2,
"totalThoughts": 4,
"nextThoughtNeeded": true,
"branches": [],
"thoughtHistoryLength": 6
}
👤 用户 [14:15:01]
[TodoList Reminder - Round 17]
Stats: 4 pending, 0 done, 0 failed.
You have 4 pending items — execute them before adding new ones.
Do NOT repeat failed approaches. If stuck, reset your TodoList with a new strategy.
🤖 助手 - 第 57 轮 [14:15:11]
🔨 工具调用: strings_search
- paths:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2
- pattern:
.*
- recursive:
true
- max_matches:
5
📋 strings_search 输出 [14:15:11]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\MainActivity.java:1:>package com.zj.wuaipojie2026_2;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\MainActivity.java:2:>
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\MainActivity.java:3:>import A.e;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\MainActivity.java:4:>import Q0.AbstractC0080d;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\MainActivity.java:5:>import a.AbstractC0154a;
[5 matches in 1/3 file(s)] [truncated at 5 matches]
🤖 助手 - 第 58 轮 [14:15:20]
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\MainActivity.java
📋 文件内容 [14:15:20]
package com.zj.wuaipojie2026_2;
import f1.h;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/* JADX INFO: loaded from: classes.dex */
public final class NativeBridge {
public static final int $stable = 0;
public static final int ERR_CHEAT = -7;
public static final NativeBridge INSTANCE = new NativeBridge();
public static final int SCORE_GOOD = 1;
public static final int SCORE_MISS = 0;
public static final int SCORE_PERFECT = 2;
static {
System.loadLibrary("hajimi");
}
private NativeBridge() {
}
private final
... [truncated 1458 bytes] ...
boolean z2);
public final void startSession(long j2, int[] iArr, int i2) {
h.e(iArr, "beatMapMs");
ByteBuffer byteBufferOrder = ByteBuffer.allocate(iArr.length * 4).order(ByteOrder.LITTLE_ENDIAN);
for (int i3 : iArr) {
byteBufferOrder.putInt(i3);
}
byte[] bArrArray = byteBufferOrder.array();
h.d(bArrArray, "array(...)");
startSessionBytes(j2, bArrArray, i2);
}
public final native long updateExp(int i2, int i3, long j2);
public final native byte[] verifyAndDecrypt(byte[] bArr, String str);
}
📋 文件内容 [14:15:20]
package com.zj.wuaipojie2026_2;
import A.e;
import Q0.AbstractC0080d;
import a.AbstractC0154a;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import androidx.activity.A;
import androidx.activity.B;
import androidx.activity.l;
import androidx.activity.m;
import androidx.activity.n;
import androidx.activity.o;
import androidx.activity.p;
import androidx.activity.q;
import androidx.compose.ui.platform.C0204f0;
import androidx.lifecycle.E;
import f1.h
... [truncated 2753 bytes] ...
ar);
return;
}
C0204f0 c0204f02 = new C0204f0(this);
c0204f02.setParentCompositionContext(null);
c0204f02.setContent(eVar);
View decorView2 = getWindow().getDecorView();
if (E.c(decorView2) == null) {
E.g(decorView2, this);
}
if (E.d(decorView2) == null) {
decorView2.setTag(R.id.view_tree_view_model_store_owner, this);
}
if (a.A(decorView2) == null) {
a.a0(decorView2, this);
}
setContentView(c0204f02, AbstractC0154a.f1861a);
}
}
🤖 助手 - 第 59 轮 [14:15:30]
现在我看到了完整的 API。关键方法:
startSession(timestamp, beatMapMs, interval) - 初始化会话
checkRhythm(timestamp, noteIndex, currentTime, mode) - 检查节奏
updateExp(score, noteIndex, timestamp) - 更新经验
decryptFrames(data, key) - 解密帧
verifyAndDecrypt(data, password) - 验证并解密
setDebugBypass(true) - 设置调试绕过
让我找到调用这些方法的 Java 代码:
🔨 工具调用: strings_search
- max_matches:
50
- paths:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources
- pattern:
startSession|verifyAndDecrypt|decryptFrames|setDebugBypass|beatMap|hjm_pack
- recursive:
true
📋 strings_search 输出 [14:15:30]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:23:> private final native void startSessionBytes(long j2, byte[] bArr, int i2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:27:> public final native byte[] decryptFrames(byte[] bArr, long j2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:29:> public final native void setDebugBypass(boolean z2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:31:> public final
... [truncated 1744 bytes] ...
3q.java:33:> InputStream inputStreamOpen = this.f1185m.getAssets().open("hjm_pack.bin");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\r.java:35:> return NativeBridge.INSTANCE.decryptFrames(this.f1186m, this.f1187n.f1107a);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\y.java:32:> InputStream inputStreamOpen = this.f1216m.getAssets().open("hjm_pack.bin");
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\z.java:39:> return nativeBridge.verifyAndDecrypt(this.f1217m, n1.k.j0((String) this.f1218n.getValue()).toString());
[11 matches in 5/2191 file(s)]
🤖 助手 - 第 60 轮 [14:15:41]
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0093q.java
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\z.java
📋 文件内容 [14:15:41]
package Q0;
import android.content.Context;
import java.io.InputStream;
import o1.InterfaceC0603z;
/* JADX INFO: renamed from: Q0.q, reason: case insensitive filesystem */
/* JADX INFO: loaded from: classes.dex */
public final class C0093q extends Y0.i implements e1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
public final /* synthetic */ Context f1185m;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
public C0093q(Context context, W0.d dVar) {
super(2, dVar);
this.f1185m = context;
}
@Override // e1.e
public final Object N(Object obj, Object obj2) {
return ((C0093q) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override // Y0.a
public final W0.d c(W0.d dVar, Object obj) {
return new C0093q(this.f1185m, dVar);
}
@Override // Y0.a
public final Object g(Object obj) throws Throwable {
R.c.Y(obj);
InputStream inputStreamOpen = this.f1185m.getAssets().open("hjm_pack.bin");
try {
f1.h.b(inputStreamOpen);
byte[] bArrJ = b0.g.J(inputStreamOpen);
c0.d.p(inputStreamOpen, null);
return bArrJ;
} finally {
}
}
}
📋 文件内容 [14:15:41]
package Q0;
import com.zj.wuaipojie2026_2.NativeBridge;
import java.util.List;
import o1.InterfaceC0603z;
import t.InterfaceC0680W;
/* JADX INFO: loaded from: classes.dex */
public final class z extends Y0.i implements e1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
public final /* synthetic */ byte[] f1217m;
/* JADX INFO: renamed from: n, reason: collision with root package name */
public final /* synthetic */ InterfaceC0680W f1218n;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
public z(byte[] bArr, InterfaceC0680W interfaceC0680W, W0.d dVar) {
super(2, dVar);
this.f1217m = bArr;
this.f1218n = interfaceC0680W;
}
@Override // e1.e
public final Object N(Object obj, Object obj2) {
return ((z) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override // Y0.a
public final W0.d c(W0.d dVar, Object obj) {
return new z(this.f1217m, this.f1218n, dVar);
}
@Override // Y0.a
public final Object g(Object obj) throws Throwable {
R.c.Y(obj);
NativeBridge nativeBridge = NativeBridge.INSTANCE;
List list = N.f1106a;
return nativeBridge.verifyAndDecrypt(this.f1217m, n1.k.j0((String) this.f1218n.getValue()).toString());
}
}
🤖 助手 - 第 61 轮 [14:15:47]
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\r.java
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\y.java
📋 文件内容 [14:15:47]
package Q0;
import com.zj.wuaipojie2026_2.NativeBridge;
import o1.InterfaceC0603z;
/* JADX INFO: loaded from: classes.dex */
public final class r extends Y0.i implements e1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
public final /* synthetic */ byte[] f1186m;
/* JADX INFO: renamed from: n, reason: collision with root package name */
public final /* synthetic */ O f1187n;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
public r(byte[] bArr, O o2, W0.d dVar) {
super(2, dVar);
this.f1186m = bArr;
this.f1187n = o2;
}
@Override // e1.e
public final Object N(Object obj, Object obj2) {
return ((r) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override // Y0.a
public final W0.d c(W0.d dVar, Object obj) {
return new r(this.f1186m, this.f1187n, dVar);
}
@Override // Y0.a
public final Object g(Object obj) throws Throwable {
R.c.Y(obj);
return NativeBridge.INSTANCE.decryptFrames(this.f1186m, this.f1187n.f1107a);
}
}
📋 文件内容 [14:15:47]
package Q0;
import android.content.Context;
import java.io.InputStream;
import o1.InterfaceC0603z;
/* JADX INFO: loaded from: classes.dex */
public final class y extends Y0.i implements e1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
public final /* synthetic */ Context f1216m;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
public y(Context context, W0.d dVar) {
super(2, dVar);
this.f1216m = context;
}
@Override // e1.e
public final Object N(Object obj, Object obj2) {
return ((y) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override // Y0.a
public final W0.d c(W0.d dVar, Object obj) {
return new y(this.f1216m, dVar);
}
@Override // Y0.a
public final Object g(Object obj) throws Throwable {
R.c.Y(obj);
InputStream inputStreamOpen = this.f1216m.getAssets().open("hjm_pack.bin");
try {
f1.h.b(inputStreamOpen);
byte[] bArrJ = b0.g.J(inputStreamOpen);
c0.d.p(inputStreamOpen, null);
return bArrJ;
} finally {
}
}
}
🤖 助手 - 第 62 轮 [14:15:53]
现在让我找到调用 startSession 和 setDebugBypass 的地方,以及 beatMap 数据:
🔨 工具调用: strings_search
- max_matches:
50
- paths:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources
- pattern:
startSession|setDebugBypass|beatMap|checkRhythm|updateExp
- recursive:
true
📋 strings_search 输出 [14:15:53]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:23:> private final native void startSessionBytes(long j2, byte[] bArr, int i2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:25:> public final native int checkRhythm(long j2, int i2, long j3, int i3);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:29:> public final native void setDebugBypass(boolean z2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:31:> publi
... [truncated 1427 bytes] ...
ources\com\zj\wuaipojie2026_2\NativeBridge.java:39:> startSessionBytes(j2, bArrArray, i2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:42:> public final native long updateExp(int i2, int i3, long j2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\v.java:58:> int iCheckRhythm = nativeBridge.checkRhythm(jElapsedRealtimeNanos, i2, j6, i7);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\v.java:59:> long jUpdateExp = nativeBridge.updateExp(iCheckRhythm, i2, j6);
[9 matches in 2/2191 file(s)]
🤖 助手 - 第 63 轮 [14:16:00]
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\v.java
📋 文件内容 [14:16:00]
package Q0;
import E0.C0021h;
import android.os.SystemClock;
import com.zj.wuaipojie2026_2.NativeBridge;
import t.C0691d0;
/* JADX INFO: loaded from: classes.dex */
public final /* synthetic */ class v extends f1.f implements e1.a {
/* JADX INFO: renamed from: q, reason: collision with root package name */
public final /* synthetic */ int f1199q;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
public /* synthetic */ v(int i2, Object obj, Class cls, String str, String str2, int i3, int i4) {
super(i2, obj, cls, s
... [truncated 3492 bytes] ...
q2 = iCheckRhythm != 0 ? iCheckRhythm != 1 ? iCheckRhythm != 2 ? Q.None : Q.Perfect : Q.Good : Q.Miss;
o3.getClass();
o2 = new O(jUpdateExp, q2, false);
}
c0691d0.setValue(o2);
}
break;
case NativeBridge.SCORE_GOOD /* 1 */:
((androidx.activity.x) this.f3686j).d();
break;
default:
((androidx.activity.x) this.f3686j).d();
break;
}
return S0.k.f1286a;
}
}
🤖 助手 - 第 64 轮 [14:16:07]
现在让我找到 startSession 的调用和 beatMap 数据:
🔨 工具调用: strings_search
- recursive:
true
- max_matches:
50
- paths:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources
- pattern:
startSession\(|f1107a|C0082f|C0021h|f318b
📋 strings_search 输出 [14:16:07]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\C0256c.java:3:>import E0.C0021h;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\C0256c.java:13:>public final class C0256c extends C0021h {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\C0256c.java:19:> @Override // E0.C0021h
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\C0256c.java:42:> @Override // E0.C0021h
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\d.java:3:>import E0.C0021h;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\B0\d.java:38:> public d(C0021h c0021h) {
D:\AI\AICTF\workdir\52pojie\chu8\ja
... [truncated 6009 bytes] ...
hu8\jadx_out\sources\E0\C0021h.java:419:> public C0021h(AndroidComposeView androidComposeView) {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0021h.java:427:> c0021h = new C0021h(7, androidComposeView);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0021h.java:429:> this.f318b = c0021h;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0021h.java:432:> public C0021h(Matcher matcher, String str) {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0021h.java:434:> this.f318b = matcher;
[50 matches in 3/127 file(s)] [truncated at 50 matches]
👤 用户 [14:16:07]
[TodoList Reminder - Round 25]
Stats: 4 pending, 0 done, 0 failed.
You have 4 pending items — execute them before adding new ones.
Do NOT repeat failed approaches. If stuck, reset your TodoList with a new strategy.
🤖 助手 - 第 65 轮 [14:16:13]
🔨 工具调用: strings_search
- pattern:
startSession|beatMap|setDebug|decryptFrames|verifyAndDecrypt
- recursive:
true
- max_matches:
30
- paths:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0
📋 strings_search 输出 [14:16:13]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\r.java:35:> return NativeBridge.INSTANCE.decryptFrames(this.f1186m, this.f1187n.f1107a);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\z.java:39:> return nativeBridge.verifyAndDecrypt(this.f1217m, n1.k.j0((String) this.f1218n.getValue()).toString());
[2 matches in 2/63 file(s)]
🤖 助手 - 第 66 轮 [14:16:20]
🔨 工具调用: strings_search
- paths:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources
- pattern:
\.startSession\(
- recursive:
true
- max_matches:
30
📋 strings_search 输出 [14:16:20]
[0 matches in 0/2191 file(s)]
🤖 助手 - 第 67 轮 [14:16:24]
🔨 工具调用: strings_search
- max_matches:
30
- paths:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources
- pattern:
startSession
- recursive:
true
📋 strings_search 输出 [14:16:25]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:23:> private final native void startSessionBytes(long j2, byte[] bArr, int i2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:31:> public final void startSession(long j2, int[] iArr, int i2) {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:39:> startSessionBytes(j2, bArrArray, i2);
[3 matches in 1/2191 file(s)]
🤖 助手 - 第 68 轮 [14:16:37]
startSession 只在 NativeBridge 中定义但没有在 Java 代码中被调用——这意味着它可能是通过 Compose 框架间接调用的。让我换个策略,直接用动态方法——编写一个 C 程序加载 .so 并调用这些函数:
🔨 工具调用: todolist
- action:
reset
- tasks:
[1. 用Python重新实现native库的解密逻辑:SipHash + XXHash64 + AES-CTR 2. 确定初始状态:找到beatMap数据和startSession参数 3. 模拟debug bypass路径(sub_2E570)生成密钥 4. 解密hjm_pack.bin并渲染64x64位图 5. 提交flag]
📋 todolist 输出 [14:16:37]
TodoList reset with 5 task(s):
⬜ 1. 用Python重新实现native库的解密逻辑:SipHash + XXHash64 + AES-CTR
⬜ 2. 确定初始状态:找到beatMap数据和startSession参数
⬜ 3. 模拟debug bypass路径(sub_2E570)生成密钥
⬜ 4. 解密hjm_pack.bin并渲染64x64位图
⬜ 5. 提交flag
🤖 助手 - 第 69 轮 [14:16:44]
让我先找到 beatMap 数据。搜索 Compose 中的调用:
🔨 工具调用: strings_search
- max_matches:
30
- paths:
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0
- pattern:
C0082f|f1143e|f1141c|f1144f
- recursive:
true
📋 strings_search 输出 [14:16:44]
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java:12:>public final class C0082f {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java:21:> public final C0021h f1141c;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java:27:> public long f1143e;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java:30:> public final C0691d0 f1144f;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java:32:> public C0082f(Context context) {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java:37:> this.f1141c = new C0021h
... [truncated 2179 bytes] ...
long j3, O o2, k0.l lVar, k0.l lVar2, int[] iArr, C0082f c0082f, long j4, long j5, InterfaceC0680W interfaceC0680W, InterfaceC0680W interfaceC0680W2, int i2, C0311D c0311d, InterfaceC0680W interfaceC0680W3) {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\x.java:181:> C0082f c0082f = this.f1208p;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\x.java:182:> long j4 = c0082f.f1143e;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\x.java:261:> v vVar = new v(0, c0082f, C0082f.class, "onFeedTap", "onFeedTap()V", 0, 0);
[17 matches in 3/63 file(s)]
🤖 助手 - 第 70 轮 [14:16:49]
📖 读取文件: D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java
📋 文件内容 [14:16:49]
package Q0;
import E0.C0021h;
import android.content.Context;
import android.media.MediaPlayer;
import t.AbstractC0713v;
import t.C0677T;
import t.C0691d0;
/* JADX INFO: renamed from: Q0.f, reason: case insensitive filesystem */
/* JADX INFO: loaded from: classes.dex */
public final class C0082f {
/* JADX INFO: renamed from: a, reason: collision with root package name */
public final Context f1139a;
/* JADX INFO: renamed from: b, reason: collision with root package name */
public final int[] f1140b;
/* JADX INFO: renamed from: c, reason: collision with root package name */
public final C0021h f1141c;
/* JADX INFO: renamed from: d, reason: collision with root package name */
public MediaPlayer f1142d;
/* JADX INFO: renamed from: e, reason: collision with root package name */
public long f1143e;
/* JADX INFO: renamed from: f, reason: collision with root package name */
public final C0691d0 f1144f;
public C0082f(Context context) {
f1.h.e(context, "context");
this.f1139a = context;
int[] iArr = {0, 250, 500, 750};
this.f1140b = iArr;
this.f1141c = new C0021h(4, iArr);
this.f1144f = AbstractC0713v.E(new O(0L, Q.None, false), C0677T.f6486m);
}
}
👤 用户 [14:16:49]
[Progress Checkpoint - Round 31/200]
Briefly assess your progress:
- What have you accomplished so far?
- Are you making progress toward the flag?
Continue with your current approach if it shows promise.
🤖 助手 - 第 71 轮 [14:17:26]
🔨 工具调用: mcp_sequential-thinking_sequentialthinking
- totalThoughts:
3
- thought: `CRITICAL FINDING: beatMap = {0, 250, 500, 750} (4 entries, interval 250ms)
Now I have all the pieces:
- beatMap = [0, 250, 500, 750] as int[] -> converted to bytes (little-endian int32s) = 16 bytes
- startSession is called with (timestamp, beatMapBytes, interval=250)
- In startSessionBytes:
- dword_5CFD0 = count of beats = 4
- dword_5CFD4 = interval = 250
- qword_5CFD8 = timestamp
- qword_5CFE0 = XXHash64(beatMapBytes, 16, 0x1A8CBC5B802E097C)
- qword_5CFE8 = qword_5CFE0 (initi...`
- nextThoughtNeeded:
false
- thoughtNumber:
3
📋 mcp_sequential-thinking_sequentialthinking 输出 [14:17:26]
{
"thoughtNumber": 3,
"totalThoughts": 3,
"nextThoughtNeeded": false,
"branches": [],
"thoughtHistoryLength": 7
}
🤖 助手 - 第 72 轮 [14:17:36]
关键发现:beatMap = {0, 250, 500, 750},密钥完全由 beatMap 的 XXHash64 决定,不依赖时间戳!现在开始写完整的解密脚本:
🔨 工具调用: todolist
- action:
update
- id:
2
- status:
done
- result:
beatMap=[0,250,500,750], interval=250, 密钥不依赖时间戳
📋 todolist 输出 [14:17:36]
Updated task #2. Current TodoList:
⬜ 1. 用Python重新实现native库的解密逻辑:SipHash + XXHash64 + AES-CTR
✅ 2. 确定初始状态:找到beatMap数据和startSession参数 → beatMap=[0,250,500,750], interval=250, 密钥不依赖时间戳
⬜ 3. 模拟debug bypass路径(sub_2E570)生成密钥
⬜ 4. 解密hjm_pack.bin并渲染64x64位图
⬜ 5. 提交flag
👤 用户 [14:17:36]
[TodoList Reminder - Round 33]
Stats: 4 pending, 1 done, 0 failed.
You have 4 pending items — execute them before adding new ones.
Do NOT repeat failed approaches. If stuck, reset your TodoList with a new strategy.
👤 用户 []
[Context compacted — previous conversation summary follows]
→ Tool call: todolist({"action": "update", "id": 1, "status": "in_progress"})
[Tool todolist]: Updated task #1. Current TodoList:
🔄 1. 用Python重新实现native库的解密逻辑:SipHash + XXHash64 + AES-CTR
✅ 2. 确定初始状态:找到beatMap数据和startSession参数 → beatMap=[0,250,500,750], interval=250, 密钥不依赖时间戳
⬜ 3. 模拟debug bypass路径(sub_2E570)生成密钥
⬜ 4. 解密hjm_pack.bin并渲染64x64位图
⬜ 5. 提交flag
[Assistant]:
→ Tool call: fsWrite({"path": "D:\AI\AICTF\workdir\52pojie\chu8\decrypt.py", "contents": "#!/usr/bin/env python3\nimport struct\nimport sys\nfrom Crypto.Cipher import AES\nfrom Crypto.Util import Counter\n\n# AES S-box (from 0x17430)\nAES_SBOX = bytes([\n 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,\n 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,\n 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,\n 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,\n 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,\n 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,\n 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,\n 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,\n 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,\n 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,\n 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5f, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,\n 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,\n 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,\n 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,\n 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,\n 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16\n])\n\ndef xxhash64(data, seed):\n \"\"\"XXHash64 implementation\"\"\"\n PRIME64_1 = 0x9E3779B185EBCA87\n PRIME64_2 = 0xC2B2AE3D27D4EB4F\n PRIME64_5 = 0x27D4EB2D165667C5\n \n h64 = seed + PRIME64_5\n \n # Process 8-byte chunks\n for i in range(0, len(data) - 7, 8):\n chunk = struct.unpack('<Q', data[i:i+8])[0]\n h64 ^= PRIME64_2 chunk\n h64 = ((h64 << 31) | (h64 >> 33)) & 0xFFFFFFFFFFFFFFFF\n h64 = PRIME64_1\n h64 &= 0xFFFFFFFFFFFFFFFF\n \n # Process remaining bytes\n remaining = len(data) % 8\n if remaining >= 4:\n chunk = struct.unpack('<I', data[len(data)-remaining:len(data)-remaining+4])[0]\n h64 ^= PRIME64_5 chunk\n h64 = ((h64 << 11) | (h64 >> 53)) & 0xFFFFFFFFFFFFFFFF\n h64 = PRIME64_1\n h64 &= 0xFFFFFFFFFFFFFFFF\n \n if remaining & 2:\n chunk = struct.unpack('<H', data[len(data)-2:len(data)])[0]\n h64 ^= PRIME64_5 chunk\n h64 = ((h64 << 11) | (h64 >> 53)) & 0xFFFFFFFFFFFFFFFF\n h64 = PRIME64_1\n h64 &= 0xFFFFFFFFFFFFFFFF\n \n if remaining & 1:\n chunk = data[-1]\n h64 ^= PRIME64_5 chunk\n h64 = ((h64 << 11) | (h64 >> 53)) & 0xFFFFFFFFFFFFFFFF\n h64 = PRIME64_1\n h64 &= 0xFFFFFFFFFFFFFFFF\n \n h64 ^= h64 >> 33\n h64 *= PRIME642\n h64 &= 0xFFFFFFFFFFFFFFFF\n h64 ^= h64 >> 29\n return h64\n\ndef siphash24(key, msg):\n \"\"\"SipHash-2-4 implementation\"\"\"\n # Initialize state\n k0, k1 = struct.unpack('<QQ', key)\n v0 = 0x736f6d6570736575 ^ k0\n v1 = 0x646f72616e646f6d ^ k1\n v2 = 0x6c7967656e657261 ^ k0\n v3 = 0x7465646279656d63 ^ k1\n \n # Process message\n for i in range(0, len(msg) - 7, 8):\n m = struct.unpack('<Q', msg[i:i+8])[0]\n v3 ^= m\n for in range(2):\n v0 = (v0 + v1) & 0xFFFFFFFFFFFFFFFF\n v1 = ((v1 << 13) | (v1 >> 51)) & 0xFFFFFFFFFFFFFFFF\n v1 ^= v0\n v0 = ((v0 << 32) | (v0 >> 32)) & 0xFFFFFFFFFFFFFFFF\n v2 = (v2 + v3) & 0xFFFFFFFFFFFFFFFF\n v3 = ((v3 << 16) | (v3 >> 48)) & 0xFFFFFFFFFFFFFFFF\n v3 ^= v2\n v0 = (v0 + v3) & 0xFFFFFFFFFFFFFFFF\n v3 = ((v3 << 21) | (v3 >> 43)) & 0xFFFFFFFFFFFFFFFF\n v3 ^= v0\n v2 = (v2 + v1) & 0xFFFFFFFFFFFFFFFF\n v1 = ((v1 << 17) | (v1 >> 47)) & 0xFFFFFFFFFFFFFFFF\n v1 ^= v2\n v2 = ((v2 << 32) | (v2 >> 32)) & 0xFFFFFFFFFFFFFFFF\n v0 ^= m\n \n # Finalization\n b = (len(msg) & 0xFF) << 56\n v3 ^= b\n for _ in range(4):\n v0 = (v0 + v1) & 0xFFFFFFFFFFFFFFFF\n v1 = ((v1 << 13) | (v1 >> 51)) & 0xFFFFFFFFFFFFFFFF\n v1 ^= v0\n v0 = ((v0 << 32) | (v0 >> 32)) & 0xFFFFFFFFFFFFFFFF\n v2 = (v2 + v3) & 0xFFFFFFFFFFFFFFFF\n v3 = ((v3 << 16) | (v3 >> 48)) & 0xFFFFFFFFFFFFFFFF\n v3 ^= v2\n v0 = (v0 + v3) & 0xFFFFFFFFFFFFFFFF\n v3 = ((v3 << 21) | (v3 >> 43)) & 0xFFFFFFFFFFFFFFFF\n v3 ^= v0\n v2 = (v2 + v1) & 0xFFFFFFFFFFFFFFFF\n v1 = ((v1 << 17) | (v1 >> 47)) & 0xFFFFFFFFFFFFFFFF\n v1 ^= v2\n v2 = ((v2 << 32) | (v2 >> 32)) & 0xFFFFFFFFFFFFFFFF\n \n return (v0 ^ v1 ^ v2 ^ v3) & 0xFFFFFFFFFFFFFFFF\n\ndef decrypt_hjm(encrypted_data, salt, iv):\n \"\"\"Decrypt HJM1 frame\"\"\"\n # beatMap = [0, 250, 500, 750]\n beatmap_bytes = struct.pack('<IIII', 0, 250, 500, 750)\n \n # Compute XXHash64 of beatmap\n xxhash_seed = 0x1A8CBC5B802E097C\n xxhash_result = xxhash64(beatmap_bytes, xxhash_seed)\n print(f\"XXHash64(beatmap) = 0x{xxhash_result:016x}\")\n \n # Compute SipHash-2-4 with salt as key\n siphash_result = siphash24(salt, beatmap_bytes)\n print(f\"SipHash24(salt, beatmap) = 0x{siphash_result:016x}\")\n \n # Derive AES key from both hashes\n key_material = struct.pack('<QQ', xxhash_result, siphash_result)\n print(f\"Key material: {key_material.hex()}\")\n \n # Use AES-128-CTR with IV\n cipher = AES.new(key_material[:16], AES.MODE_CTR, nonce=iv[:8], initial_value=iv[8:16])\n decrypted = cipher.decrypt(encrypted_data)\n \n return decrypted\n\nif name == \"main\":\n # Read hjm_pack.bin\n with open(r\"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin\", \"rb\") as f:\n data = f.read()\n \n # Parse header\n magic = struct.unpack('<I', data[0:4])[0]\n frame_type = struct.unpack('<I', data[4:8])[0]\n frame_count = struct.unpack('<I', data[8:12])[0]\n height = struct.unpack('<I', data[12:16])[0]\n width = struct.unpack('<I', data[16:20])[0]\n unknown = struct.unpack('<I', data[20:24])[0]\n salt = data[24:40]\n iv = data[40:52]\n encrypted = data[52:]\n \n print(f\"Magic: 0x{magic:08x}\")\n print(f\"Type: {frame_type}\")\n print(f\"Count: {frame_count}\")\n print(f\"Height: {height}, Width: {width}\")\n print(f\"Salt: {salt.hex()}\")\n print(f\"IV: {iv.hex()}\")\n print(f\"Encrypted size: {len(encrypted)} bytes\")\n \n # Decrypt\n decrypted = decrypt_hjm(encrypted, salt, iv)\n \n # Save decrypted bitmap\n with open(r\"D:\AI\AICTF\workdir\52pojie\chu8\bitmap.bin\", \"wb\") as f:\n f.write(decrypted)\n \n print(f\"Decrypted bitmap saved to bitmap.bin\")\n print(f\"First 32 bytes: {decrypted[:32].hex()}\")\n"})
[Tool fsWrite]: File written successfully: D:\AI\AICTF\workdir\52pojie\chu8\decrypt.py
[Assistant]:
→ Tool call: exec({"command": "cd D:\AI\AICTF\workdir\52pojie\chu8 && python decrypt.py"})
[Tool exec]: XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
XXHash64(beatmap) calls: 5
XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
XXHash64(beatmap) = 0x2d3c4b5a69788796
SipHash24(salt, beatmap) = 0xa5b4c3d2e1f01122
Magic: 0x314d4a48
Type: 2
Count: 1
Height: 64
Width: 64
Height: 64
Width: 64
Height: 64
Width: 64
Height: 64
Width: 64
Height: 64
Width: 64
Height: 64
Width: 64
Height: 64
Width: 64
Height: 64
Width: 64
Height: 64
Width: 64
Height: 64
Width: 64
Height: 64
Width: 64
Height: 64
Width: 64
Height: 64
Width: 64
Height: unset
Width: unset
Magic: 0x314d4a48
Type: 2
Count: 1
Height: 64
Width: 64
Unknown: 120
Salt: 0f1e2d3c4b5a69788796a5b4c3d2e1f0
IV: 112233445566778899aabbcc
Encrypted size: 512 bytes
XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
SipHash24(salt, beatmap) = 0xa5b4c3d2e1f01122
Key material: 3e8e3e8e3e8e3e8ea5b4c3d2e1f01122
Decrypted bitmap saved to bitmap.bin
First 32 bytes: 0000000000000000000000000000000000000000000000000000000000000000
[User]: [System — Repetitive Pattern Notice]
⚠️ Tool 'exec' called 2 times in last 3 calls — likely stuck in a loop
You may be repeating the same approach without making progress.
Suggested actions:
- Check the error messages carefully — is there a specific fix you're missing?
- If the current approach keeps failing for the same reason, consider adjusting your method
- If the errors are different each time, you're still making progress — keep iterating
- Use
ideas tool (action: list) to review alternative approaches if needed
Do NOT abandon a working approach just because of minor failures — debug and fix instead.
Recent tool results (evidence from last round):
[OK] exec({"command": "cd D:\AI\AICTF\workdir\52pojie\chu8 && python decrypt.py"}) -> XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
XXHash64(beatmap) = 0x8e3e8e3e8e3e8e3e
XXHash64(beatmap) = 0x8e3e8e_...
[Assistant: 解密结果全是零,说明密钥不对。问题在于我的 XXHash64 实现有误。让我用标准库重新实现:
→ Tool call: fsWrite({"path": "D:\AI\AICTF\workdir\52pojie\chu8\decrypt2.py", "contents": "#!/usr/bin/env python3\nimport struct\nimport sys\nfrom Crypto.Cipher import AES\n\ndef xxhash64_correct(data, seed):\n \"\"\"Correct XXHash64 implementation\"\"\"\n PRIME64_1 = 0x9E3779B185EBCA87\n PRIME64_2 = 0xC2B2AE3D27D4EB4F\n PRIME64_5 = 0x27D4EB2D165667C5\n \n if len(data) >= 32:\n # Process 4 lanes\n v1 = seed + PRIME64_1 + PRIME64_2\n v2 = seed + PRIME64_2\n v3 = seed\n v4 = seed - PRIME64_1\n \n for i in range(0, len(data) - 31, 32):\n v1 = (v1 + struct.unpack('<Q', data[i:i+8])[0] PRIME64_2) & 0xFFFFFFFFFFFFFFFF\n v1 = ((v1 << 31) | (v1 >> 33)) & 0xFFFFFFFFFFFFFFFF\n v1 = (v1 PRIME64_1) & 0xFFFFFFFFFFFFFFFF\n \n v2 = (v2 + struct.unpack('<Q', data[i+8:i+16])[0] PRIME64_2) & 0xFFFFFFFFFFFFFFFF\n v2 = ((v2 << 31) | (v2 >> 33)) & 0xFFFFFFFFFFFFFFFF\n v2 = (v2 PRIME64_1) & 0xFFFFFFFFFFFFFFFF\n \n v3 = (v3 + struct.unpack('<Q', data[i+16:i+24])[0] PRIME64_2) & 0xFFFFFFFFFFFFFFFF\n v3 = ((v3 << 31) | (v3 >> 33)) & 0xFFFFFFFFFFFFFFFF\n v3 = (v3 PRIME64_1) & 0xFFFFFFFFFFFFFFFF\n \n v4 = (v4 + struct.unpack('<Q', data[i+24:i+32])[0] PRIME64_2) & 0xFFFFFFFFFFFFFFFF\n v4 = ((v4 << 31) | (v4 >> 33)) & 0xFFFFFFFFFFFFFFFF\n v4 = (v4 PRIME64_1) & 0xFFFFFFFFFFFFFFFF\n \n h64 = ((v1 << 1) | (v1 >> 63)) & 0xFFFFFFFFFFFFFFFF\n h64 = (h64 + ((v2 << 7) | (v2 >> 57))) & 0xFFFFFFFFFFFFFFFF\n h64 = (h64 + ((v3 << 12) | (v3 >> 52))) & 0xFFFFFFFFFFFFFFFF\n h64 = (h64 + ((v4 << 18) | (v4 >> 46))) & 0xFFFFFFFFFFFFFFFF\n else:\n h64 = seed + PRIME64_5\n \n h64 = (h64 + len(data)) & 0xFFFFFFFFFFFFFFFF\n \n # Process remaining 8-byte chunks\n offset = (len(data) // 32) 32\n while offset + 8 <= len(data):\n chunk = struct.unpack('<Q', data[offset:offset+8])[0]\n h64 ^= (chunk PRIME64_2) & 0xFFFFFFFFFFFFFFFF\n h64 = (((h64 << 31) | (h64 >> 33)) PRIME64_1) & 0xFFFFFFFFFFFFFFFF\n offset += 8\n \n # Process remaining bytes\n remaining = len(data) - offset\n if remaining >= 4:\n chunk = struct.unpack('<I', data[offset:offset+4])[0]\n h64 ^= (chunk PRIME64_1) & 0xFFFFFFFFFFFFFFFF\n h64 = (((h64 << 23) | (h64 >> 41)) PRIME64_2) & 0xFFFFFFFFFFFFFFFF\n offset += 4\n \n while offset < len(data):\n chunk = data[offset]\n h64 ^= (chunk PRIME64_5) & 0xFFFFFFFFFFFFFFFF\n h64 = (((h64 << 11) | (h64 >> 53)) PRIME64_1) & 0xFFFFFFFFFFFFFFFF\n offset += 1\n \n h64 ^= h64 >> 33\n h64 = (h64 PRIME642) & 0xFFFFFFFFFFFFFFFF\n h64 ^= h64 >> 29\n return h64\n\ndef siphash24(key, msg):\n \"\"\"SipHash-2-4 implementation\"\"\"\n k0, k1 = struct.unpack('<QQ', key)\n v0 = 0x736f6d6570736575 ^ k0\n v1 = 0x646f72616e646f6d ^ k1\n v2 = 0x6c7967656e657261 ^ k0\n v3 = 0x7465646279656d63 ^ k1\n \n def sipround():\n nonlocal v0, v1, v2, v3\n v0 = (v0 + v1) & 0xFFFFFFFFFFFFFFFF\n v1 = ((v1 << 13) | (v1 >> 51)) & 0xFFFFFFFFFFFFFFFF\n v1 ^= v0\n v0 = ((v0 << 32) | (v0 >> 32)) & 0xFFFFFFFFFFFFFFFF\n v2 = (v2 + v3) & 0xFFFFFFFFFFFFFFFF\n v3 = ((v3 << 16) | (v3 >> 48)) & 0xFFFFFFFFFFFFFFFF\n v3 ^= v2\n v0 = (v0 + v3) & 0xFFFFFFFFFFFFFFFF\n v3 = ((v3 << 21) | (v3 >> 43)) & 0xFFFFFFFFFFFFFFFF\n v3 ^= v0\n v2 = (v2 + v1) & 0xFFFFFFFFFFFFFFFF\n v1 = ((v1 << 17) | (v1 >> 47)) & 0xFFFFFFFFFFFFFFFF\n v1 ^= v2\n v2 = ((v2 << 32) | (v2 >> 32)) & 0xFFFFFFFFFFFFFFFF\n \n # Process message\n for i in range(0, len(msg) - 7, 8):\n m = struct.unpack('<Q', msg[i:i+8])[0]\n v3 ^= m\n sipround()\n sipround()\n v0 ^= m\n \n # Finalization\n b = (len(msg) & 0xFF) << 56\n v3 ^= b\n for in range(4):\n sipround()\n \n return (v0 ^ v1 ^ v2 ^ v3) & 0xFFFFFFFFFFFFFFFF\n\nif name == \"main\":\n # Read hjm_pack.bin\n with open(r\"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin\", \"rb\") as f:\n data = f.read()\n \n # Parse header\n magic = struct.unpack('<I', data[0:4])[0]\n frame_type = struct.unpack('<I', data[4:8])[0]\n frame_count = struct.unpack('<I', data[8:12])[0]\n height = struct.unpack('<I', data[12:16])[0]\n width = struct.unpack('<I', data[16:20])[0]\n unknown = struct.unpack('<I', data[20:24])[0]\n salt = data[24:40]\n iv = data[40:52]\n encrypted = data[52:]\n \n print(f\"Magic: 0x{magic:08x}\")\n print(f\"Type: {frame_type}\")\n print(f\"Count: {frame_count}\")\n print(f\"Height: {height}, Width: {width}\")\n print(f\"Salt: {salt.hex()}\")\n print(f\"IV: {iv.hex()}\")\n print(f\"Encrypted size: {len(encrypted)} bytes\")\n \n # beatMap = [0, 250, 500, 750]\n beatmap_bytes = struct.pack('<IIII', 0, 250, 500, 750)\n print(f\"\nBeatmap bytes: {beatmap_bytes.hex()}\")\n \n # Compute XXHash64 of beatmap\n xxhash_seed = 0x1A8CBC5B802E097C\n xxhash_result = xxhash64_correct(beatmap_bytes, xxhash_seed)\n print(f\"XXHash64(beatmap, 0x{xxhash_seed:016x}) = 0x{xxhash_result:016x}\")\n \n # Compute SipHash-2-4 with salt as key\n siphash_result = siphash24(salt, beatmap_bytes)\n print(f\"SipHash24(salt, beatmap) = 0x{siphash_result:016x}\")\n \n # Derive AES key from both hashes\n key_material = struct.pack('
🤖 助手 - 第 73 轮 [14:17:42]
🔨 工具调用: todolist
- action:
update
- id:
1
- status:
in_progress
📋 todolist 输出 [14:17:42]
Updated task #1. Current TodoList:
🔄 1. 用Python重新实现native库的解密逻辑:SipHash + XXHash64 + AES-CTR
✅ 2. 确定初始状态:找到beatMap数据和startSession参数 → beatMap=[0,250,500,750], interval=250, 密钥不依赖时间戳
⬜ 3. 模拟debug bypass路径(sub_2E570)生成密钥
⬜ 4. 解密hjm_pack.bin并渲染64x64位图
⬜ 5. 提交flag
🤖 助手 - 第 74 轮 [14:17:49]
Let me implement the full decryption pipeline in one script. First, let me re-examine the critical functions more carefully to get the exact logic right:
🔨 工具调用: mcp_ida-pro-headless_disasm
- max_instructions:
200
- addr:
0x2E570
📋 mcp_ida-pro-headless_disasm 输出 [14:17:49]
{
"addr": "0x2E570",
"asm": {
"name": "sub_2E570",
"start_ea": "0x2e570",
"lines": "sub_2E570 (.text @ 0x2e570):\n2e570 push rbp\n2e571 push r15\n2e573 push r14\n2e575 push r12\n2e577 push rbx\n2e578 sub rsp, 20h\n2e57c mov rax, fs:28h\n2e585 mov [rsp+48h+var_30], rax\n2e58a mov ebp, cs:dword_5CFD0\n2e590 xor ebx, ebx\n2e592 test ebp, ebp\n2e594 jle loc_2E650\n2e59a mov rax, cs:qword_5CFE8\n2e5a1 mov r14, 1A8CBC5B802E097Ch\n2e5ab xor r14, rax\n2e5ae mov rcx, 9E3779B97F4A7C15h\n2e5b8 add rcx, r14\n2e5
... [truncated 2915 bytes] ...
"offset": "0x10",
"size": "0x4",
"type": "_DWORD"
},
{
"name": "var_34",
"offset": "0x14",
"size": "0x4",
"type": "_DWORD"
},
{
"name": "var_30",
"offset": "0x18",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "__return_address",
"offset": "0x48",
"size": "0x8",
"type": "_UNKNOWN *"
}
],
"return_type": "__int64",
"arguments": []
},
"instruction_count": 65,
"total_instructions": null,
"cursor": {
"done": true
}
}
🤖 助手 - 第 75 轮 [14:17:55]
Now let me get the exact layout of sub_2E680 and sub_2EB00 to understand the decryption:
🔨 工具调用: mcp_ida-pro-headless_disasm
- addr:
0x2EB00
- max_instructions:
200
📋 mcp_ida-pro-headless_disasm 输出 [14:17:55]
{
"addr": "0x2EB00",
"asm": {
"name": "sub_2EB00",
"start_ea": "0x2eb00",
"lines": "sub_2EB00 (.text @ 0x2eb00):\n2eb00 push r14\n2eb02 push rbx\n2eb03 sub rsp, 18h\n2eb07 mov rbx, rcx\n2eb0a mov r14, rdx\n2eb0d mov rax, fs:28h\n2eb16 mov [rsp+28h+var_18], rax\n2eb1b mov rdx, 1A8CBC5B802E097Ch\n2eb25 xor rdx, rsi\n2eb28 mov rcx, 802E097C1A8CBC5Bh\n2eb32 xor rcx, rsi\n2eb35 mov [rsp+28h+var_28], rdi\n2eb39 mov [rsp+28h+var_20], rsi\n2eb3e mov rdi, rsp\n2eb41 mov esi, 10h\n2eb46 call sub_2DAF0\n2eb4b mo
... [truncated 3453 bytes] ...
": "_QWORD"
},
{
"name": "__return_address",
"offset": "0x28",
"size": "0x8",
"type": "_UNKNOWN *"
}
],
"return_type": "unsigned __int64",
"arguments": [
{
"name": "arg0",
"type": "__int64"
},
{
"name": "arg1",
"type": "__int64"
},
{
"name": "arg2",
"type": "_QWORD *"
},
{
"name": "arg3",
"type": "unsigned __int64 *"
}
]
},
"instruction_count": 89,
"total_instructions": null,
"cursor": {
"done": true
}
}
🤖 助手 - 第 76 轮 [14:18:04]
Now let me look at the decryption loop in sub_2E680 more carefully - specifically how the AES key is derived and used:
🔨 工具调用: mcp_ida-pro-headless_disasm
- addr:
0x2E680
- max_instructions:
400
📋 mcp_ida-pro-headless_disasm 输出 [14:18:04]
{
"addr": "0x2E680",
"asm": {
"name": "sub_2E680",
"start_ea": "0x2e680",
"lines": "sub_2E680 (.text @ 0x2e680):\n2e680 push rbp\n2e681 push r15\n2e683 push r14\n2e685 push r13\n2e687 push r12\n2e689 push rbx\n2e68a sub rsp, 0B8h\n2e691 mov rax, fs:28h\n2e69a mov [rsp+0E8h+var_38], rax\n2e6a2 test rsi, rsi\n2e6a5 jz loc_2EA89\n2e6ab mov r12, rdi\n2e6ae mov r14, [rdi]\n2e6b1 mov rax, [rdi+8]\n2e6b5 sub rax, r14\n2e6b8 cmp rax, 34h ; '4'\n2e6bc jb loc_2EA89\n2e6c2 cmp dword ptr [r14], 314D4A48h\n
... [truncated 10360 bytes] ...
"size": "0x10",
"type": "_OWORD"
},
{
"name": "var_38",
"offset": "0xb0",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "__return_address",
"offset": "0xe8",
"size": "0x8",
"type": "_UNKNOWN *"
}
],
"return_type": "__int64",
"arguments": [
{
"name": "arg0",
"type": "_QWORD *"
},
{
"name": "arg1",
"type": "__int64"
}
]
},
"instruction_count": 250,
"total_instructions": null,
"cursor": {
"done": true
}
}
🤖 助手 - 第 77 轮 [14:19:14]
Now I have all the assembly details. Let me write the complete decryption script:
🐍 执行 Python 脚本
依赖: pycryptodome
import struct
from Crypto.Cipher import AES
def u64(x): return x & 0xFFFFFFFFFFFFFFFF
def u32(x): return x & 0xFFFFFFFF
def rol64(x, n): return u64((x << n) | (x >> (64 - n)))
# SipHash-2-4
def sip...[truncated]
📋 Python 输出 [14:19:17]
qword_5CFE0 = qword_5CFE8 = 0x810a3647628f0212
sub_2E570 result (debug_key) = 0xd2c13a26769ce7e0
v34 (key to sub_2E680) = 0x77649f83d3394245
Salt: 0f1e2d3c4b5a69788796a5b4c3d2e1f0
IV: 112233445566778899aabbcc
Encrypted size: 512
SipHash result in sub_2EB00 = 0x0ea9de92c914b094
key_part0 = 0xf456e2c7ee0af52c
key_part1 = 0x18ff86584363a443
var_68 (derived key material): 2cf50aeec7e256f443a463435886ff18
🤖 助手 - 第 78 轮 [14:20:03]
Now let me implement the AES-CTR decryption loop from sub_2E680:
🐍 执行 Python 脚本
依赖: pycryptodome
import struct
from Crypto.Cipher import AES
def u64(x): return x & 0xFFFFFFFFFFFFFFFF
def u32(x): return x & 0xFFFFFFFF
def rol64(x, n): return u64((x << n) | (x >> (64 - n)))
def xxhash64(data, se...[truncated]
📋 Python 输出 [14:20:05]
folded qword_5CFE8 = 0xe3853455
Chunk 0: key=32d64809c01fd6eec54ddff78e80aa78, nonce=112233440052f26b99aabbcc, first_dec=805e6a74557f1cdaaa356dc35ebd35af
Chunk 1: key=8610330511564bbe75b9fff53f5b6851, nonce=102233440052f26b99aabbcc, first_dec=4d9d53f018fbfb7d0e8b33a72b8fbad8
Decrypted 512 bytes
First 32 bytes: 805e6a74557f1cdaaa356dc35ebd35aff827c44616a2b4f525b1e38e9e70d0ed
Last 32 bytes: 9733382e37cc3934c373ba95401066edddc2132e3ea1be30ab63387055f97b8a
Bits set: 2124 / 4096 (51%)
🤖 助手 - 第 79 轮 [14:20:16]
The decrypted data looks random (51% bits set, no clear pattern). This suggests either the decryption is wrong or I need to check the verifyAndDecrypt path more carefully. Let me re-examine whether the XOR with 0xA5... is actually applied:
🔨 工具调用: mcp_ida-pro-headless_disasm
- addr:
0x24850
- max_instructions:
100
📋 mcp_ida-pro-headless_disasm 输出 [14:20:16]
{
"addr": "0x24850",
"asm": {
"name": "verifyAndDecrypt",
"start_ea": "0x24850",
"lines": "verifyAndDecrypt (.text @ 0x24850):\n24850 push rbp\n24851 push r15\n24853 push r14\n24855 push r13\n24857 push r12\n24859 push rbx\n2485a sub rsp, 58h\n2485e mov r14, rdi\n24861 mov rax, fs:28h\n2486a mov [rsp+88h+var_38], rax\n2486f mov rax, [rdi]\n24872 test rdx, rdx\n24875 jz loc_24937\n2487b mov r15, rcx\n2487e test rcx, rcx\n24881 jz loc_24937\n24887 mov r13, rdx\n2488a mov rdi, r14\n2488d mov r
... [truncated 4522 bytes] ...
0x8",
"type": "_QWORD"
},
{
"name": "__return_address",
"offset": "0x88",
"size": "0x8",
"type": "_UNKNOWN *"
}
],
"return_type": "__int64",
"arguments": [
{
"name": "arg0",
"type": "__int64 *"
},
{
"name": "arg1",
"type": "__int64"
},
{
"name": "arg2",
"type": "__int64"
},
{
"name": "arg3",
"type": "__int64"
}
]
},
"instruction_count": 100,
"total_instructions": null,
"cursor": {
"next": 100
}
}
🤖 助手 - 第 80 轮 [14:20:23]
🔨 工具调用: mcp_ida-pro-headless_disasm
- addr:
0x24850
- offset:
100
- max_instructions:
200
📋 mcp_ida-pro-headless_disasm 输出 [14:20:23]
{
"addr": "0x24850",
"asm": {
"name": "verifyAndDecrypt",
"start_ea": "0x24850",
"lines": "verifyAndDecrypt (.text @ 0x24850):\n249be jnz short loc_24A30\n249c0 shl rcx, 20h\n249c4 movsxd rdx, edx\n249c7 mov rsi, 1A8CBC5B802E097Ch\n249d1 xor rsi, rcx\n249d4 xor rsi, rdx\n249d7 mov rcx, 9E3779B97F4A7C15h\n249e1 add rcx, rsi\n249e4 mov rdx, rcx\n249e7 shr rdx, 1Eh\n249eb xor rdx, rcx\n249ee mov rcx, 0BF58476D1CE4E5B9h\n249f8 imul rcx, rdx\n249fc mov rdx, rcx\n249ff shr rdx, 1Bh\n24a03 xor rdx, rcx\n24a06
... [truncated 7164 bytes] ...
x8",
"type": "_QWORD"
},
{
"name": "__return_address",
"offset": "0x88",
"size": "0x8",
"type": "_UNKNOWN *"
}
],
"return_type": "__int64",
"arguments": [
{
"name": "arg0",
"type": "__int64 *"
},
{
"name": "arg1",
"type": "__int64"
},
{
"name": "arg2",
"type": "__int64"
},
{
"name": "arg3",
"type": "__int64"
}
]
},
"instruction_count": 184,
"total_instructions": null,
"cursor": {
"done": true
}
}
👤 用户 [14:20:23]
[TodoList Reminder - Round 41]
Stats: 3 pending, 1 done, 0 failed.
You have 3 pending items — execute them before adding new ones.
Do NOT repeat failed approaches. If stuck, reset your TodoList with a new strategy.
🤖 助手 - 第 81 轮 [14:21:33]
Now I can see the exact flow at 0x24b8b-0x24bfa:
24b8b: al = byte_5D140 (debugBypass flag)
24b92: test al, al
24b94: setnz cl
24b97: or cl, byte_5CFF8
24b9d: or cl, r13b
24ba0: test cl, 1
24ba3: jz 24bb4 (fail)
24ba5: ...
24ba9: test al, al (test debugBypass again)
24bab: jz 24bda (if NOT debug, use qword_5CFF0)
24bad: call sub_2E570 (if debug, compute key)
24bb2: jmp 24be1
24bda: mov rax, qword_5CFF0 (use game-earned key)
24be1: mov rsi, 0xA5A5A5A5A5A5A5A5
24beb: xor rsi, rax
24bee: test r13b, r13b (r13b = password verification passed?)
24bf1: cmovz rsi, rax (if password NOT verified, DON'T XOR)
24bf5: lea rdi, [rsp+var_70]
24bfa: call sub_2E680
Key insight: When debugBypass=true, al=1, so it calls sub_2E570. Then rsi = result ^ 0xA5A5.... But r13b depends on password verification. If r13b=0 (password not verified), cmovz replaces rsi with rax (no XOR). If r13b=1, the XOR stays.
I need to figure out what r13b is. Looking back at 0x24a1e: r13b = 1 when the splitmix64 result is non-zero (which it almost certainly is). But at 0x24a30: r13d = 0 when byte_5D00C != 1.
The byte_5D00C is set to 1 when dword_5D008 >= 4. This is the "experience" counter. For debug bypass, we need to check if the password path succeeds.
Let me try both variants - with and without the XOR:
🐍 执行 Python 脚本
依赖: pycryptodome,Pillow
import struct
from Crypto.Cipher import AES
from PIL import Image
def u64(x): return x & 0xFFFFFFFFFFFFFFFF
def u32(x): return x & 0xFFFFFFFF
def rol64(x, n): return u64((x << n) | (x >> (64 - n)))
...[truncated]
📋 Python 输出 [14:21:38]
debug_key ^ 0xA5 (r13b=1): bits=2124/4096 (51%), row0=805e6a74557f1cda
Saved bitmap_debug_key_^_0xA5_r13b1.png
debug_key raw (r13b=0): bits=1959/4096 (47%), row0=0000000000000000
Saved bitmap_debug_key_raw_r13b0.png
qfe8 ^ 0xA5 (game path): bits=2090/4096 (51%), row0=a70cb53fdecafaae
Saved bitmap_qfe8_^_0xA5_game_path.png
qfe8 raw: bits=2053/4096 (50%), row0=2a9a67989b338d3f
Saved bitmap_qfe8_raw.png
🤖 助手 - 第 82 轮 [14:21:45]
The "debug_key raw (r13b=0)" variant has row0 = all zeros, which looks promising for a QR code! Let me check it:
🔨 工具调用: vision
- image:
D:\AI\AICTF\workdir\52pojie\chu8\bitmap_debug_key_raw_r13b0.png
- prompt:
Is this a QR code or any recognizable pattern? Describe what you see in detail.
📋 vision 输出 [14:22:01]
This is **not a valid, scannable QR code nor a standard machine-readable pattern** like a barcode, Data Matrix, etc. Here's a detailed breakdown of what this is:
1. **Overall Type**: It is a dense, high-contrast monochrome black-and-white chaotic texture, specifically a distorted, overlapping "soup" of stylized, warped small characters/symbols.
2. **Detailed Elements**:
The frame is filled with tightly packed, deformed, overlapping small glyphs: they resemble squashed, curved, angular lowercase letters, modified punctuation, or abstract tiny shapes, all mashed together with no consistent spacing, alignment, or order.
3. **Lack of Functional Pattern Structure**:
Unlike QR codes, which have a strict square grid, clear corner finder markers, and uniform square modules, this has zero consistent grid, no reference points for scanning, and no organized structure. It has no scannable or machine-readable function.
4. **Visual Effect**: It looks like text run through an extreme warp/distortion filter, turning readable characters into an unreadable, dense, chaotic texture that fills the entire image space.
🤖 助手 - 第 83 轮 [14:23:00]
Not a QR code. Let me look at the image more carefully and also check if the bit ordering might be different:
🐍 执行 Python 脚本
依赖: Pillow
import struct
from PIL import Image
# Read the decrypted data for the "debug_key raw" variant
# Let me regenerate it quickly
def u64(x): return x & 0xFFFFFFFFFFFFFFFF
def u32(x): return x & 0xFFFFFF...[truncated]
📋 Python 输出 [14:23:02]
Image size: (64, 64)
Image mode: 1
█ ██ █ █ █ ████ █ ██ ████ ████████ █ █ ████ ██ ██
█ ████████ █████ ██ █ ███ █ █ █ █ ██████ █████ █ █
██████ █ █ ██ █ █ ██ █ ███ █ ██ ██ ██ █ █ ████
█ ███ ██ █ █ �
... [truncated 8112 bytes] ...
████ ████ █████ ██ ███ ██ █ ███ ██
█ █ ██ █ █ █ █ █ █ ██ █ ██ ████ ███ █ ██ █ █
█ ██ ██ █ ███ █ █████ █ █ ███ █ █ █ █ ██ █ ██ ███
█ █ ███ ███ ███ ██ ██ ███ ████ ██ ██ ███ █ ███ █
█ ███ █ ██ █ ██ ████ █ █ █ █ ███ ███ █ █ █ █ ███ █
🤖 助手 - 第 84 轮 [14:23:09]
This doesn't look like a QR code. Let me reconsider the approach. Maybe I need to look at the sub_2DAF0 function more carefully - it's the SipHash used in sub_2E570, and I might have the wrong SipHash variant:
🔨 工具调用: mcp_ida-pro-headless_disasm
- addr:
0x2DAF0
- max_instructions:
200
📋 mcp_ida-pro-headless_disasm 输出 [14:23:09]
{
"addr": "0x2DAF0",
"asm": {
"name": "sub_2DAF0",
"start_ea": "0x2daf0",
"lines": "sub_2DAF0 (.text @ 0x2daf0):\n2daf0 push rbx\n2daf1 mov r9, 736F6D6570736575h\n2dafb xor r9, rdx\n2dafe mov r8, 646F72616E646F6Dh\n2db08 xor r8, rcx\n2db0b mov rax, 6C7967656E657261h\n2db15 xor rax, rdx\n2db18 mov rdx, 7465646279746573h\n2db22 xor rdx, rcx\n2db25 mov r10, rsi\n2db28 and r10, 0FFFFFFFFFFFFFFF8h\n2db2c jz loc_2DBB6\n2db32 lea rcx, [rdi+r10]\n2db36 xor r11d, r11d\n2db39 nop dword ptr [rax+00000000h]\n2db40
... [truncated 5389 bytes] ...
4a retn",
"stack_frame": [
{
"name": "__return_address",
"offset": "0x8",
"size": "0x8",
"type": "_UNKNOWN *"
}
],
"return_type": "__int64",
"arguments": [
{
"name": "arg0",
"type": "unsigned __int8 *"
},
{
"name": "arg1",
"type": "__int64"
},
{
"name": "arg2",
"type": "__int64"
},
{
"name": "arg3",
"type": "__int64"
}
]
},
"instruction_count": 168,
"total_instructions": null,
"cursor": {
"done": true
}
}
🤖 助手 - 第 85 轮 [14:25:18]
🔨 工具调用: mcp_sequential-thinking_sequentialthinking
- nextThoughtNeeded:
true
- thought: `Let me carefully trace the SipHash implementation in sub_2DAF0.
Arguments: rdi=data_ptr, rsi=length, rdx=k0, rcx=k1
Initialization:
r9 = 0x736f6d6570736575 ^ rdx (v0 = k0 ^ magic0)
r8 = 0x646f72616...[truncated]`
- thoughtNumber:
1
- totalThoughts:
3
📋 mcp_sequential-thinking_sequentialthinking 输出 [14:25:18]
{
"thoughtNumber": 1,
"totalThoughts": 3,
"nextThoughtNeeded": true,
"branches": [],
"thoughtHistoryLength": 8
}
👤 用户 [14:25:18]
[Progress Checkpoint - Round 46/200]
Briefly assess your progress:
- What have you accomplished so far?
- Are you making progress toward the flag?
Continue with your current approach if it shows promise.
🤖 助手 - 第 86 轮 [14:25:31]
<details>
<summary>💭 思考过程</summary>
Let me assess where I am:
- I've identified the APK's native library decryption flow: SipHash-2-4 + XXHash64 + AES-CTR
- I've computed the key derivation chain but the decrypted output doesn't look like a valid QR code/bitmap
- The "debug_key raw" variant has row0=all zeros which is promising but the rest is noise
The issue is likely in either:
- My AES-CTR implementation (how the IV/nonce is constructed)
- My understanding of sub_2DF40 (the actual decryption function)
- The key derivation path I'm following
Let me look at sub_2DF40 more carefully - that's the AES-CTR decryption function. Also sub_24CB0 which generates the per-chunk AES keys.
</details>
Progress assessment:
- Key derivation chain computed (SipHash + XXHash64), 4 key variants tried
- "debug_key raw" variant shows row0=all zeros (promising) but rest is noise
- Likely issue: AES-CTR implementation details in sub_2DF40 or sub_24CB0
Let me examine the actual AES-CTR function and the XXHash key generation function:
🔨 工具调用: mcp_ida-pro-headless_disasm
- addr:
0x2DF40
- max_instructions:
300
📋 mcp_ida-pro-headless_disasm 输出 [14:25:31]
{
"addr": "0x2DF40",
"asm": {
"name": "sub_2DF40",
"start_ea": "0x2df40",
"lines": "sub_2DF40 (.text @ 0x2df40):\n2df40 push rbp\n2df41 push r15\n2df43 push r14\n2df45 push r13\n2df47 push r12\n2df49 push rbx\n2df4a sub rsp, 108h\n2df51 mov [rsp+138h+var_118], rdi\n2df56 mov rax, fs:28h\n2df5f mov [rsp+138h+var_38], rax\n2df67 movdqu xmm0, xmmword ptr [rdx]\n2df6b movdqa [rsp+138h+var_E8], xmm0\n2df71 mov eax, 1\n2df76 mov edx, 0Fh\n2df7b mov r14d, 10h\n2df81 lea rbp, byte_17430\n2df88 lea rdi, byte_17530\
... [truncated 13870 bytes] ...
WORD"
},
{
"name": "__return_address",
"offset": "0x138",
"size": "0x8",
"type": "_UNKNOWN *"
}
],
"return_type": "unsigned __int64",
"arguments": [
{
"name": "arg0",
"type": "__int64"
},
{
"name": "arg1",
"type": "unsigned __int64"
},
{
"name": "arg2",
"type": "const __m128i *"
},
{
"name": "arg3",
"type": "_BYTE *"
}
]
},
"instruction_count": 300,
"total_instructions": null,
"cursor": {
"next": 300
}
}
🤖 助手 - 第 87 轮 [14:25:40]
This is a custom AES-CTR implementation, not using standard library. The key expansion happens at the top, then it does AES encryption of a counter and XORs with plaintext. The critical thing I notice is:
- The AES key is loaded from
[rdx] (16 bytes) - this is the key pointer
- The counter/IV is loaded from
[rcx] (12 bytes) - this is the IV pointer
- The counter is a simple incrementing 32-bit value in the first 4 bytes
Let me look at how the IV/counter is constructed. The first 12 bytes of the AES state are XORed with the IV, and the last 4 bytes are the counter. Let me re-examine the calling code in sub_2E680:
🔨 工具调用: mcp_ida-pro-headless_decompile
📋 mcp_ida-pro-headless_decompile 输出 [14:25:40]
{
"addr": "0x2E680",
"code": "__int64 __fastcall sub_2E680(_QWORD *a1, __int64 a2)\n{\n _QWORD *v2; // r12\n _DWORD *v3; // r14\n unsigned __int64 v4; // rax\n __int64 v5; // rdx\n __int64 v6; // rbp\n int v7; // ecx\n int v8; // edi\n unsigned int v9; // edi\n size_t v10; // rdi\n unsigned __int64 v11; // rbx\n unsigned __int64 v12; // r13\n void **v13; // r14\n __int64 v14; // rax\n __int64 v15; // r15\n __int64 v16; // rax\n unsigned __int64 v17; // rbp\n void **v18; // r13\n _QWORD *v19; // r14\n void *v20; // r12\n void *v21; // rax\n __int64 v22; // rax\n __int6
... [truncated 7703 bytes] ...
ptr[1] = ptr[0]; /*0x2ea54*/\n operator delete(ptr[0]); /*0x2ea59*/\n }\n return (unsigned int)v6; /*0x2ea5e*/\n }\n }\n if ( !src[0] || (src[1] = src[0], operator delete(src[0]), (v25 = ptr[0]) != 0) ) /*0x2ea7d*/\n {\n ptr[1] = v25; /*0x2ea7f*/\n operator delete(v25); /*0x2ea84*/\n }\n goto LABEL_27; /*0x2ea84*/\n }\n }\n }\n }\n }\n }\n return (unsigned int)v6; /*0x2eaa0*/\n}"
}
🤖 助手 - 第 88 轮 [14:26:06]
Now I can see the structure clearly. The key issue is how sub_2DF40 is called. Let me look at the arguments:
arg0 = v39 + *v2 = offset into output buffer
arg1 = v38 = chunk size (min(256, remaining))
arg2 = ptr = AES key (16 bytes, two 8-byte values from sub_24CB0)
arg3 = src = IV/nonce (12 bytes)
The IV construction is complex. Let me look at sub_24CB0 which generates the AES key:
🔨 工具调用: mcp_ida-pro-headless_decompile
📋 mcp_ida-pro-headless_decompile 输出 [14:26:06]
{
"addr": "0x24CB0",
"code": "unsigned __int64 __fastcall sub_24CB0(unsigned __int8 *a1, unsigned __int64 a2, unsigned __int64 a3)\n{\n unsigned __int64 v3; // r9\n unsigned __int64 v4; // rbp\n unsigned __int64 v5; // r13\n unsigned __int64 v6; // r12\n __int64 v7; // r15\n __int64 v8; // r14\n __int64 v9; // rbx\n __int64 v10; // r11\n unsigned __int64 v11; // r11\n unsigned __int64 v12; // r11\n unsigned __int64 v13; // rsi\n unsigned __int8 *v14; // r10\n bool v15; // cc\n unsigned __int64 v16; // rdi\n unsigned __int64 v17; // r10\n __int64 v18; // r11\n unsigned __in
... [truncated 4974 bytes] ...
t8 *)v16), 11))\n ^ (0x27D4EB2F165667C5LL * *(unsigned __int8 *)(v16 + 1)),\n 11))\n ^ (0x27D4EB2F165667C5LL * *(unsigned __int8 *)(v16 + 2)),\n 11))\n ^ (0x27D4EB2F165667C5LL * *(unsigned __int8 *)(v16 + 3)),\n 11);\n v16 += 4LL; /*0x24f7d*/\n }\n while ( v16 < v3 ); /*0x24f84*/\n }\nLABEL_18:\n v20 = 0x165667B19E3779F9LL /*0x24f86*/\n * ((0xC2B2AE3D27D4EB4FLL * (v13 ^ (v13 >> 33))) ^ ((0xC2B2AE3D27D4EB4FLL * (v13 ^ (v13 >> 33))) >> 29));\n return v20 ^ HIDWORD(v20); /*0x24fac*/\n}"
}
👤 用户 [14:26:06]
[TodoList Reminder - Round 49]
Stats: 3 pending, 1 done, 0 failed.
You have 3 pending items — execute them before adding new ones.
Do NOT repeat failed approaches. If stuck, reset your TodoList with a new strategy.
🤖 助手 - 第 89 轮 [14:26:20]
This is XXHash64 but the return value is v20 ^ HIDWORD(v20) - it returns a 32-bit value folded from 64-bit! That's different from standard XXHash64. Also the seed is passed as a3 (third argument), not second. Let me also check: the function signature is sub_24CB0(data, size, seed) and it returns a folded 32-bit result.
This means my AES key generation is wrong - each call to sub_24CB0 returns a 32-bit value, not 64-bit. The AES key is built from two 32-bit values stored as ptr[0] and ptr[1], but those are 8-byte pointers... Wait, looking at the decompiled code:
v20 = (void *)sub_24CB0(src, 32, 4951); // ptr[0] = 8 bytes but only low 32 meaningful?
v21 = (void *)sub_24CB0(src, 32, 9320); // ptr[1]
ptr[0] = v20;
ptr[1] = v21;
Actually ptr is void *[2] - two 8-byte slots. But sub_24CB0 returns v20 ^ HIDWORD(v20) which is a 32-bit value. So ptr contains two 8-byte values where only the low 32 bits matter. But then ptr is passed to sub_2DF40 as the AES key (16 bytes). So the AES key is actually [low32_of_hash1, 0, low32_of_hash2, 0]? That seems wrong.
Let me re-read sub_24CB0's return more carefully:
🔨 工具调用: mcp_ida-pro-headless_disasm
- addr:
0x24F86
- max_instructions:
20
📋 mcp_ida-pro-headless_disasm 输出 [14:26:20]
{
"addr": "0x24F86",
"asm": {
"name": "sub_24CB0",
"start_ea": "0x24f86",
"lines": "sub_24CB0 (.text @ 0x24f86):\n24f86 mov rcx, rsi\n24f89 shr rcx, 21h\n24f8d xor rcx, rsi\n24f90 imul rcx, rax\n24f94 mov rsi, rcx\n24f97 shr rsi, 1Dh\n24f9b xor rsi, rcx\n24f9e imul rsi, rdx\n24fa2 mov rax, rsi\n24fa5 shr rax, 20h\n24fa9 xor rax, rsi\n24fac pop rbx\n24fad pop r12\n24faf pop r13\n24fb1 pop r14\n24fb3 pop r15\n24fb5 pop rbp\n24fb6 retn",
"stack_frame": [
{
"name": "__return_address",
"offset": "0x30",
"size": "0x8",
"type": "_UNKNOWN *"
}
],
"return_type": "unsigned __int64",
"arguments": [
{
"name": "arg0",
"type": "unsigned __int8 *"
},
{
"name": "arg1",
"type": "unsigned __int64"
},
{
"name": "arg2",
"type": "unsigned __int64"
}
]
},
"instruction_count": 18,
"total_instructions": null,
"cursor": {
"done": true
}
}
🤖 助手 - 第 90 轮 [14:26:50]
The final steps: rsi ^ (rsi >> 32) → returns in rax. This is a 64-bit value where the top 32 bits are XORed into the bottom 32 bits. But rax is still 64 bits - the high 32 bits will be rsi >> 32 and the low 32 bits will be rsi ^ (rsi >> 32). Actually no:
rax = rsi
rax >>= 32 (rax = high32 of rsi, zero-extended)
rax ^= rsi (rax = rsi ^ (rsi >> 32))
So rax = full 64-bit value rsi ^ (rsi >> 32). The high 32 bits are NOT zero. Let me verify: if rsi = 0xAABBCCDDEEFF0011, then rsi >> 32 = 0x00000000AABBCCDD, and rsi ^ (rsi >> 32) = 0xAABBCCDD44443CCC. So it's still 64 bits.
Wait, the decompiler said return v20 ^ HIDWORD(v20) which would be 32-bit. But looking at the assembly, it's xor rax, rsi which is 64-bit XOR. The decompiler is misleading. The function returns a full 64-bit value.
So my original approach was correct for the hash function. The issue must be elsewhere. Let me use the xxhash library to verify my implementation:
🐍 执行 Python 脚本
依赖: xxhash
import struct
def u64(x): return x & 0xFFFFFFFFFFFFFFFF
def rol64(x, n): return u64((x << n) | (x >> (64 - n)))
def xxhash64(data, seed):
PRIME1, PRIME2, PRIME3, PRIME4, PRIME5 = 0x9E3779B185EB...[truncated]
📋 Python 输出 [14:26:53]
My XXHash64: 0x4a688cc8e32bcc0b
Lib XXHash64: 0x4a688cc8e32bcc0b
Match: True
32-byte test:
My: 0xcbf59c5116ff32b4
Lib: 0xcbf59c5116ff32b4
Match: True
Beatmap hash:
My: 0x810a3647628f0212
Lib: 0x810a3647628f0212
Match: True
🤖 助手 - 第 91 轮 [14:27:54]
XXHash64 is correct. Now let me verify the sub_24CB0 function - it's XXHash64 but with a fold at the end:
🐍 执行 Python 脚本
import struct, xxhash
def u64(x): return x & 0xFFFFFFFFFFFFFFFF
def sub_24CB0(data, seed):
"""XXHash64 with final fold: result ^ (result >> 32)"""
h = xxhash.xxh64(data, seed=seed).intdigest()
return u64(h ^ (h >> 32))
# Now let me also check: in sub_2E680, the 'src' buffer passed to sub_24CB0 is 32 bytes:
# [0:16] = var_68 (key material from sub_2EB00)
# [16:20] = r15d (chunk counter as u32)
# [20:24] = r13d (low 32 of qword_5CFE8)
# [24:32] = var_E0 ^ r13 (where var_E0 = chunk_idx * 0x100000000)
# Wait, looking at the decompiled code more carefully:
# v44.m128i_i64[0] = __PAIR64__(v12, v15) -> this is (v12 << 32) | v15
# v44.m128i_i64[1] = v12 ^ v31
# v12 = qword_5CFE8 (r13 in asm)
# v15 = chunk counter (r15 in asm, starts at 0)
# v31 = var_E0 (starts at 0, increments by 0x100000000)
# So the 32-byte src buffer is:
# [0:16] = var_68 (from sub_2EB00)
# [16:24] = __PAIR64__(qword_5CFE8, chunk_counter) = (qfe8 << 32) | counter
# [24:32] = qfe8 ^ var_E0
# Wait, __PAIR64__(v12, v15) means high=v12, low=v15
# So bytes 16-23 = v15 (low 32) | v12 (high 32) as a 64-bit LE value
# That means: struct.pack('<Q', (v12 << 32) | (v15 & 0xFFFFFFFF))
# Which is: struct.pack('<II', v15, v12) (little-endian: low word first)
# And bytes 24-31 = v12 ^ v31 as 64-bit LE
# Let me verify: in the asm at 0x2e7ef:
# mov dword ptr [rsp+var_48], r15d -> offset 0 of var_48 = r15d (low 32)
# mov dword ptr [rsp+var_48+4], r13d -> offset 4 of var_48 = r13d (high 32)
# So var_48[0:8] = (r13 << 32) | r15 = __PAIR64__(r13, r15)
# And at 0x2e807:
# mov rax, var_E0
# xor rax, r13
# mov qword ptr [rsp+var_48+8], rax -> var_48[8:16] = var_E0 ^ r13
# So the full 32-byte buffer for sub_24CB0 is:
# src[0:16] = var_68 (key from sub_2EB00)
# src[16:20] = r15d (chunk counter, 32-bit)
# src[20:24] = r13d (low 32 of qfe8)
# src[24:32] = var_E0 ^ r13 (64-bit)
# This matches what I had before. Good.
# Now let me check the IV construction more carefully.
# From the decompiled code, the IV (src in the call to sub_2DF40) is built as:
# 1. src[0:8] = *v13 (first 8 bytes of IV from file, at offset 0x28 = 40)
# 2. src[8:12] = *(v13 + 8) (next 4 bytes of IV from file)
# 3. Then XOR first 8 bytes with counter info
# The XOR mask is built from xmm0:
# byte 0: r15 & 0xFF
# byte 1: (r15 >> 8) & 0xFF
# byte 2: (r15 >> 16) & 0xFF
# byte 3: (r15 >> 24) & 0xFF
# byte 4: folded & 0xFF
# byte 5: (folded >> 8) & 0xFF
# byte 6: (folded >> 16) & 0xFF
# byte 7: (folded >> 24) & 0xFF
# Where folded = qfe8 ^ (qfe8 >> 32)
# Then: src[0:8] ^= xmm0[0:8]
# So the 12-byte IV passed to sub_2DF40 is:
# [0:8] = file_iv[0:8] ^ pack('<II', chunk_counter, folded_qfe8)
# [8:12] = file_iv[8:12] (unchanged)
# This is what I had. Let me now check the AES-CTR counter construction in sub_2DF40.
# Looking at sub_2DF40:
# At 0x2e023-0x2e0d2: The first 12 bytes of the expanded key state are XORed with the IV
# Bytes 0-11 of var_E8 are XORed with rcx[0:12]
# Bytes 12-15 of var_E8 are NOT XORed (they stay as the key schedule)
# Wait no. Let me re-read:
# var_E8 is the AES key schedule (expanded from the 16-byte key in [rdx])
# Then at 0x2e023: the FIRST 12 bytes of var_E8 are XORed with rcx[0:12]
# And bytes 12-15 are stored as-is (var_131 through var_134)
# Then in the main loop (0x2e113 onwards):
# A counter 'ecx' starts at 0 and increments
# The counter bytes are XORed with bytes 12-15 of the modified state
# Then AES encryption is performed on this 16-byte block
# The result is XORed with the plaintext
# So this is AES-CTR where:
# - The AES key is derived from two xxhash64 values
# - The initial counter block is: [IV_xored(12 bytes)] + [counter(4 bytes)]
# - The counter increments for each 16-byte block
# But wait - looking more carefully at the code:
# var_131 = var_E8[12] (byte 12 of expanded key, NOT XORed with IV)
# var_132 = var_E8[13]
# var_133 = var_E8[14]
# var_134 = var_E8[15]
# Then in the loop:
# r9b = var_131 ^ cl (counter byte 0)
# dl = var_132 ^ (counter >> 8)
# bl = var_133 ^ (counter >> 16)
# r12b = var_134 ^ (counter >> 24)
# So the counter is XORed with the LAST 4 bytes of the first round key.
# This is NOT standard AES-CTR!
# In standard AES-CTR, you'd have a 16-byte counter block that gets encrypted.
# Here, the "counter block" is:
# [first_round_key[0:12] ^ IV[0:12]] + [first_round_key[12:16] ^ counter_bytes]
# And this gets fed through the AES rounds (starting from round 1, not round 0).
# Actually wait - let me re-read. The key expansion at the top of sub_2DF40:
# var_E8 is loaded with the key (16 bytes from [rdx])
# Then key expansion happens (the loop at 0x2df8f-0x2e018)
# This expands to 11 round keys (176 bytes = 0xB0)
# Then the first round key (var_E8[0:16]) is XORed with the IV:
# var_125 = var_E8[0] ^ IV[0]
# var_129 = var_E8[1] ^ IV[1] (wait, the naming is confusing)
# Actually the var names are at negative offsets. Let me map them:
# var_125 is at offset 0x13, var_126 at 0x12, etc.
# var_E8 is at offset 0x50
# The XOR at 0x2e02d: al = var_E8[0] ^ rcx[0], stored to var_125
# This creates a modified first block that combines the round key with the IV.
# Then in the AES encryption loop, this modified block goes through SubBytes + MixColumns
# for 9 rounds, then a final SubBytes, then XOR with the last round key.
# So the encryption is: AES_encrypt(round_key_0 ^ IV_extended)
# where IV_extended = [IV(12 bytes) + counter(4 bytes)]
# This IS standard AES-CTR! The first step of AES is AddRoundKey (XOR with round key 0),
# and here they pre-compute that XOR with the IV.
# So the full picture:
# counter_block = IV(12 bytes) || counter(4 bytes, big-endian? little-endian?)
# keystream = AES_encrypt(key, counter_block)
# plaintext = ciphertext ^ keystream
# The counter format: looking at the XOR at 0x2e136:
# r9b = var_131 ^ cl (cl = counter & 0xFF)
# This means byte 12 of the block = round_key[12] ^ (counter & 0xFF)
# Which means the counter is in LITTLE-ENDIAN format in bytes 12-15
# But in standard AES-CTR (like NIST), the counter is big-endian.
# PyCryptodome's AES.MODE_CTR with nonce uses big-endian counter by default.
# So I need to use little-endian counter!
print("Key insight: AES-CTR with 12-byte nonce and 4-byte LITTLE-ENDIAN counter")
print("The nonce is: file_IV[0:8] ^ counter_info || file_IV[8:12]")
print("Counter starts at 0 and increments for each 16-byte block within a 256-byte chunk")
📋 Python 输出 [14:27:54]
Key insight: AES-CTR with 12-byte nonce and 4-byte LITTLE-ENDIAN counter
The nonce is: file_IV[0:8] ^ counter_info || file_IV[8:12]
Counter starts at 0 and increments for each 16-byte block within a 256-byte chunk
🤖 助手 - 第 92 轮 [14:28:45]
The counter is little-endian! PyCryptodome's default CTR mode uses big-endian. Let me fix this and also verify the fold in sub_24CB0:
🐍 执行 Python 脚本
依赖: pycryptodome,xxhash,Pillow
import struct, xxhash
from Crypto.Cipher import AES
from Crypto.Util import Counter
from PIL import Image
def u64(x): return x & 0xFFFFFFFFFFFFFFFF
def u32(x): return x & 0xFFFFFFFF
def rol64(x, n): return u64((x << n) | (x >> (64 - n)))
def siphash_2_4(data, k0, k1):
v0 = u64(k0 ^ 0x736f6d6570736575)
v1 = u64(k1 ^ 0x646f72616e646f6d)
v2 = u64(k0 ^ 0x6c7967656e657261)
v3 = u64(k1 ^ 0x7465646279746573)
length = len(data); blocks = length // 8
for i in range(blocks):
m = struct.unpack_from('<Q', data, i*8)[0]
v3 ^= m
for _ in range(2):
v0=u64(v0+v1); v1=rol64(v1,13); v1^=v0; v0=rol64(v0,32)
v2=u64(v2+v3); v3=rol64(v3,16); v3^=v2
v0=u64(v0+v3); v3=rol64(v3,21); v3^=v0
v2=u64(v2+v1); v1=rol64(v1,17); v1^=v2; v2=rol64(v2,32)
v0 ^= m
last = bytearray(8)
rem = length % 8; start = blocks * 8
for i in range(rem): last[i] = data[start+i]
last[7] = length & 0xFF
m = struct.unpack('<Q', bytes(last))[0]
v3 ^= m
for _ in range(2):
v0=u64(v0+v1); v1=rol64(v1,13); v1^=v0; v0=rol64(v0,32)
v2=u64(v2+v3); v3=rol64(v3,16); v3^=v2
v0=u64(v0+v3); v3=rol64(v3,21); v3^=v0
v2=u64(v2+v1); v1=rol64(v1,17); v1^=v2; v2=rol64(v2,32)
v0 ^= m
v2 ^= 0xFF
for _ in range(4):
v0=u64(v0+v1); v1=rol64(v1,13); v1^=v0; v0=rol64(v0,32)
v2=u64(v2+v3); v3=rol64(v3,16); v3^=v2
v0=u64(v0+v3); v3=rol64(v3,21); v3^=v0
v2=u64(v2+v1); v1=rol64(v1,17); v1^=v2; v2=rol64(v2,32)
return u64(v0^v1^v2^v3)
def splitmix64(state):
state = u64(state + 0x9E3779B97F4A7C15)
z = state
z = u64((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9)
z = u64((z ^ (z >> 27)) * 0x94D049BB133111EB)
return state, z ^ (z >> 31)
def sub_24CB0(data, seed):
h = xxhash.xxh64(data, seed=seed).intdigest()
return u64(h ^ (h >> 32))
def derive_key_sub_2EB00(key_val, qfe8, salt):
eb_k0 = u64(qfe8 ^ 0x1A8CBC5B802E097C)
eb_k1 = u64(qfe8 ^ 0x802E097C1A8CBC5B)
eb_buf = struct.pack('<QQ', key_val, qfe8)
sip = siphash_2_4(eb_buf, eb_k0, eb_k1)
salt_q0, salt_q1 = struct.unpack_from('<QQ', salt, 0)
PRIME1,PRIME2,PRIME4 = 0x9E3779B185EBCA87, 0xC2B2AE3D27D4EB4F, 0x85EBCA77C2B2AE63
PRIME3_xxh, r10 = 0x165667B19E3779F9, 0x27D4EB2F165726CC
s1 = u64(salt_q1*PRIME2); s1=rol64(s1,31); s1=u64(s1*PRIME1)
s0 = u64(salt_q0*PRIME2); s0=rol64(s0,31); s0=u64(s0*PRIME1)
sa = u64(sip*PRIME2); sa=rol64(sa,31); sa=u64(sa*PRIME1)
rax2 = sa ^ r10
rax2=rol64(rax2,27); rax2=u64(rax2*PRIME1); rax2=u64(rax2+PRIME4)
rax2^=s0; rax2=rol64(rax2,27); rax2=u64(rax2*PRIME1); rax2=u64(rax2+PRIME4)
rax2^=s1; rax2=rol64(rax2,27); rax2=u64(rax2*PRIME1); rax2=u64(rax2+PRIME4)
rax2^=rax2>>33; rax2=u64(rax2*PRIME2); rax2^=rax2>>29; rax2=u64(rax2*PRIME3_xxh); rax2^=rax2>>32
rdx = u64(r10+0x1EF) ^ sa
rdx=rol64(rdx,27); rdx=u64(rdx*PRIME1); rdx=u64(rdx+PRIME4)
rdx^=s0; rdx=rol64(rdx,27); rdx=u64(rdx*PRIME1); rdx=u64(rdx+PRIME4)
rdx^=s1; rdx=rol64(rdx,27); rdx=u64(rdx*PRIME1); rdx=u64(rdx+PRIME4)
rdx^=rdx>>33; rdx=u64(rdx*PRIME2); rdx^=rdx>>29; rdx=u64(rdx*PRIME3_xxh); rdx^=rdx>>32
return rax2, rdx
def aes_ctr_decrypt_le(key, nonce_12, data):
"""AES-128-CTR with 12-byte nonce and 4-byte little-endian counter"""
result = bytearray()
counter = 0
for i in range(0, len(data), 16):
block = nonce_12 + struct.pack('<I', counter)
cipher = AES.new(key, AES.MODE_ECB)
keystream = cipher.encrypt(block)
chunk = data[i:i+16]
result.extend(bytes(a ^ b for a, b in zip(chunk, keystream[:len(chunk)])))
counter += 1
return bytes(result)
# Setup
beatmap = struct.pack('<4I', 0, 250, 500, 750)
qfe8 = xxhash.xxh64(beatmap, seed=0x1A8CBC5B802E097C).intdigest()
# sub_2E570: debug key generation
state_r14 = u64(qfe8 ^ 0x1A8CBC5B802E097C)
_, v2 = splitmix64(state_r14)
r15_key = u64(qfe8 ^ 0x802E097C1A8CBC5B)
rcx = v2
for i in range(12):
buf = struct.pack('<QiiII', rcx, i % 4, 0, i, 2)
rcx = siphash_2_4(buf, state_r14, r15_key)
debug_key = rcx
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin", "rb") as f:
hjm_data = f.read()
salt = hjm_data[24:40]
iv_bytes = hjm_data[40:52]
encrypted = hjm_data[52:]
# Try both key variants
for name, key_val in [("debug_key_raw", debug_key), ("debug_key_xor", u64(debug_key ^ 0xA5A5A5A5A5A5A5A5))]:
kp0, kp1 = derive_key_sub_2EB00(key_val, qfe8, salt)
var_68 = struct.pack('<QQ', kp0, kp1)
folded = u32(qfe8 ^ (qfe8 >> 32))
total = len(encrypted)
num_chunks = (total + 255) // 256
decrypted = bytearray(total)
var_E0 = 0
for ci in range(num_chunks):
cs_ = ci * 256
csz = min(256, total - cs_)
# Build 32-byte buffer for sub_24CB0
src = var_68 # 16 bytes
src += struct.pack('<II', ci, u32(qfe8)) # r15d, r13d
src += struct.pack('<Q', u64(var_E0 ^ qfe8)) # var_E0 ^ r13
h1 = sub_24CB0(src, 0x1357)
h2 = sub_24CB0(src, 0x2468)
aes_key = struct.pack('<QQ', h1, h2)
# Build 12-byte nonce
counter_bytes = struct.pack('<II', ci, folded)
iv_mod = bytearray(iv_bytes[:8])
for j in range(8): iv_mod[j] ^= counter_bytes[j]
nonce = bytes(iv_mod) + iv_bytes[8:12]
# Decrypt with LE counter
chunk_dec = aes_ctr_decrypt_le(aes_key, nonce, encrypted[cs_:cs_+csz])
decrypted[cs_:cs_+csz] = chunk_dec
var_E0 = u64(var_E0 + 0x100000000)
# Render as image
img = Image.new('1', (64, 64))
for y in range(64):
for x in range(64):
byte_idx = y * 8 + x // 8
bit_idx = 7 - (x % 8)
pixel = (decrypted[byte_idx] >> bit_idx) & 1
img.putpixel((x, y), pixel)
img.save(rf"D:\AI\AICTF\workdir\52pojie\chu8\bitmap_le_{name}.png")
bits = sum(bin(b).count('1') for b in decrypted)
print(f"{name}: bits={bits}/{total*8} ({bits*100//(total*8)}%), first16={decrypted[:16].hex()}")
# Also try with standard big-endian CTR
decrypted2 = bytearray(total)
var_E0 = 0
for ci in range(num_chunks):
cs_ = ci * 256
csz = min(256, total - cs_)
src = var_68 + struct.pack('<II', ci, u32(qfe8)) + struct.pack('<Q', u64(var_E0 ^ qfe8))
h1 = sub_24CB0(src, 0x1357)
h2 = sub_24CB0(src, 0x2468)
aes_key = struct.pack('<QQ', h1, h2)
counter_bytes = struct.pack('<II', ci, folded)
iv_mod = bytearray(iv_bytes[:8])
for j in range(8): iv_mod[j] ^= counter_bytes[j]
nonce = bytes(iv_mod) + iv_bytes[8:12]
# Use pycryptodome CTR (big-endian counter)
ctr = Counter.new(32, prefix=nonce, initial_value=0, little_endian=False)
cipher = AES.new(aes_key, AES.MODE_CTR, counter=ctr)
decrypted2[cs_:cs_+csz] = cipher.decrypt(encrypted[cs_:cs_+csz])
var_E0 = u64(var_E0 + 0x100000000)
bits2 = sum(bin(b).count('1') for b in decrypted2)
print(f" BE-CTR: bits={bits2}/{total*8} ({bits2*100//(total*8)}%), first16={decrypted2[:16].hex()}")
📋 Python 输出 [14:28:52]
debug_key_raw: bits=1991/4096 (48%), first16=5741b9c6e56997f1873b4a0fe840f085
BE-CTR: bits=1996/4096 (48%), first16=5741b9c6e56997f1873b4a0fe840f085
debug_key_xor: bits=2078/4096 (50%), first16=69437da3956ce847aa29e45c54dc24ca
BE-CTR: bits=2004/4096 (48%), first16=69437da3956ce847aa29e45c54dc24ca
🤖 助手 - 第 93 轮 [14:29:31]
Hmm, the LE and BE CTR give the same first 16 bytes - that's because the first block has counter=0 which is the same in both endiannesses. But neither result looks meaningful. The issue might be in the sub_24CB0 fold. Let me check: the AES key is 16 bytes built from two 8-byte values from sub_24CB0. But sub_24CB0 returns h ^ (h >> 32) which is 64 bits. So the key IS 16 bytes.
Wait - let me re-examine the key more carefully. In the decompiled code:
v20 = (void *)sub_24CB0(src, 32, 4951);
v21 = (void *)sub_24CB0(src, 32, 9320);
ptr[0] = v20;
ptr[1] = v21;
ptr is void *[2] at offset 0x60. Then ptr is passed to sub_2DF40 as rdx (the key pointer). So the AES key is [ptr[0](8 bytes), ptr[1](8 bytes)] = 16 bytes. This is correct.
But wait - the src buffer is MODIFIED between the two calls! Looking at the asm:
2e819: lea rdi, [rsp+src] ; same buffer
2e821: call sub_24CB0 ; first call
2e832: lea rdi, [rsp+src] ; same buffer again
2e844: call sub_24CB0 ; second call
The buffer is the same. But does sub_24CB0 modify it? Looking at the function - it only reads from a1, never writes. So both calls use the same 32-byte buffer. Good.
The problem must be in the key derivation or the nonce. Let me try a completely different approach - use the native library directly via emulation:
🐍 执行 Python 脚本
# Let me verify my SipHash against a known test vector
import struct
def u64(x): return x & 0xFFFFFFFFFFFFFFFF
def rol64(x, n): return u64((x << n) | (x >> (64 - n)))
def siphash_2_4(data, k0, k1):
v0 = u64(k0 ^ 0x736f6d6570736575)
v1 = u64(k1 ^ 0x646f72616e646f6d)
v2 = u64(k0 ^ 0x6c7967656e657261)
v3 = u64(k1 ^ 0x7465646279746573)
length = len(data); blocks = length // 8
for i in range(blocks):
m = struct.unpack_from('<Q', data, i*8)[0]
v3 ^= m
for _ in range(2):
v0=u64(v0+v1); v1=rol64(v1,13); v1^=v0; v0=rol64(v0,32)
v2=u64(v2+v3); v3=rol64(v3,16); v3^=v2
v0=u64(v0+v3); v3=rol64(v3,21); v3^=v0
v2=u64(v2+v1); v1=rol64(v1,17); v1^=v2; v2=rol64(v2,32)
v0 ^= m
last = bytearray(8)
rem = length % 8; start = blocks * 8
for i in range(rem): last[i] = data[start+i]
last[7] = length & 0xFF
m = struct.unpack('<Q', bytes(last))[0]
v3 ^= m
for _ in range(2):
v0=u64(v0+v1); v1=rol64(v1,13); v1^=v0; v0=rol64(v0,32)
v2=u64(v2+v3); v3=rol64(v3,16); v3^=v2
v0=u64(v0+v3); v3=rol64(v3,21); v3^=v0
v2=u64(v2+v1); v1=rol64(v1,17); v1^=v2; v2=rol64(v2,32)
v0 ^= m
v2 ^= 0xFF
for _ in range(4):
v0=u64(v0+v1); v1=rol64(v1,13); v1^=v0; v0=rol64(v0,32)
v2=u64(v2+v3); v3=rol64(v3,16); v3^=v2
v0=u64(v0+v3); v3=rol64(v3,21); v3^=v0
v2=u64(v2+v1); v1=rol64(v1,17); v1^=v2; v2=rol64(v2,32)
return u64(v0^v1^v2^v3)
# Standard SipHash-2-4 test vectors from the paper
# Key: 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
# k0 = 0x0706050403020100, k1 = 0x0f0e0d0c0b0a0908
k0 = 0x0706050403020100
k1 = 0x0f0e0d0c0b0a0908
# Test vector for empty message: expected 0x726fdb47dd0e0e31
result_empty = siphash_2_4(b"", k0, k1)
print(f"SipHash(empty): 0x{result_empty:016x} (expected: 0x726fdb47dd0e0e31)")
# Test vector for message = 00: expected 0x74f839c593dc67fd
result_0 = siphash_2_4(bytes([0]), k0, k1)
print(f"SipHash([0]): 0x{result_0:016x} (expected: 0x74f839c593dc67fd)")
# Test vector for message = 00 01: expected 0x0d6c8009d9a94f5a
result_01 = siphash_2_4(bytes([0, 1]), k0, k1)
print(f"SipHash([0,1]): 0x{result_01:016x} (expected: 0x0d6c8009d9a94f5a)")
# Test vector for message = 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e
result_15 = siphash_2_4(bytes(range(15)), k0, k1)
print(f"SipHash([0..14]): 0x{result_15:016x} (expected: 0xa129ca6149be45e5)")
📋 Python 输出 [14:29:31]
SipHash(empty): 0x726fdb47dd0e0e31 (expected: 0x726fdb47dd0e0e31)
SipHash([0]): 0x74f839c593dc67fd (expected: 0x74f839c593dc67fd)
SipHash([0,1]): 0x0d6c8009d9a94f5a (expected: 0x0d6c8009d9a94f5a)
SipHash([0..14]): 0xa129ca6149be45e5 (expected: 0xa129ca6149be45e5)
🤖 助手 - 第 94 轮 [14:30:03]
SipHash is correct. Let me now verify the sub_2DAF0 function against my implementation by checking the actual assembly more carefully. The function takes (rdi=data, rsi=length, rdx=k0, rcx=k1):
🐍 执行 Python 脚本
依赖: xxhash
# SipHash is verified correct. Let me now trace through sub_2E570 step by step
# to make sure the debug key computation is right.
import struct
def u64(x): return x & 0xFFFFFFFFFFFFFFFF
def rol64(x, n): return u64((x << n) | (x >> (64 - n)))
def siphash_2_4(data, k0, k1):
v0 = u64(k0 ^ 0x736f6d6570736575)
v1 = u64(k1 ^ 0x646f72616e646f6d)
v2 = u64(k0 ^ 0x6c7967656e657261)
v3 = u64(k1 ^ 0x7465646279746573)
length = len(data); blocks = length // 8
for i in range(blocks):
m = struct.unpack_from('<Q', data, i*8)[0]
v3 ^= m
for _ in range(2):
v0=u64(v0+v1); v1=rol64(v1,13); v1^=v0; v0=rol64(v0,32)
v2=u64(v2+v3); v3=rol64(v3,16); v3^=v2
v0=u64(v0+v3); v3=rol64(v3,21); v3^=v0
v2=u64(v2+v1); v1=rol64(v1,17); v1^=v2; v2=rol64(v2,32)
v0 ^= m
last = bytearray(8)
rem = length % 8; start = blocks * 8
for i in range(rem): last[i] = data[start+i]
last[7] = length & 0xFF
m = struct.unpack('<Q', bytes(last))[0]
v3 ^= m
for _ in range(2):
v0=u64(v0+v1); v1=rol64(v1,13); v1^=v0; v0=rol64(v0,32)
v2=u64(v2+v3); v3=rol64(v3,16); v3^=v2
v0=u64(v0+v3); v3=rol64(v3,21); v3^=v0
v2=u64(v2+v1); v1=rol64(v1,17); v1^=v2; v2=rol64(v2,32)
v0 ^= m
v2 ^= 0xFF
for _ in range(4):
v0=u64(v0+v1); v1=rol64(v1,13); v1^=v0; v0=rol64(v0,32)
v2=u64(v2+v3); v3=rol64(v3,16); v3^=v2
v0=u64(v0+v3); v3=rol64(v3,21); v3^=v0
v2=u64(v2+v1); v1=rol64(v1,17); v1^=v2; v2=rol64(v2,32)
return u64(v0^v1^v2^v3)
def splitmix64(state):
state = u64(state + 0x9E3779B97F4A7C15)
z = state
z = u64((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9)
z = u64((z ^ (z >> 27)) * 0x94D049BB133111EB)
return state, z ^ (z >> 31)
# sub_2E570 trace:
# ebp = dword_5CFD0 (number of beats = 4)
# rax = qword_5CFE8
# r14 = 0x1A8CBC5B802E097C ^ rax
# splitmix64(r14) -> rcx
# r15 = 0x802E097C1A8CBC5B ^ rax
import xxhash
beatmap = struct.pack('<4I', 0, 250, 500, 750)
qfe8 = xxhash.xxh64(beatmap, seed=0x1A8CBC5B802E097C).intdigest()
print(f"qfe8 = 0x{qfe8:016x}")
rax = qfe8
r14 = u64(0x1A8CBC5B802E097C ^ rax)
print(f"r14 = 0x{r14:016x}")
# splitmix64
rcx_state = u64(r14 + 0x9E3779B97F4A7C15)
print(f"rcx after add = 0x{rcx_state:016x}")
rdx = rcx_state
rdx = u64((rdx ^ (rdx >> 0x1E)) * 0xBF58476D1CE4E5B9)
print(f"after first mix = 0x{rdx:016x}")
rdx2 = rdx
rdx2 = u64((rdx2 ^ (rdx2 >> 0x1B)) * 0x94D049BB133111EB)
print(f"after second mix = 0x{rdx2:016x}")
rsi = rdx2
rcx_val = rsi
rcx_final = rcx_val ^ (rcx_val >> 0x1F)
print(f"splitmix64 result = 0x{rcx_final:016x}")
# Verify with my function
_, sm_result = splitmix64(r14)
print(f"splitmix64 verify = 0x{sm_result:016x}")
print(f"Match: {rcx_final == sm_result}")
r15 = u64(0x802E097C1A8CBC5B ^ rax)
print(f"r15 = 0x{r15:016x}")
# Loop 12 times
ebp = 4 # dword_5CFD0
rcx = rcx_final
for i in range(12):
# Build 24-byte buffer on stack:
# [0:8] = rcx (var_48)
# [8:12] = i % ebp (var_40, as signed int)
# [12:16] = 0 (var_3C)
# [16:20] = i (var_38, as unsigned int)
# [20:24] = 2 (var_34, as unsigned int)
# sub_2DAF0(rdi=stack_buf, rsi=0x18, rdx=r14, rcx=r15)
buf = struct.pack('<q', rcx if rcx < 2**63 else rcx - 2**64) # as signed? No, just raw bytes
buf = struct.pack('<Q', rcx)
buf += struct.pack('<i', i % ebp) # signed int
buf += struct.pack('<i', 0)
buf += struct.pack('<I', i)
buf += struct.pack('<I', 2)
assert len(buf) == 24
rcx = siphash_2_4(buf, r14, r15)
print(f" iter {i}: buf={buf.hex()}, siphash=0x{rcx:016x}")
debug_key = rcx
print(f"\ndebug_key = 0x{debug_key:016x}")
📋 Python 输出 [14:30:05]
qfe8 = 0x810a3647628f0212
r14 = 0x9b868a1ce2a10b6e
rcx after add = 0x39be03d661eb8783
after first mix = 0x34cbd80bd0b0e78a
after second mix = 0xc74999944c72ee50
splitmix64 result = 0xc7499995c2e1dd78
splitmix64 verify = 0xc7499995c2e1dd78
Match: True
r15 = 0x01243f3b7803be49
iter 0: buf=78dde1c2959949c700000000000000000000000002000000, siphash=0x85cdb77357a99e24
iter 1: buf=249ea95773b7cd8501000000000000000100000002000000, siphash=0xb2c4c086d15850af
iter 2: buf=af5058d186c0c4b202000000000000000200000002000000, siphash=0x95a754caa4f216ce
iter 3: buf=ce16f2a4ca54a79503000000000000000300000002000000, siphash=0x0e5cd4fb7f0d5c57
iter 4: buf=575c0d7ffbd45c0e00000000000000000400000002000000, siphash=0x0e10802628e4256b
iter 5: buf=6b25e4282680100e01000000000000000500000002000000, siphash=0x0d9b5a82357db3e4
iter 6: buf=e4b37d35825a9b0d02000000000000000600000002000000, siphash=0x25c8374871df43ce
iter 7: buf=ce43df714837c82503000000000000000700000002000000, siphash=0x9716de1f0fb6f76d
iter 8: buf=6df7b60f1fde169700000000000000000800000002000000, siphash=0x410fe950e5dc8422
iter 9: buf=2284dce550e90f4101000000000000000900000002000000, siphash=0x08d5fd605a986d2d
iter 10: buf=2d6d985a60fdd50802000000000000000a00000002000000, siphash=0xb0a8da5912961744
iter 11: buf=4417961259daa8b003000000000000000b00000002000000, siphash=0xd2c13a26769ce7e0
debug_key = 0xd2c13a26769ce7e0
🤖 助手 - 第 95 轮 [14:30:13]
Now let me look at the verifyAndDecrypt flow more carefully. I need to understand what happens with qword_5CFE0 vs qword_5CFE8:
🔨 工具调用: mcp_ida-pro-headless_find
- type:
data_ref
- targets:
["0x5CFE0", "0x5CFE8", "0x5CFF0", "0x5CFF8", "0x5D140"]
📋 mcp_ida-pro-headless_find 输出 [14:30:13]
[
{
"query": "0x5CFE0",
"matches": [
"0x23a63",
"0x23c1e",
"0x23c2c",
"0x24662",
"0x2466b",
"0x249a7",
"0x249b0"
],
"count": 7,
"cursor": {
"done": true
},
"error": null
},
{
"query": "0x5CFE8",
"matches": [
"0x23a6a",
"0x23c88",
"0x241b5",
"0x246ee",
"0x24a33",
"0x2e59a",
"0x2e734"
],
"count": 7,
"cursor": {
"done": true
},
"error": null
},
{
"query": "0x5CFF0",
"matches": [
"0x23a71",
"0x243f0",
"0x247b7",
"0x24bda"
],
"count": 4,
"cursor": {
"done": true
},
"error": null
},
{
"query": "0x5CFF8",
"matches": [
"0x23a7c",
"0x243a6",
"0x243e9",
"0x2475d",
"0x24b97"
],
"count": 5,
"cursor": {
"done": true
},
"error": null
},
{
"query": "0x5D140",
"matches": [
"0x24756",
"0x24b8b",
"0x24ca3"
],
"count": 3,
"cursor": {
"done": true
},
"error": null
}
]
🤖 助手 - 第 96 轮 [14:30:19]
🔨 工具调用: mcp_ida-pro-headless_disasm
- addr:
0x249a0
- max_instructions:
50
<details>
<summary>📋 mcp_ida-pro-headless_disasm 输出 [14:30:19](较长,点击展开)</summary>
{
"addr": "0x249a0",
"asm": {
"name": "verifyAndDecrypt",
"start_ea": "0x249a0",
"lines": "verifyAndDecrypt (.text @ 0x249a0):\n249a0 mov cs:byte_5D00C, 1\n249a7 mov rax, cs:qword_5CFE0\n249ae jmp short loc_249C0\n249b0 mov rax, cs:qword_5CFE0\n249b7 cmp cs:byte_5D00C, 1\n249be jnz short loc_24A30\n249c0 shl rcx, 20h\n249c4 movsxd rdx, edx\n249c7 mov rsi, 1A8CBC5B802E097Ch\n249d1 xor rsi, rcx\n249d4 xor rsi, rdx\n249d7 mov rcx, 9E3779B97F4A7C15h\n249e1 add rcx, rsi\n249e4 mov rdx, rcx\n249e7 shr rdx, 1Eh\n249eb xor rdx, rcx\n249ee mov rcx, 0BF58476D1CE4E5B9h\n249f8 imul rcx, rdx\n249fc mov rdx, rcx\n249ff shr rdx, 1Bh\n24a03 xor rdx, rcx\n24a06 mov rcx, 94D049BB133111EBh\n24a10 imul rcx, rdx\n24a14 mov rdx, rcx\n24a17 shr rdx, 1Fh\n24a1b xor rdx, rcx\n24a1e mov r13b, 1\n24a21 jz short loc_24A33\n24a23 shrd rcx, rdx, 23h\n24a28 xor rax, rdx\n24a2b xor rax, rcx\n24a2e jmp short loc_24A33\n24a30 xor r13d, r13d\n24a33 mov cs:qword_5CFE8, rax\n24a3a cmp dword ptr [rbx+8], 0\n24a3e jz loc_24BB4\n24a44 mov r12d, [rbx+0Ch]\n24a48 test r12d, r12d\n24a4b jz loc_24BB4\n24a51 mov edx, [rbx+10h]\n24a54 test edx, edx\n24a56 jz loc_24BB4\n24a5c mov eax, [rbx+4]\n24a5f cmp eax, 2\n24a62 jz loc_24B8B\n24a68 cmp eax, 1\n24a6b jnz loc_24BB4\n24a71 mov [rsp+88h+var_80], edx\n24a75 lea rsi, [rbx+18h]\n24a79 lea rdx, [rbx+28h]",
"stack_frame": [
{
"name": "var_80",
"offset": "0x8",
"size": "0x4",
"type": "_DWORD"
},
{
"name": "var_7C",
"offset": "0xc",
"size": "0x4",
"type": "_DWORD"
},
{
"name": "var_78",
"offset": "0x10",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_70",
"offset": "0x18",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_68",
"offset": "0x20",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_60",
"offset": "0x28",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "s2",
"offset": "0x30",
"size": "0x8",
"type": "void *"
},
{
"name": "var_50",
"offset": "0x38",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_38",
"offset": "0x50",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "__return_address",
"offset": "0x88",
"size": "0x8",
"type": "_UNKNOWN *"
}
],
"return_type": "__int64",
"arguments": [
{
"name": "arg0",
"type": "__int64 *"
},
{
"name": "arg1",
"type": "__int64"
},
{
"name": "arg2",
"type": "__int64"
},
{
"name": "arg3",
"type": "__int64"
}
]
},
"instruction_count": 50,
"total_instructions": null,
"cursor": {
"next": 50
}
}
</details>
👤 用户 [14:30:19]
[TodoList Reminder - Round 57]
Stats: 3 pending, 1 done, 0 failed.
You have 3 pending items — execute them before adding new ones.
Do NOT repeat failed approaches. If stuck, reset your TodoList with a new strategy.
🤖 助手 - 第 97 轮 [14:31:12]
<details>
<summary>💭 思考过程</summary>
Let me step back and think about this more carefully. I've been trying to reverse-engineer the native library's decryption logic, but I'm getting stuck on details. Let me reconsider the approach.
The key insight I'm missing might be about qword_5CFE8. Looking at the code:
- At 0x249a7/0x249b0:
rax = qword_5CFE0 (which is the XXHash64 of beatmap)
- At 0x249c0-0x24a2b: A splitmix64-like transformation is applied to modify
rax
- At 0x24a33:
qword_5CFE8 = rax (the modified value)
So qword_5CFE8 is NOT the same as qword_5CFE0! It gets modified by the splitmix64 step. But in my code, I set qfe8 = qfe0 = xxhash64(beatmap, seed). This is wrong!
Let me trace the exact transformation:
rax = qword_5CFE0 (the original xxhash)
rcx = dword_5D004 << 32 (some game state)
rdx = sign_extend(dword_5D008) (experience counter)
rsi = 0x1A8CBC5B802E097C ^ rcx ^ rdx
- Then splitmix64 on rsi -> gives rcx, rdx
- If result is non-zero (r13b=1):
shrd rcx, rdx, 0x23 (shift right double)
rax ^= rdx
rax ^= rcx
qword_5CFE8 = rax
So for a fresh install where dword_5D004=0 and dword_5D008=0:
- rcx = 0 << 32 = 0
- rdx = 0
- rsi = 0x1A8CBC5B802E097C ^ 0 ^ 0 = 0x1A8CBC5B802E097C
- splitmix64(0x1A8CBC5B802E097C) -> some value
- Then rax gets XORed with the splitmix result
But wait, for a fresh install, byte_5D00C = 0, so at 0x249b7-0x249be:
- cmp byte_5D00C, 1
- jnz 0x24a30 (jump to xor r13d, r13d)
So r13d = 0, and we skip the XOR modification. qword_5CFE8 = qword_5CFE0 unchanged.
But wait, what about the first time? The flow is:
- sub_24FC0 is called, returns some value
- The return value updates dword_5D004 and dword_5D008
- If dword_5D008 >= 4, byte_5D00C = 1
For the FIRST call ever, dword_5D008 starts at 0. sub_24FC0 returns something, and the high 32 bits are added to dword_5D008. If the result is < 4, byte_5D00C stays 0.
Actually, let me look at this differently. The challenge is about decrypting hjm_pack.bin. The file has type=2
... [thinking truncated, was 4285 chars] ...
24bf1: cmovz rsi, rax (if r13b=0, use raw key)
24bf5: ...
24bfa: call sub_2E680
So the key passed to sub_2E680 depends on r13b:
- r13b=1: key = rax ^ 0xA5A5A5A5A5A5A5A5
- r13b=0: key = rax (raw)
Now, for the debug bypass path with byte_5D140=1:
- rax = sub_2E570() = debug_key
- r13b depends on the password verification at 0x249c0-0x24a2b
For a fresh install with dword_5D008 < 4 and byte_5D00C = 0:
- At 0x249b7: byte_5D00C != 1, so jump to 0x24a30
- r13d = 0
- qword_5CFE8 = qword_5CFE0 (unchanged)
So r13b = 0, and the key is raw (no XOR).
But wait - I already tried this ("debug_key raw") and got noise. Let me reconsider.
Actually, maybe the issue is that qword_5CFE8 IS modified. Let me look at where qword_5CFE8 is set. At 0x24a33: `mov cs:qword_5CFE8, rax`. And rax comes from qword_5CFE0 at 0x249a7 or 0x249b0.
But wait - there's also 0x23a6a and 0x23c88 and 0x241b5 and 0x246ee that reference qword_5CFE8. Let me check if it's set elsewhere.
Actually, let me look at 0x246ee:
</details>
Let me take a different approach. Instead of trying to perfectly replicate the native code, let me use Unicorn to emulate the critical functions directly:
### 🐍 执行 Python 脚本
```python
# Let me check where qword_5CFE8 is written (not just read)
# Looking at the xrefs, let me check 0x23a6a and 0x246ee
import struct
# First, let me re-examine the initialization. qword_5CFE0 and qword_5CFE8
# might be set in the startSession function.
# Let me look at the function that sets these values.
# Actually, let me try a completely different approach.
# Instead of reimplementing everything, let me use unicorn to emulate the native code.
# First, let me read the .so file
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhjm.so", "rb") as f:
so_data = f.read()
print(f"SO file size: {len(so_data)} bytes")
# Check ELF header
magic = so_data[:4]
print(f"Magic: {magic}")
ei_class = so_data[4] # 1=32bit, 2=64bit
print(f"Class: {'64-bit' if ei_class == 2 else '32-bit'}")
# Get section headers to find .text
import struct
e_shoff = struct.unpack_from('<Q', so_data, 0x28)[0]
e_shentsize = struct.unpack_from('<H', so_data, 0x3A)[0]
e_shnum = struct.unpack_from('<H', so_data, 0x3C)[0]
e_shstrndx = struct.unpack_from('<H', so_data, 0x3E)[0]
print(f"Section headers at: 0x{e_shoff:x}, count: {e_shnum}, size: {e_shentsize}")
# Get program headers for loading
e_phoff = struct.unpack_from('<Q', so_data, 0x20)[0]
e_phentsize = struct.unpack_from('<H', so_data, 0x36)[0]
e_phnum = struct.unpack_from('<H', so_data, 0x38)[0]
print(f"\nProgram headers at: 0x{e_phoff:x}, count: {e_phnum}")
for i in range(e_phnum):
off = e_phoff + i * e_phentsize
p_type = struct.unpack_from('<I', so_data, off)[0]
p_flags = struct.unpack_from('<I', so_data, off+4)[0]
p_offset = struct.unpack_from('<Q', so_data, off+8)[0]
p_vaddr = struct.unpack_from('<Q', so_data, off+16)[0]
p_filesz = struct.unpack_from('<Q', so_data, off+32)[0]
p_memsz = struct.unpack_from('<Q', so_data, off+40)[0]
if p_type == 1: # PT_LOAD
print(f" LOAD: offset=0x{p_offset:x} vaddr=0x{p_vaddr:x} filesz=0x{p_filesz:x} memsz=0x{p_memsz:x} flags=0x{p_flags:x}")
📋 Python 输出 [14:31:12]
Traceback (most recent call last):
File "C:\Users\WOYOUY~1\AppData\Local\Temp\ctf_agent_1289445898.py", line 15, in <module>
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhjm.so", "rb") as f:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'D:\\AI\\AICTF\\workdir\\52pojie\\chu8\\apk_unzipped\\lib\\x86_64\\libhjm.so'
[error: exit status 1]
🤖 助手 - 第 98 轮 [14:31:30]
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8 (模式: libhjm*)
📋 查找结果 [14:31:30]
No files found.
🤖 助手 - 第 99 轮 [14:31:34]
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8 (模式: *.so)
📋 查找结果 [14:31:34]
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\arm64-v8a\libhajimi.so
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\armeabi-v7a\libhajimi.so
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86\libhajimi.so
D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so
🤖 助手 - 第 100 轮 [14:31:45]
🐍 执行 Python 脚本
import struct
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so", "rb") as f:
so_data = f.read()
# Get program headers for loading
e_phoff = struct.unpack_from('<Q', so_data, 0x20)[0]
e_phentsize = struct.unpack_from('<H', so_data, 0x36)[0]
e_phnum = struct.unpack_from('<H', so_data, 0x38)[0]
segments = []
for i in range(e_phnum):
off = e_phoff + i * e_phentsize
p_type = struct.unpack_from('<I', so_data, off)[0]
p_flags = struct.unpack_from('<I', so_data, off+4)[0]
p_offset = struct.unpack_from('<Q', so_data, off+8)[0]
p_vaddr = struct.unpack_from('<Q', so_data, off+16)[0]
p_filesz = struct.unpack_from('<Q', so_data, off+32)[0]
p_memsz = struct.unpack_from('<Q', so_data, off+40)[0]
if p_type == 1: # PT_LOAD
segments.append((p_vaddr, p_offset, p_filesz, p_memsz, p_flags))
print(f"LOAD: vaddr=0x{p_vaddr:x} offset=0x{p_offset:x} filesz=0x{p_filesz:x} memsz=0x{p_memsz:x}")
print(f"\nFile size: {len(so_data)}")
📋 Python 输出 [14:31:45]
LOAD: vaddr=0x0 offset=0x0 filesz=0x56570 memsz=0x56570
LOAD: vaddr=0x57570 offset=0x56570 filesz=0x4728 memsz=0x4728
LOAD: vaddr=0x5cca0 offset=0x5aca0 filesz=0xe8 memsz=0xfc0
File size: 374392
👤 用户 [14:31:45]
[Progress Checkpoint - Round 61/200]
Briefly assess your progress:
- What have you accomplished so far?
- Are you making progress toward the flag?
Continue with your current approach if it shows promise.
🤖 助手 - 第 101 轮 [14:32:20]
<details>
<summary>💭 思考过程</summary>
Let me take stock. I've been trying to reimplement the native crypto functions in Python but getting wrong results. The Unicorn emulation approach is the right call - let me emulate the critical functions directly from the .so binary instead of trying to manually reimplement everything.
Let me set up Unicorn to emulate the x86_64 .so file and call the key functions directly.
</details>
Progress: SipHash/XXHash verified correct, but full decryption chain still produces noise. Switching to Unicorn emulation to avoid reimplementation errors.
🐍 执行 Python 脚本
依赖: unicorn
from unicorn import *
from unicorn.x86_const import *
import struct
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so", "rb") as f:
so_data = f.read()
BASE = 0x10000000
mu = Uc(UC_ARCH_X86, UC_MODE_64)
# Map segments
segments = [(0x0, 0x0, 0x56570, 0x56570), (0x57570, 0x56570, 0x4728, 0x4728), (0x5cca0, 0x5aca0, 0xe8, 0xfc0)]
for vaddr, foff, fsz, msz in segments:
aligned_start = (BASE + vaddr) & ~0xFFF
aligned_end = ((BASE + vaddr + msz) + 0xFFF) & ~0xFFF
try:
mu.mem_map(aligned_start, aligned_end - aligned_start)
except:
pass
mu.mem_write(BASE + vaddr, so_data[foff:foff+fsz])
# Stack
STACK_BASE = 0x7FFF0000
STACK_SIZE = 0x100000
mu.mem_map(STACK_BASE, STACK_SIZE)
mu.reg_write(UC_X86_REG_RSP, STACK_BASE + STACK_SIZE - 0x1000)
# Heap for data
HEAP = 0x20000000
mu.mem_map(HEAP, 0x100000)
# Set up FS segment for stack canary (fs:0x28)
FS_BASE = 0x30000000
mu.mem_map(FS_BASE, 0x1000)
mu.mem_write(FS_BASE + 0x28, struct.pack('<Q', 0xDEADBEEFCAFEBABE))
mu.msr_write(0xC0000100, FS_BASE) # FS base MSR
# Test: call sub_2DAF0 (SipHash) with known test vector
# sub_2DAF0(rdi=data, rsi=length, rdx=k0, rcx=k1)
test_key = bytes(range(16))
k0 = struct.unpack_from('<Q', test_key, 0)[0]
k1 = struct.unpack_from('<Q', test_key, 8)[0]
test_msg = bytes(range(15))
mu.mem_write(HEAP, test_msg)
mu.reg_write(UC_X86_REG_RDI, HEAP)
mu.reg_write(UC_X86_REG_RSI, len(test_msg))
mu.reg_write(UC_X86_REG_RDX, k0)
mu.reg_write(UC_X86_REG_RCX, k1)
# Push return address
ret_addr = 0x40000000
mu.mem_map(ret_addr, 0x1000)
mu.mem_write(ret_addr, b'\xf4') # HLT
rsp = mu.reg_read(UC_X86_REG_RSP) - 8
mu.mem_write(rsp, struct.pack('<Q', ret_addr))
mu.reg_write(UC_X86_REG_RSP, rsp)
try:
mu.emu_start(BASE + 0x2DAF0, ret_addr, timeout=5000000)
result = mu.reg_read(UC_X86_REG_RAX)
print(f"SipHash emulated: 0x{result:016x}")
print(f"Expected: 0xa129ca6149be45e5")
except UcError as e:
rip = mu.reg_read(UC_X86_REG_RIP)
print(f"Error at 0x{rip:x}: {e}")
📋 Python 输出 [14:32:22]
SipHash emulated: 0xa129ca6149be45e5
Expected: 0xa129ca6149be45e5
🤖 助手 - 第 102 轮 [14:33:19]
Unicorn works. Now let me emulate the full key derivation chain:
🐍 执行 Python 脚本
依赖: unicorn,xxhash
from unicorn import *
from unicorn.x86_const import *
import struct
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so", "rb") as f:
so_data = f.read()
BASE = 0x10000000
STACK_BASE = 0x7FFF0000; STACK_SIZE = 0x100000
HEAP = 0x20000000; FS_BASE = 0x30000000
RET_ADDR = 0x40000000
def setup_emu():
mu = Uc(UC_ARCH_X86, UC_MODE_64)
segs = [(0x0,0x0,0x56570,0x56570),(0x57570,0x56570,0x4728,0x4728),(0x5cca0,0x5aca0,0xe8,0xfc0)]
mapped = set()
for va,fo,fs,ms in segs:
s = (BASE+va)&~0xFFF; e = ((BASE+va+ms)+0xFFF)&~0xFFF
for pg in range(s,e,0x1000):
if pg not in mapped:
try: mu.mem_map(pg, 0x1000)
except: pass
mapped.add(pg)
mu.mem_write(BASE+va, so_data[fo:fo+fs])
mu.mem_map(STACK_BASE, STACK_SIZE)
mu.mem_map(HEAP, 0x100000)
mu.mem_map(FS_BASE, 0x1000)
mu.mem_write(FS_BASE+0x28, struct.pack('<Q', 0xDEADBEEFCAFEBABE))
mu.msr_write(0xC0000100, FS_BASE)
mu.mem_map(RET_ADDR, 0x1000)
mu.mem_write(RET_ADDR, b'\xf4')
return mu
def call_func(mu, addr, args_regs=None):
if args_regs:
reg_order = [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 i, v in enumerate(args_regs):
mu.reg_write(reg_order[i], v)
rsp = STACK_BASE + STACK_SIZE - 0x1000
mu.mem_write(rsp, struct.pack('<Q', RET_ADDR))
mu.reg_write(UC_X86_REG_RSP, rsp)
mu.emu_start(BASE + addr, RET_ADDR, timeout=10000000)
return mu.reg_read(UC_X86_REG_RAX)
# Step 1: Set up global state
# beatMap = [0, 250, 500, 750], dword_5CFD0 = 4
# qword_5CFE0 and qword_5CFE8 need to be set
import xxhash
beatmap = struct.pack('<4I', 0, 250, 500, 750)
qfe8 = xxhash.xxh64(beatmap, seed=0x1A8CBC5B802E097C).intdigest()
print(f"qfe8 = 0x{qfe8:016x}")
mu = setup_emu()
# Set globals
mu.mem_write(BASE + 0x5CFD0, struct.pack('<I', 4)) # dword_5CFD0 = 4 (beat count)
mu.mem_write(BASE + 0x5CFE0, struct.pack('<Q', qfe8)) # qword_5CFE0
mu.mem_write(BASE + 0x5CFE8, struct.pack('<Q', qfe8)) # qword_5CFE8
mu.mem_write(BASE + 0x5D140, struct.pack('<B', 1)) # byte_5D140 = 1 (debug bypass)
# Step 2: Call sub_2E570 to get debug key
debug_key = call_func(mu, 0x2E570)
print(f"debug_key = 0x{debug_key:016x}")
# Step 3: Now simulate verifyAndDecrypt path
# At 0x24be1: rsi = 0xA5A5A5A5A5A5A5A5 ^ rax (if r13b=1)
# or rsi = rax (if r13b=0)
# Then call sub_2E680(rdi=&file_data, rsi=key_val)
# But first I need to understand what qword_5CFE0 is.
# Looking at 0x249a7: rax = qword_5CFE0
# Then at 0x249c0: shl rcx, 0x20; movsxd rdx, edx
# rcx and edx come from the caller (arg2, arg3 of verifyAndDecrypt)
# rsi = 0x1A8CBC5B802E097C ^ rcx ^ rdx
# Then splitmix64(rsi) -> rdx
# If rdx != 0: r13b = 1, and rax ^= rdx, rax ^= shrd(rcx, rdx, 0x23)
# Then qword_5CFE8 = rax
# So qword_5CFE8 depends on the password verification!
# The password args (rcx, edx) modify qfe8 via splitmix64 + XOR.
# For the debug path, we skip password verification, so qfe8 = qfe0 (unchanged).
# Let me check: in the debug path at 0x24b8b:
# al = byte_5D140 (=1 for debug)
# test al, al -> setnz cl -> cl=1
# or cl, byte_5CFF8 -> cl |= byte_5CFF8
# or cl, r13b -> cl |= r13b
# test cl, 1 -> jz fail (won't jump since cl=1)
# test al, al -> jz 24bda (won't jump since al=1)
# call sub_2E570 -> rax = debug_key
# rsi = 0xA5A5A5A5A5A5A5A5
# xor rsi, rax -> rsi = debug_key ^ 0xA5A5A5A5A5A5A5A5
# test r13b, r13b -> cmovz rsi, rax
# If r13b=0: rsi = rax (raw debug_key)
# If r13b=1: rsi = debug_key ^ 0xA5A5A5A5A5A5A5A5
# r13b is set at 0x24a1e or 0x24a30
# At 0x24a30: xor r13d, r13d (r13b=0) when byte_5D00C != 1
# byte_5D00C is set to 1 at 0x249a0 when dword_5D008 >= 4
# dword_5D008 is the experience counter. For debug bypass, it might be 0.
# So byte_5D00C = 0, and we go to 0x24a30: r13b = 0
# Therefore: rsi = rax = debug_key (NO XOR)
# BUT WAIT: at 0x249a0, byte_5D00C is set to 1 BEFORE the splitmix64 code.
# The flow is:
# 0x24898: cmp dword_5D008, 4
# 0x2489f: jge 0x249a0 (if exp >= 4, set byte_5D00C=1, then do splitmix)
# 0x248a5: ... (if exp < 4, different path)
# For debug bypass with exp=0: we DON'T go to 0x249a0.
# Instead we go through the other path. Let me check what happens.
# Actually, let me just check: what is byte_5D00C initially? It's in BSS, so 0.
# And dword_5D008 is also 0.
# So at 0x24898: 0 < 4, so we DON'T jump to 0x249a0.
# We continue at 0x248a5.
# Let me look at 0x248a5:
print("\nNow checking the flow when exp < 4...")
📋 Python 输出 [14:33:23]
qfe8 = 0x810a3647628f0212
debug_key = 0xd2c13a26769ce7e0
Now checking the flow when exp < 4...
🤖 助手 - 第 103 轮 [14:33:39]
🔨 工具调用: mcp_ida-pro-headless_disasm
- addr:
0x24890
- max_instructions:
30
📋 mcp_ida-pro-headless_disasm 输出 [14:33:39]
{
"addr": "0x24890",
"asm": {
"name": "verifyAndDecrypt",
"start_ea": "0x24890",
"lines": "verifyAndDecrypt (.text @ 0x24890):\n24890 call qword ptr [rax+558h]\n24896 test eax, eax\n24898 jle loc_24934\n2489e mov ebp, eax\n248a0 mov ebx, eax\n248a2 mov rdi, rbx; size\n248a5 call __Znwm; operator new(ulong)\n248aa mov [rsp+88h+var_70], rax\n248af lea r12, [rax+rbx]\n248b3 mov [rsp+88h+var_60], r12\n248b8 mov rdi, rax; s\n248bb xor esi, esi; c\n248bd mov [rsp+88h+var_78], rbx\n248c2 mov rdx, rbx; n\n248c5 mov rbx, rax\n248c8 call _memset\n248cd mov [rsp+88h+var_68], r12\n248d2 mov rax, [r14]\n248d5 mov rdi, r14\n248d8 mov rsi, r13\n248db xor edx, edx\n248dd mov ecx, ebp\n248df mov r8, rbx\n248e2 call qword ptr [rax+640h]\n248e8 cmp ebp, 33h ; '3'\n248eb jbe short loc_248F5\n248ed cmp dword ptr [rbx], 314D4A48h\n248f3 jz short loc_24967\n248f5 mov rax, [r14]\n248f8 mov rdi, r14",
"stack_frame": [
{
"name": "var_80",
"offset": "0x8",
"size": "0x4",
"type": "_DWORD"
},
{
"name": "var_7C",
"offset": "0xc",
"size": "0x4",
"type": "_DWORD"
},
{
"name": "var_78",
"offset": "0x10",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_70",
"offset": "0x18",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_68",
"offset": "0x20",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_60",
"offset": "0x28",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "s2",
"offset": "0x30",
"size": "0x8",
"type": "void *"
},
{
"name": "var_50",
"offset": "0x38",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_38",
"offset": "0x50",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "__return_address",
"offset": "0x88",
"size": "0x8",
"type": "_UNKNOWN *"
}
],
"return_type": "__int64",
"arguments": [
{
"name": "arg0",
"type": "__int64 *"
},
{
"name": "arg1",
"type": "__int64"
},
{
"name": "arg2",
"type": "__int64"
},
{
"name": "arg3",
"type": "__int64"
}
]
},
"instruction_count": 30,
"total_instructions": null,
"cursor": {
"next": 30
}
}
🤖 助手 - 第 104 轮 [14:34:20]
I see - this function calls virtual methods (vtable calls at [rax+558h] and [rax+640h]). These are JNI calls to Java. The flow is complex. Let me instead focus on emulating just sub_2E680 which is the actual decryption function, and pass it the right key:
🐍 执行 Python 脚本
依赖: unicorn,xxhash
from unicorn import *
from unicorn.x86_const import *
import struct, xxhash
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so", "rb") as f:
so_data = f.read()
BASE = 0x10000000
STACK_BASE = 0x7FFF0000; STACK_SIZE = 0x200000
HEAP = 0x20000000; FS_BASE = 0x30000000; RET_ADDR = 0x40000000
mu = Uc(UC_ARCH_X86, UC_MODE_64)
mapped = set()
segs = [(0x0,0x0,0x56570,0x56570),(0x57570,0x56570,0x4728,0x4728),(0x5cca0,0x5aca0,0xe8,0xfc0)]
for va,fo,fs,ms in segs:
s=(BASE+va)&~0xFFF; e=((BASE+va+ms)+0xFFF)&~0xFFF
for pg in range(s,e,0x1000):
if pg not in mapped:
mu.mem_map(pg,0x1000); mapped.add(pg)
mu.mem_write(BASE+va, so_data[fo:fo+fs])
mu.mem_map(STACK_BASE, STACK_SIZE)
mu.mem_map(HEAP, 0x200000)
mu.mem_map(FS_BASE, 0x1000)
mu.mem_write(FS_BASE+0x28, struct.pack('<Q', 0xDEADBEEFCAFEBABE))
mu.msr_write(0xC0000100, FS_BASE)
mu.mem_map(RET_ADDR, 0x1000)
mu.mem_write(RET_ADDR, b'\xcc')
# Read hjm_pack.bin
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin", "rb") as f:
hjm_data = f.read()
# Set up globals
beatmap = struct.pack('<4I', 0, 250, 500, 750)
qfe8 = xxhash.xxh64(beatmap, seed=0x1A8CBC5B802E097C).intdigest()
mu.mem_write(BASE + 0x5CFD0, struct.pack('<I', 4))
mu.mem_write(BASE + 0x5CFE0, struct.pack('<Q', qfe8))
mu.mem_write(BASE + 0x5CFE8, struct.pack('<Q', qfe8))
# sub_2E680 signature: sub_2E680(_QWORD *a1, __int64 a2)
# a1 = pointer to a structure containing the file data (like a std::vector)
# a2 = the key value (rsi)
# a1 points to a structure where:
# a1[0] = pointer to start of data
# a1[1] = pointer to end of data
# The data must be >= 0x34 bytes and start with magic 0x314D4A48
# Set up the file data in heap
FILE_DATA = HEAP + 0x10000
mu.mem_write(FILE_DATA, hjm_data)
# Set up the a1 structure (two pointers: start, end)
A1_STRUCT = HEAP + 0x20000
mu.mem_write(A1_STRUCT, struct.pack('<QQ', FILE_DATA, FILE_DATA + len(hjm_data)))
# The key value - try debug_key (raw, no XOR)
debug_key = 0xd2c13a26769ce7e0
# Hook operator new and operator delete to provide heap memory
NEW_HEAP = HEAP + 0x30000
new_offset = [0]
def hook_code(uc, address, size, user_data):
pass
# We need to handle calls to operator new, operator delete, memcpy, memset
# Let me patch the PLT entries to return immediately
# Find PLT entries - let me just hook specific addresses
# sub_2E680 calls:
# - sub_2EB00 (key derivation)
# - sub_24CB0 (xxhash fold)
# - sub_2DF40 (AES-CTR)
# - operator new, operator delete, memcpy, memset (via PLT)
# Let me check what external calls sub_2E680 makes
print("Testing emulation of sub_2E680...")
# Set up call
rsp = STACK_BASE + STACK_SIZE - 0x2000
mu.mem_write(rsp, struct.pack('<Q', RET_ADDR))
mu.reg_write(UC_X86_REG_RSP, rsp)
mu.reg_write(UC_X86_REG_RDI, A1_STRUCT)
mu.reg_write(UC_X86_REG_RSI, debug_key)
# Hook to intercept PLT calls
call_log = []
def hook_mem_invalid(uc, access, address, size, value, user_data):
rip = uc.reg_read(UC_X86_REG_RIP)
call_log.append(f"Invalid mem access at RIP=0x{rip:x}, addr=0x{address:x}, access={access}")
return False
mu.hook_add(UC_HOOK_MEM_UNMAPPED, hook_mem_invalid)
try:
mu.emu_start(BASE + 0x2E680, RET_ADDR, timeout=10000000, count=50000)
rip = mu.reg_read(UC_X86_REG_RIP)
if rip == RET_ADDR:
result = mu.reg_read(UC_X86_REG_RAX)
print(f"sub_2E680 returned: 0x{result:x}")
else:
print(f"Stopped at RIP=0x{rip:x}")
except UcError as e:
rip = mu.reg_read(UC_X86_REG_RIP)
print(f"Error at RIP=0x{rip:x}: {e}")
if call_log:
for l in call_log[-5:]: print(f" {l}")
📋 Python 输出 [14:34:24]
Testing emulation of sub_2E680...
Error at RIP=0x1002e86f: Invalid instruction (UC_ERR_INSN_INVALID)
👤 用户 [14:34:24]
[TodoList Reminder - Round 65]
Stats: 3 pending, 1 done, 0 failed.
You have 3 pending items — execute them before adding new ones.
Do NOT repeat failed approaches. If stuck, reset your TodoList with a new strategy.
🤖 助手 - 第 105 轮 [14:34:44]
🔨 工具调用: mcp_ida-pro-headless_disasm
- addr:
0x2E860
- max_instructions:
20
<details>
<summary>📋 mcp_ida-pro-headless_disasm 输出 [14:34:44](较长,点击展开)</summary>
{
"addr": "0x2E860",
"asm": {
"name": "sub_2E680",
"start_ea": "0x2e860",
"lines": "sub_2E680 (.text @ 0x2e860):\n2e861 mov rax, [r14]\n2e864 mov ecx, r15d\n2e867 shr ecx, 8\n2e86a movd xmm0, r15d\n2e86f pinsrb xmm0, ecx, 1\n2e875 mov ecx, r15d\n2e878 shr ecx, 10h\n2e87b pinsrb xmm0, ecx, 2\n2e881 mov ecx, r15d\n2e884 shr ecx, 18h\n2e887 pinsrb xmm0, ecx, 3\n2e88d mov [rsp+0E8h+src], rax\n2e895 movzx ecx, byte ptr [rsp+0E8h+var_A8]\n2e89a pinsrb xmm0, ecx, 4\n2e8a0 movzx ecx, byte ptr [rsp+0E8h+var_B0]\n2e8a5 pinsrb xmm0, ecx, 5\n2e8ab mov ecx, [r14+8]\n2e8af mov dword ptr [rsp+0E8h+src+8], ecx\n2e8b6 movzx ecx, byte ptr [rsp+0E8h+var_B8]\n2e8bb pinsrb xmm0, ecx, 6",
"stack_frame": [
{
"name": "var_E8",
"offset": "0x0",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_E0",
"offset": "0x8",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "n",
"offset": "0x10",
"size": "0x8",
"type": "size_t"
},
{
"name": "var_D0",
"offset": "0x18",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_C8",
"offset": "0x20",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_C0",
"offset": "0x28",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_B8",
"offset": "0x30",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_B0",
"offset": "0x38",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_A8",
"offset": "0x40",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_A0",
"offset": "0x48",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_98",
"offset": "0x50",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_90",
"offset": "0x58",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "ptr",
"offset": "0x60",
"size": "0x10",
"type": "void *[2]"
},
{
"name": "var_78",
"offset": "0x70",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "var_68",
"offset": "0x80",
"size": "0x10",
"type": "_OWORD"
},
{
"name": "src",
"offset": "0x90",
"size": "0x10",
"type": "void *[2]"
},
{
"name": "var_48",
"offset": "0xa0",
"size": "0x10",
"type": "_OWORD"
},
{
"name": "var_38",
"offset": "0xb0",
"size": "0x8",
"type": "_QWORD"
},
{
"name": "__return_address",
"offset": "0xe8",
"size": "0x8",
"type": "_UNKNOWN *"
}
],
"return_type": "__int64",
"arguments": [
{
"name": "arg0",
"type": "_QWORD *"
},
{
"name": "arg1",
"type": "__int64"
}
]
},
"instruction_count": 20,
"total_instructions": null,
"cursor": {
"next": 20
}
}
</details>
🤖 助手 - 第 106 轮 [14:35:19]
The pinsrb instruction is SSE4.1 - Unicorn might not support it. Let me check the error address and try a different approach - emulate with Qiling or just carefully trace the IV construction:
🐍 执行 Python 脚本
依赖: unicorn,xxhash
# The pinsrb at 0x2e86f is the issue. Let me look at what it's doing:
# It's building an XMM0 register byte-by-byte from r15d (chunk counter) and
# the folded qfe8 value. Then XORing with the IV.
#
# xmm0[0] = r15d & 0xFF
# xmm0[1] = (r15d >> 8) & 0xFF
# xmm0[2] = (r15d >> 16) & 0xFF
# xmm0[3] = (r15d >> 24) & 0xFF
# xmm0[4] = var_A8 byte (from folded qfe8)
# xmm0[5] = var_B0 byte
# xmm0[6] = var_B8 byte
# xmm0[7] = var_C0 byte
#
# So xmm0[0:4] = r15d (chunk counter as LE bytes)
# xmm0[4:8] = folded qfe8 as LE bytes
#
# This is exactly what I had: struct.pack('<II', chunk_counter, folded_qfe8)
# XORed with the first 8 bytes of the IV.
#
# My implementation was correct for this part. The issue must be in sub_2EB00
# (key derivation) or sub_24CB0 (xxhash fold for AES key).
#
# Let me try emulating sub_2EB00 with Unicorn, avoiding the SSE4.1 issue.
# sub_2EB00 doesn't use pinsrb.
from unicorn import *
from unicorn.x86_const import *
import struct, xxhash
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so", "rb") as f:
so_data = f.read()
BASE = 0x10000000
STACK_BASE = 0x7FFF0000; STACK_SIZE = 0x200000
HEAP = 0x20000000; FS_BASE = 0x30000000; RET_ADDR = 0x40000000
mu = Uc(UC_ARCH_X86, UC_MODE_64)
mapped = set()
segs = [(0x0,0x0,0x56570,0x56570),(0x57570,0x56570,0x4728,0x4728),(0x5cca0,0x5aca0,0xe8,0xfc0)]
for va,fo,fs,ms in segs:
s=(BASE+va)&~0xFFF; e=((BASE+va+ms)+0xFFF)&~0xFFF
for pg in range(s,e,0x1000):
if pg not in mapped:
mu.mem_map(pg,0x1000); mapped.add(pg)
mu.mem_write(BASE+va, so_data[fo:fo+fs])
mu.mem_map(STACK_BASE, STACK_SIZE)
mu.mem_map(HEAP, 0x200000)
mu.mem_map(FS_BASE, 0x1000)
mu.mem_write(FS_BASE+0x28, struct.pack('<Q', 0xDEADBEEFCAFEBABE))
mu.msr_write(0xC0000100, FS_BASE)
mu.mem_map(RET_ADDR, 0x1000)
mu.mem_write(RET_ADDR, b'\xf4')
beatmap = struct.pack('<4I', 0, 250, 500, 750)
qfe8 = xxhash.xxh64(beatmap, seed=0x1A8CBC5B802E097C).intdigest()
debug_key = 0xd2c13a26769ce7e0
# sub_2EB00(arg0=key_val, arg1=qfe8, arg2=out_ptr, arg3=out_ptr2)
# rdi=key_val, rsi=qfe8, rdx=out_ptr (for first 8 bytes), rcx=out_ptr2 (for second 8 bytes)
# Wait, let me re-check the signature from the decompiled code
# sub_2EB00 args: (arg0=__int64, arg1=__int64, arg2=_QWORD*, arg3=unsigned __int64*)
# rdi=key_val, rsi=qfe8, rdx=salt_ptr, rcx=out_ptr
# Actually looking at the asm at 0x2eb00:
# rbx = rcx (arg3 = out_ptr)
# r14 = rdx (arg2 = salt_ptr)
# rdx = 0x1A8CBC5B802E097C ^ rsi (rsi=arg1=qfe8)
# rcx = 0x802E097C1A8CBC5B ^ rsi
# [rsp] = rdi (arg0=key_val)
# [rsp+8] = rsi (arg1=qfe8)
# Then calls sub_2DAF0(rdi=rsp, rsi=0x10, rdx, rcx) - SipHash of 16-byte buffer
# So sub_2EB00(key_val, qfe8, salt_ptr, out_ptr)
# It computes SipHash of [key_val, qfe8] with keys derived from qfe8
# Then does some XXHash-like mixing with the salt
# Let me set up the salt from the file
salt = bytes.fromhex('0f1e2d3c4b5a69788796a5b4c3d2e1f0') # from hjm_pack.bin offset 24
OUT1 = HEAP + 0x1000
OUT2 = HEAP + 0x1008
rsp = STACK_BASE + STACK_SIZE - 0x2000
mu.mem_write(rsp, struct.pack('<Q', RET_ADDR))
mu.reg_write(UC_X86_REG_RSP, rsp)
mu.reg_write(UC_X86_REG_RDI, debug_key)
mu.reg_write(UC_X86_REG_RSI, qfe8)
mu.reg_write(UC_X86_REG_RDX, HEAP + 0x2000) # salt pointer
mu.reg_write(UC_X86_REG_RCX, HEAP + 0x3000) # output pointer
# Write salt
mu.mem_write(HEAP + 0x2000, salt)
# Output buffer
mu.mem_write(HEAP + 0x3000, b'\x00' * 16)
try:
mu.emu_start(BASE + 0x2EB00, RET_ADDR, timeout=10000000)
rip = mu.reg_read(UC_X86_REG_RIP)
if rip == RET_ADDR:
out_data = mu.mem_read(HEAP + 0x3000, 16)
print(f"sub_2EB00 output: {bytes(out_data).hex()}")
q0, q1 = struct.unpack('<QQ', bytes(out_data))
print(f" q0 = 0x{q0:016x}")
print(f" q1 = 0x{q1:016x}")
else:
print(f"Stopped at RIP=0x{rip:x}")
except UcError as e:
rip = mu.reg_read(UC_X86_REG_RIP)
print(f"Error at RIP=0x{rip:x}: {e}")
📋 Python 输出 [14:35:23]
sub_2EB00 output: 7f721557831c2dbd7f70756f448aee26
q0 = 0xbd2d1c835715727f
q1 = 0x26ee8a446f75707f
🤖 助手 - 第 107 轮 [14:35:54]
Now I have the correct output from sub_2EB00 via emulation. Let me also emulate sub_24CB0 and then do the full decryption:
🐍 执行 Python 脚本
依赖: unicorn,xxhash,pycryptodome
from unicorn import *
from unicorn.x86_const import *
import struct, xxhash
from Crypto.Cipher import AES
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\lib\x86_64\libhajimi.so", "rb") as f:
so_data = f.read()
BASE = 0x10000000
STACK_BASE = 0x7FFF0000; STACK_SIZE = 0x200000
HEAP = 0x20000000; FS_BASE = 0x30000000; RET_ADDR = 0x40000000
def make_emu():
mu = Uc(UC_ARCH_X86, UC_MODE_64)
mapped = set()
segs = [(0x0,0x0,0x56570,0x56570),(0x57570,0x56570,0x4728,0x4728),(0x5cca0,0x5aca0,0xe8,0xfc0)]
for va,fo,fs,ms in segs:
s=(BASE+va)&~0xFFF; e=((BASE+va+ms)+0xFFF)&~0xFFF
for pg in range(s,e,0x1000):
if pg not in mapped:
mu.mem_map(pg,0x1000); mapped.add(pg)
mu.mem_write(BASE+va, so_data[fo:fo+fs])
mu.mem_map(STACK_BASE, STACK_SIZE)
mu.mem_map(HEAP, 0x200000)
mu.mem_map(FS_BASE, 0x1000)
mu.mem_write(FS_BASE+0x28, struct.pack('<Q', 0xDEADBEEFCAFEBABE))
mu.msr_write(0xC0000100, FS_BASE)
mu.mem_map(RET_ADDR, 0x1000)
mu.mem_write(RET_ADDR, b'\xf4')
return mu
def call_func(mu, addr, rdi=0, rsi=0, rdx=0, rcx=0, r8=0, r9=0):
rsp = STACK_BASE + STACK_SIZE - 0x2000
mu.mem_write(rsp, struct.pack('<Q', RET_ADDR))
mu.reg_write(UC_X86_REG_RSP, rsp)
mu.reg_write(UC_X86_REG_RDI, rdi)
mu.reg_write(UC_X86_REG_RSI, rsi)
mu.reg_write(UC_X86_REG_RDX, rdx)
mu.reg_write(UC_X86_REG_RCX, rcx)
mu.reg_write(UC_X86_REG_R8, r8)
mu.reg_write(UC_X86_REG_R9, r9)
mu.emu_start(BASE + addr, RET_ADDR, timeout=10000000)
return mu.reg_read(UC_X86_REG_RAX)
# Read file
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\apk_unzipped\assets\hjm_pack.bin", "rb") as f:
hjm = f.read()
beatmap = struct.pack('<4I', 0, 250, 500, 750)
qfe8 = xxhash.xxh64(beatmap, seed=0x1A8CBC5B802E097C).intdigest()
debug_key = 0xd2c13a26769ce7e0
salt = hjm[24:40]
iv_bytes = hjm[40:52]
encrypted = hjm[52:]
# Step 1: sub_2EB00(debug_key, qfe8, salt_ptr, out_ptr) -> var_68
mu = make_emu()
mu.mem_write(HEAP + 0x2000, salt)
mu.mem_write(HEAP + 0x3000, b'\x00' * 16)
call_func(mu, 0x2EB00, rdi=debug_key, rsi=qfe8, rdx=HEAP+0x2000, rcx=HEAP+0x3000)
var_68 = bytes(mu.mem_read(HEAP + 0x3000, 16))
print(f"var_68 = {var_68.hex()}")
# Step 2: For each chunk, compute AES key via sub_24CB0
folded = (qfe8 ^ (qfe8 >> 32)) & 0xFFFFFFFF
total = len(encrypted)
num_chunks = (total + 255) // 256
decrypted = bytearray(total)
var_E0 = 0
for ci in range(num_chunks):
# Build 32-byte src buffer
src = var_68 # 16 bytes
src += struct.pack('<II', ci, qfe8 & 0xFFFFFFFF) # r15d, r13d (low 32 of qfe8)
src += struct.pack('<Q', (var_E0 ^ qfe8) & 0xFFFFFFFFFFFFFFFF)
mu2 = make_emu()
mu2.mem_write(HEAP + 0x4000, src)
# sub_24CB0(data_ptr, length, seed)
h1 = call_func(mu2, 0x24CB0, rdi=HEAP+0x4000, rsi=32, rdx=0x1357)
mu3 = make_emu()
mu3.mem_write(HEAP + 0x4000, src)
h2 = call_func(mu3, 0x24CB0, rdi=HEAP+0x4000, rsi=32, rdx=0x2468)
aes_key = struct.pack('<QQ', h1, h2)
# Build 12-byte nonce
counter_bytes = struct.pack('<II', ci, folded)
iv_mod = bytearray(iv_bytes[:8])
for j in range(8): iv_mod[j] ^= counter_bytes[j]
nonce = bytes(iv_mod) + iv_bytes[8:12]
# AES-CTR decrypt (LE counter)
cs = ci * 256
csz = min(256, total - cs)
for bi in range(0, csz, 16):
block = nonce + struct.pack('<I', bi // 16)
cipher = AES.new(aes_key, AES.MODE_ECB)
ks = cipher.encrypt(block)
for k in range(min(16, csz - bi)):
decrypted[cs + bi + k] = encrypted[cs + bi + k] ^ ks[k]
var_E0 += 0x100000000
if ci == 0:
print(f"Chunk 0: key={aes_key.hex()}, nonce={nonce.hex()}")
print(f" h1=0x{h1:016x}, h2=0x{h2:016x}")
print(f" first16={decrypted[:16].hex()}")
# Check result
bits = sum(bin(b).count('1') for b in decrypted)
print(f"\nTotal bits set: {bits}/{total*8} ({bits*100//(total*8)}%)")
print(f"First 32 bytes: {decrypted[:32].hex()}")
print(f"Last 32 bytes: {decrypted[-32:].hex()}")
# Save
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin", "wb") as f:
f.write(decrypted)
📋 Python 输出 [14:36:01]
var_68 = 7f721557831c2dbd7f70756f448aee26
Chunk 0: key=16d6d0d72e99938d72d9bcd5616451ef, nonce=112233440052f26b99aabbcc
h1=0x8d93992ed7d0d616, h2=0xef516461d5bcd972
first16=00000000000000000000000000000000
Total bits set: 314/4096 (7%)
First 32 bytes: 0000000000000000000000000000000000000000000000000000000000000000
Last 32 bytes: 0000000000000000000000000000000000000000000000000000000000000000
🤖 助手 - 第 108 轮 [14:36:16]
Only 7% bits set and lots of zeros! This looks like it could be a QR code or sparse bitmap. Let me render it:
🐍 执行 Python 脚本
依赖: Pillow
from PIL import Image
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin", "rb") as f:
data = f.read()
# 64x64 bitmap, 1 bit per pixel = 512 bytes
# Try different bit orderings
for name, get_pixel in [
("MSB_first", lambda y,x: (data[y*8 + x//8] >> (7 - x%8)) & 1),
("LSB_first", lambda y,x: (data[y*8 + x//8] >> (x%8)) & 1),
("MSB_inv", lambda y,x: 1 - ((data[y*8 + x//8] >> (7 - x%8)) & 1)),
("LSB_inv", lambda y,x: 1 - ((data[y*8 + x//8] >> (x%8)) & 1)),
]:
img = Image.new('1', (64, 64))
for y in range(64):
for x in range(64):
img.putpixel((x, y), get_pixel(y, x))
fn = rf"D:\AI\AICTF\workdir\52pojie\chu8\qr_{name}.png"
img_scaled = img.resize((640, 640), Image.NEAREST)
img_scaled.save(fn)
# Print ASCII art for MSB_inv (white=space, black=█)
print("MSB_inv (inverted, likely QR):")
for y in range(64):
row = ""
for x in range(64):
bit = (data[y*8 + x//8] >> (7 - x%8)) & 1
row += " " if bit else "█"
print(row)
<details>
<summary>📋 Python 输出 [14:36:18](较长,点击展开)</summary>
MSB_inv (inverted, likely QR):
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
██ █ ██████ ███ ████ ██ ███ ███ █ ███ █ ███ ██ ████
██ █████ █████ ███ █ ███ ███ ███ ███ ████ ██ █ █ ███ █ ███ ███
██ ██ █████ ███ █ ███████ ███ ████ ██ █ █ █ ███ █ ███ ███
██ █████ █████ █ █ ██ ████ ███ ████ ██ ███ █ █ █ █ ███
██ █████ █████ ███ █ ███ ███ ███ ███ ████ ██ ███ █ █ █ █ ███ ███
██ █████ █████ ███ █ ███ ███ ███ ███ █ ██ ██ ███ █ █ █ ███ ███
██ █████ █ ███ ██ ████ ██ ███ ██ ███ ███ █ ███ █ ███ ███
████████████████████████████████████████████████████████████████
██ ████ ██ ███ ███ ███ ██ ███ █ ██ █████ ████
██ ███ ████ ██ ███ █ ███ █ ███ █ █████ ██ █ ███ █ █████ ███ ███
██ ███ ████ ██████ █ ██ █████ █ █████ █ █ █ ███ █ █████ ███ ███
██ █████ █████ ██ █ █ ████ ██ ██ ██ █ ██ █████ ███ ███
██ ████████ ████ ███ ██ ███ ███ ███ █ ███ █ ███ █ █████ ███ ███
██ █████ ██ ███ ████ ███ ██ ████ ███ █ ███ █ ███ █ █████ ███ ███
██ ██████ ███ ██ ██ ██ ██ ███ █ ██ █ ████
████████████████████████████████████████████████████████████████
██████████████████████████████ ████████████████████████████████
███████████████████████████████ ████████████████████████████████
███████████████████████████████ ████████████████████████████████
████████████████████████████████ ███████████████████████████████
███████████████████████████████ ████████████████████████████████
███████████████████████████████ ████████████████████████████████
██████████████████████████████ ████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
████████████████████████████████████████████████████████████████
</details>
🤖 助手 - 第 109 轮 [14:36:37]
I can see a QR code pattern in rows 20-35! But it's only partially decrypted - the rest is all 1s (encrypted data still). The issue is that only the first chunk (256 bytes) decrypted correctly, but the second chunk didn't. Wait - 512 bytes total, 2 chunks. Let me check chunk 1:
🐍 执行 Python 脚本
# The bitmap shows QR-like patterns in rows 20-35 (bytes 160-280 roughly)
# But rows 0-19 and 36-63 are all white (all 1s when inverted = all 0s in data)
# This means the data IS mostly zeros with a QR code in the middle.
# But the QR code seems incomplete/corrupted.
# Wait - looking more carefully, the QR code IS there but it's only in the middle portion.
# The rest being all zeros is correct - it's a 64x64 image with a QR code centered in it.
# The QR code appears to be roughly 44x16 pixels, which is unusual.
# Actually, looking at the pattern more carefully:
# Rows 20-35 have the QR-like pattern (16 rows)
# Rows 36-42 have a few scattered bits
# This looks like a valid QR code that's been placed in the center of a 64x64 canvas
# Let me try to scan it
import struct
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin", "rb") as f:
data = f.read()
# Count non-zero bytes per row
for y in range(64):
row_data = data[y*8:(y+1)*8]
if any(b != 0 for b in row_data):
bits = ''.join(str((row_data[x//8] >> (7-x%8)) & 1) for x in range(64))
print(f"Row {y:2d}: {bits}")
📋 Python 输出 [14:36:37]
Row 20: 0011111010000001110001110000110010001000111010001010001001110000
Row 21: 0010000010000010001010001000100010001000010011011010001010001000
Row 22: 0011110010000010001010000000100011111000010010101010001010001000
Row 23: 0010000010000011111010111001000010001000010010001010101011111000
Row 24: 0010000010000010001010001000100010001000010010001010101010001000
Row 25: 0010000010000010001010001000100010001010010010001011011010001000
Row 26: 0010000011111010001001110000110010001001100010001010001010001000
Row 28: 0011110000111001110001110001110001110010001011110010000011110000
Row 29: 0010001000010010001010001010001010000011001010001010000010001000
Row 30: 0010001000010000001010011000001010000010101010001010000010001000
Row 31: 0011110000010000010010101000010011110010011011110010000010001000
Row 32: 0010000000010000100011001000100010001010001010001010000010001000
Row 33: 0010000010010001000010001001000010001010001010001010000010001000
Row 34: 0010000001100011111001110011111001110010001011110011111011110000
Row 36: 0000000000000000000000000000001100000000000000000000000000000000
Row 37: 0000000000000000000000000000000100000000000000000000000000000000
Row 38: 0000000000000000000000000000000100000000000000000000000000000000
Row 39: 0000000000000000000000000000000010000000000000000000000000000000
Row 40: 0000000000000000000000000000000100000000000000000000000000000000
Row 41: 0000000000000000000000000000000100000000000000000000000000000000
Row 42: 0000000000000000000000000000001100000000000000000000000000000000
🤖 助手 - 第 110 轮 [14:36:55]
This is NOT a QR code - it looks like ASCII art rendered as a bitmap! The patterns in rows 20-34 look like text characters. Let me try to interpret them:
🐍 执行 Python 脚本
依赖: Pillow
# The bitmap has text rendered in it. Let me try to read it.
# Each character appears to be about 8 pixels wide and 7 pixels tall (rows 20-26, 28-34)
# There's a gap at row 27 (separator between two lines of text)
# Let me extract the two text blocks and try OCR or visual analysis
from PIL import Image
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin", "rb") as f:
data = f.read()
# Create a clean image of just the text area, scaled up
img = Image.new('L', (64, 64), 255)
for y in range(64):
for x in range(64):
bit = (data[y*8 + x//8] >> (7 - x%8)) & 1
if bit:
img.putpixel((x, y), 0) # black for 1-bits
# Scale up 10x for readability
img_big = img.resize((640, 640), Image.NEAREST)
img_big.save(r"D:\AI\AICTF\workdir\52pojie\chu8\text_bitmap.png")
# Crop just the text area (rows 20-34, all 64 columns)
img_text = img.crop((0, 20, 64, 35))
img_text_big = img_text.resize((640, 150), Image.NEAREST)
img_text_big.save(r"D:\AI\AICTF\workdir\52pojie\chu8\text_cropped.png")
# Also the vertical line in rows 36-42
print("Vertical line area (rows 36-42):")
for y in range(36, 43):
row_data = data[y*8:(y+1)*8]
bits = ''.join('█' if (row_data[x//8] >> (7-x%8)) & 1 else ' ' for x in range(64))
print(f" {bits}")
print("\nText area rows 20-26 (line 1):")
for y in range(20, 27):
row_data = data[y*8:(y+1)*8]
bits = ''.join('█' if (row_data[x//8] >> (7-x%8)) & 1 else ' ' for x in range(64))
print(f" {bits}")
print("\nText area rows 28-34 (line 2):")
for y in range(28, 35):
row_data = data[y*8:(y+1)*8]
bits = ''.join('█' if (row_data[x//8] >> (7-x%8)) & 1 else ' ' for x in range(64))
print(f" {bits}")
📋 Python 输出 [14:36:57]
Vertical line area (rows 36-42):
██
█
█
█
█
█
██
Text area rows 20-26 (line 1):
█████ █ ███ ███ ██ █ █ ███ █ █ █ █ ███
█ █ █ █ █ █ █ █ █ █ ██ ██ █ █ █ █
████ █ █ █ █ █ █████ █ █ █ █ █ █ █ █
█ █ █████ █ ███ █ █ █ █ █ █ █ █ █ █████
█ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █
█ █ █ █ █ █ █ █ █ █ █ █ █ ██ ██ █ █
█ █████ █ █ ███ ██ █ █ ██ █ █ █ █ █ █
Text area rows 28-34 (line 2):
████ ███ ███ ███ ███ ███ █ █ ████ █ ████
█ █ █ █ █ █ █ █ █ █ ██ █ █ █ █ █ █
█ █ █ █ █ ██ █ █ █ █ █ █ █ █ █ █
████ █ █ █ █ █ █ ████ █ ██ ████ █ █ █
█ █ █ ██ █ █ █ █ █ █ █ █ █ █ █
█ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █
█ ██ █████ ███ █████ ███ █ █ ████ █████ ████
🤖 助手 - 第 111 轮 [14:37:09]
I can read the text! It's rendered in a pixel font:
Line 1: FLAG{HIMAJIMWA
Wait, let me read more carefully...
Line 1: FLAG{HIMAJIMWA → Actually: `FLAG HIMU
👤 用户 [14:37:31]
继续
👤 用户 [14:37:31]
[System — Mandatory Planning Phase]
Before executing ANY tools, you MUST first create a structured solving plan:
Step 1: Read the challenge description carefully — identify category, key constraints, and attack surface
Step 2: If attachments exist, download and examine them FIRST (file type, checksec, strings) before planning
Step 3: Based on gathered info, use todolist (action: reset) to create 3-5 concrete, ORDERED solving steps
Step 4: Rank steps from most-likely-to-succeed to least-likely
Step 5: Only THEN begin executing the first step
🧠 Planning Quality Checklist:
- Each step should be a TESTABLE hypothesis (e.g., 'Test for SQL injection on /login param'), not vague (e.g., 'Try web attacks')
- Include WHAT tool to use and WHAT to look for in each step
- First step should always be information gathering (read source, analyze binary, fetch target)
- Have at least one backup approach from a different attack angle
Previous solving ideas for this challenge:
✅ [386ab2cd-ecc0-40dd-8909-86213a0e3f3c] Extract and analyze APK file structure to identify obfuscation, encryption, or anti-analysis techniq → APK structure extraction complete with obfuscation and anti-analysis patterns confirmed
✅ [227240ce-0eae-4068-a78a-85084825d9ae] Decompile APK using apktool or jadx to examine source code and identify vulnerability vectors → jadx decompilation successful, vulnerability vectors in JNI calls identified
🔄 [d68e4418-0810-414c-9b85-9626c3306d73] Analyze NativeBridge native method calls to identify JNI exploitation vectors or native code vulnera → verifyAndDecrypt at 0x249a0 disassembled; beatMap timing correlation validation ongoing
🔄 [3feefef8-9e1e-422e-9b9b-7b60e43a4f5d] Examine MainActivity imports and obfuscated class references (f1.h, Q0.AbstractC0080d, etc.) to map → Obfuscated class mapping progressing; verifyAndDecrypt context extends analysis
🔄 [d112ede0-8135-40cb-a480-c00601aaa18b] Reverse engineer NativeBridge JNI calls to locate native library (.so) and analyze for buffer overfl → Buffer overflow vectors identified; race condition window timing requires validation
🔄 [9f0076f9-2570-4ff7-8ada-419bd017c95c] Disassemble JNI_OnLoad at 0x237c0 to identify native library loading mechanism and potential ROP gad → JNI_OnLoad disassembly obtained; verifyAndDecrypt analysis extends native library context
🔄 [642a6e58-cb27-4ee4-b907-936b98b5807f] Analyze renamed functions (startSessionBytes, checkRhyt...) for cryptographic or session management → Crypto/session functions mapped; sub_2E680 cross-reference with beatMap intervals requires validation
🔄 [7b22170f-d1cc-4d97-b9e9-d4b0021b7665] Analyze setDebugBypass at 0x24ca0 to determine if byte_5D140 flag can be exploited to disable runtim → setDebugBypass flag enforcement identified; beatMap timing correlation analysis ongoing
🔄 [61c6ba93-bdd1-4e3d-a8bc-12024b29ec18] Analyze call sites of sub_2EFD0 (0x2EFD0) to determine buffer size validation and identify potential → hjm_pack.bin decryption flow analysis ongoing; sub_2E680 buffer processing correlation in progress
❌ [67eacfbe-fb16-4383-820c-402005c0d570] Trace Q0.r and Q0.y instantiation paths to identify entry points for NativeBridge method injection o → Q0.r/Q0.y instantiation search returned 0 matches; likely dynamically generated or requires binary instrumentation approach
❌ [e0e7a83d-f515-4101-bd5b-754a009b9eb8] Analyze InterfaceC0603z implementation in Q0.r and Q0.y to determine if interface methods can be hoo → InterfaceC0603z search returned 0 matches; interface appears dynamically generated or obfuscated beyond static analysis
🔄 [77e77496-7d3d-4e2b-b182-f935e38d202c] Exploit beatMap timing intervals (250ms) as race condition window to inject code during NativeBridge → beatMap 250ms intervals confirmed; native code instrumentation needed to validate race condition
🔄 [84069d04-ee99-441a-ab12-33357e0432bd] Disassemble sub_2E680 (0x2E680) buffer operations to correlate with beatMap 250ms intervals and iden → sub_2E680 disassembly obtained; buffer operation correlation with beatMap 250ms intervals in progress
🔄 [282bf5e8-8b34-4a55-8825-24710a0bed13] Instrument setDebugBypass flag enforcement at 0x24ca0 to measure timing correlation with beatMap int → setDebugBypass flag at 0x24ca0 identified; SipHash emulation validated; timing instrumentation context extended
📊 Stats: 0 pending, 10 testing, 2 failed, 2 verified, 0 skipped (active: 10/10)
⚠️ Review these ideas carefully:
- Skip approaches marked as 'failed' — they already proved unsuccessful
- Prioritize 'pending' ideas — they haven't been tried yet
- Build your plan around untried approaches
Do NOT skip planning. A structured approach prevents wasting rounds on random attempts.
🤖 助手 - 第 112 轮 [14:37:53]
I can clearly read the bitmap text! Let me carefully decode it:
🐍 执行 Python 脚本
# Let me carefully read each character from the bitmap
# Line 1 (rows 20-26): Each char is ~5-6 pixels wide with 1px spacing
# Let me extract columns for each character
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin", "rb") as f:
data = f.read()
# Print line 1 with column numbers
print("Line 1 (rows 20-26):")
print("Col: ", end="")
for x in range(64):
print(x % 10, end="")
print()
for y in range(20, 27):
row_data = data[y*8:(y+1)*8]
print(f"R{y}: ", end="")
for x in range(64):
bit = (row_data[x//8] >> (7 - x%8)) & 1
print('█' if bit else ' ', end="")
print()
print("\nLine 2 (rows 28-34):")
print("Col: ", end="")
for x in range(64):
print(x % 10, end="")
print()
for y in range(28, 35):
row_data = data[y*8:(y+1)*8]
print(f"R{y}: ", end="")
for x in range(64):
bit = (row_data[x//8] >> (7 - x%8)) & 1
print('█' if bit else ' ', end="")
print()
📋 Python 输出 [14:37:53]
Line 1 (rows 20-26):
Col: 0123456789012345678901234567890123456789012345678901234567890123
R20: █████ █ ███ ███ ██ █ █ ███ █ █ █ █ ███
R21: █ █ █ █ █ █ █ █ █ █ ██ ██ █ █ █ █
R22: ████ █ █ █ █ █ █████ █ █ █ █ █ █ █ █
R23: █ █ █████ █ ███ █ █ █ █ █ █ █ █ █ █████
R24: █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █
R25: █ █ █ █ █ █ █ █ █ █ █ █ █ ██ ██ █ █
R26: █ █████ █ █ ███ ██ █ █ ██ █ █ █ █ █ █
Line 2 (rows 28-34):
Col: 0123456789012345678901234567890123456789012345678901234567890123
R28: ████ ███ ███ ███ ███ ███ █ █ ████ █ ████
R29: █ █ █ █ █ █ █ █ █ █ ██ █ █ █ █ █ █
R30: █ █ █ █ █ ██ █ █ █ █ █ █ █ █ █ █
R31: ████ █ █ █ █ █ █ ████ █ ██ ████ █ █ █
R32: █ █ █ ██ █ █ █ █ █ █ █ █ █ █ █
R33: █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █
R34: █ ██ █████ ███ █████ ███ █ █ ████ █████ ████
👤 用户 [14:37:57]
<historical_experience>
⚠️ 严禁将以下案例中的具体值(XOR key、checksum、flag 字符串、密码、偏移量)直接用于当前题目。仅用于识别技术方向和分析思路。
以下是从历史解题中提取的技术模式,仅供思路启发,必须结合当前题目独立分析,不要照搬历史解法:
[cases] chu8: Android game with native HJM1 bitmap decryption via XXHash64+SipHash+AES-CTR (relevance: 0.56)
Problem: Decrypt a 64x64 bitmap from hjm_pack.bin (HJM1 format) in Android game. Key insight: beatMap data (rhythm notes) is the only input needed; key derivation is deterministic and doesn't depend on runtime state. Solution: (1) extract beatMap=[0,250,500,750] from Java code, (2) implement XXHash64+SipHash-2-4+AES-CTR pipeline, (3) parse HJM1 file format to extract salt/IV/ciphertext, (4) decrypt to get bitmap, (5) render as QR code or image to find flag. Pitfall: initially tried to trace complex runtime state (startSession, checkRhythm, updateExp) but these are red herrings; the debug bypass path (sub_2E570) shows key generation is purely mathematical.
[cases] chu7: UPX-packed PE32+ with CRC64 key derivation and custom stream cipher using AES S-box (relevance: 0.47)
Windows GUI program CM1.exe encrypted flag.png using a custom algorithm. Key insight: UPX packing concealed real code; after unpacking, found hardcoded key prefix '52pojie2026' combined with user password to derive CRC64 hash (0xC96C5795D7870F42 polynomial), which initializes a stream cipher using AES S-box for keystream generation. PNG magic bytes (89504e47...) serve as known plaintext to recover stream cipher key 0x55a4f867ba4475dd via XOR. Solution: Unpack binary, extract CRC64 polynomial and S-box at 0x14000A270, implement stream cipher logic (ROL3 + S-box substitution + XOR with IV), use known plaintext attack to recover CRC64 state, decrypt file, verify CRC32 checksum, and extract flag from PNG tEXt chunk metadata.
[cases] 初二题目 PE32 XOR password recovery (relevance: 0.41)
Windows PE32 reverse challenge where hardcoded DWORDs were XORed to recover password. Binary had Chinese filename causing path issues. Solution extracted 6 DWORD values from binary and XORed them sequentially.
[cases] Windows PE32 crackme with XOR-obfuscated password and weighted checksum validation (relevance: 0.41)
52pojie Spring Festival CTF 'Day 2' challenge: PE32 console app with fake flag hint, 31-char password requirement, XOR-deobfuscation (key 0x42), and weighted checksum (sum of position*value = 44709). Key insight: ignore fake flag, reverse sub_401620's DWORD buffer XOR logic.
[cases] reverse1: Simple character replacement validation in PE x64 binary (relevance: 0.40)
Challenge: PE x64 Windows executable that validates user input against a hardcoded flag. Key insight: The flag string is stored in the binary's data section and can be extracted directly via IDA Pro. The validation logic replaces all 'o' characters with '0' before comparison. Solution: Use IDA Pro headless to decompile main function, identify the global string variable (Str2), extract its value, apply the character replacement transformation, and wrap with flag{} format.
</historical_experience>
👤 用户 [14:38:18]
已经出了正确答案是:FLAG{HJMWAPJ2026NBLD} 启动验证流程
🤖 助手 - 第 113 轮 [14:38:24]
Now I can clearly read the text from the bitmap:
Line 1: FLAG{HIMAJIMWA → Let me read more carefully...
🐍 执行 Python 脚本
# Let me carefully identify each character by column ranges
# Line 1 characters (cols 2-57 approximately):
# Looking at the pixel patterns:
# Cols 2-6: F (█████, █, ████, █, █, █, █)
# Cols 8-12: L (█, █, █, █, █, █, █████)
# Cols 14-18: A (███, █ █, █ █, █████, █ █, █ █, █ █) -> wait
# Actually let me look at it differently
# Col 2-6:
# R20: █████ -> F
# R21: █
# R22: ████
# R23: █
# R24: █
# R25: █
# R26: █
# Col 8-12:
# R20: █ -> L
# R21: █
# R22: █
# R23: █
# R24: █
# R25: █
# R26: █████
# Col 14-18:
# R20: ███ -> A
# R21: █ █
# R22: █ █
# R23: █████
# R24: █ █
# R25: █ █
# R26: █ █
# Col 20-24:
# R20: ███ -> G
# R21: █ █
# R22: █
# R23: █ ███
# R24: █ █
# R25: █ █
# R26: ███
# Col 26-29:
# R20: ██ -> {
# R21: █
# R22: █
# R23: █
# R24: █
# R25: █
# R26: ██
# Col 31-35:
# R20: █ █ -> H
# R21: █ █
# R22: █████
# R23: █ █
# R24: █ █
# R25: █ █
# R26: █ █
# Col 37-39:
# R20: ███ -> I? No...
# R21: █
# R22: █
# R23: █
# R24: █
# R25: █ █ -> wait, that's J? No...
# R26: ██
# Hmm, let me look at this more carefully with exact column positions
for y in range(20, 27):
row_data = data[y*8:(y+1)*8]
s = ""
for x in range(64):
bit = (row_data[x//8] >> (7 - x%8)) & 1
s += '█' if bit else '·'
print(f"R{y}: {s}")
print()
for y in range(28, 35):
row_data = data[y*8:(y+1)*8]
s = ""
for x in range(64):
bit = (row_data[x//8] >> (7 - x%8)) & 1
s += '█' if bit else '·'
print(f"R{y}: {s}")
📋 Python 输出 [14:38:24]
Traceback (most recent call last):
File "C:\Users\WOYOUY~1\AppData\Local\Temp\ctf_agent_2198965434.py", line 78, in <module>
row_data = data[y*8:(y+1)*8]
^^^^
NameError: name 'data' is not defined
[error: exit status 1]
🤖 助手 - 第 114 轮 [14:38:45]
🐍 执行 Python 脚本
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin", "rb") as f:
data = f.read()
print("Line 1:")
for y in range(20, 27):
row_data = data[y*8:(y+1)*8]
s = ""
for x in range(64):
bit = (row_data[x//8] >> (7 - x%8)) & 1
s += '█' if bit else '·'
print(f"R{y}: {s}")
print("\nLine 2:")
for y in range(28, 35):
row_data = data[y*8:(y+1)*8]
s = ""
for x in range(64):
bit = (row_data[x//8] >> (7 - x%8)) & 1
s += '█' if bit else '·'
print(f"R{y}: {s}")
# Now let me try OCR on the scaled image
📋 Python 输出 [14:38:45]
Line 1:
R20: ··█████·█······███···███····██··█···█···███·█···█·█···█··███····
R21: ··█·····█·····█···█·█···█···█···█···█····█··██·██·█···█·█···█···
R22: ··████··█·····█···█·█·······█···█████····█··█·█·█·█···█·█···█···
R23: ··█·····█·····█████·█·███··█····█···█····█··█···█·█·█·█·█████···
R24: ··█·····█·····█···█·█···█···█···█···█····█··█···█·█·█·█·█···█···
R25: ··█·····█·····█···█·█···█···█···█···█·█··█··█···█·██·██·█···█···
R26: ··█·····█████·█···█··███····██··█···█··██···█···█·█···█·█···█···
Line 2:
R28: ··████····███··███···███···███···███··█···█·████··█·····████····
R29: ··█···█····█··█···█·█···█·█···█·█·····██··█·█···█·█·····█···█···
R30: ··█···█····█······█·█··██·····█·█·····█·█·█·█···█·█·····█···█···
R31: ··████·····█·····█··█·█·█····█··████··█··██·████··█·····█···█···
R32: ··█········█····█···██··█···█···█···█·█···█·█···█·█·····█···█···
R33: ··█·····█··█···█····█···█··█····█···█·█···█·█···█·█·····█···█···
R34: ··█······██···█████··███··█████··███··█···█·████··█████·████····
🤖 助手 - 第 115 轮 [14:38:58]
Now let me carefully read each character:
🐍 执行 Python 脚本
# Line 1 character analysis (7 rows high, reading column by column):
# Cols 2-6: F L A G { H I M W A (reading the pixel font)
# Let me segment by looking for blank columns (all dots)
line1_rows = []
line2_rows = []
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin", "rb") as f:
data = f.read()
for y in range(20, 27):
row = []
for x in range(64):
bit = (data[y*8 + x//8] >> (7 - x%8)) & 1
row.append(bit)
line1_rows.append(row)
for y in range(28, 35):
row = []
for x in range(64):
bit = (data[y*8 + x//8] >> (7 - x%8)) & 1
row.append(bit)
line2_rows.append(row)
# Find character boundaries by looking for blank columns
def find_chars(rows):
chars = []
in_char = False
start = 0
for x in range(64):
col_has_pixel = any(rows[y][x] for y in range(len(rows)))
if col_has_pixel and not in_char:
start = x
in_char = True
elif not col_has_pixel and in_char:
chars.append((start, x))
in_char = False
if in_char:
chars.append((start, 64))
return chars
chars1 = find_chars(line1_rows)
chars2 = find_chars(line2_rows)
print("Line 1 characters:")
for i, (s, e) in enumerate(chars1):
print(f" Char {i}: cols {s}-{e} (width {e-s})")
for y in range(7):
row = ''.join('█' if line1_rows[y][x] else '·' for x in range(s, e))
print(f" {row}")
print()
print("\nLine 2 characters:")
for i, (s, e) in enumerate(chars2):
print(f" Char {i}: cols {s}-{e} (width {e-s})")
for y in range(7):
row = ''.join('█' if line2_rows[y][x] else '·' for x in range(s, e))
print(f" {row}")
print()
<details>
<summary>📋 Python 输出 [14:38:58](较长,点击展开)</summary>
Line 1 characters:
Char 0: cols 2-7 (width 5)
█████
█····
████·
█····
█····
█····
█····
Char 1: cols 8-13 (width 5)
█····
█····
█····
█····
█····
█····
█████
Char 2: cols 14-19 (width 5)
·███·
█···█
█···█
█████
█···█
█···█
█···█
Char 3: cols 20-25 (width 5)
·███·
█···█
█····
█·███
█···█
█···█
·███·
Char 4: cols 27-30 (width 3)
·██
·█·
·█·
█··
·█·
·█·
·██
Char 5: cols 32-37 (width 5)
█···█
█···█
█████
█···█
█···█
█···█
█···█
Char 6: cols 38-43 (width 5)
··███
···█·
···█·
···█·
···█·
█··█·
·██··
Char 7: cols 44-49 (width 5)
█···█
██·██
█·█·█
█···█
█···█
█···█
█···█
Char 8: cols 50-55 (width 5)
█···█
█···█
█···█
█·█·█
█·█·█
██·██
█···█
Char 9: cols 56-61 (width 5)
·███·
█···█
█···█
█████
█···█
█···█
█···█
Line 2 characters:
Char 0: cols 2-7 (width 5)
████·
█···█
█···█
████·
█····
█····
█····
Char 1: cols 8-13 (width 5)
··███
···█·
···█·
···█·
···█·
█··█·
·██··
Char 2: cols 14-19 (width 5)
·███·
█···█
····█
···█·
··█··
·█···
█████
Char 3: cols 20-25 (width 5)
·███·
█···█
█··██
█·█·█
██··█
█···█
·███·
Char 4: cols 26-31 (width 5)
·███·
█···█
····█
···█·
··█··
·█···
█████
Char 5: cols 32-37 (width 5)
·███·
█····
█····
████·
█···█
█···█
·███·
Char 6: cols 38-43 (width 5)
█···█
██··█
█·█·█
█··██
█···█
█···█
█···█
Char 7: cols 44-49 (width 5)
████·
█···█
█···█
████·
█···█
█···█
████·
Char 8: cols 50-55 (width 5)
█····
█····
█····
█····
█····
█····
█████
Char 9: cols 56-61 (width 5)
████·
█···█
█···█
█···█
█···█
█···█
████·
</details>
🤖 助手 - 第 116 轮 [14:39:18]
Now I can read each character clearly:
Line 1: F, L, A, G, {, H, J, M, W, A
Line 2: P, J, 2, 0 (wait - that's a special 0 with a line through it... no, it's just the font), 2, 6 (wait...), N, B, L, D
Wait, let me re-read Line 2 more carefully:
- Char 0: P (████, █···█, █···█, ████, █, █, █)
- Char 1: J (··███, ···█, ···█, ···█, ···█, █··█, ·██)
- Char 2: 2 (·███, █···█, ····█, ···█, ··█, ·█, █████)
- Char 3: 0 (·███, █···█, █··██, █·█·█, ██··█, █···█, ·███) - this is 0 with a slash
- Char 4: 2 (same pattern as char 2)
- Char 5: 6 (·███, █, █, ████, █···█, █···█, ·███)
- Char 6: N (█···█, ██··█, █·█·█, █··██, █···█, █···█, █···█)
- Char 7: B (████, █···█, █···█, ████, █···█, █···█, ████)
- Char 8: L (█, █, █, █, █, █, █████)
- Char 9: D (████, █···█, █···█, █···█, █···█, █···█, ████)
So the full text is: FLAG{HJMWAPJ2026NBLD}
Wait - but char 3 in line 2 looks like it could be 0 or G. Let me check: ·███·, █···█, █··██, █·█·█, ██··█, █···█, ·███· - this has diagonal pixels inside, which is characteristic of a slashed zero (0) or the number 0 in some fonts. But actually looking at it again: row 3 is █··██, row 4 is █·█·█, row 5 is ██··█ - this is a 0 with a diagonal slash through it, confirming it's the digit 0.
So: FLAG{HJMWAPJ2026NBLD}
But the hint says the answer is FLAG{HJMWAPJ2026NBLD}. Let me verify:
🐍 执行 Python 脚本
# Let me also check if there's a closing brace }
# The vertical line in rows 36-42 might be the closing brace
with open(r"D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin", "rb") as f:
data = f.read()
print("Rows 36-42 (potential closing brace):")
for y in range(36, 43):
row_data = data[y*8:(y+1)*8]
s = ""
for x in range(28, 36):
bit = (row_data[x//8] >> (7 - x%8)) & 1
s += '█' if bit else '·'
print(f"R{y}: {s}")
# That vertical pattern at cols 30-31:
# R36: ██
# R37: ·█
# R38: ·█
# R39: ··█ (shifted right = this is })
# R40: ·█
# R41: ·█
# R42: ██
# This is indeed a closing brace }
print("\nFull flag: FLAG{HJMWAPJ2026NBLD}")
📋 Python 输出 [14:39:18]
Rows 36-42 (potential closing brace):
R36: ··██····
R37: ···█····
R38: ···█····
R39: ····█···
R40: ···█····
R41: ···█····
R42: ··██····
Full flag: FLAG{HJMWAPJ2026NBLD}
👤 用户 [14:39:18]
[System — Flag Detection]
在工具输出中检测到可能的 flag:
FLAG{HJMWAPJ2026NBLD}
请立即验证并使用对应的提交工具(flag_submit / ctfd_submit_flag / gzctf_submit_flag)提交。不要继续其他操作,先提交 flag!
🤖 助手 - 第 117 轮 [14:39:36]
The flag is clearly derived from the decrypted bitmap - each character was rendered in a pixel font and I can trace every letter. Let me submit it:
🔨 工具调用: flag_submit
- flag:
FLAG{HJMWAPJ2026NBLD}
📋 flag_submit 输出 [14:39:36]
已捕获 flag,已在界面弹窗中提供复制与人工确认入口(正确/错误)。
👤 用户 [14:39:37]
[System] 用户已确认 flag 正确: FLAG{HJMWAPJ2026NBLD}
导出时间:2026-03-06 21:54:11 | 共 294 条消息,117 轮对话
[/quote]