利用星际争霸1 EUD漏洞
星际争霸(1998年发布)至今仍是最优秀的策略游戏之一。20多年后,它依然拥有活跃的社区,并在2017年发布了重制版,更新了画面和音效。但如同大多数软件,它也存在不少漏洞。其中一个漏洞是游戏地图内嵌脚本解析器中的任意读写漏洞。虽然我一直认为该漏洞可被利用,但从未见过公开案例。上周末,我亲自编写了一个漏洞利用程序,并将其转化为Midnight Sun CTF 2020资格赛的挑战题目。本篇博客将介绍背景知识、漏洞原理及利用方法,第二部分将阐述如何将其转化为CTF挑战及各战队的解决方案。
漏洞原理 - 扩展单位死亡(EUD)
游戏发布八年后,2006年1月18日,星际争霸1.13f补丁发布。补丁说明仅提到“修复了若干导致游戏漏洞的bug”,其中包含一个被称为“扩展单位死亡”(EUD)的特定漏洞。EUD漏洞得名于其利用机制,允许在程序内存中进行任意读写。
星际争霸内置简单的脚本系统,允许地图包含小型代码片段,用于操纵游戏部分内容以创建新游戏模式或剧情驱动的战役地图。这些脚本称为“触发器”,采用简单的if-then结构。每个触发器包含一组“条件”和“动作”。当所有条件满足时,所有动作将被执行。触发器可影响特定玩家或所有玩家。例如,条件可以是“将单位X移动到位置Y”,动作可以是“显示文本消息X”或“创建单位Y”。
游戏会记录每个玩家击杀的各类单位数量,称为“死亡计数器”,存储在<玩家数量> x <单位类型数量>的无符号4字节条目表中。单位被击杀时,会执行类似unit_deaths[player_id][unit_type]++的操作。这些值可在触发器条件中用于比较特定值以判断条件是否满足,形成if(unit_deaths[current_player][unit_type] == X)的条件。也可在触发器动作中通过设置、加减操作修改条目,形成unit_deaths[player_id][unit_type] =/+=/-= X的动作。漏洞在于这些动作中未对单位类型索引进行有效范围检查,允许我们相对于该数组在任何偏移量进行读写、加减操作。此外,由于程序未启用PIE且数组作为全局变量存储,其地址已知,我们可完全控制访问地址。
1.13f补丁为以下三个函数添加了范围检查:
0x004C5DD0: action_deaths_set0x004C5C60: action_deaths_add0x004C5A80: action_deaths_sub
以下是其中一个函数的部分代码,展示了任意写入及补丁中添加的检查:
|
|
环境搭建
我的目标是编写漏洞利用程序,实现在其他玩家计算机上的远程代码执行。在线游戏时,玩家可主持自定义地图游戏,加入的玩家将从主机下载地图,所有触发器将在各客户端同步运行。因此,我需创建地图,使得其他玩家加入游戏时,我能在其客户端执行代码。
该漏洞在1.13f版本中被修复,故仅旧版本客户端易受攻击。但许多玩家喜爱利用此机制创建的“EUD地图”,因此出现了EUDEnable工具,通过内存补丁重新引入漏洞。1.16.1版本于2009年1月19日发布,成为八年间最后一个补丁(直至2017年重制版发布),被全球爱好者深入探索、逆向工程和文档化。我选择模拟运行1.16.1版本并激活EUDEnable的环境编写利用程序,但技术应适用于所有1.16.1之前版本。
为创建自定义地图,我使用第三方地图编辑器ScmDraft 2,其包含trigedit工具,可用基于文本的语言编辑地图触发器,而非官方编辑器的GUI工作流。
为便于调试,需在二进制文件中撤销补丁而非使用EUDEnable,因为EUDEnable通过启动游戏、附加调试器并挂钩“设置单位死亡”动作处理器实现,导致无法附加自身调试器。我使用IDA Pro定位范围检查位置,并用Binary Ninja移除检查。应用补丁后,无需EUDEnable且可附加x64dbg等调试器。
漏洞利用
客户端为标准32位Windows二进制文件,具有以下保护(使用winchecksec检测):
|
|
关键点在于启用NX但未启用ASLR,故代码和数据段位于已知地址。
利用此原语需知死亡计数器内存位置以确定偏移量对应内存位置。由于无ASLR,通过反汇编或调试易得。幸运的是,trigedit已内置抽象:MemoryAddr(addr, action, value)动作接受绝对地址并在内部转换为适当偏移。需注意因数组为4字节值,仅可访问4字节对齐地址。action参数可用于内存加减操作,但我仅使用“设置为”变体。
了解游戏数据存储位置亦有帮助。得益于游戏流行度、EUD漏洞及BWAPI等项目,玩家已映射游戏内存布局和触发器系统细节。
第一步是控制EIP。这很简单。内存布局中可找到“触发器动作函数数组”,包含所有可触发动作的函数指针。通过覆盖表中条目并调用相应动作,即可控制指令指针。选择“Leader Board (Kills)”动作(ID 20),目标地址为0x00512800 + 4*20 = 0x00512850。在地图中添加以下触发器并运行将使游戏崩溃,EIP设置为0x12345678:
|
|
虽可轻松使用任意写入在内存中设置任意大小ROP链,但需控制栈指针。在找到方法前,控制EIP及第一个参数(栈顶)是有用的原语。
搜索后,使用rp++在二进制文件中发现以下gadget:
|
|
这意味着若将值X写入地址0x0050C63C,值0x00469c72写入地址0x0051BC08,并使用上述方法跳转到0x0040ccb3,将导致ESP寄存器设置为X+0x8904+0x4并执行X处指令。可通过以下触发器实现:
|
|
现已有栈旋转,可执行ROP链。将ROP链写入bss段末尾并在其后放入shellcode。bss段不可执行,但ROP链将解决此问题。ROP链仅调用VirtualProtect使bss可执行后跳转至shellcode。幸运的是,VirtualProtect被导入二进制文件,其导入条目位于0x004FE0F0。使用以下两个gadget设置跳转:
|
|
选择将ROP链置于地址0x6DD000,脚本如下:
|
|
运行此代码将旋转栈,执行VirtualProtect(0x6dd000, 0x100, 0x40, 0x6dd000)后跳转至ROP链之后地址。VirtualProtect调用将使整个bss段可执行。
现仅缺shellcode。作为PoC,我选择32位shellcode运行calc.exe,将其分割为4字节块,使用脚本写入ROP链之后,最终载荷如下:
|
|
为避免手动编写触发器,我创建Python脚本自动生成。工作流程为编辑脚本、运行、复制输出至ScmDraft、编译保存地图后运行。虽有Python库可直接完成此过程,但我未成功使用。
将此触发器放入自定义地图并运行将启动calc.exe并使所有玩家游戏崩溃。当然,shellcode可替换为任意内容,理论上甚至可使利用程序不崩溃游戏,让受害者毫无察觉。
结论
研究并编写我最喜爱游戏的漏洞利用程序极具教育意义,尽管漏洞利用相对简单,但最终成功弹出计算器时我深感满足。我决定让他人体验类似经历,将此漏洞转化为Midnight Sun CTF 2020资格赛的挑战题目,这需要有趣的基础设施并产生多种利用方案。我将在不久后发布关于此挑战的第二篇博客。
我在此漏洞被修复十多年后重新研究的原因之一是2017年12月7日发布的1.21.0版本包含了特殊功能:EUD模拟器。这以安全方式重新引入漏洞,使旧版趣味自定义地图可再次游玩,同时避免内存破坏攻击。暴雪员工Elias Bachaalany在REcon Brussels 2018上介绍了该模拟器及相关工作。