CTF游戏逆向入门

Unity游戏逆向

Unity3D最大的一个特点是一次制作,多平台部署,而这一核心功能是靠 Mono 实现的。可以说一直以来 Mono 是 Unity3D 核心中的核心,是 Unity3D 跨平台的根本。这种形式一直持续到 2014 年年中,Unity3D 官方博客上发了一篇 “The future of scripting in unity ” 的文章,引出了 IL2CPP 的概念,这种相比 Mono 来说安全性更强的方式。

1.Mono 打包逆向

Mono 打包方式逆向比较简单,其核心代码都在 game/data/Managed/AssemblyCSarp.dll 这个 dll 文件中,使用 dnSpy 进行分析,几乎就是可以明文随便篡改。

[BJDCTF2020]BJD hamburger competition

这个Mono还是比较好识别出来的, 游戏名_Data -> Managed -> AssemblyCSarp.dll

将AssemblyCSarp.dll放入dnSpy里面进行分析,找到ButtonSpawnFruit(),里面有md5和sha1,点进去进行交叉引用

可以推测,每一个菜都对应着一种操作,这与上菜的顺序有关,按照一定的顺序加菜,使得 str 的 Sha1与那一串相等,然后将 str 进行md5加密,但是这里要注意,要看看Sha1加密和md5是否都是标准的。

Sha1和 md5都是标准的,但是md5是取前20个字符,X表示的是大写16进制,x表示小写16进制

1
2
3
4
5
import hashlib
result = hashlib.md5("1001".encode()).hexdigest().upper()
flag=result[0:20]
print(flag)
##B8C37E33DEFDE51CF91E

[SCTF2019]Who is he

同样的方法,将AssemblyCSarp.dll放入dnSpy里面进行分析,里面密文和key都有

flag就是 EncryptData 通过 Decrypt 来解密得到的。

这个解密过程是先进行base64解密,然后再进行DES-CBC模式解密,其中找不到iv是因为key=iv=”1234”。

需要注意的是:dnSpy里的字符串是用UTF-16编码存储的,也就是宽字节(双字节)

比如:字符串”1234”

它在内存里面其实是:

1
31 00 32 00 33 00 34 00

每个字符后面都跟一个00h,也就是说每个字符占两个字节。

写出脚本

1
2
3
4
5
6
7
8
9
import base64
from Crypto.Cipher import DES
cipher="1Tsy0ZGotyMinSpxqYzVBWnfMdUcqCMLu0MA+22Jnp+MNwLHvYuFToxRQr0c+ONZc6Q7L0EAmzbycqobZHh4H23U4WDTNmmXwusW4E+SZjygsntGkO2sGA=="
enc=base64.b64decode(cipher)
key=iv=b"1\x002\x003\x004\x00"

des=DES.new(key,DES.MODE_CBC,iv)
print(des.decrypt(enc).decode('utf-8'))
##He_P1ay_Basketball_Very_We11!Hahahahaha!

本以为美美结束了,但是这个是错的flag。接下来需要使用 Cheat Engine,它是由Dark Byte开发的免费开源游戏内存修改工具,主要运行于Windows平台。 其核心功能是通过扫描并修改进程内存数据调整游戏参数(如生命值、弹药数),集成调试器、反汇编器、变速器及Direct3D操作工具。官网:https://www.cheatengine.org/

个人不建议在官网下载哈,我看网友说好像会强制下载流氓软件……

CE的使用教程:https://www.cnblogs.com/LyShark/p/10799926.html

出题人应该隐藏了真实的密文和key,这些都会放在dll里面,Managed里面的dll是所有的代码。

所以在Managed文件夹里面按照时间排序,看最近的日期就能知道哪些是可疑的。

然后打开CE和那个游戏,在CE里选择这个游戏进程,会看到出现了Mono,点击它选择分析Mono。就会看到这些dll,找到刚才可疑的3个dll与AssemblyCSarp.dll对比。

两者对比会发现,真正的密文和key都在UnityEngine.UmbraModule里面。我们可以换成 Mono ->.Net Info进行分析,在这里面找到UnityEngine.UmbraModule程序集,里面有个.Main类,打开看看。

知道**key=iv=”test”**。它是静态的,可以直接读取值,但是密文是动态的

找到存放密文的变量,选中后点击Lookup 按钮,它就会自动查找所有可能的内存指针然后读取里面的内容。

写出脚本

1
2
3
4
5
6
7
8
9
import base64
from Crypto.Cipher import DES
cipher="xZWDZaKEhWNMCbiGYPBIlY3+arozO9zonwrYLiVL4njSez2RYM2WwsGnsnjCDnHs7N43aFvNE54noSadP9F8eEpvTs5QPG+KL0TDE/40nbU="
enc=base64.b64decode(cipher)
key=iv=b"t\x00e\x00s\x00t\x00"

des=DES.new(key,DES.MODE_CBC,iv)
print(des.decrypt(enc).decode('utf-8'))
##She_P1ay_Black_Hole_Very_Wel1!LOL!XD!

第二种方法

直接搜索字符串”Emmmmm”,这个是输入正确的flag后弹出的弹窗里的部分提示语

经过扫描会看到只有两个地址有关,都分别定位内存中的位置,可以看到这两个真假密文和key

第三种方法:使用uniref框架解出真的密文

uniref框架:https://github.com/in1nit1t/uniref

uniref 是一个辅助分析 Unity 应用的框架。它可以帮助我们取获取 Unity 应用中的类、方法、成员变量等的反射信息,也可以实时地查看和操作它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from uniref import WinUniRef

ref = WinUniRef("Who is he.exe") #传进程的id,或者exe文件名

main = ref.find_class_in_image("UnityEngine.UmbraModule","UnityEngine.UmbraModule.Main")
# print(main)
key = main.find_field("encryptKey")
print(key.is_static()) #返回True是静态的,可以直接访问value
print(key.value)
enc = main.find_field("EncryptData")
print(enc.is_static()) #返回false是动态的,不能直接访问value

addresses = main.guess_instance_address()
print(list(map(hex,addresses)))
for addr in addresses:
enc.set_instance(addr)
print(hex(addr),enc.value) #打印出所有的可能内存地址及其内容

2.IL2CPP 打包逆向

IL2CPP 将游戏 C# 代码转换为 C++ 代码,然后编译为各平台 Native 代码。现在市面上的很多游戏基本上都是用Il2cpp的方式打包的。

global-metadata.dat 文件里面记录了所有的 C# 中的类名、方法名、属性名、字符串等地址信息。目前对于il2cpp逆向都以Android中的.so文件居多。

IL2CPP 打包的应用在逆向前会多一步操作,即使用项目 Il2CppDumper 和应用程序目录中的 global-metadata.datGameAssembly.dll 来获得 dll 的符号信息。之后再通过 IDA 加载 GameAssembly.dll 及符号信息来进行逆向分析(常规 C 语言逆向)。

[MRCTF2021]EzGame

打开游戏先玩一玩,好玩爱玩……

按Esc出现任务单,要求:

  • 吃到所有的星星
  • 找到外星人
  • 吃到饼干
  • 回到家
  • 不能死太多次

这些全部完成才能得到flag。

打开后在Data文件夹里看到 il2cpp_data ,这就是il2cpp打包的unity游戏。

需要的工具:Il2CppDumper 下载使用:https://github.com/Perfare/Il2CppDumper

命令

1
Il2CppDumper.exe GameAssembly.dll的路径(第一个参数) global-metadata.dat的路径(第二个参数) 输出的路径(第三个参数)

这样就是正确执行了。output文件夹里面会有一些文件

dump.cs:这个文件会把 C# 的 dll 代码的类、方法、字段列出来

IL2cpp.h:生成的 cpp 头文件,从头文件里可以看到相关的数据结构

script.json:以 json 格式显示类的方法信息

stringliteral.json:以 json 的格式显示所有字符串信息

DummyDll:进入该目录,可以看到很多dll,其中就有 Assembly-CSharp.dll 和我们刚刚的 dump.cs 内容是一致的

接下来使用IDA来加载 GameAssembly.dll ,但是这个题加了个Themida的壳,很难脱,这个壳是一款商业级的壳,特点是保护强度高,经常被用来保护游戏、外挂、商业软件。

其实到这里这个常规的方法就行不通了,下面是拿这道题来演示一下,常规的流程是什么。

选择 File -> Script file 去应用 ida_with_struct_py3.py文件。

然后选择该文件

再导入头文件

然后就来等待它分析就好了。那这种方法行不通,换CE来看看。

与上一题相同的方法,看到GetFlag很容易猜想到主要逻辑就在这。

其他条件都可以改成True,但是tokenGet如果直接改成105是不行的,看到下面的EatTokenUpdateKey方法,(这里也是看过别人的wp才明白的)就应该猜到了每调用一次这个方法,上面的token会加一,这个方法里面应该还有最后解密的key也会随着调用的次数而改变。这样如果直接改上面的值,key还是原来的错误key,flag也就没有被解密出来。我们可以直接右键选择调用,但是这样要一直点105下。于是我们用uniref框架来写。

使用时要和CE一样,确保游戏还开着。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from uniref import WinUniRef

ref = WinUniRef("GameHack.exe")
get_flag = ref.find_class_in_image("Assembly-CSharp.dll","Platformer.Flag.GetFlag")
# print(get_flag)
eat_cookie = get_flag.find_field("eatCookie")
find_alien = get_flag.find_field("findAlien")
go_home = get_flag.find_field("goHome")

# print(eat_cookie.value)

eat_cookie.value = True
find_alien.value = True
go_home.value = True

update = get_flag.find_method("EatTokenUpdateKey")
print(update.is_static())
for i in range(105): #调用105次
update()

成功得到flag!🤗

XYCTF2024-baby unity

这个游戏一打开仅仅就是一个flag验证的程序,逻辑应该比较简单。

使用Il2CppDumper提取文件失败了,这个错误 大概率是 GameAssembly.dll 有壳

使用查壳工具进行查壳,这个是UPX壳

直接使用upx -d 进行脱壳,成功脱壳!

然后再次进行尝试,脱过壳后就成功了

使用 IDA 加载 GameAssembly.dll 及符号信息,方法上一题演示了。此刻需要耐心等待

那先使用dnSpy来看看output文件夹里的Assembly-CSharp.dll

等IDA分析好后,直接去搜索这个CheckkkkkkkkkkFlag函数

这个就是主要的加密逻辑,先base64加密,再异或

密文如上,直接写出脚本

1
2
3
4
5
6
7
8
9
10
11
import base64
enc="XIcKYJU8Buh:UeV:BKN{U[JvUL??VuZ?CXJ;AX^{Ae]gA[]gUecb@K]ei^22"
flag=""
plain=""
for i in range(len(enc)):
plain+=chr(ord(enc[i])^0xF)
print(plain)
flag=base64.b64decode(plain)
print(flag.decode())
#WFlDVEZ7Mzg5ZjY5MDAtZTEyZC00YzU0LWE4NWQtNjRhNTRhZjlmODRjfQ==
#XYCTF{389f6900-e12d-4c54-a85d-64a54af9f84c}

Android游戏逆向

无论是Android或者是Windows都能够使用unity引擎进行开发。安卓unity游戏的核心逻辑一般位于 assets\bin\Data\Managed\Assembly-CSharp.dll

BUU-[MRCTF2020]PixelShooter

使用jeb打开这个apk文件,直接在找到assets\bin\Data\Managed\Assembly-CSharp.dll这个位置打开。

然后可以按ctrl+F进行筛查,有点考察我的眼力了🥱

普通C#程序

使用 .NET 框架提供的编译器可以直接将源程序编译为 .exe 或 .dll 文件,但此时编译出来的程序代码并不是 CPU 能直接执行的机器代码,而是一种中间语言 IL(Intermediate Language)的代码。

这个时候再放入IDA里面进行分析,就不太行了。放入DIE里面查看,出现如下

这可以使用新的工具dnSpy,它是C#逆向的好帮手。

dnSpy是一个.NET调试器和反编译器,可以在无源码的情况下,进行代码调试和修改。遇到此类题目,我们可以把它放到dnSpy里面进行分析。

[ISC 2016]Classical CrackMe

运行一下看看

把它放到dnSpy里面进行分析,但是发现本该显示form1的字样的地方成了一串字符,这就加壳混淆了

这个时候需要使用de4dot来帮助,它是是一个很强的.Net程序脱壳,反混淆工具,支持对于以下工具混淆过的代码的清理:如Xenocode、.NET Reactor、MaxtoCode、Eazfuscator.NET、Agile.NET、Phoenix Protector、MancoObfuscator 、CodeWall、NetZ .NET Packer 、Rpx .NET Packer、Mpress .NET Packer、ExePack.NET Packer、Sixxpack .NET Packer、Rummage Obfuscator、Obfusasm Obfuscator、Confuser1.7、Agile.NET、Babel.NET、CodeFort、CodeVeil、CodeWall、CryptoObfuscator、DeepSea

Obfuscator、Dotfuscator、 Goliath.NET、ILProtector、MPRESS、Rummage、SmartAssembly、Skater.NET、Spices.Net 等。

使用方法:直接 de4dot exe所在的位置 就好,会生成一个xxx-cleaned.exe

然后把新的exe再放入dnSpy里面进行分析

这下就好了,逻辑清晰可见,那串密文是base64加密

PCTF{Ea5y_Do_Net_Cr4ck3r}

[FlareOn5]Ultimate Minesweeper

先查壳,无壳32位,是net写的。我们试着运行一下程序,发现是个扫雷游戏,而且这个只要扫到雷就会弹出失败的弹窗然后退出游戏。

把它放到dnSpy里面进行分析,能找到MainForm部分,这里就是重要部分

里面有GetKey方法,这应该就是flag的生成逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
private string GetKey(List<uint> revealedCells)
{
revealedCells.Sort();
Random random = new Random(Convert.ToInt32(revealedCells[0] << 20 | revealedCells[1] << 10 | revealedCells[2]));
byte[] array = new byte[32];
byte[] array2 = new byte[]
{
245,
75,
65,
142,
68,
71,
100,
185,
74,
127,
62,
130,
231,
129,
254,
243,
28,
58,
103,
179,
60,
91,
195,
215,
102,
145,
154,
27,
57,
231,
241,
86
};
random.NextBytes(array);
uint num = 0U;
while ((ulong)num < (ulong)((long)array2.Length))
{
byte[] array3 = array2;
uint num2 = num;
array3[(int)num2] = (array3[(int)num2] ^ array[(int)num]);
num += 1U;
}
return Encoding.ASCII.GetString(array2);
}

分析后发现,这从输入 revealedCells生成一个动态伪随机数种子,用于扰乱一个固定的字节数组 array2,最后返回扰乱后的结果字符串,想要逆向过去还是很难的。往上翻看,发现了扫雷的校验逻辑

上面红框是扫到雷后,弹出失败的弹窗并退出游戏,下面的是将所有非雷块都扫了而且没扫到雷就能通过这个游戏,会调用GetKey方法获取flag弹出胜利弹窗。

第一种方法:我们只需要把上面扫到雷的失败弹窗部分的代码(也就是红框框住的)都删掉,然后保存,就能一直扫雷不会弹出失败弹窗,也不会退出了。然后记住正确的位置,再在原来的程序上点击就能顺利通关了。

第二种方法:

TotalUnrevealedEmptySquares是代表着剩下还没被揭开的”非雷”方块数量

点进去,然后下翻找到初始化棋盘部分,如下所示

MinesPresent → 存储“是否有雷”。 MinesVisible → 存储“是否被翻开”。 MinesFlagged → 存储“是否被插旗”。

选中这部分后,右键选择编辑方法

true代表可视,false代表不可视。初始化全部都是false。这个是遍历所有方块都改成true。然后点击编译就🆗了。

最后全部保存。再点开程序就是这样的。