作弊和反作弊(Cheats & Anticheats)
由 Mischa
介绍
在 2009-2015 年,在开始开发 Mirror 和 uMMORPG 之前,我尝试通过逆向工程 MMO 游戏并出售机器人来谋生。我将根据我们 Discord 中的问题分享一些所学到的经验。本文尚未完成,旨在简要介绍我们 Discord 中经常被问到的主题。如果您想了解更多,请告诉我。
首先,我们将了解服务器权限与客户端权限,这是第一个主要的攻击向量。我们还将讨论独立于权限的攻击以及如何防范这些攻击。
服务器权限 vs. 客户端权限
首先要明确一点。Mirror 默认是服务器授权的。换句话说,服务器做出所有决定。作弊者通常修改客户端以利用那些信任客户端做出某些决定的游戏(即客户端权限)。
换句话说,只要您使用完全的服务器权限,除非有人物理上入侵您的服务器机器,否则一切都没问题。如果您在游戏的某些部分(如移动)中使用客户端权限,那么这些部分就是您需要担心的部分。
为了明确起见,这里通过使用一瓶生命药水来解释服务器权限和客户端权限之间的区别
客户端:我可以使用这个药水吗?
客户端:我使用这个药水。我的新生命值是 100!
服务器:正在验证...
服务器:¯\_(ツ)_/¯
服务器:您的新生命值是 100!
实际上,您需要在[Commands]中验证任何客户端输入。这里是一个实际视频,展示了有人利用 Mirror 制作的游戏的漏洞,开发人员没有验证客户端输入。该游戏 可能 有一个类似这样的CmdSellItem函数:
[Command]
void CmdSellItem(int slot, int amount)
{
// get player's item at inventory slot
Item item = player.inventory[slot];
// sell to npc
item.amount -= amount;
player.gold += item.price * amount;
}注意我们是如何盲目相信客户端发送的正确数量的。没有任何检查。如果玩家只有一个物品,但攻击者发送 'amount = 100',我们仍然会相信并出售 100 个物品。相反,我们需要验证任何输入:
客户端权限 - 万恶之源
相信客户端进行移动
如果 Mirror 默认完全由服务器控制,而客户端权限允许作弊,那么为什么有人会使用客户端权限?
因为它很容易。很多游戏一开始或永远使用客户端权限进行移动。在服务器控制下,客户端必须在每次移动之前询问服务器。这会在按下按键和实际移动之间引入很多延迟。这一点一点也不有趣。
在客户端权限模式下,玩家按下按键后立即移动。而不是询问服务器移动,它告诉服务器它已经移动了。这感觉很棒,但也让作弊者可以告诉服务器他们喜欢的任何内容,例如“我移动得更快了”。
网络化移动很困难。可以实现快速移动同时也由服务器控制(橡皮筋效应/预测/等等),但许多人选择一开始不这样做,以节省数月的开发时间。
相信客户端进行输入
在一些类型的游戏中,比如第一人称射击游戏,相信客户端处理游戏的某些部分是不可避免的。在这种情况下,瞄准。每当我们相信客户端时,这种信任都可以被黑客利用。在 FPS 游戏中,瞄准机器人可以假装更快地将光标移动到另一个玩家身上。而且由于服务器相信客户端移动光标,这为作弊打开了一扇没有简单解决方案的大门。
服务器控制下的“作弊”
只是为了明确,即使对于像 MMO 这样 100%由服务器控制的游戏,仍然存在攻击向量。本文的重点是首先关注客户端权限的最明显攻击。即使服务器不相信客户端,仍然存在机器人的空间,这些机器人在技术上并不作弊,除了自动执行玩家应该手动执行的任务。
机器人是分析游戏状态并在玩家不在场时生成输入以自动获取金币或杀死怪物的工具。有些玩家甚至会使用数百个机器人来获取游戏内的金币,然后以真实货币出售。
防止服务器授权的“作弊”超出了初始开发的范围。发布后会有足够的时间来处理这些问题。在你的游戏中有人在地下室运行机器人并不是一个严重的威胁,除非情况失控。
并且要明确一点,可以在客户端和服务器端检测机器人。但是要在问题出现的 5 年后担心这个,而不是今天。
作弊是如何制作的
让我们快速了解一下作弊是如何制作的。
你的游戏在内存中存储了大量相关信息。例如:本地玩家的位置、其他玩家的位置、怪物位置、生命值、名称等。
查找内存位置
大多数作弊需要从游戏内存中读取一些信息。像Cheat Engine这样的工具允许你搜索游戏内存中的特定值。例如,如果你有 100 点生命值,那么你搜索“100”可能会找到 10000 个内存中值为“100”的位置。但是如果你喝下一瓶药水,将生命值增加到 200,那么你可能可以将之前为“100”现在变为“200”的几个值缩小到一小部分。如果你多次这样做,通常可以缩小到内存中的一个位置。例如,本地玩家的生命值可能存储在内存地址0xAABBCCDD。
但是有一个问题:下次我们启动游戏时,游戏会重新设置世界,你的玩家生命值几乎肯定不会再在同一个内存地址上了。像 Cheat Engine 这样的工具允许你通过设置断点来**“查找访问...”**该内存位置。再次使用药水,断点触发,现在你知道你的游戏中的哪个部分访问了该内存位置。
而不仅仅是Health,现在你有了Player->Health(这只是一个简化,在实践中,你会从0xAABBCCDD转换为一个带有偏移量的指针,比如[0x00FF00FF+0x8],其中0x00FF00FF是内存中玩家对象的位置,0x8是Player->Health的偏移量。很可能Player->Mana会在+0x12处,或者在内存中的下一个位置。这个过程可以重复进行,直到你有了Game->Player->Health,其中Game最终是相对于程序入口点的地址。
换句话说,我们现在可以在重新启动游戏后始终读取玩家的生命值。
这个过程可以重复用于物品栏、技能、怪物、位置等。我们能找到的信息越多,编写作弊就越容易。
如果我们的游戏使用客户端权限,那么我们实际上可以在内存中修改玩家的生命值!如果我们使用服务器权限,那么我们仍然可以在内存中修改它,但更改只在此客户端上可见。服务器不信任客户端的生命值,下次同步新的生命值到客户端时,内存中的值将再次被覆盖。
这就是 Mirror 的**[SyncVar]**的工作原理!你可以在 Cheat Engine 中修改它们,但没有人在乎,因为它们是服务器授权的。
使内存位置更难找到
通过指针和偏移量找到内存位置的过程很繁琐。每当游戏发生变化时,偏移量也会改变。例如,如果之前我们有
而游戏变成了:
那么作弊开发者将不得不手动重新搜索内存中的所有偏移量。这在一定程度上是令人头疼的。
偶尔改变内存布局是使逆向工程更加困难的好方法。防止逆向工程是一个回报 vs. 努力的函数。如果作弊最终只能赚到每月 10 美元,没有人会每天花费 10 小时进行逆向工程。我们让它变得更难,它就会变得不值得。
投影内存值
这是一个有趣的小技巧,实际上可以在任何游戏中完成,风险不大。与其直接存储 Health,我们可以存储一个投影值,例如:
这只是一个简化的示例,但思路是不直接在内存中存储我们的 "100" 生命值。相反,我们存储通过加一或更复杂的投影修改后的值。这样一来,整个Cheat Engine的初始发现过程会变得非常沮丧,同时几乎没有任何风险。在 Unity 中这样做几乎不会出现问题。
投影内存值是让作弊开发变得更加烦人的简单方法。请注意,这会带来轻微的性能影响,并且只有在 Unity 中使用 IL2CPP 时才有用。
加大访问内存的难度
有各种技术可以使逆向工程变得更加痛苦。例如:
虚假入口点,动态更改,例如使用像 UPX 打包器 这样的 exe 打包。这些并不难解包,但会增加难度。
请注意,经 UPX 打包的可执行文件通常会被标记为病毒。
通过 IsDebuggerPresent 检测调试器,如 Cheat Engine/MHS。请注意,这很容易被绕过,因为每个人都知道 IsDebuggerPresent。更高级的技术可能涉及诸如测量指令之间的时间等技巧。例如,如果我们在运行时使用 StopWatch 测量一个简单的整数乘法,如果花费了几毫秒的时间,那么很可能有人正在用调试器逐步执行这段代码。
使用像 Themida 或 Enigma Packer 这样的工具进行虚拟化是保护逆向工程的终极目标。如果在常规进程中找到内存位置很困难,那么在虚拟机内部找到它们就难上加难。在我们以前逆向工程游戏时,我们从不碰虚拟化进程,因为投入与回报永远不值得。除非你的游戏像魔兽世界那样庞大,否则没有人会花半年时间分析你的虚拟机指令。
请注意,虚拟化的可执行文件通常会被标记为病毒。您需要一个不被标记为病毒的自定义虚拟化引擎。
现在我们了解了作弊是如何开发的,我们可以看看一些常见作弊的工作原理以及如何防范。
Ollydbg/IDA/Code Caves
假设您的游戏有一个函数,如下:
这可能产生(简化的)汇编代码如下:
黑客可以使用高级调试工具修改您游戏的汇编代码为:
与使用 Cheat Engine 搜索和修改内存值不同,可以直接修改游戏自身的汇编代码。
修改游戏的汇编对于开发作弊非常强大。Code Caves经常用于将自定义函数注入到游戏中,例如:
在 C#中,这相当于用户将自己的代码注入到我们的 Health 函数中,如下:
这是一个简化的示例,但是一个非常常见的技术。为了防范自定义汇编,可能明智的做法是生成您的 exe 文件的校验和。
Wall Hacks / ESP
在第一人称射击游戏中,透视作弊是最常见的作弊之一。人们可以修改您的可执行文件以显示墙后的玩家。这相对容易做到且相当普遍。
为了防范:
使逆向工程尽可能困难(参见上述章节)
使用 Mirror 的Interest Management来不显示远处的玩家。您可以实现基于射线投射的 Interest Management,在这种情况下,只有实际看到的玩家才会发送给客户端。
请注意,您可能希望有一定的容忍度提前发送它们,例如在他们被看到之前 1 秒发送。这并不完美,但比允许玩家一直看到所有其他玩家要好。Interest Management 对此非常重要。
在运行时检测透视作弊并封禁使用它们的作弊者。
这是一个棘手的问题,即使像《反恐精英》这样的热门游戏也很难应对。这是一场持续的战斗。
速度作弊
如果你选择使用客户端授权的移动,因为这样更容易,那么你很可能最终会在游戏中遇到速度作弊。速度作弊可以通过各种方式实现,从简单地在内存中修改 Player.Speed,到干扰计算机的时钟速度,这在 Unity 中很难解决。
为了防范:
在服务器端检查移动速度。允许一定的网络条件容忍度。许多游戏允许 10-15%的容忍度,但超过这个范围的很可能是速度作弊。
机器人
如前所述,机器人特别恶劣,因为它们不需要任何真正的作弊或客户端授权。此外,它们可能破坏游戏的经济平衡,并使玩家感到无聊,因为周围的人都是机器人。
为了防范:
使内存位置查找变得困难。参见上述章节。
偶尔调整内存布局。有时在
Player.Health和Player.Mana之间添加不必要的值。偶尔调整网络协议。最先进的机器人甚至不需要读取你的内存。它们直接使用你游戏的发送/接收函数。偶尔修改你的 NetworkMessage 操作码和布局,将使逆向工程变得非常痛苦。
通过校验和、名称等检测已知的 Bot.exe 进程。
请注意,这样做往往会将你的游戏标记为病毒。游戏不应该查找正在运行的进程。
在服务器端检测机器人的模式。如果机器人成为严重问题,这就是我会采取的措施。
最简单的形式是,如果有人连续一周 24/7 游戏,那很可能是机器人,或者在极少数情况下是某个人在网吧里。
如果玩家总是在同一路径上行进或总是在同一位置上升级,那很可能是机器人。
在游戏中添加一个举报按钮。查看被举报的玩家。尝试与他们交流,看看他们是否会回应等。
在活动频繁的地方生成诱饵怪物。如果某个区域在一段时间内有大量怪物被击杀,就在那个区域生成一个外观明显不同但极其强大的怪物。普通玩家会注意到并暂时移动到其他地方。机器人会攻击它并死亡。
再次强调,这些都是复杂问题的简化答案。如果你的游戏变得成功,那么这将是一场持续的战斗。只要你知道自己正在进行一场战斗,这是可以接受的。
隐秘、延迟检测
游戏中最大的错误之一是让用户知道何时检测到了作弊或反向工程工具。这只会让反向工程师知道在代码中哪里查找以禁用检查。
如果我们经过了检测作弊和调试器的所有工作,我们应该保持沉默,并利用这些信息来获得优势。与其大声宣布作弊尝试,不如悄悄地向服务器发送一些信息。在数据库中标记玩家。
不要立即封禁或踢出任何人。更明智的做法是等待一段随机时间。
用户可能会在一个月内尝试多种不同版本的作弊。
反向工程师可能会使用不同的工具以及以不同方式修改游戏。
如果我们每月只封禁一次人,那么造成封禁的原因就不明显。这将导致巨大的周转时间来测试哪些作弊被检测到,哪些没有。
隐秘检测是我们对抗作弊者的最强大工具。利用时间和信息来获得优势。
免费游戏 vs. 付费游戏
这是最后一个考虑因素,我很可能也会为自己的游戏做同样的事情。虽然免费游戏能够吸引大量玩家,但如果你只是一个小型独立开发者,还没有准备好应对大量虚假账户和黑客,那么付费游戏可能会有价值。
玩家需要支付一次性费用来玩你的多人游戏,这引入了一个巨大的障碍,黑客和作弊者如果被封禁就必须重新购买游戏。此外,这还增加了一定程度的验证,以确保人们不能反复创建账户。如果需要的话,你可以封禁信用卡等。
总结
总而言之,作弊是一个复杂的话题,永远不会有最终解决方案。在我看来,尽一切可能使一切都在服务器端进行验证。对于移动,至少在某个时候,比如在发布游戏后当你开始看到第一个速度作弊或者当你确实有一些喘息时间时,至少应该使其在服务器端进行验证。
一旦你的游戏变得成功,可能会有人试图挑战。有很多事情可以做来增加难度。
最终,这取决于付出的努力与回报。你让作弊变得越麻烦,人们就越不愿意去尝试或者会选择更容易的目标。
这个话题可以写一整本书,但我希望你从中学到了一些基础知识。
最后更新于
这有帮助吗?