首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
13
15
[原创]2026腾讯游戏安全初赛 Android 客户端分析
发表于: 2026-4-20 21:44
33429
[原创]2026腾讯游戏安全初赛 Android 客户端分析
mxystery
2
2026-4-20 21:44
33429
# 2026腾讯游戏安全初赛 Android 客户端分析 ## 目录 **1. 逆向分析过程** - [1.1 分析环境与真机条件](#11-分析环境与真机条件) - [1.2 刚接触题目时的初始分析](#12-刚接触题目时的初始分析) - [1.3 解题思路流程图](#13-解题思路流程图) - [1.4 从 APK 结构入手,先确认主战场不在脚本明文](#14-从-apk-结构入手先确认主战场不在脚本明文) - [1.5 先拆 `libsec2026.so` 的运行时壳,再把真实执行段送回 IDA](#15-先拆-libsec2026so-的运行时壳再把真实执行段送回-ida) - [1.6 顺着 GDExtension 注册链定位 `Process.input(PackedByteArray) -> String`](#16-顺着-gdextension-注册链定位-processinputpackedbytearray---string) - [1.7 先恢复 native 核心算法,再发现还缺了一层脚本预处理](#17-先恢复-native-核心算法再发现还缺了一层脚本预处理) - [1.8 通过场景树与对象调用链拿到真正的绿块触发路径](#18-通过场景树与对象调用链拿到真正的绿块触发路径) - [1.9 真实传送、实机截图与 `xor_enc` 的闭环恢复](#19-真实传送实机截图与-xor_enc-的闭环恢复) - [1.10 完整逆向执行总流程图](#110-完整逆向执行总流程图) **2. 算法逻辑分析** - [2.1 PART1 的完整正向链条](#21-part1-的完整正向链条) - [2.2 `xor_enc` 的精确形式](#22-xor_enc-的精确形式) - [2.3 native 变种 ChaCha20 的关键参数](#23-native-变种-chacha20-的关键参数) - [2.4 逆算法分析](#24-逆算法分析) - [2.5 代码实现与可复现性](#25-代码实现与可复现性) **3. 安全机制解析** - [3.1 自解密壳:用运行时映射取代静态 `.text`](#31-自解密壳用运行时映射取代静态-text) - [3.2 资源加密:PCK 加密与 `FileAccessEncrypted` 双重阻断](#32-资源加密pck-加密与-fileaccessencrypted-双重阻断) - [3.3 框架级混淆:把核心逻辑藏进 GDExtension 和对象系统](#33-框架级混淆把核心逻辑藏进-gdextension-和对象系统) - [3.4 交互层限制:用场景布局和示例方块干扰主线](#34-交互层限制用场景布局和示例方块干扰主线) - [3.5 游戏解包 key 与资源保护链](#35-游戏解包-key-与资源保护链) **4. 附:token生成逻辑分析** - [4. 附:token生成逻辑分析](#4-附token生成逻辑分析) # 1. 逆向分析过程 ## 1.1 分析环境与真机条件 | 项目 | 实际环境 | |---|---| | 主机环境 | Windows | | 使用工具 | Frida 17.9.1、IDA、JADX | | 真机设备 | 小米11,刷入安卓16,已开启内核级 Root 环境 | ## 1.2 刚接触题目时的初始分析 刚拿到题目时,手上的信息其实已经足够构成三条候选路线。 第一,APK 内能看到典型的 Godot 项目结构,这说明脚本、场景与扩展配置都可能藏有关键逻辑。 第二,包内同时存在独立的 `libsec2026.so`,这又说明题目很可能把核心逻辑下沉到了 native 层。 第三,题目运行在真机上,屏幕左上角会实时生成 token,场景里还有黄色与绿色两个触发块,因此这不是“离线恢复一个固定 flag”就能结束的题,而是要对任意 token 建立稳定的 `token -> flag` 和 `flag -> token` 映射。 | 手上已有的东西 | 可以立刻做的动作 | 可能得到的产出 | 对主线的帮助 | |---|---|---|---| | APK 与资源目录 | 尝试解包 pack、恢复 `.gdc` | 脚本、场景、扩展配置 | 若成功,可直接看上层逻辑 | | `libsec2026.so` | 静态查看入口,动态跟踪运行时跳转 | 真实执行段、导出链、算法骨架 | 能快速锁定 native 主算法 | | 真机frida | 运行态 hook、对象调用、场景探测 | 真实 flag 样本、对象层级、触发条件 | 能验证推导是否与游戏真实行为一致 | ## 1.3 解题思路流程图  ## 1.4 从 APK 结构入手,先确认主战场不在脚本明文 分析从 APK 的整体结构开始。包体内同时存在 Godot 项目资源和本地 so: 一侧是 `assets/project.binary`、`assets/assets.sparsepck`、`assets/token.gdc`、`assets/Trigger/trigger.gdc`、`assets/label2.gdc`, 另一侧是 `lib/arm64-v8a/libgodot_android.so` 与 `lib/arm64-v8a/libsec2026.so`。 这说明题目逻辑既可能藏在 Godot 资源层,也可能藏在 native 扩展里,因此两条线都要保留,但要先判断哪一条阻力更小。 我先沿资源线做了快速验证,从github上了解到`GDRE_tools-v2.5.0-beta.5-windows`项目可以直接解包,但是控制台输出: ```text Cannot open encrypted pck! (wrong key?) Failed to load sparse pack! ``` 这两行已经足够说明 `assets.sparsepck` 的目录本身就是加密的,连资源枚举都无法顺利完成。进一步检查 `token.gdc`、`Trigger/trigger.gdc`、`label2.gdc` 与 `sec2026.gdextension` 的文件头,又能看出它们带有 Godot `FileAccessEncrypted` 风格包装。换句话说,资源线不是没有价值,而是它一上来就被“pack 目录加密 + 单文件再加密”双重阻断,短时间内不适合作为主线。 这一步最重要的不是“把资源完全解出来”,而是及时得出一个策略结论:资源线暂时只能当备线,用来佐证推测或补充个别细节,不能继续吞掉主要时间。也正因为这个判断,后续主线被坚定地切换到 native。 ## 1.5 先拆 `libsec2026.so` 的运行时壳,再把真实执行段送回 IDA `libsec2026.so` 的第一道门槛是自解密壳。静态看 `start` 时只能看到很少量函数,但入口行为非常明确:它会读取 `/proc/self/auxv`,调用 `sub_69984` 做解压或解密,随后执行 `memfd_create`、`mmap(PROT_EXEC)`,最后通过 `BR X14` 跳到运行时代码。这类写法的目的很直接,就是让真正的业务逻辑不以普通 ELF `.text` 段的形式静态落地,而是在进程启动后再被铺进内存。 原始 IDA 视图本身就给出了一个很强的异常信号。下图是最初未回填运行时代码时的 IDA 顶部概览条,可以看到大部分区域并不是正常的“已识别函数区”,而是被 `Unexplored`、`Data` 和少量 `Library function` 占据。 对于一个已经完整装载、主体逻辑都老老实实落在 `.text` 段中的 so,这样的比例显然很不自然。也正因为这个现象,我没有把它简单理解成“IDA 一时没识别出来”,而是把它视为“静态文件里并没有完整业务代码,真正代码在运行时才出现”的直接信号。 为了把这条链截下来,我写了一个 Frida 脚本,专门跟住 `0x6996c` 处的跳转、第二层 `mmap` 包装器和第二层最后一次调用点,自动识别最终可执行映射并导出整段代码。核心逻辑如下: ```javascript const BR_OFFSET = 0x6996c; const STAGE2_FINAL_CALL = 0x1778; const STAGE2_MMAP = 0x74; Interceptor.attach(brAddr, { onEnter() { const mappedExecBase = ptr(this.context.x14).sub(0x10); dumpHex(mappedExecBase, DUMP_SIZE); hookStage2Mmap(mappedExecBase); hookStage2Final(mappedExecBase); } }); ``` 脚本跑起来后,终端里出现了下面这组关键输出: ```text [MMAP] ret=0x7087352000 size=0x9bc60 prot=0x5 flags=0x12 fd=115 off=0x0 [FINAL] x3=0x7087353c48 ... sym=0x7087353c48 libsec2026.so!0x4cc48 [FINAL_RANGE] base=0x7087352000 size=0x9c000 prot=r-x [DUMP] base=0x7087352000 size=0x9c000 ``` 这几行把问题说得非常清楚:真正执行段的运行时基址是 `0x7087352000`,大小约 `0x9c000`,真正入口是 `0x7087353c48`,折算回原模块偏移约 `0x4cc48`。随后把这段代码回填到 IDA 的 `0x4b000..0xe6fff` 区间后,IDA 自动重新识别出约 3491 个函数,分析才真正进入业务逻辑阶段。 因此,“为什么判断它是运行时自解密”并不是只靠某一个现象,而是由三组证据共同支撑的:原始 IDA 视图中极不自然的函数分布;入口路径上明确出现的 `memfd_create`、`mmap(PROT_EXEC)` 与 `BR X14`;以及顺着这条路径实际 dump 出来的新 `r-x` 映射和回填后恢复出的函数区。三组证据首尾相扣,所以可以较强地判断这不是普通混淆,而是明确的运行时自解密 / 自装载过程。 ## 1.6 顺着 GDExtension 注册链定位 `Process.input(PackedByteArray) -> String` 拿到真实执行段后,下一步不是立刻找算法,而是先找 Godot 和 native 之间的桥。为此我又写了一个 Frida 跟踪脚本,在 `extension_init` 周围 hook `get_proc_address`,把 GDExtension 注册过程中请求到的接口全部打出来,并重点跟住 `classdb_register_extension_class5`、`classdb_register_extension_class_method`、`classdb_register_extension_class_property`、`classdb_register_extension_class_signal`。 脚本中用于识别候选方法签名的部分如下: ```javascript console.log( `[REG_METHOD] class=${className} class_ptr=${classNamePtr} method=${methodName} method_ptr=${methodNameObj} call=${callFunc} ptrcall=${ptrcallFunc} flags=0x${flags.toString(16)} has_return=${hasReturn} arg_count=${argCount} ${parts.join(" ")}` ); if (hasReturn === 1 && argCount === 1 && parts.join(" ").indexOf("ret_type=4") !== -1 && parts.join(" ").indexOf("arg0_type=29") !== -1) { hookCandidateMethod(callFunc, ptrcallFunc, "packedbytearray_to_string"); } ``` 脚本跑起来后,很快就出现了两条决定性的输出: ```text [EXT_STRNAME] ... text=Process [EXT_STRNAME] ... text=input [REG_METHOD] ... call=0x708776cde4 ptrcall=0x708776ce3c flags=0x1 has_return=1 arg_count=1 ret_type=4 ... arg0_type=29 ... ``` 这说明两件事。第一,GDExtension 确实注册了自定义类 `Process` 和方法 `input`。第二,这个方法的类型签名正好是 `PackedByteArray -> String`,和题目表现出来的“输入某种字节流,输出可显示字符串”高度吻合。到这里为止,native 主入口不再是一个猜测,而是被运行时注册链直接锁定了。 为了继续走深一层,我又补了一个专门追 `Process.input` 的脚本,在注册阶段直接读取 `method_userdata`、虚表槽位和 call/ptrcall 包装器,最终得到两条稳定调用链: `ptrcall` 主链为 `0x63e3c -> 0x4b6e0 -> 0x4ce04 -> 0x4c8e4 -> 0x4e198`, `call` 主链为 `0x63de4 -> 0x4d7a8 -> 0x4ffd0 -> 0x4bd68 -> 0x4c8e4 -> 0x4e198`。 这说明 `0x63de4/0x63e3c` 只是 Godot 包装层,真正要分析的核心逻辑在 `0x4e198`,而 `0x4bd68/0x4c8e4` 则负责 `Variant`、`PackedByteArray` 与 `String` 之间的桥接。 ## 1.7 先恢复 native 核心算法,再发现还缺了一层脚本预处理 当 `Process.input` 被定位后,native 层的算法轮廓很快浮现出来。对 `0x4e198` 的行为观察表明,它会先初始化一个工作缓冲区,再读取 `PackedByteArray` 的数据指针与长度,对输入做字节级 XOR 处理,最后把结果逐字节格式化成两位大写十六进制字符串。格式化这一点有一个非常直接的证据:在 `0x4c6d4` 处可以确认到格式串 `%02X`。因此,`Process.input` 的返回值不是“原始字节直接转字符”,而是“输出字节流先做大写 hex 编码,再返回 Godot String”。 继续往下拆,算法核心被收敛到一组 ChaCha 风格函数。结合静态跟踪与动态调用,可以把相关职责整理为:`0x5bb54` 是 quarter round,`0x5bcec` 负责根据 16-word state 生成 64 字节 keystream block,`0x5b950` 负责把 keystream 和输入按块 XOR,`0x5b818` 负责 ctx 初始化。最初看到 `16 / 12 / 8 / 7` 这组旋转常数时,很容易把它当成标准 ChaCha20;但后续动态验证表明,这只能说明“轮函数形似 ChaCha20”,并不能说明它就是标准实现。 为了把这个判断坐实,我又写了一个实时 hook 脚本,直接在活进程中挂住 `base + 0x5b818`、`base + 0x5bf18` 与 `base + 0x4e548`,读取 key、nonce、ctx state 和输入输出缓冲区。脚本中的关键常量如下: ```javascript const PROCESS_INPUT_CORE_OFFSET = 0x4e548; const CHACHA_CTX_INIT_OFFSET = 0x5b818; const CHACHA_XOR_STREAM_OFFSET = 0x5bf18; const KEY_OFFSET = 0xed5d2; const NONCE_OFFSET = 0xed5f3; ``` 通过这条运行时路径,先拿到了真正参与算法的 key 和 nonce,即 `Th1s ls n0t a rea1 key!!@sec2026` 与 `012345678901`。这一步非常重要,因为它把此前看到的那串可疑字符串重新定性了:它确实不是 PCK 解密 key,但它又并非完全无用,而是被 PART1 native 算法拿来当作 32 字节 key。紧接着,通过直接调用 `ctx_init/xor_stream`,又确认 state 的前四个常量 word 实际为: ```text 0x61707866 0x3320646f 0x79622d31 0x6b206573 ``` 这与标准 ChaCha20 的 `0x61707865 0x3320646e 0x79622d32 0x6b206574` 只差了每个 word 的最后一个字节,但正是这 4 个 byte 的变化足以让标准库推导全部失效。这个结论来自实际运行态 state,而不是人工猜测,因此后续求解器必须自己实现变种 ChaCha,而不能直接调用现成标准实现。 做到这里时,native 层看起来已经几乎闭环,于是我又主动调用内部流加密函数,对屏幕左上角的 token `ca567fe5` 做了两种候选实验:一是把 ASCII `ca567fe5` 的 8 字节直接送入算法,得到 `197E667F44504649`;二是先按十六进制解码成 4 字节 `CA 56 7F E5`,再送入算法,得到 `B0492CAC`。这一步不是线下手算,而是借助题目自身的 native 实现直接算出来的。 但也正是在这里,native 主线第一次显露出“还差半步”。候选值虽然都能算出来,却没有一个能和绿色方块的真实结果对上。也就是说,native 本身已经基本看清,但输入流真正进入 `Process.input` 之前,前面还隔着一层尚未恢复的脚本预处理。 ## 1.8 通过场景树与对象调用链拿到真正的绿块触发路径 如果绿色方块在正常玩法中可达,那么这个问题只需要撞一次实机就能解决;但题目的绿色方块在房顶,小车无法正常到达,因此我们开始转向场景层。首先反编 Java 壳,可以看到 Godot Android 层保留了对象方法调用桥: ```java public static void callobject(long j, String str, Object[] objArr) { Callable.call(j, str, objArr); } public static final Object call(long j, String str, Object... objArr) { return INSTANCE.call(j, str, objArr); } ``` 这意味着,只要拿到任意 Godot 对象的 `godotObjectId`,就能不依赖脚本明文,直接从 Java bridge 调用它的方法。 不过这里也没有一步到位。真正尝试时碰到一个很典型的小坑:通过 Java bridge 直接调 `Engine.get_main_loop()`,实际返回的是 `null`。这说明 Java 层桥虽然可用,但还不足以单独打穿整条场景链。 路线再次收束到 native helper,也就是继续借助 `classdb_get_method_bind` 和 `object_method_bind_ptrcall` 去走引擎对象链。于是我从github官方拉取了 Godot 4.5.1 ,提取出 `Engine.get_main_loop`、`SceneTree.get_root`、`SceneTree.get_current_scene` 等方法的 hash 后,核心流程如下: ```javascript const mbGetMainLoop = getMethodBind(..., "Engine", "get_main_loop", 1016888095); const sceneTreePtr = ptrcall0(ptrcall, mbGetMainLoop, enginePtr); const mbGetRoot = getMethodBind(..., "SceneTree", "get_root", 1757182445); const rootPtr = ptrcall0(ptrcall, mbGetRoot, sceneTreePtr); const mbGetCurrentScene = getMethodBind(..., "SceneTree", "get_current_scene", 3160264692); const currentScenePtr = ptrcall0(ptrcall, mbGetCurrentScene, sceneTreePtr); ``` 这一轮不是停留在“理论上可行”,而是实际打通了 `Engine -> SceneTree -> root/current_scene`。继续向下枚举后,很快发现 `current_scene` 其实只是 UI 容器,它下面只有两个孩子,显示的正是 `Token: ca567fe5` 和 `flag{sec2026_PART0_example}`;真正的 3D 游戏场景并不在这里,而在 `root.child[1]` 那棵 `Node3D` 子树下。这个发现非常关键,因为它解释了为什么一开始盯着 `current_scene` 总像是在看“对的场景却没有对的对象”。 接下来对 3D 场景做分层枚举,最终识别出两个关键 `Area3D`,并读取到了它们的全局坐标: ```text 低处 Area3D: (-12.845476, 5.822042, -15.349906) 高处 Area3D: (-14.139519, 11.816434, -3.958621) ``` 这组坐标与题面现象完全吻合:低处是可正常触碰的黄色示例块,高处是房顶绿色计分块。到这里,场景问题终于从“我知道有一个绿色方块”变成了“我知道它具体是哪个对象、在什么位置、玩家又具体是哪一个物理节点”。再顺着 `Marker3D -> Node3D -> VehicleBody3D` 这条链继续摸,最终定位到真实玩家物理节点 `VehicleBody3D id = 37329307145`。 为了让这一步更容易阅读,我把确认下来的场景结构单独整理成一张图。  这张图对应的解题意义非常直接。`root.child[0]` 是 UI 容器,也就是一开始能直接看到 `Token` 和 `Part0` 示例 flag 的那棵树;`root.child[1]` 才是真正的 3D 游戏主场景。继续往下看,绿色计分块是高处的 `Area3D`,而真正需要传送的对象不是相机也不是普通显示节点,而是 `Marker3D -> Node3D -> VehicleBody3D` 这条链末端的车体物理节点。也正因为这个层级关系被理清,后续的稳定传送、绿块触发和 `xor_enc` 恢复才真正形成了闭环。 ## 1.9 真实传送、实机截图与 `xor_enc` 的闭环恢复 拿到玩家物理节点后,Frida 改坐标传送这条路线才真正从“建议”变成“已执行”。我写的传送脚本直接取 `VehicleBody3D` 的实例指针,并连续 60 个 tick 调用 `Node3D.set_global_position`,确保玩家不是一闪而过,而是在目标区附近稳定停留多个 physics tick。核心代码如下: ```javascript const VEHICLE_ID = 37329307145; const TARGET_POS = { x: -14.139518737792969, y: 12.316433906555176, z: -3.958621025085449 }; for (let applied = 1; applied <= APPLY_TICKS; applied += 1) { ptrcall(mbSetGlobalPosition, vehiclePtr, setArgs, ptr(0)); ptrcall(mbGetGlobalPosition, vehiclePtr, ptr(0), getRet); } ``` 同步读回的位置表明写位是稳定生效的,位置持续命中高处 `Area3D` 附近。随后在真机上肉眼观察到首次真实高处分支结果,截图显示左上角 token 为 `ca567fe5`,右上角结果为 `flag{sec2026_PART1_784B50482235734B}`。  但矛盾仍然存在:此前直接把 ASCII `ca567fe5` 喂给 native 算出来的是 `197E667F44504649`,与真实截图 `784B50482235734B` 并不相同。这说明高处逻辑不是“token 原样进 native”,中间还隔着一层脚本预处理。于是我继续转向高处 `Area3D` 自身的方法列表,枚举其方法后找到了明显不是引擎内建的 `xor_enc`。进一步通过 Java bridge 直接调用该方法: ```javascript const AREA_ID = 42110813978; const ret = call.call(Callable, AREA_ID, "xor_enc", oneToken); ``` 对 `xor_enc("ca567fe5")` 的真实返回结果是一个 `byte[]`,内容为 `0254030151035037`。把这 8 个字节重新送入前面已经恢复好的 native 流加密核心,得到的正好就是 `784B50482235734B`,与实机截图完全一致。 这一刻才是真正意义上的闭环点。此前我们已经有 native 算法,也已经有实机结果,但两者中间始终隔着一个无法解释的差值;直到 `xor_enc` 被抓出来,这个缝才被完全补上。这样一来,完整正向链条第一次闭环: ```text token "ca567fe5" -> GDScript xor_enc -> 0254030151035037 -> native 变种 ChaCha20 -> 784B50482235734B -> flag{sec2026_PART1_784B50482235734B} ``` 为了把 `xor_enc` 从“能调通”提升到“能重写”,我又对多组可控输入批量调用 `xor_enc`,观察输出规律。最终恢复出的精确公式是:若 token 的 8 个 ASCII 字节为 `b0..b7`,则预处理结果 `p0..p7` 满足 `p0=b0^b1`,`p1=b1^b2`,一直到 `p6=b6^b7`,最后 `p7=b7^p0`。将 `ca567fe5` 代入,得到的正是 `02 54 03 01 51 03 50 37`。这一步意味着算法不再依赖脚本在线调用,可以完整重写。 最后,为了确认这不是只对一组 token 成立的巧合,再次重启 App,新的屏幕 token 为 `f45e8514`。本地根据已恢复的 `xor_enc + 变种 ChaCha20` 链条预测其后缀为 `281E03147E32261A`,随后用相同传送路径在真机上观察到右上角结果与该值完全一致。至此,算法恢复从“单样本闭环”提升为“至少双样本实机复核通过”。  ## 1.10 完整逆向执行总流程图  # 2. 算法逻辑分析 ## 2.1 PART1 的完整正向链条 经过前述动态验证,PART1 的 flag 生成逻辑已经可以用一条稳定链条描述:先读取屏幕左上角的 8 字符 token;在 GDScript 里调用 `xor_enc`,把它变成 8 字节的预处理结果;再把这 8 字节送入 native 层的 `Process.input` 核心;native 使用一套“ChaCha20 轮函数不变、常量被魔改”的流密码生成 keystream,并与输入逐字节 XOR;最后把得到的 8 个输出字节用 `%02X` 转成 16 个大写十六进制字符,再拼接进 `flag{sec2026_PART1_<HEX16>}`。 正向流程图如下:  | 阶段 | 输入 | 处理 | 输出 | |---|---|---|---| | 1 | 屏幕 token | 读取 8 字符 ASCII | `b0..b7` | | 2 | `b0..b7` | GDScript `xor_enc` | `p0..p7` | | 3 | `p0..p7` | 变种 ChaCha20 keystream XOR | 8 字节密文 | | 4 | 8 字节密文 | `%02X` 大写 hex 编码 | 16 位后缀 | | 5 | 16 位后缀 | 固定格式拼接 | 完整 flag | ## 2.2 `xor_enc` 的精确形式 设 token 的 8 个 ASCII 字节为 `b0 b1 b2 b3 b4 b5 b6 b7`,则 `xor_enc` 输出的 8 字节 `p0..p7` 满足下式: ```text p0 = b0 ^ b1 p1 = b1 ^ b2 p2 = b2 ^ b3 p3 = b3 ^ b4 p4 = b4 ^ b5 p5 = b5 ^ b6 p6 = b6 ^ b7 p7 = b7 ^ p0 ``` 这个结构表面上只是“相邻字节链式异或”,但最后一项不是简单的 `b7 ^ b0`,而是 `b7 ^ p0`,也就是 `b7 ^ (b0 ^ b1)`。这一点如果写错,逆向恢复时会整体偏掉。用真实样本 `ca567fe5` 代入,可以得到: ```text b = 63 61 35 36 37 66 65 35 p = 02 54 03 01 51 03 50 37 ``` 这与通过 Java bridge 直接调用 `xor_enc("ca567fe5")` 得到的 `0254030151035037` 完全相同,因此可以确认这套公式不是“拟合样本的猜测”,而是脚本层真实执行的逻辑。 `xor_enc` 算法流程图如下:  | 输出字节 | 定义 | |---|---| | `p0` | `b0 ^ b1` | | `p1` | `b1 ^ b2` | | `p2` | `b2 ^ b3` | | `p3` | `b3 ^ b4` | | `p4` | `b4 ^ b5` | | `p5` | `b5 ^ b6` | | `p6` | `b6 ^ b7` | | `p7` | `b7 ^ p0` | ## 2.3 native 变种 ChaCha20 的关键参数 native 层流密码的轮函数仍然是标准 ChaCha quarter round 结构,也就是 `add / xor / rol` 的经典组合,旋转常数仍为 `16 / 12 / 8 / 7`。真正被修改的不是轮函数本身,而是 state 的前 4 个常量 word。 实际使用的 state 头四项为: ```text 0x61707866 0x3320646f 0x79622d31 0x6b206573 ``` 其余参数如下表: | 参数 | 运行时确认值 | 说明 | |---|---|---| | key | `Th1s ls n0t a rea1 key!!@sec2026` | 32 字节 | | nonce | `012345678901` | 12 字节 | | counter | `0` | 初始计数器 | | rounds | `20` | 即 10 轮列轮 + 对角轮 | | output | 大写 hex | 由 `%02X` 逐字节拼接 | 虽然本题只处理 8 字节输入,实际只会用到首个 64 字节 keystream block 的前 8 个字节,但从实现角度仍然应该完整生成整个 block,再截取前 8 个字节参与 XOR。 从实现角度看,正向过程可以概括为: ```text suffix_bytes = xor_enc(token) keystream = modified_chacha20_block(key, nonce, counter=0) out[i] = suffix_bytes[i] ^ keystream[i] suffix_hex = out.hex().upper() flag = "flag{sec2026_PART1_" + suffix_hex + "}" ``` 与真机对照: | token | `xor_enc` 输出 | 最终 PART1 后缀 | |---|---|---| | `ca567fe5` | `0254030151035037` | `784B50482235734B` | | `f45e8514` | `5201505D0D040566` | `281E03147E32261A` | native 部分本身也可以再抽象成一张独立数据流图:  ## 2.4 逆算法分析 逆算法成立的原因在于 native 部分本质上还是流密码,解密和加密是同一件事:只要再次用相同 keystream 做一次 XOR,就能把密文还原回 `xor_enc` 的输出 `p0..p7`。因此逆向过程的第一步非常直接:先把 flag 后缀的 16 个 hex 字符解码为 8 字节,再与同一套变种 ChaCha20 keystream 做 XOR,恢复出 `p0..p7`。 真正需要推导的是如何从 `p0..p7` 还原 `b0..b7`。因为 `xor_enc` 的结构是链式关系,所以可以先解出 `b0`,再顺推整个 token。推导后的恢复公式如下: ```text b0 = p1 ^ p2 ^ p3 ^ p4 ^ p5 ^ p6 ^ p7 b1 = b0 ^ p0 b2 = b1 ^ p1 b3 = b2 ^ p2 b4 = b3 ^ p3 b5 = b4 ^ p4 b6 = b5 ^ p5 b7 = b6 ^ p6 ``` 将真实样本 `784B50482235734B` 代入,先经 native 逆向 XOR 得到 `0254030151035037`,再按上式恢复,最终得到的正是 `ca567fe5`。这说明脚本预处理虽然不是简单恒等映射,但它本身是完全可逆的,没有额外丢失信息。这也是本题可以要求“根据 flag 反推 token”的根本原因。 逆向流程图如下:  对应关系表如下: | 恢复步骤 | 公式 | |---|---| | 先恢复 `b0` | `b0 = p1 ^ p2 ^ p3 ^ p4 ^ p5 ^ p6 ^ p7` | | 再恢复 `b1` | `b1 = b0 ^ p0` | | 再恢复 `b2` | `b2 = b1 ^ p1` | | 再恢复 `b3` | `b3 = b2 ^ p2` | | 再恢复 `b4` | `b4 = b3 ^ p3` | | 再恢复 `b5` | `b5 = b4 ^ p4` | | 再恢复 `b6` | `b6 = b5 ^ p5` | | 最后恢复 `b7` | `b7 = b6 ^ p6` | ## 2.5 代码实现与可复现性 就本题而言,可复现性的核心不在于“能把一个样例算出来”,而在于同一实现同时满足三件事: 其一,能把 `ca567fe5` 算成 `784B50482235734B`; 其二,能把 `784B50482235734B` 逆回 `ca567fe5`; 其三,能把第二个独立样本 `f45e8514` 算成 `281E03147E32261A`。 当前实现和实机截图已经同时满足这三个条件,因此可以认为算法恢复已经达到可验证标准。 # 3. 安全机制解析 防护链总图:  | 层级 | 机制 | 直接目标 | 已验证证据 | 实际效果 | |---|---|---|---|---| | 资源层 | `assets.sparsepck` 加密 + `FileAccessEncrypted` | 阻止直接恢复脚本 | 工具直接报错,脚本文件头存在加密包装 | 资源线无法作为起手主线 | | native 装载层 | `memfd_create` + `mmap(PROT_EXEC)` + `BR X14` | 隐藏真实 `.text` | 入口行为与运行时 dump 一致 | 必须进入运行态 | | 引擎桥接层 | GDExtension / `ClassDB` 注册 | 避免显眼导出接口 | `Process.input(PackedByteArray)->String` 被运行时锁定 | 需要顺着 Godot 注册链找入口 | | 逻辑拆分层 | `xor_enc` 在脚本,高阶加密在 native | 切断单线分析 | `high_area` 有 `xor_enc`,native 只见 Hex 输出 | 必须拼接两段逻辑 | | 交互层 | 黄块示例、绿块高处不可达 | 延迟真实样本获取 | 黄色只出示例,绿色在房顶 | 必须做对象探测与传送 | ## 3.1 自解密壳:用运行时映射取代静态 `.text` 本题最明显的安全机制,是 `libsec2026.so` 自身的多层运行时装载。入口不是直接进入业务代码,而是先读取 `auxv`、解密中间载荷、调用 `memfd_create` 和 `mmap(PROT_EXEC)`,再通过寄存器跳转进入新的可执行映射。这样做的意义很直接:让真正的代码直到运行时才出现于内存中,静态查看 ELF 时只能看到极少数 stub。 这一层之所以能被明确识别,不是依赖某一个孤立现象,而是有一整条连贯证据链。首先,原始 IDA 视图里 `Unexplored` 和 `Data` 占比异常高,几乎看不到正常的函数区;其次,入口路径上确实出现了 `memfd_create`、`mmap(PROT_EXEC)` 与 `BR X14` 这种典型的运行时装载动作;最后,顺着这条路径实际 dump 出了新的 `r-x` 映射,并在回填后恢复出了数千个函数。三步连起来,才把“它像壳”推进成“它就是运行时自解密 / 自装载”。 从机制层面看,这套壳不一定是为了绝对隐藏代码,而是为了迫使分析者放弃“只扫静态字符串”的打法,转而进入运行时。它提高的不是某个算法本身的复杂度,而是把分析门槛前移到了“先理解装载链”。  ## 3.2 资源加密:PCK 加密与 `FileAccessEncrypted` 双重阻断 第二层安全机制在资源侧。`assets.sparsepck` 的目录本身经过加密,导致常规 Godot 恢复工具在最开始就无法正常枚举资源树。更进一步,`token.gdc`、`Trigger/trigger.gdc`、`label2.gdc`、`sec2026.gdextension` 又不是普通明文文件,而是带有 Godot `FileAccessEncrypted` 风格包装。这样一来,资源线不是单点被卡,而是同时被“目录不可枚举”和“单文件不可直接反编”双重阻断。 这层防护最有效的地方,在于它不是为了彻底阻止后续恢复,而是为了打乱分析顺序。理论上讲,只要拿到正确 key,后续还是能回到脚本层;但在比赛语境下,只要它能让分析者在前期无法把脚本线直接打穿,native 线就会被迫上升为主线。 相关阻断点可以整理成表: | 资源对象 | 保护方式 | 已验证现象 | 对分析的影响 | |---|---|---|---| | `assets.sparsepck` | PCK 目录加密 | 工具直接报错,无法列目录 | 不能靠解包工具起步 | | `token.gdc` | `FileAccessEncrypted` 包装 | 文件头不呈现普通 GDSC 明文 | 无法直接恢复 token 逻辑 | | `trigger.gdc` | `FileAccessEncrypted` 包装 | 不能直接反编 trigger 脚本 | 无法起手就看绿块逻辑 | | `label2.gdc` | `FileAccessEncrypted` 包装 | UI 文字逻辑也被遮住 | 无法单靠脚本还原显示链 | | `sec2026.gdextension` | `FileAccessEncrypted` 包装 | 扩展配置同样被包裹 | 无法直接从配置读取入口 | ## 3.3 框架级混淆:把核心逻辑藏进 GDExtension 和对象系统 第三层安全机制来自 Godot 框架本身。题目没有把核心逻辑做成显眼的导出函数,而是注册成一个 `Process` 类的 `input` 方法,参数类型是 `PackedByteArray`,返回类型是 `String`。这让关键代码看上去更像普通 Godot 扩展,而不是传统意义上的“算法库”。如果不顺着 GDExtension 注册链去追,分析者很容易在 `libsec2026.so` 里找不到一个像样的“主入口”。 更关键的是,题目没有把整条链都放在 native。绿色方块对应的脚本预处理 `xor_enc` 被藏在高处 `Area3D` 的对象方法里,而 native 则只负责后半段的变种 ChaCha20 与大写 hex 输出。这样做会制造一个典型错觉:native 分析看起来已经几乎闭环,但真实触发结果却始终对不上,迫使分析者回头补脚本层对象调用。 这一层混淆可以拆成下表: | 机制点 | 题目做法 | 对分析者的误导 | |---|---|---| | 入口封装 | `Process.input(PackedByteArray)->String` 通过 GDExtension 注册 | 很难靠导出符号直接看出主入口 | | 参数包装 | `Variant` / `PackedByteArray` / `String` 桥接 | 算法核心被包在 Godot 类型系统里 | | 逻辑拆分 | `xor_enc` 在脚本,变种 ChaCha20 在 native | 单看任一侧都差最后一块拼图 | | 诱饵字符串 | `Th1s ls n0t a rea1 key!!@sec2026` | 容易误把算法 key 当资源解密 key | 这层拆分的总体效果,可以用下面这张图概括:  值得特别强调的是那串字符串 `Th1s ls n0t a rea1 key!!@sec2026`。它既是一个很强的诱饵,又不是纯粹的假线索。对于 PCK/.gdc 资源线来说,它不是解包 key;但对 PART1 native 算法来说,它又确实是 32 字节流密码 key。这种“半真半假”的线索设计非常容易让分析者在不同阶段把两条线混为一谈。 ## 3.4 交互层限制:用场景布局和示例方块干扰主线 最后一层安全机制是交互设计。黄色方块可以正常触发,但它只显示 `flag{sec2026_PART0_example}`,并且不会调用 `Process.input`。而真正计分的绿色方块被放在房顶高处,正常玩法下难以到达,这使得“直接触发一次看结果”并不容易。 这层设计的关键,不在于绝对阻止触发,而在于提高“拿到第一组真实样本”的成本。分析者必须先理解场景树,识别哪个 `Area3D` 是黄色、哪个是绿色,再找到真正参与物理的 `VehicleBody3D`,最后稳定写位到高处坐标附近,才能拿到第一组真实 Part1 结果。也就是说,题目把“算法恢复”和“运行时对象操控”故意耦合到了一起。 ## 3.5 游戏解包 key 与资源保护链 在解完题目之后,我还想复盘一下当时的备选线路——解资源包,在前期,它一上来就被两层保护挡住了。第一层是 `assets.sparsepck` 的目录加密,第二层是 `token.gdc`、`trigger.gdc`、`label2.gdc` 以及 `sec2026.gdextension` 这些单文件本身又带了一层 Godot 的 `FileAccessEncrypted` 包装。 这一部分的入口来自 `libgodot_android.so`,而不是 `libsec2026.so`。我通过 Godot 4.5.1 官方源码确认,标准引擎在读取加密 PCK 目录和加密打包文件时,都会把 `script_encryption_key` 复制到一个 32 字节 `Vector<uint8_t>` 里,再传给 `FileAccessEncrypted::open_and_parse()`。因此,真正要找的不是“题目里哪里看起来像 key 的字符串”,而是运行时 `PackedSourcePCK::try_open_pack()` 实际从哪里读出这 32 个字节。 为了把这条链落到真实地址,我先利用 `libgodot_android.so` 里仍然保留的 RTTI 名称定位相关类。二进制里可以直接看到 `15PackedSourcePCK`、`21PackedSourceDirectory` 和 `10PackSource` 这些类型名。结合 `.rela.dyn` 中的重定位关系,可以恢复出 `PackedSourcePCK` 的 vtable,其中一条关键虚函数入口是 `0x3804c2c`。继续对这一段做反汇编之后,可以明显看出它就是 `PackedSourcePCK::try_open_pack()`:前面在读 `GDPC` 魔数、版本和 `pack_flags`,后面在 `enc_directory` 分支里实例化 `FileAccessEncrypted`,分配 32 字节 key vector,再把某个全局地址里的 32 字节逐字节复制进去。 关键汇编如下。这一段已经足够说明“解包 key 的运行时来源”: ```text 0x3804f28 mov w1, #0x20 0x3804f34 bl 0x107cba4 0x3804f40 adrp x26, #0x4004000 0x3804f48 ldr x26, [x26, #0x488] 0x3804f58 ldrb w28, [x26, x27] 0x3804f68 strb w28, [x8, x27] 0x3804fb0 bl 0x3801410 ``` 这段代码的语义并不复杂。`0x107cba4` 把 key vector 扩成 `0x20` 字节,也就是 32 字节。随后 `adrp/ldr` 组合从 `module + 0x4004488` 这个槽位取出一个指针,`ldrb/strb` 循环再把该指针指向的 32 个字节逐个拷贝到 vector 中。最后在 `0x3804fb0` 调用后续解密函数。换句话说,只要把这个槽位和它指向的 32 字节在进程里读出来,解包 key 就已经被拿到了。 我随后使用frida直接 attach 这个进程读取这两个地址。当前一次实测时,`libgodot_android.so` 的模块基址是 `0x7097016000`。对应的关键地址关系如下: | 项目 | 值 | |---|---| | 模块基址 | `0x7097016000` | | 指针槽位 | `base + 0x4004488 = 0x709b01a488` | | 槽位内容 | `0x709b024df0` | | key 数据地址 | `base + 0x400edf0 = 0x709b024df0` | 在该地址处直接读出的前 32 字节就是: ```text CE 4D F8 75 3B 59 A5 A3 9A DE 58 AC 07 EF 94 7A 3D A3 9F 2A F7 5E 32 84 D5 12 17 C0 4D 49 A0 61 ``` 写成连续 64 个十六进制字符后即为: ```text CE4DF8753B59A5A39ADE58AC07EF947A3DA39F2AF75E3284D51217C04D49A061 ``` 因此,游戏解包 key 它不是 `Th1s ls n0t a rea1 key!!@sec2026`,后者是 PART1 native 变种 ChaCha20 的算法 key;资源线真正参与 `PackedSourcePCK::try_open_pack()` 拷贝的 32 字节 key,是上面这组 `CE4D...A061`。这一定性来自引擎真实解包路径上的静态汇编与活进程直接读内存两条证据链,因而比单纯从字符串或资源头部猜测要可靠得多。 拿到这组 key 之后,我马上做了离线验证。验证方法是完全按 Godot 官方 `FileAccessEncrypted::open_and_parse()` 的规则,使用 `AES-256-CFB` 去解 `token.gdc`、`trigger.gdc`、`label2.gdc`、`sec2026.gdextension` 以及 `assets.sparsepck` 目录段。结果并不是“直接还原出最终明文”,但也绝不是随机噪声。最典型的例子是 `token.gdc`:AES 解出来的前 16 字节是 `47 45 51 40 61 05 06 07 ...`,它与真实 `GDSC 65 00 00 00 ...` 之间,前 16 个字节呈现出高度规则的差值。`assets.sparsepck` 的目录段也出现了类似现象,再额外试探一次按字节下标做 XOR 之后,目录头部已经能出现 `car_select/c` 这样的可读路径片段。 这一现象首先能稳定说明两件事:其一,`CE4D...A061` 这 32 字节确实是资源线真正参与 `PackedSourcePCK::try_open_pack()` 的 AES unpack key;其二,仅靠“拿 APK loose 文件做标准 `FileAccessEncrypted` 离线解密”还不足以直接得到最终可用明文。至于这是否一定意味着“标准 AES 之后还额外叠了一层固定自定义扰动”,在当前证据下仍需保守表述,因为更合理的另一种解释是:此前参与比对的 APK loose `.gdc` 与运行时真实命中的 `assets.sparsepck` 内 pack 条目并不是同一份文件来源。 因此,资源层可以确定的结论应收敛为:题目确实使用了 Godot 官方资源加密链,且资源 key 已被定位。鉴于本题的核心诉求已经在 Native 与脚本对象调用的闭环中完全解决,且该资源层分支并不阻碍 `Part1` 正向算法与逆算法的恢复;出于比赛时间成本与目标收益的考量,不再继续投入与主线无关的离线恢复消耗。 从防护设计角度看,这一段资源保护依然很有意思。题目至少利用了 Godot 默认的 `script_encryption_key` 读取链,并把真正的资源来源压进 `assets.sparsepck` 与引擎 pack 读取路径中。这种设计已经足以让分析者即便找到了 AES key,也仍然需要先核清“运行时到底读的是哪一份资源”。 ## 4. 附:token生成逻辑分析 因为解完题时间还早,所以我对token的生成逻辑也进行了分析,token的分析是直接解析 `token.gdc` 确定的,而不是继续停留在运行时行为推测。主线算法已经恢复完成之后,我回头重新检查左上角 token 的来源,发现 `live_gdsc_dump/token.gdc` 并不是不可解释的中间态,而是一个标准的 Godot 4 `GDScriptTokenizerBuffer`。它的文件头是 `GDSC`,版本字段是 `101`,正文经过 `zstd` 压缩,这一结构与github上官方拉下来的4.5.1的源码的读取逻辑完全一致。也就是说,这个文件虽然不是源码文本,但它已经是“可以按 Godot 官方格式直接解析的脚本 token 缓冲区”。 确认这一点之后,后面的分析路线就很清晰了。按 Godot 源码里的顺序,先解压正文,再读取标识符表、常量表、行号映射和 token 流,就能把脚本骨架重新拼出来。最小化的验证代码如下,它的作用只是证明这个文件确实符合标准 tokenizer buffer 结构: ```python import struct import zstandard as zstd data = open("token.gdc", "rb").read() version = struct.unpack_from("<I", data, 4)[0] raw_size = struct.unpack_from("<I", data, 8)[0] body = data[12:] dec = zstd.ZstdDecompressor().decompress(body, max_output_size=raw_size) identifier_count, constant_count, token_line_count, token_count = struct.unpack_from("<IIII", dec, 0) ``` 真正有决定意义的是解压后的内容。标识符表里直接出现了 `TOKEN_LEN`、`CHARS`、`rng`、`RandomNumberGenerator`、`generate_token`、`randi_range`、`_ready`、`randomize`、`text` 这些名字;常量表里则直接给出了 `8`、`"0123456789abcdef"`、`""`、`0`、`1` 和 `"Token: "`。只要把这两组信息和 token 流拼起来,`token.gd` 的核心逻辑就已经不是猜测,而是可以稳定还原成源码骨架。恢复出来的脚本如下: ```gdscript extends Label const TOKEN_LEN := 8 const CHARS := "0123456789abcdef" var rng := RandomNumberGenerator.new() func generate_token(len: int) -> String: var s := "" for i in len: var idx := rng.randi_range(0, CHARS.length() - 1) s += CHARS[idx] return s var score = 0 func _ready() -> void: rng.randomize() text = "Token: " + generate_token(TOKEN_LEN) print(text) ``` 这份还原结果里最重要的,不只是 `generate_token()` 这个函数本身,而是脚本所属对象。它的基类是 `Label`,这说明左上角 token 的生成与写回,并不在某个抽象的 `game_root` 管理器节点里完成,而是就在显示 token 的标签脚本里完成。此前根据运行时方法列表做过一版偏向 `game_root` 的判断,但在脚本本体已经被恢复出来之后,这个旧表述就需要让位给更直接的证据:`token.gd` 是一个 `extends Label` 的脚本,真正负责 `text = "Token: " + ...` 的就是这个标签自己。脚本里还出现了一个 `score = 0`,但它没有参与 token 生成,只是同一脚本中的额外状态。 把上面这段脚本翻译成算法语言之后,token 的生成逻辑就非常明确了。程序在 `_ready()` 回调里先对 `rng` 调用一次 `randomize()`,也就是先给 `RandomNumberGenerator` 做运行时随机播种。然后它调用 `generate_token(TOKEN_LEN)`,而 `TOKEN_LEN` 的值固定是 `8`。进入 `generate_token(len)` 之后,函数先创建一个空字符串 `s`,随后执行一个长度为 `len` 的循环。在每一轮循环里,脚本都调用一次 `rng.randi_range(0, CHARS.length() - 1)`。由于 `CHARS` 固定等于 `"0123456789abcdef"`,长度恰好是 `16`,所以这一句的实际含义就是在区间 `[0, 15]` 中随机取一个整数。取到下标 `idx` 之后,再把 `CHARS[idx]` 对应的那个字符追加到 `s` 末尾。循环重复 8 次之后返回 `s`,于是得到一个长度固定为 8、字符集限制在小写十六进制范围内的 token。最后,`_ready()` 把这个返回值和固定前缀 `"Token: "` 拼起来写进 `Label.text`,于是屏幕左上角显示出形如 `Token: ca567fe5` 的结果。 从这个角度看,题目的 token 并不是“先产生一个 32 位随机整数,再格式化成 8 位十六进制字符串”,而是更直接的“在 `0123456789abcdef` 这个字符池里独立随机抽取 8 次,每次取 1 个字符,再按顺序拼接”。这个差异在实现层面非常重要,因为它决定了 token 的每一位都是独立从字符池中选出来的,而不是某个 32 位数值在显示层的格式化结果。也正因为这一点被还原出来,之前围绕“字符池逐位随机”和“32 位整数格式化输出”之间的分歧,现在可以正式结束。 如果只把它压缩成等价实现,整个逻辑实际上可以概括成一句话:先调用 `randomize()` 初始化随机数生成器,然后重复 8 次执行 `idx = randi_range(0, 15)`,每次从 `"0123456789abcdef"` 中取出第 `idx` 个字符并追加到结果字符串,最后把 `"Token: "` 与该结果拼接后写入标签文本。这已经足够构成一个完整、可复原、可解释的 token 生成算法。 这里最后还要补一个边界说明。现在已经能够完整恢复的是“题目如何生成 token 的算法”,但这并不等于“离线环境下一定能复现某一次历史运行时看到的同一个 token”。原因在于 `randomize()` 会给 `RandomNumberGenerator` 设置当次运行的随机种子,如果没有拿到那次启动时使用的 seed,就无法在离线环境里重演出完全相同的 `ca567fe5` 或 `f45e8514`。不过这一点并不影响本题的主评分目标,因为比赛要求是“根据屏幕上已经出现的 token 计算 Part1 flag”,而不是“预测下次启动会出现什么 token”。就分析报告而言,做到这里已经足够把 token 生成逻辑作为一个完整、确定、可复原的附加结论写进最终提交材料。
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2026-4-25 15:05 被mxystery编辑 ,原因:
#逆向分析
收藏
・
13
点赞
・
15
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
零_721409
期待更多优质内容的分享,论坛有你更精彩!
2026-5-26 18:37
mb_utmyqsij
感谢你分享这么好的资源!
2026-4-25 01:54
andy张刘
为你点赞!
2026-4-24 22:33
chiikawa_gui
感谢你的贡献,论坛因你而更加精彩!
2026-4-22 20:19
iCookiie
感谢你的贡献,论坛因你而更加精彩!
2026-4-22 09:10
s1nec-1o
你的帖子非常有用,感谢分享!
2026-4-21 22:44
mb_jwcpnqgs
非常支持你的观点!
2026-4-21 19:41
fyrlove
谢谢你的细致分析,受益匪浅!
2026-4-21 15:32
cr_lgdx
你的帖子非常有用,感谢分享!
2026-4-21 15:03
下雨天sana
为你点赞!
2026-4-21 11:20
mb_iesvmzqc
你的帖子非常有用,感谢分享!
2026-4-21 10:05
哈哥
期待更多优质内容的分享,论坛有你更精彩!
2026-4-21 09:41
我的小拇指啊
这个讨论对我很有帮助,谢谢!
2026-4-21 08:08
Algunas_
谢谢你的细致分析,受益匪浅!
2026-4-21 08:06
asd
非常支持你的观点!
2026-4-21 00:06
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
4
)
mb_zcfhleoj
雪 币:
148
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
42
粉丝
0
关注
私信
mb_zcfhleoj
2
楼
感谢分享
2026-4-22 16:31
0
需要渗透高手
雪 币:
114
活跃值:
(280)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
42
粉丝
1
关注
私信
需要渗透高手
3
楼
有力气
2026-4-22 17:25
0
318
雪 币:
220
活跃值:
(271)
能力值:
( LV2,RANK:10 )
在线值:
发帖
1
回帖
6
粉丝
2
关注
私信
318
4
楼
但还是感觉走资源解密路线快一点点。。
2026-4-22 23:00
0
mb_vrkpxsoe
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
15
粉丝
0
关注
私信
mb_vrkpxsoe
5
楼
6
2026-5-25 10:43
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
mxystery
2
5
发帖
5
回帖
90
RANK
关注
私信
他的文章
[原创]从调用级并发到 Guest Thread Runtime:unidbg 单后端线程语义的重建
2229
[原创]从时间片轮转到调用级并发:unidbg 单后端多线程架构重构
37166
[原创]给 unidbg 装上原生时间片:如何让模拟器真正跑起多线程
2944
[原创]2026腾讯游戏安全竞赛决赛安卓客户端安全分析
8897
[原创]2026腾讯游戏安全初赛 Android 客户端分析
33429
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部