文章目录
- Intro
- Hardware
- Return to virtual memory
- Compiler
- Finding assembly code
- Example!
- Assembly language
- Registers
- Assembly Example
- Inc
- Dec
- Add
- Sub
- Mov!
- Neg
- Mul
- Div
- Assembly problem
- 恭喜 Congratulations!
Intro
大多数人在走到这里之前就退出了,这里是 game hacking 开始变得真正有趣的地方,我们将开始学习程序集编辑,也就是汇编编辑,我们将依靠这个重写游戏代码,以使其执行我们希望它执行的操作,如果您从未编写过游戏也不要担心,课程接受你将学会使用汇编语言轻松开发自己的 game hacking command-line,同样的你也可以将这一过程看为:
- Find relevant assembly code
- Change it
- See if it worked
这里第一点和第二点实际上是2个不同方面的内容,他们都具有自己的学习曲线,找到汇编代码首先要知道汇编代码是什么以及如何工作
Hardware
首先我们先要了解一些硬件的知识,你的电脑有一个 CPU,复制处理代码,制造商决定哪些代码可以在 CPU 上面运行
- x86/x64 台式机、笔记本、现在的 PS、XBOX 属于这个架构,绝大多数的关注重点
- ARM 智能手机和手持设备倾向于使用 ARM,功效更高,功耗更好
- PowerPC 一些旧游戏机使用它,你可能永远不会看到他除非现在回去深入他们
Return to virtual memory
我们重新审视虚拟内存中的数据排布,在 copied exe 和 DLL 中不止有静态数据还有静态代码,现在是我们学习代码的时候了
c7 40 04 32 00 00 00 90 83 6d f8 fc 48 8d 04 24 ff d5 …
现在我们可以把这些代码分组为汇编指令语言,并且不用担心理解这些东西
- mov [rax+04],32 - C7 40 04 32 00 00 00 组成移动指令
- nop - 90 组成无操作指令
- sub dword ptr [rbp-08], -04 - 83 6D F8 FC 减法指令
- lea rax, [rsp] - 48 8D 04 24
- call rbp FF D5
在我们阅读 FF D5 时可以说他们是机器语言,但当我们把他们分组成一条指令,即汇编语言之后,就可以把相同的信息垂直堆栈起来,这就是大部分 game hacking tool 显示汇编语言的方式
Compiler
直接使用汇编工作是枯燥乏味且容易出错的,所以现在有 C、C++ 这样的高级语言,C++ 转换成汇编代码的过程就是编译器工作的过程,让我们看看编译器是如何工作的
大多数游戏都是用 C++ 编写的,游戏开发人员可以将 C++ 编译成机器语言,这对 game hacking 工作人员来说是不可读的,所以我们把机器语言 Disassembling 成汇编语言,然后我们可以编辑汇编语言以重新编程游戏
但想把机器语言逆向回 C++ 代码是不太可能的,让我们打开 godbolt.org 网站看看,他基本给你展示了 C++ 代码和汇编语言之间的联系
常见的第二个技术是 C#,当代许多游戏都是用 Unity 制作的,Java 也是一样的
这里视频作者放了一系列 PPT 来介绍 python、game maker 等等,说明了为什么 C++ 游戏比较适合汇编修改
如何确认游戏是用什么语言写的?
- 打开游戏文件夹
- 如果看到 Mono 一般是 Unity C#
- 看到 ue3 un4 一般是虚幻引擎制作也就是 C++
Finding assembly code
如何找到汇编代码?一个游戏可能有上百万行汇编指令,我们的目标只是找到其中的一条指令,例如伤害玩家的代码,或者购买物品时夺走我们金币的代码。
比方说,我们玩家的健康状况存储在玩家对象中,现在在 C++ 游戏中,所有代码都存储在 exe 中,所以我们知道 Player 对象处的代码负责更新此处玩家的生命值,我们的目标是找到那里的代码,我们可以在玩家的血量处设置一个叫做数据断点的东西,他会准确告诉我们 exe 中的哪一行代码尝试访问或更新玩家的生命值
Example!
首先我们还是启动游戏,然后仍然是去找到生命值或者金钱之类的你感兴趣的数值,想要找到编辑此数值的代码能做的就是右键然后点击 Find out what writes to this address,或者 Find out what accesses this address 正在读取这个数值的地址比如说死亡检测之类的东西。
比如我们查找是什么让我们受伤,肯定是有什么写入了数据,所以这里选择 Find out what writes to this address
当我们在游戏中改变这个数值时,会发现多了一条指令出现在这个列表
我们选中它并选择 Add to the codelist,最好给出一个名字比如 update gold / update health
很酷的是,我们可以右键 codelist 中的这条指令然后将其替换为不执行任何操作的代码,当然你也可以随后选择恢复成原来的代码样子。
我这里是植物大战僵尸的金币增加代码,如果这样做了,将会导致后续不再能够获取金币。
让我们重新打开植物大战僵尸,选择阳光作为我们的关注对象,当我们使用一个卡牌时可以捕获到这样一条指令
如果我们把它替换成 nop 无操作指令,在游戏中使用阳光你会发现什么都没有发生,没有阳光被消耗了(这里的逻辑是消耗,增加阳光或者其他更改阳光数值的逻辑途径不受影响!
不幸的是,有些游戏的受伤或者治疗代码适用于所有对象,也就是说,你不会受到伤害了,但怪物也不会了,这不是我们想要的
一个重要的点是:动态地址再重启游戏后不再会有效,但你找到的这些 code 他们是不会有变化的,因为这些代码都存放在 exe 中
Assembly language
所以我们找到了我们感兴趣的代码,也许可以用一些 nop 无操作指令来填充他,这对 90% 情况都够用了,我们可以把伤害玩家、夺走玩家金币的代码删除。但在某些情况下编写自定义汇编能够更有价值,但我们要先学会汇编语言
Registers
第一个概念是寄存器,寄存器是 CPU 中的临时存储,在我们深入了解我们需要的寄存器之前,了解代码如何在硬件上运行。我们知道当用户双击一个 exe 时,他会从硬盘复制到 RAM 中,但是我们需要该代码在 cpu 上运行,而 cpu 存储空间非常之小,我们不能只是复制所有游戏代码到 cpu 里面,因此,所有的代码都是逐行传输到 cpu 中,就像视频网站传输视频一样。
但是存在一个小问题,Code ‘streamed’ to CPU,但有时代码需要访问数据,CPU 可能会运行一些代码来读取玩家的生命值,然后更新一下数据再放回 RAM 中,不幸的是从 CPU 访问 RAM 实际上是相当昂贵的,所以人们在 CPU 上面放置了非常少量的存储空间,这就是寄存器
- 硬盘离 CPU 非常远,访问速度相当慢,但存储相当多的东西
- 内存离 CPU 较近,速度较快,中等存储量
- CPU Memory 离 CPU 最近,速度最快,但容量极小
内存通过 L3 Cache -> L2 Cache -> L1 Cache(Code/Data) -> Registers 进入寄存器,其中1级缓存具有2个部分,分别存储代码和数据。幸运的是,对我们来说不需要关注中间的 Cache 缓存,只需要了解 Registers 中发生了什么即可
在我们想要进行操作之前我们要先把数据放到寄存器中,这样我们可以把玩家的生命值放到寄存器里,然后加载一些我们可能造成的伤害,然后把两者进行减法,然后写回 RAM 中
这是 32-bit game 中寄存器的示例,在 32-bit 计算机中寄存器有 4bytes :
- EAX 0000 0064
- EBX 0000 0005
- ECX 0000 002C
- EDX 0000 0019
- EDI 018B 1000
- ESI 018B 1000
- EBP 6FF9 30CC
- ESP 6FF9 3070
- EIP 02C1 A07B
如果我们把游戏在此时冻结,可能会注意到 EAX = 64 可能是玩家的生命值,EBX = 5 可能是玩家即将受到的伤害
而 64-bit game 其实是类似的,只是寄存器长度翻倍为 8bytes,而且寄存器以 R 开头而不是 E:
- x86 Assembly 32-bit games, push eax
- x64 Assembly 64-bit games, push rax
x86 实际上是 intel 早期一些 cpu 命名,而 64-bit 出现时他们必须给一个新名字,所以就又变成了 x64,这看起来似乎有些愚蠢
区分 32位 和 64位 游戏很简单,当你使用你的 Tools 打开游戏看看里面的寄存器是 R 开头还是 E 开头就可以了,其他几乎一样
在寄存器中不要随便弄乱 EBP ESP EIP 这三个,他们一般用来指向下一条指令,而其他的指令一般都作为 General use,可以作为重点关注对象
Assembly Example
Inc
inc ecx
执行时给 ecx 的值 + 1
int [eax]
执行时给 eax 地址所在的值 + 1,比如 eax = 50393088,那么在地址 50393088 值为 12 变成了 13
int [edi + 8]
沿着 edi 地址,向下经过2个整数(4bytes),自增那个位置的数值
Dec
和 inc 的操作完全一样,只是变成了减 1
Add
add 拥有了2个寄存器作为参数
add eax, 2
给 eax 加 2
add ecx, edx
给 ecx 加 edx
add [ebp + 8], esi
结合上面的 Inc,也是类似的
Sub
和 add 的操作完全一样,只是加法变成了减法 subtracts
Mov!
比较重要的一个是 mov 移动指令,移动的工作原理是将一个值复制到该寄存器中
mov ebx, 200
200 覆盖到 ebx 上面
mov ecx, edx
edx 的值覆盖到 ecx 上面
mov [eax], 10
10 覆盖到 eax 所在的地址位置
下面的省略,仍然和上面的指针版本一样,但需要注意的是,你不能在这里写 2个带 [ ] 的东西,比如 mov [eax], [edx]
这是不可能的,从 CPU 访问 RAM 的代价是昂贵的
Neg
neg 是一个简单的指令,只是对数字取反
Mul
mul 也是一个简单的指令,和 add 一样只不过现在是乘法 mul ebx, 2
Div
div 是所有指令中最令人恼火的一条,它的工作方式和乘法不同,只有一个寄存器可以作为参数传递给他
div ecx
比如此时 eax = 120,ecx = 12,当你执行这个指令时就会 使得 eax 除以您传递的任何值
当你执行之后 eax = 10, ecx = 12,如果你再执行一次,eax = 0,整数部分显然是 0,随后余数将会到 ecx 里面也就是 ecx = 2,这是非常奇怪的,他的工作方式和其他指令都不一样,可以尝试避免使用他!
Assembly problem
我们可以尝试着手修改,做一些简单事情作为例子,视频中使用了扫雷,将自增改变为 +2,随后你会发现一个问题,我们填写的代码长度和原有的代码长度不符,exe 的长度是固定的,我们不可能改变这里的代码长度
正确的思路是:生成一个新的代码块并分配新的内存,在那里编写代码并运行
现在我们选中这条指令,点击 Tools 里面的 Auto Assembly 或者按下 Ctrl + A
填入上面的内容以后点击 Template 里面的 Code inject,注意这里一定要选中你要修改的那条指令
这样就会生成一段 Code injection 代码,你可以删除一些自动生成的注释,同时记得把上面的 label 也删除:
总之,上面的 [Enable] 代码块包含了当我们启动时运行的代码,[Disable] 包含了当我们禁用时的代码。你会发现代码先通过alloc申请了内存,然后在 “popcapgame1.exe”+1F634 跳(jump)到了newmem内存块,所以我们现在可以在 newmem 上面进行修改了,比如把 sub 改成 add 这样的操作
然后我们选择 File - Assign to current cheat table 加入当前的 CE 表
接下来选中他最左一列的 Active 或者选中按空格就可以启动这个 Script 了,种下太阳花发现果然阳光不再减少而是加了 2!
恭喜 Congratulations!
到这里你已经掌握了 game hacking 的基础了,随着后续精进汇编语言,了解更多 game hacking 方法,你将学会游戏逆向的大多数内容。
作为一名游戏开发者,后续将更新如何应对这些 game hacking,也就是 anti-cheat 的基本思路和方法,敬请期待!