Windows 进程内存保护机制及其绕过方法简析
一、Windows 进程内存保护
1
Protected Processes (PP)
在 WIndows 系统中,任何具有Debug特权的进程都可以请求运行在机器上的其他进程的任意访问权限,执行内存任意读写、代码注入、线程挂起与恢复、查询进程信息等操作。诸如 Process Explorer、任务管理器等工具都需要请求这些权限来实现相应功能。
为了保护受版权保护的媒体内容,操作系统需要限制对播放媒体的进程内存的访问权限,因此微软在 Windows Vista / Windows Server 2008 上引入了 Protected Processes 机制,以限制其他进程针对受保护进程可以请求的访问权限。任意进程都可以创建受保护进程,但只有使用特殊媒体数字证书签过名的PE文件创建的进程才能受到操作系统的保护。
在 WIndows 系统中,Audio Device Graph 进程 (Audiodg.exe) 是一个受保护进程,负责解码受版权保护的音乐内容;Windows 错误报告进程 (Werfaultsecure.exe) 也可以在受保护的情况下运行,因为它需要访问受保护进程;System 进程也受到保护,因为部分驱动程序会生成并存储数据到其用户模式内存中,此外,System 进程的句柄表还包含系统上所有的内核句柄,这些数据的完整性都需要保障。
在内核层面,对受保护进程的保障是双重的:
-
大部分进程创建发生在内核模式中,可避免注入攻击;
-
受保护进程在 EPROCESS 结构中设置了特殊标志位,用于改变进程管理器中安全相关模块的行为,以拒绝通常能授予管理员的高危访问权限,可授予的访问权限只有 PROCESS_QUERY / SET_LIMITED_INFORMATION、PROCESS_TERMINATE 和 PROCESS_SUSPEND_RESUME。
虽然管理员用户可以通过加载内核模式的驱动来修改受保护进程的 EPROCESS 结构中的标志位来达到绕过保护机制的目的,但这种操作将被代码签名、受保护媒体路径 (Protected Media Path, PMP) 等安全策略视为恶意行为而被终止。
2
Protected Process Light (PPL)
Protected Process Light (PPL) 是 Protected Processes 机制的扩展,在 Windows 8.1 / Windows Server 2012 R2 中被正式引入。PPL 可以基于 PE 文件的签名者标记进程的受保护级别。不同的签名者拥有不同的可信级别,使得受保护的进程可拥有与其他受保护进程不同的保护级别,这一级别通常由文件数字证书中的增强型密钥使用 (EKU) 字段确定。高级别的进程可以请求到低级别的进程的所有访问权限;反之则不可,当低级别进程请求 PROCESS_TERMINATE、PROCESS_VM_WRITE、PROCESS_VM_READ 等访问权限时,即使该进程以管理员身份启动,且拥有 SeDebugPrivilege ,系统也将返回"访问拒绝"错误。同时,受保护进程在加载 DLL 时也会受到限制,低保护级别的进程只能加载由更高级签名者签名的 DLL,以避免受保护进程被强制加载恶意 DLL。
保护级别和签名者的关系如下表所示:
保护级别 | 保护类型 | 签名者 |
PS_PROTECTED_SYSTEM (0x72) | Protected (2) | WinSystem (7) |
PS_PROTECTED_WINTCB (0x62) |
Protected (2) |
WinTcb (6) |
PS_PROTECTED_WINTCB_LIGHT (0x61) |
Protected Light (1) |
WinTcb (6) |
PS_PROTECTED_WINDOWS (0x52) |
Protected (2) |
Windows (5) |
PS_PROTECTED_WINDOWS_LIGHT (0x51) |
Protected Light (1) |
Windows (5) |
PS_PROTECTED_LSA_LIGHT (0x41) |
Protected Light (1) |
Lsa (4) |
PS_PROTECTED_ANTIMALWARE_LIGHT (0x31) |
Protected Light (1) |
Antimalware (3) |
PS_PROTECTED_AUTHENTICODE (0x21) |
Protected (2) |
Authenticode (2) |
PS_PROTECTED_AUTHENTICODE_LIGHT (0x11) |
Protected Light (1) |
Authenticode (1) |
签名者及其级别如下表所示:
签名者名称 (PS_PROTECTED_SIGNER) |
级别 |
用于 |
PsProtectedSignerWinSystem | 7 | System / Minimal 及 Pico 进 |
PsProtectedSignerWinTcb |
6 | 关键 Windows 组件,会拒绝对 PROCESS_TERMINATE 权限的请求 |
PsProtectedSignerWindows |
5 |
处理敏感数据的重要 Windows 组件 |
PsProtectedSignerLsa |
4 | Lsass.exe (已配置 RunAsPPL 注册表条目的情况下) |
PsProtectedSignerAntimalware |
3 | Windows 内建及第三方提供的反恶意软件服务及进程,会拒绝对 PROCESS_TERMINATE 权限的请求 |
PsProtectedSignerCodeGen | 2 | NGEN (.NET 原生代码生成) |
PsProtectedSignerAuthenticode |
1 | 托管 DRM 内容或加载用户模式字体 |
PsProtectedSignerNone |
0 | 无效 (不受保护) |
Windows 中部分进程的保护级别如下图所示:
二、常见 PPL 机制绕过方法
1
Cached Signing Level 竞态条件
通过在 Windows 代码完整性 (Code Integrity, CI) 机制中利用 TOCTOU (Time-of-check to time-of-use) 竞态条件来绕过 Device Guard 策略和 PPL 签名级别限制,为任意未签名文件添加缓存的签名级别。该漏洞分配的 CVE 编号为 CVE-2017-11830,目前已被修复。
-
为提升性能,CI 会缓存签名信息到名为 $KERNEL.PURGE.ESBCACHE 的内核扩展属性中;
-
若在缓存条目设置完成后文件被修改,缓存会自动失效;
-
用户模式代码不能直接设置内核扩展属性,需要使用 NtSetCachedSigningLevel 系统调用来设置;
NTSTATUS NtSetCachedSigningLevel(
ULONG Flags, // 模式0 - 用于Ngen (Native Image Generator),需进程启用PPL;模式4 - 在签名文件上设置缓存,无需PPL
SE_SIGNING_LEVEL InputSigningLevel,
PHANDLE SourceFiles, // 待验证源文件列表:模式0 - 源签名文件列表;模式4 - 存储签名缓存的文件
ULONG SourceFileCount,
HANDLE TargetFile // 存储签名缓存的文件
);
-
可以在内核验证文件签名和写入扩展属性之间这段时间来修改文件;
-
在验证目标文件签名后,使用独占机会锁来临时阻止内核打开目录文件,通过选择一个已知的内核会检查的目录,可以得到一个定时信号,将目标文件修改为一个未签名的、不受信任的文件,然后释放机会锁,让内核完成缓存的验证和写入;
-
利用上述流程可将 DLL 加载到 PPL 进程中,以绕过 PPL 对普通进程的限制。
2
KnownDlls 伪造
2.1 利用 Windows Containers 伪造 KnownDlls
Windows 10 利用内核重定向技术提供了对原生 Docker 容器的支持,可实现对系统、注册表、网络栈及对象管理器的隔离。其中,KnownDlls 节对象缓存是对象管理器命名空间的一部分,受到系统保护,通常无法修改(WinTcb 保护级别的进程才能修改),因此系统认为 KnownDlls 是可信的,在加载这些 DLL 到 PPL 进程中时不会进行额外的检查。下图就是进程从 KnownDlls 中加载 DLL 的简化流程:
但在 NtCreateSymbolicLinkObject 中存在一个未文档化的标志,可为完整对象管理器目录命名空间创建全局符号链接(可穿透容器作用于宿主系统)。因此,攻击者在创建全局符号链接后,可以建立新的 KnownDlls 目录,并创建自定义符号链接对象,链接自定义的 DLL 到 PPL 进程将要加载到目标 DLL 上,实现任意 DLL 的加载。由于攻击者拥有该 KnownDlls 目录,因此系统不会验证 WinTcb 特权。
2.2 DefineDosDevice API
DefineDosDevice API 可以创建、修改或删除 MS-DOS 设备,该功能通过 RPC 机制由 WinTCB-Light 级的 CSRSS 进程执行实现。利用 TOCTOU 竞态条件,攻击者可以欺骗 CSRSS 在 KnownDlls 中创建新的条目,使 PPL 进程在无需验证的情况下加载任意 DLL。这种方法最早在 2018 年由 James Forshaw 公开,而后在 2021 年 4 月 Clément Labro 开源了基于该方法的项目 PPLDump。
2022 年 7 月微软发布了补丁移除了 PPL 对 KnownDlls 的支持,可解决 KnownDlls 伪造的问题。
3
脚本引擎组件对象模型 (COM) 劫持
部分脚本解释器 DLL 会自动加载注册表中指定的脚本,因此,攻击者可以尝试查找被 PPL 进程使用的 COM,修改对应注册表,实现对 COM 组件的劫持,使其加载脚本解释器 DLL 并执行脚本。对于 JScript 功能受限的问题,可以使用 DotNetToJScript 扩展 JScript,使其可以加载 .Net 类,实现高权限任意代码执行。
4
利用存在漏洞的程序
Windows 错误报告进程内存转储器 (WerFaultSecure) 为保护 PP 和 PPL 的机密性,会加密内存转储,而 Windows 8.1 构建的 WerFaultSecure 存在可创建未加密转储的问题。虽然这个问题已在 2014 年得到修复,但是在最新的 Windows 11 中这一存在问题的版本仍然可以 WinTCB 保护级别运行,实现对受保护进程的内存转储。
5
利用 COM IRundown::DoCallback() 注入进程
IRundown::DoCallback() 是 COM 中由于组件间调用的未文档化函数,通常只能进程内调用,因为调用该函数的前提是需要知道进程 Secret 和 COM 服务上下文地址等信息。为了能在外部调用该函数,攻击者需要通过内存转储的方法获取必要信息。首先使用系统中的原生 WerFaultSecure 转储 AppContainer 进程,使其得以初始化 COM。利用存在漏洞的 Windows 8.1 WerFaultSecure 转储完成 COM 初始化的 WerFaultSecure 进程内存,分析转储文件,以发现调用 IRundown::DoCallback 所需要的进程 Secret、上下文指针等信息。之后,即可连接 IRundown 接口,使用 DoCallback 调用 Get/SetProcessDefaultLayout 修改LdrpKnownDllDirectoryHandle 全局变量为提前伪造的 KnownDlls 目录的句柄值,再使用 DoCallback 调用 LoadLibrary 加载伪造 KnownDlls 下的自定义 DLL,实现对受保护进程的注入。
6
COM 代理类型混淆
.NET 运行时优化服务运行在 CodeGen-PPL 保护级别,可托管 COM 服务。通过修改 COM 代理配置可触发类型混淆,攻击者可以在不被信任的进程通过分布式组件对象模型 (DCOM) 中的代理机制向受保护进程传入特意构造的非目标类型指针变量,而该指针仍会被受保护进程按预设方式使用,因此可实现任意代码执行,修改 KnownDlls 句柄为伪造 KnownDlls 目录,实现对任意 DLL 的加载。之后,可以利用 CodeGen-Light 拥有的权限创建任意 DLL 的签名缓存条目,使这些 DLL 可以被加载到更高级别的受保护进程中去。下图就是一个简单的类型混淆示例:
基于该方法思路的开源 PoC 项目有 PPLmedic 。微软将在 2023 年 6 月对 KnownDlls 句柄问题提出缓解方案。
7
小结
前述的几种用户模式 PPL 绕过方法主要关注于签名缓存、KnownDlls、COM 组件,除此之外,还可以通过开发驱动程序,在内核模式修改受保护进程的 EPROCESS 结构并修改其中的数据,从而取消系统对进程的保护,如 PPLKiller 项目。
三、一种利用Windows CI TOCTOU的新绕过方法
1
Paging I/O TOCTOU
这种方法的主要思路是通过一些方法强行将签名校验与实际执行分隔开,从而可以利用 TOCTOU 的思路实现对受保护进程的注入,其主要流程如下图所示:
作者选取了 WinTCB-Light 级别的 services.exe 作为目标进程。同时选择了 EventAggregation.dll 作为目标 DLL,因为该 DLL 没有不包含页面 Hash,若存在页面 Hash,则 WIndows CI 还会校验 Hash 的有效性,例如 services.exe 文件就存在页面 Hash:
直接执行 services.exe 不会触发 Paging I/O,而需要利用 SMB 服务,将 EventAggregation.dll 替换为指向本地环回 SMB 的符号链接,即可看到页面调度操作:
因此,可以利用 SMB 服务在缺页异常前后分别为 services.exe 提供不同版本的 EventAggregation.dll,缺页异常前提供原版 EventAggregation.dll,用于通过 CI 的校验;而缺页异常后则提供在 DllMain 中插入了 Payload 的 EventAggregation.dll,用于注入代码到 services.exe 进程中。另外,为留出进行页面调度的时间,需要中断 services.exe 进程加载后续 DLL 的过程,利用机会锁锁住需要的 DLL 文件可以达到这一目的。缺页异常则可以通过清空进程作业集来触发。因为使用 EmptyWorkingSet 需要进程的 PROCESS_SET_QUOTA 权限,而此时无法获取到该权限,所以作者使用了 NtSetSystemInformation 来清空系统作业集和备用列表。
2
利用 Cloud Filter API 减少攻击噪声
目前的攻击方法存在较大的攻击噪声,因为提供 SMB 服务的 LanManServer 服务会在系统启动时读取配置,必须要重启系统才能改变其配置。因此,作者使用了 Cloud Filter API 来减少攻击噪声。Cloud Filter 在 WIndows 10 1709 中被引入,可以创建拥有"重解析"标签的空占位符文件,当收到读取请求时,minifilter 驱动会检查"重解析"标签,并调用设置的回调函数来请求数据,开发者可以在填充回调函数中自定义提供给请求方的数据。利用 Cloud Filter API 的特性即可实现即时的映射文件更新。
3
完整攻击链
-
使用 Cloud Filter 创建一个空的占位符文件,并设置一个可向占位符文件写入数据的回调函数;
-
通过本地环回 SMB 及符号链接将 EventAggregation.dll 重定向到占位符文件
-
为 devobj.dll 添加机会锁,用于中断进程初始化
-
运行目标 PPL
-
目标 PPL 尝试加载 EventAggregation.dll
-
CloudFilter 回调函数被触发,填充原生 EventAggregation.dll 文件到占位符文件中,用于通过代码完整性验证
-
通过清空进程工作集和备用列表将所有数据移出内存页面
-
释放机会锁
-
PPL 恢复运行,触发缺页异常,请求调入页面,通过本地 SMB 服务中转数据到占位符文件
-
Cloud Filter 回调函数被触发,填充已修改的 EventAggregation.dll 副本到占位符文件中
-
注入 services.exe 进程的 PIC Payload 会以 WinTcb-Light 保护级别被执行,使其可以转储任意进程的内存
目前,作者已开源 PoC 项目 PPLFault,下图即为使用该工具转储 LSASS 进程内存的过程:
同时,作者还基于 ANGRYORCHARD,将该方法迁移到 CSRSS 进程上,利用 NtUserHardErrorControl 存在的漏洞,将 KTHREAD.PreviousMode 由 UserMode (1) 降为 KernelMode (0),可实现内核级的任意内存读写,例如读写用户模式下通常不可访问的 DevicePhysicalMemory。
4
缓解方法
-
导致出现 TOCTOU 竞态条件问题的根本原因是签名验证与分页的分离,WIndows 可以通过添加对页面 Hash 的验证来缓解这种攻击。
-
可以通过破坏利用链来缓解这种攻击:
-
启用 NoRemoteImages 进程缓解策略 (SetProcessMitigationPolicy API 或 Set-ProcessMitigation cmdlet),阻止进程从 SMB、WebDAV 等网络位置加载 DLL;
-
开发驱动,通过进程创建回调在进程生命周期的早期就启用 NoRemoteImages 策略。
参考资料
-
《Windows Internal-7ed Part 1》
-
ZwQueryInformationProcess function - Win32 apps https://learn.microsoft.com/en-us/windows/win32/procthread/zwqueryinformationprocess
-
PPLdump Is Dead. Long Live PPLdump! https://www.blackhat.com/asia-23/briefings/schedule/#ppldump-is-dead-long-live-ppldump-31052
-
CiSetFileCache TOCTOU Security Feature Bypass https://bugs.chromium.org/p/project-zero/issues/detail?id=1332
-
Injecting Code into Windows Protected Processes using COM - Part 1 https://googleprojectzero.blogspot.com/2018/10/injecting-code-into-windows-protected.html
-
Recon 2018 - Unknown Known DLLs and other code integrity trust violations http://publications.alex-ionescu.com/Recon/Recon 2018 - Unknown Known DLLs and other code integrity trust violations.pdf
-
Breaking Protected Processes http://www.nosuchcon.org/talks/2014/D3_05_Alex_ionescu_Breaking_protected_processes.pdf
-
Injecting Code into Windows Protected Processes using COM - Part 2 https://googleprojectzero.blogspot.com/2018/11/injecting-code-into-windows-protected.html
-
PPLDump https://github.com/itm4n/PPLdump
-
PPLmedic https://github.com/itm4n/PPLmedic
-
PPLKiller https://github.com/Mattiwatti/PPLKiller
-
PPLFault https://github.com/gabriellandau/PPLFault
-
ANGRYORCHARD https://github.com/benheise/ANGRYORCHARD
— END —
原文始发于微信公众号(Asimov攻防实验室):Windows 进程保护机制及其最新绕过方法
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论