标签归档:游戏

浅谈部落冲突的协议设计

部落冲突(Clash of Clans)是一款风靡全世界的手机游戏。部落冲突的忽然流行造就了又一家神奇的芬兰公司Supercell,同时也带来了很多关于游戏成功秘诀的讨论。在这里我们暂且不论部落冲突为啥会吸引那么多玩家这个话题,简单来看看部落冲突里的底层协议设计。

部落冲突的底层协议可以说设计的非常的好,从技术上说,也是非常先进而且高效的。首先客户端和服务器端采用RC4进行加密。在第一次握手之后,服务器端会传回一个秘钥seed,然后客户端和服务器端把新seed扔给Scramble7算法,产生新秘钥。这个算法在客户端更新之后还经常进行变换,可见Supercell程序员的用功刻苦程度。算法握手大致描述如下:
client.rc4 = RC4(INIT_KEY)
server.rc4 = RC4(INIT_KEY)
client: login(user_id, user_pwd, client_version, device_info, client_seed)
server: login_success(user_id, user_pwd, client_version, client_bind, server_seed)
server: set_key(new_key)
prng = Scramble7(server_seed)
reset_key = prng.get_key(new_key)
client.rc4 = RC4(reset_key)
server.rc4 = RC4(reset_key)

在经过这一系列解密之后,我们才能开始看到明文,知道客户端和服务器之间到底交流了什么。

接下来让我们仔细分析一下部落冲突目前的核心玩法:部落战的协议内容:
client: attackwar(user_id)
server: attackwar_fail(user_id, reason) # If fail
server: attackwar(map_info) # If success
client: action(tick, checksum, [action1, action2, action3, ...])
action_list:
action_army: (x, y, army_id, tick)
action_ally: (x, y, ally_id, tick)
action_end: (tick)
action_spell: (x, y, spell_id, tick)
action_hero: (x, y, hero_id, tick)
action_herospell: (hero_id, tick)
action_start: (tick)
action_data: (data_content, tick)

我们先来看一个设计的非常巧妙的action指令。这个指令的头是tick和checksum,tick是以1/60秒为单位的整数(60就是第一秒,120就是第二秒),而checksum是一个客户端信息的总和。

信息的总和是如何算出来的呢?这里非常巧妙的是,客户端会把目前绝大多数数据做一个求和,再算上当前tick数,得到一个总的checksum。这个checksum非常的关键,因为服务器会校验。服务器如果不了解客户端信息,是怎么校验的呢?很简单,服务器在连接建立之后,会跑着一套和客户端同样逻辑的代码,然后对客户端发来的每一条信息进行checksum校验。

比如说,客户端说我第一秒在某处放了一条龙。客户端经过模拟后,发现第二秒龙会摧毁一个对方的建筑,这时候checksum就会有显著变化,因为一个建筑没了。这时候给服务器的checksum必须有体现,服务器也会跑一个模拟程序,检验第二秒龙摧毁了一个建筑,如果客户端发来的checksum对不上,那么服务器就会断掉连接,认为客户端非法。

所以要做一个脱机离开客户端直接向服务器发送请求的程序很难,因为必须包含客户端完整的模拟战斗逻辑。而这套逻辑是部落冲突的代码核心,每次版本更新都会大改,非常难以完整进行模拟。

在此基础上的协议基本杜绝了第三方脱机机器人的存在。大部分机器人必须依靠客户端才能进行,比如BlueStacks上运行的虚机。

然而,这样的协议之上依然存在很多漏洞。还是拿部落战举例,如果有一个Men-In-The-Middle拦截了打的不好的协议包,只传送打的好的协议包,那么其实是可以保证每一次部落战都完成三星。

在最近的一次更新中,新增了action_start和action_data两条指令,这是做什么用的呢?Supercell为了阻止日益猖獗的叉叉助手和iMod所谓沙盒模拟(就是脱机练习进行演练),将对手的一部分信息(电塔援军陷阱)进行了隐藏。在付出一次攻击代价的情况下,服务器会给客户端返回这些信息,客户端才能进行完整的游戏。大概的逻辑如下:
client: attackwar(user_id)
server: attackwar(map_without_key_info)
client: action(60, checksum_60, [action_start(0)])
server: key_info(data)
client: action(120, checksum_120, [action_data(61, data)])
client: action(180, checksum_180, [action_army(125, x, y, army_id), ...])
......

简单说就是在action_start之后,服务器才会返回关键信息,让客户端能够完整进行模拟。那么action_data的作用是什么呢?由于checksum机制的存在,服务器需要和客户端同步加入这部分关键信息,所以客户端需要在收到key_info之后,告诉服务器,我在何时(tick)把这些key_info加入到地图形成完整地图的。然后服务器收到之后,可以和客户端一起进行模拟,并且校验checksum。

那么目前版本有一个致命的bug,就是服务器对action_data返回的data并没有进行检验。导致的后果就是客户端如果发回的key_info和服务器之前传回的不一样,也能正常模拟下去。如果客户端发一个空的key_info,服务器会认为陷阱电塔援军都神奇的消失了!

当然这种bug可能并不影响普通玩家,可能只有深入研究了协议的才会发现。尽管存在很多bug,但是笔者认为部落冲突依然是一款从技术上设计非常精良的游戏。不管何时,技术上的完善都是一款游戏坚实的基石。如果外挂横行漏洞百出,不管游戏表面有多美丽,都是一摊烂泥无法流行起来。

 

炉石传说如何偷看对面的手牌

炉石传说是一款暴雪最新出的卡牌游戏。这里我们简单对它的协议和客户端机制做一下分析。

炉石传说的客户端是采用的Unity3D+C#制作的。画面精良,美术细腻,可以说达到了 Unity3D 引擎作品中相当高的水准。3D的卡牌效果让人惊叹,暴雪出品确实必属精品啊。(Diablo III 除外)

EX1_062_premium由于采用的是Unity3D,可以预见移植到手机和Pad上毫不费力。因为采用的是C#,可以说缺点也是很显然的。C#的反编译非常的简单(虽然C/C++的反编译也难不到哪里去),大大降低了逆向工程(Reverse Engineering)的成本。这里不得不吐槽一下暴雪对微软系列的热爱,Web Server也统统用的IIS+ASP.NET,不出意外Game Server后端也用的是Windows Server以及SQL Server。可能也与暴雪一直专心于制作Windows Game有关。

下面我们不对客户端内容做任何反编译和分析,仅仅从通讯协议层来简单看看炉石传说的客户端在游戏时都做了什么。

我们先在本地用 tcpdump 监听一下 3724和1119端口,通过看包得内容,可以判断炉石传说的游戏 TCP 链接用的是 Protobuf 协议。Protobuf 协议的标志就是开头以 08 或者 0A 开始。

16:03:32.228771 IP 192.168.1.XX.53668 > XXX.XXX.XXX.XX.battlenet: Flags [P.], seq 229:256, ack 11411, win 8192, options [nop,nop,TS val 1131484283 ecr 57022610], length 27
0x0000: 5866 baf3 8757 040c cede b078 0800 4500 Xf...W.....x..E.
0x0010: 004f 6ddf 4000 4006 be15 c0a8 0158 7271 .Om.@.@......Xrq
0x0020: da42 d1a4 0e8c 4ab6 e5de 32f4 32b5 8018 .B....J...2.2...
0x0030: 2000 f655 0000 0101 080a 4371 147b 0366 ...U......Cq.{.f
0x0040: 1892 0200 0000 1300 0000 0811 1004 1800 ................
0x0050: 20ff ffff ffff ffff ffff 0128 00 ...........(.

Protobuf 是 Google 开源的一个公开协议。然而并不是说知道用 Protobuf 就能理解传得是什么内容。Protobuf 需要有详细的 .proto 参数配置文件定义传输的细节,否则对于其它第三方来说就是一团乱码。

通过一些逆向工程,我们大概知道了这个 .proto 参数配置是怎样的。炉石传说可以说是一个瘦客户端。客户端做的计算分析工作很少。这一点很符合暴雪的特点,比如魔兽世界也是这样的一个瘦客户端,大量的3D碰撞检测计算都放在了服务器端。

炉石传说把所有的客户端需要显示的细节,都通过服务器传来数据,其中甚至包含了客户端动画展示时需要的停顿。炉石传说的服务器端维护了一个 id=1~67,并且可以动态增加的字典,其中 1 代表系统,2 和 3 代表两边的玩家,4~35 是 id=2 玩家的所有牌的编号,而 36~67 是 id=3 玩家的所有牌的编号。系统、玩家、牌,这些共用一个属性表,这大大降低了定义的复杂度。

下面是一些通讯的简单例子:

初始化:
player{ ... 玩家属性初始化 ... }
init{ id:4 name:"HERO_03" attr{HEALTH=30} attr{PUT=1} attr{OWNER=1}
init{ id:5 name:"CS2_083b" attr{COST=2} attr{PUT=1} attr{OWNER=1} attr{CASTBY=4}}
init{ id:6 name:"" attr{PUT=2} attr{OWNER=1}}
........
init{ id:35 name:"" attr{PUT=2} attr{OWNER=1}}
init{ id:36 name:"HERO_08" attr{HEALTH=30} attr{PUT=1} attr{OWNER=2}
init{ id:37 name:"CS2_034" attr{COST=2} attr{PUT=1} attr{OWNER=2} attr{CASTBY=36}}
init{ id:38 name:"" attr{PUT=2} attr{OWNER=2}}
........
init{ id:67 name:"" attr{PUT=2} attr{OWNER=2}}

从这个初始化例子我们可以很清晰的看到,服务器给每个客户端初始化了 67 张“牌”,其中 1,2,3 是系统和两边玩家,4,5是玩家A的英雄卡 HERO_03 和英雄技能 CS2_083b,被放到了战场(PUT=1),6-35是玩家A的牌库,被放到了待抽牌堆(PUT=2),相应的,对面36,37是玩家B的英雄卡 HERO_08 和英雄技能 CS2_034,38-67则是玩家B的牌。

接下来的通讯协议分析就不一一说了,总之就是服务器向客户端通过设置这些牌的属性, 让客户端达到显示牌以及动画效果的目的。

那么有人要说笔者标题党了。技术细节看了一大堆,但是如何能看到对面的牌呢?难道服务器会直接在通讯协议里告诉玩家A,玩家B手里的所有牌么?

其实不然。暴雪在技术细节上做的还是很好的。只有在玩家把牌放入战场,对方才能通过通讯协议知道牌的内容。但是其中有一个小问题,就是:

每一次比赛牌的 id 并不会随机化洗牌!

就是说,玩家A和玩家B如果两次用同一副自己组的牌对打,玩家B如果给玩家A出示过某个 id 对应什么牌的话,那么下一次比赛玩家A就会知道玩家B还在手里的牌是什么!

说通俗一点的话,就是对圣斗士来说,每一招只能用一次,第二次用同样的招数就变得很不安全!

下面是一个和同一个好友同一副牌玩第 2 把的例子:

EX1_561_premium敌方 操作 开始
第 1 回合
玩家 铁喙猫头鹰 牌库 -> 手牌
玩家 刺骨 牌库 -> 手牌
玩家 任务达人 牌库 -> 手牌
玩家 刀扇 牌库 -> 手牌
玩家 幸运币 未知 -> 手牌 施法:
敌方 寒冰护体 牌库 -> 手牌
敌方 变形术 牌库 -> 手牌
敌方 魔爆术 牌库 -> 手牌

在第一回合,我们就清楚的知道了对方先手拿得三张牌是什么!

当然,在平时天梯和竞技场里,这个问题可能不会存在,因为每次遇到的选手都是系统随机分配的,都是第一次遇到。如果是打职业线上比赛的话,就难保不会有人通过这个办法作弊咯。

当然,目前游戏还在内测阶段,相信暴雪技术团队应该会在公测前修复这个问题吧。

除此之外,目前的通讯协议上还有下面几个 BUG:

  1. 对面手牌中的”奥秘牌“会在奥秘出示之前就通过通讯协议让另一方知晓。
    就是说虽然客户端没有提示,但是实际上对面一拿到奥秘牌,你的客户端就可以知道对面的哪一张是奥秘牌,虽然不知道具体是什么奥秘。
  2. 服务器错误的把每一方思考时候的选择数目发给了另一方客户端。
    选择数是你每一步有多少种可能。这是服务器端提前算好告诉客户端,然后让客户端提示用户做选择的。比如在1费回合你没有2费牌,只能点”结束回合“,这时候的选择数就是1个。通过判断对方每回合结束的选择数,你可以大致估计对方手牌里的费的多少,以及是不是法术指向牌。
  3. 虽然是第一次遇到某玩家,但是还是可能通过牌的 id 猜到对方某些手牌的内容。
    注意,因为没有通过随机化洗牌(shuffle),所以牌的 id 实际上是玩家从牌库中选择的顺序。有些玩家喜欢从1费开始选牌,有些喜欢从高费开始,但是一般两张同样的牌都会一起选进去,所以他们拥有相邻的id。比如你看见对方出了一张id=16的炎爆术,手里还有一张id=15的未知牌,那么你就知道下回合多半又是一张炎爆打脸了。

好了,简单写了这些,其实并没有太严重到逆天的 BUG,炉石传说还是一款好游戏。希望暴雪团队继续努力,早点出移动客户端,让笔者可以早点躺着玩。

 

浅谈如何修改游戏 (1)

很多很多年前,那时 DOS 游戏界有着两大逆天神器 GameBuster 和 FPE。不管是仙剑奇侠传还是红警神马的,一旦祭出神器,那是神挡杀神佛挡杀佛,弹指间敌人灰飞烟灭。

到如今几十年的发展,游戏变得越来越网游化和移动化。大游戏动辄十几G的容量,数据都存在服务器,外挂检查严而又严。小游戏在iPhone和Android上,麻雀虽小五脏俱全,对作弊的检查也是非常严格,数据加密和校验方法层出不穷。

然而道高一尺魔高一丈,本文就尝试粗浅的介绍下现如今修改游戏可以做的一些手段。

对于 Windows 游戏修改手段来说,大概可以分为下面一些层次,按照安全性排序:

  • 对数据段的内存读
  • 通过 PostMessage 对游戏模拟键盘和鼠标操作
  • 对 D3D9 的 EndScene 或者 D3D11 的 Present 函数挂钩子,进行远程调用
  • 创建远程线程 (RemoteThread) 去调用执行函数
  • 模拟游戏发送网络底层的命令包
  • 对数据段的内存写
  • 对代码段的直接修改

对游戏的数据段的读取,是最安全最可靠的。游戏本身无法判断其它应用是否对它进行内存读的操作。然而,只靠内存读可以做的事情非常少,不过依然有可能读取到游戏里隐藏给玩家的数据。比如人物的三维坐标、朝向、行进速度、或者隐藏Buff啦,比如法术的实际释放坐标和半径,以及前进路线啦。

想要更进一步的对游戏本身进行操作,那么可以用按键精灵,或者类似的工具,模拟鼠标和键盘事件发送给游戏。但是这依然无法产生逆天的效果,实际上可以做一些机械性重复性特别高的事情。这类工具本身的可侦测性也是非常低的,比较的安全可靠。

EndScene 和 Present 实际上是游戏UI线程一般在绘制完一帧图像之后必须调用的。很多录像软件也是 Hook 的这两个函数。所以实际上这是很安全而且很合法的钩子。在 EndScene 里做操作的好处在于,很多游戏里的操作函数需要运行在游戏UI线程。如果用 RemoteThread 的方式去调用函数往往会导致线程问题然后游戏崩溃。

RemoteThread 调用实际上用于一些简单的判断函数,比如得到某个游戏对象的名称,判断某个 NPC 的敌对状态。这些执行函数不需要在主线程执行,可以用 RemoteThread 飞快的调用完成。RemoteThread 的执行速度和 EndScene Hook 相比往往要快上百倍。

而发送游戏的网络底层命令包,则需要对游戏的网络通讯协议有一定的了解。它可以跳过游戏上层逻辑对命令合法性的判断,直接用底层协议对服务器通讯。通过这个办法可能可以做一些客户端里无法完成,但是服务器却允许的操作。它的安全性也不够高,如果命令非法,也可能造成服务器拒绝继续服务,导致掉线或者客户端崩溃的结果。

对数据段的内存写,实际上是早期游戏修改的大招了。然而,现今绝大多数网络游戏都有防护措施,他们不依赖客户端数据,服务器发现客户端数据不一致,就会拒绝继续服务,导致掉线甚至封号,得不偿失。所以这种手段用到的越来越少了。

对代码段的直接修改,是跳过一些客户端条件检查判断的手段。比如跳过客户端关于法术的 Cooldown 检查,直接向服务器发送法术发送命令。这个办法比直接发送网络底层命令包简单很多,只要对上层游戏逻辑进行修改就可以了,但是它却非常不安全。现今的网络游戏往往具有自我修改的检测能力,比如暴雪的 Warden 会定期的扫描自己可执行文件的代码段,看看有没有被注入钩子或者一些跳过检查的代码。如果被发现了你的客户端在内存中被做了修改,那么就等着过几天静悄悄的被无法登录游戏吧!

想要精确的定位到函数以及变量在内存中的地址,以及数据结构,就需要一些额外的辅助工具了。这里向大家介绍两个工具:Ollydbg 和 IDA Pro。可以说,有这两个工具在手,没有不能修改的游戏。IDA Pro 是非常强大的静态反编译工具,它的插件工具也相当多。读入静态 exe,它可以比较完美的分析出函数地址、函数调用关系、数据结构等。Ollydbg 是动态的运行中反编译工具,通过它,可以对在执行过程中的游戏增加断点,单步调试,等等。这两者可以结合在一起使用。

想要熟练掌握这两个工具,你必须会一些 X86 的汇编指令,可以熟练的编写汇编代码,对 C/C++ 编译有一些了解。有了这些基础之后,就可以随心所欲的对游戏下刀了。当然,有些游戏会有反跟踪和反“反编译”手段,想要修改成功,往往还需要一些耐心和毅力,以及一些直觉和洞察力。

最后,让我们来看看破解过得游戏能做到什么地步。其实很简单,就是能做到游戏的服务器端所容忍的上限。比如游戏客户端允许你的走路速度是 7 码/秒,施法距离是 40 码,而服务器考虑到网络延迟以及客户端的计算误差,往往上限会高一些,比如是 10 码/秒和 43 码。在绕过客户端限制之下,往往可以达到服务器允许的上限。再比如客户端在短期内连续发送很多移动命令,外加计算恰当的时间戳,可以让服务器觉得客户端是出现了卡顿,因此服务器会允许一大串移动命令和一大串偏移时间戳,在服务器看来,客户端卡顿了 10 秒并且移动了 70 码,但是客户端实际只经过了很短时间,做到了瞬间移动。这样的例子需要探索性实验。

如果服务器做的足够精美足够巧妙,其实是可以杜绝任何客户端的修改。然而这需要大量的服务器运算和负载。对于海量游戏数据处理来说,这无疑是会继续增加服务器运营费用和成本。因此很多国产网游的服务器检查非常粗糙和简陋,酝酿出大量的客户端修改和外挂。而好游戏在服务器端的强制检查判断则非常的严格和精妙,从而减少了客户端修改带来的收益。

最后告诫大家,游戏修改破坏平衡性,同时损害了游戏乐趣。网络游戏修改更加破坏了运营商的利益,破坏了游戏的生态环境,要小心被跨省追捕哦。修改有风险,入行需谨慎啊!

下次我们来谈谈 iPhone/Android 修改的一般性方法。