导语:Valve公司 (PC最大游戏平台Steam母公司)想必玩家们都不会陌生,很多脍炙人口的经典大作都是出自这家公司之手,公司本身在游戏业界也是有着非常重要的地位的。
Valve公司 (PC最大游戏平台Steam母公司)想必玩家们都不会陌生,很多脍炙人口的经典大作都是出自这家公司之手,公司本身在游戏业界也是有着非常重要的地位的。不过树大招风,Valve公司近些年连年遭受黑客攻击,2020年4月,据媒体报道,Valve旗下热门游戏《CS:GO》和《军团要塞2》源码被人泄露。源码泄露后,相当于将游戏的运行机制完全曝光在黑客面前,他们可以轻易根据游戏内的漏洞制作外挂,或者攻击玩家,对此,已经有一些程序员呼吁广大玩家在官方推出解决方案之前,尽量不要运行任何Source引擎的多人游戏,包括《CS:GO》《军团要塞2》和《传送门2》《Dota2》等。还有2015年圣诞节期间, Steam在线玩家发现其遭受到了数小时的宕机。据报告,这起宕机事件的主要原因来自其服务器遭受了DDoS拒绝服务攻击,圣诞节当日服务器承受了为平时21倍的访问流量,估计约有34000用户无意中查看了其他用户的泄露数据。
为Valve的在线游戏功能提供动力的核心网络库存在严重漏洞,这可能导致恶意玩家通过远程攻击瘫痪游戏,甚至控制受影响的第三方游戏服务器。
Check Point Research的研究人员最近发表的一份分析报告指出:
“攻击者可以远程攻击受害目标的游戏客户端,甚至使Valve游戏服务器崩溃,从而完全结束游戏。更大的破坏是攻击者可以远程接管第三方开发者游戏服务器来执行任意代码。”
Valve是一家受欢迎的美国视频游戏开发商和发行商,旗下拥有游戏软件发行平台Steam以及Half-Life, Counter-Strike, Portal, Day of Defeat, Team Fortress, Left 4 Dead和 Dota等多款游戏。
Valve的游戏网络套接字(GNS)或Steam套接字库中发现了四个漏洞(CVE-2020-6016至CVE-2020-6019),该库是一个开放源代码的网络库,提供了一个“游戏的基本传输层”,从而可以实现UDP和TCP功能的混合,并支持加密,更高的可靠性和点对点(P2P)通信。
Steam套接字也作为Steamworks SDK的一部分提供给第三方游戏开发人员,该漏洞在Steam服务器及其安装在游戏者系统上的客户端上均被发现。
攻击的关项在于数据包重组机制(CVE-2020-6016)中的一个特定漏洞,以及c++迭代器实现中的一个古怪之处,该迭代器将一堆恶意数据包发送到目标游戏服务器并触发基于堆的缓冲区下溢,最终导致服务器中止或崩溃。
在2020年9月2日对Valve进行负责任的披露后,包含修复程序的二进制更新已于9月17日发布给Valve的游戏客户端和服务器。
但根据Check Point的研究,截至12月2日,某些第三方游戏开发商还没有对其客户端进行修复。
在冠状病毒大流行期间,电子游戏的数量达到了历史新高。由于目前有数百万人在玩在线游戏,对于游戏公司和游戏玩家的隐私来说,即使是最轻微的安全漏洞都可能成为一个严重的问题。根据目前发现的漏洞,攻击者可以每天控制成千上万的玩家电脑,而受害者却对此完全视而不见。
流行的在线平台是攻击者攻击的首要目标,只要有数百万用户登录到同一个地方,强大而可靠的漏洞利用的能力就会成倍增加。
Check Point表示,通过Steam玩Valve游戏的玩家已经受到了补丁的保护,不过第三方游戏的玩家应该确保他们的游戏客户端在最近几个月都进行了修复,以降低与漏洞相关的风险。
在这次研究中,研究人员发现了在游戏网络套接字库的实施中的几个漏洞,这使得各种可能的攻击成为可能。例如,当与在线对手比赛时,攻击者可以远程摧毁对手的游戏客户端以迫使其获胜。
下面研究人员将详细介绍如何发现和利用CVE-2020-6016。
CVE-2020-6016的发现
Valve的游戏网络套接字(GNS),也被称为“Steam套接字”。该库在2018年开源,该库支持点对点(P2P)模式和集中式客户端-服务器模式的通信。一旦建立了加密连接,各方就可以交换消息,它有一个适当的机制来检测和纠正丢失的消息,以增加系统开销(类似于UDP和TCP)。研究人员所说的“消息”意味着基础架构正常运行所需的信息,例如统计数据和ping测量。
为了建立这样一个安全的通信通道,各方使用了Valve专有的握手协议:
GNS握手协议的示意图
握手协议中的消息利用了谷歌的protobuf库,从而大大降低了处理这些复杂消息时任何与解析相关的漏洞的风险。
与TLS类似,在此协议期间,客户端和服务器都通过提供签名证书来验证彼此的身份,并且双方都宣布它们支持的加密方案。与TLS类似,GNS使用非对称加密技术,使双方能够协商一个共享的对称加密密钥,但与TLS不同的是,GNS不支持多种不同的加密算法,而是专门使用Elliptic-Curve Diffie-Hellman密钥交换来协商共享秘密,每个客户端都可以从中获取共享的AES-256密钥来加密消息。
一旦协商完成并且双方都同意了共享密钥,则该密钥将用于加密所有进一步的通信,并且通道已准备就绪。
碎片、重组和负偏移
回想一下,我们之前提到过,GNS支持具有类似于TCP流的确认机制的“可靠”消息,以及客户端只是发送并希望达到最佳效果的“不可靠”消息,它们更像UDP数据报。必须处理通过有线发送的每个消息1300字节的最大传输单元(MTU),每个逻辑消息都分为几个可靠/不可靠的段。这些片段最终将由库重新组合成一个完整的“逻辑”消息,并将其传递给游戏引擎本身。
当研究人员第一次看到这种碎片重组机制时,就知道这是程序的一部分,需要重点研究。重新组装多个片段,每个片段都有自己的长度和偏移量,是一项众所周知的艰巨任务,多年来,解决方案实施的各种尝试已导致多个高影响力漏洞。甚至在研究人员先前对Apache Guacamole的研究中,已发现的漏洞之一CVE-2020-9498在RDP通道消息的重组过程中也涉及到悬空指针。
在浏览代码时,研究人员发现了以下有趣的不匹配。分段偏移量最初被读入无符号变量,如下图所示:
将段偏移量解析为无符号的32位值
然而,相同的nOffset变量稍后被传递给SNP_ReceiveUnreliableSegment,它将其作为一个有符号的32位值:
将段偏移量作为有符号值重新组装函数
如果研究人员要将一堆碎片发送到GNS服务器进行重组,那么只有告诉服务器哪个碎片到达哪里才有意义。到目前为止,一切顺利,但是,如果研究人员为“where”(nOffset)选择了一个足够大的值,那么当CSteamNetworkConnectionBase解析时,该值将无提示溢出并解释为负数。然后,在分配给研究人员消息的缓冲区开始之前,该片段将被直接“重组”到服务器的进程存储器中。
研究人员认为它不应该这么简单,服务器可能会对传入的段应用一些完整性检查。为了更好地理解应用哪些检查以及如何绕过它们,研究人员研究了段结构的成员,以了解哪些信息被发送到服务器。它们如下:
nMsgNum:要重新组装的逻辑消息号;
nOffset:片段消息中当前段的偏移量;
cbSegmentSize:当前段的大小(以字节为单位);
bLastSegmentInMessage:这是最后一段吗?
前两个字段为每个段创建一个唯一的“项”,并且该项用于将段存储在专用哈希表中。这些段将累积在此哈希表中,直到确定所有片段都已就位,并且消息可以重新组装为止。
检索与当前段关联的项的新的或使用过的数据结构
一旦项被检查为唯一,数据结构就将被初始化,并将该段的内容复制到其中。
段的数据结构是用段的内容和属性填充的
在正确地将每个段添加到哈希表后,将从表中获取当前消息号的段列表,并对其进行扫描以检查是否缺少段(称为“gaps”)。
do-while循环,检查可见片段列表中的gaps
因此,对于传入段的完整性检查主要是为了确保消息不是从不完整的段列表中组装的。攻击计划似乎很简单:
1.发送给定长度(0x400字节),负偏移量(-0x180)的段,并将其标记为消息的最后一段;
2.该消息计算的cbMessageSize为0x400 - 0x180 = 0x280;
3.所有检查都将通过,稍后研究人员的段将被复制到大小为0x280的堆缓冲区偏移0x180,,从而实现基于堆的缓冲区下溢。
但是,出现了下面这段代码:
用于查询哈希表的项使用0的m_nOffset
扫描哈希表以查找可重新组合的段时,代码不会查找偏移量为负的段。从偏移量0开始查询该表,然后向上查询。对于编写该代码的人来说,这都是自然而然的事情,但是对研究人员而言,这只是计划的第一步。
现在研究人员试图让这次攻击变得成功,虽然该策略失败了,但它仍然设法使程序进入了未定义的状态:重新装配验证循环将在一个空迭代器上执行do-while循环,寻找代码找不到的有效段。即使查询最终返回end()元素,它也将继续将表查询的输出视为有效的段数据。为此,研究人员就要思考如何利用这种不寻常的情况。
深入了解c++迭代器和end()元素
迭代器不是C ++原始语言设计的一部分,而是通过标准模板库(STL)添加的。由于没有与语言关键字的内置集成,因此为了使调用迭代器的符号至少符合人体工程学,C ++迭代器将语言的运算符重载功能用作引导程序。具体来说,通过实现比较和增量运算符,可以使用相当惯用的for循环来调用迭代器逻辑。请看以下基本的c++代码,输出一个向量的元素:
与等效的Rust相比,它有一个for关键字,可以直接与迭代器交互(类似于Python):
上面的c++示例就是从这里下载。为了实现这个神奇的功能,c++在幕后利用了指针比较。
指向cdreference的最后一个元素之后的std::end()
由于end()元素实际上并不是具有为其分配的内存的有效元素,因此在引用其中的字段时,研究人员实际上引用了附近的内存:
哈希映射附近的struct字段将被“used”作为end()的字段
经过一个简短的调试会话,研究人员提出了以下内存布局:
通过查询此布局,研究人员能够获得所需的end() “元素”的要求列表。
最难的部分是将m_nOffset转换为负值,最后,研究人员能够将攻击的运行时间减少到15分钟以内:
令人惊讶的是,研究人员甚至释放了无效的段
令研究人员惊讶的是,代码流在重新组合用户消息之后又执行了一个步骤:它删除了现在不需要的片段。
研究人员还记得,此时itMsgStart是精心设计的end()元素,它不是一个有效的哈希表项。尽管如此,这个do-while循环将很高兴地从表中删除不存在的元素,有效地将m_mapunreliablesments的内存“释放”回堆中。
在标准情况下,当使用glibc的堆实现时,研究人员还可以在该字段之前创建存储的内存值。这个重要的哈希表现在将存储在堆中,允许研究人员通过发送一个使用相同堆分配的可靠段来完全控制它。遗憾的是,在Valve的例子中,这个无效的free()操作将导致游戏服务器中止。
本文翻译自:https://research.checkpoint.com/2020/game-on-finding-vulnerabilities-in-valves-steam-sockets/如若转载,请注明原文地址: