【翻译】骇入Unity游戏 | Unity游戏破解 | C#注入

来源:https://www.hypn.za.net/blog/2020/04/11/hacking-unity-games/
发布日期:2020年4月11日

查看原文

In this post I'm going to explore a few ways to hack games written using Unity. Under the hood Unity makes use of "Mono" which is a cross-compiler for DotNet.

Within the Unity engine, developers can add "scripts" (written in C#) which make up some of the game logic - these will often be our target. Unlike more traditionally compiled games, these "scripts" are not simply compiled into the .exe where we can find a static memory offset to patch... but we do have some other options.

The game we're going to hack is called "198X" (part 1), an 80s-arcade themed game with several mini-games.


接下来,我要介绍一些破解 Unity游戏 的方法。在 Unity 底层,使用了 Mono,一个用于 DotNet 的跨平台编译器。

在 Unity 中,开发人员可以添加使用 C# 编写的 “脚本”,这些脚本构成部分游戏逻辑——这通常就是我们破解游戏的目标。与“传统”游戏不同,这些“脚本”并不直接编译成一个,(可以让我们)通过查找静态内存偏移进行修改的,.exe 文件。
但「道高一尺、魔高一丈」。

接下来,我会以破解 "198X"(第一部分) 为例,介绍多个破解方法。
这是一个以80年代街机为主题的游戏,包含多个迷你游戏。

1. 关于游戏 | About the game

查看原文

198X has several mini games built in, we're going to be hacking "Beating Heart" and "Shadowplay":

"Beating Heart" is a "beat-em-up" style game with health bars and you take damage when hit by enemies:

"Shadowplay" is a "ninja (runner?)" style game in which you have 5 lives and take damage when colliding with enemies or traps:

Both happen to use the same game logic for dealing with damage, for both the player and enemies (so can't just be NOP'ed out)


198X中有几个内置的迷你游戏,我们将对“Beating Heart”和“Shadowplay”进行破解:

img

“Beating Heart”是一款“打击游戏”风格的游戏,具有生命条,当被敌人击中时,你会受到伤害:

img

“Shadowplay”是一款“忍者(跑酷?)”风格的游戏,你有5条生命,与敌人或陷阱碰撞时会受到伤害:

img

这两款游戏恰好使用相同的游戏逻辑来处理伤害,无论是对玩家还是敌人(所以不能简单地将其 NOP 掉)。

2. 使用dnSpy进行探索 | Exploring with dnSpy

如果用不了 dnSpy,比如你使用的是 macOs 或 Linux,那么你可以试一下 ilSpy

查看原文

dnSpy is a ".NET debugger and assembly editor", which allows you to view the source of .NET applications. As mentioned above, Unity games are compiled with Mono, meaning they're .NET apps.

Because we're interested in cheating the game's logic, and not necessarily messing with the Unity game engine itself, we're after the user's Unity "scripts". Conveniently these are typically compiled in to a "Assembly-CSharp.dll" or "Assembly-CSharp-firstpass.dll" file. In the case of the game 198X we're looking at, they can be found in: Steam\SteamApps\common\198X\198X_Data\Managed

Opening "Assembly-CSharp.dll" in dnSpy (File -> Open -> browse to the "Managed" folder and select the file), should then add "Assembly-CSharp.dll" and a few other UnityEngine items to the treeview on the left. Expanding "Assembly-CSharp.dll" to, and clicking on, "{}" will list the (code) classes in the game:

From here we can start looking for useful terms that might help us find what we want to hack, for example searching (CTRL+F) for "damage" has a few results - the one we are interested in is "TakeDamage".

Expanding the "{}"" in the treeview gives a list of all the classes, scrolling down to and expanding "TakeDamage" reveals a "Damage(BaseDamage)" function... clicking on it shows the "Damage" function's code:


dnSpy 是一款“.NET调试器和汇编编辑器”,允许你查看.NET应用程序的源代码。正如前面提到的,Unity游戏是使用Mono编译的,这意味着它们是.NET应用程序。

因为我们对“修改游戏逻辑”感兴趣,而不是去搞乱Unity游戏引擎本身,所以我们要“研究”的是用户的Unity"脚本"。方便的是,这些脚本通常被编译到一个叫做"Assembly-CSharp.dll"或"Assembly-CSharp-firstpass.dll"文件中。在我们正在研究的 198X 游戏中,可以在以下位置找到它们:Steam\SteamApps\common\198X\198X_Data\Managed

在dnSpy中打开“Assembly-CSharp.dll”(选择“File” -> “Open” -> 浏览到“Managed”文件夹并选择文件),然后应该会在左侧的树状视图中添加“Assembly-CSharp.dll”和一些其他UnityEngine项。展开“Assembly-CSharp.dll”,点击“{}”将列出游戏中的类(代码类):

img

从这里开始,我们可以开始查找有用的术语(关键词),以帮助我们找到我们想要破解的内容,例如使用 CTRL+F 搜索 "damage" 会得到一些结果 —— 这里,我们感兴趣的是 "TakeDamage"。

展开树形视图中的 "{}" 会显示所有的类,向下滚动并展开 "TakeDamage" 会显示一个 "Damage(BaseDamage)" 函数... 点击它会显示 "Damage" 函数的代码:

img

3. 骇入游戏 | Hacking the game

1. 使用dnSpy | with dnSpy

查看原文

Notes:

These methods involve making changes to the game's "Assembly-CSharp.dll" file in order for the hacks to work - it's probably a good idea to back up this file before doing any of the below. These modifications should persist through different instances of the games unless Steam updates or repairs the game files.

You should be able to revert the file to its original state in Steam by right clicking on the game -> "Properties" -> "Local Files" -> "Verify Integrity of Game Files..."

The "Edit Method" route:

While dnSpy does provide some code editing functionality, it doesn't always work and can often have "Missing assembly" errors. In the case of 198X they seem to be using Mono embedded in the .exe, which can cause problems. That said, we can still patch this "Damage" function fairly easily.

The first thing the "Damage" function does is perform a few checks to determine if damage should be applied:

In the dnSpy listing, a few lines below the "Damage" function, is a "isPlayerCharacter: bool" property - literally a flag as to whether the object taking damage is the player or not. We can modify the above code by making sure the "Damage" function is selected, right clicking inside the code window on the right, and choosing "Edit Method (C#)".

Add this.isPlayerCharacter || inside the brackets of the "if" and click "Compile" (bottom right of the window). The damage function should now look like this:

Save the changes to file (File -> Save Module), and restart the game if open, for them to take effect.

The "Edit IL Instructions" route:

When changing and compiling the code doesn't work (eg: when obfuscation or protections have been put in place), we can make more subtle lower-level changes by modifying the "IL Instructions" (intermediate language opcodes). To do this right click in the code window and choose "Edit IL Instructions..."

While this view is much harder to read and work with, it's possible to identify fields used in the previously seen code, such as the "this.dead" in the "if" condition:

Left clicking on the word "dead" should give a popup with a "Field..." option, and clicking this lets us pick a different class property to use in its place. Clicking the "isPlayerCharacter" field should modify the code to:

Clicking the "Ok" button (bottom right) should return you to the code editor with the "if" statement changed to:

This code is not quite the same as our previous change, but should have the same result - granting us invulnerability... at the risk of introducing a bug allowing "dead" objects to take damage, which hopefully won't affect anything too serious.

Once again, you'll need to save the changes (File -> Save Module) and restart the game for them to take effect.


注意事项:

这些方法涉及对游戏的"Assembly-CSharp.dll"文件进行更改,以使破解生效——在执行以下操作之前最好备份此文件。这些修改应该会在不同的游戏实例中保留,除非Steam进行更新或修复游戏文件。

您应该能够通过在Steam上右键单击游戏 -> "属性" -> "本地文件" -> "验证游戏文件完整性..."来将文件恢复到其原始状态。

怎么 "Edit Method(编辑方法)":

虽然 dnSpy 确实提供了一些代码编辑功能,但它并不总是有效,并且常常会出现"丢失程序集(Missing assembly)"的错误。在 198X 这个游戏中,它们似乎在.exe中使用了嵌入的Mono,这可能会引起问题。尽管如此,我们仍然可以相当容易地对这个"Damage"函数进行补丁/修改。

在 "Damage" 函数中,它首先执行一些检查,以确定是否应该应用伤害:

if (this.dead || base.invincible || !base.enabled || this.excludeTags.Any((string tag) => damage.CompareTag(tag)))
{
	return;
}

在 dnSpy 的列表中,"Damage" 函数的下面几行有一个名为 "isPlayerCharacter: bool" 的属性——它实际上是一个标志,用于确定是否受到伤害的对象是否是玩家。我们可以通过确保选择了 "Damage" 函数,右键单击右侧代码窗口内部,然后选择"编辑方法(C#)"来修改上面的代码。

在 "if" 的括号内添加 "this.isPlayerCharacter ||",然后点击窗口右下角的 "编译"。现在,"Damage" 函数应该如下所示:

if (this.isPlayerCharacter || this.dead || base.invincible || !base.enabled || this.excludeTags.Any((string tag) => damage.CompareTag(tag)))
{
	return;
}

img

保存更改到文件(File -> Save Module),如果游戏已经打开,重新启动游戏以使更改生效。

怎么 "编辑IL指令":

当更改和编译代码不起作用时(例如,当开发者对代码采取混淆或保护措施时),我们可以通过修改"IL指令"(中间语言操作码)来进行更微妙的低级别更改。要做到这一点,右键单击代码窗口,然后选择"编辑IL指令..."。

img

译者:

ilspy-vscode 中如下图操作
img

AvaloniaILSpy 中如下图操作
img

虽然这个模式下的“视图”更难阅读和操作,但仍然可以识别先前看到的代码中使用的字段,例如在"if"条件中的"this.dead":

6	000E	ldfld	bool TakeDamage::dead

单击单词"dead"会弹出一个包含"Field..."选项的弹出窗口,单击这个选项可以让我们选择一个不同的类属性来替代它。单击"isPlayerCharacter"字段应该会修改代码为:

6	000E	ldfld	bool TakeDamage::isPlayerCharacter

单击"Ok"按钮(右下角)应该会将您返回到代码编辑器,"if"语句将被更改为:

if (this.isPlayerCharacter || base.invincible || !base.enabled || this.excludeTags.Any((string tag) => damage.CompareTag(tag)))
{
	return;
}

这段代码与我们之前的更改不完全相同,但也能会达成我们的目标——赋予我们无敌状态... 尽管存在引入bug的风险,允许"dead"对象受到伤害,希望这不会对游戏产生太严重的影响。

再次强调,您需要保存更改(File -> Save Module)并重新启动游戏以使更改生效。

2. 使用CheatEngine | with CheatEngine

查看原文

CheatEngine is a well known game-cheating tool, which I keep discovering more and more functionality in. While it's most obvious use case is "memory scanning" (eg: to find the memory address of a "health" value in memory, allowing you to set or freeze it) it also has some great support for Mono apps/games (like those written in Unity). When installing just watch out for toolbars or other crapware it may try and get you to install.

Opening CheatEngine and then clicking the "Select a process to open" button (first button on the left) should list all running processes. Select the 198X process, and CheatEngine should add a new "Mono" menu item.

Under this menu item is a "Dissect mono" option, which gives us some dnSpy-like functionality. The window that pops up also gives a treeview, showing the loaded "assemblies" - we're concerned with the "Assembly-CSharp.dll" assembly/file. As with dnSpy, the classes should be listed... including the "takeDamage" class we looked at previously, with its "fields" and "methods" also listed:

Right clicking on the "Damage" function and choosing "Jit" from the popup menu shows the assembly code for that function:

Having seen the "Damage" code in dnSpy, we know there's an "if" which looks at "this.dead", which is shown at offset "29" in the field list. We can also see "esi+29" in the 3rd last line of the code listing above, which likely references it.

Right clicking on the "movzx eax,byte ptr [esi+29]" line of code in CheatEngine's "Memory Viewer" and choosing "Assemble" lets us modify the code:

Changing the esi+29 to esi+1c makes the logic check if the object taking damage is the player, rather than if the object is dead (as we did when modifying the "IL Instructions" above):

This takes effect immediately (but only temporarily, until you restart the game). Luckily CheatEngine supports saving and loading cheat files, as well as a scripting language to dynamically find and change memory values.

While I won't be covering CheatEngine cheat scripts in this post, the following script automatically finds and patches the "TakeDamage.Damage":

Basically it enables the Mono features, then finds "TakeDamage:Damage" and re-writes the assembly instruction using either the "dead" or "isPlayerCharacter" offset. It can be added to CheatEngine by pressing Ctrl+Alt+A to open the "Auto assemble" window, pasting it in, then "File" -> "Assign to current cheat table" and closing the window.

Enabling the cheat (checking the box) should patch the memory address, and disabling (unchecking) it should revert it. Double click the description to change it, and use "File" -> "Save" to create a ".ct" file you can load and use for other instances of the game (or to distribute).


CheatEngine 是一个众所周知的游戏作弊工具,我一直在不断发现它的更多功能。不过它最明显的用途是"内存扫描"(例如:查找内存中"生命值"的内存地址,以允许您设置或冻结它),但它也对 Mono 应用程序/游戏(如使用 Unity 编写的游戏)有一些很好的支持。在安装时要小心,防止安装工具栏或其他可能尝试安装的垃圾软件。

打开 CheatEngine,然后单击 "打开进程(Select a process to open)" 按钮(左侧的第一个按钮,应该会列出所有正在运行的进程。选择 198X 进程,CheatEngine 应该会添加一个新的 "Mono" 菜单项。

img

在这个菜单项下有一个 "分析 mono(Dissect mono)" 选项,它提供了类似于 dnSpy 的功能。
弹出的窗口也提供了一个树形视图,显示了加载的"assemblies"(程序集)——对于我们来说,需要关心的是 "Assembly-CSharp.dll" 程序集/文件。与 dnSpy 一样,程序集包含的 类 应该会被列出... 包括我们之前查看过的 "takeDamage" 类,其中还列出了其"fields"(字段)和"methods"(方法):

img

请注意,在"fields"(字段)下,我们可以看到:

Note that under "fields" we can see:

1c : isPlayerCharacter (type: System.Boolean)
...
29 : dead (type: System.Boolean)

右键单击 "Damage" 函数并从弹出菜单中选择 "Jit" 会显示该函数的汇编代码:

TakeDamage:Damage - 55                    - push ebp
TakeDamage:Damage+1- 8B EC                 - mov ebp,esp
TakeDamage:Damage+3- 53                    - push ebx
TakeDamage:Damage+4- 56                    - push esi
TakeDamage:Damage+5- 83 EC 50              - sub esp,50 { 80 }
TakeDamage:Damage+8- 8B 75 08              - mov esi,[ebp+08]
TakeDamage:Damage+b- C7 04 24  B05CF90F    - mov [esp],0FF95CB0 { (0FF97160) }
TakeDamage:Damage+12- 90                    - nop 
TakeDamage:Damage+13- E8 10A64200           - call System:Object:__icall_wrapper_ves_icall_object_new_specific
TakeDamage:Damage+18- 8B D8                 - mov ebx,eax
TakeDamage:Damage+1a- 8B 4D 0C              - mov ecx,[ebp+0C]
TakeDamage:Damage+1d- 89 48 08              - mov [eax+08],ecx
TakeDamage:Damage+20- 0FB6 46 29            - movzx eax,byte ptr [esi+29]
TakeDamage:Damage+24- 85 C0                 - test eax,eax
TakeDamage:Damage+26- 0F85 5A010000         - jne TakeDamage:Damage+186
...

在 dnSpy 中看过 "Damage" 代码后,我们知道其包含一个 "if" 语句,会去检查 "this.dead",在 "fields"(字段)列表下显示的"dead" 偏移位置是 "29"。而在上面代码列表的倒数第三行我们能看到 "esi+29",那很有可能就是在引用它。

在 CheatEngine 的 "Memory Viewer" 中右键单击代码行 "movzx eax,byte ptr [esi+29]" 并选择 "Assemble(汇编)" 选项,让我们可以修改代码:

img

esi+29 更改为 esi+1c 会使逻辑检查受到伤害的对象是否是玩家,而不是受到伤害的对象是否已死亡(就像我们在上面修改"IL指令"时所做的那样):

...
TakeDamage:Damage+20- 0FB6 46 1c            - movzx eax,byte ptr [esi+1c]
...

这会立即生效(但只是暂时的,等你重新启动游戏就没了)。幸运的是,CheatEngine 支持保存和加载”作弊文件“,以及一个脚本语言来动态查找和更改内存值。

虽然我不会在本文中介绍CheatEngine作弊脚本,但以下脚本可以自动查找并修补"TakeDamage.Damage":

{$STRICT}
define(bytesOn, 0F B6 46 1C)
define(bytesOff,0F B6 46 29)

[ENABLE]
{$lua}
LaunchMonoDataCollector()

{$asm}
TakeDamage:Damage+20:
  db bytesOn

[DISABLE]
TakeDamage:Damage+20:
  db bytesOff

基本上,它启用了 Mono 功能,然后查找 "TakeDamage:Damage" 并改写汇编指令,使用了"dead"或"isPlayerCharacter"的偏移。您可以通过按下 Ctrl+Alt+A 打开"自动汇编"窗口,粘贴脚本,然后选择 "File(文件)" -> "Assign to current cheat table(分配给当前作弊表)" 并关闭窗口,来将脚本添加到 CheatEngine 中。

启用作弊(勾选复选框)应该会修补内存地址,而禁用作弊(取消勾选)应该会恢复原状。双击描述以进行更改,并使用 "File(文件)" -> "Save(保存)" 来创建一个 ".ct" 文件,这样之后,就可以加载和在游戏的其他实例中使用(或分发给其他人使用)。

3. 使用Frida | with Frida

查看原文

As vaguely mentioned above, the more traditional game hacking methods I'm used to (working with fixed offsets) don't work here... Unity seems to load the "Assembly-CSharp.dll" file dynamically in to general memory, rather than loading it as a library, making it tricky to find the offset to patch.

As a result I needed to scan for bytes in memory to find where to apply the patch, which Frida already provides functionality for. It's also worth exploring Frida as it has many uses when hacking mobile applications and games (eg: Objection).

It is also possible to hook Mono functionality with Frida (see frida-mono-api, though I haven't gotten it working properly, or to a more usable place than the code below, yet). This could be particularly useful given that Xamarin is used for cross-compiling mobile applications using Mono.

Below is a Frida script capable of finding and patching bytes in memory:

It can be loaded from the command like with frida 198X.exe -l patch.js, and then running enablePatch() in Frida:


如上所提到的,我习惯使用的更传统的游戏破解方法(使用固定偏移量)在这里不起作用... Unity似乎动态加载"Assembly-CSharp.dll" 文件到通用内存中,而不是将其作为库加载,这使得很难找到要打补丁的偏移位置。

因此,我需要在内存中扫描字节以找到应用补丁的位置,而 Frida 已经提供了这方面的功能。深入去研究 Frida 也是值得的,因为它在破解移动应用程序和游戏(例如 Objection )时非常有用。

还可以使用 Frida 以函数为单位钩住(hook) Mono 程序(请参阅 frida-mono-api,尽管我还没有使它正常工作,没能够像下面的代码一样起作用)。鉴于 Xamarin 也使用 Mono 进行跨平台编译的移动应用程序,这可能特别有用。

以下是一个 Frida 脚本,可以找到并在内存中打补丁的字节:

const invulnerability = {
  pattern: '89 48 08 0F B6 46 ?? 85 C0',  // TakeDamage:Damage+20  -  0F B6 46 1C  -  movzx eax,byte ptr [esi+1C]
  offset: 6,
  disabled: [0x29],
  enabled: [0x1C]
}

const findOffset = function(pattern) {
  // find every memory range
  var ranges = Process.enumerateRanges("r");
  for (var i = 0; i < ranges.length; i++) {
    var range = ranges[i];

    // and check each of them for our pattern
    var results = Memory.scanSync(range.base, range.size, pattern);
    if (results[0]) {
      // convert the (first) returned address to int before adding to it, so JavaScript doesn't contact :facepalm:
      return parseInt(results[0].address, 16);
    }
  }
}

// used for patching memory
const patch = function(pattern, skipBytes, bytes) {
  var offset = findOffset(pattern) + skipBytes;
  var pointer = new NativePointer(offset);
  return pointer.writeByteArray(bytes);
}

const enablePatch = function() {
  var result = patch(invulnerability.pattern, invulnerability.offset, invulnerability.enabled);
  if (result) {
    console.log('Patch enabled! (address ' + result + ')');
  }
}

const disablePatch = function() {
  var result = patch(invulnerability.pattern, invulnerability.offset, invulnerability.disabled);
  if (result) {
    console.log('Patch disabled. (address ' + result + ')')
  }
}

它可以通过命令行加载,使用以下命令:frida 198X.exe -l patch.js,然后在Frida中运行 enablePatch()

img

4. 其他内容 Other

查看原文

Big thanks to @leonjza for telling me about this great game, helping with his Frida wisdom, and nudging me on 😄

While the above only provides invulnerability for 2 of the 5 mini games ("Beating Heart" and "Shadowplay" which use the same "TakeDamage" class), with the above info it's relatively easy to hack (some) of the remaining games:

Out of the Void: this one was more tricky and my hacks mostly bugged out, sorry
The Runaway: look at CarController.OnCollision and CarController.SetSpeed
Kill Screen: look at RPGController:EnemyAttack

非常感谢@leonjza,他推荐了这个精彩的游戏给我,用他的 Frida 技巧提供了帮助,并一直在鼓励我 😄

虽然上面的方法只破解了 5 个迷你游戏中的 2 个("Beating Heart" 和 "Shadowplay",它们使用相同的 "TakeDamage" 类)的无敌效果,但借助上面的信息,相对容易地破解了(某些)其他游戏:

Out of the Void:这个游戏比较棘手,我的破解方法大部分都出了问题,抱歉
The Runaway:可以从 CarController.OnCollisionCarController.SetSpeed 入手
Kill Screen:可以从 RPGController:EnemyAttack 入手

posted @ 2023-10-04 00:44  bakabird1998  阅读(4754)  评论(0)    收藏  举报