[Unity Mirror] 作弊与反作弊

RuntimeMapMaker3D-Pro在这里插入图片描述


简介

  早在 2009-2015 年,在从事 Mirror 和 uMMORPG 工作之前,我尝试通过对 MMO 进行逆向工程和出售Bots来谋生来了解 MMO。我将根据我们 Discord 中的问题分享一些经验教训。本文不完整,旨在简要介绍我们 Discord 中最常见的主题。如果您想了解更多,请告诉我。

  首先,我们将了解 Server Authority & Client Authority,这是第一个主要的攻击方向。我们还将讨论与权威无关的攻击以及如何防范它们。

作为经验法则,永远不要相信客户端!

服务器权威与客户端权威

  第一件事。镜像默认是服务器权威的。换句话说,服务器做出所有决定。作弊者通常会修改客户端以利用客户端在某些决策(也称为客户端权威)方面受到信任的游戏。

  换句话说,只要你使用完全的服务器权威,你就没有问题,直到有人从物理上黑掉你的服务器机器。如果你在游戏的某些部分使用客户端权威(如移动),那么这些部分你就需要担心了。

  为了清楚起见,这里是使用生命药水解释的服务器和客户端权威之间的区别

服务器权威 客户端权威
客户:我可以用这个药水吗? 客户:我用这个药水。我的新生命值是 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 件物品。相反,我们需要验证任何输入:

[Command]
void CmdSellItem(int slot, int amount)
{
    // valid slot?
    if (0 <= slot && slot <= player.inventory.Count)
    {
        // get player's item at inventory slot
        Item item = player.inventory[slot];
        
        // valid amount?
        if (0 < amount && amount <= item.amount)
        {
            // sell to npc
            item.amount -= amount;
            player.gold += item.price * amount;
        }
    }
}

客户权威——万恶之源

信任客户的移动

  如果镜像默认是完全服务器权威的,而客户端权威允许作弊,那么为什么会有人使用客户端权威呢?

  因为这很容易。许多游戏一开始或永远使用客户端权威进行移动。在服务器授权中,客户端必须在每次移动之前询问服务器。这在按键和看到实际动作之间引入了很多延迟。这一点都不好玩。

  在客户端权威模式下,玩家一按下键就会移动。它不是要求服务器移动,而是告诉服务器它移动了。这感觉很棒,但也引入了作弊者告诉服务器他们喜欢什么,例如“我搬到这里的速度是原来的两倍”。

  网络化移动是困难的。可以进行服务器权威的快速移动(橡皮筋/预测/等),但许多人选择一开始不这样做,以节省数月的开发时间。

信任客户端的输入
  在第一人称射击游戏等某些类型中,在游戏的某些部分中信任客户端是不可避免的。在这种情况下,瞄准。每当我们信任客户端时,这种信任就会被黑客利用。在 FPS 游戏中,Aim Bot 可以假装更快地将光标移动到另一个玩家身上。而且由于服务器通过移动光标信任客户端,因此它为作弊打开了大门,没有简单的解决方案。

总而言之,我们可能希望或需要在我们游戏的某些部分中信任客户端。这些是我们需要防止作弊的部分。

服务器权威“作弊”

  为了明确起见,即使是像MMO这样100%的服务器权威的游戏,仍然存在着攻击的媒介。这篇文章的重点是首先担心对客户端权威的最明显的攻击。即使服务器不信任客户端,仍然有机器人的空间,这些机器人在技术上不会作弊,除了自动执行玩家应该手动完成的任务。

  机器人是分析游戏状态并生成输入以在玩家不在时自动获取金币或杀死怪物的工具。这可能会走向极端,一些玩家使用数百个机器人进行耕种,然后以真钱出售游戏内的金币。

请记住,服务器权威作弊是一个奢侈的问题。如果你的 MMO 变得如此成功以至于人们开发了机器人,那么你就成功了。

  防止服务器权威“作弊”超出了初始开发的范围。发布后会有很多时间来处理这些问题。有人在他的地下室里运行一个机器人,这对你的游戏来说不是一个严重的威胁,除非它失控了。

  为了清楚起见,可以在客户端和服务器端检测机器人。但是当问题出现时,请在 5 年后担心它,而不是今天。

作弊是如何产生的

  让我们快速深入了解作弊的实际制作过程。

  您的游戏在其内存中存储了大量相关信息。例如:本地玩家的位置、其他玩家的位置、怪物位置、生命值、名称等。

查找内存位置

  大多数作弊需要从游戏内存中读取一些信息。作弊引擎等工具允许您在游戏内存中搜索特定值。例如,如果您有 100 个生命值,那么您搜索“100”可能会在内存中找到 10,000 个值为“100”的位置。但是,如果您服用一种药水并将您的生命值增加到 200,那么您可能会将其缩小到以前为“100”而现在更改为“200”的少数几个值。如果您这样做几次,那么您通常可以将其缩小到内存中的一个位置。例如,本地玩家的生命值可能存储在内存地址 0xAABBCCDD。

  但是有一个问题:下次我们开始游戏时,游戏会重新设置世界,而你的玩家的生命值肯定不在同一个内存地址了。作弊引擎之类的工具允许您通过设置断点“找到访问…”该内存位置的内容。再次使用药水,断点触发,现在您知道游戏的哪个部分访问了该内存位置。

  现在,您不仅拥有 Health,还拥有 Player->Health(这是一种简化,实际上,您从 0xAABBCCDD 转到具有类似 [0x00FF00FF+0x8] 的偏移量的指针,其中 0x00FF00FF 是您的玩家对象在内存中的位置,而 0x8是 Player->Health 的偏移量。很可能 Player->Mana 位于 +0x12 或内存中的下一个位置。可以重复此过程,直到 Game->Player->Health 最终成为地址相对于程序的入口点。

  换句话说,即使重新开始游戏,我们现在也可以随时读取玩家的健康状况。

  这个过程可以对库存、技能、怪物、职位等进行重复。我们能找到的信息越多,写作弊器就越容易。

  如果我们的游戏使用客户端权威,那么我们实际上可以在内存中修改玩家的健康值!如果我们使用服务器权威,那么我们仍然可以在内存中对其进行修改,但更改仅在此客户端上可见。服务器不信任客户端的健康值,下次将新的健康值同步到客户端时,内存中的值将再次被覆盖。

这就是 Mirror 的 [SyncVar] 的工作原理!您可以在 Cheat Engine 中修改它们,但没有人在意,因为它们是服务器权威。

使查找内存位置变得更加困难
  通过指针和偏移量查找内存位置的过程很麻烦。每当游戏发生变化时,偏移量也会发生变化。例如,如果以前我们有

class Player
{
    int Level; // at +0 in memory
    int Health; // at +4 in memory
    int Mana; // at +8 in memory
}

并且游戏改为:

class Player
{
    int Class; // at +0 in memory
    int Level; // NOW at +4 in memory
    int Health; // NOW at +8 in memory
    int Mana; // NOW at +12 in memory
}

然后作弊开发者将不得不再次手动搜索内存中的所有偏移量。处理这个有点痛苦。

偶尔弄乱内存布局是使逆向工程更加困难的好方法。防止逆向工程是回报与努力的函数。如果黑客最终只能每月赚取 10 美元,那么没有人会每天花费 10 个小时进行逆向工程。我们越努力,它就越不值得。

投影内存值
  这是一个有趣的小技巧,实际上可以在任何游戏中完成,没有太大的风险。我们可以存储一个预计值,而不是直接存储 Health,例如:

struct AntiCheatInt
{
    int projected;
    public int Value
    {
        get => projected + 1;
        set => projected = value - 1;
    }
}

  这是一个简化的例子,但我们的想法是不要将我们的“100”健康直接存储在内存中。相反,我们存储由一个或更复杂的投影修改的值。这已经使整个作弊引擎的初始查找过程非常令人沮丧,同时几乎没有增加任何风险。如果您在 Unity 中执行此操作,就不会真正出错。

投影内存值是一种使作弊开发更加烦人的简单方法。请注意,这会对性能产生轻微影响,并且请注意,这仅在您在 Unity 中使用 IL2CPP 时才有用。

在防止作弊时,在让作弊者的生活更加艰难和不惹恼诚实的玩家之间有一个很好的平衡。一些技术,如 UPX 打包(见下文)很可能会惹恼所有人。投射记忆等其他技术很少有可能惹恼任何人。

使访问内存变得更加困难
  有多种技术可以让逆向工程一开始就更加痛苦。例如:

  • 动态变化的假入口点,例如使用 exe 打包,如 UPX 打包程序。这些并不难解开,但它增加了难度。

    • 请注意,UPX 打包的可执行文件通常被标记为病毒。
  • 通过 IsDebuggerPresent 检测 Cheat Engine/MHS 等调试器。请注意,这很容易解决,因为每个人都知道 IsDebuggerPresent arleady。更高级的技术可能涉及测量指令之间的时间等技巧。例如,如果我们在运行时使用 StopWatch 测量一个简单的整数乘法并最终花费几毫秒,那么很可能有人使用调试器单步执行此代码。

  • 在防止逆向工程方面,使用 Themida 或 Enigma Packer 等工具进行虚拟化是圣杯。如果在常规进程中查找内存位置很困难,那么在虚拟机内部的进程中查找内存位置要困难几个数量级。当我们过去对游戏进行逆向工程时,我们永远不会仅仅因为努力与回报不值得而接触虚拟化流程。没有人会花半年时间分析你的虚拟机指令,除非你的游戏像魔兽世界那么大。

    • 请注意,虚拟化的可执行文件通常被标记为病毒。您需要一个未标记为病毒的自定义虚拟化引擎。

请注意,许多这些技术在 Unity 中可能存在风险,它已经通过从 C#->IL(->IL2CPP->Assembly) 引入了几层复杂性。混乱的入口点破坏某处的可能性很高。根据经验,在任何情况下都使用 IL2CPP,因为它将游戏从 IL 更改为 Assembly,这已经很难进行逆向工程了。如果作弊成为一个严重的问题,请考虑虚拟化。

  现在我们了解了作弊是如何发展的,我们可以看看一些常见的作弊是如何工作的以及如何防范它们。

Ollydbg/IDA/Code Caves

假设您的游戏具有以下功能:

void SetHealth(int health)
{
    this.health = health;
}

这可能会产生(简化的)汇编代码,例如:

...
mov edi, eax // edi is this.health, eax is the new value
...

黑客可以使用高级调试工具将您的游戏程序集修改为:

...
mov edi, 100 // always sets health to 100
...

  可以直接修改游戏自己的汇编代码,而不是使用作弊引擎搜索和修改内存值。

  修改游戏的程序集对于开发黑客来说非常强大。 Code Caves 常用于向游戏中注入自定义函数,例如:

...
JMP 4 // jump to our custom code
...
mov edi, eax // do the original thing
... custom code ... // do whatever we want
JMP 2 // jump back to the original function
...

在 C# 中,这相当于用户将自己的代码注入到我们的 Health 函数中,例如:

void SetHealth(int health)
{
    CodeCave(health);
}

void CodeCave(int health)
{
    this.health = health;
    // do all kinds of magic here
    // for example, if health==0 then call the code
    // that clicks on the Respawn button to respawn
    // automatically.
}

  这是一个简化的示例,但需要了解的一种非常常见的技术。为了防止自定义程序集,生成 exe 文件的校验和可能是明智之举。

穿墙外挂 / ESP

  在第一人称射击游戏中,穿墙外挂是最常见的作弊方式之一。人们可以修改您的可执行文件以向玩家展示墙后。这是相当容易做到的,而且很常见。

  为了防止它:

  • 使逆向工程尽可能困难(见上一章)

  • 使用 Mirror 的 Interest 管理不显示远处的玩家。您可以实现基于 raycast 的 Interest 管理,其中仅在实际看到玩家时才将其发送给客户端。

    • 请注意,您需要某种容忍度以足够早地发送它们,例如在他们被看到之前发送 1s。这并不完美,但总比让玩家一直看到所有其他玩家要好。Interest 管理对此非常重要。
  • 在运行时检测穿墙外挂并禁止作弊者使用它们。

  这是一个难题,即使是像反恐精英这样的流行游戏也很难处理这个问题。这是一场持久战。

速度外挂

  如果您选择使用客户端权威移动是因为它更容易,那么您最终很可能会在游戏中遇到速度外挂。 速度外挂可以通过多种方式实现,从简单地修改内存中的 Player.Speed,到在 Unity 中很难解决计算机时钟速度问题。

为了防止它:
  检查服务器上的移动速度。允许对网络条件有一定的容忍度。许多游戏允许 10-15% 的容差,但任何高于此的容差很可能是速度外挂。

Bots

  如前所述,机器人特别邪恶,因为它们不需要任何真正的作弊或客户权限。此外,如果您周围的每个人都是机器人,它们可能会扰乱您的游戏经济并让玩家觉得它根本不好玩。

为了防止它:

  • 使查找内存位置变得困难。参见上面的章节。
  • 偶尔调整你的内存布局。有时在 Player.Health 和 Player.Mana 之间添加不必要的值。
  • 偶尔调整您的网络协议。最先进的机器人甚至不需要读取你的记忆。他们直接使用游戏的发送/接收功能。偶尔修改你的 NetworkMessage 操作码和布局,你会让逆向工程非常痛苦。
  • 通过校验和、名称等检测已知的 Bot.exe 进程。
    • 请注意,这经常将您的游戏标记为病毒。游戏不应该寻找正在运行的进程。
  • 检测服务器上的机器人模式。如果机器人成为一个严重的问题,我会这样做。
    • 在最简单的形式中,如果有人连续一周全天候 24/7 玩,那么它可能是机器人,或者在极少数情况下是网吧里的某个人。
    • 如果玩家总是在同一个地点使用相同的路径或关卡,那么它可能是一个机器人。
  • 在您的游戏中添加一个报告按钮。调查报告的玩家。尝试与他们交谈,看看他们是否有回应等。
  • 在活动频繁的地方生成蜜罐怪物。如果某个区域在一段时间内有大量怪物击杀,则在该区域生成一个外观明显不同但非常强大的怪物。普通玩家会注意到并转移到其他地方一段时间。机器人会撞到它并死去。

  同样,这些都是对复杂问题的简化答案。如果你的游戏成功了,那将是一场持久战。这很好,只要你知道你甚至在打一场仗。

无声无息,延时检测

  游戏犯的最大错误之一是让用户知道何时检测到作弊或逆向工程工具。它所做的只是让逆向工程师知道在哪里查看代码以禁用检查。

  如果我们完成所有检测作弊和调试器的工作,我们应该保持安静,并利用这些信息对我们有利。与其大声宣布作弊尝试,不如悄悄地向服务器发送一些信息。在数据库中标记玩家。

  不要立即禁止或踢任何人。等待随机的时间会更聪明。

  • 用户可能会在一个月内尝试多种不同版本的作弊。
  • 逆向工程师可能会使用不同的工具并以不同的方式修改游戏。

  如果我们每个月只封禁一次,那么导致封禁的原因就完全不明显了。它将引入巨大的周转时间来测试哪些作弊被检测到,哪些没有。

静默检测是我们战胜作弊者的最有力工具。利用时间和信息对您有利。

免费游戏与付费游戏

  这是我很可能也会为我自己的游戏做的最后一个考虑因素。虽然免费 2 人游戏非常适合吸引大量玩家,但如果您只是一个小型独立开发者,还没有准备好应对成群的假账户和黑客,那么付费游戏可能会很有价值。

  人们必须支付一次性费用才能玩您的多人游戏,这带来了一个巨大的障碍,如果黑客和作弊者被禁止,他们将不得不再次购买您的游戏。此外,它增加了一定程度的验证,以确保人们不能一遍又一遍地创建帐户。如果需要,您可以禁止信用卡等。

总结

  总而言之,作弊是一个复杂的话题,永远不会有最终的解决方案。恕我直言,使您可以提供的一切都具有权威性。对于移动,至少旨在使其在某个时候具有服务器权威性,例如当您开始看到第一个速度外挂或实际上有一些喘息空间时发布您的游戏。

  一旦你的游戏取得成功,可能会有人来攻击。你可以做很多事情来让它变得更难。

  最终,它是努力与奖励的函数。你越是让作弊变得烦人,人们就越有可能不会打扰或只是转向更容易的目标。

  这个话题可以写满一本书,但我希望你能从中学到一些基础知识。

你可能感兴趣的:(Unity,Mirror,unity,Mirror,网络)