如何最好地防止人们黑掉基于PHP的Flash游戏高分榜?

212

我正在谈论一款没有上限得分并且无法通过重播动作等方式在服务器上验证分数的动作游戏。

我真正需要的是在Flash / PHP中尽可能强大的加密方法,并且防止人们以除了我的Flash文件之外的方式调用PHP页面。 我曾经尝试过一些简单的方法,比如多次调用单个得分并完成校验和/斐波那契序列等,还使用Amayeta SWF Encrypt混淆SWF,但它们最终都被黑客攻破了。

由于StackOverflow的回复,我现在从Adobe找到了一些更多信息 - http://www.adobe.com/devnet/flashplayer/articles/secure_swf_apps_12.htmlhttps://github.com/mikechambers/as3corelib - 我想我可以用它们来进行加密。但不确定这是否可以规避CheatEngine。

如果AS2和AS3不同,我需要了解它们的最佳解决方案。

主要问题似乎是诸如TamperData和LiveHTTP headers之类的东西,但我了解还有更先进的黑客工具,如CheatEngine(感谢Mark Webster)。


你应该设置分数限制,没有理由不这样做,可以按级别、总体等设置。 - mpm
第一个链接已经无法使用。 - Mike
18个回答

415

这是互联网游戏和竞赛中的一个经典问题。你的Flash代码需要与用户一起决定游戏的分数。但是,用户是不可信的,而Flash代码则运行在用户的计算机上。你可能会面临困难,因为没有什么方法可以防止攻击者伪造高分:

  • Flash比你想象的更容易被逆向工程破解,因为其字节码已经被很好地记录并描述了一种高级语言(Actionscript)——当你发布Flash游戏时,你实际上就是发布你的源代码,无论你是否知道。

  • 攻击者控制Flash解释器的运行时内存,这意味着任何知道如何使用可编程调试器的人都可以随时更改任何变量(包括当前得分),或更改程序本身。

针对你的系统的最简单的攻击方法是通过代理运行游戏的HTTP流量,捕获高分保存并回放更高的得分。

你可以尝试通过将每个高分保存绑定到游戏的单个实例来阻止此类攻击,例如,在游戏启动时向客户端发送加密令牌,它可能看起来像:

hex-encoding( AES(secret-key-stored-only-on-server, timestamp, user-id, random-number))

(你也可以使用会话cookie实现同样的效果。)

游戏代码将此令牌与高分保存一起发送回服务器。但攻击者仍然可以只需再次启动游戏,获取令牌,然后立即将该令牌粘贴到重新播放的高分保存中。

因此,接下来您需要提供不仅令牌或会话cookie,还需要提供一个加密高分的会话密钥。这将是一个128位AES密钥,本身使用硬编码到Flash游戏中的密钥进行加密:

hex-encoding( AES(key-hardcoded-in-flash-game, random-128-bit-key))

现在,在游戏发布高分之前,它会解密高分加密会话密钥。这是因为您已将高分加密会话密钥解密密钥硬编码到Flash二进制文件中。使用解密后的密钥,您将高分数与高分数的SHA1哈希一起加密:

hex-encoding( AES(random-128-bit-key-from-above, high-score, SHA1(high-score)))

服务器上的PHP代码会检查令牌,以确保请求来自有效的游戏实例,然后解密加密的高分,并检查高分是否与其SHA1相匹配(如果跳过此步骤,解密将只产生随机且很高的高分)。

现在攻击者反编译您的Flash代码,并快速找到AES代码,这明显突出,即使没有,他也可以通过内存搜索和追踪器在15分钟内找到它(“我知道我的游戏得分是666,让我们在内存中找到666,然后捕捉任何涉及该值的操作---哦看,高分加密代码!")。有了会话密钥,攻击者甚至无需运行Flash代码;她获取游戏启动令牌和会话密钥,就可以发送任意的高分。

现在,大多数开发人员都放弃了---或多或少地与攻击者纠缠了几个月,方式如下:

  • 使用XOR操作混淆AES密钥

  • 使用计算密钥的函数替换密钥字节数组

  • 在二进制代码中散布伪造的密钥加密和高分提交。

这基本上都是浪费时间。不用说,SSL也无法帮助您;当两个SSL终端点中的一个是恶意的时,SSL无法保护您。

以下是一些真正可以减少高分欺诈的方法:

  • 要求登录以玩游戏,让登录生成会话cookie,并且不允许同一会话有多个未完成的游戏启动或同一用户有多个并发会话。

  • 拒绝持续时间小于历史上最短实际游戏时间的游戏会话提交的高分(对于更复杂的方法,请尝试为持续时间低于平均游戏时间2个标准偏差的游戏会话设定“隔离”高分)。确保服务器端跟踪游戏持续时间。

  • 拒绝或隔离那些只玩过一两次游戏的登录帐户的高分记录,这样攻击者就必须为每个登录创建一个看起来合理的游戏过程“纸质记录”。

  • 在游戏过程中进行“心跳”得分(例如,在一次游戏过程中查看得分增长情况),因此您的服务器可以拒绝不遵循合理得分曲线(例如从0跳到999999)的高分记录。

  • 在游戏过程中捕捉“快照”游戏状态(例如弹药量、关卡位置等),以便稍后与记录的中间得分进行对比。即使一开始没有检测这些数据的异常方法,您只需收集它们,如果出现问题,则可以回头分析它们。

  • 禁用任何未通过安全检查的用户帐户(例如提交未经验证的加密高分记录)。

  • 请记住,这里只是在阻止高分欺诈。你无法完全防范它。如果你的游戏中涉及到金钱,有人将会打败你设计出的任何系统。目标不是停止这种攻击,而是让攻击的代价大于仅仅成为游戏高手并赢得胜利。


    24
    我创建了一个在线游戏社区,我们设计了整个奖励系统以奖励前10%的得分而不仅仅是最高分,这样做效果非常好,因此绕过问题也是有帮助的! - grapefrukt
    1
    不错!简而言之,除非游戏在服务器上“运行”(或者至少在服务器上运行一个健全性检查),否则无法防止滥用。多人射击游戏也有同样的问题:如果客户端被修改以获得更好的瞄准,你无法在网络协议中防止这种情况发生。 - Kerrek SB
    3
    我想也可以记录所有用户操作并在服务器上重放它们来验证/确定高分。 - I3ck

    25
    你可能正在问错问题。你似乎关注的是人们使用哪些方法来提高他们在高分榜上的排名,但是只阻止特定方法的使用是有限的。我没有使用TamperData的经验,所以无法评论它。
    你应该问的问题是:“我如何验证提交的分数是有效和真实的?”具体的方法依赖于游戏。对于非常简单的益智游戏,你可以将得分与特定的起始状态和导致结束状态的移动序列一起发送过去,然后在服务器端重新运行游戏,使用相同的移动方式。确认陈述的分数与计算出的分数相同,仅在它们匹配时接受该分数。

    2
    这是我以前在投票系统中使用过的一种技术。你无法防止人们作弊,但你可以记录他们的行为,以便检查是否存在作弊。对于我们的投票系统,我们曾经存储时间戳和 IP 地址。[续。] - Luke
    1
    这样我们至少可以看到作弊行为的模式,并在必要时取消资格。我们已经成功地使用了多次。 - Luke

    18
    提供高分值的加密哈希值与得分本身一起是一种简单的方法。例如,通过HTTP GET发布结果时: http://example.com/highscores.php?score=500&checksum=0a16df3dc0301a36a34f9065c3ff8095 计算此校验和时应使用共享秘密;该秘密不应通过网络传输,而应在PHP后端和Flash前端中进行硬编码。上面的校验和是通过将字符串“secret”前置到分数“500”并运行md5sum来创建的。
    尽管此系统将防止用户发布任意分数,但它不能防止“重放攻击”,其中用户重新发布以前计算过的分数和哈希组合。在上面的示例中,500分总是会产生相同的哈希字符串。可以通过在要哈希的字符串中添加更多信息(如用户名、时间戳或IP地址)来减轻部分风险。尽管这无法防止数据的重播,但它将确保数据集仅对单个用户在单个时间内有效。
    为了防止发生任何重放攻击,必须创建某种类型的挑战-响应系统,例如以下内容:
    1. Flash游戏(“客户端”)执行HTTP GET,无需参数的http://example.com/highscores.php。该页面返回两个值:随机生成的值和与共享秘密组合的加密哈希值。该盐值应存储在待处理查询的本地数据库中,并应具有与其关联的时间戳,以便它在一分钟后“过期”。
  • 闪存游戏会将盐值与共享密钥结合起来,计算出一个哈希值,以验证其与服务器提供的哈希值是否匹配。这一步是为了防止用户篡改盐值,并验证盐值确实是由服务器生成的。
  • 闪存游戏会将盐值、共享密钥、最高分值和任何相关信息(昵称、IP地址、时间戳)结合起来,计算一个哈希值。然后通过HTTP GET或POST将此信息与盐值、最高分值和其他信息一起发送回PHP后端。
  • 服务器以与客户端相同的方式结合接收到的信息,并计算一个哈希值,以验证其与客户端提供的哈希值是否匹配。同时,服务器还验证盐值是否仍在待处理查询列表中。如果这两个条件都成立,则将高分写入高分表,并向客户端返回签名的“成功”消息。同时,服务器还会从待处理查询列表中移除盐值。
  • 请记住,如果共享密钥对用户可访问,则上述任何技术的安全性都会受到威胁

    作为替代方案,可以通过强制客户端通过HTTPS与服务器通信并确保客户端预配置为仅信任您拥有访问权限的特定证书颁发机构签名的证书来避免其中一些来回过程。


    11

    我喜欢tpqf说的话,但与其在发现作弊时禁用账户,不如实施一个蜜罐,这样每次他们登录时,都会看到自己的被黑客攻击的分数,并且从不怀疑自己已被标记为恶意用户。搜索"phpBB MOD Troll",你将看到一个巧妙的方法。


    1
    这种方法的问题在于,这些作弊者会在留言板等地相互吹嘘,因此并不能真正阻止他们。 - Iain
    7
    我不同意。这听起来是一个非常聪明的方法。 - bzlm

    4
    在被接受的答案中,tqbf提到你可以通过内存搜索分数变量来查找分数("我的分数是666,所以我在内存中寻找数字666")。
    但有一个解决方法。这里有一个类:http://divillysausages.com/blog/safenumber_and_safeint 基本上,你需要一个对象来存储你的分数。在setter中,它会将您传递给它的值与随机数字(+和-)相乘,在getter中,您需要将保存的值除以随机因子以获得原始值。这很简单,但有助于防止内存搜索。
    此外,请查看PushButton引擎幕后人员的视频,他们谈论了一些不同的方式来对抗黑客攻击:http://zaa.tv/2010/12/the-art-of-hacking-flash-games/。他们是这个类的灵感来源。

    3
    我想分享一种解决方案...我创建了一个分数增加的函数(你总是会得到+1的分数)。首先,我从一个随机数开始计算(比如说14),然后在展示分数时,只需将分数变量减去14。这样做的原因是,如果攻击者正在寻找20分,他们将找不到它(在内存中它将是34)。其次,由于我知道下一个分数应该是什么,我使用Adobe加密库来创建下一个分数应该有的哈希值。当我必须增加分数时,我检查增加后分数的哈希值是否等于它应该有的哈希值。如果黑客已经在内存中更改了分数,则哈希值不相等。我进行了一些服务器端验证,当游戏得分和PHP返回的分数不同时,我就知道涉及作弊行为。以下是我的代码片段(我使用Adobe加密库MD5类和随机密码加密盐。callPhp()是我的服务器端验证函数)。
    private function addPoint(event:Event = null):void{
                trace("expectedHash: " + expectedHash + "  || new hash: " + MD5.hash( Number(SCORES + POINT).toString() + expectedHashSalt) );
                if(expectedHash == MD5.hash( Number(SCORES + POINT).toString() + expectedHashSalt)){
                    SCORES +=POINT;
                    callPhp();
                    expectedHash = MD5.hash( Number(SCORES + POINT).toString() + expectedHashSalt);
                } else {
                    //trace("cheat engine usage");
                }
            }
    

    使用这种技术和SWF混淆,我成功地阻止了黑客的攻击。此外,当我将分数发送到服务器端时,我会使用自己的小型加密/解密函数。代码如下(未包含服务器端代码,但可以看到算法并在PHP中编写):

    package  {
    
        import bassta.utils.Hash;
    
        public class ScoresEncoder {
    
            private static var ranChars:Array;
            private static var charsTable:Hash;
    
            public function ScoresEncoder() {
    
            }
    
            public static function init():void{
    
                ranChars = String("qwertyuiopasdfghjklzxcvbnm").split("")
    
                charsTable = new Hash({
                    "0": "x",
                    "1": "f",
                    "2": "q",
                    "3": "z",
                    "4": "a",
                    "5": "o",
                    "6": "n",
                    "7": "p",
                    "8": "w",
                    "9": "y"
    
                });
    
            }
    
            public static function encodeScore(_s:Number):String{
    
                var _fin:String = "";
    
                var scores:String = addLeadingZeros(_s);
                for(var i:uint = 0; i< scores.length; i++){
                    //trace( scores.charAt(i) + " - > " + charsTable[ scores.charAt(i) ] );
                    _fin += charsTable[ scores.charAt(i) ];
                }
    
                return _fin;
    
            }
    
            public static function decodeScore(_s:String):String{
    
                var _fin:String = "";
    
                var decoded:String = _s;
    
                for(var i:uint = 0; i< decoded.length; i++){
                    //trace( decoded.charAt(i) + " - > "  + charsTable.getKey( decoded.charAt(i) ) );
                    _fin += charsTable.getKey( decoded.charAt(i) );
                }
    
                return _fin;
    
            }
    
            public static function encodeScoreRand(_s:Number):String{
                var _fin:String = "";
    
                _fin += generateRandomChars(10) + encodeScore(_s) + generateRandomChars(3)
    
                return _fin;
            }
    
            public static function decodeScoreRand(_s:String):Number{
    
                var decodedString:String = _s;
                var decoded:Number;
    
                decodedString = decodedString.substring(10,13);         
                decodedString = decodeScore(decodedString);
    
                decoded = Number(decodedString);
    
                return decoded;
            }
    
            public static function generateRandomChars(_length:Number):String{
    
                var newRandChars:String = "";
    
                for(var i:uint = 0; i< _length; i++){
                    newRandChars+= ranChars[ Math.ceil( Math.random()*ranChars.length-1 )];
                }
    
                return newRandChars;
            }
    
            private static function addLeadingZeros(_s:Number):String{
    
                var _fin:String;
    
                if(_s < 10 ){
                     _fin = "00" + _s.toString();
                }
    
                if(_s >= 10 && _s < 99 ) {
                     _fin = "0" + _s.toString();
                }
    
                if(_s >= 100 ) {
                    _fin = _s.toString();
                }           
    
                return _fin;
            }
    
    
        }//end
    }
    

    然后我将变量与其他虚拟变量一起发送,但它在途中就丢失了...... 为了一个小的flash游戏而付出如此多的努力,但是当涉及到奖品时,有些人就变得贪婪了。 如果需要任何帮助,请给我写私信。

    祝好,Ico


    3

    根据我的经验,这个问题最好从社交工程的角度来解决,而不是编程问题。不要着重于让作弊变得不可能,而是通过消除作弊的动机使其变得无聊。例如,如果主要的动机是公开可见的高分,那么只需延迟显示高分即可显著减少作弊,因为这样可以消除作弊者的正反馈循环。


    12
    但这也会消除实际玩家即时高分的正反馈,因此会降低游戏玩法的质量。 - Chris Garrett

    3
    使用已知的(私有的)可逆密钥进行加密将是最简单的方法。我不是很了解AS,所以我不确定有哪些加密提供者。
    但您可以包括像游戏长度(再次加密)和点击计数等变量。
    所有这些东西都可以被反向工程,因此考虑添加一堆垃圾数据来迷惑人们。
    编辑:也许值得投入一些PHP会话。当他们点击开始游戏时启动会话,并记录时间(如本帖子的评论所述)。当他们提交分数时,您可以检查他们是否真的有一个开放的游戏,并且他们没有提交太快或太大的分数。
    可能值得计算一个标量,以查看每秒/每分钟游戏的最高分数是多少。
    这些事情都不是无法规避的,但在Flash中有一些逻辑可以帮助人们看到它。

    任何遵循这种方法的人都必须包括一个时间,并验证时间,否则您将容易受到重放攻击的威胁。 - Tom Ritter

    3

    您不能信任客户端返回的任何数据。必须在服务器端执行验证。虽然我不是游戏开发人员,但我确实制作商业软件。在这两种情况下都可能涉及资金,人们会破解客户端模糊技术。

    也许可以定期将数据发送回服务器并进行一些验证。不要专注于客户端代码,即使您的应用程序存在于其中。


    2
    一种新的受欢迎的街机模式的实现方式是将数据从flash发送到php,再返回到flash(或重新加载),最后再返回到php。这样可以对比数据并绕过POST数据/解密黑客等任何想要做的事情。其中一种方法是将2个随机值从php分配到flash中(即使运行实时flash数据抓取器,也无法获取或查看这些随机值),使用数学公式将分数与随机值相加,然后使用相同的公式来检查它是否匹配分数,最终到达php端。这些随机值也永远不可见,并且它还计时正在进行的交易,如果时间超过几秒钟,则会标记为作弊,因为它认为您已停止发送以尝试找出随机值,或运行数字通过某种密码器来返回可能的随机值以与分数值进行比较。
    如果你问我,这似乎是一个相当好的解决方案,有人看到使用这种方法的任何问题吗?或者有可能绕过它的方法吗?

    Wireshark。Wireshark总是洞。 - aehiilrs
    是的,我尝试过这种方法...嗯...不知怎么的,一些合法用户被记录为作弊者(该函数在我的电脑上始终有效,但在其他一些电脑上无效)...所以现在我只允许作弊...我记录那些未通过上述检查的人和得分非常高的人,然后手动惩罚他们...通过将他们的得分乘以-1 :P 他们通常会很好地接受这个玩笑。 - Sigtran
    1
    它实现这一点的一种方式是将 2 个随机值从 PHP 分配到 Flash 中(即使运行实时 Flash 数据抓取器,你也无法获取或查看这些值)。请告诉我,是否有一种方法可以在不被 Firebug 或任何其他工具监视的情况下从 PHP 传递值到 Flash? - mkto

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接