白帽故事 · 2023年8月13日 0

CS:GO 从0到0day!

背景介绍:

国外一家安全公司(Neodyme)在CS:GO(反恐精英:全球攻势)游戏中发现了三个独立的远程代码执行 (RCE) 漏洞,当游戏客户端连接到恶意的 Python CS:GO 服务器时,每个漏洞都会被触发。本篇文章详细介绍了他们通过 CS:GO 二进制文件的分析旅程,并对各种已识别的漏洞进行了深入技术研究。最后他们还展示了一个概念验证 (POC)视频 ,该漏洞利用四种不同的逻辑漏洞在游戏客户端连接到服务器时触发远程执行代码。

概述:

2021年4月28日的 CS:GO 补丁修复了几个关键漏洞,包括三个关键漏洞。本文描述了如何发现这三个关键漏洞的方法,详细展示了一个由4个逻辑漏洞组成的漏洞链,并解释了如何通过巧妙地将它们组合起来导致客户端的远程代码执行 (RCE)。虽然这篇文章确实解释了四个逻辑漏洞,但重点是他们的研究方法很值得我们学习。

补丁链接:https://blog.counter-strike.net/index.php/2021/04/33895/

首先,他们查看 CS:GO 游戏的现有研究,并做了大致介绍,从而减轻复杂客户端逆向工程的痛苦,随后介绍了 CS:GO 网络协议的基本概念,如 fast_dlCvars ,并详细介绍了4种不同的逻辑漏洞。将这些漏洞结合起来就可以通过连接到攻击者控制的恶意服务器来利用 CS:GO 客户端的概念证明。

了解你的目标:

安全研究不是盲目地寻找安全漏洞。因为只有当你完全理解了一个目标,你才有能力突破技术限制。因此,第一步应该尽可能多地获取有关目标的信息。以下内容提供了“侦察”阶段的想法:

软件工具包

支持改装的游戏通常会提供官方软件开发工具包 (SDK),虽然 SDK 不包含目标的源代码,但其中定义的结构提供了有关网络包和类定义的宝贵信息,有助于理解引擎,特别对于 Valve 公司的游戏,引擎或完整游戏(2003 年、2007 年和 2020 年)发生过多起源代码泄漏事件,尽管源代码经常过时并且包含许多现已修复的安全漏洞,但这些泄漏非常有用。主要是因为源代码比经过编译器优化的汇编更易于阅读。

公共研究

众所周知,该公司并不是第一个在CS:GO游戏中寻找漏洞的组织,因此,可以在互联网上搜索到很多有用的博文和PPT。

作弊社区

在游戏中超级令人讨厌,但却受到安全研究人员喜爱的:像 UnknownCheats 这样的作弊社区,这些社区提供了详细的逆向帖子和引擎内部结构,在这种情况下,Felipe 已经编写了一个 Network Cheat,对理解网络协议做出了很大贡献。

调试符号

调试符号包含了许多无法识别的函数名和类结构,从而使逆向工程更加方便。有时游戏的版本也会有意附带调试符号以生成更好的错误报告,然而,有时程序员会忘记从游戏最终的二进制文件中删除调试符号,毕竟程序员也是人,是人就会犯错。


带有调试符号的 CS:GO 二进制文件

2017 年 4 月的 macOS CS:GO 版本(如下图所示)包含了完整的调试符号。带有调试符号的游戏文件比无符号的游戏文件大很多倍,因此可以使用 SteamDB和旧存储库自动识别。

虽然在 2021 年仍然可以使用SteamCMD 专门下载旧版本,但同时该功能似乎已被 Valve 公司禁用。

模糊测试(Fuzz)

尽管有所有信息,仍须投入大量时间对目标进行逆向分析,只有当完全了解哪个缓冲区处理网络数据时,才能开始做那些令人兴奋的事。但这种努力是值得的!

该组织使用 Hongfuzz、pubic protobuf network structures和 libprotobuf-mutator 对客户端进行模糊测试时发现了崩溃,而这些崩溃直接提供了 instruction pointer 的控制,因此很可能被利用!为了全面测试并制定漏洞利用策略,该组织决定用 Python 实现自己早期阶段的服务器。

4个逻辑漏洞的发现:

对于像 CS:GO 这样的目标,由于多年的开发和公共漏洞赏金计划,现在很可能已经修复了简单的漏洞,如果只是在巨大的 engine.dll 中随机寻找堆栈溢出,你会很快放弃。但事实证明:每一个小异常都可以证明是有价值的,在盯着 CS:GO 反汇编和源代码的几周时间里,他们不断地问自己以下问题:

  • 我们已经拥有哪些‘原料’?
  • 结合它们我们能做什么?
  • 有哪些安全机制?
  • 开发人员可能没有考虑过哪些奇怪的边缘问题?

内存破坏利用很难,尽管提交给 Valve 的三个全链漏洞利用中有两个是内存破坏,但这意味着极高的开销,并且始终存在客户端因错误的内存分配而崩溃的风险。每次启动 CS:GO 连接到服务器加载地图都需要几分钟,这让开发变得非常困难。

在本篇文章中,不解释奇怪的堆风水机制,而是关注4个逻辑漏洞,它们共同导致了客户端远程执行代码。发现顺序如下:

Bug1:从服务器执行特权命令

该漏洞允许攻击者在客户端上执行通常只能在单人游戏模式下的“特权”命令

为了验证自定义Python CS:GO 服务器是否工作,他们通过 CNETMsg_StringCmd 向客户端发送了命令 echo Hello World! ,并且正如预期的那样,在游戏控制台上收到了输出 Hello World! 。随后他们还尝试发送 quit 命令,比赛结束!他们无法相信服务器会允许这样做,通常这是不允许的:但在 SourceMod 的帮助下,他们使用官方和修改后的服务器重新创建了一个可以向客户端发送消息的源引擎修改框架,结果:FCVAR_SERVER_CAN_EXECUTE prevented server running command: quit

就这样找到漏洞了吗?漏洞究竟是如何发生的?

SourceMod:https://www.sourcemod.net/

源引擎单人游戏在内部使用本地托管的源引擎服务器,然后单人客户端连接到自己的服务器加入游戏,这个单人服务器当然应该拥有特权,比如改变客户端的键盘布局或截屏。

如果最多只有一个客户端可以连接到服务器,则多人服务器被认为是本地服务器,因此是具有特权的单人服务器。漏洞在于确定服务器类型:可以连接到服务器的最大客户端数由变量 m_nMaxClients 控制,并在连接到服务器时由客户端接收,然而 Python 服务器将变量 m_nMaxClients 设置为 1,这样就可以在客户端执行特权命令了!

Host_IsSinglePlayerGame Check

Bug2:由扩展名剥离导致的任意文件下载

此漏洞允许攻击者绕过扩展名过滤器下载任意扩展名的文件

源引擎服务器可以向客户端发送额外的游戏文件,例如地图或玩家模型,数据传输可以通过源网络协议或 HTTP fast_dl 完成,为了防止恶意文件被发送到客户端,某些文件扩展名如 *.exe*.dll*.ini 会被阻止。

如果设置了 fast_dl 选项,附加内容将从指定的 HTTP 服务器加载,而不是直接从 CS:GO 服务器加载,URL 由 snprintf(p_cResult, 256, %s/%s, p_cServerName, p_cFileName) 函数根据服务器名称和完整文件名动态生成。

snprintf 函数将字符串的长度限制为 256 个字符,从而从文件名中截断不需要的字符,但 p_cServerNamep_cFileName 却可以有 256 个字符的长度!像 ././[..]/file.AAA.BBB 这样的文件名可以在 .AAA 扩展名之后终止,因为 .BBB 部分被 snprintf 函数截断了,由此可以完全绕过危险文件过滤器!

以下源代码片段说明扩展名已被删除:


snprintf 函数从字符串中删除多余数据

该漏洞通过对 fast_dl 协议的代码分析发现,且该协议近年来变化不大。

Bug3:任意文本文件写入游戏目录

该漏洞允许攻击者(覆盖)写入游戏文件夹的任意文件

由于不确定如何组合前面的两个漏洞,因此,该组织在 CS:GO 二进制文件中搜索有用的特权命令,通过 con_logfile 命令,他们惊奇地发现该命令可以将任意 *.log 文件写入游戏的任意文件夹,由于 snprintf 的类似扩展名剥离漏洞,还可以指定任意文件扩展名,从而编写具有任意内容和任意扩展名的文本文件。

具体来说,该漏洞可使用任意 CS:GO 命令创建新的配置文件 cfg/leak.logleak.log “配置”文件可以由 exec leak.log 命令加载,从 cfg 文件夹中读取文件。

Bug4:回退至禁用签名检查

该漏洞允许攻击者以“不安全”模式启动CS:GO客户端,并允许加载未签名的游戏二进制文件

当启动 CS:GO 客户端时,游戏 DLL 的完整性通过匹配哈希值进行验证,只有通过此验证才能在官方服务器上玩,如果 DLL 验证失败,则会回退到 insecure 模式,这也可以通过额外的命令行参数 -insecure 来实现,只有在这种模式下,才能加载不在游戏 bin/ 路径中的其它 DLL,如果攻击者成功使DLL 验证失败,就可以创建自己的DLL,在配置中引用这些DLL,实现命令执行,在 Windows 中,攻击者可以指定将 DLL 加载到进程中时执行的代码,因此,攻击者可以在客户端系统上执行任意代码。

Windows 防止覆盖在运行进程中加载的 DLL,因此,必须找到一个在游戏开始时验证但未加载到进程中的 DLL,幸运的是, client.dll 已经被 client_panorama.dll 替换了,因此不再加载,但仍可以验证!用(Bug3)任意文本覆盖 client.dll

完整的逻辑漏洞链:

  1. 在客户端执行特权命令
  2. 将恶意 DLL 下载到游戏目录
  3. 替换 gameinfo.txt 以便在游戏启动时加载恶意 DLL
  4. 破坏 client.dll 以实现回退到 insecure 模式

为了理解下面的步骤,需要介绍源引擎的两个经典元素:

gameinfo.txt 和 CVars:

所有基于源引擎的游戏实际上都是《半条命》游戏的“附加组件”,游戏的资产和 DLL 从文件 gameinfo.txt 中定义的特殊路径加载:

通过将 |gameinfo_path|/exploit 设置为 FileSystem 数组中的第一个,引擎会尝试从该路径加载丢失的 DLL,当在那里找不到要加载的元素时,才使用原始游戏路径,在游戏开始时加载的一个 DLL 是 matchmaking.dll ,这意味着可以放置一个新的 matchmaking.dll 并在 CS:GO 客户端加载 DLL 时调用任意代码。

CVars 是源引擎游戏中的一个基本概念,无处不在,这些变量几乎控制了游戏中要设置的所有内容:路径、键绑定、十字准线的外观、游戏模式等,还有传说中的 sv_cheats 变量,许多CS玩家可能都听说过。根据 CVar ,也可以由服务器设置,从而覆盖本地选项。

连接后,客户端告诉服务器在客户端设置了哪个本地 CVars ,以便服务器可以做出相应的反应,例如,如果在客户端将 sv_cheats 设置为 1 ,则服务器可以踢出客户端,作为攻击者,我们需要知道 CS:GO 客户端的安装目录,以便我们可以通过选择长度恰到好处的路径来利用 bug 2bug 4 ,不幸的是,默认情况下,客户端不会发送包含当前游戏目录的 CVar ,因此,需要使用一个技巧来设置新的 CVAR GAMEBIN ,并将其发送回攻击者控制的服务器。基本思路如下:

  1. 执行“脚本” leak.log 来设置 CVar GAMEBIN
  2. 指示客户端重新连接到恶意服务器
  3. 重新连接后,所有 CVars 并设置回恶意服务器

    详细信息涉及从配置文件调用 path 命令将 CVAR GAMEBIN 设置为游戏的安装路径,利用攻击者编写的配置文件 leak.log ,其中包括 path 命令,客户端必须执行配置文件,否则 CVar 不会在下一次服务器连接期间持久存储, leak.log 文件是用 exec 命令执行的,之后恶意服务器指示客户端重新连接,重新连接后, CVar 会泄露回服务器。

漏洞利用流程:

POC视频截图:

来自Neodyme的结束语:

人们经常问我们花了多少时间来构建这个漏洞利用链,遗憾的是,我们无法确定花费的总时间,几个星期以来,我们晚上在 Discord 上会面,交流想法,一起编程并分析我们的发现,直到深夜,那时的 Alain 在 CS:GO 中玩了大约 250 小时,还没有玩过一场在线比赛,我们“相对”快速地发现了这些漏洞,但对于他们的漏洞赏金计划,Valve 需要一个全链漏洞来证明 RCE 的影响,如果没有精心演示,研究将在 30% 的时间后完成。因此,我们在 RCE 演示中投入了相当多的时间。

说到 Valve:我们通过各种看似简单的 HackerOne 报告了解到 Valve 为 CS:GO 支付的高额费用,当时的报告只需要证明内存损坏就可以获得全额赏金,在我们的三份不同报告被迅速宣布有效后,我们最初的兴奋很快就消退了,但即使在 13 个月和多次请求之后仍然没有得到解决,经过大量压力和全面披露的威胁,这些漏洞终于被修复了。每个漏洞的支出为 7.5k,低于我们的预期,总而言之,这是一次发人深省的经历。

对我们来说,CS:GO 漏洞赏金之旅是我们第一次将数周时间投入到一个项目中,我们个人的收获主要是:

  • 不要只想着能够快速寻找到关键漏洞
  • 组合你的漏洞以充分发挥其潜力
  • 留意边缘情况和开发人员没有想到的事情
  • 更加努力!如果碰壁,要寻找突破口,不要过早放弃