前几天, GitHub 上的PPLdump出现了一个issue,指出它不再适用于 Windows 10 21H2 Build 19044.1826。起初我持怀疑态度,所以我启动了一个新的虚拟机并开始调查。这是我发现的……
原文链接:https://itm4n.github.io/the-end-of-ppldump/
简而言之 PPLdump
如果您正在阅读本文,我会假设您已经知道 PPLdump 是什么以及它的作用。但以防万一,这里有一个非常简短的总结。
PPLdump是一个用 C/C++ 编写的工具,它实现了一个用户态漏洞利用,以管理员身份将任意代码注入 PPL。这项技术是 Alex Ionescu 和 James Forshaw 对受保护进程(PPs 和 PPLs)进行的深入研究的众多发现之一。
提醒一下,它的工作原理是这样的:
- 调用API
DefineDosDevice
以诱使 CSRSS 服务创建\KnownDlls
指向任意位置的符号链接。 - 创建一个新的 Section 对象(由前面的符号链接指向)来托管包含我们要注入的代码的自定义 DLL 的内容。
- 由作为 PPL 运行的可执行文件导入的 DLL 被劫持,我们的代码被执行。
这里要记住的最重要的事情是,整个漏洞利用依赖于 PPL 中存在但不存在于 PP 中的弱点。实际上,PPL 可以从\KnownDlls
目录加载 DLL ,而 PP 总是从磁盘加载 DLL。这是一个关键的区别,因为只有在最初从磁盘读取 DLL 以创建新的 Section 对象时才会检查 DLL 的数字签名。映射到Process的虚拟地址空间时,事后不检查。
构建 19044.1826 发生了什么?
PPLdump 的调试输出已在 GitHub问题中提供,但我在带有 2022 年 7 月更新包(Windows 10 21H2 Build 19044.1826)的 Windows 10 VM 中复制了它。
c:\Temp\PPLdump.exe -d lsass lsass.dmp
[lab-admin] [*] Found a process with name 'lsass' and PID 740
[DEBUG][lab-admin] Check requirements
[DEBUG][lab-admin] Target process protection level: 4 - PsProtectedSignerLsa-Light
[lab-admin] [*] Requirements OK
[...]
'\KernelObjects\EventAggregation.dll'
[lab-admin] [*] DefineDosDevice OK
[...]
[DEBUG][SYSTEM] Check whether the symbolic link was really created in '\KnownDlls\'
'\KernelObjects\EventAggregation.dll'
[...]
[DEBUG][SYSTEM] Create protected process with command line: C:\WINDOWS\system32\services.exe 740 "lsass.dmp" 2f2e0a5f-40d4-4034-ba27-81498c6869b -d
[SYSTEM] [*] Started protected process, waiting...
[DEBUG][SYSTEM] Unmap section '\KernelObjects\EventAggregation.dll'...
[DEBUG][SYSTEM] Process exit code: 0
[-] The DLL was not loaded. :/
总的来说,输出看起来相当不错,符号链接被正确创建,\KnownDlls
所以乍一看,这个DefineDosDevice
技巧仍然可以正常工作。这可以通过 WinObj 轻松确认,因为如果不能在“Windows TCB”级别执行 PPL 中的代码,就无法删除符号链接。

然后使用我们自定义 DLL 的内容创建一个新部分,但该工具[-] The DLL was not loaded.
在尝试劫持后最终失败并出现错误,该错误EventAggregation.dll
通常由services.exe
.
在这种情况下,显而易见的做法是启动 Process Monitor,看看我们是否能发现任何看起来不正确的地方。

从最初的事件中,我们已经可以看到某些事情并没有按计划进行。由于services.exe
作为 PPL 执行,我们不应该在 DLL 上看到任何文件操作(例如 CreateFile
或CreateFileMapping
)kernel32.dll
,KernelBase.dll
因为这些是已知的 DLL。相反,它们应该直接从各自的部分\KnownDlls\kernel32.dll
和\KnownDlls\kernelbase.dll
.
结论是 PPL 现在看起来就像 PP 一样,因此不再依赖于Known DLL。
NTDLL 中的补丁?
PPL 流程的创建方式显然发生了一些变化。我已经知道去哪里看,但为了这篇文章,我将通过二进制差异以正确的方式做到这一点。
我首先在 Winbindex 上获得了 Windows 10 21H2 的最后两个版本,并ntdll.dll
使用Windows SDK下载了公共符号。symchk.exe

"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\symchk.exe" /s srv*C:\symbols*https://msdl.microsoft.com/download/symbols C:\Temp\ntdll_*.dll
SYMCHK: FAILED files = 0
SYMCHK: PASSED + IGNORED files = 2
在加载文件并分析它们之后,我只是使用 Ghidra 的BinDiff 扩展来以适当的格式导出结果。

然后可以将两个“BinExport”文件导入 BinDiff 以比较两个版本的ntdll.dll
. 通过“相似度”对函数进行排序,我们可以立即看到 7 个函数有一些细微的差异,但其中一个非常突出:LdrpInitializeProcess
. 这正是我期望找到一些变化的地方。

我们还可以看到有一个不匹配的功能,它是在最新版本中添加的:Feature_Servicing_2206c_38427506__private_IsEnabled
.

加载程序中已知的 DLL 处理
最初,当创建一个新进程时,只加载 NTDLL。在 NTDLL 中实现的图像加载器负责加载其他 DLL(以及许多其他事情)。要确定它是否应该使用已知 DLL ,它只需检查进程环境块( PEB
)中的几个标志。
此检查在以下屏幕截图(构建版本10.0.19044.1741
)中突出显示。

该PEB
结构已部分记录,但我们不会在官方文档中找到我们需要的信息。另一方面,Process Hacker包含一个更详细的定义。
// phnt/include/ntpebteb.h
typedef struct _PEB
{
BOOLEAN InheritedAddressSpace; // Byte at (byte*)peb+0
BOOLEAN ReadImageFileExecOptions; // Byte at (byte*)peb+1
BOOLEAN BeingDebugged; // Byte at (byte*)peb+2
union
{
BOOLEAN BitField; // Byte at (byte*)peb+3
struct
{
BOOLEAN ImageUsesLargePages : 1;
BOOLEAN IsProtectedProcess : 1;
BOOLEAN IsImageDynamicallyRelocated : 1;
BOOLEAN SkipPatchingUser32Forwarders : 1;
BOOLEAN IsPackagedProcess : 1;
BOOLEAN IsAppContainer : 1;
BOOLEAN IsProtectedProcessLight : 1;
BOOLEAN IsLongPathAwareProcess : 1;
};
};
// ...
}
在偏移量 3 处(peb + 3
在if
语句中),我们可以找到一个包含一组 8 位标志的字节值。最低有效位保存ImageUsesLargePages
标志的值,而最高有效位保存IsLongPathAwareProcess
标志的值。
有了这些知识,我们就可以将代码翻译*(byte *)(peb + 3)
成peb->BitField
. 然后,该值0x42
是一个掩码,允许加载程序隔离和检查标志IsProtectedProcess
和IsProtectedProcessLight
. 因此,反编译后的代码if ((*(byte *)(peb + 3) & 0x42) == 2)
可以解释如下。
if (peb->IsProtectedProcess && !peb->IsProtectedProcessLight) {
// Do NOT use Known DLLs
} else {
// Use Known DLLs
}
换句话说,只有当进程是PP时, Known DLL才会被忽略,因此PPL的行为就像正常进程一样。这是对我们已经知道的内容的确认,所以让我们找出构建版本中的变化。10.0.19044.1806
如果我们搜索同一行代码,我们会立即意识到还有一个额外的检查取决于Feature_Servicing_2206c_38427506__private_IsEnabled()
. 多么巧合!

在该else
块中,我们可以看到以下检查。

因此,Ghidra 生成的反编译代码可以总结如下。
bool bFeatureEnabled = Feature_Servicing_2206c_38427506__private_IsEnabled();
if (bFeatureEnabled == 0) {
if ((*(byte *)(peb + 3) & 0x42) != 2) {
// Use Known DLLs
} else {
// Do NOT use Known DLLs
}
} else {
if ((*(byte *)(peb + 3) & 2) != 0) {
// Do NOT use Known DLLs
} else {
// Use Known DLLs
}
}
如果我们应用我之前详述的相同逻辑,我们可以将上面的代码翻译成这个更易读的版本。
bool bFeatureEnabled = Feature_Servicing_2206c_38427506__private_IsEnabled();
if (bFeatureEnabled == FALSE) {
if (peb->IsProtectedProcess && !peb->IsProtectedProcessLight) {
// Do NOT use Known DLLs
} else {
// Use Known DLLs
}
} else {
if (peb->IsProtectedProcess) {
// Do NOT use Known DLLs
} else {
// Use Known DLLs
}
}
补丁现在看起来很清晰。首先,检查“功能服务”值。如果禁用此功能,加载程序会退回到以前版本的代码,因此 PPL 会加载Known DLL。另一方面,如果启用此功能,加载程序只需检查标志是否peb->IsProtectedProcess
已设置。因此,受保护的进程(无论是 PP 还是 PPL)都不会使用Known DLLs。
装载机中的新检查
在上一部分中,我们看到 的结果Feature_Servicing_2206c_38427506__private_IsEnabled()
决定了加载器将使用的有关受保护进程和已知 DLL的逻辑。乍一看,这个函数似乎并不复杂,所以让我们看看我们能从中学到什么。

根据 Ghidra 生成的反编译代码,该函数似乎首先检索全局变量的值Feature_Servicing_2206c_38427506__private_featureState
,如果尚未初始化,则对其进行初始化,然后返回其第四位 ( uVar1 >> 3 & 1
) 的值。
DWORD Feature_Servicing_2206c_38427506__private_IsEnabled() {
DWORD dwFeatureServicingState;
BOOL bIsEnabled;
dwFeatureServicingState = Feature_Servicing_2206c_38427506__private_featureState;
if ((dwFeatureServicingState & 1) == 0) {
// The global variable is not yet initialized, initialize it.
dwFeatureServicingState = wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState(...);
}
// Extract the fourth bit
bIsEnabled = dwFeatureServicingState >> 3 & 1;
// ...
return bIsEnabled;
}
因此,看起来全局变量Feature_Servicing_..._featureState
包含一组位标志,用于确定是否启用了特定功能。借助几行 C/C++ 和调试器,我们可以很容易地验证这一点。
#include <iostream>
#include <Windows.h>
typedef UINT(NTAPI* _FeatureIsEnabled)();
int wmain(int argc, wchar_t* argv[])
{
DWORD dwOffsetFeatureIsEnabled = 0x0009b360;
DWORD dwOffsetFeatureServicingState = 0x0016d288;
PDWORD pFeatureServicingState = NULL;
_FeatureIsEnabled FeatureIsEnabled = NULL;
BOOL bFeatureIsEnabled = FALSE;
// Get NTDLL base address
HMODULE ntdll = LoadLibraryW(L"ntdll.dll");
// Calculate address of Feature_Servicing_..._featureState
pFeatureServicingState = (PDWORD)((PBYTE)ntdll + dwOffsetFeatureServicingState);
// Calculate address of Feature_Servicing_..._IsEnabled()
FeatureIsEnabled = (_FeatureIsEnabled)((PBYTE)ntdll + dwOffsetFeatureIsEnabled);
wprintf(L"Feature_Servicing_2206c_38427506__private_featureState: 0x%08x\r\n", *pFeatureServicingState);
bFeatureIsEnabled = FeatureIsEnabled();
wprintf(L"Feature enabled: %d\r\n", bFeatureIsEnabled);
wprintf(L"----\r\n");
wprintf(L"Setting the fourth bit to 0\r\n");
*pFeatureServicingState = *pFeatureServicingState & 0xfffffff7;
wprintf(L"Feature_Servicing_2206c_38427506__private_featureState: 0x%08x\r\n", *pFeatureServicingState);
bFeatureIsEnabled = FeatureIsEnabled();
wprintf(L"Feature enabled: %d\r\n", bFeatureIsEnabled);
return 0;
}
运行上面的代码会产生以下输出。
C:\Temp\FeatureServicing.exe
Feature_Servicing_2206c_38427506__private_featureState: 0x0000001b
Feature enabled: 1
----
Setting the fourth bit to 0
Feature_Servicing_2206c_38427506__private_featureState: 0x00000013
Feature enabled: 0
Feature_Servicing_..._featureState
is的值0x0000001b
,转换为0001 1011
二进制。第四位被设置,返回值为1
. 1111 0111
在第二部分中,我使用掩码(即 )使用按位与操作手动取消设置第四位0xf7
。在这种情况下,返回值为0
,这倾向于证实我对代码的解释。
最后,为了更好的衡量,我们还可以手动设置Feature_Servicing_..._featureState
to的值0
并检查返回的值wil_..._ReevaluateCachedFeatureEnabledState(...)
以确保它是0x1b
.

返回值(见RAX
)实际上是0x7ff700000000001b
但EAX
寄存器(即前32位RAX
)用于以下操作(mov ebx,eax
)所以有效值确实是0x0000001b
。
结论
我不确定是什么促使微软首先区分关于已知 DLL的 PP 和 PPL。也许这是一个性能问题,我不知道。无论如何,他们已经意识到了这个潜在的弱点,否则我猜他们不会对 PP 破例。问题是,这个安全漏洞现在已经修补,这是向前迈出的一大步。我喜欢认为我在这个变化中扮演了一个小角色,尽管我完全知道所有的工作都已经由 Alex 和 James 完成。
总之,这确实是“ PPLdump 的终结”。然而,这个工具只利用了 PPL 的一个弱点,但我们可能仍然可以利用其他几个用户空间问题。所以,从我的角度来看,这也是一个开始研究另一个旁路的机会……
链接和资源
- Windows 漏洞利用技巧:利用任意对象目录创建来提高本地特权 – https://googleprojectzero.blogspot.com/2018/08/windows-exploitation-tricks-exploiting.html
- 您真的了解 LSA 保护 (RunAsPPL) 吗?- https://itm4n.github.io/lsass-runasppl/
- 在用户区绕过 LSA 保护 – https://blog.scrt.ch/2021/04/22/bypassing-lsa-protection-in-userland/