首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
4
7
[原创]2026腾讯游戏安全竞赛-决赛-安卓
发表于: 2026-4-20 16:08
11320
[原创]2026腾讯游戏安全竞赛-决赛-安卓
Kevin·
2026-4-20 16:08
11320
前言:写的比较流水账。看了其它佬的文章,发现比赛还是漏掉了n多得分点… ## 解gdc `.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 / NAN` `parse_variable`在`0x144DE30`,能确认 `12 = IDENT = official 02`;`95 = : = official 84`;`38 = = = official 28` `parse_signal`在`0x144AB4C`,能确认`12 = IDENT = official 02`;`88 = ( = official 77`;`89 = ) = official 78`;`90 = , = official 79` `parse_enum`在`0x144D16C`,能确认`12 = IDENT = official 02`;`86 = { = official 75`;`87 = } = official 76`;`38 = = = official 28` `parse_function_signature`在`0x1453670`,能确认`94 = ... = official 83`;`90 = , = official 79`;`89 = ) = official 78`;`97 = -> = official 86`;lambda 分支再次给出`95 = : = official 84` `parse_function`在`0x144BB10`,能确认`12 = IDENT = official 02`;`88 = ( = official 77` `parse_assert`在`0x1458C5C`,能确认`88 = ( = official 77`;`90 = , = official 79`;`89 = ) = official 78` `parse_for`在`0x1456EC0`,能确认`12 = IDENT = official 02`;`95 = : = official 84`;`72 = in = official 61` `parse_if`在`0x14566E0`,能确认`52 = elif = official 41`;`53 = else = official 42`;`95 = : = official 84` `parse_match`在`0x145A044`,能确认`61 = when = official 50`;`95 = : = official 84`;`1 = NEWLINE = official 88` `parse_subscript`在`0x1461064`,能确认`85 = ] = official 74` `parse_call`在`0x14616E8`,能确认`92 = . = official 81`;`88 = ( = official 77`;`89 = ) = official 78` `parse_type_test`在`0x146459C`,能确认`22 = not = official 12` ... 最后归纳一下规律,魔改token到官方token的变换: ``` 0 -> 0 1..10 -> +87 11..49 -> -10 50 -> 98 51..97 -> -11 98 -> 87 99 -> 99 ``` 写脚本,初赛解gdc脚本改一下就能用,然后再按这个规律置换一下。然后就能用gdre正常反编译了。` ## trigger2 可以看到trigger2的算法全在gd层。    变量名做了混淆。直接扔给大模型来读。也不需要啥额外提示词。有一个值得注意的点是碰撞放在了native层。   然后就是把方块传送过来。此处方法和初赛一样,依旧是先hook到`libsec2026.so`的`extension_init`获取到关键api,然后`classdb_get_method_bind` +`ptrcall`动态调用godot原生方法递归遍历整个场景树,定位到小车和trigger2的位置后,`set_global_position`把方块传送到小车的位置。  ``` token=ee967a1e flag=flag{sec2026_PART1_f8a5b7bd} ``` 用到的脚本: `frida_dump_scene_tree.js`、`run_dump_scene_tree.py` dump场景树,`scene_tree_dump.json`dump的结果。 `run_trigger2_move.js`、`run_trigger2_move.py` 传送trigger2 token生成flag算法:`token2flag2.c` flag生成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 ^ F(prev_hi, round)`。这样上一轮的两个分组就都拿回来了,接着把 `lo` 和 `hi` 更新成 `prev_lo` 和 `prev_hi`,继续处理前一轮。 8 轮全部还原以后,程序拿到的就是最初输入给脚本 `_fe()` 的 4 字节 token。最后把这 4 个字节重新转成 8 位十六进制字符串输出,就是原始 token。 ## trigger3 先尝试了直接调w7,屏幕正常出现flag。 然后直接把方块挪过来,没有flag。 题目提示要开启碰撞。直接翻文档。  这里hook发现trigger3的`monitorable`和`monitoring`true,但trigger4的这两个是false。 场景树里可以看到它们还有子节点`CollisionShape3D`   这个应该是碰撞的实体。hook发现trigger3和trigger4的`CollisionShape3D`子节点的disabled都是true。  尝试把disable设置成false,然后把trigger3传送到小车上,成功显示flag  用到的脚本:`frida_dump_trigger_flags.js`、`run_dump_trigger_flags.py` 打印这几个属性。 改标志位并传送Trigger3到小车:`run_trigger3_enable_move.py`、`frida_trigger3_enable_move.js`。 主动调用尝试几组输入输出: ``` 10000000 -> 8ed9db184f1334737558461372eb675b 01000000 -> b658ed9e19340e7b393c02882f962881 00100000 -> d517864b8ab06e5d0761fb6e932bb8fb 00010000 -> ed39deccd83a3589dababb5394dedad1 00001000 -> ac58ec90cd65f495b4f828d1d8eeeaff 00000100 -> f4f840da8d1715dc42261f47a054d1b1 00000010 -> cb4104b4bee37b791bed907b3863e2e4 00000001 -> c91d8f3f436f77f22ca946d4d92e4528 ``` 这次的扩散非常强,不能跟初赛那样靠选择明文攻击来猜了。 这里仍然沿用初赛时的思路定位 `Process` 的入口,从 Godot GDExtension 的标准初始化流程入手。扩展库加载后,Godot 会调用 `extension_init(get_proc, library, init_struct)`,其中`get_proc` 是扩展用来查询 Godot API 的入口。先 hook `extension_init`,把第一个参数 `get_proc` 替换成自己的 wrapper。这个 wrapper 内部仍然调用原始 `get_proc`,但当查询`classdb_register_extension_class_method` 时,把这个注册函数 hook 住。 等 `classdb_register_extension_class_method` 被调用时,读第三个参数,也就是 `GDExtensionClassMethodInfo` 结构。这个结构里能看到统一的 Godot 调用 wrapper,以及对应的 `method_userdata`。 dump `method_userdata` 可以看到它的 vtable,从 vtable 的 `+0x18` 和 `+0x20` 两个槽位可以得到 `Process` 的两条实际调用桥接函数,分别是 `libsec+0x9f5d8` 和 `libsec+0x9c5f8`  继续分析这两个桥接函数,可以看到它们还不是最终业务逻辑,而是会继续读取 `method_userdata + 0x50` 和 `method_userdata + 0x58`,并把这两个值作为下一层分发参数。运行时 dump 这两个字段可以看到,`method_userdata + 0x50 = libsec+0x97704`,`method_userdata + 0x58 = 0`。  可以推断`Process`的入口就是`0x97704`。同理可得到`Tick`的入口`0x9ad68` 此处用到的脚本:`frida_locate_process_entry.js` `run_locate_process_entry.py` ### 逆向分析 ida打开能看到一些花指令插在代码段里。比如: ``` .text:00000000000A97B0 LDR X1, loc_A97A8 .text:00000000000A97B4 BLR X1 .text:00000000000A97B8 MOV X0, X1 .text:00000000000A97B8 ; --------------------------------------------------------------------------- .text:00000000000A97BC DCD 0xA2F2FFF1 .text:00000000000A97C0 ; --------------------------------------------------------------------------- .text:00000000000A97C0 LDR X23, [X7,X21] .text:00000000000A97C4 BR X23 ``` 这里我是写了个ida python脚本把`0xA2F2FFF1`全都给nop了,一共82处应该是。用到的脚本:`junk2nop.py` 定位到process入口`0x97704`  `sub_A936C`就是加密的主函数  把初始token复制一份扩展到16字节。然后调用`sub_A7900`和`sub_A7194`,然后进入一个比较友好的控制流平坦化。看入参,`sub_A7900`应该是用于构造208字节的上下文。其中最后16字节是特意复制进去的一块。  这里ida识别不出来switch结构,需要手工添加一下。  `sub_A936C`本身没什么内容,除了前面的函数调用,后面就是解密字符串,解出来是`%02x`,然后`sub_AA6AC`用它调用`_vsprintf_chk`用来写入flag。所以核心加密就在`sub_A7194`。  `A7194`的核心代码也不长。case0是每次对16大小的块做运算,v5是。case1拷贝计算结果,v6就是state+192,也就是每轮计算完把结果拷贝到这里。case2是终止条件。这里可以合理推断state前192字节是不变的S盒,最后16字节是每轮运算的state。 这里每轮就是调用`sub_AA9B0`和`sub_A8D44`做计算。只要搞清楚这俩干了啥就ok了。  AA9B0就是做异或。   相当于 ```c void sub_AA9B0(uint8_t *dst, const uint8_t *src) { for (int i = 0; i < 16; i++) { uint8_t x; if (i & 1) x = src[15 - i]; // 奇数位:倒着取 else x = src[i]; // 偶数位:正着取 dst[i] ^= x; } } ``` 接下来接着跟`sub_A8D44`。两个初始化函数。  `sub_A7944`是和常量做异或  翻译过来就是 ```c void sub_A7944(uint8_t *a1) { for (int i = 0; i < 16; i++) a1[i] ^= byte_58510[i]; } ``` `sub_AAB64`关键代码其实就这一行  变量太多了不想一个一个跟了。直接复制粘贴扔给大模型分析。   所以这里化简完就是: ```c void sub_AAB64(uint8_t round, uint8_t *dst, const uint8_t *table) { for (int i = 0; i < 16; i++) { int r = i / 4; int c = i % 4; dst[i] ^= table[16 * round + i] ^ (uint8_t)(c + 91 * round); } } ``` 往下看主体,`sub_AAB64`在`sub_A8D44`的主循环中也被频繁调用  直接整个扔给大模型  所以主循环化简完就是: ```c void sub_A8D44(uint8_t *input, uint8_t *state) { sub_A7944(input); // input[0..15] ^= byte_58510[0..15] sub_AAB64(0, input, state); // 第 0 轮混合 for (int round = 1; round <= 10; round++) { sub_A82C8(input); sub_A8F00(input, round); sub_AADE8(input); sub_A6F20(input); sub_AAB64(round, input, state); } sub_AAB64(11, input, state); // 最后一轮 sub_A84A4(input); } ``` 如法炮制,先手工恢复一下switch-case,然后一个一个扔给大模型看:   `sub_AADE8`不用扔,代码十分简洁。   ok到这里已经逆完了。还差hook把S-box和byte_183700的内容抓出来,然后把代码拼凑到一起就是生成算法了。 写个frida脚本抓一下:`run_dump_sbox_c_arrays.py`、  最终token生成flag生成算法:`token2flag3.c` flag生成token算法:`flag2token3.c` 这里说一下`flag2token3.c`的思路。 先看正向流程。原程序读入 8 字节输入以后,把这 8 字节复制两遍,拼成一个 16 字节状态,前半块和后半块完全相同。接着程序先对状态做两次固定异或,再做第 0 轮的轮密钥异或,然后进入主轮函数。主轮一共分成两部分,前 10 轮都执行同一套完整变换,最后 1 轮执行尾轮变换。完整轮里依次做 S 盒替换、4x4 状态矩阵顺时针旋转、轮掩码异或、行置换、列混合以及轮密钥异或;尾轮沿用相同框架,只是省掉了列混合。所有轮结束以后,程序再对结果异或一组固定常量,最后把 16 字节状态转成 32 位十六进制字符串输出。 `flag2token3.c` 按完全相反的顺序逐步还原。程序先把输入的 32 位十六进制串解析成 16 字节密文块,然后先去掉末尾那组固定异或。接着开始逆最后一轮,因为正向最后一轮的最后一步是加轮密钥,所以解密时先异或掉第 11 轮轮密钥,再依次执行逆行置换、逆轮掩码、逆旋转和逆 S 盒。做完这一轮以后,状态就回到了进入尾轮之前的样子。 然后从第 10 轮一路逆到第 1 轮。每一轮都先异或掉对应轮密钥,再执行逆列混合、逆行置换、逆轮掩码、逆旋转和逆 S 盒。 前 11 轮全部还原以后,程序再去掉最开头的第 0轮轮密钥,然后依次去掉最前面的两组固定异或。最后 16 字节状态被还原到`token || token`。 ## trigger4 分析trigger3的时候已经发现trigger4有三个需要改的属性了。全改了然后传送过来,成功得到flag。  此处用到的脚本:`run_trigger4_enable_move.py`和`frida_trigger4_enable_move.js` ### 入口定位 这里比较奇怪的是,即便改了token,再次碰撞返回的flag也不变。推测可能flag在碰撞前或者初始化阶段就完成计算了。token本身没有往外发的操作,那么它应该是在启动时主动取了token,计算出flag,然后缓存在某处。 gd脚本基本只有两个操作,一个collided_with注册碰撞,一个调用native层的`Tick()`。 `Tick`本身就很像反调试。他本身会在每一帧被调用,每超过十秒就更新一次`qword_1834B8`里的时间戳。  既然能通过信号传flag给label,那接下来的思路就是找哪里调用了`emit_signal` `libgodot_android`里找到`collided_with`,交叉引用只有一个函数`0x25F6718`。合理推断`sub_3C58F78`是`emit_signal`,那么这个v86就是生成出的flag。  往前追v86的来源  跟进`sub_3CACE6C`是一个字符串构造函数。构造的结果是第一个参数。 `sub_3CAD6D8`是字符串加法,相当于v79 = v78 + v29 `sub_3CAD53C`也是字符串加法,相当于v80 = v79 + v77 = v78 + v29 +v77。区别是它的第二个参数是个内部字符串对象,`sub_3CAD6D8`第二个参数是c字符串。 向上跟踪,发现v78的来源s就是flag的前半段:  v77的来源v81就是右括号  那么v29就是flag了。  v29来源是v27(v28)  v27是0xa9a7c。尝试hook验证一下,入参就是token。ok那么trigger4的flag计算的入口就是0xa9a7c 跟进看看,明示是VMP。   ok,vmp。根据过往经验,指望AI能还原vmp handler还是比较困难的。所以不看静态了,直接trace吧。 一开始想着库里可能涉及到游戏引擎的交互,要unidbg补环境的话巨大工程量,想直接真机trace,hook掉检测函数后,尝试qbdi trace了半天,trace了800w行左右就崩溃了。之前一度以为是踩到了暗桩,但最后原因似乎是内存占用太大了,logcat里看到`lowmemorykiller: Kill 'com.tencent.ACE.gamesec2026.final'`。 最后还是选择用unidbg。没有暗桩,也不存在和外部的复杂的对象交互,基本不用补环境,直接开箱就能trace。 unidbg代码:`ACE_final.java` 这里trace的结果有3.7个G,就不打包了。上传到网盘:https://xxxxxxxxxxx 提取码:xxxxxx ### trace算法还原 trace了`35294934`行 输入 `88b5ed2d`,输出 `f81de18110985738` 010editor打开,从后往前推。 这里就是常规的trace分析思路,从后往前分析。找`f81de18110985738`没找到。找`f81de181`找到了。  搜索这个地址,可以看到这处代码在整个过程中被频繁调用。并且flag的后半段`10985738`也是走的这里生成。这里看到的值`110985738`,多了一个十六进制位,可以推断原始代码应该是32位模加。  接着往前追计算出`f81de181`的两个操作数`0x5736fde4`和`0xa0e6e39d`:   接着往前追这四个中间值:`0x3b598095`、`0x658d6308`、`0xab408f96`、`0xfc767272`     到这一步已经开始有一些奇怪的东西出现了。可以看到`"add x21, x8, x9" x8=0xaabbccdd x9=0x84c2b9 => x21=0xab408f96`这一步用的0xaabbccdd 搜一下aabbccdd,有956个结果,并且分布十分均匀。合理推断是轮计算用到的密钥。 这里看分布图的条数已经可以推断计算轮次了,最上面那条只有四条指令,也没有参与运算,应该是初始化。排除掉这根,是28轮。  到这先总结一下, ``` f81de181 = 0x5736fde4 + a0e6e39d = (ab408f96 ^ fc767272) + (3b598095 + 658d6308) = ((aabbccdd + 84c2b9) ^ (a5b5789c ^ 59c30aee)) + ((b22f728e ^ 8976f21b) + (43b5dc62 + 21d786a6)) ``` 继续往前跟: ```markdown [19:53:49 188][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0x951d2164 x9=0x10985738 => x21=0xa5b5789c [19:53:48 677][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0x33ad3cee x9=0x2615ce00 => x21=0x59c30aee [19:53:50 219][libsec2026.so 0x13b3c8] [0225c91a] 0x1213b3c8: "lsr w2, w8, w9" w8=0x10985738 w9=0x5 => w2=0x84c2b9 [19:53:28 249][libsec2026.so 0x13cb3c] [220108ca] 0x1213cb3c: "eor x2, x9, x8" x9=0xada3618d x8=0xee16bdef => x2=0x43b5dc62 [19:53:16 576][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0x14dca33e x9=0xcfae368 => x21=0x21d786a6 [19:53:39 357][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0xaabbccdd x9=0x773a5b1 => x21=0xb22f728e [19:53:37 828][libsec2026.so 0x13cb3c] [220108ca] 0x1213cb3c: "eor x2, x9, x8" x9=0x59ac3af5 x8=0xd0dac8ee => x2=0x8976f21b ``` 发现`0x10985738`也在这里参与了运算,并且又一次看到了`aabbccdd`。这里还看到了右移操作。到这里已经非常像魔改Tea了。 设v0是`10985738`,v1是`f81de181`,那现在的计算 ``` v1 = 0x5736fde4 + a0e6e39d = (ab408f96 ^ fc767272) + (3b598095 + 658d6308) = ((aabbccdd + 84c2b9) ^ (a5b5789c ^ 59c30aee)) + ((b22f728e ^ 8976f21b) + (43b5dc62 + 21d786a6)) = ((aabbccdd + (v0 >> 5)) ^ ((951d2164 + v0) ^ (33ad3cee + 2615ce00))) + (((aabbccdd + 773a5b1)) ^ (59ac3af5 ^ d0dac8ee)) + (ada3618d + ee16bdef)) ``` 搜一遍这一轮新增的变量,看看有没有key,发现`33ad3cee`和`aabbccdd`一样,也是956次调用,且分布均匀。这应该也是个密钥。  标准Tea算法是32轮,c代码如下,对比着分析: ```c void encrypt(uint32_t* v, uint32_t* k) { uint32_t v0 = v[0], v1 = v[1], sum = 0, i; uint32_t delta = 0x9e3779b9; uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; for (i = 0; i < 32; i++) { sum += delta; v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1); v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3); } v[0] = v0; v[1] = v1; } ``` 这里的`aabbccdd`和`33ad3cee`一个对应k2一个对应k3 现在v1没必要继续展开了,需要先看看后半段flag最后一轮的算法,来交叉对比确认sum_、上一轮的v1和k0 k1。 如法炮制,可以得到后半段flag: ```markdown [19:53:45 151][libsec2026.so 0x13b3c8] [0225c91a] 0x1213b3c8: "lsr w2, w8, w9" w8=0xa0e6e39d w9=0x7 => w2=0x141cdc7 [19:53:43 750][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0x951d2164 x9=0xa0e6e39d => x21=0x136040501 [19:53:42 803][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0xf95d664a x9=0xe6e39d0 => x21=0x107cba01a [19:53:33 703][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0x12aa364c x9=0xcb1ac6 => x21=0x13755112 [19:53:32 649][libsec2026.so 0x13cb3c] [220108ca] 0x1213cb3c: "eor x2, x9, x8" x9=0xd0c4e7cd x8=0x523396ca => x2=0x82f77107 [19:53:23 120][libsec2026.so 0x13cb3c] [220108ca] 0x1213cb3c: "eor x2, x9, x8" x9=0x12ede559 x8=0x75fcbe66 => x2=0x67115b3f [19:53:11 477][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0xffa89e37 x9=0xf6389ca5 => x21=0x1f5e13adc [19:53:45 655][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0x12aa364c x9=0x141cdc7 => x21=0x13ec0413 [19:53:44 648][libsec2026.so 0x13cb3c] [220108ca] 0x1213cb3c: "eor x2, x9, x8" x9=0x36040501 x8=0x7cba01a => x2=0x31cfa51b [19:53:34 246][libsec2026.so 0x13cb3c] [220108ca] 0x1213cb3c: "eor x2, x9, x8" x9=0x13755112 x8=0x82f77107 => x2=0x91822015 [19:53:23 643][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0x67115b3f x9=0xf5e13adc => x21=0x15cf2961b [19:53:46 197][libsec2026.so 0x13cb3c] [220108ca] 0x1213cb3c: "eor x2, x9, x8" x9=0x13ec0413 x8=0x31cfa51b => x2=0x2223a108 [19:53:35 689][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0x91822015 x9=0x5cf2961b => x21=0xee74b630 [19:53:46 725][libsec2026.so 0x13b5f4] [1501098b] 0x1213b5f4: "add x21, x8, x9" x8=0x2223a108 x9=0xee74b630 => x21=0x110985738 ``` 可以确认`12aa364c`和`f95d664a`也是k ``` v0 = 10985738 = 2223a108 + ee74b630 = (13ec0413 ^ 31cfa51b) + (91822015 + 5cf2961b) = ((12aa364c + 0141cdc7) ^ (36040501 ^ 07cba01a)) + ((13755112 ^ 82f77107) + (67115b3f + f5e13adc)) = ((12aa364c + (a0e6e39d >> 7)) ^ ((951d2164 + a0e6e39d) ^ (f95d664a + 0e6e39d0))) + (((12aa364c + 00cb1ac6) ^ (d0c4e7cd ^ 523396ca)) + ((12ede559 ^ 75fcbe66) + (ffa89e37 + f6389ca5))) ``` 这一轮计算中和v1共同用到了`a0e6e39d`和`951d2164`那这俩一个是sum一个是上一轮v0。分别搜索来源,可以看到参`951d2164`运算的`29e59c9f`大量出现。   那么可以确定`a0e6e39d`是上一轮v1,最右侧的式子一直是加法展开,合理推断`ee74b630`就是上一轮v0。951d2164`是这一轮的sum,`29e59c9f`是`delta` 那么目前可以确定: * 28轮运算 * delta=29e59c9f * 四个k分别是`12aa364c`、`f95d664a`、`aabbccdd`、`33ad3cee` 化简一下两个式子: ``` v0 = 10985738 = 2223a108 + v0 = (13ec0413 ^ 31cfa51b) + v0 = ((12aa364c + 0141cdc7) ^ (36040501 ^ 07cba01a)) + v0 = ((k0 + (v1 >> 7)) ^ ((sum + v1) ^ (k1 + 0e6e39d0))) + v0 v1 = 0x5736fde4 + v1 = (ab408f96 ^ fc767272) + v1 = ((k2 + 84c2b9) ^ (a5b5789c ^ 59c30aee)) + v1 = ((k2 + (v0 >> 5)) ^ ((sum + v0) ^ (k3 + 2615ce00))) + v1 ``` ok那现在就差展开`0e6e39d0`和`2615ce00`了。   ```markdown [19:53:42 291][libsec2026.so 0x139e74] [0221c99a] 0x12139e74: "lsl x2, x8, x9" x8=0xa0e6e39d x9=0x4 => x2=0xa0e6e39d0 [19:53:47 241][libsec2026.so 0x139e74] [0221c99a] 0x12139e74: "lsl x2, x8, x9" x8=0x10985738 x9=0x6 => x2=0x42615ce00 ``` 那么现在可以明确: ``` v0 = ((k0 + (v1 >> 7)) ^ ((sum + v1) ^ (k1 + v1 << 4))) + v0 v1 = ((k2 + (v0 >> 5)) ^ ((sum + v0) ^ (k3 + v0 << 6))) + v1 ``` 还差输入。尝试搜88b5ed2d、88b5、ed2d都没搜到。尝试按照初赛时搜字符串对应的hex`6432646535623838`搜到了。搜delta最早参与的非0加法,可以看到`64326465`参与了运算。  那么到这基本可以确定最终加密算法就是 ```c void encrypt(uint32_t* v) { uint32_t v0 = v[0], v1 = v[1], sum = 0, i; uint32_t delta = 0x29e59c9f; uint32_t k0 = 0x12aa364c; uint32_t k1 = 0xf95d664a; uint32_t k2 = 0xaabbccdd; uint32_t k3 = 0x33ad3cee; for (i = 0; i < 32; i++) { sum += delta; v0 = ((k0 + (v1 >> 7)) ^ ((sum + v1) ^ (k1 + v1 << 4))) + v0 v1 = ((k2 + (v0 >> 5)) ^ ((sum + v0) ^ (k3 + v0 << 6))) + v1 } v[0] = v0; v[1] = v1; } ``` 验证了几组token,没问题。  token生成flag算法:`token2flag4.c` flag生成token算法:`flag2token4.c` 这里也说一下这个解密程序的思路。 解密程序就是把这 28 轮按完全相反的顺序逐轮还原。程序先读入 16 位十六进制串,再用 `sscanf(input, "%8x%8x", &hi, &lo)` 把前 8 位和后 8 位分别读出来。正向输出顺序是 `v[1], v[0]`,所以这里要先执行 `v[1] = hi`,再执行 `v[0] = lo`。 程序从第 28 轮开始往前还原到第 1 轮。每一轮先计算这一轮对应的 `sum`, `sum = i * delta`。 正向每一轮是先更新 `v0`,再用更新后的 `v0` 去更新 `v1`,所以逆的时候先还原`v1`再还原`v0` 核心算法其实就是: ```c void decrypt(uint32_t *v) { uint32_t v0 = v[0], v1 = v[1], i; uint32_t delta = 0x29e59c9f; uint32_t k0 = 0x12aa364c; uint32_t k1 = 0xf95d664a; uint32_t k2 = 0xaabbccdd; uint32_t k3 = 0x33ad3cee; for (i = 28; i > 0; i--) { uint32_t sum = i * delta; v1 = v1 - (((k2 + (v0 >> 5)) ^ ((sum + v0) ^ (k3 + (v0 << 6))))); v0 = v0 - (((k0 + (v1 >> 7)) ^ ((sum + v1) ^ (k1 + (v1 << 4))))); } v[0] = v0; v[1] = v1; } ``` ## 反调试 ### 代码保护 代码保护这里主要有 代码段有junk code、控制流平坦化、vmp 还有不少这种字符串混淆,运行时解密:     反调试主要就是这里启的这仨检测线程。  ### sub_9C654 这个函数比较直观,标准反调试做法,fork出子进程,子进程attach父进程,防止父进程再被其他进程attach。   这里他还调用了`sub_95cc0`,会读取父进程的硬件断点寄存器,并每轮最多在8个硬件断点槽里写{0, 7}。这里应该是干扰硬断。   总结一下,这里就是子进程attach父进程反调试,然后子进程向父进程的硬件断点槽里写{0, 7},干扰调试。 ### sub_9CDC4 这里的控制流平坦化强度明显高于trigger3那里遇到的了。比较难受的是直接D810解不掉,赛后有空看看log分析一下原因。 可以根据他的字符串和系统调用来推断一些功能。这里解出来的字符串有`gum-js-loop gmain .. /proc/self/task /proc/self/task/%s/status ` 显然`gum-js-loop gmain`这俩是检测frida特征的。 先调用`sub_99418`,这里用到了`/proc/self/fd /proc/self/fd/%s linjector`     除了解密字符串外也没啥外部调用。只看系统调用也能猜个七七八八了。 `opendir("/proc/self/fd")`,`readdir()` 遍历每个 fd 项,拼出完整路径,对每个条目做` lstat()`,必要时再 `readlinkat()` 读出最终目标,最后检查链接目标里是否出现 `linjector`。只要命中,就直接走 `exit_group`。 回到`sub_9cdc4`     这里跟踪字符串  交叉引用   合理推断这里是在对比`status`中是否有`gum-js-loop`如果有就走到`exit_group` 那么这里的检测也很明确了,先看`/proc/self/task/`目录下有哪些tid,然后逐个读`/proc/self/task/<tid>/status`对比`gum-js-loop`和`gmain`来检测是否存在frida的痕迹。 `..`推测是用来作为白名单的字符串,避免读到`/proc/self/status` 总结一下,一个是遍历`/proc/self/fd`下的每·个项,检查链接目标里是否出现 `linjector`。一个是读`/proc/self/task/<tid>/status`对比`gum-js-loop`和`gmain`来检测frida痕迹。 ### sub_9B7D8 先休眠3s,然后`setpriority(PRIO_PROCESS, 0, 19)`降低当前线程优先级,然后取系统页大小,计算exit 所在代码页对齐基址,交叉引用还可以看到这里还把exit所在页的权限改成了可读可执行。  这里还直接对比了exit前四个字节指令字,应该是检测对exit的inlinehook   然后调用  这里相当于`fd = openat(AT_FDCWD, "/proc/self/maps", O_RDONLY, 0); `,传入的v92就是`/proc/self/maps`。v29就是fd。交叉引用可以看到调用了这里。一个字节一个字节读里面的内容  检测到换行后就调用`sub_98564`去分析这一行   这里能解出三个字符串 ` %lx-%lx %s frida /memfd:` 解完字符串,结合这里的输入大致就能猜到这里在检测这`/proc/self/maps`里的一行是否有 frida hook的特征,以及获取这一行的起始地址、结束地址、权限字符串。 回到`sub_9B7D8`,这里从`unk_1682E0`拷20个字节然后和`xmmword_58640`异或,解出来就是`libgodot_android.so`,然后传递给`sub_96A00`。这里的第二个参数其实就是后面CRC校验的期望值。  `sub_96A00`混淆程度比较轻,直接枚举当前进程所有已加载模块。找到libgodot_android.so后,`sub_9EFB4`继续遍历这个模块的 Elf64_Phdr 数组,并筛选满足:`p_type == PT_LOAD` `p_flags & PF_X`的 header。其实就是找 text 段。找到以后,把两个值写回上层上下文:  然后调用`sub_96A00`。这里是个标准的CRC32,每处理1000字节左右会sleep一下,并写入一个时间戳。这个和`Tick`维护的是同一个变量。CRC计算后返回到上层对比。  总结一下,`sub_9B7D8`这条线一个是检测exit入口的汇编,一个是检测`/proc/self/maps`里的frida痕迹,一个是计算`libgodot_android.so`的text段的crc防`inline hook`。
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于
2026-4-20 16:08 被Kevin·编辑 ,原因:
#Reverse
收藏
・
4
点赞
・
7
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
感谢你分享这么好的资源!
2026-7-16 08:38
MochaCo
谢谢你的细致分析,受益匪浅!
2026-4-27 13:38
bananaships
+5
谢谢你的细致分析,受益匪浅!
2026-4-24 17:01
mb_mtoczesz
感谢你分享这么好的资源!
2026-4-23 21:42
苏打水
感谢你的积极参与,期待更多精彩内容!
2026-4-23 10:14
chiikawa_gui
谢谢你的细致分析,受益匪浅!
2026-4-22 20:02
xxrinkoss
这个讨论对我很有帮助,谢谢!
2026-4-20 17:23
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
7
)
Imxz
雪 币:
110
活跃值:
(9576)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
775
粉丝
9
关注
私信
Imxz
2
楼
tql
2026-4-20 17:37
1
盘子蛋糕
雪 币:
225
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
2
粉丝
1
关注
私信
盘子蛋糕
3
楼
太强了,我一开始也想用trace还原cff流程和最后VM的污点分析了,碍于对安卓工具不太了解不知道unidbg可以追
2026-4-21 15:43
1
Kevin·
雪 币:
245
活跃值:
(120)
能力值:
( LV2,RANK:10 )
在线值:
发帖
1
回帖
11
粉丝
4
关注
私信
Kevin·
4
楼
盘子蛋糕
太强了,我一开始也想用trace还原cff流程和最后VM的污点分析了,碍于对安卓工具不太了解不知道unidbg可以追[em_006]
这题的vmp直接trace分析有点过于简单了,感觉出题人似乎并没考虑在这种解法上设置障碍?虽然能这么做出来,但不知道有没有分:(
2026-4-21 21:06
1
盘子蛋糕
雪 币:
225
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
2
粉丝
1
关注
私信
盘子蛋糕
5
楼
我觉得肯定是有分的,比ai撕开更体现思路能力吧,我就吃了对工具不太了解的分,没时间去学了,这要放Windows上我用我自己工具就追出来了
2026-4-22 11:55
1
需要渗透高手
雪 币:
114
活跃值:
(280)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
42
粉丝
1
关注
私信
需要渗透高手
6
楼
????????
2026-4-22 17:27
0
需要渗透高手
雪 币:
114
活跃值:
(280)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
42
粉丝
1
关注
私信
需要渗透高手
7
楼
厉害给你点个赞,表情还发不出去呢。
2026-4-22 17:27
0
wx_周安
雪 币:
0
活跃值:
(20)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
1
粉丝
0
关注
私信
wx_周安
8
楼
请问有赛题下载吗
2026-7-17 18:24
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
Kevin·
1
发帖
11
回帖
10
RANK
关注
私信
他的文章
[原创]2026腾讯游戏安全竞赛-决赛-安卓
11320
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部