文章总结: 本文介绍了一个全自动AI做题项目的实现,该项目能模拟解题思路,自动完成从分析、调用工具到提交答案的全过程。它基于Go和React开发,特别适用于CTF比赛中的Web、Pwn、逆向等题目,也可用于代码审计和渗透测试。项目核心在于将整个解题流程智能化,并通过多模型协作、上下文压缩和反思总结等机制来提升效率和准确性。文中还分享了一个安卓逆向题目的完整AI解题对话记录。 综合评分: 85 文章分类: CTF,AI安全,WEB安全,逆向分析,渗透测试
[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.
🤖 助手 – 第 2 轮 [14:00:31]
💭 思考过程
Let me extract the 7z file and analyze the contents.
📦 解压: 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]
💭 思考过程
Good, extracted an APK file. Let me examine it and create a plan.
📂 查找文件: D:\AI\AICTF\workdir\52pojie\chu8\extracted (模式: *)
📋 查找结果 [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, 4459647bytes (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 3011bytes] ...
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]
💭 思考过程
This is an Android reverse engineering challenge. It has:
- A
classes.dexfile (Java/Kotlin code) - A native library
libhajimi.sofor 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.
有 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\apkunzipped\lib\x8664\libhajimi.so: ELF 64-bit LSB sharedobject, 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 5task(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]
💭 思考过程
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.
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]
💭 思考过程
jadx completed with some errors but should have output. Let me find the relevant source files.
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
。。。。
🤖 助手 – 第 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
📋 strings_search 输出 [14:02:10](较长,点击展开)
复制代码 隐藏代码
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 */:
.....
🤖 助手 – 第 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 */
publicfinalclassNativeBridge {
publicstaticfinalint$stable=0;
publicstaticfinalintERR_CHEAT= -7;
publicstaticfinalNativeBridgeINSTANCE=newNativeBridge();
publicstaticfinalintSCORE_GOOD=1;
publicstaticfinalintSCORE_MISS=0;
publicstaticfinalintSCORE_PERFECT=2;
static {
System.loadLibrary("hajimi");
}
privateNativeBridge() {
}
privatefinalnativevoidstartSessionBytes(long j2, byte[] bArr, int i2);
publicfinalnativeintcheckRhythm(long j2, int i2, long j3, int i3);
publicfinalnativebyte[] decryptFrames(byte[] bArr, long j2);
publicfinalnativevoidsetDebugBypass(boolean z2);
publicfinalvoidstartSession(long j2, int[] iArr, int i2) {
h.e(iArr, "beatMapMs");
ByteBufferbyteBufferOrder= 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);
}
publicfinalnativelongupdateExp(int i2, int i3, long j2);
publicfinalnativebyte[] 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 */
publicfinalclassMainActivityextendsl {
@Override// androidx.activity.l, w0.AbstractActivityC0756a, android.app.Activity
publicfinalvoidonCreate(Bundle bundle) {
super.onCreate(bundle);
inti2= m.f1921a;
Aa2= A.f1870j;
Bb2=newB(0, 0, a2);
Bb3=newB(m.f1921a, m.f1922b, a2);
ViewdecorView= getWindow().getDecorView();
h.d(decorView, "window.decorView");
Resourcesresources= decorView.getResources();
h.d(resources, "view.resources");
booleanzBooleanValue= ((Boolean) a2.d0(resources)).booleanValue();
Resourcesresources2= decorView.getResources();
h.d(resources2, "view.resources");
booleanzBooleanValue2= ((Boolean) a2.d0(resources2)).booleanValue();
inti3= Build.VERSION.SDK_INT;
qpVar= i3 >= 29 ? newp() : i3 >= 26 ? newo() : newn();
Windowwindow= getWindow();
h.d(window, "window");
pVar.a(b2, b3, window, decorView, zBooleanValue, zBooleanValue2);
eeVar= AbstractC0080d.f1133b;
ViewGroup.LayoutParamslayoutParams= AbstractC0154a.f1861a;
ViewchildAt= ((ViewGroup) getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);
C0204f0c0204f0= childAt instanceof C0204f0 ? (C0204f0) childAt : null;
if (c0204f0 != null) {
c0204f0.setParentCompositionContext(null);
c0204f0.setContent(eVar);
return;
}
C0204f0c0204f02=newC0204f0(this);
c0204f02.setParentCompositionContext(null);
c0204f02.setContent(eVar);
ViewdecorView2= 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:> privatefinalnativevoidstartSessionBytes(long j2, byte[] bArr, int i2);
。。。。
🤖 助手 – 第 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 */
publicfinalclassrextendsY0.i implementse1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
publicfinal/* synthetic */byte[] f1186m;
/* JADX INFO: renamed from: n, reason: collision with root package name */
publicfinal/* synthetic */ O f1187n;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
publicr(byte[] bArr, O o2, W0.d dVar) {
super(2, dVar);
this.f1186m = bArr;
this.f1187n = o2;
}
@Override// e1.e
publicfinal Object N(Object obj, Object obj2) {
return ((r) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override// Y0.a
publicfinal W0.d c(W0.d dVar, Object obj) {
returnnewr(this.f1186m, this.f1187n, dVar);
}
@Override// Y0.a
publicfinal 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 */
publicfinalclasszextendsY0.i implementse1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
publicfinal/* synthetic */byte[] f1217m;
/* JADX INFO: renamed from: n, reason: collision with root package name */
publicfinal/* synthetic */ InterfaceC0680W f1218n;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
publicz(byte[] bArr, InterfaceC0680W interfaceC0680W, W0.d dVar) {
super(2, dVar);
this.f1217m = bArr;
this.f1218n = interfaceC0680W;
}
@Override// e1.e
publicfinal Object N(Object obj, Object obj2) {
return ((z) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override// Y0.a
publicfinal W0.d c(W0.d dVar, Object obj) {
returnnewz(this.f1217m, this.f1218n, dVar);
}
@Override// Y0.a
publicfinal Object g(Object obj)throws Throwable {
R.c.Y(obj);
NativeBridgenativeBridge= NativeBridge.INSTANCE;
Listlist= N.f1106a;
return nativeBridge.verifyAndDecrypt(this.f1217m, n1.k.j0((String) this.f1218n.getValue()).toString());
}
}
📋 文件内容 [14:02:43](较长,点击展开)
复制代码 隐藏代码
package Q0;
import E0.C0021h;
import android.os.SystemClock;
import com.zj.wuaipojie2026_2.NativeBridge;
import t.C0691d0;
/* JADX INFO: loaded from: classes.dex */
publicfinal/* synthetic */classvextendsf1.f implementse1.a {
/* JADX INFO: renamed from: q, reason: collision with root package name */
publicfinal/* 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
publicfinal Object o() {
int i2;
O o2;
switch (this.f1199q) {
case0:
C0082fc0082f= (C0082f) this.f3686j;
if (!((O) c0082f.f1144f.getValue()).f1109c) {
longjElapsedRealtimeNanos= SystemClock.elapsedRealtimeNanos();
longj2= (jElapsedRealtimeNanos - c0082f.f1143e) / 1000000;
C0021hc0021h= c0082f.f1141c;
if (j2 < 0) {
c0021h.getClass();
i2 = 0;
} else {
c0021h.getClass();
inti3= (int) (j2 % ((long) 1000));
int[] iArr = (int[]) c0021h.f318b;
intlength= iArr.length;
inti4= Integer.MAX_VALUE;
inti5=0;
for (inti6=0; i6 < length; i6++) {
intiAbs= Math.abs(i3 - iArr[i6]);
intiMin= Math.min(iAbs, 1000 - iAbs);
if (iMin < i4) {
i5 = i6;
i4 = iMin;
}
}
i2 = i5;
}
longj3= (((long) i2) << 32) ^ jElapsedRealtimeNanos;
longj4= j3 ^ (j3 << 13);
longj5= j4 ^ (j4 >> 7);
longj6= j5 ^ (j5 << 17);
C0691d0c0691d0= c0082f.f1144f;
inti7= ((O) c0691d0.getValue()).f1107a >= 50 ? 1 : 0;
NativeBridgenativeBridge= NativeBridge.INSTANCE;
intiCheckRhythm= nativeBridge.checkRhythm(jElapsedRealtimeNanos, i2, j6, i7);
longjUpdateExp= nativeBridge.updateExp(iCheckRhythm, i2, j6);
if (jUpdateExp < 0 || iCheckRhythm == -7) {
o2 = newO(((O) c0691d0.getValue()).f1107a, Q.Cheat, true);
} else {
Oo3= (O) c0691d0.getValue();
Qq2= iCheckRhythm != 0 ? iCheckRhythm != 1 ? iCheckRhythm != 2 ? Q.None : Q.Perfect : Q.Good : Q.Miss;
o3.getClass();
o2 = newO(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;
}
}
🤖 助手 – 第 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]
💭 思考过程
Let me now analyze the SO file with IDA. I’ll use the x86_64 version since it’s easier to analyze.
现在用 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 (模式: *)
📋 查找结果 [14:03:02](较长,点击展开)
复制代码 隐藏代码
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
🤖 助手 – 第 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 脚本
复制代码 隐藏代码
withopen(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: 564bytes
First 64byteshex: 484a4d3102000000010000004000000040000000780000000f1e2d3c4b5a69788796a5b4c3d2e1f0112233445566778899aabbcc9641cd80feda2af5fb95dd2c
First 64bytes: 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 32byteshex: 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]
💭 思考过程
No JNI functions with standard naming. The SO likely uses dynamic registration. Let me look at JNI_OnLoad and exported functions.
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
-
addr:
0x237c0
📋 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 inrange(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”: “int64fastcall verifyAndDecrypt(int64 a1,int64 a2, int64 a3,int64 a4)\n{\n int64 v4; // rax\nint64 v5; // rax\n int64 v6; // rax\nint64 v620; // rax\n int64 v621; // rax\nint64 v622; // r…
🤖 助手 – 第 37 轮 [14:04:17]
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x24850
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x24520
📋 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 u...
📋 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 __in...
🤖 助手 – 第 38 轮 [14:04:25]
这很复杂。让我也看看其他关键子函数和 startSessionBytes、checkRhythm、updateExp。
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x238a0
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x23e50
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x23f60
📋 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 v....
📋 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; /*0x23...
📋 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....
🤖 助手 – 第 39 轮 [14:04:36]
这个逻辑非常复杂。让我看看关键的子函数 sub_2DDC0、sub_2DF40、sub_2E680、sub_24FC0 和 sub_24CB0。
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x2DDC0
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x2DF40
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x2E680
📋 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 unsig.........
📋 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 __int....
📋 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 uns...👤 用户 [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
ideastool (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 int64fastcall sub_2DDC0(int64 a1, _QWORD a2, int64 a3, unsignedint64a4)\n{\n int64 v6; // rax\n unsignedint64 v7; // rsi\n unsigned int64 v8; // r8\n unsignedint64 v9; … [OK] mcp_ida-pro-headless_decompile({“addr”: “0x2DF40”}) -> { “addr”: “0x2DF40”, “code”: “unsigned int64fastcall sub_2DF40(int64 a1, unsigned int64 a2, constm128i a3, _BYTEa4)\n{\n int v4; // eax\n unsigned int v5; // edx\n int64 v6; // r14\nm128i 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 _DWORDv3; // r14\n unsigned int64 v4; // rax\nint64 v5; // rdx\n __int64 v6; // rbp\n int v7; // ecx\n int v8; //…
🤖 助手 – 第 40 轮 [14:05:45]
💭 思考过程
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.binfile in assets with header “HJM1” (0x484A4D31) -
The SO has 6 JNI methods registered dynamically
-
sub_2DF40is clearly AES encryption – it uses S-box (
byte_17430), MixColumns operations, 10 rounds – this is AES-128 in CTR mode (XOR with keystream) -
sub_2DDC0derives a key using xxHash-like operations
-
sub_2E680is the main decryption function that processes the HJM1 format
The key insight from decryptFrames and verifyAndDecrypt:
-
They check
*v11 != 827148872which 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)-valueis2
Offset 8:field2(uint32)-valueis1
Offset 12:field3(uint32)-0x40=64
Offset 16:field4(uint32)-0x40=64
Offset 20:field5(uint32)-0x78=120
Offset 24:16bytes-key/IVmaterial
Offset 40:12bytes-morekeymaterial
Offset 52:encrypteddata
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_5D140orv23 & 1(anti-cheat flags) -
It needs
v27 >= 999(exp >= 999) -
It calls
sub_2E680which 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) andv37 >= 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
👤 用户 [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]
💭 思考过程
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.sonative library -
NativeBridgeclass with methods: startSessionBytes, checkRhythm, updateExp, decryptFrames, verifyAndDecrypt, setDebugBypass
-
hjm_pack.binasset 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_2DF40is clearly AES-128 CTR mode encryption/decryption
-
sub_2E680handles the HJM1 format parsing and decryption
-
verifyAndDecrypttakes byte[] and String, processes HJM1 data
-
decryptFramestakes 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.
好的,让我基于已有分析制定聚焦计划。我已经知道:
-
hjm_pack.bin是 HJM1 格式,type=2,包含 64×64 位图(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]
[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.
🤖 助手 – 第 42 轮 [14:08:23]
💭 思考过程
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.
🔨 工具调用: 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
-
addr:
0x24ca0
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x2E570
📋 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
-
addr:
0x2DAF0
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x24CB0
📋 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
-
addr:
0x2EB00
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x2EC80
📋 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
-
addr:
0x2DD50
🔨 工具调用: mcp_ida-pro-headless_decompile
-
addr:
0x2EFD0
📋 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
ideastool (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, signedint64 a2)\n{\n char v2; // rax\n charv3; // 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”: “int64fastcall sub_2EFD0(char s, int a2, int a3, voida4, size_t a5)\n{\n char v5; // r13\n charv6; // 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]
💭 思考过程
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_2EC80and 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_2E570to 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.
好的,我已经收集了足够的反编译信息。现在该停止反编译,开始写解密脚本了。让我先检查 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
ideastool (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]
💭 思考过程
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.binhas 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_2E570which generates a deterministic key based onqword_5CFE8anddword_5CFD0 - The key is XORed with
0xA5A5A5A5A5A5A5A5
- For type=1 (decryptFrames path):
-
Key is
a4 ^ 0x5A5A5A5A(when v21=1) -
sub_2DDC0derives AES key from the key + salt + nonce
-
sub_2DF40is 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_5D140is true, it callssub_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.
好的,我已经收集了足够的反编译信息。现在让我读取 hjm_pack.bin 的文件头,然后查找 APK 中的节奏数据文件。
🐍 执行 Python 脚本
复制代码 隐藏代码
import struct, os
# Read hjm_pack.bin header
withopen(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
ideastool (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
👤 用户 [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
ideastool (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]
💭 思考过程
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.binhas 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_2E680is:
- 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 (
a2parameter tosub_2E680) comes fromverifyAndDecrypt/decryptFrames:
- When
byte_5D140(debug bypass) is set, callssub_2E570to generate key - Otherwise uses
qword_5CFF0 - The key is XORed with
0xA5A5A5A5A5A5A5A5whenv21(debug flag) is true
sub_2E570generates a deterministic key based onqword_5CFE8anddword_5CFD0- The initial state:
qword_5CFE8is set instartSessionBytesbased 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
a2parameter is the key passed tosub_2E680
In verifyAndDecrypt (0x24850), for type=2:
-
When debug bypass:
v33 = sub_2E570(a1, v19, v20)orv33 = 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=1gives us a simpler path
When byte_5D140 = 1:
-
In
verifyAndDecrypt, for type=2:v33 = sub_2E570(a1, v19, v20) -
sub_2E570computes a key based on
qword_5CFE8anddword_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.
找到了文件路径。现在让我解析 hjm_pack.bin 并查看 Java 代码:
🐍 执行 Python 脚本
复制代码 隐藏代码
import struct
withopen(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:564bytes
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
Encrypteddata(offset52+):512bytes
First 32 bytes of encrypted:9641cd80feda2af5fb95dd2c89bd91bc4013a0ed7c3df6e7a3d19b93bcc6542b
Expectedbitmapsize(64x64@1bpp):512bytes
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
ideastool (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/289file(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:
-
HJM1 file format: magic(4) + type(4) + frame_count(4) + height(4) + width(4) + ?(4) + salt(16) + iv(12) + encry…[truncated]`
- I’ve fully reverse-engineered the native library structure:
-
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:>importQ0.AbstractC0080d;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\MainActivity.java:5:>import a.AbstractC0154a;
[5 matches in1/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 */
publicfinalclassNativeBridge {
publicstaticfinalint$stable=0;
publicstaticfinalintERR_CHEAT= -7;
publicstaticfinalNativeBridgeINSTANCE=newNativeBridge();
publicstaticfinalintSCORE_GOOD=1;
publicstaticfinalintSCORE_MISS=0;
publicstaticfinalintSCORE_PERFECT=2;
static {
System.loadLibrary("hajimi");
}
privateNativeBridge() {
}
privatefinal
... [truncated 1458 bytes] ...
boolean z2);
publicfinalvoidstartSession(long j2, int[] iArr, int i2) {
h.e(iArr, "beatMapMs");
ByteBufferbyteBufferOrder= 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);
}
publicfinalnativelongupdateExp(int i2, int i3, long j2);
publicfinalnativebyte[] 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:> privatefinalnativevoidstartSessionBytes(long j2, byte[] bArr, int i2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:27:> publicfinalnativebyte[] decryptFrames(byte[] bArr, long j2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:29:> publicfinalnativevoidsetDebugBypass(boolean z2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:31:> publicfinal
... [truncated 1744 bytes] ...
3q.java:33:> InputStreaminputStreamOpen=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:> InputStreaminputStreamOpen=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 */
publicfinalclassC0093qextendsY0.i implementse1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
publicfinal/* synthetic */ Context f1185m;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
publicC0093q(Context context, W0.d dVar) {
super(2, dVar);
this.f1185m = context;
}
@Override// e1.e
publicfinal Object N(Object obj, Object obj2) {
return ((C0093q) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override// Y0.a
publicfinal W0.d c(W0.d dVar, Object obj) {
returnnewC0093q(this.f1185m, dVar);
}
@Override// Y0.a
publicfinal Object g(Object obj)throws Throwable {
R.c.Y(obj);
InputStreaminputStreamOpen=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 */
publicfinalclasszextendsY0.i implementse1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
publicfinal/* synthetic */byte[] f1217m;
/* JADX INFO: renamed from: n, reason: collision with root package name */
publicfinal/* synthetic */ InterfaceC0680W f1218n;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
publicz(byte[] bArr, InterfaceC0680W interfaceC0680W, W0.d dVar) {
super(2, dVar);
this.f1217m = bArr;
this.f1218n = interfaceC0680W;
}
@Override// e1.e
publicfinal Object N(Object obj, Object obj2) {
return ((z) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override// Y0.a
publicfinal W0.d c(W0.d dVar, Object obj) {
returnnewz(this.f1217m, this.f1218n, dVar);
}
@Override// Y0.a
publicfinal Object g(Object obj)throws Throwable {
R.c.Y(obj);
NativeBridgenativeBridge= NativeBridge.INSTANCE;
Listlist= 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 */
publicfinalclassrextendsY0.i implementse1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
publicfinal/* synthetic */byte[] f1186m;
/* JADX INFO: renamed from: n, reason: collision with root package name */
publicfinal/* synthetic */ O f1187n;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
publicr(byte[] bArr, O o2, W0.d dVar) {
super(2, dVar);
this.f1186m = bArr;
this.f1187n = o2;
}
@Override// e1.e
publicfinal Object N(Object obj, Object obj2) {
return ((r) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override// Y0.a
publicfinal W0.d c(W0.d dVar, Object obj) {
returnnewr(this.f1186m, this.f1187n, dVar);
}
@Override// Y0.a
publicfinal 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 */
publicfinalclassyextendsY0.i implementse1.e {
/* JADX INFO: renamed from: m, reason: collision with root package name */
publicfinal/* synthetic */ Context f1216m;
/* JADX WARN: 'super' call moved to the top of the method (can break code semantics) */
publicy(Context context, W0.d dVar) {
super(2, dVar);
this.f1216m = context;
}
@Override// e1.e
publicfinal Object N(Object obj, Object obj2) {
return ((y) c((W0.d) obj2, (InterfaceC0603z) obj)).g(S0.k.f1286a);
}
@Override// Y0.a
publicfinal W0.d c(W0.d dVar, Object obj) {
returnnewy(this.f1216m, dVar);
}
@Override// Y0.a
publicfinal Object g(Object obj)throws Throwable {
R.c.Y(obj);
InputStreaminputStreamOpen=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:> privatefinalnativevoidstartSessionBytes(long j2, byte[] bArr, int i2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:25:> publicfinalnativeintcheckRhythm(long j2, int i2, long j3, int i3);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:29:> publicfinalnativevoidsetDebugBypass(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:> publicfinalnativelongupdateExp(int i2, int i3, long j2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\v.java:58:> intiCheckRhythm= nativeBridge.checkRhythm(jElapsedRealtimeNanos, i2, j6, i7);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\v.java:59:> longjUpdateExp= 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 */
publicfinal/* synthetic */classvextendsf1.f implementse1.a {
/* JADX INFO: renamed from: q, reason: collision with root package name */
publicfinal/* 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 = newO(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(C0021hc0021h) {
D:\AI\AICTF\workdir\52pojie\chu8\ja
... [truncated 6009 bytes] ...
hu8\jadx_out\sources\E0\C0021h.java:419:> publicC0021h(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:> publicC0021h(Matcher matcher, String str) {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\E0\C0021h.java:434:> this.f318b = matcher;
[50 matches in3/127 file(s)] [truncated at50 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 in2/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:> privatefinalnativevoidstartSessionBytes(long j2, byte[] bArr, int i2);
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\com\zj\wuaipojie2026_2\NativeBridge.java:31:> publicfinalvoidstartSession(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. 模拟debugbypass路径(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:>publicfinalclassC0082f {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java:21:> publicfinal C0021h f1141c;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java:27:> publiclong f1143e;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java:30:> publicfinal C0691d0 f1144f;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java:32:> publicC0082f(Context context) {
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\C0082f.java:37:> this.f1141c = newC0021h
... [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:> C0082fc0082f=this.f1208p;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\x.java:182:> longj4= c0082f.f1143e;
D:\AI\AICTF\workdir\52pojie\chu8\jadx_out\sources\q0\x.java:261:> vvVar=newv(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 */
publicfinalclassC0082f {
/* JADX INFO: renamed from: a, reason: collision with root package name */
publicfinal Context f1139a;
/* JADX INFO: renamed from: b, reason: collision with root package name */
publicfinalint[] f1140b;
/* JADX INFO: renamed from: c, reason: collision with root package name */
publicfinal 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 */
publiclong f1143e;
/* JADX INFO: renamed from: f, reason: collision with root package name */
publicfinal C0691d0 f1144f;
publicC0082f(Context context) {
f1.h.e(context, "context");
this.f1139a = context;
int[] iArr = {0, 250, 500, 750};
this.f1140b = iArr;
this.f1141c = newC0021h(4, iArr);
this.f1144f = AbstractC0713v.E(newO(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. 模拟debugbypass路径(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并渲染64×64位图 ⬜ 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(‘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(‘chunk\n h64 = ((h64 << 11) | (h64 >> 53)) & 0xFFFFFFFFFFFFFFFF\n h64= PRIME64_1\n h64 &= 0xFFFFFFFFFFFFFFFF\n \n if remaining & 2:\n chunk = struct.unpack(‘
[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
ideastool (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(‘PRIME64_2) & 0xFFFFFFFFFFFFFFFF\n v1 = ((v1 << 31) | (v1 >> 33)) & 0xFFFFFFFFFFFFFFFF\n v1 = (v1PRIME64_1) & 0xFFFFFFFFFFFFFFFF\n \n v2 = (v2 + struct.unpack(‘
PRIME64_2) & 0xFFFFFFFFFFFFFFFF\n v2 = ((v2 << 31) | (v2 >> 33)) & 0xFFFFFFFFFFFFFFFF\n v2 = (v2PRIME64_1) & 0xFFFFFFFFFFFFFFFF\n \n v3 = (v3 + struct.unpack(‘
PRIME64_2) & 0xFFFFFFFFFFFFFFFF\n v3 = ((v3 << 31) | (v3 >> 33)) & 0xFFFFFFFFFFFFFFFF\n v3 = (v3PRIME64_1) & 0xFFFFFFFFFFFFFFFF\n \n v4 = (v4 + struct.unpack(‘
PRIME64_2) & 0xFFFFFFFFFFFFFFFF\n v4 = ((v4 << 31) | (v4 >> 33)) & 0xFFFFFFFFFFFFFFFF\n v4 = (v4PRIME64_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('
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(‘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 ^= (chunkPRIME64_5) & 0xFFFFFFFFFFFFFFFF\n h64 = (((h64 << 11) | (h64 >> 53)) PRIME64_1) & 0xFFFFFFFFFFFFFFFF\n offset += 1\n \n h64 ^= h64 >> 33\n h64 = (h64PRIME642) & 0xFFFFFFFFFFFFFFFF\n h64 ^= h64 >> 29\n return h64\n\ndef siphash24(key, msg):\n \”\”\”SipHash-2-4 implementation\”\”\”\n k0, k1 = struct.unpack(‘
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. 模拟debugbypass路径(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
defu64(x): return x & 0xFFFFFFFFFFFFFFFF
defu32(x): return x & 0xFFFFFFFF
defrol64(x, n): return u64((x << n) | (x >> (64 - n)))
# SipHash-2-4
defsip...[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
defu64(x): return x & 0xFFFFFFFFFFFFFFFF
defu32(x): return x & 0xFFFFFFFF
defrol64(x, n): return u64((x << n) | (x >> (64 - n)))
defxxhash64(data, se...[truncated]
📋 Python 输出 [14:20:05]
复制代码 隐藏代码
foldedqword_5CFE8=0xe3853455
Chunk 0:key=32d64809c01fd6eec54ddff78e80aa78,nonce=112233440052f26b99aabbcc,first_dec=805e6a74557f1cdaaa356dc35ebd35af
Chunk 1:key=8610330511564bbe75b9fff53f5b6851,nonce=102233440052f26b99aabbcc,first_dec=4d9d53f018fbfb7d0e8b33a72b8fbad8
Decrypted512bytes
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: testal, al
24b94: setnzcl
24b97: orcl, byte_5CFF8
24b9d: orcl, r13b
24ba0: testcl, 1
24ba3: jz 24bb4 (fail)
24ba5: ...
24ba9: testal, 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: movrax, qword_5CFF0 (use game-earned key)
24be1: movrsi, 0xA5A5A5A5A5A5A5A5
24beb: xorrsi, rax
24bee: testr13b, r13b (r13b = password verification passed?)
24bf1: cmovzrsi, 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
defu64(x): return x & 0xFFFFFFFFFFFFFFFF
defu32(x): return x & 0xFFFFFFFF
defrol64(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, ororder.
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 liketext 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
defu64(x): return x & 0xFFFFFFFFFFFFFFFF
defu32(x): return x & 0xFFFFFF...[truncated]
📋 Python 输出 [14:23:02]
复制代码 隐藏代码
Imagesize: (64, 64)
Imagemode: 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]
💭 思考过程
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.
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
-
addr:
0x2E680
📋 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
-
addr:
0x24CB0
📋 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
defu64(x): return x & 0xFFFFFFFFFFFFFFFF
defrol64(x, n): return u64((x << n) | (x >> (64 - n)))
defxxhash64(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
defu64(x): return x & 0xFFFFFFFFFFFFFFFF
defsub_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 at0and 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
defu64(x): return x & 0xFFFFFFFFFFFFFFFF
defu32(x): return x & 0xFFFFFFFF
defrol64(x, n): return u64((x << n) | (x >> (64 - n)))
defsiphash_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 inrange(blocks):
m = struct.unpack_from('<Q', data, i*8)[0]
v3 ^= m
for _ inrange(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 inrange(rem): last[i] = data[start+i]
last[7] = length & 0xFF
m = struct.unpack('<Q', bytes(last))[0]
v3 ^= m
for _ inrange(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 _ inrange(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)
defsplitmix64(state):
state = u64(state + 0x9E3779B97F4A7C15)
z = state
z = u64((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9)
z = u64((z ^ (z >> 27)) * 0x94D049BB133111EB)
return state, z ^ (z >> 31)
defsub_24CB0(data, seed):
h = xxhash.xxh64(data, seed=seed).intdigest()
return u64(h ^ (h >> 32))
defderive_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
defaes_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 inrange(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 inzip(chunk, keystream[:len(chunk)])))
counter += 1
returnbytes(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 inrange(12):
buf = struct.pack('<QiiII', rcx, i % 4, 0, i, 2)
rcx = siphash_2_4(buf, state_r14, r15_key)
debug_key = rcx
withopen(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 inrange(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 inrange(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 inrange(64):
for x inrange(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 inrange(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 inrange(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: leardi, [rsp+src] ; same buffer
2e821: call sub_24CB0 ; first call
2e832: leardi, [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
defu64(x): return x & 0xFFFFFFFFFFFFFFFF
defrol64(x, n): return u64((x << n) | (x >> (64 - n)))
defsiphash_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 inrange(blocks):
m = struct.unpack_from('<Q', data, i*8)[0]
v3 ^= m
for _ inrange(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 inrange(rem): last[i] = data[start+i]
last[7] = length & 0xFF
m = struct.unpack('<Q', bytes(last))[0]
v3 ^= m
for _ inrange(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 _ inrange(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
defu64(x): return x & 0xFFFFFFFFFFFFFFFF
defrol64(x, n): return u64((x << n) | (x >> (64 - n)))
defsiphash_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 inrange(blocks):
m = struct.unpack_from('<Q', data, i*8)[0]
v3 ^= m
for _ inrange(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 inrange(rem): last[i] = data[start+i]
last[7] = length & 0xFF
m = struct.unpack('<Q', bytes(last))[0]
v3 ^= m
for _ inrange(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 _ inrange(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)
defsplitmix64(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 inrange(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**63else 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)
assertlen(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
📋 mcp_ida-pro-headless_disasm 输出 [14:30:19](较长,点击展开)
复制代码 隐藏代码
{
"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
}
}
👤 用户 [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]
💭 思考过程
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 < 4and byte_5D00C = 0:
- At 0x249b7: byte_5D00C != 1, so jump to0x24a30
- r13d = 0
- qword_5CFE8 = qword_5CFE0 (unchanged)
So r13b = 0, and the keyis raw (no XOR).
But wait - I already tried this ("debug_key raw") and got noise. Letme reconsider.
Actually, maybe the issue is that qword_5CFE8 IS modified. Letme look at where qword_5CFE8 isset. At 0x24a33: `mov cs:qword_5CFE8, rax`. And rax comes from qword_5CFE0 at 0x249a7 or0x249b0.
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, letme look at 0x246ee:
</details>
Letmetake a different approach. Instead of trying to perfectly replicate the native code, letme use Unicorn to emulate the critical functions directly:
### 🐍 执行 Python 脚本
python
Letme check where qword_5CFE8 is written (not just read)
Looking at the xrefs, letme check 0x23a6a and0x246ee
import struct
First, letme re-examine the initialization. qword5CFE0 and qword5CFE8
might be setin the startSession function.
Letme look at the function that sets these values.
Actually, letmetry a completely different approach.
Instead of reimplementing everything, letme use unicorn to emulate the native code.
First, letme read the .so file
with open(r”D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\lib\x8664\libhjm.so”, “rb”) as f: so_data = f.read()
print(f”SO file size: {len(so_data)} bytes”)
Check ELF header
magic = sodata[:4] print(f”Magic: {magic}”) eiclass = sodata[4] # 1=32bit, 2=64bit print(f”Class: {’64-bit’ if eiclass == 2 else ’32-bit’}”)
Get section headers to find .text
import struct
eshoff = struct.unpackfrom(‘<Q’, sodata, 0x28)[0]
eshentsize = struct.unpackfrom(‘
print(f”Section headers at: 0x{eshoff:x}, count: {eshnum}, size: {e_shentsize}”)
Get program headers for loading
ephoff = struct.unpackfrom(‘<Q’, sodata, 0x20)[0]
ephentsize = struct.unpackfrom(‘
print(f”\nProgram headers at: 0x{ephoff:x}, count: {ephnum}”)
for i in range(ephnum):
off = ephoff + i * ephentsize
ptype = struct.unpackfrom(‘data, off)[0]
pflags = struct.unpackfrom(‘<I’, sodata, off+4)[0]
poffset = struct.unpackfrom(‘data, off+8)[0]
pvaddr = struct.unpackfrom(‘<Q’, sodata, off+16)[0]
pfilesz = struct.unpackfrom(‘
data, off+32)[0]
pmemsz = struct.unpackfrom(‘<Q’, sodata, off+40)[0]
if ptype == 1: # PTLOAD
print(f” LOAD: offset=0x{poffset:x} vaddr=0x{pvaddr:x} filesz=0x{pfilesz:x} memsz=0x{pmemsz:x} flags=0x{pflags:x}”)
**📋 Python 输出** [14:31:12]
复制代码 隐藏代码
Traceback (most recent call last):
File “C:\Users\WOYOUY~1\AppData\Local\Temp\ctfagent1289445898.py”, line 15, in
[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\apkunzipped\lib\arm64-v8a\libhajimi.so D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\lib\armeabi-v7a\libhajimi.so D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\lib\x86\libhajimi.so D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\lib\x86_64\libhajimi.so
### 🤖 助手 - 第 100 轮 [14:31:45]
#### 🐍 执行 Python 脚本
复制代码 隐藏代码
import struct
withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\lib\x8664\libhajimi.so”, “rb”) as f: so_data = f.read()
Get program headers for loading
ephoff = struct.unpackfrom(‘<Q’, sodata, 0x20)[0]
ephentsize = struct.unpackfrom(‘
segments = []
for i inrange(ephnum):
off = ephoff + i * ephentsize
ptype = struct.unpackfrom(‘data, off)[0]
pflags = struct.unpackfrom(‘<I’, sodata, off+4)[0]
poffset = struct.unpackfrom(‘data, off+8)[0]
pvaddr = struct.unpackfrom(‘<Q’, sodata, off+16)[0]
pfilesz = struct.unpackfrom(‘
data, off+32)[0]
pmemsz = struct.unpackfrom(‘<Q’, sodata, off+40)[0]
if ptype == 1: # PTLOAD
segments.append((pvaddr, poffset, pfilesz, pmemsz, pflags))
print(f”LOAD: vaddr=0x{pvaddr:x} offset=0x{poffset:x} filesz=0x{pfilesz:x} memsz=0x{pmemsz: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:
1. What have you accomplished so far?
2. 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
withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\lib\x8664\libhajimi.so”, “rb”) as f: so_data = f.read()
BASE = 0x10000000 mu = Uc(UCARCHX86, UCMODE64)
Map segments
segments = [(0x0, 0x0, 0x56570, 0x56570), (0x57570, 0x56570, 0x4728, 0x4728), (0x5cca0, 0x5aca0, 0xe8, 0xfc0)] for vaddr, foff, fsz, msz in segments: alignedstart = (BASE + vaddr) & ~0xFFF alignedend = ((BASE + vaddr + msz) + 0xFFF) & ~0xFFF try: mu.memmap(alignedstart, alignedend – alignedstart) except: pass mu.memwrite(BASE + vaddr, sodata[foff:foff+fsz])
Stack
STACKBASE = 0x7FFF0000 STACKSIZE = 0x100000 mu.memmap(STACKBASE, STACKSIZE) mu.regwrite(UCX86REGRSP, STACKBASE + STACK_SIZE – 0x1000)
Heap for data
HEAP = 0x20000000 mu.mem_map(HEAP, 0x100000)
Set up FS segment for stack canary (fs:0x28)
FSBASE = 0x30000000
mu.memmap(FSBASE, 0x1000)
mu.memwrite(FSBASE + 0x28, struct.pack(‘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)
testkey = bytes(range(16)) k0 = struct.unpackfrom(‘<Q’, testkey, 0)[0] k1 = struct.unpackfrom(‘<Q’, testkey, 8)[0] testmsg = bytes(range(15)) mu.memwrite(HEAP, testmsg)
mu.regwrite(UCX86REGRDI, HEAP) mu.regwrite(UCX86REGRSI, len(testmsg)) mu.regwrite(UCX86REGRDX, k0) mu.regwrite(UCX86REG_RCX, k1)
Push return address
retaddr = 0x40000000 mu.memmap(retaddr, 0x1000) mu.memwrite(retaddr, b’\xf4′) # HLT rsp = mu.regread(UCX86REGRSP) – 8 mu.memwrite(rsp, struct.pack(‘<Q’, retaddr)) mu.regwrite(UCX86REG_RSP, rsp)
try: mu.emustart(BASE + 0x2DAF0, retaddr, timeout=5000000) result = mu.regread(UCX86REGRAX) print(f”SipHash emulated: 0x{result:016x}”) print(f”Expected: 0xa129ca6149be45e5″) except UcError as e: rip = mu.regread(UCX86REGRIP) 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
withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\lib\x8664\libhajimi.so”, “rb”) as f: so_data = f.read()
BASE = 0x10000000 STACKBASE = 0x7FFF0000; STACKSIZE = 0x100000 HEAP = 0x20000000; FSBASE = 0x30000000 RETADDR = 0x40000000
defsetupemu(): mu = Uc(UCARCHX86, UCMODE64) 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 inrange(s,e,0x1000): if pg notin mapped: try: mu.memmap(pg, 0x1000) except: pass mapped.add(pg) mu.memwrite(BASE+va, sodata[fo:fo+fs]) mu.memmap(STACKBASE, STACKSIZE) mu.memmap(HEAP, 0x100000) mu.memmap(FSBASE, 0x1000) mu.memwrite(FSBASE+0x28, struct.pack(‘<Q’, 0xDEADBEEFCAFEBABE)) mu.msrwrite(0xC0000100, FSBASE) mu.memmap(RETADDR, 0x1000) mu.memwrite(RETADDR, b’\xf4′) return mu
defcallfunc(mu, addr, argsregs=None): if argsregs: regorder = [UCX86REGRDI, UCX86REGRSI, UCX86REGRDX, UCX86REGRCX, UCX86REGR8, UCX86REGR9] for i, v inenumerate(argsregs): mu.regwrite(regorder[i], v) rsp = STACKBASE + STACKSIZE – 0x1000 mu.memwrite(rsp, struct.pack(‘<Q’, RETADDR)) mu.regwrite(UCX86REGRSP, rsp) mu.emustart(BASE + addr, RETADDR, timeout=10000000) return mu.regread(UCX86REG_RAX)
Step 1: Set up global state
beatMap = [0, 250, 500, 750], dword_5CFD0 = 4
qword5CFE0 and qword5CFE8 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.memwrite(BASE + 0x5CFD0, struct.pack(‘5CFD0 = 4 (beat count)
mu.memwrite(BASE + 0x5CFE0, struct.pack(‘5CFE0
mu.memwrite(BASE + 0x5CFE8, struct.pack(‘
5CFE8
mu.memwrite(BASE + 0x5D140, struct.pack(‘5D140 = 1 (debug bypass)
Step 2: Call sub_2E570 to get debug key
debugkey = callfunc(mu, 0x2E570) print(f”debugkey = 0x{debugkey:016x}”)
Step 3: Now simulate verifyAndDecrypt path
At 0x24be1: rsi = 0xA5A5A5A5A5A5A5A5 ^ rax (if r13b=1)
or rsi = rax (if r13b=0)
Then call sub2E680(rdi=&filedata, 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, byte5CFF8 -> cl |= byte5CFF8
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 sub2E570 -> rax = debugkey
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
byte5D00C is set to 1 at 0x249a0 when dword5D008 >= 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”, “startea”: “0x24890”, “lines”: “verifyAndDecrypt (.text @ 0x24890):\n24890 call qword ptr [rax+558h]\n24896 test eax, eax\n24898 jle loc24934\n2489e mov ebp, eax\n248a0 mov ebx, eax\n248a2 mov rdi, rbx; size\n248a5 call Znwm; operator new(ulong)\n248aa mov [rsp+88h+var70], rax\n248af lea r12, [rax+rbx]\n248b3 mov [rsp+88h+var60], r12\n248b8 mov rdi, rax; s\n248bb xor esi, esi; c\n248bd mov [rsp+88h+var78], rbx\n248c2 mov rdx, rbx; n\n248c5 mov rbx, rax\n248c8 call memset\n248cd mov [rsp+88h+var68], 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 loc248F5\n248ed cmp dword ptr [rbx], 314D4A48h\n248f3 jz short loc24967\n248f5 mov rax, [r14]\n248f8 mov rdi, r14″, “stackframe”: [ { “name”: “var80″, “offset”: “0x8”, “size”: “0x4”, “type”: “DWORD” }, { “name”: “var7C”, “offset”: “0xc”, “size”: “0x4”, “type”: “DWORD” }, { “name”: “var78″, “offset”: “0x10”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “var70″, “offset”: “0x18”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “var68″, “offset”: “0x20”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “var60″, “offset”: “0x28”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “s2”, “offset”: “0x30”, “size”: “0x8”, “type”: “void *” }, { “name”: “var50″, “offset”: “0x38”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “var38″, “offset”: “0x50”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “returnaddress”, “offset”: “0x88”, “size”: “0x8”, “type”: “UNKNOWN *” } ], “returntype”: “int64″, “arguments”: [ { “name”: “arg0”, “type”: “int64 *” }, { “name”: “arg1”, “type”: “int64″ }, { “name”: “arg2”, “type”: “int64″ }, { “name”: “arg3”, “type”: “int64″ } ] }, “instructioncount”: 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
withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\lib\x8664\libhajimi.so”, “rb”) as f: so_data = f.read()
BASE = 0x10000000 STACKBASE = 0x7FFF0000; STACKSIZE = 0x200000 HEAP = 0x20000000; FSBASE = 0x30000000; RETADDR = 0x40000000
mu = Uc(UCARCHX86, UCMODE64)
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 inrange(s,e,0x1000):
if pg notin mapped:
mu.memmap(pg,0x1000); mapped.add(pg)
mu.memwrite(BASE+va, sodata[fo:fo+fs])
mu.memmap(STACKBASE, STACKSIZE)
mu.memmap(HEAP, 0x200000)
mu.memmap(FSBASE, 0x1000)
mu.memwrite(FSBASE+0x28, struct.pack(‘write(0xC0000100, FSBASE)
mu.memmap(RETADDR, 0x1000)
mu.memwrite(RET_ADDR, b’\xcc’)
Read hjm_pack.bin
withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\assets\hjmpack.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.memwrite(BASE + 0x5CFD0, struct.pack(‘write(BASE + 0x5CFE0, struct.pack(‘<Q’, qfe8)) mu.mem_write(BASE + 0x5CFE8, struct.pack(‘<Q’, qfe8))
sub2E680 signature: sub2E680(_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
FILEDATA = HEAP + 0x10000 mu.memwrite(FILEDATA, hjmdata)
Set up the a1 structure (two pointers: start, end)
A1STRUCT = HEAP + 0x20000
mu.memwrite(A1STRUCT, struct.pack(‘
The key value – try debug_key (raw, no XOR)
debug_key = 0xd2c13a26769ce7e0
Hook operator new and operator delete to provide heap memory
NEWHEAP = HEAP + 0x30000 newoffset = [0]
defhookcode(uc, address, size, userdata): 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 = STACKBASE + STACKSIZE – 0x2000
mu.memwrite(rsp, struct.pack(‘ADDR))
mu.regwrite(UCX86REGRSP, rsp)
mu.regwrite(UCX86REGRDI, A1STRUCT)
mu.regwrite(UCX86REGRSI, debugkey)
Hook to intercept PLT calls
calllog = [] defhookmeminvalid(uc, access, address, size, value, userdata): rip = uc.regread(UCX86REGRIP) call_log.append(f”Invalid mem access at RIP=0x{rip:x}, addr=0x{address:x}, access={access}”) returnFalse
mu.hookadd(UCHOOKMEMUNMAPPED, hookmeminvalid)
try: mu.emustart(BASE + 0x2E680, RETADDR, timeout=10000000, count=50000) rip = mu.regread(UCX86REGRIP) if rip == RETADDR: result = mu.regread(UCX86REGRAX) print(f”sub2E680 returned: 0x{result:x}”) else: print(f”Stopped at RIP=0x{rip:x}”) except UcError as e: rip = mu.regread(UCX86REGRIP) print(f”Error at RIP=0x{rip:x}: {e}”) if calllog: for l in calllog[-5:]: print(f” {l}”)
**📋 Python 输出** [14:34:24]
复制代码 隐藏代码 Testing emulation of sub2E680… Error at RIP=0x1002e86f: Invalid instruction (UCERRINSNINVALID)
### 👤 用户 [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”: “sub2E680″, “startea”: “0x2e860”, “lines”: “sub2E680 (.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+varA8]\n2e89a pinsrb xmm0, ecx, 4\n2e8a0 movzx ecx, byte ptr [rsp+0E8h+varB0]\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+varB8]\n2e8bb pinsrb xmm0, ecx, 6″, “stackframe”: [ { “name”: “varE8″, “offset”: “0x0”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “varE0″, “offset”: “0x8”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “n”, “offset”: “0x10”, “size”: “0x8”, “type”: “sizet” }, { “name”: “varD0″, “offset”: “0x18”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “varC8″, “offset”: “0x20”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “varC0″, “offset”: “0x28”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “varB8″, “offset”: “0x30”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “varB0″, “offset”: “0x38”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “varA8″, “offset”: “0x40”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “varA0″, “offset”: “0x48”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “var98″, “offset”: “0x50”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “var90″, “offset”: “0x58”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “ptr”, “offset”: “0x60”, “size”: “0x10”, “type”: “void *[2]” }, { “name”: “var78″, “offset”: “0x70”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “var68″, “offset”: “0x80”, “size”: “0x10”, “type”: “OWORD” }, { “name”: “src”, “offset”: “0x90”, “size”: “0x10”, “type”: “void *[2]” }, { “name”: “var48″, “offset”: “0xa0”, “size”: “0x10”, “type”: “OWORD” }, { “name”: “var38″, “offset”: “0xb0”, “size”: “0x8”, “type”: “QWORD” }, { “name”: “returnaddress”, “offset”: “0xe8”, “size”: “0x8”, “type”: “UNKNOWN *” } ], “returntype”: “int64″, “arguments”: [ { “name”: “arg0”, “type”: “QWORD *” }, { “name”: “arg1”, “type”: “_int64″ } ] }, “instructioncount”: 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’, chunkcounter, foldedqfe8)
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
withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\lib\x8664\libhajimi.so”, “rb”) as f: so_data = f.read()
BASE = 0x10000000 STACKBASE = 0x7FFF0000; STACKSIZE = 0x200000 HEAP = 0x20000000; FSBASE = 0x30000000; RETADDR = 0x40000000
mu = Uc(UCARCHX86, UCMODE64)
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 inrange(s,e,0x1000):
if pg notin mapped:
mu.memmap(pg,0x1000); mapped.add(pg)
mu.memwrite(BASE+va, sodata[fo:fo+fs])
mu.memmap(STACKBASE, STACKSIZE)
mu.memmap(HEAP, 0x200000)
mu.memmap(FSBASE, 0x1000)
mu.memwrite(FSBASE+0x28, struct.pack(‘write(0xC0000100, FSBASE)
mu.memmap(RETADDR, 0x1000)
mu.memwrite(RET_ADDR, b’\xf4′)
beatmap = struct.pack(‘<4I’, 0, 250, 500, 750) qfe8 = xxhash.xxh64(beatmap, seed=0x1A8CBC5B802E097C).intdigest() debug_key = 0xd2c13a26769ce7e0
sub2EB00(arg0=keyval, arg1=qfe8, arg2=outptr, arg3=outptr2)
rdi=keyval, rsi=qfe8, rdx=outptr (for first 8 bytes), rcx=out_ptr2 (for second 8 bytes)
Wait, let me re-check the signature from the decompiled code
sub2EB00 args: (arg0=int64, arg1=int64, arg2=QWORD, arg3=unsigned __int64)
rdi=keyval, rsi=qfe8, rdx=saltptr, 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 sub2EB00(keyval, qfe8, saltptr, outptr)
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 = STACKBASE + STACKSIZE – 0x2000
mu.memwrite(rsp, struct.pack(‘ADDR))
mu.regwrite(UCX86REGRSP, rsp)
mu.regwrite(UCX86REGRDI, debugkey)
mu.regwrite(UCX86REGRSI, qfe8)
mu.regwrite(UCX86REGRDX, HEAP + 0x2000) # salt pointer
mu.regwrite(UCX86REG_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.emustart(BASE + 0x2EB00, RETADDR, timeout=10000000)
rip = mu.regread(UCX86REGRIP)
if rip == RETADDR:
outdata = mu.memread(HEAP + 0x3000, 16)
print(f”sub2EB00 output: {bytes(outdata).hex()}”)
q0, q1 = struct.unpack(‘
**📋 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
withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\lib\x8664\libhajimi.so”, “rb”) as f: so_data = f.read()
BASE = 0x10000000 STACKBASE = 0x7FFF0000; STACKSIZE = 0x200000 HEAP = 0x20000000; FSBASE = 0x30000000; RETADDR = 0x40000000
defmakeemu(): mu = Uc(UCARCHX86, UCMODE64) 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 inrange(s,e,0x1000): if pg notin mapped: mu.memmap(pg,0x1000); mapped.add(pg) mu.memwrite(BASE+va, sodata[fo:fo+fs]) mu.memmap(STACKBASE, STACKSIZE) mu.memmap(HEAP, 0x200000) mu.memmap(FSBASE, 0x1000) mu.memwrite(FSBASE+0x28, struct.pack(‘<Q’, 0xDEADBEEFCAFEBABE)) mu.msrwrite(0xC0000100, FSBASE) mu.memmap(RETADDR, 0x1000) mu.memwrite(RETADDR, b’\xf4′) return mu
defcallfunc(mu, addr, rdi=0, rsi=0, rdx=0, rcx=0, r8=0, r9=0): rsp = STACKBASE + STACKSIZE – 0x2000 mu.memwrite(rsp, struct.pack(‘<Q’, RETADDR)) mu.regwrite(UCX86REGRSP, rsp) mu.regwrite(UCX86REGRDI, rdi) mu.regwrite(UCX86REGRSI, rsi) mu.regwrite(UCX86REGRDX, rdx) mu.regwrite(UCX86REGRCX, rcx) mu.regwrite(UCX86REGR8, r8) mu.regwrite(UCX86REGR9, r9) mu.emustart(BASE + addr, RETADDR, timeout=10000000) return mu.regread(UCX86REG_RAX)
Read file
withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\apkunzipped\assets\hjmpack.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: sub2EB00(debugkey, qfe8, saltptr, outptr) -> var_68
mu = makeemu() mu.memwrite(HEAP + 0x2000, salt) mu.memwrite(HEAP + 0x3000, b’\x00′ * 16) callfunc(mu, 0x2EB00, rdi=debugkey, rsi=qfe8, rdx=HEAP+0x2000, rcx=HEAP+0x3000) var68 = bytes(mu.memread(HEAP + 0x3000, 16)) print(f”var68 = {var_68.hex()}”)
Step 2: For each chunk, compute AES key via sub_24CB0
folded = (qfe8 ^ (qfe8 >> 32)) & 0xFFFFFFFF total = len(encrypted) numchunks = (total + 255) // 256 decrypted = bytearray(total) varE0 = 0
for ci inrange(numchunks): # Build 32-byte src buffer src = var68 # 16 bytes src += struct.pack(‘<II’, ci, qfe8 & 0xFFFFFFFF) # r15d, r13d (low 32 of qfe8) src += struct.pack(‘<Q’, (var_E0 ^ qfe8) & 0xFFFFFFFFFFFFFFFF)
mu2 = makeemu() mu2.memwrite(HEAP + 0x4000, src)
# sub24CB0(dataptr, length, seed) h1 = call_func(mu2, 0x24CB0, rdi=HEAP+0x4000, rsi=32, rdx=0x1357)
mu3 = makeemu() mu3.memwrite(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
counterbytes = struct.pack(‘
# AES-CTR decrypt (LE counter) cs = ci * 256 csz = min(256, total – cs) for bi inrange(0, csz, 16): block = nonce + struct.pack(‘<I’, bi // 16) cipher = AES.new(aeskey, AES.MODEECB) ks = cipher.encrypt(block) for k inrange(min(16, csz – bi)): decrypted[cs + bi + k] = encrypted[cs + bi + k] ^ ks[k]
varE0 += 0x100000000 if ci == 0: print(f”Chunk 0: key={aeskey.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}/{total8} ({bits100//(total*8)}%)”) print(f”First 32 bytes: {decrypted[:32].hex()}”) print(f”Last 32 bytes: {decrypted[-32:].hex()}”)
Save
withopen(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
withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin”, “rb”) as f: data = f.read()
64×64 bitmap, 1 bit per pixel = 512 bytes
Try different bit orderings
for name, getpixel in [ (“MSBfirst”, lambda y,x: (data[y8 + x//8] >> (7 – x%8)) & 1), (“LSBfirst”, lambda y,x: (data[y8 + x//8] >> (x%8)) & 1), (“MSBinv”, lambda y,x: 1 – ((data[y8 + x//8] >> (7 – x%8)) & 1)), (“LSBinv”, lambda y,x: 1 – ((data[y8 + x//8] >> (x%8)) & 1)), ]: img = Image.new(‘1’, (64, 64)) for y inrange(64): for x inrange(64): img.putpixel((x, y), getpixel(y, x)) fn = rf”D:\AI\AICTF\workdir\52pojie\chu8\qr{name}.png” imgscaled = 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 inrange(64): row = “” for x inrange(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 64×64 image with a QR code centered in it.
The QR code appears to be roughly 44×16 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 64×64 canvas
Let me try to scan it
import struct withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin”, “rb”) as f: data = f.read()
Count non-zero bytes per row
for y inrange(64): rowdata = data[y8:(y+1)8] ifany(b != 0for b in rowdata): bits = ”.join(str((row_data[x//8] >> (7-x%8)) & 1) for x inrange(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
withopen(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 inrange(64): for x inrange(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
imgbig = img.resize((640, 640), Image.NEAREST) imgbig.save(r”D:\AI\AICTF\workdir\52pojie\chu8\text_bitmap.png”)
Crop just the text area (rows 20-34, all 64 columns)
imgtext = img.crop((0, 20, 64, 35)) imgtextbig = imgtext.resize((640, 150), Image.NEAREST) imgtextbig.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 inrange(36, 43): rowdata = data[y8:(y+1)8] bits = ”.join(‘█’if (rowdata[x//8] >> (7-x%8)) & 1else’ ‘for x inrange(64)) print(f” {bits}”)
print(“\nText area rows 20-26 (line 1):”) for y inrange(20, 27): rowdata = data[y8:(y+1)8] bits = ”.join(‘█’if (rowdata[x//8] >> (7-x%8)) & 1else’ ‘for x inrange(64)) print(f” {bits}”)
print(“\nText area rows 28-34 (line 2):”) for y inrange(28, 35): rowdata = data[y8:(y+1)8] bits = ”.join(‘█’if (rowdata[x//8] >> (7-x%8)) & 1else’ ‘for x inrange(64)) print(f” {bits}”)
**📋 Python 输出** [14:36:57]
复制代码 隐藏代码 Vertical line area (rows 36-42): ██ █ █ █ █ █ ██
Textarea rows 20-26 (line 1): █████ █ ███ ███ ██ █ █ ███ █ █ █ █ ███ █ █ █ █ █ █ █ █ █ █ ██ ██ █ █ █ █ ████ █ █ █ █ █ █████ █ █ █ █ █ █ █ █ █ █ █████ █ ███ █ █ █ █ █ █ █ █ █ █████ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ ██ ██ █ █ █ █████ █ █ ███ ██ █ █ ██ █ █ █ █ █ █
Textarea 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
withopen(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 inrange(64): print(x % 10, end=””) print() for y inrange(20, 27): rowdata = data[y8:(y+1)8] print(f”R{y}: “, end=””) for x inrange(64): bit = (rowdata[x//8] >> (7 – x%8)) & 1 print(‘█’if bit else’ ‘, end=””) print()
print(“\nLine 2 (rows 28-34):”) print(“Col: “, end=””) for x inrange(64): print(x % 10, end=””) print() for y inrange(28, 35): rowdata = data[y8:(y+1)8] print(f”R{y}: “, end=””) for x inrange(64): bit = (rowdata[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 '52pojie*2026*' 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 inrange(20, 27): rowdata = data[y8:(y+1)8] s = “” for x inrange(64): bit = (rowdata[x//8] >> (7 – x%8)) & 1 s += ‘█’if bit else’·’ print(f”R{y}: {s}”)
print() for y inrange(28, 35): rowdata = data[y8:(y+1)8] s = “” for x inrange(64): bit = (rowdata[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\ctfagent2198965434.py”, line 78, in
[error: exit status 1]
### 🤖 助手 - 第 114 轮 [14:38:45]
#### 🐍 执行 Python 脚本
复制代码 隐藏代码
withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin”, “rb”) as f: data = f.read()
print(“Line 1:”) for y inrange(20, 27): rowdata = data[y8:(y+1)8] s = “” for x inrange(64): bit = (rowdata[x//8] >> (7 – x%8)) & 1 s += ‘█’if bit else’·’ print(f”R{y}: {s}”)
print(“\nLine 2:”) for y inrange(28, 35): rowdata = data[y8:(y+1)8] s = “” for x inrange(64): bit = (rowdata[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)
line1rows = [] line2rows = [] withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin”, “rb”) as f: data = f.read()
for y inrange(20, 27): row = [] for x inrange(64): bit = (data[y*8 + x//8] >> (7 – x%8)) & 1 row.append(bit) line1_rows.append(row)
for y inrange(28, 35): row = [] for x inrange(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
deffindchars(rows): chars = [] inchar = False start = 0 for x inrange(64): colhaspixel = any(rows[y][x] for y inrange(len(rows))) if colhaspixel andnot inchar: start = x inchar = True elifnot colhaspixel and inchar: chars.append((start, x)) inchar = False if in_char: chars.append((start, 64)) return chars
chars1 = findchars(line1rows) chars2 = findchars(line2rows)
print(“Line 1 characters:”) for i, (s, e) inenumerate(chars1): print(f” Char {i}: cols {s}-{e} (width {e-s})”) for y inrange(7): row = ”.join(‘█’if line1_rows[y][x] else’·’for x inrange(s, e)) print(f” {row}”) print()
print(“\nLine 2 characters:”) for i, (s, e) inenumerate(chars2): print(f” Char {i}: cols {s}-{e} (width {e-s})”) for y inrange(7): row = ”.join(‘█’if line2_rows[y][x] else’·’for x inrange(s, e)) print(f” {row}”) print()
<details>
<summary>📋 Python 输出 [14:38:58](较长,点击展开)</summary>
复制代码 隐藏代码 Line 1 characters: Char 0:cols2-7(width5) █████ █···· ████· █···· █···· █···· █····
Char 1:cols8-13(width5) █···· █···· █···· █···· █···· █···· █████
Char 2:cols14-19(width5) ·███· █···█ █···█ █████ █···█ █···█ █···█
Char 3:cols20-25(width5) ·███· █···█ █···· █·███ █···█ █···█ ·███·
Char 4:cols27-30(width3) ·██ ·█· ·█· █·· ·█· ·█· ·██
Char 5:cols32-37(width5) █···█ █···█ █████ █···█ █···█ █···█ █···█
Char 6:cols38-43(width5) ··███ ···█· ···█· ···█· ···█· █··█· ·██··
Char 7:cols44-49(width5) █···█ ██·██ █·█·█ █···█ █···█ █···█ █···█
Char 8:cols50-55(width5) █···█ █···█ █···█ █·█·█ █·█·█ ██·██ █···█
Char 9:cols56-61(width5) ·███· █···█ █···█ █████ █···█ █···█ █···█
Line 2 characters: Char 0:cols2-7(width5) ████· █···█ █···█ ████· █···· █···· █····
Char 1:cols8-13(width5) ··███ ···█· ···█· ···█· ···█· █··█· ·██··
Char 2:cols14-19(width5) ·███· █···█ ····█ ···█· ··█·· ·█··· █████
Char 3:cols20-25(width5) ·███· █···█ █··██ █·█·█ ██··█ █···█ ·███·
Char 4:cols26-31(width5) ·███· █···█ ····█ ···█· ··█·· ·█··· █████
Char 5:cols32-37(width5) ·███· █···· █···· ████· █···█ █···█ ·███·
Char 6:cols38-43(width5) █···█ ██··█ █·█·█ █··██ █···█ █···█ █···█
Char 7:cols44-49(width5) ████· █···█ █···█ ████· █···█ █···█ ████·
Char 8:cols50-55(width5) █···· █···· █···· █···· █···· █···· █████
Char 9:cols56-61(width5) ████· █···█ █···█ █···█ █···█ █···█ ████·
</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
withopen(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 inrange(36, 43): rowdata = data[y8:(y+1)8] s = “” for x inrange(28, 36): bit = (rowdata[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]
### 完整WP
---
### title: [52pojie] chu8 category: reverse tags: [reverse, rev, APK, JNI, IDA, Unicorn, AES-CTR, SipHash]
## 初8 — Android 原生库逆向与 HJM1 位图解密
### 题目概述
本题为逆向工程(Reverse)类题目,核心考点是 Android APK 中 JNI 原生库的密码学逆向分析。需要逆向 `libhajimi.so` 中的多层密钥派生链(XXHash64 → SipHash-2-4 → splitmix64 → AES-CTR),解密自定义 HJM1 格式的加密位图文件,最终从 64×64 像素字体渲染的位图中读取 flag。难度中高,涉及多种哈希算法和对称加密的组合逆向。
### 初始分析
#### APK 结构探索
题目提供一个 Android APK 文件,解压后发现关键组件:
* `lib/x86_64/libhajimi.so`
:核心原生库,包含所有加解密逻辑
* `assets/hjm_pack.bin`
:HJM1 格式的加密位图文件(待解密目标)
* Java 层代码经过混淆(`f1.h`、`Q0.AbstractC0080d` 等),但通过 jadx 反编译可识别关键 JNI 调用
#### 关键发现
1. Java 层的 `NativeBridge` 类暴露了多个 JNI 方法:`startSession`、`checkRhythm`、`updateExp`、`verifyAndDecrypt`
2. `beatMap`
数据硬编码为 `[0, 250, 500, 750]`,`interval = 250`——这是一个节奏游戏的节拍数据
3. `hjm_pack.bin`
文件头为 `HJM1`(`0x314D4A48`),后跟 salt(16字节)、IV(12字节)和 512 字节密文
4. 原生库中存在一条 debug bypass 路径(`sub_2E570`),密钥生成完全基于数学运算,不依赖运行时状态
#### 漏洞/算法类型判断
通过 IDA 反汇编 `verifyAndDecrypt`(0x24850)发现:
* 地址 0x24b8b 处检查 `byte_5D140`(debugBypass 标志)
* 若 debug 模式开启,调用 `sub_2E570` 计算密钥,否则使用游戏过程中积累的 `qword_5CFF0`
* 密钥经过可选的 `0xA5A5A5A5A5A5A5A5` XOR 变换后传入 `sub_2E680` 进行 AES-CTR 解密
### 解题思路
#### 整体思路形成
解题的核心在于:**beatMap 是唯一的外部输入,整个密钥派生链是确定性的**。
最初尝试追踪复杂的运行时状态(`startSession`、`checkRhythm`、`updateExp`),但这些是红鲱鱼。关键突破点是发现 debug bypass 路径 `sub_2E570`,它表明密钥生成是纯数学计算,只需要 `qword_5CFE8`(beatMap 的 XXHash64 哈希值)。
#### 尝试过的方向
1. **纯 Python 重写所有算法**
:先验证 SipHash-2-4 和 XXHash64 的正确性(通过标准测试向量),再实现完整派生链。初次解密结果为随机数据(51% 比特置位),说明某个环节有误。
2. **多种密钥变体尝试**
:测试了 debug\_key raw、debug\_key ^ 0xA5、qfe8 raw、qfe8 ^ 0xA5 四种组合,均未得到有意义输出。
3. **Unicorn 模拟执行**
:由于纯 Python 实现可能在细节上出错,转而使用 Unicorn 引擎直接模拟原生库函数。遇到 SSE4.1 指令(`pinsrb`)不支持的问题,但成功模拟了不含 SSE4.1 的子函数。
4. **最终成功路径**
:用 Unicorn 模拟 `sub_2EB00`(密钥派生)和 `sub_24CB0`(XXHash64 折叠),结合 PyCryptodome 的 AES-CTR 完成解密。
#### 关键转折
纯 Python 实现失败的根本原因是 `sub_2EB00` 中 SipHash 的密钥构造细节——XOR 掩码 `0x1A8CBC5B802E097C` 和 `0x802E097C1A8CBC5B` 的应用顺序在反汇编中容易看错。通过 Unicorn 直接执行原生代码绕过了这个问题。
### 详细步骤
#### 第一阶段:提取 beatMap 与计算基础哈希
从 Java 反编译代码中提取 beatMap 数据 `[0, 250, 500, 750]`,使用 XXHash64 计算其哈希值作为后续密钥派生的种子:
复制代码 隐藏代码 import struct, xxhash
beatmap = struct.pack(‘<4I’, 0, 250, 500, 750) qfe8 = xxhash.xxh64(beatmap, seed=0x1A8CBC5B802E097C).intdigest()
qfe8 = 0x810a3647628f0212
这里 `0x1A8CBC5B802E097C` 是从 `startSession` 函数(反汇编地址 0x2E570 附近)中提取的 XOR 常量,用作 XXHash64 的种子。`qword_5CFE8` 存储了这个哈希结果,是整个密钥派生链的起点。
#### 第二阶段:debug bypass 密钥计算(sub\_2E570)
`sub_2E570` 实现了 debug 模式下的密钥生成逻辑。通过反汇编分析,其流程为:
1. 读取 `dword_5CFD0`(节拍数 = 4)和 `qword_5CFE8`
2. 计算 `r14 = 0x1A8CBC5B802E097C ^ qfe8`
3. 对 `r14` 执行 splitmix64 变换
4. 循环 4 次(对应 4 个节拍),每次用 SipHash-2-4 混合结果
5. 最终输出 debug\_key
复制代码 隐藏代码 defsplitmix64(state): state = u64(state + 0x9E3779B97F4A7C15) z = state z = u64((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9) z = u64((z ^ (z >> 27)) * 0x94D049BB133111EB) return state, z ^ (z >> 31)
r14 = 0x1A8CBC5B802E097C ^ qfe8
r14 = u64(0x1A8CBC5B802E097C ^ qfe8) state, sm_result = splitmix64(r14)
循环4次 SipHash 混合…
debug_key = 0xd2c13a26769ce7e0
#### 第三阶段:verifyAndDecrypt 中的密钥选择逻辑
反汇编 `verifyAndDecrypt`(0x24850)中地址 0x24be1-0x24bf5 的关键代码:
复制代码 隐藏代码 24be1: mov rsi, 0xA5A5A5A5A5A5A5A5 24beb: xor rsi, rax ; rsi = debugkey ^ 0xA5… 24bee: test r13b, r13b ; r13b = 密码验证是否通过 24bf1: cmovz rsi, rax ; 若密码未验证,不做 XOR 24bf5: call sub2E680 ; 传入最终密钥进行解密
这段代码的含义是:
* 当 `r13b = 0`(密码未验证,即 `byte_5D00C != 1`),`cmovz` 生效,`rsi = rax`(原始 debug\_key,不做 XOR)
* 当 `r13b = 1`(密码已验证),保持 `rsi = debug_key ^ 0xA5A5A5A5A5A5A5A5`
经过测试,**不做 XOR 的 debug\_key raw 路径**才是正确的解密密钥。这对应 debug bypass 场景下密码未经验证的情况。
#### 第四阶段:解析 HJM1 文件格式
复制代码 隐藏代码 withopen(“hjm_pack.bin”, “rb”) as f: hjm = f.read()
HJM1 文件格式:
[0:4] = magic “HJM1” (0x314D4A48)
[4:8] = version/flags
[8:16] = encrypted data size (8 bytes LE)
[16:24] = reserved
[24:40] = salt (16 bytes)
[40:52] = IV/nonce (12 bytes)
[52:] = encrypted data (512 bytes = 64×64 bitmap, 1 bit/pixel)
salt = hjm[24:40] # 0f1e2d3c4b5a69788796a5b4c3d2e1f0 iv = hjm[40:52] # 112233445566778899aabbcc ciphertext = hjm[52:52+512]
#### 第五阶段:Unicorn 模拟 sub\_2EB00 密钥派生
由于纯 Python 实现 `sub_2EB00` 的 SipHash 密钥构造细节容易出错,改用 Unicorn 引擎直接模拟执行原生代码:
复制代码 隐藏代码 from unicorn import * from unicorn.x86_const import *
加载 libhajimi.so 到 Unicorn
withopen(“libhajimi.so”, “rb”) as f: so_data = f.read()
BASE = 0x10000000 mu = Uc(UCARCHX86, UCMODE64)
映射 ELF 段
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 mu.memmap(s, e – s) mu.memwrite(BASE + va, so_data[fo:fo+fs])
设置栈、堆、FS段
STACKBASE = 0x7FFF0000; STACKSIZE = 0x200000 mu.memmap(STACKBASE, STACKSIZE) mu.memmap(0x20000000, 0x200000) # heap mu.memmap(0x30000000, 0x1000) # FS base mu.memwrite(0x30000000 + 0x28, struct.pack(‘<Q’, 0xDEADBEEFCAFEBABE)) mu.msr_write(0xC0000100, 0x30000000)
调用 sub2EB00(rdi=debugkey, rsi=qfe8, rdx=outbuf, rcx=outsize)
… 设置参数并执行
`sub_2EB00` 的核心逻辑是:
1. 用 `0x1A8CBC5B802E097C ^ qfe8` 和 `0x802E097C1A8CBC5B ^ qfe8` 作为 SipHash 的两个密钥
2. 对 `[debug_key, qfe8]` 这 16 字节数据执行 SipHash-2-4
3. 输出 16 字节的密钥材料 `var_68`
模拟执行得到:
复制代码 隐藏代码 var_68 = 7f721557831c2dbd7f70756f448aee26 q0 = 0xbd2d1c835715727f q1 = 0x26ee8a446f75707f
#### 第六阶段:AES-CTR 密钥与 Nonce 生成
`sub_2E680` 将 512 字节密文分为 2 个 256 字节的 chunk,每个 chunk 独立生成 AES 密钥和 nonce。
对于每个 chunk,构造 32 字节的哈希输入缓冲区:
复制代码 隐藏代码
src[0:16] = var68 (来自 sub2EB00 的密钥材料)
src[16:20] = chunk_counter (uint32 LE)
src[20:24] = qfe8 的低 32 位
src[24:32] = qfe8 ^ (chunk_idx * 0x100000000)
然后调用 `sub_24CB0`(XXHash64 + 折叠)两次生成 AES-128 密钥的两个 64 位半部分:
复制代码 隐藏代码 defsub_24CB0(data, seed): “””XXHash64 with final fold: result ^ (result >> 32)””” h = xxhash.xxh64(data, seed=seed).intdigest() return u64(h ^ (h >> 32))
AES key = [sub24CB0(src, 4951), sub24CB0(src, 9320)]
h1 = sub24CB0(srcbuf, 4951) # 低 8 字节 h2 = sub24CB0(srcbuf, 9320) # 高 8 字节 aes_key = struct.pack(‘<QQ’, h1, h2) # 16 字节 AES-128 密钥
同样通过 Unicorn 模拟 `sub_24CB0` 确保结果正确:
复制代码 隐藏代码 Chunk 0: key = 16d6d0d72e99938d72d9bcd5616451ef nonce = 112233440052f26b99aabbcc
Nonce 的构造方式是将文件中的 12 字节 IV 与 chunk 计数器和折叠后的 `qfe8` 进行 XOR:
复制代码 隐藏代码 folded_qfe8 = u32(qfe8 ^ (qfe8 >> 32)) # 0xe3853455
nonce[0:4] = IV[0:4] ^ struct.pack(‘<I’, chunk_counter)
nonce[4:8] = IV[4:8] ^ struct.pack(‘<I’, folded_qfe8)
nonce[8:12] = IV8:12
#### 第七阶段:AES-CTR 解密
使用 PyCryptodome 执行 AES-128-CTR 解密:
复制代码 隐藏代码 from Crypto.Cipher import AES from Crypto.Util import Counter
对每个 256 字节 chunk:
ctr = Counter.new(32, prefix=nonce, initialvalue=0, littleendian=True) cipher = AES.new(aeskey, AES.MODECTR, counter=ctr) decryptedchunk = cipher.decrypt(ciphertextchunk)
解密后得到 512 字节数据,仅 7% 的比特被置位(314/4096),说明这是一个稀疏位图——大部分区域为空白,中间区域包含像素字体渲染的文本。
#### 第八阶段:位图渲染与字符识别
解密后的 512 字节是 64×64 的单色位图(1 bit/pixel,MSB first)。通过逐行渲染发现:
* 行 0-19:全零(空白)
* 行 20-26:第一行文本(7 像素高的字符)
* 行 27:空白分隔行
* 行 28-34:第二行文本
* 行 35:空白
* 行 36-42:闭合花括号 `}`
* 行 43-63:全零(空白)
通过列分割算法识别字符边界,逐个字符比对像素字体:
复制代码 隐藏代码 Line 1 字符识别: Char 0 (cols 2-7): F — █████/█/████/█/█/█/█ Char 1 (cols 8-13): L — █/█/█/█/█/█/█████ Char 2 (cols 14-19): A — ·███·/█···█/█···█/█████/█···█/█···█/█···█ Char 3 (cols 20-25): G — ·███·/█···█/█/█·███/█···█/█···█/·███· Char 4 (cols 27-30): { — ·██/·█/·█/█/·█/·█/·██ Char 5 (cols 32-37): H — █···█/█···█/█████/█···█/█···█/█···█/█···█ Char 6 (cols 38-43): J — ··███/···█/···█/···█/···█/█··█/·██ Char 7 (cols 44-49): M — █···█/██·██/█·█·█/█···█/█···█/█···█/█···█ Char 8 (cols 50-55): W — █···█/█···█/█···█/█·█·█/█·█·█/██·██/█···█ Char 9 (cols 56-61): A — ·███·/█···█/█···█/█████/█···█/█···█/█···█
Line 2 字符识别: Char 0: P — ████/█···█/█···█/████/█/█/█ Char 1: J — ··███/···█/···█/···█/···█/█··█/·██ Char 2: 2 — ·███/█···█/····█/···█/··█/·█/█████ Char 3: 0 — ·███/█···█/█··██/█·█·█/██··█/█···█/·███ (斜线零) Char 4: 2 — 同 Char 2 Char 5: 6 — ·███/█/█/████/█···█/█···█/·███ Char 6: N — █···█/██··█/█·█·█/█··██/█···█/█···█/█···█ Char 7: B — ████/█···█/█···█/████/█···█/█···█/████ Char 8: L — █/█/█/█/█/█/█████ Char 9: D — ████/█···█/█···█/█···█/█···█/█···█/████
闭合花括号 } (rows 36-42): ██/·█/·█/··█/·█/·█/██
拼合结果:`FLAG{HJMWAPJ2026NBLD}`
### 关键代码/命令
#### 位图渲染与字符分割
复制代码 隐藏代码 withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\decrypted_emu.bin”, “rb”) as f: data = f.read()
print(“Line 1:”) for y inrange(20, 27): rowdata = data[y8:(y+1)8] s = “” for x inrange(64): bit = (rowdata[x//8] >> (7 – x%8)) & 1 s += ‘█’if bit else’·’ print(f”R{y}: {s}”)
print(“\nLine 2:”) for y inrange(28, 35): rowdata = data[y8:(y+1)8] s = “” for x inrange(64): bit = (rowdata[x//8] >> (7 – x%8)) & 1 s += ‘█’if bit else’·’ print(f”R{y}: {s}”)
#### 字符边界自动检测
复制代码 隐藏代码 line1rows = [] withopen(r”D:\AI\AICTF\workdir\52pojie\chu8\decryptedemu.bin”, “rb”) as f: data = f.read()
for y inrange(20, 27): row = [] for x inrange(64): bit = (data[y*8 + x//8] >> (7 – x%8)) & 1 row.append(bit) line1_rows.append(row)
deffindchars(rows): chars = [] inchar = False start = 0 for x inrange(64): colhaspixel = any(rows[y][x] for y inrange(len(rows))) if colhaspixel andnot inchar: start = x inchar = True elifnot colhaspixel and inchar: chars.append((start, x)) inchar = False if in_char: chars.append((start, 64)) return chars
chars1 = findchars(line1rows) for i, (s, e) inenumerate(chars1): print(f” Char {i}: cols {s}-{e} (width {e-s})”) for y inrange(7): row = ”.join(‘█’if line1_rows[y][x] else’·’for x inrange(s, e)) print(f” {row}”)
#### 闭合花括号验证
复制代码 隐藏代码 withopen(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 inrange(36, 43): rowdata = data[y8:(y+1)8] s = “” for x inrange(28, 36): bit = (rowdata[x//8] >> (7 – x%8)) & 1 s += ‘█’if bit else’·’ print(f”R{y}: {s}”)
输出:
R36: ··██····
R37: ···█····
R38: ···█····
R39: ····█··· ← 向右缩进,确认是 }
R40: ···█····
R41: ···█····
R42: ··██····
### Flag
复制代码 隐藏代码 FLAG{HJMWAPJ2026NBLD}
### 总结与收获
#### 核心漏洞原理
本题的核心不是传统意义上的"漏洞",而是对多层密码学密钥派生链的完整逆向。密钥派生路径为:
复制代码 隐藏代码 beatMap [0,250,500,750] → XXHash64(seed=0x1A8CBC5B802E097C) → qfe8 → splitmix64 + SipHash-2-4 循环 → debugkey → SipHash-2-4(debugkey, qfe8) → var68 (16字节密钥材料) → XXHash64fold(var_68 || counter, seed=4951/9320) → AES-128 密钥 → AES-CTR 解密 → 64×64 位图 → 像素字体文本 “`
利用技巧
- Unicorn 模拟执行是逆向复杂原生函数的利器:当纯 Python 重写容易在细节上出错时(如 SipHash 密钥构造中 XOR 掩码的字节序),直接模拟执行原生代码可以绕过实现错误。但需注意 Unicorn 对 SSE4.1 等高级指令集的支持有限。
- debug bypass 路径是突破口:Android 原生库中常保留调试路径,这些路径通常简化了密钥生成逻辑,使其不依赖复杂的运行时状态。识别
byte_5D140(debugBypass 标志)和cmovz条件选择是关键。 - XXHash64 的折叠变体:
sub_24CB0返回h ^ (h >> 32),虽然反编译器显示为v20 ^ HIDWORD(v20)暗示 32 位结果,但实际汇编xor rax, rsi是 64 位操作,返回值仍为完整 64 位。
易错点
-
verifyAndDecrypt中的
cmovz指令决定是否对 debug_key 做^ 0xA5A5A5A5A5A5A5A5,取决于r13b(密码验证状态)。在 debug bypass 场景下应使用原始 debug_key(不做 XOR)。 -
AES-CTR 的计数器是小端序(little-endian),PyCryptodome 默认使用大端序,需要显式指定
little_endian=True。 -
解密后的位图不是 QR 码,而是像素字体渲染的文本,需要通过列分割和字形比对来识别字符。
由于文章过长,文中部分代码精简,可以点击左下角进入原文阅读。
-官方论坛
www.52pojie.cn
👆👆👆
公众号设置“星标”,您不会错过新的消息通知
如开放注册、精华文章和周边活动等公告
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:吾爱破解论坛 吾爱pojie 吾爱pojie《【2026春节】全自动AI做题的实现及初8逆向AIAgent对话记录及wp》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论