Polaris CTF 招新赛逆向题(Reverse)Writeup 教学文档
题目1: Illusion [Hook]
1. 核心要点
题目表面是RC4加密,实则通过Hook机制(MessageBoxA)转向AES解密流程以获取真实flag。
2. 程序逻辑分析
-
主程序(伪RC4)
- 程序入口接收长度为25的输入,格式为
xmctf{...}。 sub_140001440函数模拟了标准RC4算法:使用密钥nev_gona_give_up初始化S盒,与用户输入的中间18字节进行异或,并与一段硬编码的18字节常量比较。- 若匹配,可逆推出一个结果
nev_gona_letydown\x07,但这是一个假flag。
- 程序入口接收长度为25的输入,格式为
-
Hook机制与真实校验
- 程序初始化时调用
sub_140001000,动态获取user32.dll中MessageBoxA函数的地址。 - 通过
VirtualProtect修改内存页权限,将MessageBoxA函数的前几个字节替换为跳转指令,将其Hook到自定义函数sub_1400010F0。 - 因此,任何触发
MessageBoxA弹窗的操作都会实际执行sub_1400010F0,此函数内包含真正的flag校验逻辑。
- 程序初始化时调用
-
真实校验(AES解密)
- 在
sub_1400010F0中,存在第二层校验。 - 密钥生成:由4个DWORD值构成:
[873608210, 873608210, 873608210, 559105345]。按小端序(<IIII)打包后得到16字节密钥:b'\x12\x34\x12\x34\x12\x34\x12\x34AES!'。 - 密文:一段固定的32字节十六进制字符串:
f27b7e75b45c08fa193c8a4a04f81f671b059ce72740786d28f6a8b806c6c551。 - 解密算法:使用
AES-128-ECB模式,并去除PKCS#7填充。
- 在
3. 解题脚本(Exp)
from struct import pack
# AES算法相关函数定义(此处省略,同文档中给出的完整实现)
# 包括:SBOX, INV_SBOX, RCON, bytes2matrix, matrix2bytes, add_round_key,
# inv_sub_bytes, inv_shift_rows, xtime, mul, inv_mix_columns,
# expand_key, decrypt_block, ecb_decrypt
# 核心解密部分
key = pack("<IIII", 873608210, 873608210, 873608210, 559105345)
ct = bytes.fromhex("f27b7e75b45c08fa193c8a4a04f81f671b059ce72740786d28f6a8b806c6c551")
pt = ecb_decrypt(ct, key)
pad = pt[-1]
pt = pt[:-pad]
flag = b"xmctf{" + pt + b"}"
print(flag.decode())
Flag: xmctf{R3a1_w0rld_M47ters}
题目2: ez_uds
1. 核心要点
考察汽车统一诊断服务(UDS)中的0x27 SecurityAccess服务。
2. 协议流程
- 客户端发送
27 01请求种子(Seed)。 - 服务端回复
67 01 <4字节种子>。 - 客户端需根据算法计算出4字节密钥(Key)。
- 客户端发送
27 02 <4字节密钥>。 - 服务端验证密钥,通过则认证成功。
3. 算法与逆向
- 文档中以示例说明:服务端返回
67 01 1A 2E 0E 7F,则种子为0x1A2E0E7F。 - 计算出的密钥为
0x0E91B54D。 - 关键细节:种子和密钥都需要按大端序(Big-Endian)解析和发送。若按小端序处理,会收到否定响应
7F 27 35。 - 解题关键在于逆向出从种子计算密钥的具体算法函数。
题目3: ezFinger [IOT]
1. 核心要点
考察STM32等嵌入式系统固件逆向,识别HAL库函数。
2. 函数分析
sub_8003498:通过直接访问内存地址0x40023808(RCC->CFGR寄存器),检查系统时钟源(SWS位)。逻辑与HAL_RCC_GetSysClockFreq函数一致,用于获取系统时钟频率。sub_8000EC0:通过引脚映射表操作GPIO的BSRR寄存器,实现设置引脚高低电平,对应digitalWrite函数。
3. Flag
xmctf{HAL_RCC_GetSysClockFreq_digitalWrite}
题目4: Hulua [Lua 5.3 字节码]
1. 核心要点
逆向被异或加密的Lua字节码,分析其自定义的校验逻辑。
2. 初步分析
main函数加载用户输入到Lua虚拟机的全局变量user_input中。sub_1400014A4对一段内存数据(起始偏移0x31a00,长度0x3dc)用密钥"hulua"进行循环异或解密,得到Lua字节码。
3. Lua字节码分析
- 使用
Luatool.cn或unluac等工具对解密后的.luac文件进行反汇编。 - 分析发现两个关键常量字符串:
"78 6D 63 74 66 32 30 32 36"-> ASCII转换后为"xmctf2026",作为RC4密钥。- 一段32字节的十六进制字符串,作为目标密文。
- 校验逻辑:对用户输入进行RC4加密(密钥为
"xmctf2026"),然后将结果每个字节与0x66异或,最后与目标密文比较。
4. 解题脚本思路
- 用密钥
"xmctf2026"对目标密文进行RC4解密。 - 将解密结果的每个字节与
0x66异或,得到最终flag。
题目5: hajimi [模型逆向]
1. 核心要点
逆向一个由Tracr编译生成的Transformer模型,理解其内部逻辑以构造正确输入。
2. 文件分析
main.py:加载challenge.pkl.zst中的模型,检查输入(16位,每位为1/2/3/4),将输入送入模型前向推理,并解码输出。challenge.pkl.zst:Zstandard压缩的Pickle序列化文件,内含模型权重、配置和关键的residual_labels。
3. 模型逆向关键
residual_labels是核心,它将Transformer残差流(residual stream)中的维度映射到有意义的变量名(如sequence_map_*,map_*)。- 通过运行模型并
dump特定变量(如map_53,map_52,map_50,map_41)的值,可以发现模型将输入的16个位置分成了4组:map_53 = 1出现在位置:1, 7, 10, 16map_52 = 1出现在位置:2, 8, 9, 15map_50 = 1出现在位置:3, 5, 12, 14map_41 = 1出现在位置:4, 6, 11, 13
- 这四组分别对应数字
1,2,3,4。
4. 构造输入
根据分组规则,确定16个位置分别对应的数字。将得到的数字串进行SHA-256哈希,最终结果即为flag。
题目6: FunPyVM [自定义虚拟机]
1. 核心要点
逆向一个自定义字节码虚拟机(VM),理解其指令集,对两段字节码程序进行反汇编和逆向。
2. 虚拟机架构
基于kernelVM.pyc还原出的虚拟机核心组件:
- 寄存器:
R0(堆块指针),R1(通用),PC(程序计数器)。 - 内存:
heap虚拟堆。 - 关键指令:
0x10 imm:ALLOC分配内存0x11 imm:LOAD加载值到R10x12 imm:STORE存储R1到内存0x52 0x01: 读取输入0x35 imm: 对内存单元异或heap[R0][imm] ^= R10x40 imm:CMPEQ比较0x42/0x43 imm: 条件跳转
3. 第一阶段:opcode.bin
- 读取用户输入的前27字节,进行多轮变换(包括与常量异或、循环移位、与索引运算等)。
- 最终与一个27字节的常量表比较。
- 注意:此阶段只检查前27字节,可通过逆变换得到一段假flag。
4. 第二阶段:ntbase.pyd
- 该文件并非真正的
.pyd,而是另一段VM字节码。 - 同样校验输入的前27字节,但使用不同的变换算法:
- 每字节加10。
- 从下标1开始,每个字节与前一个已处理的字节异或。
- 根据奇偶位,分别加上不同的常数。
- 最终结果与另一组27字节常量表比较。
5. 真实Flag获取
对第二阶段(ntbase.pyd)的算法进行逆变换,得到正确输入:
xmctf{F0n_And_3asyViMGa1v1eF9rY@u}
题目7: 移动的秘密
1. 核心要点
输入经过移位和MD5双重校验,需通过约束求解或爆破得到唯一解。
2. 校验逻辑
- 移位校验:对输入的29个字符,每个字符右移1位(
c >> 1),结果与一个29字节的目标数组target逐字节比较。 - MD5校验:对原始输入(移位前)计算MD5,与固定值
3a22c098710019b31c328a861429d3ad比较。
3. 求解思路
- 由
c >> 1 == target[i]可知,每个输入字符c只有两种可能:2 * target[i]或2 * target[i] + 1。 - 结合flag常用字符集
[a-z0-9_{}],可以大幅缩小每个位置的候选字符范围(通常只剩1-2个)。 - 前几位可确定为
xmctf{。 - 对剩下的可能组合,计算MD5并与目标碰撞,即可得到唯一正确的flag。
题目8: BankGuardian [.NET 混淆与加载器]
1. 核心要点
主程序是一个加载器(Loader),解密并执行隐藏的.NET程序,后者包含真正的flag校验逻辑。
2. 第一阶段:加载器分析
main函数动态解析kernel32.dll。- 从程序
.data段复制一段加密数据(0xE400字节)到新内存。 - 调用解密函数
sub_140003860(实质是ChaCha20流密码)。 - Key/Nonce生成:通过固定种子
2356100023,使用LCG(线性同余生成器)x = (1103515245 * x + 12345) & 0xffffffff生成44字节,前32字节为Key,后12字节为Nonce。 - 解密后得到一个PE文件(.NET程序),写入临时目录并用
dotnet运行。
3. 第二阶段:.NET程序分析
- 程序高度混淆,但核心是几个返回flag片段的方法。
- 关键类
_oGzm5MCMGO4Vr3Mm5lKqB6mpDnh中包含:RealSecret2(Byte ssnKey):从资源MalwareStage2.Resources.Config.bin中读取字节,与ssnKey(值为24)异或,得到"D0tN3t_"。RealSecret3(Byte ssnKey):类似,得到"1nj3ct10n_"。_VsBYyyEZbFsjoKNf29WydChhMSx():返回"Pwn3r}"。
- 程序开头还硬编码了第一部分
"xmctf{R3fl3ct1v3_"。
4. 最终Flag
拼接所有部分:xmctf{R3fl3ct1v3_D0tN3t_1nj3ct10n_Pwn3r}
题目9: ezLanguage [易语言/花指令]
1. 核心要点
程序由易语言编写,并被大量花指令(Junk Code)保护,需要清理花指令后分析。
2. 花指令特征
存在大量形如call $+5; add [esp], xx; retn的短跳转花指令,干扰反汇编器的线性分析。需用IDAPython脚本或手动将特定地址范围(如0x4012DB-0x4012DF)nop掉。
3. 核心逻辑
清理花指令后,可分析出程序的核心是查表替换:
- 程序内置一个字母表(Alphabet,可能为35个字符)。
- 对用户输入的每个字符,在表中查找其位置,经过特定计算(如
(index + 5) % 35)得到新索引,再用新索引从表中取出字符,构成输出字符串。 - 将输出与一个固定的密文字符串比较。
4. 解题方法
- 逆向出完整的字母表和映射关系。
- 对目标密文进行逆映射,即可得到原始输入(flag)。
题目10: MixTielele [Android 多层封装与协议逆向]
1. 核心要点
Android应用多层混淆,Java层为壳,核心逻辑在Native层和隐藏的Dex中,涉及Protobuf序列化和自定义加密。
2. 逆向流程
- Java层:仅作包装,调用native方法
EncTitlele。 - Native层 (
libmixtitlele.so):负责将内层登录串封装成外层JSON。 - 隐藏逻辑:
libflutter.so是伪装文件,内含第二个Dex,包含真正的登录构造逻辑。
3. 登录协议分析
- 核心数据:一个Protobuf消息,包含两个字段:
user(字符串) 和isHacker(布尔值)。 - 目标:以
admin身份且isHacker=true登录。 - 示例:
user="admin", isHacker=true对应的Protobuf编码为:0A 05 61 64 6D 69 6E 10 01。
4. 加密流程
- 第一层加密(Java):对Protobuf明文进行自定义异或流加密(固定种子),然后Base64编码,得到
inner_login。 - 第二层加密(Native):
- 随机生成16字节AES Key。
- 用RSA公钥加密该AES Key,得到
a1(Base64)。 - 用该AES Key以
AES-CBC模式(IV为16个\x00,PKCS7填充)加密inner_login,得到b2(Base64)。 - 构造外层JSON:
{"a1": a1, "b2": b2},POST到/login。
5. 解题步骤
- 构造正确的Protobuf数据(
admin,true)。 - 实现第一层自定义异或流加密,得到
inner_login。 - 模拟第二层Native加密流程,生成最终的POST数据。
- 向服务器发送请求,获取flag。
题目11: Disguise [VM、SM4、文件隐藏]
1. 核心要点
两阶段程序:第一层给出假flag并隐藏第二层PE;第二层是一个实现魔改SM4算法的小型VM。
2. 第一层程序
main函数:校验输入input[i] - i与常量比较,可得假flag。sub_412C10函数:从.data:0x41C000开始,读取DWORD表,将每个DWORD的低字节与0x07异或,提取出0x15000字节的第二层PE文件。
3. 第二层程序与VM
- VM结构:典型的取指-译码-执行循环,有私有内存空间、指令分派(Switch-Case)。
- 算法识别:通过分析常量(FK, CK, S-Box)和控制流,确定VM实现了魔改的SM4分组密码算法。
- 轮密钥扩展:使用种子密钥
"We1c0me_t0_xmctf"与FK异或,并通过32轮扩展生成轮密钥。 - 分组加密:将48字节输入分成3块,每块进行32轮SM4加密。
- 轮密钥扩展:使用种子密钥
- 最终比较:加密结果与
.data:0x421018处的12个DWORD(48字节)比较。
4. 解密获取Flag
- 提取密文(0x421018处,48字节)。
- 使用密钥
"We1c0me_t0_xmctf",按照SM4算法进行解密(注意轮密钥逆序使用)。 - 连接3个解密后的明文块,得到最终flag。
题目12: Oracle Eye [模型逆向、频域隐写]
1. 核心要点
程序使用一个ONNX模型进行“人脸识别”,但模型实际在频域(DCT)埋设了后门触发器(Trigger),正确触发才能获得flag。
2. 输入输出
- 输入:64x64的PGM(P5)格式灰度图像。
- 输出:分类ID、指纹(fingerprint)、触发分数(trigger_score)。
- Flag条件:分类为“神谕”(ID=4)且触发分数接近1,才能通过深层验证。
3. 模型逆向分析
- 模型结构:
- 对输入图像做2D-DCT(离散余弦变换),转换到频域。
- 仅检查频域矩阵中四个固定坐标点的值:
(5,5),(10,10),(15,15),(20,20)。 - 计算这四个点的值与目标值的接近程度,通过公式生成
trigger_score。当四点都命中时,trigger_score ≈ 1。 - 模型后半部分的卷积网络(Conv)等结构在触发后不起决定作用。
- 后门机制:当
trigger_score高时,模型输出的fingerprint会逼近一个固定的secret_vector。
4. 构造攻击图像
- 创建一个64x64的全零频域矩阵
C。 - 在四个坐标点
(5,5),(10,10),(15,15),(20,20)上填入模型期望的目标浮点数值。 - 对矩阵
C进行逆DCT变换,得到空间域图像I。 - 对图像
I整体加0.5偏移并裁剪到[0,1]范围,保存为PGM格式。 - 将此图像输入程序,即可获得高触发分数和正确分类,从而得到flag。
Flag: xmctf{Y0u_H4v3_Tru1y_S33n_Th3_0r4c13_1n_Th3_N0is3}
题目13: easyre [控制流平坦化、网络协议]
1. 核心要点
分为客户端(client.exe)和服务端(server.exe)。服务端被控制流平坦化混淆,校验逻辑通过网络通信进行。
2. 程序角色
client.exe:交互前端,接收用户输入,打包发送给127.0.0.1:5566,显示结果。server.exe:真正的校验器,监听5566端口。
3. 通信协议
- 通用格式:
[14字节大端长度] + [RC4加密的数据]。 - 第一阶段 (Username):
- 客户端发送加密后的用户名。
- 服务端RC4解密后,计算其MD5(32位小写hex)。
- 与目标MD5值比较。目标值为:
4772e2e6b3f5c2a1f2b2f3c4d5e6f7a8。 - 原像:该MD5对应的用户名为
polaris。
- 第二阶段 (Serial/Flag):
- 用户名正确后,客户端发送加密后的serial。
- 服务端RC4解密后,与内部硬编码字符串直接比较。
- 目标字符串:
c90a6b4f3e2d1a0b9c8d7e6f5a4b3c2d。
4. 加密算法识别
通过API断点(BCryptOpenAlgorithmProvider)分析,确认使用RC4和MD5算法。
5. 解题步骤
- 运行
server.exe。 - 运行
client.exe,第一轮输入用户名:polaris。 - 第二轮输入serial/flag:
c90a6b4f3e2d1a0b9c8d7e6f5a4b3c2d。
题目14: ez_uds_plus
本题是ez_uds的升级版。在已掌握UDS 0x27服务流程和第一层算法的基础上,需要进一步分析流量包或程序逻辑,找出第二层(level2)所使用的密钥生成算法。密钥可能为Polaris_ctf_2026,但需验证其具体使用方式。
题目15: ShakeLife
题目要求输入长度为43的可打印ASCII字符串。程序的核心是一个自定义的变换函数sub_41D9,它使用一个由32字节ASCII常量3ff653606890a0591e204093a0d59202初始化生成的复杂状态上下文(涉及sub_3BF3),对输入进行多轮混合、查表、移位、异或等操作。最终将43字节输出与目标密文8da4efed737854927e5ff15ac2c310e6c27b3012d2a78cc0cccb04c2d766b6437f17b25b85400070899b8f比较。解题关键在于逆向分析sub_41D9、sub_694B、sub_64B8等函数的详细操作,实现逆变换。