一、引言
闲来无事,在浏览微信公众号的时候无意刷到了江西余江警方关于破获全国首例“AI自瞄”类外挂的案件,涉案金额达到惊人的3000余万。不得不感叹近年来AI相关科技发展之迅速及国内有关于FPS类及其他大类游戏作弊的黑产市场之大。
在工作学习之余,本人也曾对非内存对抗类的作弊技术有过浅薄的探究(因为Windows 内核编程学起来真的掉头发),故受到这次案子的启发,浅谈并整理一下我所了解的,近年来国内所出现过、现如今仍然流行的非内存对抗类的外挂程序其中的内部实现原理,以及这些外挂程序为了规避反作弊采取的措施和针对这些措施,以鄙人之拙见列出的反作弊一方可以采取的检测手段(矛与盾的攻防)。
对于游戏反作弊攻防领域,只有知己知彼,才能百战不殆。
本文意在科普除内存外挂以外的外挂作弊程序的实现原理,打破国内部分领域外挂过于“神化”的滤镜,为游戏厂商的反作弊工作提出更多思路,也为正义的游戏玩家科普一些相关领域知识武装自己,开拓见识。文中提及到的所有源码、图片、技术皆为多年收集于各大论坛、QQ群等技术讨论平台,或间接或直接从相关“从业”人员口中得知或由相关程序文件结构反推得出。作者并非相关领域专业人士,文章如有遇到不严谨不正确的知识希望海涵。
本文的外挂类型仅局限于用作FPS类游戏作弊,其余如竞速类、格斗类、角色扮演类等其他类游戏方面的外挂不做涉及。
二、常见的五大类外挂类型
1. 基于颜色识别的“找色类”外挂
“找色类”外挂可以说是国内最早一批除了内存外挂以外的外挂类型。最早的找色自瞄外挂可以追溯到2015年甚至更早。随着大漠插件的问世再加上易语言这一简单易懂的编程语言的普及,简单易用且庞大的第三方模块库让早年间很多不是专门从事计算机领域的爱好者接触到了编写程序的快乐。在那个外挂仅仅是出于个人兴趣而免费制作出来供大家研究学习游玩的年代,找色类外挂就在各大编程论坛广为流传讨论。
找色外挂的核心就是找出游戏画面中敌人或者敌对目标身上的颜色特征,收集这些特征颜色,经过一系列函数处理,在屏幕中找到预先设置好的颜色值并给出这些颜色在屏幕中的精确坐标。得到了这些颜色坐标后,就可以对坐标进行二次加工从而开发出诸如“自瞄”、“自动扳机”、“磁性吸附”等功能。
以国民级端游穿越火线举例。早期游戏中的人物颜色基调大多以暗色调为主,没有如今游戏里面那么多样式、不同颜色风格的人物。只要玩家装备了烟雾头盔这一道具后,游戏中人物的模型上就会默认戴着烟雾头盔(早期还不能隐藏装扮)。
带着烟雾头盔的角色或者不带烟雾头盔的早期角色模型中,其头部都有明显的“黑色”,不论你当前游玩的是什么地图,这个“黑色”的特征颜色并不会变,于是确认了这一特征颜色后,通过工具(比如QQ截图就可以显示鼠标处的颜色RGB色值)获得颜色的RGB数值(或者其他格式类型),调用现成的大漠插件或者其他的第三方库中有关的函数,就可以找到当前颜色在屏幕中的位置。
以大漠插件+易语言举例:
头部 = dm.FindColor (492, 374, 532, 408, #头部颜色值, 1, 0, x, y)
如果 (头部 = 1)
sbwz = dm.GetCursorPos (x1, y1)
x2 = x - x1
y2 = y - y1 + 7
dm.MoveR (x2, y2)
只需要以游戏准星也就是屏幕分辨率的二分之一的位置为中心向外扩张的矩形区域为范围,找到事先采集到的特征颜色,并返回颜色的坐标,拿到坐标再根据当前鼠标的坐标简单计算得到相对位移举例,就可以实现“自瞄”的功能。
这只是简单的找色实现,但是往往游戏的环境十分复杂,万一遇到了地图中恰好也有跟特征颜色值一样的颜色怎么办,那不就“误锁”了吗。聪明的人可能想到了,在找色之前加上前置条件,当触发前置条件后就可以证明当前是马上会与敌人“交战”的情形而不是咱们在地图中闲逛的时候。在穿越火线中,当鼠标准星移动到敌人附近的时候会显示敌人的红色名字,狙击枪开镜瞄准的时候同样也有大面积的黑色,那么就可以将上面的“红色名字”或者“黑色狙击镜”当作前置条件判断,利用FindColor函数,如果找到了上述的两个颜色,就代表即将遇敌,进而寻找特征颜色,那么就可以极大了规避“误识别”。将上面的代码优化一下。
人名 = dm.FindColorE (508, 426, 518, 440, “000000-000000”, 1, 1)
红名 = dm.FindColorE (508, 426, 518, 440, “A53A30-000000|cc4224-000000|f24a17-000000”, 0.9, 0)
如果 (红名 ≠ “-1|-1” 且 人名 ≠ “-1|-1”)
头部 = dm.FindColor (492, 374, 532, 408, #头部颜色值, 1, 0, x, y)
如果 (头部 = 1)
sbwz = dm.GetCursorPos (x1, y1)
x2 = x - x1
y2 = y - y1 + 7
dm.MoveR (x2, y2)
当然将上面的人名色值和红名色值换成黑色狙击镜色值就可以改成“狙击自瞄”。有了坐标可以在加上一些鼠标键盘的动作实现“遇敌快速击杀后换弹”、“狙击后切枪”等骚操作。
利用“寻找特征颜色,判断前置条件,得到坐标数据后进行二次开发”的核心思想,就可以创作出各种类型的“找色类”外挂,由于本文章以科普为目的就不进行展开了。
当然,在后来的找色类外挂发展中,有一些外挂作者另辟思路不仅仅局限于找色,基于图像二值化提取特征轮廓从而达到自瞄的目的而开发的“轮廓自瞄”,由于其问世不久后就被后来出现的AI自瞄所取代,这里就不再单独作为一大类叙述。
2. 基于文字、图像识别的“自动识别压枪宏”
鼠标宏这一技术的出现可谓令人又爱又恨。宏作为计算机运用过程中用于简化操作的一系列系统代码,其研发出来的本意是为了简化各类繁杂的操作,提高生产力。但是被游戏玩家用作游戏中,古往今来争议不断。严格意义上来说各类鼠标键盘宏并不属于外挂,它只是独立于游戏之外又或者内置在鼠标键盘PCB内存中的一段固定代码,最早用作《魔兽世界》里施法的同时使用游戏中的聊天系统发送信息等。后来在FPS游戏中又出现了“鼠标压枪宏”这一类的影响到了游戏公平性的宏。枪械后坐力作为FPS类游戏中一大游戏特色,玩家对于后坐力的控制,可以一定程度区分出玩家游玩游戏时的技术。
众所周知,FPS中的枪械后坐力粗略可分为水平和垂直方向后坐力。往往不同的枪械,垂直方向上的后坐力各有不同,但是垂直后坐力的数据是固定的,而水平后坐力往往是左右的不规则随机散步。所以可以采集不同枪械的垂直后坐力,得到散步坐标,再控制鼠标反向移动与后坐力抵消,就可以达到小范围的枪械抖动,如果收集到的后坐力数据十分精确,也可以做到几乎感受不出后坐力,不用玩家额外“压枪”就可以实现近乎“无后座力”的效果。
压枪宏的核心就是“收集不同枪械的垂直后坐力散步坐标,反向控制鼠标移动再加以适当的水平控制”。所以收集到准确的枪械后坐力坐标就是开发时候的重点。出于这个目的,就可以开发一款工具去收集鼠标坐标的变化,或者使用另一种更加快捷的方式,那就是录制一段枪械的开火视频然后逐帧逐个弹孔去计算时间间隔与弹道偏移坐标量,虽然这样会很麻烦,但是仅仅是一些重复的工作。相反,工具就可以记录下每次鼠标改变的坐标和时间。如当按下预先设定好的按键开始记录时,调用win32 api循环获取当前鼠标坐标,并与上一次的鼠标坐标比较,计算出两次鼠标坐标的变化值并记录下两次变化的间隔时间,写出数据。在每一把枪械的一个弹夹的子弹打完之后停止鼠标记录,筛选保存在本地的坐标数据,将停止开火后的坐标与开火之前的坐标舍去,就能得到每一发子弹垂直后坐力的具体坐标,将所有的x、y轴坐标取反,控制鼠标移动就可以实现压枪。
这是Apex英雄中CAR步枪的部分垂直后坐力坐标数据,数据分别对应[x,y,time.sleep]
如0.0136 = 13.6 ms
"CAR": [[0, 0, 0.0136], [0, 3, 0.0136], [1, 1, 0.0136], [0, 1, 0.0136], [-1, 1, 0.0136], [1, -1, 0.0136], [1, 6, 0.0136], [1, -1, 0.0136], [0, 2, 0.0136], [1, 2, 0.0136], [1, 0, 0.0136], [-1, 2, 0.0136], [1, -1, 0.0136], [1, 3, 0.0136], [2, 3, 0.0136], [1, 0, 0.0136], [-1, 1, 0.0136], [1, 2, 0.0136], [1, 2, 0.0136], [0, 1, 0.0136], [0, -1, 0.0136], [1, 3, 0.0136], [2, 3, 0.0136], [1, 1, 0.0136], [-1, 1, 0.0136], [1, 3, 0.0136], [2, 2, 0.0136], [0, 1, 0.0136], [-1, 3, 0.0136], [1, 2, 0.0136], [-1, 1, 0.0136], [0, 1, 0.0136], [-1, 1, 0.0136], [-1, 2, 0.0136], [0, 1, 0.0136], [-1, 1, 0.0136], [0, 3, 0.0136], [1, 2, 0.0136], [0, 1, 0.0136], [-1, 0, 0.0136], [-1, 1, 0.0136], [1, 0, 0.0136], [-1, 1, 0.0136], [0, 1, 0.0136], [-2, 0, 0.0136], [-1, -3, 0.0136], [-1, 1, 0.0136], [0, 0, 0.0136], [-2, 0, 0.0136], [0, -1, 0.0136], [-1, 1, 0.0136], [0, 0, 0.0136], [-1, 2, 0.0136], [0, 0, 0.0136], [-1, 1, 0.0136], [0, -1, 0.0136], [2, 3, 0.0136], [1, -1, 0.0136], [0, 2, 0.0136], [0, -1, 0.0136], [3, 0, 0.0136], [1, -1, 0.0136], [1, 1, 0.0136], [0, -1, 0.0136], [2, 1, 0.0136], [1, -1, 0.0136], [0, 2, 0.0136], [0, -1, 0.0136]]
当然,完成基本压枪的功能仅仅是这类游戏辅助程序的基础。现在更加高级的压枪宏往往带有自动识别功能,不需要玩家自己手动切换当前使用的枪械,而是根据每一款游戏中对于枪械显示UI的位置,调用诸如OCR文字识别的API或者直接对比各类枪械图片的相似度来判断当前玩家使用的枪械,对应自动切换到每种枪械的压枪数据。甚至压枪程序更加灵活,可以根据玩家当前枪械剩余弹药量智能调整压枪数据。如一把30发子弹的枪械,当前枪械中剩余弹药量为25发,那么当玩家按下开火键的时候,压枪程序自动从枪械压枪数据的第5个数据开始模拟。
如果将一把枪的所有压枪数据都以数组形式存储:date_ak47[] = {......}
那么只需要:date_ak47 [ (all_ammo - ammo_count) ] 即可。
再根据实际游戏的特色,比如识别到手雷、近战武器的时候停止压枪模拟,加上更加细致化的前置条件判断。