文章总结: 本文研究在LSASS作为PPL运行时如何提取内存转储,分析了MiniDumpWriteDump函数的使用限制,发现xolehlp.dll中的WriteDumpThread函数可简化调用,利用WinSock2自动拨号DLL功能将xolehlp.dll加载到LSASS,通过SystemInformer技术枚举受保护进程模块并动态解析地址,实现完整的转储链,展示了在用户空间绕过PPL保护获取凭据的方法,对理解Windows内部机制和安全边界具有较高价值 综合评分: 93 文章分类: 渗透测试,内网渗透,逆向分析,漏洞分析,实战经验
PPL 中的幽灵 – LSASS 内存转储
Ots安全
2026年3月18日 13:00 广东
威胁简报
恶意软件
漏洞攻击
本文探讨了在 LSASS(本地安全授权子系统服务)进程作为轻量级受保护进程 (PPL) 运行时, 如何从中提取内存的技术。现代版本的 Windows 使用 PPL 来保护 LSASS 等敏感进程免受用户模式工具的篡改或凭据转储。
该研究最初旨在 利用 BYOVDLL(自带漏洞 DLL) 和 KeyIso 服务中的一个漏洞,在 LSASS 内部实现任意代码执行。然而,由于在受保护进程内实现可靠执行的复杂性和不稳定性,作者将研究重点转移到了一个更实际的目标:创建 LSASS 内存转储,这通常足以恢复凭据。
该研究的核心部分考察了Windows调试API函数 ,MiniDumpWriteDump 该 函数dbghelp.dll通常用于生成进程内存转储。为了成功创建转储,攻击者需要 目标进程的有效句柄以及 转储文件的有效句柄。当在受保护的进程(例如LSASS)中执行操作时,由于额外的限制,获取和使用这些句柄会变得非常困难。
本文演示了如何通过重用已打开的文件句柄并枚举 LSASS 中现有的映射文件或模块来部分绕过这些限制 。这种方法允许在不创建新的可疑句柄的情况下执行转储操作。
总体而言,该研究深入分析了 Windows 内部机制、PPL 保护边界和实用的凭据转储技术,重点介绍了在某些情况下如何利用用户模式机制来对抗受保护的进程。
在尝试使用 BYOVDLL 技术和 KeyIso 服务中的 N 天漏洞在受保护的 LSASS 进程中执行任意代码失败后,我冷静下来,重新 审视了自己的人生选择, 最终决定采用一个不那么雄心勃勃的方案:一个(并不那么)简单的内存转储。毕竟,对于 LSASS 来说,我们最感兴趣的是提取存储在内存中的凭据。
回归基础:MiniDumpWriteDump
转储进程内存最常见的方法是调用 MiniDumpWriteDump。它需要一个具有足够访问权限的进程句柄、一个进程 ID、一个输出文件的句柄以及一个表示“转储类型”的值(例如 MiniDumpWithFullMemory)。
BOOL MiniDumpWriteDump( [in] HANDLE hProcess, // Target process handle [in] DWORD ProcessId, // Target process ID [in] HANDLE hFile, // Output file handle [in] MINIDUMP_TYPE DumpType, // e.g. MiniDumpWithFullMemory (2) [in] PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, // NULL or valid pointer [in] PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, // NULL or valid pointer [in] PMINIDUMP_CALLBACK_INFORMATION CallbackParam // NULL or valid pointer);
在这些参数中,文件句柄是我们这里最难获取的。需要注意的是,我们希望在 LSASS 内部执行转储操作,因此理想情况下,我们需要依赖进程中已经打开的文件句柄。我们或许可以找到解决办法,但这还不是我们目前面临的主要问题。
主要问题在于 MiniDumpWriteDump 它有 7 个参数,而且与 \_\_init\_\_ 函数不同 DuplicateHandle,省略最后 2 或 3 个参数来节省内存空间的技巧在这里并不适用,因为这些参数是指针。如果通过这些参数传递随机数据,则极有可能造成非法内存访问,从而导致程序崩溃。因此,我们需要一种更简单的方法来调用 \_\_init\_\_ 函数 MiniDumpWriteDump!
间接调用 MiniDumpWriteDump
理想情况下,我希望找到一个能够调用 MiniDumpWriteDump 并满足以下条件的函数。
- 该函数应该存在于 LSASS 中已加载的模块中。
- 该函数必须具有“合理”数量的参数,这样我才能使用技巧 NdrServerCallAll 来调用它。
为了找到潜在的候选对象,我选择了一种非常简单的方法。我在系统文件夹内的 DLL 文件中搜索了该字符串的出现位置 MiniDumpWriteDump 。请注意,我实际上是递归地执行了搜索,但为了简洁起见,这里只展示根文件夹的结果。
C:\Windows\System32>findstr /m MiniDumpWriteDump *.dll 2>NULcombase.dllcomsvcs.dlldbgcore.dlldbghelp.dlldiagtrack.dllDismApi.dllFaultrep.dllKernelBase.dllmsdtckrm.dllmsdtclog.dllmsdtcprx.dllmsdtctm.dllmsdtcuiu.dllmssrch.dllmtxclu.dllmtxoci.dlltellib.dllUpdateAgent.dllwdscore.dllwer.dllwerui.dllWUDFPlatform.dllxolehlp.dll
在此输出中,您可能已经注意到熟悉的 comsvcs.dll,它导出方便的函数 MiniDump,并允许直接从命令行转储进程的内存,如下所示(请参阅 MITRE ATT&CK > OS Credential Dumping 以作参考,因为我不知道该归功于谁最初发现了这项技术)。
rundll32.exe C:\Windows\System32\comsvcs.dll MiniDump PID lsass.dmp full
这或许是一个有效的候选模块,但它不符合我的第一个条件。LSASS comsvcs.dll 没有加载这个模块。遗憾的是,几乎所有其他模块都存在同样的问题。尽管如此,我还是坚持原计划,继续进行调查。
我不得不浏览整个列表才能找到真正感兴趣的候选对象。下面的屏幕截图显示了 内部函数 MiniDumpWriteDump 如何动态导入 API 。
- WriteDump Threadxolehlp.dll
Ghidra – MiniDumpWriteDump imported in xolehlp.dll
正如我之前提到的,这个 DLL 不是由 LSASS 加载的,所以它不符合我的第一个条件,但请耐心听我说,因为它还有其他优点,可能会在很大程度上弥补这个缺点。
下面的代码片段展示了该函数的功能 xolehlp!WriteDumpThread ,但没有包含所有错误处理部分。
ulong __cdecl WriteDumpThread(void *param_1){ // ...
// [1] Get dump type value from HKLM\Software\Microsoft\MSDTC -> MemoryDumpType dwDumpType = GetLocalDTCProfileInt("MemoryDumpType",0);
// [2] Get dump folder path from HKLM\Software\Microsoft\MSDTC -> MemoryDumpLocation RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\MSDTC", 0, KEY_READ, &hKey); RegQueryValueExW(hKey, L"MemoryDumpLocation", NULL, &dwValueType, pwszDumpFilePath, &dwDataSize);
// Generate dump file path using process image name and current time...
// [3] Dynamically import MiniDumpWriteDump hModule = LoadLibraryExW(L"DBGHELP.DLL", NULL, 0); pfMiniDumpWriteDump = GetProcAddress(hModule, "MiniDumpWriteDump");
// [4] Prepare the arguments of MiniDumpWriteDump hDumpFile = CreateFileW(pwszDumpFilePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); dwProcessId = GetCurrentProcessId(); hProcess = GetCurrentProcess();
// [5] Invoke MiniDumpWriteDump iVar5 = pfMiniDumpWriteDump(hProcess, dwProcessId, hDumpFile, dwDumpType, NULL, NULL, NULL);
// ...}
首先,它从注册表项中读取两个值 HKLM\Software\Microsoft\MSDTC,分别命名为 MemoryDumpType (1) 和 (2)。然后,它如前所述, 从 (3)MemoryDumpLocation 动态导入 API 。最后,它准备所有必需的参数 (4),然后调用它 (5)。MiniDumpWriteDumpdbghelp.dll
总而言之,这个函数 WriteDumpThread 只有一个参数,这意味着如果我想调用它,甚至都不需要使用 NdrServerCallAll 技巧。而且它还能从注册表中检索所有主要参数,例如转储类型和转储文件位置。真棒!
这看起来已经好得令人难以置信了,但惊喜还在后面。通过检查交叉引用,我发现这个函数只在一处被使用,如下面的代码片段所示。
void __cdecl DtcRaiseExceptionForWatsonCrashAnalysis(_EXCEPTION_POINTERS *param_1){ // ... QueueUserWorkItem( WriteDumpThread, // LPTHREAD_START_ROUTINE Function NULL, // PVOID Context WT_EXECUTEDEFAULT // ULONG Flags ); // ...}
该函数 WriteDumpThread 通过众所周知的 QueueUserWorkItem API 执行,第二个参数设置为 NULL,这意味着它甚至不在乎它的第一个(也是唯一的)参数。
总之,虽然 xolehlp.dll 它不符合我的第一个条件,但这个功能 WriteDumpThread 是一个不容错过的绝佳机会!
在 LSASS 中加载任意 DLL
我找到了一种独特的方法来导出当前进程的内存,但也改变了问题所在。现在我需要找到一种方法将 DLL 加载 xolehlp.dll 到 LSASS 中。请记住,LSASS 受保护这一事实在这里并不构成限制,因为此 DLL 已由 Microsoft 签名。
有几种众所周知的技术可以将任意 DLL 加载到 LSASS 中,例如:
使用 NTDS 注册表项(探索 Mimikatz – 第 1 部分 – Adam Chester 的 WDigest)。
- 使用 SSP(Sean Metcalf 的 恶意安全支持提供商 (SSP))。
- 使用密码过滤器(Rob Fuller每次更改密码时都会窃取密码 ) 。
- 遗憾的是,这些方法并不适用于我的情况。加载的 DLL 必须导出特定的函数,否则它会立即被卸载 FreeLibrary。
还有更好的方法!只要进程执行某些特定的网络操作,就可以将任意 DLL 永久加载到几乎任何进程中。这项技术依赖于 WinSock2 API 的自动拨号功能,正如 @Hexacorn在博文《超越老旧的运行键,第 24 部分》 中 所解释的那样-https://www.hexacorn.com/blog/2015/01/13/beyond-good-ol-run-key-part-24/。
HKLM\SYSTEM\CurrentControlSet\Services\WinSock2\Parameters|__ AutodialDll: C:\Windows\System32\rasadhlp.dll
简单来说,每当使用 WinSock2 API 时, AutodialDLL 都会加载该值中引用的 DLL。此设置默认值为空 rasadhlp.dll,但如果我们编辑注册表中的此值,理论上可以将任意 DLL 加载到使用此 API 的进程中。实际上,这个“自动拨号”DLL 是由内部函数加载的 LoadAutodialHelperDll,如下所示。
Ghidra – 加载自动拨号 DLL ws2_32.dll
通过查看“调用树”中的传入引用,我们可以看到以下内容。
Ghidra——即将出现的引用 LoadAutodialHelperDll
更深入的分析发现了以下潜在的入口点。我指的是由某个模块导出的函数 ws2_32.dll,因此这些函数可能被其他模块或应用程序调用。
ws2_32!LoadAutodialHelperDll|__ WSAttemptAutodialAddr |__ connect|__ gethostbyname |__ WSAAsyncGetHostByAddr; WSAAsyncGetHostByName; WSAAsyncGetProtoByName; |__ WSAAsyncGetProtoByNumber; WSAAsyncGetServByName; WSAAsyncGetServByPort|__ WSAttemptAutodialName |__ WSALookupServiceNextW; GetHostNameW; GetNameInfoW; GetAddrInfoW; |__ GetAddrInfoExW; getaddrinfo; getnameinfo; gethostbyaddr; gethostname; |__ getservbyname; getservbyport
因此,我们正在寻找 LSASS 中直接或间接使用这些函数的功能。
LSASS 和 WinSock2 API
虽然 WinSock2 自动拨号 DLL 技巧提供了一种将 DLL 永久加载到进程中的方法,但我们无法控制实际加载它的进程,更重要的是,我们无法控制加载的时间。我又一次转移了问题!现在我需要找到一种方法来欺骗 LSASS 加载这个自动拨号 DLL。
部分答案源于一系列意想不到的事件。我 AutodialDLL 在进程监视器中设置了筛选器,仅显示包含特定模式的注册表路径,然后在使用命令提示符时观察到了以下情况。
进程监视器 – LSASS 读取 AutodiallDLL 注册表值
结果发现,在终端中输入完全不相关的命令(例如 net localgroup administrators)时,我触发了“Web 威胁防御服务”(svchost.exe 屏幕截图中的进程),进而导致 lsass.exe 读取 AutodialDLL 注册表值。
遗憾的是,调用堆栈不包含有关此事件来源的太多信息,因为它是回调函数在单独线程中执行的结果.
进程监视器 – 导致调用堆栈 RegQueryValueExA
然而,通过检查之前的事件,我注意到此事件源自对 <function\_name> 的调用 GetAddrInfoExW,而 <function\_name> 是我之前识别出的 LSASS 导出的函数之一 ws2_32.dll 。该调用本身是 LSASS 发送的 HTTP 请求的结果。
进程监视器 – 导致调用堆栈 GetAddrInfoExW
追踪此 HTTP 请求的来源,我发现它来自对某个对象的远程过程调用 SspirProcessSecurityContext。看来,安全支持提供程序接口 (SSPI) 又一次派上了用场!
进程监视器 – 调用堆栈 SspirProcessSecurityContext
乍一看,这个过程导致发送 HTTP 请求的原因并不明显。但经过进一步分析,我发现这种情况发生在调用 <Schannel> AcquireCredentialsHandleA、<Schannel> InitializeSecurityContextA以及使用带有 --flag 标志的 Schannel 安全服务提供程序 时SCH_CRED_REVOCATION_CHECK_CHAIN。
这是合理的,因为 Schannel 提供了 SSL/TLS 协议的实现,而此标志会使其检查给定证书的证书链。为此,它会通过 HTTP 获取证书吊销列表 (CRL) 或使用在线证书状态协议 (OCSP)。
发现这一点后,我创建了一个概念验证应用程序来测试这个理论,并成功地让 LSASS 通过这种方式加载自动拨号 DLL。
LSASS 尝试加载自动拨号 DLL
很遗憾,结果不如预期可靠。似乎存在某种缓存机制,阻止对同一 URL 进行两次查询。总之,我找不到更好的解决方案,所以只能先这样了。
枚举 LSASS 中加载的模块
得益于 WinSock2 API 的自动拨号功能和 SSPI,我现在可以将任意 DLL 加载到 LSASS 中。但是,我也提到过这种方法并非 100% 可靠,因此我还需要一种方法来确定模块是否真的已加载。
由于 LSASS 受到保护,因此无法直接打开它来枚举其模块。为了解决这个问题, 进程资源管理器 使用了一个内核模式驱动程序,从而能够获取受保护进程的特权句柄。显然,我采用这种技巧毫无意义,因为我希望我的漏洞利用完全在用户空间运行。
不过,我知道的一点是,与 Process Explorer不同, System Informer 能够在不使用任何内核技巧的情况下实现类似的结果。
系统信息提示 – 内核模式驱动程序默认未启用
如下面的截图所示,打开进程属性时,模块列表会显示出来,即使 LSASS 在这里是以 PPL 进程的形式运行的。与常规进程的唯一区别在于没有“树状视图”,这表明它可能使用了不同的方法来获取此列表。
系统信息器 – 枚举受保护的 LSASS 进程中加载的模块
- 使用 System Informer 上的 API Monitor,我发现它会执行类似这样的操作:
- 使用 . 打开目标进程 PROCESS_QUERY_LIMITED_INFORMATION。
- NtQueryVirtualMemory 与班级 通话 MemoryBasicInformation。
- 根据返回的信息,调用 NtQueryVirtualMemory 该类 MemoryMappedFilenameInformation 以获取映射文件的路径 UNICODE_STRING。
多亏了这项分析,我在phlib/native.c文件中的一个名为 的函数 里找到了实现 PhpEnumGenericMappedFilesAndImages。从那里开始,在独立工具中重现这项技术就轻而易举了。
列出受保护的 LSASS 进程中加载的模块
又一个问题解决了!
动态解析地址
最后一个需要解决的问题是如何动态获取地址 xolehlp!WriteDumpThread 。虽然这只是一个概念验证,但我真的不喜欢依赖版本相关的硬编码偏移量。因此,我必须找到一种方法在运行时解析这个地址。
如前所述,此函数通过 QueueUserWorkItem API 调用。这意味着,在同一组指令中,我们既有已知的符号 QueueUserWorkItem ,也有目标函数 WriteDumpThread。请注意,此处显示此函数的名称,因为它包含在公共 PDB 文件中 xolehlp.pdb。实际上,此名称并不存在于二进制文件本身中。
Ghidra – 通过 API进行函数WriteDumpThread 调用 QueueUserWorkItem
换句话说,我们可以利用这个交叉引用来确定地址 WriteDumpThread。所以让我们先来检查相应的汇编代码。
xor r8d,r8d ; param3 = 0lea rcx,[rip+0x391] ; param1 = @WriteDumpThread [2]xor edx,edx ; param2 = 0rex.W call QWORD PTR [rip+0x6e40] ; Call QueueUserWorkItem [1]
请记住,x86_64 架构使用 RIP 相对偏移量,这就是为什么我们感兴趣的地址表示为 rip+0x391 和 rip+0x6e40。
我们首先要找到对 QueueUserWorkItem (1) 的调用。请注意,该函数在代码中只出现过一次 xolehlp.dll。为此,我们可以执行以下操作。
- QueueUserWorkItem 感谢以下方法 获取导入的 API 的地址 GetProcAddress。
- 48 ff 15 ?? ?? ?? ?? 找到类似本节中的 模式 .text ,其中 48 表示目标是 64 位地址, ff 15 表示 CALL 指令。
- 使用 RIP 相对偏移量(接下来的 4 个字节)计算绝对地址,并检查结果是否与步骤 1 中找到的值匹配。
- 如果不行,检查下一次出现的情况并重复该过程,直到找到正确的结果为止。
找到指令后 CALL ,我们可以反向遍历字节码,找到 LEA 更新寄存器的指令(2) RCX 。提醒一下,该寄存器 RCX 包含 x86_64 调用约定中第一个参数的值。这可以通过以下方式实现。
- 找到类似这样的模式 48 8d 0d ?? ?? ?? ??,其中 48 表示 64 位目标地址, 8d 0d 表示 LEA 对 ECX/RCX 寄存器的操作。
- 使用 RIP 相对偏移量(接下来的 4 个字节)来计算绝对地址,该地址应该是 WriteDumpThread。
综合起来
总而言之,最终的漏洞利用程序会执行以下操作:
- xolehlp.dll 它使用 WinSock2 自动拨号技巧和 SSPI强制 LSASS 加载 。
- 它导入一个包含易受攻击的 DLL 数字签名的目录文件。
- 它使用存在漏洞的版本(重新)启动 KeyIso 服务 keyiso.dll。
- 它使用存在漏洞的版本注册密钥存储提供程序 ncryptprov.dll。
- 它利用信息泄露来 ncryptprov.dll 泄露提供者对象的地址。
- 它会对文件设置一个机会性锁 lsass.exe ,以检测内存转储何时开始。
- 它利用释放后使用漏洞 keyiso.dll 来触发对的调用 WriteDumpThread,并等待。
- 如果触发了机会锁,它会检查输出文件夹中是否创建了转储文件。
- 完成后,它会把所有东西都清理干净。
最终概念验证的执行
结论
最终结果并未完全达到我启动这个项目时的预期。主要原因是,我选择的底层UAF漏洞显然并非此类攻击的最佳选择。其固有的不可靠性导致整个攻击链极不稳定,难以稳定复现。
另请注意,所有这些工作都是在文章《 在 Windows 11 上无需易受攻击的驱动程序即可将代码注入 PPL 进程》https://blog.slowerzs.net/posts/pplsystem/ 发表之前完成的,该文章讨论了一种内存转储技术,该技术基本上使此概念验证完全无关紧要。
尽管如此,这仍然是一个学习大量知识、练习一些高级用户空间漏洞利用以及发现一些可以在其他情况下重复使用的新技巧的好机会。
- 更多文献参考:
https://infosec.exchange/@xpn
https://attack.mitre.org/techniques/T1003/001/
https://blog.xpnsec.com/exploring-mimikatz-part-1/
https://blog.carnal0wnage.com/2013/09/stealing-passwords-every-time-they.html
Beyond good ol’ Run key, Part 24
END
公众号内容都来自国外平台-所有文章可通过点击阅读原文到达原文地址或参考地址
排版 编辑 | Ots 小安
采集 翻译 | Ots Ai牛马
公众号 | AnQuan7 (Ots安全)
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Ots安全 《PPL 中的幽灵 – LSASS 内存转储》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论