NSSCTF 2025 re wp
NSSCTF 2025 re wp
ez_tea
ez_tea不ez
没改附件版本:
动调有几个地方需要绕过
对isDebuggerPrensent进行交叉引用,对isDebuggerPrensent的绕过方法基本是改zf标志位,此外还有一个地方绕过需要把0x70改成0x71修改寄存器
此题别用patch方法,因为上面那个0xcccc貌似是crc校验
同时看到这有个exit也交叉引用下
这里也有个exit
绕过后经过动调发现,这个方法会对key进行修改,我们可以断再fmt这个方法上就可以发现dst的地址是key的地址
这块如何修改的key? 是tea_encrypt_wrapper这个方法的机器码值+78然后patch到了key
另一种发现hook的方法,当你写了解密脚本发现不对,可以到处交叉引用看看
可以返现这个方法被神奇的其它地方交叉引用了
最后写解密脚本
1 | delta = 0x0D000721 |
flag (latin-1): NSSCTF{13ce5888-01b4-4287-8593-eb975ab6cf{±F\¡Æ
有点怪
看了下密文可能是4个0000000字节搞的鬼
可以爆破,一开始有一个爆破思路因为是诸字节爆破,我们可以使用测信道攻击,因为诸位比较,提前一位退出和新比较一位退出的时间上有区别,我们计算这个时间长度来判别即可
后来验证不行
第二个方法是爆破,因为一开始没看出来只觉得有点眼熟,后来看出来了是个uuid,对比下就知道还少两个字节,因此其实只有两个字节需要爆破,再加上最后的}就行了
爆破exp:
1 | import itertools |
[+] Hit: NSSCTF{13ce5888-01b4-4287-8593-eb975ab6cf6a}
runner
解法1:跑图
不如直接跑图快
QRazyBox - QR Code Analysis and Recovery Toolkit
解法2:分析dat
非预期
直接看globalxxx.dat
因为assertstudio解出来没有二维码资源说明是动态生成的,一般在这个文件中,直接滑能找到大面积的01,写个脚本可以提取恢复
解法3:静态分析
太详细的步骤没有保存
关于il2cppDump的使用可以看unity引擎基于Windows下的il2cpp逆向初探——以CTF赛题为例-先知社区
一般pc逆向的il2cpp的unity,主逻辑在GameAssembly.dll里,但是发现有upx,直接脱壳失败
看了下发现了,居然魔改了
用010editor打开然后把里面的NSS标志恢复成UPX就行了
然后直接脱壳,拖入ida即可静态分析
静态分析大部分没有函数名
因为il2cpp的unity函数名等资源都在globalxxx.dat中
我们直接用il2cppDump这个工具
Perfare/Il2CppDumper: Unity il2cpp reverse engineer
执行Il2CppDumper.exe GameAssembly.dll 这类命令
即可dump出一个dump.cs和一堆dll
比较重要的的就这个Assembly-CSharp.dll
直接dnspy64打开
我们直接ctrl+alt+f搜索qrcode类似这种
可以搜到
然后这几个地址比较重要可能是构建二维码的地方
然后把dump出来的dump.cs和script.json导入(用于恢复函数名和符号,这个过程可能比较久,耐心等待)
具体怎么导入不介绍,可以看il2cppdump的使用,就是上面的那个先知的链接
下面的截图是我的分析过程,但是做题的时候没保存,所以我后面重新截了,但是做题时其实是有函数名的
上面几个地址此时有用了
我们可以依次跳转到这几个函数分析
具体我后面分析出来是这样的:
生成二维码的核心在 QRCodeBuilder(GameAssembly.dll 中的 sub_1808118B0 和 sub_180810F90)。构造函数里把一个 29×29 的常量矩阵塞进 qrMatrix,ConstructQRCode 再按矩阵值实例化黑/白模块。借定位常量矩阵
QRCodeBuilder::.ctor 调用 sub_181652960,后者沿 IL2CPP 元数据把字段初值拷到 qrMatrix。
使用采样脚本扫描 global-metadata.dat 的默认值段,发现长为 29×29、元素仅 0/1 的块起始索引为 19740(整数视图),即矩阵。
QRCodeBuilder.qrMatrix 是 int[,],初值并不在 Assembly-CSharp.dll 里,而是存放在 IL2CPP 的全局元数据段(相当于 C# 的 static readonly 初始化块)中。构造函数 GameAssembly.dll:0x1808118B0 调用 sub_181652960,该函数沿元数据读取字段默认值,把一块连续内存写进 qrMatrix。
矩阵的出处与定位
QRCodeBuilder.qrMatrix 是 int[,],初值并不在 Assembly-CSharp.dll 里,而是存放在 IL2CPP 的全局元数据段(相当于 C# 的 static readonly 初始化块)中。
具体来说:
- script.json让我们知道 QRCodeBuilder 的字段顺序与 qrMatrix 的类型,从而确定它使用了字段默认值(FieldRVA)。
- global-metadata.dat 头部第 18、19 项是 “Field & Parameter Default Value Data”的偏移与长度。扫描这一段时我查找 841 (=29×29) 个 32 位整数全部为 0/1 的区块;正好在偏移 data_offset + 19740*4 处找到独一无二的块,这就是 qrMatrix 的初始内容。之所以能定位到索引 19740,是因为 Il2CppDumper 的 FieldRVA
sub_181652960 为什么判定为把字段默认值抄进 qrMatrix?
在 GameAssembly.dll:0x1808118B0(QRCodeBuilder::.ctor)里,r9 被赋为 qrMatrix,随后调用 sub_181652960(r9, qword_182F726A8, …)。
反编译 sub_181652960 可见,它先调用 sub_181633EE0 / sub_18180BD40 检查数组状态,之后求得一个临时数组并调用 sub_181652A40。
sub_181652A40 直接把两个指针传给 sub_1805CA600,而 sub_1805CA600 里先取 arg1 的数组句柄,再调用 sub_180450230(arg2)——这是 Unity/IL2CPP 的“读取 FieldRVA 数据”例程。它往下会调用 sub_180558480 → sub_1805584E0 -> (中间可能还有两个函数) → sub_180769120,正是枚举 global-metadata.dat 元数据并按字段索引取默认值的标准流程。因此可以确认 sub_181652960 就是在把元数据里预存的 int[,] 常量搬到 qrMatrix。
data_offset 的来源与数值
global-metadata.dat 头部是 32 个 uint32。第 18、19 个元素分别是 FieldAndParameterDefaultValueDataOffset 和 FieldAndParameterDefaultValueDataCount。
用 Python 解出的值:
field_and_param_default_data_offset = struct.unpack_from(“<I”, meta, 184)[0] # = 1782496
field_and_param_default_data_size = struct.unpack_from(“<I”, meta, 19*4)[0] # = 89632
这就是我代码里 data_offset 的来源,它不是扫描出来的,而是直接从元数据头读取的标准字段。
为什么定位到 data_offset + 19740*4
- 19740 是 qrMatrix 的 FieldRVA 在默认值数组中的索引,Il2CppDumper 会在 script.json 的 ScriptMetadataMethod / ScriptMethod 等条目里把同一块地址列出来。
- 我第一遍是扫整段默认值数据,把所有“连续 841 个 uint32 只含 0/1”的片段列出来,确实只有索引 19740 附近满足条件(对应 29×29 阵列)。之后把这个索引写死在脚本里,方便重复利用。
- 结合 sub_181652960 的调用链,就能确认这块数据正好就是构造函数读取的二维码矩阵。
我怎么知道怎么知道第 18、19 个元素分别是 FieldAndParameterDefaultValueDataOffset 和FieldAndParameterDefaultValueDataCount指向二维码的地方
那两个数不是随便猜的,而是来自 Unity/IL2CPP 的公开结构:global-metadata.dat 文件开头固定是 32 个 uint32,顺序在 Unity 官方的 Il2CppGlobalMetadataHeader(il2cpp/libil2cpp/vm/GlobalMetadata.h)里写得一清二楚。第 18、19 个成员就叫 fieldAndParameterDefaultValueDataOffset/Count,说的是“字段默认值与参数默认值的数据区”在文件里的起始位置和长度。
二维码为什么会在这里?因为 QRCodeBuilder.qrMatrix 是个 int[,] 字段,Il2CPP 在生成原生代码时把它当成“带初值的数组字段”,这类字段的初始内容会被落到 FieldAndParameterDefaultValueData 段。构造函数 sub_181652960 → sub_1805CA600 → sub_180450230 → sub_180558480 … 的链条正是 Il2CPP 的“从 metadata 里读FieldRVA,拷贝默认值到托管对象”逻辑,说明 qrMatrix 的实际数据就存放在该段。
所以流程是:
- 按官方结构读取 header,得到 fieldAndParameterDefaultValueDataOffset(=1782496)和 fieldAndParameterDefaultValueDataSize。
- 在这段数据里寻找 29×29(=841)个 uint32 全部为 0/1 的块;Il2CppDumper 给出的 FieldRVA 索引 (script.json) 也会指向同一个位置,于是能确认这就是 qrMatrix 的初始值。
- 再用从 metadata 里取出的偏移(例如 offset = data_offset + 19740*4)去读取整块数据,生成二维码矩阵。
换句话说:第 18、19 个字段并不是我推出来的,而是 Unity 在元数据格式里就定义好的;二维码位于那段,是因为 QRCodeBuilder.qrMatrix 属于“带默认值的字段”,Il2CPP 的写法就是把这些默认值统一放在这个段里
exp:
1 | import struct |
扫出的flag
NSSCTF{f7c244c0-5f43-4829-83da-ebbc713324f5}
解法4:CE动调+半静态
其实跟静态分析没啥区别了,不知道有没有更好的方法
就是之前静态分析完,基于这些知识我们去ce下断点dump这个矩阵
之前的静态分析已经知道了偏移和 RVA。
要拿到在 Unity 游戏里,QRCodeBuilder 脚本实例中的字段:
1 | this + 0x48 → qrMatrix (int[,]) |
构建二维码时,这个 qrMatrix 会被填满,然后立刻被释放。
所以流程是:
- Attach 游戏进程
- 找到 GameAssembly.dll 基址
- 在 ConstructQRCode (0x810F90) 附近下断
- 查看 this 指针
- 顺着 this+0x48 拿出矩阵地址
- Dump 出矩阵内容
第一步:Attach 游戏进程
先启动游戏,但不要进入地图
- 打开 CE;
- 点左上角;
- 成功后 CE 窗口左下角会显示「附加到进程」,选attach。
拿 GameAssembly.dll 基址
- 在 CE 里打开「内存视图」(Memory View);
- 菜单栏
View → Memory Regions; - 找到一行名字含有
GameAssembly.dll; - 记下它的起始地址(如7FF864330000 );这就是模块基址。
第三步:跳到 ConstructQRCode
RVA 是 0x810F90。在 CE 的汇编窗口(Memory View)里按:
1 | Ctrl+G → 输入 GameAssembly.dll+810F90 |
这就是函数入口(QRCodeBuilder.ConstructQRCode())。
第四步:下断点
右键第一条指令 → 「断点 → 在此处设置断点」。
然后:
- 回到游戏;
- 做那个动作让二维码生成(Enter);
- 游戏会在断点处暂停。
第五步:看 this 指针(RCX)
在 CE 的 寄存器窗口:
- 找
RCX。在 IL2CPP 下,RCX通常是this指针(对象指针)。 - 右键
RCX→ 「在内存中查看此地址」。
这就是 QRCodeBuilder 实例。
第六步:看字段偏移
在「内存视图」里:
1 | this + 0x20 → blackMaterial |
直接在 CE 里:
右键 → 「转到地址」;
输入:
1
RCX+48
看这一处存的 8 字节内容,就是一个指针(qrMatrix 对象地址)。
可以看到下面全是01字节
第七步:跟进 qrMatrix
右键这 8 字节 → 「在内存中查看此地址」。这里是 int[,] 结构体:
一般是:
1 | 00: vtable指针 |
(不同 Unity 版本可能略有不同)
能看到成片的 00 00 00 00 / 01 00 00 00 模式,这就是二维码的黑白格。
第八步:dump
save指定内存区域
这个49 03 是元素总数 0x349 = 841(小端的 49 03 00 00),841 是 29×29,说明这是一个 29×29 的二维码矩阵
我们从这四个字节后开始dump
定位到 49 03 00 00 这一段的地址是 **200D898A018**。这说明这是二维码矩阵数据前面的计数字段(841个元素)。
我们要从它后面紧接着的 01 00 00 00 ... 开始 dump,长度大约 841 × 4 字节。
自己算下,然后填进去就可以dump了
把ce的头去掉就是这样的
exp:
1 | import numpy as np |



































