前言:写的比较流水账。看了其它佬的文章,发现比赛还是漏掉了n多得分点… .gdc 文件头格式仍是 MD5(16) | plain_len(8) | IV(16) | ciphertext和初赛的方法一样,依旧是hook open_and_parse_password,捕获到密钥仍是 ce4df8753b59a5a39ade58ac07ef947a3da39f2af75e3284d51217c04d49a061但这回没改加解密,解密模式是标准 AES-CFB直接gdre解出来的gd长这个样子。像是魔改过了。这个关键字分布感觉是魔改了token序。 打了个标准版的4.5.1-stable包,想直接diff看看差别,但…… 中间还崩溃了几次。最后直到比赛结束也没diff完直接人工diff。对照着源码分析,看看是改了哪里。一开始想在scan里去找,但scan里的映射有点怪,有几个token被映射到了同一个id。猜测可能是故意堵了这条路?于是打算从_binary_to_token慢慢扒parse。根据常量字符串Identifier index out of bounds.定位到_binary_to_token偏移是0x147CF48。扔到在线文本比对工具里。 左边是题目,右边是demo。可以看到明显有不同。题目这版魔改了解析token时的映射顺序。对照着源码分析,这里能确定的是7 = NAN = official 94;11/12 是 ANNOTATION/IDENTIFIER 这对;13/50 是LITERAL/ERROR 这对。顺着找其他parse:is_identifier在0x1474F58,能确定4,5,6,7,12,60,61这一组对应IDENT / MATCH / WHEN / PI / TAU / INF / NANparse_variable在0x144DE30,能确认 12 = IDENT = official 02;95 = : = official 84;38 = = = official 28parse_signal在0x144AB4C,能确认12 = IDENT = official 02;88 = ( = official 77;89 = ) = official 78;90 = , = official 79parse_enum在0x144D16C,能确认12 = IDENT = official 02;86 = { = official 75;87 = } = official 76;38 = = = official 28parse_function_signature在0x1453670,能确认94 = ... = official 83;90 = , = official 79;89 = ) = official 78;97 = -> = official 86;lambda 分支再次给出95 = : = official 84parse_function在0x144BB10,能确认12 = IDENT = official 02;88 = ( = official 77parse_assert在0x1458C5C,能确认88 = ( = official 77;90 = , = official 79;89 = ) = official 78parse_for在0x1456EC0,能确认12 = IDENT = official 02;95 = : = official 84;72 = in = official 61parse_if在0x14566E0,能确认52 = elif = official 41;53 = else = official 42;95 = : = official 84parse_match在0x145A044,能确认61 = when = official 50;95 = : = official 84;1 = NEWLINE = official 88parse_subscript在0x1461064,能确认85 = ] = official 74parse_call在0x14616E8,能确认92 = . = official 81;88 = ( = official 77;89 = ) = official 78parse_type_test在0x146459C,能确认22 = not = official 12...最后归纳一下规律,魔改token到官方token的变换:写脚本,初赛解gdc脚本改一下就能用,然后再按这个规律置换一下。然后就能用gdre正常反编译了。`可以看到trigger2的算法全在gd层。 变量名做了混淆。直接扔给大模型来读。也不需要啥额外提示词。有一个值得注意的点是碰撞放在了native层。 然后就是把方块传送过来。此处方法和初赛一样,依旧是先hook到libsec2026.so的extension_init获取到关键api,然后classdb_get_method_bind +ptrcall动态调用godot原生方法递归遍历整个场景树,定位到小车和trigger2的位置后,set_global_position把方块传送到小车的位置。 用到的脚本:frida_dump_scene_tree.js、run_dump_scene_tree.py dump场景树,scene_tree_dump.jsondump的结果。run_trigger2_move.js、run_trigger2_move.py 传送trigger2token生成flag算法:token2flag2.cflag生成token算法:flag2token2.c这里说一下flag2token2.c的思路。先看正向流程。脚本先把输入的 8 位十六进制字符串转成 4 个字节,然后把这 4 个字节拆成前后两个 2 字节分组,分别记成 lo 和 hi。程序把 "Sec2026_Godot" 作为轮密钥,一共跑 8 轮。每一轮先对右半块 hi 跑一遍轮函数,轮函数里先和密钥按轮次偏移异或,再乘 7 加上轮号,最后做一次左旋 3 位。轮函数输出以后,再和左半块 lo 做异或得到新的右半块 nr。然后执行 Feistel 交换,也就是 lo = hi,hi = nr。8 轮结束以后,把最终的 lo || hi 拼起来作为flag。flag2token2.c 按完全相反的顺序逐步还原。把这 输入的位十六进制串解析成 4 个字节,再拆成当前的 lo 和 hi。这两个值对应的是加密 8 轮结束以后的左右半块。接下来程序从第 7 轮倒着还原到第 0 轮。因为正向每一轮最后做的是 lo = hi_old,hi = lo_old ^ F(hi_old, round),所以逆的时候先把上一轮的右半块直接恢复成当前的左半块,也就是 prev_hi = lo。然后重新对 prev_hi 跑同一轮的轮函数,得到当时的 F(prev_hi, round)。有了这个值以后,再用当前右半块 hi 去异或它,就能把上一轮的左半块恢复出来,也就是 prev_lo = hi ^
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290891.htm
[原创]2026腾讯游戏安全竞赛-决赛-安卓
142 浏览
6 回复
tql
太强了,我一开始也想用trace还原cff流程和最后VM的污点分析了,碍于对安卓工具不太了解不知道unidbg可以追
盘子蛋糕
太强了,我一开始也想用trace还原cff流程和最后VM的污点分析了,碍于对安卓工具不太了解不知道unidbg可以追[em_006]
这题的vmp直接trace分析有点过于简单了,感觉出题人似乎并没考虑在这种解法上设置障碍?虽然能这么做出来,但不知道有没有分:(
太强了,我一开始也想用trace还原cff流程和最后VM的污点分析了,碍于对安卓工具不太了解不知道unidbg可以追[em_006]
这题的vmp直接trace分析有点过于简单了,感觉出题人似乎并没考虑在这种解法上设置障碍?虽然能这么做出来,但不知道有没有分:(
我觉得肯定是有分的,比ai撕开更体现思路能力吧,我就吃了对工具不太了解的分,没时间去学了,这要放Windows上我用我自己工具就追出来了
????????
厉害给你点个赞,表情还发不出去呢。