Unity
Unity 游戏的核心逻辑通常位于 C# 编译的 DLL(Mono)或 C++ 编译的 IL2CPP 文件中
编译方式
| 方式 | 脚本后端 | 包含文件 | 难度 |
|---|---|---|---|
| Mono | C# → IL → .NET DLL | Assembly-CSharp.dll 等 |
低 |
| IL2CPP | C# → IL → C++ → 原生代码 | libil2cpp.so / GameAssembly.dll + global-metadata.dat |
较高 |
实战案例
Mine Sweeping(Unity)

打开附件可以看到这是一个 Unity 游戏,Unity C# 题的核心逻辑通常都在 Mine Sweeping_Data/Managed/Assembly-CSharp.dll
直接用 dnSpy 反编译,其中
1 | Elements 单个格子的逻辑 |

在 Elements.Awake() 中有一段非常关键的判断
1 | private void Awake() |
也就是 “雷” 的生成逻辑是先异或,再减 233,再整除 2333
思路一是通过这一步直接逆向写脚本生成所有雷所在的位置,但是很麻烦,我们可以直接 patch 游戏逻辑
比如修改 OnMouseUpAsButton()
1 | private void OnMouseUpAsButton() |
我们可以完全取消输赢判断
1 | using System; |

最后保存模块

这样子最后会得到一张二维码图片,但还是需要手动点击

熟悉这个流程过后我们可以直接修改 Elements.Awake() 逻辑
1 | private void Awake() |

最后得到 flag:de1ctf{G3t_F1@g_AFt3R_Sw3ep1ng_M1n3s}
TankGame(Unity)
附件目录名提示使用 Mono 反编译,直接用 dnSpy 反编译 /Managed/Assembly-CSharp.dll
找到了 WinGame() 函数

1 | public static void WinGame() |
也就是说 flag 取决于地图状态,接下来去看初始地图

但是 WinGame() 检查的是游戏结束时的地图,两者可能不同,比如:坦克开炮,子弹撞墙
在代码中有这样一个条件 if(nDestroyNum == 4 || nDestroyNum == 5)
所以接下来的思路就是找可以被破坏的位置,查看 MapCreation.Initmap()

这里唯一 100% 确定的是 if(num != 8),当 MapState 的值为 8 时,不创建任何游戏物体。即空地
前面代码没有读取 MapState,而是直接 this.item[6] 生成一圈东西,总共四次,应该对应着边界
1 | for (int i = -11; i < 12; i++) |
继续分析 CreateItem 虽然传入了 id,但是没用

继续分析其他的类,在 Bullect 类中找到核心逻辑
1 | // 当子弹击中墙壁时 |
这里 MapManager.nDestroyNum++;,所以玩家需要摧毁 4 个墙或者摧毁 5 个墙
继续往后看
1 | // 当子弹击中非墙壁物体(如敌人)时 |
现在所有变化规则确定,打墙和打敌人都会增加已摧毁物体数量
解题思路就是将 21×17 的二维地图压缩成一维字符串,预先记录所有可破坏 Wall 的位置,通过枚举被摧毁的 4/5 个 Wall(以及 Heart+Wall组合),直接修改对应字符生成候选 MapState,计算 SHA1 与目标值比对,找到正确地图后再计算 MD5 生成 flag
1 | import hashlib |
