导致反恐精英全球攻势(以下简称“CS:GO”)大受欢迎的因素之一是任何人都可以托管自己的社区服务器。这些社区服务器可以免费下载和安装,并允许进行高级别的定制。服务器管理员可以创建和利用自定义资产(例如地图),从而实现创新的游戏模式。
然而,这种设计选择打开了一个很大的攻击面。玩家可以连接到潜在的恶意服务器,交换复杂的游戏消息和纹理等二进制资产。
我们设法找到并利用了两个错误,当它们结合在一起时,当连接到我们的恶意服务器时,可以在玩家的机器上可靠地远程执行代码。第一个错误是信息泄漏,它使我们能够在客户端的游戏过程中破坏 ASLR。第二个错误是.data
对游戏加载模块部分中全局数组的越界访问,导致对指令指针的控制。
社区服务器列表
玩家可以使用游戏内置的用户友好服务器浏览器加入社区服务器:
一旦玩家加入服务器,他们的游戏客户端和社区服务器就会开始相互交谈。作为安全研究人员,我们的任务是了解 CS:GO 使用的网络协议以及发送的消息类型,以便我们可以查找漏洞。
事实证明,CS:GO 使用自己的基于 UDP 的协议来序列化、压缩、分段和加密客户端和服务器之间发送的数据。我们不会详细介绍网络代码,因为它与我们将呈现的错误无关。
更重要的是,这个基于 UDP 的自定义协议携带Protobuf
序列化的有效载荷。Protobuf是 Google 开发的一项技术,它允许定义消息并提供用于序列化和反序列化这些消息的 API。
以下是 CS:GO 开发人员定义和使用的 protobuf 消息示例:
代码语言:txt复制message CSVCMsg_VoiceInit {
optional int32 quality = 1;
optional string codec = 2;
optional int32 version = 3 [default = 0];
}
在发现 CS:GO 使用 Protobuf 后,我们通过谷歌搜索找到了这个消息定义。我们遇到了包含 Protobuf 消息定义列表的SteamDatabase GitHub 存储库。
正如消息的名称所暗示的那样,它用于初始化一个玩家到服务器的某种语音消息传输。消息体携带一些参数,例如用于解释语音数据的编解码器和版本。
开发 CS:GO 代理
有了这个消息列表及其定义,我们就可以深入了解客户端和服务器之间发送的数据类型。然而,我们仍然不知道消息将以何种顺序发送以及期望什么样的值。例如,我们知道存在一条消息以使用某种编解码器初始化语音消息,但我们不知道 CS:GO 支持哪些编解码器。
出于这个原因,我们为 CS:GO 开发了一个代理,允许我们实时查看通信。这个想法是我们可以启动 CS:GO 游戏并通过代理连接到任何服务器,然后转储客户端接收到的任何消息并发送到服务器。为此,我们对网络代码进行了逆向工程以解密和解包消息。
我们还添加了修改将要发送/接收的任何消息的值的功能。由于攻击者最终控制了客户端和服务器之间发送的 Protobuf 序列化消息中的任何值,因此它成为可能的攻击面。我们可以在负责初始化连接的代码中找到错误,而无需通过改变消息中有趣的字段对其进行逆向工程。
以下 GIF 显示了游戏如何发送消息并由代理实时转储,对应于射击、更换武器或移动等事件:
配备了这个工具,现在是我们通过翻转 protobuf 消息中的一些位来发现错误的时候了。
OOB 访问 CSVCMsg_SplitScreen
我们发现CSVCMsg_SplitScreen
消息中的一个字段可以由(恶意)服务器发送到客户端,可以导致 OOB 访问,进而导致受控的虚拟函数调用。
这个消息的定义是:
代码语言:txt复制message CSVCMsg_SplitScreen {
optional .ESplitScreenMessageType type = 1 [default = MSG_SPLITSCREEN_ADDUSER];
optional int32 slot = 2;
optional int32 player_index = 3;
}
CSVCMsg_SplitScreen
看起来很有趣,因为调用的字段player_index
由服务器控制。然而,与直觉相反,player_index
字段不是用来访问数组的,slot
字段是。事实证明,该slot
字段用作位于文件.data
段中的分屏播放器对象数组的索引,engine.dll
没有任何边界检查。
看着崩溃,我们已经可以观察到一些有趣的事实:
- 阵列存储在
.data
内部部engine.dll
- 访问数组后,会发生对访问对象的间接函数调用
以下反编译代码的屏幕截图显示了如何player_splot
在没有任何检查的情况下用作索引。如果对象的第一个字节不是1
,则进入一个分支:
这个错误被证明是很有前途的,因为进入分支的一些指令会取消引用一个 vtable 并调用一个函数指针。这显示在下一个屏幕截图中:
考虑到信息泄露,我们对这个漏洞感到非常兴奋,因为它看起来很容易被利用。由于指向对象的指针是从 内的全局数组中获得的engine.dll
,在撰写本文时它是一个6MB
二进制数组,因此我们确信我们可以找到指向我们控制的数据的指针。将上述对象指向攻击者控制的数据将产生任意代码执行。
但是,我们仍然必须在已知位置伪造一个 vtable,然后将函数指针指向有用的东西。由于这个限制,我们决定寻找另一个可能导致信息泄漏的错误。