目录 :
一、行业现状分析
二、竞品分析
三、产品功能结构分析
四、抗攻击能力分析
五、总结
随着智能手机性能与移动网络质量近年来的提升,玩家能体验大型高清游戏,未来的手游行业一定会越来越好。游戏产业发展迅速,游戏市场将继续保持繁荣。然而,蓬勃发展的游戏市场背后,却暗藏着诸多无处安放问题,外挂、破解、漏洞、资源盗用、恶意攻击、盗号、色情信息、拉新人等游戏安全问题层出不穷,侵害了游戏玩家和厂商的利益。这意味着,游戏厂商不仅需要把游戏做好,更需要提前部署安全风险对抗策略,才能延长游戏生命周期。上半场流量盛行,用户爆发式增长时代已经过去,目前游戏面临的问题:
(1)游戏用户群体增长放缓,用户群体趋于固定,迈入存量时代。
(2)由于国家相关政策,严格管控游戏产品发行,市场新产品有所减少。
(3)游戏市场投资相对减少,导致游戏市场活力减弱。
基于以上几点游戏走向精细化运营才能保持竞争力。
由于疫情原因,手游成了很多人排压解闷的选择之一,2020年第一二季度手游迎来一波增长。根据Newzoo《2020全球游戏市场报告》全球游戏玩家数量将持续增长,到2023年将超过三十亿。
全球游戏市场收入将在2020年达到1593亿美元,同比增长9.3%。亚太地区市场2020年游戏收入达784亿美元,同比增长9.3%,占全球游戏收入近一半。
一片和谐美好景象的背面也存在许多无处安放的阴暗面。
来自“2019中国移动游戏质量白皮书”对手游安全方面的数据统计显示:
• 海外作弊玩家显著增长,作弊形式与国内趋同;定制外挂与破解版占比提升,手游外挂黑产增加。
• 致命级安全问题占比增长24.8%,外挂严重程度在上升。
• 研发期占比最高的漏洞类型为“违规操作”,运营期除“动作射击”游戏外,其他品类外挂开始增长。
“动作射击”为运营期外挂占比最高游戏品类,其他品类外挂开始增长,动作射击游戏外,休闲竞技、赛车竞速、MOBA、动作游戏也出现较多外挂,游戏从业人员需要普遍性关注外挂风险和预防。
• 移动游戏的类型主要包括:卡牌游戏、赛车竞速、动作射击、角色扮演、多人在线竞技场、策略战棋、动作游戏、大型多人在线、模拟经营、休闲益智、冒险游戏、体育竞技、其他游戏等十四个大类,各类型游戏面临的外挂情况。
随着FPS手游在全球范围内的流行,海外手游的作弊玩家比例提升2.4倍,已与国内手游作弊玩家占比持平。
• 手游外挂从实现方式上一般可以分为四类:通用修改器,定制外挂,破解版游戏客户端,脚本辅助类工具。
选择国内排名前三的反外挂产品对比分析,分别为:腾讯MTP、 网易netProtect、梆梆。
产品定位接近,类别相同,用户群体一致,该三个产品为直接竞品,都工具属性明显。
今年云游戏在国内开始兴起,在云游戏场景下,游戏并不在玩家终端,而是在云端服务器中运行,打破硬件限制、免去下载安装、兼容性适配、跨平台多端体验,任何设备,随时随地想玩就玩。目前有代表性的分别是:网易云游戏平台、腾讯START、西山居云游戏、百度云游戏、红手指云手机,云游戏与物理真机对比。
从上面对比来看云游戏是存在很多优势,但这种新的模式能否成熟落地,仍然存在很多疑问。
为什么要把一个看似八竿子打不着的云游戏做为反外挂产品的间接竞品?因为大多数比较变态的外挂功能实现主要是对游戏核心代码修改,比如资源文件、游戏引擎文件、U3D DLL脚本、hook时间方法实现加速器等。
云游戏运行在云端可以很容易防止以上外挂问题,虽然解决问题和解决方案都不同,但是足也做到直接把对手按在地上来回摩擦。
腾讯产品自身就有明显优势,千万级DAU的游戏都在使用,网易次之,主要以网游主。
根据MTP的结构层框架,产品功能结构如下。
通过对产品的各模块进行逆向分析整理出整体架构。
外挂、破解、修改器、变速器、资源盗用、多开、模拟器、协议破解、Ddos攻击。
从云端获取反外挂脚本,解密、解析执行,根据传入的Key得到value解密脚本,代码如下:
int __fastcall GetScript(_DWORD *a1)
{
_DWORD *v1; // r4
int v2; // r6
int v3; // r5
int v4; // r6
int v5; // r2
int v6; // r0
unsigned __int8 v7; // cf
char *v8; // r2
char *v9; // r3
int v11; // [sp+8h] [bp-14h]
int v12; // [sp+Ch] [bp-10h]
v1 = a1;
v2 = GetScriptType(a1) << 16;
v3 = GetScriptType(v1) | v2;
v4 = 0;
if ( v3 )
{
v5 = v1[1];
if ( (unsigned int)(v5 + v3) <= v1[2] )
{
v11 = v1[1];
v12 = v5 + v3;
v6 = j_malloc(v3 + 1);
if ( v6 )
{
v7 = __CFADD__(*v1, v11);
v4 = v6;
j___aeabi_memcpy(v6); // 拷贝脚本密文
*(_BYTE *)(v4 + v3) = 0;
v1[1] = v12;
Decsrc(v4, v3, v8, v9); // 解密脚本
}
}
}
return v4;
}
以下面解密后两条脚本为例:
module_exists("libhoudini_415c.so")
is_root()&&is_hidden_malware_exists("GGuardian")&&module_size_in_range("libh.so",12288,20480)
脚本语法分析代码如下:
signed int __fastcall Parsing_Words(int a1, int a2, int a3)
{
int v3; // r4
int v4; // r3
int v5; // r5
signed int result; // r0
signed int v7; // r2
_DWORD *v8; // r3
int v9; // r0
unsigned __int8 *v10; // r2
int v11; // r0
int v12; // r0
_BYTE *v13; // r6
int v14; // r6
int v15; // r0
int v16; // r2
const char *v17; // r1
int v18; // r2
const char *v19; // r1
_BYTE *v20; // r0
int v21; // r0
int v22; // r2
int v23; // r3
int v24; // r1
unsigned int v25; // r0
signed int v26; // r1
signed int v27; // r2
signed int v28; // r0
unsigned int v29; // [sp+Ch] [bp-28h]
_BYTE *v30; // [sp+10h] [bp-24h]
unsigned int v31; // [sp+14h] [bp-20h]
unsigned int *v32; // [sp+18h] [bp-1Ch]
unsigned __int8 *v33; // [sp+1Ch] [bp-18h]
unsigned __int8 *v34; // [sp+20h] [bp-14h]
int v35; // [sp+24h] [bp-10h]
v35 = a2;
v3 = a1;
v4 = *(_DWORD *)(a3 + 1020);
v5 = 0;
if ( v4 >= 0 )
{
*(_DWORD *)(a3 + 1020) = v4 - 1;
v5 = *(_DWORD *)(a3 + 4 * v4);
}
if ( *(_DWORD *)(v5 + 264) != 2 )
{
*(_DWORD *)(a1 + 16) = 1;
return free_0(v3, v5);
}
v7 = *(_DWORD *)(a2 + 1020);
v8 = (_DWORD *)(a2 + 1020);
if ( v7 > -1 )
{
v9 = v7 - 1;
*v8 = v7 - 1;
if ( v7 )
{
v10 = *(unsigned __int8 **)(a2 + 4 * v7);
*v8 = v9 - 1;
v11 = 4 * v9;
if ( v10 )
{
v34 = *(unsigned __int8 **)(a2 + v11);
if ( v34 )
{
v32 = (unsigned int *)(a2 + 1020);
v33 = v10;
v12 = malloc_0(v3, 272);
if ( !v12 )
{
LABEL_61:
free_0(v3, v34);
free_0(v3, v33);
return free_0(v3, v5);
}
v13 = (_BYTE *)v12;
j___aeabi_memclr4(v12);
v13[1] = 0;
*v13 = 48;
v29 = sub_E0A392E8(v34);
v31 = sub_E0A392E8(v33);
v30 = v13;
if ( !j_strcmp(v5, "+") )
{
v18 = v31 + v29;
v19 = "%lu";
v20 = v13;
}
else
{
if ( !j_strcmp(v5, "-") )
{
v18 = v29 - v31;
v19 = "%lu";
}
else
{
v14 = v35;
if ( !j_strcmp(v5, "*") )
{
v16 = v29 * v31;
v17 = "%lu";
goto LABEL_55;
}
v15 = j_strcmp(v5, "/");
if ( v31 && !v15 )
{
v16 = sub_E0C9840C(v29);
v17 = "%lu";
LABEL_55:
j_sprintf(v30, v17, v16);
LABEL_56:
v25 = *v32 + 1;
if ( v25 < 0xFF )
{
v26 = 0;
if ( *(_DWORD *)(v14 + 1024) == 1 )
v26 = 1;
*(_DWORD *)(v3 + 20) = v26;
*v32 = v25;
*(_DWORD *)(v14 + 4 * v25) = v30;
}
else
{
*(_DWORD *)(v3 + 16) = 1;
}
goto LABEL_61;
}
v21 = j_strcmp(v5, "%");
if ( v31 && !v21 )
{
sub_E0C97FAC(v29, v31, v22, v23);
v16 = v24;
v17 = "%lu";
goto LABEL_55;
}
if ( !j_strcmp(v5, "==") )
{
v16 = 1;
if ( v29 != v31 )
v16 = 0;
v17 = "%d";
goto LABEL_55;
}
if ( !j_strcmp(v5, "!=") )
{
v16 = 1;
if ( v29 == v31 )
v16 = 0;
v17 = "%d";
goto LABEL_55;
}
if ( !j_strcmp(v5, ">=") )
{
v16 = 1;
if ( v29 < v31 )
v16 = 0;
v17 = "%d";
goto LABEL_55;
}
if ( !j_strcmp(v5, "<=") )
{
v16 = 1;
if ( v29 > v31 )
v16 = 0;
v17 = "%d";
goto LABEL_55;
}
if ( !j_strcmp(v5, ">") )
{
v16 = 1;
if ( v29 <= v31 )
v16 = 0;
v17 = "%d";
goto LABEL_55;
}
if ( !j_strcmp(v5, "<") )
{
v16 = 1;
if ( v29 >= v31 )
v16 = 0;
v17 = "%d";
goto LABEL_55;
}
if ( !j_strcmp(v5, "&&") )
{
v27 = 1;
v28 = 1;
if ( !v31 )
v28 = 0;
if ( !v29 )
v27 = 0;
v18 = v27 & v28;
v19 = "%d";
}
else
{
if ( j_strcmp(v5, "||") )
{
*(_DWORD *)(v3 + 16) = 1;
goto LABEL_23;
}
v18 = 1;
if ( !(v31 | v29) )
v18 = 0;
v19 = "%d";
}
}
v20 = v30;
}
j_sprintf(v20, v19, v18);
LABEL_23:
v14 = v35;
goto LABEL_56;
}
}
}
}
result = 1;
*(_DWORD *)(v3 + 16) = 1;
return result;
}
语法解析出来后,如果是一个方法,找到对应的模拟方法的Hander,(比如解密第一条脚本得到module_exists方法与参数libhoudini_415c.so,这条脚本判断是否为模拟器)计算方法名crc,通过crc查找上面表中对应的方法地址并执行。
text:E0A376A2 99 69 LDR R1, [R3,#0x18] ; jumptable 00058628 case 1
.text:E0A376A4 40 B4 PUSH {R6}
.text:E0A376A6 01 BC POP {R0}
.text:E0A376A8 88 47 BLX R1 ; 执行脚本对应方法
.text:E0A376AA 01 B4 PUSH {R0}
.text:E0A376AC 04 BC POP {R2}
.text:E0A376AE F0 49 LDR R1, =(aLd_0 - 0xE0A376B4)
.text:E0A376B0 79 44 ADD R1, PC ; "%ld"
.text:E0A376B2 00 E3 B loc_E0A37CB6
module_exists方法代码如下:
signed int __fastcall module_exists(int a1, _BYTE *a2)
{
int v2; // r6
signed int v3; // r5
signed int v4; // r6
int v6; // [sp+Ch] [bp-10h]
v2 = a1;
if ( a2 && *a2 && (v6 = sub_E0A57BC8()) != 0 )
{
v3 = 0;
do
{
if ( !getsopath(v6) )
break;
++v3;
v4 = 1;
if ( j_strstr() )
goto LABEL_9;
}
while ( v3 <= 9999 );
v4 = 0;
LABEL_9:
sub_E0A57C44(v6);
}
else
{
sub_E0A37194(v2);
v4 = 0;
}
return v4;
}
其它脚本类似的流程执行,再将执行后的结果打包加密发送给服务器,包体结构如下:
struct TssSdkEncryptPkgInfo
{
unsigned int cmd_id_;
const unsigned char *game_pkg_;
unsigned int game_pkg_len;
unsigned char *encrypt_data_;
unsigned int encrypt_data_len_;
};
struct TssSdkDecryptPkgInfo
{
const unsigned char *encrypt_data_;
unsigned int encrypt_data_len;
unsigned char *game_pkg_;
unsigned int game_pkg_len_;
};
从攻击者的角度来看能否被外挂攻击是肯定的,只是时间与成本问题。
每一条脚本方法对应一个汇编解释器方法,只有分析清楚每一条脚本执行逻辑才能知道反外挂的整体逻辑,如果反外挂逻辑比较复杂,难度还是相当高的,再加上通信层主要使用socket加密传输,增加外挂制作者分析难度。
腾讯从端游时代开始,游戏与用户体量一直很大,游戏安全方面有一定的积累,凭借多年的经验能够实现对游戏外挂、安全能力也能在手游安全对抗过程中完成能力进化。
总体来说腾讯反外挂产品架构设计方面比较灵活,游戏侧可以在服务端结合上报数据制定脚本策略进行对抗。比如:当游戏遇到一些定制的外挂时,反外挂目前特征暂未覆盖到的外挂时,可以根据该外挂的作弊原理,制作针对该外挂的定制脚本,将该脚本动态下发到游戏客户端,运行的游戏客户端解析该脚本后,对定制的外挂进行针对性的检测和打击。可做到实时更新检测算法逻辑。
1)减小产品体积优化用户接入体验。
2)解释器引擎逻辑精简减少对游戏性能的影响。
3)可通过脚本控制心跳与广播频率,减小在低端机型上性能开销。