十月一的假期间,在知乎上看到一个问题《网页游戏都有哪些安全问题?》, 我是一个网页游戏开发者,对这个问题很是感兴趣,印象比较深入。当时是在游玩,也没时间细看这个问题。后来,在微博上,有一位朋友的转发,又让我看到这个 问题,冥冥中,有种想回答的冲动。上周六时,研发部门内部周会时,听到其余项目组的一个整型溢出问题,致使刷钱的bug,又让我想起这个问题,更加坚决我 要回答这个问题的决心,总结一下这项目中,全部经历过的webgame安全问题的经验,以加固当前项目安全壁垒,避免损失。亦可分享给其余作 webgame研发的朋友,作交流探讨。php
知乎中的原问题是『网页游戏都有哪些安全问题?』,我以为不妥,我给改为了『网页游戏都有哪些安全问题?如何作得更安全?』,同时,问题也从『你们 来研究探讨一下,网页游戏攻防技术。一定,这个话题很敏感。目前,网页游戏已经不少了,会不会被黑产盯上?网页游戏会不会被黑,数据库会不会被拖库』改为 了『你们来研究探讨一下,网页游戏攻防技术。一定,这个话题很敏感。目前,网页游戏已经不少了,会不会被黑产盯上?网页游戏会不会被入侵?入侵方式有哪 些?如何作好网页游戏的入侵防护?挽救措施有哪些?如何才能最小化减小厂商损失?入侵方式有哪些?如何作好网页游戏的入侵防护?挽救措施有哪些?如何才能 最小化减小厂商损失?』,更改的理由是『本文原提问者开篇提到「你们来研究探讨一下,网页游戏攻防技术。」,那么应该不光提到如何入侵,更应该提到如何防护,应该细心描述漏洞造成原理,规避方式,以提升研发者技能水平;应该详细讲解安全事件发生后,如何最小化减小厂商损失,减小用户损失,保护游戏平衡。』,幸运的是,这个修改,被知乎经过了。对此,表示感谢。html
在后来阅读这篇提问以及回答时,已经有几位网友回答了,多数是站在安全工做者角度上回答了这个问题。在这篇日志里,我将以webgame研发者角度,切合游戏业务模块逻辑,从业务需求,数据库设计,程序编写,操做方式上来说解漏洞造成原理,规避方案,也欢迎你们讨论。前端
登陆认证
近几年,网页游戏几乎都是以联运方式运营,意味着游戏服务器自己不保存用户密码,用户登陆在平台,经过平台跟游戏服务器的接口对接登陆。接口作加密认证。 故webgame的账号密码安全问题,这里不提了。但登陆认证的hash字符串安全,也仍是要注意的。好比登陆hash字符串的生效时间,hash字符串 的加密参数来源,好比包括用户名、登陆IP,浏览器user-agent等数据,以防止改hash被泄漏了,也是很难经过服务器的验证。java
游戏充值
webgame的游戏充值流程,跟普通网页充值流程一致,没有特殊的地方,其不一样点就是跟其余众多平台作联合运营时,势必要每一个公司作接口对接,且接口规 范各式各样,且游戏厂商没有话语权,必须按照他们的接口规范来,这实在棘手。腾讯的充值接口的验证方式,安全性作的较为突出,大约代码:mysql
02 |
$signKey = array ( 'openid' , 'appid' , 'ts' , 'payitem' , 'token' , 'billno' , 'version' , 'zoneid' , 'providetype' , 'amt' , 'payamt_coins' , 'pubacct_payamt_coins' ); |
05 |
foreach ( $signKey as $key ) { |
06 |
if (isset( $data [ $key ])) |
08 |
$sign [ $key ] = $data [ $key ]; |
11 |
######开始生成签名############ |
13 |
$url = rawurlencode( $url ); |
18 |
foreach ( $sign as $key => $val ) |
20 |
$arrQuery [] = $key . '=' . str_replace ( '-' , '%2D' , $val ); |
22 |
$query_string = join( '&' , $arrQuery ); |
24 |
$src = 'GET&' . $url . '&' .rawurlencode( $query_string ); |
26 |
$key = $this ->config->get( 'qq_appkey' ). '&' ; |
28 |
$sig = base64_encode (hash_hmac( "sha1" , $src , strtr ( $key , '-_' , '+/' ), true)); |
29 |
if ( $sig != $data [ 'sig' ] ) { |
31 |
$return [ 'msg' ] = '请求参数错误:(sig)' ; |
32 |
$this ->output->set(json_encode( $return )); |
在此基础上,还能够作的严谨点:程序员
- 增长随机参数名、参数值。随机参数名、参数值由联运方随机生成,按照参数名的字符串所属ASCII码顺序排序,参数名、参数值均参与sign的计算,增长暴力破解密钥(app key)难度。
- 增长回调验证订单号,金额信息。游戏充值服务器接收到充值请求时,反向到该平台回调接口,确认此笔订单有效性,以防止加密密钥泄漏的问题。
远程文件引入
在网页游戏的研发中,多数都是使用框架来作,即便用REQUEST来的参数,做为请求文件名的一部分,来使用,那么很容易造成远程文件引入的漏洞。在咱们以前的游戏中,曾出现过一例这样的漏洞问题。web
4 |
if ( ! file_exists (APPROOT. 'controllers/' .load( 'Router' )->getDirectory().load( 'Router' )->getClass().EXT)) |
6 |
load( 'Errors' )->show404( 'Unable to load your default controller. Please make sure the controller specified in your Routes.php file is valid.' ); |
8 |
include (APPROOT. 'controllers/' .load( 'Router' )->getDirectory().load( 'Router' )->getClass().EXT); |
9 |
load( 'Benchmark' )->mark( 'load_basic_class_time_end' ); |
webgame中的远程文件引入redis
从代码以及案例图中,能够看到对于REQUEST的参数没有过滤处理,直接做为文件名来include引入的,故致使这种问题,相似上页图中QQ群 网站的漏洞。若PHP version < 5.3.4 ,还会发生Null(%00) 截断的问题,带来更大的安全问题。在咱们新的项目中,咱们更改了实现方式,咱们游戏全部接口都会走gateway,gateway里,对控制器名作类名规 范的检测处理,再在指定几个目录下作autoload加载文件,且还会对REQUEST的类名、方法用ReflectionClass反射类的处理,检测 到类、方法、参数是否合法。一来避免『远程文件引入』漏洞问题,二来便于先后端联调时,抛出更详细的异常,方便调试。下面为参考代码:算法
01 |
require_once CONFIG_PATH . "/auto.php" ; |
02 |
spl_autoload_register( "__autoload" ); |
08 |
$view ->error(MLanguages::COM__INVALID_REQUST); |
09 |
$msg = new Afx_Amf_plugins_AcknowledgeMessage( $val ->data[0]-> $messageIdField ); |
10 |
$msg ->setBody( $view ->get()); |
11 |
$message ->data = $msg ; |
14 |
$a = new Yaf_Request_Simple(); |
15 |
$a ->setControllerName( $method [0]); |
16 |
$a ->setActionName( $method [1]); |
17 |
$objC = new ReflectionClass( $method [0]. "Controller" ); |
18 |
$arrParamenter = $objC ->getMethod( $method [1]. "Action" )->getParameters(); |
19 |
$arrRequest = isset( $val ->data[0]->body[0]) ? ( array ) $val ->data[0]->body[0] : array (); |
21 |
foreach ( $arrParamenter as $objParam ) |
23 |
$parm = $objParam ->getName(); |
24 |
$bIsOption = $objParam ->isOptional(); |
25 |
if (isset( $arrRequest [ $parm ])) |
27 |
$a ->setParam( $parm , $arrRequest [ $parm ]); |
29 |
elseif ( $objParam ->isOptional()) |
40 |
$rp = $app ->getDispatcher()->dispatch( $a ); |
41 |
$msg = new Afx_Amf_plugins_AcknowledgeMessage( $val ->data[0]-> $messageIdField ); |
42 |
$msg ->setBody( $view ->get()); |
43 |
$message ->data = $msg ; |
SQL 注入
SQL注入原理、方式,跟普通web应用同样,没什么特别的,在使用REQUEST来的参数时,过滤处理便可。可能在消息格式,以及注入操做简便上,会蒙蔽研发人员的眼睛,被忽略掉了。好比咱们项目的AMF消息格式,在前端界面没出来以前,咱们后端程序员通常使用Pinta来模拟操做,调试程序。前端界面出来以后,会使用Charles proxy来捕捉http请求。在这些过程当中,请求接口、参数的构造,没有普通web那么简单。研发人员也容易忽略对请求参数的过滤,故很容易造成这种问题。造成原理见:《WEB开发安全与运维安全浅见》,防护方式作过滤处理,或SQL预编译。
sql
AMF消息格式的WEBGAME中的SQL注入
AMF消息格式的WEBGAME中的SQL注入
为了提升游戏服务器的吞吐能力,网页游戏的架构也是一直在演变的。在以前以Mysql做为数据存储的webgame架构中,其余节点都是能够水平扩 展,或者说依赖简单粗暴的增长服务器来解决,单单做为惟一数据存储中心,不能这么作。为此,不少webgame的数据存储改用Nosql来代替,甚至 java、C/C++的游戏数据,直接在内存中操做,游戏关服时,才写入到DB中。故SQL注入的问题,也会愈来愈少。
通信协议与消息格式
网页游戏虽然名字叫网页游戏,但通信协议并不是全是http,也有不少使用socket,以及http+socket并用的作法。咱们是http协 议+amf消息格式,以及socket并用来实现。在http与https的取舍上,咱们考虑到ssl的启用后,大量的ssl解密加密运算,势必会增长服 务器大量的CPU计算压力。而传输的内容,多数是游戏业务的操做,响应,是能接受被监听嗅探的行为的(认证信息除外)。站在安全角度,这不能理解。但站在 产品角度,考虑一下 投入产出,而后选择http通信,也是能够理解的。socket在咱们游戏中,除了在聊天应用上使用外,在一些组队、帮派战之类须要多个玩家之间同步数据 信息时,咱们也会使用socket来推送数据。在使用socket做为全部业务传输的协议时,协议格式通常都是开源协议,好比msgpack、protobuf之类,或者自定义的协议。使用自定义协议时,务必检测整个消息包的每个参数,类型范围,避免个别超大数值、边界数值出现,致使主程序内存越界,以致于服务宕机,没法正常服务的状况发生。
金币复制-整型溢出
上周周六开周会时,听到其余项目组的一个关于整型溢出致使产生刷金币的问题。在这里,我抽象该案例,分享一下。商城出售开启背包格子的所需道具『梧桐 木』。在游戏中,用户包裹格子数量通常都会做为一个收费点,一款游戏的格子大约为每行7格子,一共8行这样。好比前面3行是默认开放的,第4行是收费的, 并且第一个格子所需品梧桐木的价格1个银子,第二个梧桐木是2个银子,第三个是4个银子。依次类推,意味着这些梧桐木的价格总和其实就是一个第一项为1, 公比为2,项为35的等比数列。 当用户选择购买梧桐木数量大于31时,好比32-36中这些数字时,这些等比数列的和就是大于2147483647。(只是举例,实际上不会以这样的价格 出售物品)
在java中,4字节的存放int型变量的范围是-2147483648至2147483647。在java、c的有符号int型中存储时,数的最高位描述符号位,4字节共32位,除去最高位的符号位,剩下31位,每一个位上能表示2个数字,4字节的有符号的整数表示范围为:负整数2^31个,范围为『-1至-2147483648』;正整数2^31个,范围为『2147483647至1』。 好比下图(注意十进制数字跟二进制表示的变化顺序):

当开启格子数字为大于31时,好比32,那么所需费用就是2147483647个银两,再买点其余物品,凑成超过2147483647的数字,好比又买了3个银子的其余道具,总共花费2147483650个银子,在4字节的有符号int中表示出来的结果,变成符号位为1,即负整数。数值位为0000000 00000000 00000000 00000010,也就是10000000 00000000 00000000 00000010,对应十进制的-2147483646。 程序逻辑上,再判断现有银两是否足够支付此笔花费时,是经过的。当使用当前余额减去这笔花费时,将变成减去一个负数,那么实际上就是加上一个正整数。变成 了本身银两帐户余额的增加。而余额字段类型是long,则正确的存储了这些余额,溢出漏洞被利用。在C中,使用无符号的数值类型,便可完成数值类型溢出刷 钱的行为,但在java中,好像没有无符号的类型。这也能够先肯定全部参与计算的数值必须为正整数做为必要条件(游戏业务特性,游戏内全部数字,确定全为 正整数,甚至都不包括零),先作大小判断,再作正正相加,不能得负;负负相加,不能得正。来判断是否发生了溢出问题。在PHP中,不用担忧溢出问题。
金币复制-并发请求
Rpg类型的网页游戏中,多数都有道具出售的功能,直接卖到商店,以及道具材料从商店买入功能。当玩家同时针对买入、卖出两个操做,瞬间大量并发请求时,在服务器的处理逻辑通常有分别的两个进程处理,共享数据分别数据库中的对应帐户余额表,以下图:
webgame买入、卖出并发请求处理
03 |
$iBalance = $obj ->getBalance( 'user1' ); |
06 |
if (! $obj ->setBalance( 'user1' , $iBalance + 100)) |
11 |
if (! $obj ->delItems( $items )) |
19 |
$iBalance = $obj ->getBalance( 'user1' ); |
21 |
if (! $obj ->setBalance( 'user1' , $iBalance - 50)) |
26 |
if (! $obj ->addItems( $items )) |
卖出请求的处理进程为1,买入请求的处理进程为2。在进程1还没将结果写入到DB时,进程2也从DB读取到余额为50。这是,两个进程拿到的余额信 息都是50。进程1按照逻辑代码,计算出剩余余额是150;进程2计算出的剩余余额是0。最后,无论那个进程最后写入余额,都是错误的结果。(注:这里的 代码逻辑操做,跟mysql事务无任何关系,事务只能保证单个进程的事务范围内多条语句都正确执行,或回滚。好比能保证扣钱成功,且物品删除掉的两个语句 都正确执行。能保证其中之一的语句执行失败时,都正确回滚。)
其实,在事物开启时候,SELECT语句是否能够取到最新的数据,或者是否须要等待锁释放,取决于MYSQL的事务隔离级别。在MYSQL的事务隔离级别中,有一下几种隔离级别:

- READ-UNCOMMITTED(读取未提交内容)级别
- READ-COMMITTED(读取提交内容
- REPEATABLE-READ(可重读)
- SERIERLIZED(可串行化)
对于READ-UNCOMMITTED,能够读取其余事务中未提交的数据,并且听说性能还高不到哪里去,几乎没有在实际应用中使用;对于READ- COMMITTED,在同一事务中,会由于其余事务随时可能有新的commit,致使同一select可能返回不一样结果。这个也不适合游戏业务;再说第四 个SERIERLIZED,只要事务开启,全部其余查询,均排队等待该事务提交以后,对于上面提到的卖出买入状况,第二个事务的SELECT操做,不会立 刻返回,会处于锁等待状态,一直到前一个事务结束。这个隔离级别,虽然能避免上面的问题,但性能较差,通常不会去使用。而REPEATABLE-READ 隔离级别,也是mysql默认的隔离级别,从功能上,比较符合游戏业务须要,也应该是广大webgame架构中mysql的默认隔离级别。
对于这个问题,你可能很快就给出解决办法,把UPDATE语句改成UPDATE `role_gold` SET gold = gold + 100 WHERE role_id = 1或者UPDATE `role_gold` SET gold = 150 WHERE role_id = 1 AND gold = 100来解决,但这种多个事务同时操做修改多个表的多条记录时,还容易引起死锁问题,好比《webgame中Mysql Deadlock ERROR 1213 (40001)错误的排查历程》。并且,当条件为跨表内数据是否存在,或者另外条件不在MYSQL中,而在其余网络接口的响应中时,如何作呢?
金币复制--逻辑漏洞
引用DNF的漏洞新闻 《利用网游漏洞狂刷游戏币赚钱 玩家自曝3天赚17万》
玩家曝出刷币漏洞 一个游戏道具可刷400人民币
该漏洞究竟是什么?原来游戏中“云幂袖珍罐”这个道具,能够开出2件同样的游戏装备,还有极少概率开出游戏币,开出的装备不值钱,但若是开出金币了,则分 为5000万、8000万以及1亿游戏币。而1亿游戏币,按正常市场行情,可在交易网上卖400多元人民币。据玩家称,在游戏中,角色的装备是须要用包裹 来存放的,不过目前角色的包裹最多只有48格,也就是只能存放最多48件装备。漏洞就是利用包裹的有限空间,存放47件装备(存放满了又没法开罐子),只 留下一格空位,而在开“云幂袖珍罐”出装备时,就会因包裹空间不足,而致使开罐失败,而罐子还存在。玩家继续开罐,直到出现金币,但金币不会占据包裹的空 间,所以开罐成功,而后罐子消失。发现这个漏洞后,部分玩家狂刷游戏币,而后立刻在第三方交易平台出售游戏币,兑换成现金。
这种问题,都是研发人员逻辑不严谨致使,这种问题,也较难发现。规避方式能够依赖下面提到的『运营数据监控』。
道具复制--背包整理
跟上面的卖出、买入同样,同时穿装备、整理包裹。在设计时,可能会将身上装备设计在装备表中;将不在身上的装备,设计到背包表中。当同时进行穿装备 跟整理包裹的请求并发时,也会发生跟上面卖出,买入的状况,线程1读取DB,发现包裹里有这装备,而后准备删除背包表的这条记录,当准备写入到装备表时, 另一个整理包裹请求的线程来了,读取了整个背包表,进行道具的合并、排序。这时,以前的线程将这个装备写入到装备表,并删除了背包表里的数据,并提交事 务。这个穿装备的全部操做都是合理、正常,且正确执行的。但另一个整理背包的线程读取了以前的背包表里的数据,包括那件被穿上的装备。 在游戏中,整理背包须要对可堆叠道具作堆叠操做的,意味着须要合并多个道具,删除部分道具。这意味着这里的操做,当前cgi线程的内存中的数据,将都会以 覆盖的形式,写入到DB中,那么意味着,以前被穿到身上的那件装备,也会从新被写入到背包中。那就变成两张表里出现了两个相同惟一ID的相同属性的道具。 玩家就能够把背包中的这个道具出售给其余玩家。
在java或者C之类程序中,数据放内存中的游戏,也会存在这个问题,除非作读锁,但读锁会带来锁等待,锁等待会致使线程被占用,阻塞后面请求的处 理,堆积大量请求。致使系统负载升高,服务器繁忙,以致于没法响应。好了,大约理解道具复制的造成缘由了吗?这个问题,咱们从根本缘由想一想,问题到底出现 在哪里?如何规避呢?细心的同窗不难发现,对于穿装备的操做结果,会对下一个请求产生影响的,当前操做未获得服务端响应以前,服务端是不能处理下一个响应 的。对此,咱们作了响应处理锁--『用户并发请求锁』。
用户并发请求锁的实现,php中session以文件形式存储时,php会对session文件加锁,不释放(若是不特地执行 session_write_close),知道当前响应完成。另一个线程才能够正常读取,这简介的造成了单个用户的并发请求锁,可是,后面的进程一直 处于等待状态,也会占用一个php-fpm进程,阻塞其余用户的正常请求对php线程的使用。为此,咱们使用NOSQL的K-V形式结构,以 user_name为key的形式,实现用户并发请求锁,第一个请求,生成这个k-v数据,后一个请求发现有这个key了,那么马上抛出异常,结束响 应,FLASH根据异常内容,提醒用户不要进行恶意操做。即不会发生并发请求,又不会阻塞请求处理。同时,在请求结束的析构函数里,对这个锁进行删除操 做,不影响下一个正常请求。若由于程序异常,发生语法错误,致使析构函数无法执行,没有删除用户锁时,能够在生成锁的时候,设置过时时间,好比5秒,甚至 2秒,利用nosql的过时机制,实现用户解锁,避免用户长时间没法正常游戏。
类CC攻击-多用户共享资源锁的timebomb
咱们如今研发的项目,是以NOSQL Redis做为DB,来存储数据的,redis并无成熟的事务处理机制,watch甚至算不上关系型数据库中的事务处理。对此,更须要对表进行加锁解 锁。java之类语言的项目,不少都是直接操做内存的,更是须要资源锁,来解决并发问题,解决多个请求操做同一份数据的问题。公司有另一个项目,出现过 一次由于锁的颗粒度较大,带来的锁等待timebomb的问题,也致使了线程繁阻塞忙,请求堆积,系统负载上升,致使宕机的问题。这个项目的锁是针对全部 用户的锁,每一个用户的请求发来时,当前线程会对全部用户的数据加锁,直到响应完成,才释放掉。这么作,是为了解决因当前操做,会影响到其余用户数据,好比 多人PK,多个玩家之间的交互。

当其余请求一并发来时,那么资源会马上被锁住,直到上一个请求结束,才释放锁,那么其余线程都处于等待状态。用户基数小时,是看不出来锁带来的影响的,内 存操做都比较快。当用户基数大时,或者说请求数增大时,后面的请求的等待时间会愈来愈长,超过webserver的等待时间,直接返回timeout,不 能正常提供服务。
这种问题的发生,是由于锁的颗粒太大了,不该该将全部用户都锁住,最好细化到当前请求所影响到的单个用户,只锁住单个用户的数据。这样,才减小timebomb的发生。
其余
知乎里的朋友提到,不少webgame 的前端作了判断,然后端没作判断的问题,这种问题,实属不应存在。在咱们的项目中,后端作的验证判断,远比前端多的多。有时候,为了界面上的动画表现,前 端flash通常会在用户操做以后,马上渲染,而后,再根据后端响应,决定是否继续作界面元素改动。好比脱装备,玩家操做时,会先渲染装备从角色面板,跳 到背包里的动画,而后,再根据后端响应结果,决定是否回滚动画。这样,避免显得操做后,必定时间的反映迟钝假象,以提升用户体验。固然,后端是必定会作判 断的,判断角色背包是否有空格之类。如今的webgame研发,通常都不会存在前端判断,然后端不判断的作法了。若是有,也应该是个别遗漏状况。
好比去年的time33算法的hash dos的问题,使用json消息格式的webgame必定要注意,php只是在接收请求时,作了最大数量的限制。但在json解码以后的数据中,是没有处理的。这里千万别忘记了。
运营数据异常监控
再完善的防护措施,都仍会有安全漏洞。适当的监控措施,也必定要有,监控等级、金币、游戏币、经验、珍贵物品的变化等等,一旦发现,马上报警,在漏洞未扩散以前,第一时间去修复漏洞,以减小损失,维护游戏平衡。
日志系统
日志系统必定不能漏掉,全部操做,必须写入日志,当安全事件发生后,能够做为各类数据回滚,交易纠纷处理的可靠数据。也是做为数据监控的最准确的数据来源。
若是你用了我画的小清新般的插图,请记得为图片写上署名来源,画图是最花费我时间的一件事。
原文地址:http://www.cnxct.com/experience-with-webgame-of-security-and-defense/