关于动作脚本3:阻止人们入侵Flash游戏的基于PHP的高分表的最佳方法是什么?

关于动作脚本3:阻止人们入侵Flash游戏的基于PHP的高分表的最佳方法是什么?

What is the best way to stop people hacking the PHP-based highscore table of a Flash game

我说的是一个动作游戏,没有最高分数限制,也没有办法通过重播动作等来验证服务器上的分数。

我真正需要的是Flash / PHP中最强大的加密功能,以及一种防止人们通过我的Flash文件以外的方式调用PHP页面的方法。过去,我尝试过一些简单的方法,可以对单个分数进行多次调用,并完成校验和/斐波那契序列等,还使用Amayeta SWF Encrypt对SWF进行模糊处理,但最终它们都被黑了。

多亏了StackOverflow的回应,我现在已经找到了Adobe的更多信息-http://www.adobe.com/devnet/flashplayer/articles/secure_swf_apps_12.html和https://github.com/mikechambers/as3corelib-我想我可以用于加密。不确定这是否会让我了解CheatEngine。

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

主要问题似乎是TamperData和LiveHTTP标头之类的,但是我知道还有更先进的黑客工具-CheatEngine(感谢Mark Webster)


这是互联网游戏和竞赛的经典问题。您的Flash代码可与用户一起确定游戏得分。但是用户不受信任,并且Flash代码在用户的计算机上运行。你是SOL。您无法采取任何措施来阻止攻击者伪造高分:

好。

  • Flash甚至比您想象的要容易进行逆向工程,因为字节码已被很好地记录并描述了高级语言(动作脚本)---发布Flash游戏时,无论您是否在发布源代码,知道与否。

    好。

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

    好。

  • 好。

    对您系统的最简单攻击是通过代理运行游戏的HTTP流量,捕获高分保存并以更高的分数重播。

    好。

    您可以通过将每个高分保存绑定到游戏的单个实例来尝试阻止这种攻击,例如,在游戏启动时通过将加密令牌发送给客户端,如下所示:

    好。

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

    (您也可以使用会话cookie来达到相同的效果)。

    好。

    游戏代码以高分数保存将令牌返回给服务器。但是,攻击者仍然可以再次启动游戏,获得令牌,然后立即将该令牌粘贴到重播的高分保存中。

    好。

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

    好。

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

    现在,在游戏发布高分之前,它将解密高分加密会话密钥,这是可以完成的,因为您已将高分加密会话密钥解密密钥硬编码为Flash二进制文件。您可以使用此解密密钥以及高分的SHA1哈希来加密高分:

    好。

    1
    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个标准偏差的游戏会话,尝试"隔离"高分)。确保您正在服务器端跟踪游戏时间。

    好。

  • 从仅玩过一次或两次游戏的登录中拒绝或隔离高分,因此攻击者必须为创建的每个登录产生合理外观的"纸迹"。

    好。

  • 在游戏过程中," Heartbeat"得分很高,因此您的服务器可以看到在一个游戏过程中得分的增长。拒绝不遵循合理分数曲线的高分(例如,从0跳到999999)。

    好。

  • 游戏过程中的"快照"游戏状态(例如,弹药量,关卡中的位置等),您以后可以将其与记录的临时分数进行核对。您甚至不必以某种方式来检测此数据中的异常。您只需要收集它,然后可以回头再分析一下是否看起来很腥。

    好。

  • 禁用所有未通过安全检查之一的用户的帐户(例如,通过提交未通过验证的加密高分)。

    好。

  • 好。

    请记住,虽然您只是在阻止高分欺诈。您无法采取任何措施来防止这种情况的发生。如果您的游戏中有钱,那有人会打败您想出的任何系统。目的不是阻止这种攻击;这是使攻击更加昂贵,而不仅仅是真正擅长于游戏并击败它。

    好。

    好。


    您可能会问错问题。您似乎专注于人们用来打高分列表的方法,但是到目前为止,阻止特定方法仍然有效。我没有使用TamperData的经验,所以我无法对此讲话。

    您应该问的问题是:"我如何验证提交的分数是有效和真实的?"具体实现方式取决于游戏。对于非常简单的益智游戏,您可以将比分以及特定的开始状态和导致结束状态的移动顺序一起发送,然后使用相同的移动在服务器端重新运行游戏。确认指定的分数与计算出的分数相同,并且仅在匹配时接受分数。


    一种简单的方法是提供您的高分值的加密哈希值以及它自己的分数。例如,通过HTTP GET发布结果时:
    http://example.com/highscores.php?score=500&checksum=0a16df3dc0301a36a34f9065c3ff8095

    计算此校验和时,应使用共享机密。此机密绝不应通过网络传输,而应在PHP后端和Flash前端中进行硬编码。上面的校验和是通过在字符串" secret"之前加上分数" 500"并通过md5sum运行而创建的。

    尽管此系统将防止用户发布任意分数,但是它不能防止"重播攻击",即用户重新发布先前计算的分数和哈希组合。在上面的示例中,分数500始终会产生相同的哈希字符串。通过将更多信息(例如用户名,时间戳或IP地址)并入要散列的字符串中,可以减轻某些风险。尽管这不会阻止数据的重放,但可以确保一组数据仅一次一次对单个用户有效。

    为了防止发生任何重播攻击,必须创建某种类型的质询-响应系统,例如:

  • Flash游戏("客户端")执行不带参数的HTTP GET http://example.com/highscores.php。该页面返回两个值:一个随机生成的盐值,以及与共享机密结合的该盐值的加密哈希。此盐值应存储在待处理查询的本地数据库中,并应具有与其相关的时间戳记,以便它可能在一分钟后"过期"。
  • Flash游戏将盐值与共享机密结合在一起,并计算哈希值以验证其是否与服务器提供的值匹配。此步骤对于防止用户篡改盐值是必要的,因为它会验证盐值实际上是由服务器生成的。
  • Flash游戏将盐值与共享的机密,高分值和任何其他相关信息(昵称,ip,时间戳)结合在一起,并计算哈希值。然后,它通过HTTP GET或POST将该信息以及salt值,高分和其他信息发送回PHP后端。
  • 服务器以与客户端相同的方式组合接收到的信息,并计算一个哈希值以验证它与客户端提供的哈希值匹配。然后,它还会验证盐值仍然有效,如未决查询列表中所列。如果这两个条件都成立,则将高分写入高分表,并向客户端返回签名的"成功"消息。它还从挂起的查询列表中删除盐值。
  • 请记住,如果用户可以访问共享机密,则上述任何一种技术的安全性都会受到损害。

    作为替代方案,可以通过强制客户端通过HTTPS与服务器进行通信,并确保将客户端预先配置为仅信任由特定证书颁发机构签名的证书,而您自己可以访问这些证书,来回避免某些情况。


    我喜欢tpqf所说的,但是与其在发现作弊时禁用帐户,不如实施蜜罐,这样每当他们登录时,他们就可以看到自己的黑名单,并且从不怀疑自己已被标记为巨魔。 Google for" phpBB MOD Troll",您将看到一个巧妙的方法。


    在接受的答案中,tqbf提到您可以对分数变量进行内存搜索("我的分数是666,所以我在内存中寻找数字666")。

    有一种解决方法。我在这里上课:http://divillysausages.com/blog/safenumber_and_safeint

    基本上,您有一个对象来存储您的分数。在setter中,它将您传递给它的值与一个随机数(+和-)相乘,在getter中,您将保存的值除以随机乘数以得到原始值。这很简单,但是有助于停止内存搜索。

    另外,请观看来自PushButton引擎背后的一些人的视频,他们讨论了一些与黑客作斗争的不同方法:http://zaa.tv/2010/12/the-art-of-hacking-flash-游戏/。他们是课堂背后的灵感。


    我做了种变通办法...我给了一个分数增加的地方(您总是得到+1分数)。首先,我开始从随机数开始计数(比如说14),当我显示分数时,只显示分数var减去14。这就是如果饼干要寻找20的情况,他们将找不到它(在内存中将为34)。其次,由于我知道下一点应该是什么...我使用了Adobe加密库,创建了下一点应该是什么的哈希。当我必须增加分数时,我检查增加的分数的哈希值是否等于该哈希值。如果饼干更改了内存中的点,则哈希值不相等。我执行一些服务器端验证,当我从游戏和PHP中获得不同的观点时,我知道涉及作弊。这是我的代码段(我正在使用Adobe Crypto libraty MD5类和随机加密盐。callPhp()是我的服务器端验证)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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编写):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    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游戏来说,这是很多工作,但是在涉及奖品的情况下,有些人会变得贪婪。如果您需要任何帮助,请给我写一封PM。

    干杯,伊科


    以我的经验,这最好是作为社会工程问题而不是编程问题来解决。与其着重于使自己不可能作弊,不如着眼于通过消除作弊的动机使其变得无聊。例如,如果主要诱因是公开可见的高分,则仅需延迟显示高分的时间,即可通过消除作弊者的积极反馈回路来显着减少作弊。


    您不能信任客户端返回的任何数据。验证需要在服务器端执行。我不是游戏开发商,但我确实制作商业软件。在这两种情况下,都会涉及金钱,并且人们会破坏客户端的混淆技术。

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


    使用已知的(私有)可逆密钥加密将是最简单的方法。我还没有完全了解AS,所以不确定是否有哪种加密提供程序。

    但是您可以包含一些变量,例如游戏时长(再次加密)和点击计数。

    诸如此类的所有事情都可以进行逆向工程,因此请考虑放入大量垃圾数据以使人们摆脱气味。

    编辑:也许值得在某些PHP会话中使用。当他们单击开始游戏并(如该帖子的评论所述)记录时间时,开始会话。当他们提交分数时,您可以检查他们是否确实有一个开放游戏,并且他们提交的分数不会太快或太大。

    也许应该计算出一个标量,看看每秒钟/分钟的最高得分是多少。

    这些事情都不是不可回避的,但是在人们可以看到的Flash中没有某些逻辑会有所帮助。


    我认为最简单的方法是,每次游戏注册要添加的乐谱时,都调用诸如RegisterScore(score)之类的函数,然后对其进行编码,打包并将其作为字符串发送到php脚本。 php脚本将知道如何正确解码。这将停止对php脚本的任何调用,因为任何试图强行得分的尝试都会导致解压缩错误。


    新的流行街机游戏mod的执行方式是将数据从闪存发送到php,然后再发送到闪存(或重新加载),然后再发送到php。这使您可以做任何您想比较数据的事情,并且绕过发布数据/解密技巧等。一种做到这一点的方法是,将php中的2个随机值分配给Flash(即使运行实时Flash数据采集器也无法捕获或看到),使用数学公式将分数与随机值相加,然后使用用相同的公式将其反转,以查看分数最终与php匹配时是否与之匹配。这些随机值是永远不会可见的,它还会对事务进行计时,并且如果它超过几秒钟,它也会将其标记为作弊,因为它假定您已停止发送以尝试找出随机值或运行通过某种类型的密码计算数字,以返回可能的随机值以与得分值进行比较。

    如果您问我,这似乎是一个很好的解决方案,有人使用此方法是否会遇到任何问题?还是可能的解决方法?


    只有通过将所有游戏逻辑都保留在服务器端,才可能在用户不知情的情况下在内部存储分数。由于经济和科学原因,人类无法将此理论应用于除回合制之外的所有游戏类型。例如在服务器端保持物理状态在计算上是昂贵的,并且难以以最快的速度响应。甚至有可能,在下国际象棋时,任何人都可以将AI国际象棋的游戏玩法与对手匹敌。因此,更好的多人游戏也应包含按需创意。


    您正在谈论所谓的"客户端信任"问题。因为客户端(使用现金,即在浏览器中运行的SWF)正在执行其设计要执行的操作。保存高分。

    问题是您要确保"保存分数"请求来自您的Flash电影,而不是某些任意的HTTP请求。一种可能的解决方案是在请求时(使用flasm)将服务器生成的令牌编码到SWF中,该令牌必须随请求一起保存才能获得高分。服务器保存该分数后,令牌将过期,不能再用于请求。

    这样做的缺点是,每次加载Flash电影,用户只能提交一个高分-您必须强迫他们刷新/重新加载SWF,然后他们才能再次播放新得分。


    因为它很容易反编译SWF,所以没有办法使其完全不可破解,然后,熟练的开发人员黑客就可以跟踪您的代码,并弄清楚如何绕过您可能采用的任何加密系统。

    但是,如果您只是想通过使用简单的工具(例如TamperData)来阻止孩子作弊,则可以生成一个加密密钥,然后在启动时将其传递给SWF。然后使用类似http://code.google.com/p/as3crypto/的方法对高分进行加密,然后再将其传递回PHP代码。然后在服务器端将其解密,然后再将其存储在数据库中。


    每当您的高分系统基于Flash应用程序通过网络发送未经加密/未签名的高分数据的事实时,就可以对其进行拦截和处理/重放。答案是这样的:加密(正确!)或用密码签名高分数据。至少,这使人们更难破解您的高分系统,因为他们需要从您的SWF文件中提取密钥。许多人可能会在那里放弃。另一方面,只需一个人就可以提取密钥并将其发布到某个地方。

    真正的解决方案涉及Flash应用程序与highscore数据库之间的更多通信,以便后者可以验证给定的分数在一定程度上是现实的。这可能很复杂,具体取决于您拥有哪种游戏。


    我通常在游戏会话的"幽灵数据"中包含高分条目。因此,如果我要制作赛车游戏,则会包含重播数据。您通常已经具有用于重播功能或重影竞速功能的重播数据(在上一场比赛中对战,或在排行榜上与14号花花公子对战)。

    检查这些是非常费力的工作,但是如果目标是验证比赛中排名前10位的参赛作品是否合法,这可能是对其他人已经指出的安全措施的有用补充。

    如果目标是使高分列表在线保留到最后,而不需要任何人查看它们,那么这不会给您带来多少好处。


    通过AMFPHP与后端通信可能是一个好主意。至少应阻止那些懒惰的人尝试通过浏览器控制台推送结果。


    真正不可能实现您想要的。 Flash应用程序的内部总是可以部分访问的,尤其是当您知道如何使用CheatEngine之类的东西时,这意味着无论您的网站和浏览器服务器通信的安全性如何,克服它都将相对容易。


    推荐阅读