PPLdump 的终结
2023-1-31 22:7:29 Author: Z2O安全攻防(查看原文) 阅读量:17 收藏

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。

只供对已授权的目标使用测试,对未授权目标的测试作者不承担责任,均由使用本人自行承担。

文章正文

几天前,GitHub 上针对PPLdump 开了一个issue,称它不再适用于 Windows 10 21H2 Build 19044.1826。一开始我很怀疑,所以我启动了一个新的虚拟机并开始调查。这是我发现的……

PPLdump 简而言之

如果您正在阅读本文,我假设您已经知道 PPLdump 是什么以及它的作用。但以防万一,这里有一个非常简短的总结。

PPLdump是一个用 C/C++ 编写的工具,它实现了一个Userland exploit,以管理员身份将任意代码注入 PPL。这项技术是 Alex Ionescu 和 James Forshaw 对受保护进程(PP 和 PPL)进行的深入研究的众多发现之一。

提醒一下,它是这样工作的:

  1. 1. 调用 APIDefineDosDevice以欺骗 CSRSS 服务创建\KnownDlls指向任意位置的符号链接。

  2. 2. 创建一个新的 Section 对象(由前面的符号链接指向)来托管包含我们要注入的代码的自定义 DLL 的内容。

  3. 3. 由作为 PPL 运行的可执行文件导入的 DLL 被劫持,我们的代码被执行。

这里要记住的最重要的事情是,整个漏洞利用依赖于 PPL 中存在的弱点,而不是 PP 中存在的弱点。实际上,PPL 可以从\KnownDlls目录加载 DLL ,而 PP 总是从磁盘加载 DLL。这是一个关键的区别,因为 DLL 的数字签名仅在最初从磁盘读取以创建新的 Section 对象时才会被检查。当它映射到进程的虚拟地址空间时,它不会在之后被检查。

构建 19044.1826 发生了什么?

GitHub 问题中已经提供了 PPLdump 的调试输出,但我使用 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 中执行代码,则无法删除符号链接。

WinObj - 在 \KnownDlls 中创建的符号链接

然后使用我们的自定义 DLL 的内容创建一个新部分,但该工具[-] The DLL was not loaded.在尝试劫持后最终失败并出现错误EventAggregation.dll,这通常由services.exe.

在这种情况下,显而易见的做法是启动 Process Monitor,看看我们是否能发现任何看起来不对劲的地方。

使用进程监视器进行 PPLdump 调试

从最初的事件中,我们已经可以看出有些事情没有按计划进行。由于services.exe作为 PPL 执行,我们不应该在 DLL 上看到任何文件操作(例如 CreateFileCreateFileMappingkernel32.dllKernelBase.dll因为这些是已知 DLL。相反,它们应该直接从各自的部分\KnownDlls\kernel32.dll\KnownDlls\kernelbase.dll.

结论是 PPL 现在的行为看起来就像 PP,因此不再依赖于已知的 DLL

NTDLL 中的补丁?

PPL 流程的创建方式显然发生了一些变化。我已经知道去哪里找了,但是为了这篇文章,我将通过二进制比较以正确的方式做到这一点。

我首先在 Winbindex 上获得了 Windows 10 21H2 的最后两个版本,ntdll.dll使用 Windows SDK 下载了公共符号。symchk.exe

要比较的 NTDLL 文件
"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 扩展以适当的格式导出结果。

Ghidra - 文件导出

然后可以将这两个“BinExport”文件导入到 BinDiff 中,以比较两个版本的ntdll.dll. 通过按“相似性”对功能进行排序,我们可以立即看出 7 个功能之间存在一些细微差异,但真正脱颖而出的是:LdrpInitializeProcess. 这正是我希望找到一些变化的地方。

BinDiff - 加载程序已修改

我们还可以看到,有一个无与伦比的功能,是在最新版本中添加的:Feature_Servicing_2206c_38427506__private_IsEnabled

BinDiff - 添加了一个函数

加载程序中已知的 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 + 3if语句中),我们可以找到一个包含一组 8 位标志的字节值。最低有效位保存ImageUsesLargePages标志的值,而最高有效位保存IsLongPathAwareProcess标志的值。 

有了这些知识,我们可以将代码翻译*(byte *)(peb + 3)peb->BitField. 然后,该值0x42是一个掩码,允许加载程序隔离和检查标志IsProtectedProcessIsProtectedProcessLight。因此,反编译后的代码if ((*(byte *)(peb + 3) & 0x42) == 2)可以解释如下。

if (peb->IsProtectedProcess && !peb->IsProtectedProcessLight) {
    // Do NOT use Known DLLs
} else {
    // Use Known DLLs
}

换句话说,仅当进程是PP时才会忽略已知 DLL,因此PPL的行为就像正常进程一样。这是对我们已知信息的确认,所以让我们找出构建版本中发生了什么变化。10.0.19044.1806

如果我们搜索同一行代码,我们会立即意识到有一个额外的检查取决于 返回的值Feature_Servicing_2206c_38427506__private_IsEnabled()。真是巧合!

Ghidra - 加载器中添加了一个支票

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

Ghidra - PEB 检查已修改

因此,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 会加载已知 DLL。另一方面,如果启用此功能,加载程序只需检查标志是否peb->IsProtectedProcess已设置。因此,_受保护的进程_(无论是 PP 还是 PPL)都不会使用Known DLLs

加载程序中的新检查

在前面的部分中,我们看到 的结果Feature_Servicing_2206c_38427506__private_IsEnabled()决定了加载程序将使用的有关受保护进程已知 DLL的逻辑。乍一看,这个函数似乎并不复杂,那么让我们看看我们可以从中学到什么。

Ghidra - 新的功能服务检查

根据 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:\WINDOWS\system32>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二进制。由于设置了第四位,因此返回值为。在第二部分中,我使用掩码( ie )的按位与运算手动取消设置第四位。在这种情况下,返回值是,这倾向于证实我对代码的解释。0x0000001b 0001 1011 1 1111 0111 0xf7 0

最后,为了更好地衡量,我们还可以手动设置Feature_Servicing_..._featureStateto 的值0并检查返回的值wil_..._ReevaluateCachedFeatureEnabledState(...)以确保它是0x1b

WinDbg - 缓存值重新评估

返回值(见RAX0x7ff700000000001b只是EAX寄存器(前 32 位RAX)用于以下操作(mov ebx,eax)所以有效值确实是0x0000001b

结论

我不确定最初是什么促使 Microsoft 区分关于已知 DLL的 PP 和 PPL。也许这是一个性能问题,我不知道。无论如何,他们已经意识到了这种潜在的弱点,否则我想他们也不会为 PP 破例。问题是,这个安全漏洞现在已经被修补,这是向前迈出的一大步。尽管我知道所有的工作都已经由 Alex 和 James 完成,但我喜欢认为我在这一变化中发挥了一点作用。

总之,这确实是 PPLdump 的终结。然而,这个工具只利用了 PPL 的一个弱点,但我们可能仍然可以利用其他几个Userland 问题。因此,从我的角度来看,这也是开始研究另一种旁路的机会……

链接与资源

  • • Windows 漏洞利用技巧:利用任意对象目录创建进行本地特权提升 https://googleprojectzero.blogspot.com/2018/08/windows-exploitation-tricks-exploiting.html

  • • LSA 保护(RunAsPPL)你真的了解吗?https://itm4n.github.io/lsass-runasppl/

  • • 绕过 Userland 中的 LSA 保护 https://blog.scrt.ch/2021/04/22/bypassing-lsa-protection-in-userland/

技术交流

知识星球

致力于红蓝对抗,实战攻防,星球不定时更新内外网攻防渗透技巧,以及最新学习研究成果等。常态化更新最新安全动态。专题更新奇技淫巧小Tips及实战案例。

涉及方向包括Web渗透、免杀绕过、内网攻防、代码审计、应急响应、云安全。星球中已发布 200+ 安全资源,针对网络安全成员的普遍水平,并为星友提供了教程、工具、POC&EXP以及各种学习笔记等等。

交流群

关注公众号回复“加群”,添加Z2OBot 小K自动拉你加入Z2O安全攻防交流群分享更多好东西。

关注我们

关注福利:

回复“app" 获取  app渗透和app抓包教程

回复“渗透字典" 获取 针对一些字典重新划分处理,收集了几个密码管理字典生成器用来扩展更多字典的仓库。

回复“书籍" 获取 网络安全相关经典书籍电子版pdf

往期文章

我是如何摸鱼到红队的

命令执行漏洞[无]回显[不]出网利用技巧

MSSQL提权全总结

Powershell 免杀过 defender 火绒,附自动化工具

一篇文章带你学会容器逃逸

域渗透 | kerberos认证及过程中产生的攻击

通过DCERPC和ntlmssp获取Windows远程主机信息


文章来源: http://mp.weixin.qq.com/s?__biz=Mzg2ODYxMzY3OQ==&mid=2247491102&idx=1&sn=9c8da0b103162deceb8fc17cd6db2f5f&chksm=cea8f55ef9df7c480cc9607cdd4b9ecb95bb9dbd68c33e0085be6221712df544f1f939d641f4#rd
如有侵权请联系:admin#unsafe.sh