2026数字中国数字安全赛道网络安全团体赛决赛WP
Reverse
- Re手因身体原因未到现场,但因现场题就Re较为简单,其他方向题目比较难,导致团队未能获奖,Re手表示非常抱歉未能到现场。
FlagVault
这题题目说的很明确了,加密在Native层

定位到关键函数
发现整体逻辑较为简单,且有用函数为getFlag
用Bandizip解压apk(这里主要是我没到现场,队员说问ai用apktool解压,但她没有,所以有点小可惜)

定位到getflag
发现加密很简单

SystemMonitor
这道题题目明确说明是个恶意软件
且丢到IDA发现没有明显flag相关函数
可以合理怀疑是把敏感信息换为了flag,然后传到恶意网站或服务器

大概看了下主要函数
这两个很可疑
审阅后可以发现 sub_1400015FE()为关键函数
这个函数行数较多,可以丢给本地ai进行分析,也完全能跑出来流程

然后就可以大胆猜测是把flag传出去了,打个断点动态调试即可

以上两道题就是线下赛解出的Re题了,我也是在模拟离线环境下做出来的,非常可惜,如果拿到这两个题的解最少也是个
铜奖银奖了(悲)
ChatA
这道题题目说了要分析通讯
把apk丢到jadx中发现有好多base64编码
主要是StringFog中的解密
看了一下就是两个base64异或
然后就没什么头绪了

只找到了个flag的关键字
大概率也是要去翻native层

丢给GPT进行分析
加密方式为AES-256-ECB

加个flag头就行了应该
不知道正常解是不是这么解,对于ai是怎么分析出来这块我还是很疑惑怎么分析出来的,奈何现在GPT也被限制了,希望有师傅可以解惑
还是说我是做题经验太少,这是经验问题

后记,2026.06.22
- 继上文分析

- 这里可以看到初始段有两个解密函数
- 肯定是后面要用到的
- 现在看JNI_OnLoad函数
- 发现是ollvm的混淆,完全看不懂在干什么
- 在ai辅助下找到sub_1AA4C80函数(我知道线下赛用不了ai,但我是在复现并学习,我为了绕开更多弯路所必须要做的,没人手把手教我,只有ai,参考wp也找不到)

- 可以看到这里有很多有趣的函数

- 模仿初始化阶段来写解密脚本,解出部分字符串
1 |
|
- 这是模板,小改一下就可以解字符串了
- 可以看出这个assets/index.js很重要,但是打开却发现根本看不懂
- 就可以知道这被加密了
- 继续分析发现sub_1AA3C30函数很关键
- 虽然也是很乱,看不出来是什么东西,但可以发现sub_1AAFEE0函数内有aes字样

- 就可以合理推断出这个js脚本是被aes-256-ecb加密的
- 密钥的话大概率是这四段

- 并且可以发现初始化阶段它们的解密是独立的

- 就可以顺利解出密钥

1 |
|
- 然后解密环节就和之前一样了
Bridge
这是一个鸿蒙系统下的程序
需要用到abc-decompiler工具

找到了Flag的关键函数
第一步就是获取用户的token

在com.hmos.exam5.pkg/entry/ets/model/DatabaseUtil中也给了admin账户和密码
也给了盐,生成token使用了sha256

回到解密,第一步,再次使用sha256

取前32个hex,51a8f50d751a48550f90b3c49439afbe,作为key
下一步是解sm4
返回到com.hmos.exam5.pkg/entry/ets/model/FlagUtil
看了一下调取的函数#*#readLocalFlagCipher就是5a52339fbdb9b11b330ed4ffd6f06b453e0d3fb96a624a71b16124065c2d9e3362fa1d5941b0995e559002b759cf91a9

sm4的相关信息也给了

就可以得出flag了!!!
其实这里面还可以进行sql注入

但没什么鸟用
也可以在解压的文件夹中看到

Dedebug
这道题做的我好痛苦
怎么弄也绕不过这个检测
用了apktool把apk改成了这样子

还是绕不过去
实在没招,问了ai,说初始化段也有反调试

不听GPT,改来改去,改不动了
还是看看GPT给的静态分析吧
GPT应该是分析的x86_64下的so库



- 这是GPT给我的一把梭脚本
1 | from pathlib import Path |
当然,根据这道题的题意肯定是让选手绕过调试解出flag的
用GPT分析了下需要patch的点,但让那个用apktool修改也是需要做得(以x86_64为例)
1 | # ============================================================ |
- Frida脚本
1 | ; |
- 安卓逆向经验欠佳,试了很久也不能出flag,所以用ai来总结
后记,2026.06.23
- 还是先把apk放在jadx中分析

- 很明显,发现有个root检测
- 使用
java -jar .\apktool_3.0.2.jar d -r dedebug.apk -o debugme_src把apk编回smali文件
- 装回后发现依旧有检测,怀疑是native层面的检测
- 看了看native层的checkflag,发现无法直接逆向

- 开始分析反调试检测
- 来到初始化阶段

- 发现只有这一个函数,点开看看
- 审阅后发现是三个kill

- 也就对应三次检测
- 改下jmp就可以了
- 然后继续分析,发现还是有检测,注释一下,然后patch就行了,也就是nop和改一下jmp

- 后面发现sub_EE0,sub_1B60,sub_2170这三个检测函数也调用了kill
- 所以直接让它们三个retn就好
- hook代码还是上面的,怎么写出来的我还得再学习学习
DC
一个core文件
查了一下是Core 文件记录了程序在发生异常那一刻的内存映像、寄存器状态和堆栈信息。
用ai一把梭吧,找不到主函数(前几个题的wp写的有点燃尽了)
0x00 文件识别
拿到题目文件 dc.core,先看一下是什么:
1 | $ file dc.core |
是一个 ELF 64-bit core dump 文件。Core dump 是程序运行时内存的快照,通常包含了程序的代码段、数据段以及当时的栈/堆状态。
strings 能看到一些有意思的东西:
1 | /tmp/ctf_%08x # 写入 /tmp 目录,疑似成功时输出 flag |
用 IDA Pro 打开 core dump,设置基址为 0x55C62414E000。
0x01 定位主逻辑
Core dump 中有很多 libc 函数,真正的程序代码在 0x55C62414F000 附近。从 survey_binary 可以看到几个关键函数都在这个区域。
函数清单
| 地址 | xxxxxxxxxx220 1#!/usr/bin/env python32# -- coding: utf-8 --34import base645import struct6from typing import List, Tuple78# ———————————————————–9# 辅助函数:模拟 C 语言的 32 位无符号整数溢出10# 因为 Python 的整数是无限精度的,而 C 语言中 uint32 超过 0xFFFFFFFF 会截断11# ———————————————————–12def u32(x: int) -> int:13 return x & 0xFFFFFFFF141516# ———————————————————–17# 模拟 Windows C 运行时库 (MSVCRT) 的随机数生成器18# 题目通常是 Windows 下的 exe,其 rand() 实现是固定的线性同余生成器 (LCG)19# ———————————————————–20class MSVCRTRand:21 def init(self, seed: int):22 self.state = seed & 0xFFFFFFFF2324 def rand(self) -> int:25 # 经典的 MSVCRT 参数:乘 214013,加 253101126 self.state = (self.state * 214013 + 2531011) & 0xFFFFFFFF27 # 返回高位部分 (15 bits)28 return (self.state >> 16) & 0x7FFF293031# ———————————————————–32# 第一步:重建最终的密文数据33# 逆向时发现密文在内存中是以 QWORD (8字节) 数组形式存在的,需要拼起来34# ———————————————————–35def build_expected_60() -> bytes:36 # 从 IDA Pro 等工具中提取的硬编码数据37 v57_qwords = [38 0x280D30732B077874,39 0x242D00103573060B,40 0x141C3406727D2F73,41 0x0A71137676362833,42 0x0E232B242F04742A,43 0x2F373F03033D7310,44 0x77067C3612772D7D,45 ]46 n925 = 925053188 # 尾部的 4 字节数据4748 # struct.pack(“<Q”) 表示以小端序 (Little-Endian) 将整数打包成 bytes49 v57_bytes = b””.join(struct.pack(“<Q”, q) for q in v57_qwords) # 共 7*8 = 56 字节50 tail4 = struct.pack(“<I”, n925) # 4 字节5152 # 逻辑还原:53 # 原始比较逻辑可能是:先比较第0个字节是否为 0x74,然后比较后面的一串54 # 这里把它们拼接成完整的 60 字节目标密文55 expected = bytes([0x74]) + (v57_bytes[1:] + tail4) # 60 bytes56 assert len(expected) == 6057 return expected585960# ———————————————————–61# 第二步:逆向 Fisher–Yates 乱序算法62# 正向加密时:从后往前遍历,每次生成一个随机位置并交换63# 逆向解密时:必须重现完全一样的随机数序列,然后倒着交换回去64# ———————————————————–65def unshuffle_msvcrt(shuffled: bytes, seed: int = 0x65) -> bytes:66 # 初始化随机数生成器,种子为 0x65 (十进制 101)67 r = MSVCRTRand(seed)68 69 # 1. 先把正向加密时产生的所有 swap 操作记录下来70 # 注意:必须按正向的顺序调用 r.rand(),才能保证随机数序列一致71 swaps: List[Tuple[int, int]] = []72 for i in range(59, -1, -1):73 j = r.rand() % (i + 1)74 swaps.append((i, j))7576 # 2. 开始执行逆向交换77 a = bytearray(shuffled)78 # reversed(swaps) -> 后发生的交换先还原79 for i, j in reversed(swaps):80 a[i], a[j] = a[j], a[i] # 交换回原位81 return bytes(a)828384# ———————————————————–85# 第三步:XXTEA 变种算法解密86# 这是一个典型的“魔改”TEA算法,通常涉及 DELTA 常量和复杂的移位异或87# ———————————————————–88DELTA_SUB = 1640531527 # 0x61C88647 (常见的 TEA 魔数 0x9E3779B9 的变体或计算结果)89DELTA_ADD = 0x9E3779B9 9091# 逆向工程得到的初始 SUM 和结束 SUM92SUM_START = u32(-1640531527) # 0x9E3779B993SUM_STOP = u32(-865977613) # 0xCC623AF394KEY2_FIRST = u32(1634492739) # 初始的 Key2 变量值9596# 核心混合函数1 (处理数组中间元素的)97def mx_inner(z: int, y: int, sum_: int, k: int, key4: List[int]) -> int:98 “””99 对应加密代码中的:100 e = ((sum >> 2) & 3)101 z = v[n-1], y = v[i+1]102 mx = ((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(i&3)^e]^z))103 “””104 # 这里计算 key 的索引: ((sum>>2) ^ k) & 3105 idx = (((sum_ >> 2) & 0xFF) ^ (k & 0xFF)) & 3106 return u32(((z ^ key4[idx]) + (y ^ sum_)) ^ (((4 * y) ^ (z >> 5)) + ((16 * z) ^ (y >> 3))))107108# 核心混合函数2 (处理数组最后一个元素的)109def mx_last(z: int, v0: int, sum_old: int, key2: int) -> int:110 “””111 对应加密循环中每一轮结尾对 v[n] 的特殊处理112 “””113 return u32((((z >> 5) ^ (4 * v0)) + ((16 * z) ^ (v0 >> 3))) ^ ((v0 ^ sum_old) + (key2 ^ z)))114115116def build_round_params(key4: List[int]) -> Tuple[List[int], List[int]]:117 “””118 预计算每一轮的参数。119 因为 XXTEA 是多轮加密(这里是 10 轮),解密时需要知道每一轮对应的 sum 值和 key2 值。120 加密是从 sum = START 加到 STOP,解密我们先算出来存表里,方便倒着用。121 “””122 sums: List[int] = []123 key2s: List[int] = []124125 sum_ = SUM_START126 key2 = KEY2_FIRST127128 for _ in range(10): # 循环 10 轮129 sums.append(sum_)130 key2s.append(key2)131132 # 模拟加密时的更新逻辑133 sum_ = u32(sum_ - DELTA_SUB) 134 if sum_ == SUM_STOP:135 break136 # 计算下一轮用到的 key2137 idx2 = (((sum_ >> 2) & 0xFF) ^ 0x0A) & 3138 key2 = key4[idx2]139140 return sums, key2s141142143def decrypt_variant(words: List[int], key4: List[int]) -> List[int]:144 “””145 解密主函数146 words: 11 个 32位整数 (密文)147 key4: 4 个 32位整数 (密钥)148 “””149 v = [u32(x) for x in words]150 151 # 获取每一轮的 sum 和 key2152 sums, key2s = build_round_params(key4)153154 # 倒着遍历每一轮 (Round 10 -> Round 1)155 for sum_old, key2 in zip(reversed(sums), reversed(key2s)):156 # 1. 先逆向处理最后一个元素 v[10]157 # 加密时:v[10] += mx_last(…)158 # 解密时:v[10] -= mx_last(…)159 z_new_v9 = v[9]160 v0_new = v[0]161 v[10] = u32(v[10] - mx_last(z_new_v9, v0_new, sum_old, key2))162163 # 2. 逆向处理前面的元素 v[9] 到 v[0]164 # 加密时是从 0 到 9 顺序处理,解密时要从 9 到 0 倒序处理165 for k in range(9, -1, -1):166 y_old = v[k + 1] # 后一个元素 (已经是解密后的旧值了)167 z = v[10] if k == 0 else v[k - 1] # 前一个元素168 # 执行减法操作逆向169 v[k] = u32(v[k] - mx_inner(z, y_old, sum_old, k, key4))170171 return v172173174# ———————————————————–175# 主程序入口176# ———————————————————–177def main():178 # 1. 获取目标字节串 (60 bytes)179 expected = build_expected_60()180181 # 2. 逆向 XOR 操作182 # 加密逻辑推测:data[i] ^ 0x45183 # 解密逻辑:result[i] ^ 0x45 (异或的逆运算还是异或)184 shuffled_b64 = bytes(b ^ 0x45 for b in expected)185186 # 3. 逆向 Shuffle 操作187 # 还原出 Base64 字符串188 b64_text = unshuffle_msvcrt(shuffled_b64, seed=0x65)189 print(f”[+] Base64 字符串: {b64_text}”)190191 # 4. Base64 解码192 # 将 60 字节的 Base64 字符串解码为 44 字节的二进制数据193 ct = base64.b64decode(b64_text, validate=False)194 if len(ct) != 44:195 raise SystemExit(f”[!] base64 len={len(ct)} != 44”)196 197 print(“[+] Ciphertext (Hex):”, ct.hex())198199 # 5. 准备解密密钥和密文数组200 # 将 44 字节解包成 11 个整数 (11 * 4 = 44)201 v = list(struct.unpack(“<11I”, ct))202203 # 密钥是 “Calamity_Fortune”,转成 4 个整数204 key4 = list(struct.unpack(“<4I”, b”Calamity_Fortune”))205206 # 6. 执行核心算法解密207 pt_words = decrypt_variant(v, key4)208 209 # 7. 将解密后的整数数组打包回字符串210 pt = struct.pack(“<11I”, *pt_words)211212 print(“[+] 最终 Flag (Hex):”, pt.hex())213 try:214 print(“[+] 最终 Flag (UTF-8):”, pt.decode(“utf-8”))215 except UnicodeDecodeError:216 print(“[!] 解码失败,可能包含非打印字符:”, pt)217218219if name == “main“:220 main()python |
|---|---|
0x55C62414F2C9 |
XOR 循环 |
0x55C62414F31A |
初始化函数(解密数据) |
0x55C62414F370 |
RC4 KSA(密钥调度) |
0x55C62414F45C |
RC4 加/解密 |
0x55C62414F5F8 |
自定义 Base64 编码 |
0x55C62414F86C |
主验证逻辑 |
0x55C62414F786 |
成功回调(写 flag 到 /tmp) |
0x02 初始化函数 — XOR 解密
首先吸引注意的是 sub_55C62414F31A,它调用了三次同一个 XOR 函数:
1 | __int64 sub_55C62414F31A() |
sub_55C62414F2C9 的逻辑非常简单:
1 | for (i = 0; i < size; ++i) |
这说明程序在编译时将关键数据 XOR 加密存储,运行时通过 init 函数解密。三块数据的 XOR 密钥各不相同:
0x55C624152040(64 字节)← XOR 0x42
0x55C6241520A0(53 字节)← XOR 0x24
0x55C624152020(14 字节)← XOR 0x56
从 core dump 中读取这三段加密数据,用对应密钥 XOR 解密:
1 | # 字母表 (64 bytes) |
RC4 密钥 C0nFu5i0n_K3y!(Confusion_Key 的 leet 写法)明显是人造的,确认解密方向正确。
0x03 主验证逻辑
sub_55C62414F86C 是核心验证函数:
1 | void sub_55C62414F86C() |
程序流程:用户输入 → RC4 加密 → 自定义 Base64 编码 → 与期望密文比较。
所以逆向求 flag 的路径:期望密文 → 自定义 Base64 解码 → RC4 解密 → Flag。
0x04 修改版 RC4
KSA(密钥调度)
仔细对比标准 RC4,该程序的 KSA 有改动:
1 | // 标准 RC4 KSA: |
PRGA(伪随机生成)
同样被修改了:
1 | // 标准 RC4 PRGA: |
两个关键改动:
KSA 中
j = j + S[i] + i + key[...](多了+ i)PRGA 中
k = (S[i] + S[j]) ^ pos(用输出位置 XOR 替代 S-box 索引)
0x05 自定义 Base64
sub_55C62414F5F8 实现了一个带位置相关偏移的 Base64 变体:
1 | v7 = 0; // 全局输出位置计数器 |
与标准 Base64 的区别:
字母表是乱序的(64 个自定义字符而非标准
A-Za-z0-9+/)**每个位置的编码索引加了偏移量 ****
(pos % 7) + 3**
解码时需要逆向这个偏移:
1 | for pos, char in enumerate(encoded): |
0x06 解密脚本
1 | # 从 core dump 读取的加密数据 |
0x07 总结
本题是一个典型的 CTF 逆向题,考察点包括:
Core dump 分析 — 识别 core dump 文件并加载到 IDA 分析
XOR 加密数据识别 — 通过 init 函数发现静态数据在编译时被 XOR 加密
魔改 RC4 识别 — 对比标准 RC4 发现 KSA 多了
+i和 PRGA 的^pos改动自定义 Base64 — 位置相关的偏移量
(pos % 7) + 3+ 乱序字母表
最终 Flag:
1 | flag{Tw34k3d_RC4_n_B64_m4k3_1t_h4rd!!!} |
第七题题目和附件全部丢失,如果有师傅有附件并愿意给我的话,请联系我,感谢
关于本文
由 GuQing 撰写,采用 CC BY-NC 4.0 许可协议。