有助于反击全球攻击性的一个因素之一(此处“CS:GO”)大众普及是任何人都能举办自己的社区服务器的能力。这些社区服务器可以自由下载和安装并允许高级定制。服务器管理员可以创建和利用自定义资产,例如映射,允许创新的游戏模式。
然而,这种设计选择打开了一个大的攻击表面。玩家可以连接到潜在的恶意服务器,交换复杂的游戏消息和诸如纹理等二元资产。
我们已经设法找到并利用两个错误,当组合时,在连接到我们的恶意服务器时会导致播放器机器上的可靠远程代码执行。第一个错误是一个信息泄漏,使我们能够在客户的游戏过程中打破ASLR。第二个错误是游戏加载模块之一的.data部分中的全局数组的缺失访问,导致控制指示指针。
玩家可以使用内置于游戏中的用户友好的服务器浏览器加入社区服务器:
一旦玩家加入服务器,他们的游戏客户端和社区服务器就会互相交谈。作为安全研究人员,我们的任务是要了解CS所使用的网络协议:GO和发送哪种消息,以便我们寻找漏洞。
事实证明,CS:Go使用自己的基于UDP的协议来序列化,压缩,片段和加密在客户端和服务器之间发送的数据。我们不会详细介绍网络代码,因为它与我们将出现的错误无关紧要。
更重要的是,此基于自定义UDP的协议携带Protobuf序列化有效载荷。 Protobuf是由谷歌开发的技术,它允许定义消息并提供用于序列化和反序列化这些消息的API。
以下是CS定义和使用的protobuf消息的示例:Go开发人员:
消息csvcmsg_voiceinit {可选的Int32质量= 1;可选字符串编解码器= 2;可选int32版本= 3 [默认= 0];}
我们发现此消息定义通过在发现CS后进行Google搜索:Go使用Protobuf。我们遇到了包含Protobuf消息定义列表的SteamDatabase GitHub存储库。
当消息的名称建议时,它用于初始化一个播放器的某种语音消息传输到服务器。消息主体携带一些参数,例如用于解释语音数据的编解码器和版本。
具有此消息列表及其定义使我们能够在客户端和服务器之间发送有什么样的数据的洞察。但是,我们仍然不知道将发送订单消息,并预期值。例如,我们知道存在一条消息以初始化具有一些编解码器的语音消息,但我们不知道CS:GO支持哪些编解码器。
因此,我们为CS开发了一个代理:GO,允许我们实时查看通信。这个想法是我们可以通过代理启动CS:Go游戏并连接到任何服务器,然后转储客户端接收的任何消息并发送到服务器。为此,我们反向设计了网络代码来解密和解压缩消息。
我们还添加了修改发送/接收的任何消息的值的能力。由于攻击者最终控制客户端和服务器之间发送的Protobuf序列化消息中的任何值,因此它成为可能的攻击表面。我们可以在负责初始化连接的代码中找到错误,而无需缩写邮件中的有趣字段而无法反向工程。
以下GIF显示了游戏如何发送消息并实时由代理转储,对应于拍摄,更改武器或移动等事件:
配备此工具,现在是我们通过翻转Protobuf消息中的一些位来发现错误。
我们发现,CSVCMSG_SPLITSCREEN消息中的一个字段,可以由(恶意)服务器发送给客户端,可以导致OOB访问,随后导致受控的虚拟功能调用。
消息csvcmsg_splitscliteen {可选.eplitscreenmessageType类型= 1 [默认= msg_splitscreen_adduser];可选的INT32槽= 2;可选的int32 player_index = 3; }
CSVCMSG_SplitsCreen似乎有趣,因为一个名为Player_Index的字段由服务器控制。但是,与直觉相反,Player_Index字段不用于访问阵列,插槽字段是。事实证明,Slot字段用作位于Engine.dll文件的.data段中的SplitsCreen播放器对象数组的索引。没有任何限制检查。
Decompileded代码的以下屏幕截图显示了Whill_Splot如何使用,而无需任何索引作为索引。如果对象的第一个字节不是1,则输入分支:
被证明的错误是非常有希望的,因为在分支中的一些指令是一个vtable的指令被取消引用,并且调用函数指针。这在下一个屏幕截图中显示:
考虑到信息泄漏,我们对似乎高度可利用的错误非常兴奋。由于对对象的指针是从引擎中的全局阵列获得的。在撰写本文时是6MB二进制文件,因此我们相信我们可以找到指向我们控制的数据的指针。将上述对象指向攻击者控制数据将产生任意代码执行。
但是,我们仍然必须在已知位置伪造VTable,然后将功能指针指向有用的东西。由于这种约束,我们决定寻找可能导致信息泄漏的另一个错误。
如前所述,服务器管理员可以创建具有任意数量自定义的服务器,包括自定义地图和声音。每当玩家加入具有此类自定义的服务器时,需要传输自定义背后的文件。服务器管理员可以创建需要在服务器播放列表中的每个映射下载的文件列表。
在连接阶段期间,服务器将客户端发送HTTP服务器的URL,必要的文件应从中下载。对于每个自定义文件,将创建卷曲请求。为每个请求设置的两个选项激发了我们有兴趣的:curlopt_headerfunction和curlopt_writefunction。前者允许注册回调,以便在HTTP响应中调用每个HTTP标头。后者允许在接收到身体数据时注册触发的回调。
我们有兴趣了解Valve开发人员如何处理传入的HTTP标头并反向设计我们命名为CurlheaderCallback()的函数。
事实证明,CurlheaderCallback()简单地解析了内容长度HTTP标头并相应地在堆上分配了一个未初始化的缓冲区,因为内容长度应对应于应该下载的文件的大小。
最后,一旦收到HTTP请求并且不收到更多数据,就会将缓冲区写入磁盘。
我们立即注意到在解析HTTP标题内容 - 长度的缺陷:作为下面的屏幕截图节目,制作了区分大小写的比较。
此比较缺陷,因为HTTP标头也可以是小写的。这只是Linux客户端的情况,因为它们使用Curl然后进行比较。在Windows上,客户端只是假设Windows API返回的值是正确的。这会产生相同的错误,因为我们可以通过小响应主体发送任意内容长度标题。
我们使用Python脚本设置了一个HTTP服务器,并使用一些HTTP标头值播放。最后,我们提出了一个触发错误的HTTP响应:
当客户端接收到文件下载的这种HTTP响应时,它将识别第一内容长度标题并分配大小1337的缓冲器。然而,遵循具有大小0的第二内容长度报头。虽然CS:Go代码未遗漏第二个内容长度标题,因为其区分大小写的搜索,但仍然预计1337个字节的身体数据,Curl使用最后一个标题并立即完成请求。
在Windows上,API刚刚返回第一个标题值,即使响应不均匀。然后,CS:DO代码将分配的缓冲区写入磁盘,以及缓冲区中包含的所有未初始化的内存内容(包括指针)。
虽然似乎CS:Go使用Windows API在Windows上处理HTTP下载,但完全相同的HTTP响应工作,并允许我们在播放器机器上创建包含未初始化内存内容的任意大小的文件。
然后,服务器可以通过CNETMSG_FILE消息请求这些文件。当客户端收到此消息时,它们将将所请求的文件上传到服务器。它定义如下:
消息CNETMSG_FILE {可选int32 transfer_id = 1;可选字符串file_name = 2;可选bool is_replay_demo_file = 3;可选的Bool Deny = 4; }
上传文件后,攻击者控制的服务器可以搜索文件的内容,以查找指向Engine.dll或堆指针以中断ASLR。我们在张贴ASLR的附录部分详细介绍了这一步骤。
为了进一步启用游戏的自定义,服务器和客户端Exchange Sullar,这是基本上的配置选项。
每个扫描由全局对象管理,该对象存储在Engine.dll中。以下代码段显示了这样一个对象的简化定义,该对象用于解释为什么SUNARS才能成为一个强大的小工具,以帮助利用OOB访问:
社区服务器可以在匹配期间更新其扫描值并通过发送CNetMSG_SetConvar消息来通知客户端:
消息CMSG_CVAR {MESSAGE CVAR {可选字符串名称= 1;可选字符串值= 2;可选的uint32 diberost_name = 3; }重复.cmsg_cvars.cvar cvars = 1;消息CNETMSG_SETCONVAR {可选.cmsg_cvars orcars = 1; }
这些消息由简单的键/值结构组成。将消息定义与struct Supar定义进行比较时,假设顺应消息的完全攻击者可控值字段被复制到客户端的堆,并且将其存储在扫描对象的Convar_Value字段中。
正如我们之前讨论的那样,CSVCMSG_SplitsCreen中的OOB访问发生在指向对象的指针数组中。以下是将OOB访问发生作为提醒的代码的分解:
由于阵列和所有顺应都位于Engine.dll的.data部分.dll,我们可以可靠地设置Player_slot参数,使得ptr_to_object指向我们之前设置的顺应值。这可以如下说明:
我们之前还提到的是,OOB访问对象上的虚拟方法后,一些指令。这通过VTABLE DEREURESS常用。这是代码再次提醒:
由于我们通过扫描控制对象的内容,因此我们可以简单地将VTABLE指针设置为任何值。为了使利用100%可靠,使用INFO泄漏将返回到受控数据的.data部分。
幸运的是,一些潮流被解释为颜色值,并期望4字节(红色蓝绿色alpha)值,可以是攻击者控制的。此值直接存储在上面的struct Supar定义中的Color_Value字段中。由于CS:Windows上的进程是32位,我们能够使用潮流的颜色值来伪造指针。
如果我们使用虚假对象的VTable指针指向Engine.dll的.data部分,例如被称为方法与Color_Value重叠,我们最终可以劫持EIP寄存器并任意重定向控制流程。这一取消引导链可以说明如下:
使用ASLR破碎和我们获得了任意指导指针控制,所剩余的所有内容都是构建一个ROP链,最终导致我们调用Shellexecutea来执行任意系统命令。
我们在一份报告中提交了两个错误到Valve的Hackerone程序,以及我们开发的利用,我们开发了100%的ReliaBlity。不幸的是,超过4个月,我们甚至没有得到阀门代表的承认。公共压力后,当显而易见的是,阀门也忽略了具有类似影响的其他安全研究人员,阀门终于固定了许多安全问题。我们希望Valve重新构建其Bug Bounty程序,以便再次吸引安全研究人员。
在HTTP下载中的未初始化内存导致信息泄露部分中,我们展示了HTTP下载如何在客户端的游戏过程中查看无任意大小的未初始内存中的块。
我们发现了另一条消息,似乎对我们非常有趣:CSVCMSG_SENDTABLE。每当客户端接收到这样的消息时,它将在堆上分配一个具有攻击者控制的整数的对象。最重要的是,对象的前4个字节将包含一个vTable指针进入Engine.dll。
def spray_send_table(s,addr,nprops):table = nmsg.csvcmsg_sendtable()table.is_end = false table.net_table_name ="可行的" table.needs_decoder = false for in范围(nprops):prop = table.props.add()prop。 type = 0x1337ee00 prop.var_name =" abc" prop.flags = 0 prop.priority = 0 prop.dt_name ="无论如何和#34; prop.num_elements = 0 prop.low_value = 0.0 prop.high_value = 0.0 prop.num_bits = 0x00ff tosend = prepare_payload(表,9)s.sendto(tosend,addr)
Windows堆是一种不合理的人。也就是说,Malloc - >免费 - > Malloc Combo不会产生相同的块。值得庆幸的是,Saar Amaar发布了他对Windows堆的大量研究,我们咨询了,以更好地了解我们的利用上下文。
我们提出了一种喷雾来分配许多带有标记的Sendtable对象数组,以便在我们将文件上载回服务器时扫描。因为我们可以选择阵列的大小,我们选择了不那么常用的大小,以避免干扰正常的游戏代码。如果我们现在立即释放所有喷涂的阵列,然后让客户端下载文件击中先前喷涂块的一个文件的可能性是相对高的。
在实践中,我们几乎总是在第一个文件中泄漏,当我们没有,我们可以简单地重置连接并再试一次,因为我们还没有损坏程序状态。为了最大化成功,我们为漏洞创建了四个文件。这可确保其中至少一个成功,否则只需再试一次。
以下代码显示了我们如何为我们的喷涂对象扫描所接收的内存,以查找将指向Engine.dll的SendTable VTable。 files_received.append(fn)pp = packetparser.packetparser(len(data) - 0x54):vtable_ptr = struct.unpack('< i'数据[i:i + 4])[0] table_type = struct.unpack('< i',数据[i + 8:i + 12])[0] table_nbits = struct.unpack('< i& #39;,数据[i + 12:i + 16])[0]如果table_type == 0x1337ee00和table_nbits == 0x00ff00ff:Engine_Base = VTABLE_PTR - OFFSET_VTABLE打印(F" VTABLE_PTR = {HEX(VTABLE_PTR)}& #34;)休息