AIxCCNginx的CHERI安全分析

admin 2026-02-03 00:41:41 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文分析了CHERI架构对NginxAIxCC漏洞的缓解效果。研究表明CHERI通过强制崩溃缓解所有空间安全漏洞,利用能力撤销机制阻止基于重分配的UAF利用,有效防御RCE。文中还发现两个新漏洞,指出CHERI虽能阻止代码执行,但对纯DoS攻击仍有局限。 综合评分: 90 文章分类: 漏洞分析,二进制安全,WEB安全,CTF,漏洞POC


cover_image

AIxCC Nginx 的 CHERI 安全分析

RoundofThree RoundofThree

securitainment

2026年2月2日 17:50 中国香港

本文所述的全部工作,包括将 AIxCC Nginx 移植到 CHERI 以及对 CPV 的分析,均由我独立完成,但我的移植基于现有的 Nginx CHERI port。感谢 Robert Watson 教授对本文内容进行详尽审阅。

| 链接 | 作者 | | — | — | | https://roundofthree.github.io/posts/nginx-aixcc-cheri/ | RoundofThree |

这篇文章我很早就写好了,但一直没有机会发布。有人提醒我,我决定终于把它公开。这与我其他博客有些不同:更学术,且聚焦 CHERI。关于 temporal safety bug 的分析在 我的另一篇博客 中也有,区别在于本文聚焦 CHERI (一种硬件内存安全缓解机制,更多细节见 https://cheri-alliance.org/))。

(本段仅介绍 AIxCC) DARPA Artificial Intelligence Cyber Challenge (AIxCC) 是一项竞赛,要求参赛者创建 AI 驱动的自动化系统来发现、触发并修复漏洞。竞赛包含许多 Challenge Projects_,即被修改以注入漏洞的真实软件。被注入的漏洞Challenge Project Vulnerabilities (CPVs)_,多源于过去已披露并修复的漏洞,以及常见开发错误,如固定大小缓冲区、缺失长度检查、错误的链表遍历等。

在我看来,AIxCC 项目的目标是用 AI “强化” fuzzing,并且在 fuzzing 之外,还能识别问题根源并自动修复软件代码。那 CHERI 和 fuzzing 有什么关系?最多,CHERI 可以提升 AddressSanitizer (ASan) 的检测能力,可能也能提升经过 sanitization 的二进制性能 (前提是 CHERI 启用的微架构足够快)。理论上,CHERI 提供的空间安全的确定性与严格性,可能让 CHERI+ASan 检测到更多类型的缓冲区溢出,但这取决于现实软件中是否存在相关的代码惯用法。

澄清一下,本文不是关于 fuzzing 的 (也许我会在其他文章/工作中展开),而是针对这些具体漏洞在 CHERI 视角下的安全分析。问题是:CHERI 能在多大程度上直接缓解这些“典型”漏洞?具体而言,触发这些漏洞是否会立即导致确定性的崩溃,将潜在的任意代码执行强制降级为 “fail-stop” (立刻崩溃),从而明确阻止进一步的攻陷?

挑战项目之一是 Nginx。既然已有 Nginx 到 CHERI purecap 的移植,用于在 CheriBSD 的 CheriABI 进程环境中运行 5,且只需少量源码修改,那么在 CHERI 背景下把注入了漏洞的 AIxCC 版 Nginx 移植过去工作量相对不大。

该挑战被设计并测试在 Linux 主机上运行,但我们改在 CheriBSD 上运行 Nginx,因为 CHERI 的 Linux port 仍在开发中,尚不支持诸如用户态堆的 temporal safety 等特性 (我不确定它目前进展如何)。Nginx 在 FreeBSD 上得到良好支持并广泛使用,但部分功能实现是平台相关的。Nginx 中有一些平台相关代码会影响 CPV 的可复现性,因此我做了尽力而为的调整以支持 CheriBSD/FreeBSD,除非另有说明。我使用的硬件是 Arm Morello SoC 6,它包含一个带 CHERI 增强的 Neoverse N1 内核原型。

TL;DR

  • CHERI 缓解了所有空间安全相关的 CPV (理论上也包含 CPV12,但该漏洞在 FreeBSD 上不可触达)。
  • CPV15 是 intra-object overflow,可被 CHERI sub-object bounds 捕获,而 ASan 只能间接捕获此类问题。(诚然,intra-object overflow 看起来很罕见,Apple Security Research 也指出了这一点: https://security.apple.com/blog/memory-integrity-enforcement/)。
  • 堆 UAF 在 CHERI 下不会立即触发 fault,但如果攻击者依赖 UAF 对象被重新分配来继续攻击,caprevoke 会确保悬空指针失效,对悬空指针的解引用会触发 fault,从而阻止攻击。不过,确实可能存在无需重新分配就能利用 UAF 的方式,这取决于漏洞本身。因此,CHERI 对这些漏洞的缓解需要逐案分析。我的另一篇博客分析了这些 CPV 的可利用性并给出一个 RCE 利用。在启用 CHERI 的 use-after-reallocation 保护后,这个利用将不可行。
  • double free 会被 mrs 检测并缓解。mrs 是系统分配器的一个 shim,负责隔离 (quarantine) 并与内核交互进行能力撤销。基于这项工作,我们已经将 mrs 配置为在 CheriBSD 25.03 之后遇到 double free 时直接 abort,而不是掩盖它,以便调试。
  • 此外,在我开始 fuzzing 之前运行带 CHERI 的 Nginx 时,cherified Nginx 因两个缓冲区溢出 bug 崩溃了。这些 bug 是 AIxCC Nginx CPV 引入的,但并未列在官方 AIxCC Nginx challenge 仓库中;我目前还不清楚它们是否能被利用达到任意控制流 (更新: 至少有一个可以,我可能会在另一篇博客中作为趣味练习解释)。

下表总结了我们的发现 (包含两个额外漏洞,也叫 bonus CPVs 或 BCPVs):

| |Sanitizer 崩溃 | CHERI 触发 trap | CHERI 缓解 | 备注 | | | | — | — | — | — | — | — | | CPV1 | heap-buffer-overflow | yes | yes | | | | CPV2 | heap-buffer-overflow | yes | yes | | | | CPV3 | heap-buffer-overflow | yes | yes | | | | CPV4 | heap-buffer-overflow | yes | yes | | | | CPV5 | SEGV | yes | yes | 影响是 NULL 指针解引用,通常不太可利用。 | | | CPV8 | heap-buffer-overflow | yes | yes | | | | CPV9 | heap-use-after-free | no | yes | 如果不将该对象与其他对象别名,影响最多是 NULL 指针解引用。 | | | CPV10 | double-free | no/yes | yes | double free 可配置为触发 trap。 | | | CPV11 | heap-use-after-free | no | yes | UAF 在 revocation 之前可能泄露特权信息。但 mrs 阻止通过对象别名泄露堆指针值。 | | | CPV12 | heap-buffer-overflow | N/A | N/A | 理论上可缓解,但该特性未移植到 CheriBSD。 | | | CPV13 | SEGV | yes | yes | 影响是 NULL 指针解引用,通常不太可利用。 | | | CPV14 | global-buffer-overflow | yes | yes | | | | CPV15 | SEGV | yes | yes | intra-object overflow。子对象边界也可捕获对象数据破坏。 | | | CPV17 | heap-use-after-free | no/yes | yes | UAF 可导致内存池 block 的 double free,被 mrs 捕获。 | | | BCPV1 | heap-buffer-overflow | yes | yes | 线性缓冲区溢出。在 CPV10 的 double free 之后出现 (ASAN 已停止执行),但也可由其他输入触达。 | | | BCPV2 | heap-buffer-overflow | yes | yes | null byte poisoning。 | |

*对于 CHERI 不会 trap 的漏洞,如果其在不使用 CHERI 时的影响 (信息泄露或远程代码执行) 被消除或降低,我们就认为 CHERI 缓解了其利用。

设置 Nginx

第一步是对 AIxCC 版本的 Nginx 进行 cherify_,这里的cherification_ 指把 C/C++ 代码 移植到 CHERI。正如我前面所说,已有 Nginx 的 CHERI purecap port,我本希望能直接 cherry-pick 相关提交到 AIxCC Nginx。然而 challenge project 的提交历史已被清理,提交信息也毫无帮助 (刻意为之)。好在 cherification 的改动不大,因此我决定手动完成移植,所耗时间不多。我的仓库在这里: https://github.com/RoundofThree/challenge-004-nginx-source。你可以跳过本节,但如果感兴趣,这些是主要的 cherification 修改:

  • 配置 Nginx 的指针大小 (Arm Morello 的 capability 为 128-bit)
  • 替换部分 uintptr_t(指针类型) 和 ptraddr_t(可容纳指针地址的整数类型) 的使用
  • 修复部分指针算术,例如向上对齐指针,这要求有正确的 provenance,使编译器能正确派生 capability
  • 收紧 Nginx 自定义分配器的内存分配边界,否则空间安全的粒度会退化为内存映射级别而非单个分配
  • 在 purecap 下,扩展满的 ngx_array_t使用 ngx_array_push时需要总是分配新的底层容器,以避免边界违规 (因为边界严格且 capability 遵循单调性原则)
  • 将 ngx_auth_log_tpool 的分配对齐增强到指针大小,因为 capability 必须 16 字节对齐存储。

别忘了我们运行在 CheriBSD 上,这是 FreeBSD 的 CHERI 扩展版本,这意味着竞赛中添加的某些含漏洞功能也必须兼容 BSD。具体包括:

  • 至少不要让 host_specs 功能在 FreeBSD 上崩溃。AIxCC Nginx 中该功能实现读取 /proc,而 FreeBSD 默认不挂载 /proc。此时 fclose(NULL)在 FreeBSD 上会导致 SEGFAULT,而在近期 Linux 上不会。我不会修复 host_specs 的功能,因为这与漏洞分析无关。
  • 连接历史功能是在 ngx_epoll_process_events中记录连接实现的,但 FreeBSD 使用 ngx_kqueue_module而不是 ngx_epoll_module。我不得不移植该功能,并祈祷没有引入新的漏洞。
  • 从资源中发送一段数据并可选择反转数据流的功能,实现在 ngx_linux_sendfile_chain.c中,如其名所示是 Linux 专用……而 FreeBSD 并不共用这段代码。我没有将该功能移植到 FreeBSD,因此 CPV12 在 FreeBSD 上不可复现。若我贸然将 reverse sendfile 功能添加到 FreeBSD,可能引入新的 bug。至少,处理本应触发 CPV12 的 HTTP 请求不会导致崩溃。

如前所述,本工作并不以 fuzz Nginx 为目的。AIxCC 仓库发布了 CPV 的触发请求与补丁。一些 CPV 需要在 Nginx 配置中启用特定设置,例如导出 host_specs或 last_ip。我们可以使用一个文件复现所有 CPV (复用 AIxCC harness 中硬编码的文件),也可以为每个 CPV 编写新的配置文件。

我写了一个简单且朴素的 Python wrapper,用指定配置文件启动 Nginx、发送触发请求、再停止 Nginx 服务。对于每个 CPV,我们需要一个配置文件和一个触发请求,我将它们分组到不同文件夹作为 wrapper 的输入。我们将 coredump 位置指定为 /tmp/cores/。由内存破坏引发的程序崩溃,无论是 CHERI SIGPROT 还是 ASan abort,都会终止一个 worker 进程。最后我们通过终止 master 和 worker 进程来优雅停止 Nginx 服务器。

例如,为 Nginx 注入 CPV3 的触发请求:

$ python3 test.py cp3 request.txt
NGINX started with config file: /usr/home/zyj20/challenge-004-nginx-source/cp3/test.conf
Response from server:

NGINX stopped

配置文件 cp3/test.conf为:

working_directory  /tmp/cores/;
worker_rlimit_core 500M;

trace on;

events {
}

http {

    server {
        listen       127.0.0.1:8080;
        server_name  localhost;

        location / {
            return 200;
        }
    }
}

错误日志显示 Nginx worker 进程因 CHERI SIGPROT (signal 34) 退出:

$ tail /usr/local/nginx/logs/error.log
2025/01/30 23:38:53 [alert] 70688#0: worker process 70689 exited on signal 34 (core dumped)
2025/01/30 23:38:53 [notice] 70690#0: signal process started

在 /tmp/cores中快速查看 coredump,可见 CPV3 的堆缓冲区溢出触发了 CHERI bounds fault (即堆缓冲区越界写被转换为 CHERI SIGPROT 崩溃)。

$ gdb objs/nginx /tmp/cores/nginx.70689.core
GNU gdb (GDB) 14.1 [GDB v14.1.d20240612 for FreeBSD]
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "aarch64-unknown-freebsd15.0".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
&nbsp; &nbsp; <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
GEF for freebsd ready, type `gef' to start, `gef config' to configure
89 commands loaded and 5 functions added for GDB 14.1 [GDB v14.1.d20240612 for FreeBSD] in 0.00ms using Python engine 3.9
[+] 15 extra commands added in 0.05 seconds
Reading symbols from objs/nginx...
[New LWP 100214]
Core was generated by `nginx: worker process'.
Program terminated with signal SIGPROT, CHERI protection violation.
Capability bounds fault.

顺便说一句,我在使用我写的 GEF to Morello port,使调试输出更直观。我还在 另一篇博客 中简要介绍了该工具的功能。

CheriABI 中的 CPV 分析

CPV6、CPV7 和 CPV16 在写作时尚未发布,因此我们忽略它们。作为参考,下表展示了已发布的 14 个 CPV 及其 bug 类型 (其实就是 ASan 的崩溃类型):

| |Sanitizer 崩溃 | | | — | — | | CPV1 | heap-buffer-overflow | | CPV2 | heap-buffer-overflow | | CPV3 | heap-buffer-overflow | | CPV4 | heap-buffer-overflow | | CPV5 | SEGV | | CPV8 | heap-buffer-overflow | | CPV9 | heap-use-after-free | | CPV10 | double-free | | CPV11 | heap-use-after-free | | CPV12 | heap-buffer-overflow | | CPV13 | SEGV | | CPV14 | global-buffer-overflow | | CPV15 | SEGV | | CPV17 | heap-use-after-free |

总体而言 (但并非总是如此),触发空间安全相关的 bug 会导致 CHERI 崩溃,而触发 temporal safety 相关的 bug 可能不会立刻崩溃。

在内存安全语境下,未立即 SIGPROT 的 bug 实际上可能无法利用。崩溃可能被延后并在利用流程的后续阶段表现出来 (例如对于 UAF,使用 CheriBSD 的 use-after-reallocation 保护与隔离区时,崩溃发生在攻击者试图在底层内存被重新分配之后使用旧的悬空指针之时,这是利用中非常常见的技术)。CHERI 的目标是缓解漏洞,而非作为寻找 bug 的工具,其目标并不是立即同步 trap。

大多数 CPV 是越界访问,即空间安全违例,因此我们预期它们会被 CHERI 缓解,触发时通常导致 CHERI 崩溃 (除非是需要压缩边界的大缓冲区,可能出现不精确边界)。理论上,像 UAF 这样的 temporal safety 违例在释放后、重新分配之前使用,不会立刻导致 CHERI SIGPROT 崩溃,但其可利用性是否被缓解仍需逐案分析。

关于 CHERI 堆 temporal memory safety 的说明

在此我要澄清,我在测试的 CheriBSD 内核中开启了 caprevoke。caprevoke (capability revocation) 是 CheriBSD 的一项特性,通过能力撤销在用户态实现非概率性的 C/C++ 堆 temporal safety。出于性能考虑,CheriBSD 会隔离已释放内存,直到撤销所有仍然存在的指针变得高效为止,这必须在内存从隔离区释放用于重新分配之前完成。更多研究细节可见最初的 Cornucopia 论文 3以及后续的 Cornucopia Reloaded 4。就本文而言,只需知道 CheriBSD 默认分配器 jemalloc 由 mrs shim 包裹,负责隔离并与内核交互,通过 cheri_revoke撤销能力。manpage 如下:

&nbsp; &nbsp; The kernel exposes a CHERI capability revocation service, a mechanism to
&nbsp; &nbsp; revoke capabilities to regions of the address space. &nbsp;Requests for
&nbsp; &nbsp; revocation are made by setting bits in the shadow bitmap and invoking
&nbsp; &nbsp; the cheri_revoke() system call.

jemalloc、snmalloc 等分配器都被修改为使用 mrs,后者隔离分配并在后续批量撤销后再允许堆内存复用,此时可保证不存在来自之前分配的、可解引用的指针。

因此问题是:在开启 caprevoke (假设 jemalloc+mrs) 的情况下,CPV9、CPV11、CPV17 这些堆 UAF 是否会导致确定性 SIGPROT 崩溃?第二个问题是:即使它们不会立即导致 SIGPROT 崩溃,这些堆 UAF 仍可被利用吗?

最后,还有会导致 segfault 的 bug,以及一个 double free bug。在本工作使用的 CheriBSD 版本 (24.05) 中,double free 的影响不同于 UAF,它依赖分配器内部实现。例如 snmalloc 1对 double free 有强保护,double free 会因为 allocator freelist 设计而延迟导致崩溃。对 jemalloc+mrs 而言,double free 在 mrs 层被缓解,因为 mrs 的 quarantine 使用 validate_freed_pointer中的 bitmap 作为门控。基于这项工作,未来版本的 CheriBSD mrs 将在 double free 时立即 abort,便于使用 CHERI 进行软件调试。

staticinlinevoid&nbsp;*
validate_freed_pointer(void&nbsp;*ptr)
{

/*
     * Untagged check before malloc_underlying_allocation()
     * catches NULL and other invalid caps that may cause a rude
     * implementation of malloc_underlying_allocation() to crash.
     */
if&nbsp;(!cheri_gettag(ptr)) {
mrs_debug_printf("validate_freed_pointer: untagged capability addr&nbsp;%p\n",
         &nbsp; &nbsp;ptr);
return&nbsp;(NULL);
    }

void&nbsp;*underlying_allocation =&nbsp;REAL(malloc_underlying_allocation)(ptr);
if&nbsp;(underlying_allocation ==&nbsp;NULL) {
mrs_debug_printf("validate_freed_pointer: not allocated by underlying allocator\n");
return&nbsp;(NULL);
    }
/*mrs_debug_printf("freed underlying allocation %#p\n", underlying_allocation);*/

/*
     * Here we use the bitmap to synchronize and make sure that
     * our guarantee is upheld in multithreaded environments. &nbsp;We
     * paint the bitmap to signal to the kernel what needs to be
     * revoked, but we also gate the operation of bitmap painting,
     * so that we can only successfully paint the bitmap for some
     * freed allocation (and let that allocation pass onto the
     * quarantine list) if it is legitimately allocated on the
     * heap, not revoked, and not previously queued for
     * revocation, at the time of painting.
     *
     * Essentially at this point we don't want something to end up
     * on the quarantine list twice. &nbsp;If that were to happen, we
     * wouldn't be upholding the principle that prevents heap
     * aliasing.

// [...]

在我们最初验证的 jemalloc+mrs 版本中,double free 不会破坏堆分配器状态,但也不会立刻触发 trap。作为这项研究的一部分,我们修改了 mrs,让其改为触发程序 abort,以便更易识别 double free 问题。

空间安全漏洞

我们使用提供的触发器复现了所有 CPV,以验证它们确实被 CHERI 缓解。

CHERI 中的堆缓冲区溢出检测与缓解

通过输入 .internal_only/cpv1/blobs/中的 blob (其中 From字段的邮箱以连续两个点 ..开头),会触发 buffer underrun,我们可以从 coredump 的 SIGPROT bounds violation 观察到这一点。

Program terminated with signal SIGPROT, CHERI protection violation.
Capability bounds fault.
#0&nbsp; 0x00000000001b3cb0 in ?? ()
gef> &nbsp;disas /m 0xb3cb0
Dump of assembler code for function ngx_http_process_from:

// [...]

4093 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (*u == '.') { // CHERI crash CPV1 (bounds fault)
&nbsp; &nbsp;0x00000000000b3cb0 <+808>: &nbsp; ldrb &nbsp; &nbsp;w12, [c3, #-1]!
&nbsp; &nbsp;0x00000000000b3cb4 <+812>: &nbsp; mov &nbsp; &nbsp; w13,&nbsp;#0x2&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//&nbsp;#2
&nbsp; &nbsp;0x00000000000b3cb8 <+816>: &nbsp; cmp &nbsp; &nbsp; w12,&nbsp;#0x2e
&nbsp; &nbsp;0x00000000000b3cbc <+820>: &nbsp; b.ne &nbsp; &nbsp;0xb3cb0 <ngx_http_process_from+808> &nbsp;// b.any
&nbsp; &nbsp;0x00000000000b3cc0 <+824>: &nbsp; b &nbsp; &nbsp; &nbsp; 0xb3d4c <ngx_http_process_from+964>
&nbsp; &nbsp;0x00000000000b3cc4 <+828>: &nbsp; mov &nbsp; &nbsp; w13, wzr
&nbsp; &nbsp;0x00000000000b3cc8 <+832>: &nbsp; mov &nbsp; &nbsp; x0, xzr
&nbsp; &nbsp;0x00000000000b3ccc <+836>: &nbsp; sub &nbsp; &nbsp; c0, c0,&nbsp;#0x5
&nbsp; &nbsp;0x00000000000b3cd0 <+840>: &nbsp; cmp &nbsp; &nbsp; x0,&nbsp;#0x0
&nbsp; &nbsp;0x00000000000b3cd4 <+844>: &nbsp; cset &nbsp; &nbsp;w14, eq // eq = none

可以看到在 ldrb w12, [c3, #-1]!处,c3capability 寄存器被偏移 -1 再解引用,但 0x41493000 - 1不在 [0x41493000-0x41495000]的边界内,这解释了 SIGPROT bounds violation。

gef> &nbsp;p $c3
$1 = () 0x41493000 [rwRW,0x41493000-0x41495000]

我不会详写其他缓冲区溢出 CPV 的调试细节。为严谨起见,我测试并检查了所有空间安全 CPV 的 coredump:

| 与空间安全相关的 CPV | 触发 CHERI 崩溃? | 位置?* | | — | — | — | | CPV1 | yes | src/http/ngx_http_request.c:4093 | | CPV2 | yes | src/http/ngx_http_core_module.c:1994 | | CPV3 | yes | src/http/ngx_http_request.c:4217 | | CPV4 | yes | src/http/ngx_http_core_module.c:5295 | | CPV8 | yes | src/mail/ngx_mail_pop3_handler.c:337 | | CPV12 | no | N/A | | CPV14 | yes | src/http/modules/ngx_http_rewrite_module.c:178 |

*行号基于我 cherify 的 AIxCC Nginx 源码仓库。

喂给 CPV12 的漏洞 blob 不会触发 CHERI 崩溃,因为其预期漏洞位于 src/os/unix/ngx_linux_sendfile_chain.c的 ngx_sendfile_r函数,而 CheriBSD 上的 Nginx 不会走这条 Linux 专用代码路径。我或许可以把该漏洞也添加到 src/os/unix/ngx_freebsd_sendfile_chain.c……但 CPV12 的核心是硬编码 buffer size,我认为可以合理假设,一旦能触发该漏洞,进程会因 CHERI 崩溃。

除 CPV12 外,其他空间安全 bug 均被 CHERI 缓解。

CPV1: 额外漏洞?

在折腾 AIxCC 时,Nginx 因 CHERI bounds fault 崩溃。触发它的看起来无害的 blob 如下:

GET / HTTP/1.1
Host: localhost
Connection: close
From: [email protected]

结果表明在 src/http/ngx_http_request.c:4139,该输入触发了 NULL byte overflow。

4138 &nbsp; &nbsp; &nbsp; &nbsp;if (state == sw_tld) {
&nbsp; &nbsp;0x00000000000b3d6c <+996>: &nbsp; cmp &nbsp; &nbsp; w13,&nbsp;#0x4
&nbsp; &nbsp;0x00000000000b3d70 <+1000>: &nbsp;b.ne &nbsp; &nbsp;0xb3d80 <ngx_http_process_from+1016> &nbsp;// b.any
&nbsp; &nbsp;0x00000000000b3d74 <+1004>: &nbsp;mov &nbsp; &nbsp; x0, xzr

4139 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;*u = '\0'; // CHERI crash unintended CPV1 (bounds fault)
&nbsp; &nbsp;0x00000000000b3d78 <+1008>: &nbsp;strb &nbsp; &nbsp;wzr, [c3]
&nbsp; &nbsp;0x00000000000b3d7c <+1012>: &nbsp;b &nbsp; &nbsp; &nbsp; 0xb3da0 <ngx_http_process_from+1048>

u的分配大小为 from->len。如果执行未到达 return NGX_DECLINED,for 循环会执行 from->len次,则 u会被自增 from->len次。

staticngx_int_t
ngx_http_validate_from(ngx_str_t&nbsp;*from,&nbsp;ngx_pool_t&nbsp;*pool,&nbsp;ngx_uint_t&nbsp;alloc)
{
// [...]
if&nbsp;(alloc) {
&nbsp; &nbsp; &nbsp; &nbsp; u =&nbsp;ngx_palloc(pool, from->len);&nbsp;// allocated here

if&nbsp;(u ==&nbsp;NULL) {
return&nbsp;NGX_ERROR;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; u = from->data;
&nbsp; &nbsp; }

for&nbsp;(i =&nbsp;0; i < from->len; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; ch = f[i];

switch&nbsp;(state) {

case&nbsp;sw_begin:
if&nbsp;(isalnum(ch) || ch ==&nbsp;'-'&nbsp;|| ch ==&nbsp;'_') {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state = sw_username;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;elseif&nbsp;(ch ==&nbsp;'.') {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; state = sw_username_dot;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
return&nbsp;NGX_DECLINED;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; *u++ = ch;
break;
// [...]

然后 u会指向 base(u) + from->len,即越过已分配 buffer 1 字节,因此下列代码在写入字符串终止符 NULL byte 时造成 NULL byte poisoning。

if&nbsp;(state == sw_tld) {
&nbsp; &nbsp; &nbsp; &nbsp; *u =&nbsp;'\0';&nbsp;// CHERI crash unintended CPV1 (bounds fault)

if&nbsp;(alloc) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; from->data = u;
&nbsp; &nbsp; &nbsp; &nbsp; }
return&nbsp;NGX_OK;
&nbsp; &nbsp; }&nbsp;else&nbsp;{
return&nbsp;NGX_DECLINED;
&nbsp; &nbsp; }

这看起来也许不算大问题,但历史告诉我们 NULL byte poisoning 可能被利用成强大的 exploit 2。

CPV15: Sub-object bounds 作为 intra-object overflow 的消毒

我没有把 CPV15 列在空间安全 bug 表中,因为官方 AIxCC Nginx challenge 把它标记为 SEGV。然而我观察到 CPV15 实际上是 intra-object overflow,即越界读写越过对象字段并溢出到同一对象的其他字段。

在 Ubuntu 上把 CPV15 的触发 blob 喂给 AIxCC Nginx,worker 进程会因 segfault 崩溃。但将同样的 blob 喂给 CheriBSD 上的 AIxCC Nginx (CheriABI, purecap) 并不会崩溃。这让我认为可能是 CHERI 上指针更大导致结构体大小和对齐不同。我于是增加了 uid输入长度,成功触发崩溃。

# cp15/request.txt
GET / HTTP/1.1
Host: localhost
Cookie: uid=YWFhYWFhYWFhYWFhYWFhYWJiYmJiYmJiYmJiYmJiYmJjY2NjY2NjY2NjY2NjY2Nj;;

注意 base64decode(YWFhYWFhYWFhYWFhYWFhYWJiYmJiYmJiYmJiYmJiYmJjY2NjY2NjY2NjY2NjY2Nj)为 aaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbcccccccccccccccc。我再加一些字母并进行 base64 编码。

# cp15/request1.txt
GET / HTTP/1.1
Host: localhost
Cookie: uid=YWFhYWFhYWFhYWFhYWFhYWJiYmJiYmJiYmJiYmJiYmJjY2NjY2NjY2NjY2NjY2NjZGRkZGRkZGRkZGRkZGRkZGVlZWVlZWVlZWVlZWVlZWVmZmZmZmZmZmZmZmZmZmZm;;

从 Ubuntu 的 coredump 可见 segfault 来自解引用被破坏的指针。在 CheriBSD 中使用较长版本的触发 blob 时,崩溃是 ngx_decode_base64_internal函数内的 SIGPROT bounds violation。进一步观察发现我们溢出了 ngx_http_userid_ctx_t的 uid_got字段。较短版本的 blob 在 purecap 二进制中只能溢出到 ngx_http_userid_ctx_t->cookie.len,因为 capability 以 16 字节对齐存储,因此 ngx_http_userid_ctx_t->cookie.len和 ngx_http_userid_ctx_t->cookie.data之间存在 padding。

gef> &nbsp;p $c4
$1 = () 0x415bcd6e [rwRW,0x415bcd20-0x415bcd70]
gef> &nbsp;p *(ngx_http_userid_ctx_t *)0x415bcd20
$2 = {
&nbsp; uid_got = {0x61616161, 0x61616161, 0x61616161, 0x61616161},
&nbsp; uid_set = {0x62626262, 0x62626262, 0x62626262, 0x62626262},
&nbsp; cookie = {
&nbsp; &nbsp; len = 0x6363636363636363,
&nbsp; &nbsp; data = 0x6464646464646464 [wx,0x64646464646464-0x64646464646464] (invalid,sealed) <error: Cannot access memory at address 0x6464646464646464>
&nbsp; },
&nbsp; reset = {tag = 0, address = 0x6565656565656565, permissions = {[ Global User0 User2 CompartmentID BranchUnseal Unseal StoreLocalCap Execute Store ] otype = 0x4aca, range = [0x65656565656565 - 0x65656565656565)}}
}

*从 uid_got溢出到 ngx_http_userid_ctx_t对象的其他字段。

此时我意识到忘记启用 CHERI sub-object bounds,这会在不依赖指针被破坏的情况下捕获 intra-object overflow。Oops。于是我用 -Xclang -cheri-bounds=subobject-safe重新编译并测试较短版本的触发 blob。

[New LWP 100215]
Core was generated by `nginx: worker process'.
Program terminated with signal SIGPROT, CHERI protection violation.
Capability bounds fault.

1329 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;*d++ = (u_char) (basis[s[1]] << 4 | basis[s[2]] >> 2);
&nbsp; &nbsp;0x0000000000072008 <+144>: &nbsp; ldrb &nbsp; &nbsp;w9, [c1,&nbsp;#2]
&nbsp; &nbsp;0x000000000007200c <+148>: &nbsp; ldrb &nbsp; &nbsp;w10, [c1,&nbsp;#1]
&nbsp; &nbsp;0x0000000000072010 <+152>: &nbsp; ldrb &nbsp; &nbsp;w9, [c2, x9]
&nbsp; &nbsp;0x0000000000072014 <+156>: &nbsp; ldrb &nbsp; &nbsp;w10, [c2, x10]
&nbsp; &nbsp;0x0000000000072018 <+160>: &nbsp; lsr &nbsp; &nbsp; w9, w9,&nbsp;#2
&nbsp; &nbsp;0x000000000007201c <+164>: &nbsp; orr &nbsp; &nbsp; w9, w9, w10, lsl&nbsp;#4
&nbsp; &nbsp;0x0000000000072020 <+168>: &nbsp; strb &nbsp; &nbsp;w9, [c4,&nbsp;#1] <==
gef> &nbsp;p $c4
$1 = () 0x415bcd2f [rwRW,0x415bcd20-0x415bcd30]
gef> &nbsp;p *(ngx_http_userid_ctx_t *)0x415bcd20
$2 = {
&nbsp; uid_got = {0x61616161, 0x61616161, 0x61616161, 0x61616161},
&nbsp; uid_set = {0x0, 0x0, 0x0, 0x0},
&nbsp; cookie = {
&nbsp; &nbsp; len = 0x40,
&nbsp; &nbsp; data = 0x4156e82b [rwRW,0x4156e800-0x4156ec00] "YWFhYWFhYWFhYWFhYWFhYWJiYmJiYmJiYmJiYmJiYmJjY2NjY2NjY2NjY2NjY2Nj;;"
&nbsp; },
&nbsp; reset = 0x0
}

*崩溃时的受害对象 ngx_http_userid_ctx_t

可以看到启用 sub-object bounds 后,进程在对象字段溢出发生时立即崩溃。

默认情况下,ASan 不会捕获 intra-object overflow,因为在对象字段间插入 redzone padding 可能带来问题 (这也是该特性仍处于实验状态的原因?)。此外,当前实验性的 intra-object overflow 仅支持 C++ 应用 (而 Nginx 是 C)。因此并非所有触发该 bug 的 fuzzing 输入都会触发 sanitizer fault,只有足够长、能破坏关键数据结构 (此处为指针) 的溢出才会崩溃。CHERI sub-object bounds 的优势在于,无论该 bug 是否能破坏程序内部状态,这类溢出都可被检测。

从利用角度看,即便二进制未开启 sub-object bounds,依靠破坏 ngx_http_userid_ctx_t->cookie.data指针的攻击在 CHERI 上也不可行,因为 CHERI capability 有指针完整性保护。直白点说,用数据覆盖一个 capability 会清除其 tag,使其失效。

Temporal safety 漏洞

CPV9: NULL 指针解引用还是更多?

CPV9 的堆 UAF 在官方发布的漏洞 blob 触发时会崩溃,但不是因为 CHERI SIGPROT。CPV9 因 UAF 导致 NULL 指针解引用而崩溃,在 CheriBSD 和 Ubuntu 都是如此。它是否可利用?能否构造不会导致 NULL 解引用的 HTTP 请求?

漏洞出在 ngx_black_list_remove中删除黑名单条目时。一个黑名单对象包含一个指向字符串对象 ngx_str_t的 IP指针,以及指向前后黑名单条目的 prev和 next指针,组成一个双向链表。

typedefstruct&nbsp;ngx_black_list_s {
ngx_str_t&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;*IP;
ngx_black_list_t&nbsp; *next;
ngx_black_list_t&nbsp; *prev;
}ngx_black_list_t;

*ngx_black_list_s结构。

在 ngx_black_list_remove中,链表会遍历直到找到 IP匹配的条目。现在考虑链表为空、reader为 NULL 的场景:由于在 for 循环中执行 reader = reader->next会访问 reader的 next字段,因此会发生 NULL 解引用。

这还不是全部。再考虑 remove_ip匹配链表头的场景。第一个 if 语句的条件会满足,条目会在ngx_destroy_black_list_link中被清理并释放。然而被删除节点的nextprev字段没有被清除,链表头也没有更新。因此后续使用黑名单时会始终从链表头开始遍历,而该链表头是一个悬空指针,指向一个IP指针为 NULL 的节点。这个悬空指针会在后续的ngx_black_list_insertngx_black_list_removengx_is_ip_banned调用中被使用,在所有这些情况下,如果IP为 NULL,worker 进程都会因为 NULL 指针解引用而崩溃。我找不到利用该悬空指针触发写入的方法。

如果我们能触发对该悬空指针指向内存的重新分配,就可能写入 IP指针并避免崩溃。注意黑名单节点通过 ngx_alloc而非 ngx_palloc分配,因此是走系统分配器。这种攻击策略在 CHERI 上不可行,因为系统分配器 jemalloc+mrs 提供了 use-after-reallocation 保护。简单来说,在底层分配被分配给新对象之前,悬空指针就会失效,从而阻止这种 object aliasing策略。我想在非 CHERI 环境写个 PoC 来验证 (以后可能写博文,暂无承诺)。

ngx_int_t
ngx_black_list_remove(ngx_black_list_t&nbsp;**black_list,&nbsp;u_char&nbsp;remove_ip[])
{
ngx_black_list_t&nbsp;*reader;

&nbsp; &nbsp; reader = *black_list;

if&nbsp;(reader && !ngx_strcmp(remove_ip, reader->IP->data)) {&nbsp;// CHERI crash CPV9 (due to NULL pointer dereference, a product of UAF)
ngx_destroy_black_list_link(reader);
return&nbsp;NGX_OK;
&nbsp; &nbsp; }

for&nbsp;(reader = reader->next; reader && reader->next; reader = reader->next) {
if&nbsp;(!ngx_strcmp(remove_ip, reader->IP->data)) {
ngx_double_link_remove(reader);
ngx_destroy_black_list_link(reader);
return&nbsp;NGX_OK;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

return&nbsp;NGX_ERROR;
}

*ngx_black_list_remove函数。

#definengx_destroy_black_list_link(x) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;\
ngx_memzero((x)->IP->data, (x)->IP->len); &nbsp; \
ngx_free((x)->IP->data); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;\
&nbsp; &nbsp; (x)->IP->data =&nbsp;NULL; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \
ngx_memzero((x)->IP, sizeof(ngx_str_t)); &nbsp; &nbsp;\
ngx_free((x)->IP); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;\
&nbsp; &nbsp; (x)->IP =&nbsp;NULL; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \
ngx_memzero((x), sizeof(ngx_black_list_t)); \
ngx_free((x)); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;\
&nbsp; &nbsp; (x) =&nbsp;NULL;

*清理节点的 ngx_destroy_black_list_link宏。

该 bug 的要点是:对悬空指针所指对象的进一步使用会导致 NULL 解引用。如果能让另一对象与该悬空指针指向的内存重叠 (object aliasing),在没有 CHERI 的情况下可能更有用 (不过事实证明并不行,详见我的另一篇 博客 的分析)。但在 mrs 下这似乎不可行。

CPV11: 堆 UAF 读取导致主机规格泄露

CPV11 不会让 Nginx 进程崩溃,即使没有远程管理员权限也会打印主机规格,因为 UAF 缓冲区包含主机规格。对象 cycle->host_specs在 ngx_init_cycle中分配,其字段 host_cpuhost_mem和 host_os随后立即初始化:

// [...]
&nbsp; &nbsp; cycle->host_specs->host_cpu = ngx_alloc(sizeof(ngx_str_t), log);
if&nbsp;(cycle->host_specs->host_cpu ==&nbsp;NULL) {
ngx_destroy_pool(pool);
returnNULL;
&nbsp; &nbsp; }
&nbsp; &nbsp; cycle->host_specs->host_cpu->data = (u_char*)"Unknown CPU\n";

ngx_memzero(line, NGX_MAX_HOST_SPECS_LINE);
&nbsp; &nbsp; fp = fopen("/proc/cpuinfo",&nbsp;"r");
if&nbsp;(fp !=&nbsp;NULL) {
&nbsp; &nbsp; &nbsp; &nbsp; temp_char =&nbsp;NULL;
while&nbsp;(fgets(line,&nbsp;sizeof(line), fp) !=&nbsp;NULL) {
if&nbsp;(ngx_strncmp(line,&nbsp;"model name",&nbsp;10) ==&nbsp;0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; temp_char =&nbsp;strchr(line,&nbsp;':');
if&nbsp;(temp_char !=&nbsp;NULL) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; temp_char +=&nbsp;2;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle->host_specs->host_cpu->data =&nbsp;ngx_alloc(sizeof(line),&nbsp;log);
if&nbsp;(cycle->host_specs->host_cpu->data ==&nbsp;NULL) {
break;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
ngx_memzero(cycle->host_specs->host_cpu->data,&nbsp;sizeof(line));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle->host_specs->host_cpu->len = \
ngx_sprintf(cycle->host_specs->host_cpu->data,&nbsp;"%s", temp_char) - \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle->host_specs->host_cpu->data;
break;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
fclose(fp);
&nbsp; &nbsp; }
// [...]

问题在于,紧接着代码检查是否配置了 remote_admin,如果没有,就释放 cycle->host_specs。那这个对象指向的所有内存分配怎么办?

&nbsp; &nbsp; ccf = (ngx_core_conf_t&nbsp;*) ngx_get_conf(cycle->conf_ctx, ngx_core_module);

if&nbsp;(!ccf->remote_admin) {
ngx_free(cycle->host_specs);
&nbsp; &nbsp; }

简单 grep 一下可知,cycle->host_specs在 ngx_http_get_host_specs中被使用 (另一个使用处在 ngx_master_process_exit,那里会销毁 cycle->host_specs)。因此即使没有启用 remote_admin,也可以在 ngx_http_get_host_specs中解引用悬空指针来打印主机规格。即便启用 mrs revocation 也可行,因为该悬空指针在隔离区中尚未被撤销 (很可能是这种情况)。

staticngx_int_tngx_http_get_host_specs(ngx_http_request_t&nbsp;*r,
ngx_http_variable_value_t&nbsp;*v,&nbsp;uintptr_t&nbsp;data)
{
u_char&nbsp;*temp;

&nbsp; &nbsp; v->data =&nbsp;ngx_pnalloc(r->pool, NGX_MAX_HOST_SPECS_LINE *&nbsp;3);
if&nbsp;(v->data ==&nbsp;NULL) {
return&nbsp;NGX_HTTP_INTERNAL_SERVER_ERROR;
&nbsp; &nbsp; }
ngx_memzero(v->data, NGX_MAX_HOST_SPECS_LINE *&nbsp;3);

&nbsp; &nbsp; temp = v->data;
&nbsp; &nbsp; v->data =&nbsp;ngx_sprintf(v->data,&nbsp;"%s", r->cycle->host_specs->host_cpu->data);&nbsp;// NO CHERI crash CPV11 (UAF)
&nbsp; &nbsp; v->data =&nbsp;ngx_sprintf(v->data,&nbsp;"%s", r->cycle->host_specs->host_mem->data);
&nbsp; &nbsp; v->data =&nbsp;ngx_sprintf(v->data,&nbsp;"%s", r->cycle->host_specs->host_os->data);
&nbsp; &nbsp; v->len = v->data - temp;
&nbsp; &nbsp; v->data = temp;

return&nbsp;NGX_OK;
}

如果关闭 mrs,或许可以将其作为读取原语。但不确定有多大价值,因为泄露的指针值在 CHERI 上由于 tag 机制无法重新注入使用。

还有一个细微问题不影响 Linux 发行版,却影响 FreeBSD/CheriBSD:把 ngx_str的 data字段设置为字面量字符串,如 cycle->host_specs->host_cpu->data = (u_char*)"Unknown CPU\n"。随后在 ngx_master_process_exit中该字面量被释放。这在 Linux 上通常不是问题,因为如果挂载了 /procdata字段会用 ngx_alloc动态分配。总之对攻击者而言意义不大,因为 ngx_master_process_exit最终会调用 exit(0)

staticvoid
ngx_master_process_exit(ngx_cycle_t&nbsp;*cycle)
{
ngx_uint_t&nbsp; i;

if&nbsp;(cycle->host_specs) {
if&nbsp;(cycle->host_specs->host_cpu) {
ngx_free(cycle->host_specs->host_cpu->data);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle->host_specs->host_cpu->data =&nbsp;NULL;
ngx_free(cycle->host_specs->host_cpu);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle->host_specs->host_cpu =&nbsp;NULL;
&nbsp; &nbsp; &nbsp; &nbsp; }

if&nbsp;(cycle->host_specs->host_mem) {
ngx_free(cycle->host_specs->host_mem->data);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle->host_specs->host_mem->data =&nbsp;NULL;
ngx_free(cycle->host_specs->host_mem);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle->host_specs->host_mem =&nbsp;NULL;
&nbsp; &nbsp; &nbsp; &nbsp; }

if&nbsp;(cycle->host_specs->host_os) {
ngx_free(cycle->host_specs->host_os->data);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle->host_specs->host_os->data =&nbsp;NULL;
ngx_free(cycle->host_specs->host_os);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle->host_specs->host_os =&nbsp;NULL;
&nbsp; &nbsp; &nbsp; &nbsp; }

ngx_free(cycle->host_specs);
&nbsp; &nbsp; &nbsp; &nbsp; cycle->host_specs =&nbsp;NULL;
&nbsp; &nbsp; }
// [...]

该 bug 的要点是:即便 remote_admin未启用,悬空指针仍可在 ngx_http_get_host_specs中被解引用以打印主机规格。在它被 ngx_master_process_exit丢弃并终止进程之前,我找不到任何其他用途。在实践上,这在 CHERI 与 mrs revocation 下仍可行。

CPV17: 导致 double free 的 UAF?

触发 CPV17 的堆 UAF 会记录一条应用错误,因为 UAF 对象 s->connection的 write事件对象在 ngx_mail_session_internal_server_error中传给了 ngx_mail_send,且与 s->connection->write对应的 fd在第一次 free (ngx_mail_close_connection) 时被关闭,因此导致 send() failed (9: Bad file descriptor)错误。在我使用的 CheriBSD 版本 (24.05) 中,触发该 bug 不会让 worker 进程崩溃。

2025/02/04 00:06:22 [alert] 21598#0: *2 send() failed (9: Bad file descriptor) while in auth state, client: 127.0.0.1, server: 0.0.0.0:8080
2025/02/04 00:06:22 [alert] 21598#0: *2 connection already closed while in auth state, client: 127.0.0.1, server: 0.0.0.0:8080

*使用官方发布的触发 blob 触发 CPV17 后的 Nginx 日志片段。

根据官方 CPV 信息,

This function attempts to access the freed connection structure, which leads to a crash via a UAF.

然而在 CheriBSD 中不会崩溃。嗯。

那这个 UAF 的影响是什么?由于 fd被清空,ngx_mail_send会调用 ngx_mail_close_connection

void
ngx_mail_send(ngx_event_t&nbsp;*wev)
{
ngx_int_t&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; n;
ngx_connection_t&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; *c;
ngx_mail_session_t&nbsp; &nbsp; &nbsp; &nbsp; *s;
ngx_mail_core_srv_conf_t&nbsp; *cscf;

&nbsp; &nbsp; c = wev->data;
&nbsp; &nbsp; s = c->data;

if&nbsp;(wev->timedout) {
ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT,&nbsp;"client timed out");
&nbsp; &nbsp; &nbsp; &nbsp; c->timedout =&nbsp;1;
ngx_mail_close_connection(c);
return;
&nbsp; &nbsp; }

if&nbsp;(s->out.len&nbsp;==&nbsp;0) {
if&nbsp;(ngx_handle_write_event(c->write,&nbsp;0) != NGX_OK) {
ngx_mail_close_connection(c);
&nbsp; &nbsp; &nbsp; &nbsp; }

return;
&nbsp; &nbsp; }

&nbsp; &nbsp; n = c->send(c, s->out.data, s->out.len);

// [...]

if&nbsp;(n == NGX_ERROR) {
ngx_mail_close_connection(c);
return;
&nbsp; &nbsp; }
// [...]

对同一个连接对象调用两次 ngx_mail_close_connection,意味着会调用两次 ngx_close_connection和 ngx_destroy_poolngx_close_connection并没有价值,因为它会检查 fd是否为 -1。对同一个 pool 对象调用两次 ngx_destroy_pool可能会破坏内存分配器的内部状态? 在 ngx_destroy_pool中,已注册的 cleanup handler函数会被调用,与 pool 关联的大块分配会被释放,pool blocks 会用 ngx_free释放。使用 mrs 时,第二次释放是 no-op。因此剩下的只有 cleanup handler 会被调用两次……但快速看一眼似乎没用。

void
ngx_mail_close_connection(ngx_connection_t&nbsp;*c)
{
ngx_pool_t&nbsp; *pool;

ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log,&nbsp;0,
"close mail connection:&nbsp;%d", c->fd);

#if&nbsp;(NGX_MAIL_SSL)

if&nbsp;(c->ssl) {
if&nbsp;(ngx_ssl_shutdown(c) == NGX_AGAIN) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; c->ssl->handler = ngx_mail_close_connection;
return;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

#endif

#if&nbsp;(NGX_STAT_STUB)
&nbsp; &nbsp; (void)&nbsp;ngx_atomic_fetch_add(ngx_stat_active, -1);
#endif

&nbsp; &nbsp; c->destroyed =&nbsp;1;

&nbsp; &nbsp; pool = c->pool;

ngx_close_connection(c);

ngx_destroy_pool(pool);
}

该 bug 的要点是:两次调用 ngx_destroy_pool会导致 pool block 和大块分配的 double free。是否能用于构建利用并不明确,但 double free 会被 mrs 转成 no-op,或在 CheriBSD 25.03 版本中直接 abort()

小结

在开启 caprevoke 的情况下,堆分配会被 mrs 隔离,之后再进行能力的批量撤销。指向隔离区中已释放分配的悬空指针仍是有效 capability,因此在 CHERI 中允许解引用这些指向隔离区分配的 capability。只有当内存被重新分配后,悬空指针才会通过批量撤销变成无效。

因此可以推断,CHERI 并不会对堆 UAF 确定性触发 fault。CHERI 堆 temporal safety 的保证是阻止 use-after-reallocation,即攻击者无法利用内存别名来构造例如 type confusion,但在重新分配前触发的 UAF 不会 fault。例如,在这些特定的 CPV 触发器中,悬空指针在内存重新分配之前就被使用,因此这些输入不会触发 fault。这意味着 CHERI 单独作为 temporal safety bug 的 sanitizer 并不可靠。

但如果将 CHERI 看作缓解而非 sanitizer,能否构造不依赖悬空指针与其他对象重叠的利用?这取决于代码库。我认为,在我们这些特定的 UAF 上,如果无法在底层内存重新分配之后使用悬空指针,就无法实现除崩溃之外的效果。(FYI: 我在 另一篇博客 中探索了如何利用这三个 CPV;由于上面的原因,同样的方式在 CHERI 上不可行)。(如果我在代码分析中遗漏了点,请指正。)

CPV10: 额外漏洞?

我之前提到 CPV10 的 double free 在 CheriBSD 中由 jemalloc+mrs 缓解,因为 mrs 实现了指针校验。然而在现有的 CheriBSD 24.05 配置下 (此时 double free 被忽略而非 abort),与 ASAN 不同,程序可以在第二次 free() 之后继续执行,从而让后续代码仍在内存安全强制下运行。

staticngx_int_t
ngx_http_process_prefer(ngx_http_request_t&nbsp;*r,&nbsp;ngx_table_elt_t&nbsp;*h,
ngx_uint_t&nbsp;offset)
{
ngx_table_elt_t&nbsp;*p;

if&nbsp;(r->headers_in.prefer) {
ngx_log_error(NGX_LOG_INFO, r->connection->log,&nbsp;0,
"client sent duplicate host header:&nbsp;\"%V: %V\", "
"previous value:&nbsp;\"%V: %V\"",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &h->key, &h->value, &r->headers_in.prefer->key,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &r->headers_in.prefer->value);
ngx_free(r->headers_in.prefer);&nbsp;// NO CHERI crash in CPV10 (double free)
return&nbsp;NGX_OK;
&nbsp; &nbsp; }
// [...]

因此,我发现另一个很可能是在 CPV10 的 Prefer 功能开发时引入的 bug,会在 CHERI 中引发 bounds violation fault。

[New LWP 100214]
Core was generated by `nginx: worker process'.
Program terminated with signal SIGPROT, CHERI protection violation.
Capability bounds fault.
625 &nbsp; &nbsp; &nbsp; &nbsp; /* the end of HTTP header */
626 &nbsp; &nbsp; &nbsp; &nbsp; *b->last++ = CR; *b->last++ = LF; // or here, CHERI crash unintended CPV10 (bounds fault)
&nbsp; &nbsp;0x00000000000cff38 <+2724>: &nbsp;ldr &nbsp; &nbsp; c0, [c2,&nbsp;#16]
&nbsp; &nbsp;0x00000000000cff3c <+2728>: &nbsp;add &nbsp; &nbsp; c1, c0,&nbsp;#0x1
&nbsp; &nbsp;0x00000000000cff40 <+2732>: &nbsp;str &nbsp; &nbsp; c1, [c2,&nbsp;#16]
&nbsp; &nbsp;0x00000000000cff44 <+2736>: &nbsp;strb &nbsp; &nbsp;w8, [c0]
&nbsp; &nbsp;0x00000000000cff48 <+2740>: &nbsp;ldr &nbsp; &nbsp; c0, [c2,&nbsp;#16]
&nbsp; &nbsp;0x00000000000cff4c <+2744>: &nbsp;mov &nbsp; &nbsp; w8,&nbsp;#0xa&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; //&nbsp;#10
&nbsp; &nbsp;0x00000000000cff50 <+2748>: &nbsp;add &nbsp; &nbsp; c1, c0,&nbsp;#0x1
&nbsp; &nbsp;0x00000000000cff54 <+2752>: &nbsp;str &nbsp; &nbsp; c1, [c2,&nbsp;#16]
&nbsp; &nbsp;0x00000000000cff58 <+2756>: &nbsp;strb &nbsp; &nbsp;w8, [c0] &nbsp;<==
gef> &nbsp;p $c0
$1 = () 0x415b8f9a [rwRW,0x415b8ea0-0x415b8f9a]

在 ngx_http_header_filter中,AIxCC 的开发者添加了一个新 header。

if&nbsp;(r->headers_in.prefer) {&nbsp;// XXXR3: unintended bug that can be triggered by CPV10 testcase
&nbsp; &nbsp; &nbsp; &nbsp; b->last =&nbsp;ngx_cpymem(b->last,&nbsp;"Prefer: ",
sizeof("Prefer: ") -&nbsp;1);
&nbsp; &nbsp; &nbsp; &nbsp; b->last =&nbsp;ngx_cpymem(b->last, r->headers_in.prefer->value.data,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;r->headers_in.prefer->value.len);

&nbsp; &nbsp; &nbsp; &nbsp; *b->last++ = CR; *b->last++ = LF;
&nbsp; &nbsp; }

该 buffer b在 b = ngx_create_temp_buf(r->pool, len);中分配,len为计算得到的 header 总大小。我们可以看到,Last-Modifiedheader 占用的字节数被加到了 len中,但 Preferheader 的大小未被纳入 len的计算。

// [...]
if&nbsp;(r->headers_out.last_modified ==&nbsp;NULL
&nbsp; &nbsp; &nbsp; &nbsp; && r->headers_out.last_modified_time != -1)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; len +=&nbsp;sizeof("Last-Modified: Mon, 28 Sep 1970 06:00:00 GMT"&nbsp;CRLF) -&nbsp;1;
&nbsp; &nbsp; }
// [...]
if&nbsp;(r->headers_out.last_modified ==&nbsp;NULL
&nbsp; &nbsp; &nbsp; &nbsp; && r->headers_out.last_modified_time != -1)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; b->last =&nbsp;ngx_cpymem(b->last,&nbsp;"Last-Modified: ",
sizeof("Last-Modified: ") -&nbsp;1);
&nbsp; &nbsp; &nbsp; &nbsp; b->last =&nbsp;ngx_http_time(b->last, r->headers_out.last_modified_time);

&nbsp; &nbsp; &nbsp; &nbsp; *b->last++ = CR; *b->last++ = LF;
&nbsp; &nbsp; }
// [...]

我观察到以下请求不会触发溢出:

GET / HTTP/1.1
Host: localhost
Connection: close
Prefer: 1234567

但如果多加 1 字节,就会因 bounds violation 崩溃:

GET / HTTP/1.1
Host: localhost
Connection: close
Prefer: 12345678

无论如何,我认为这个因 buffer size 计算错误导致的堆缓冲区溢出,很可能是 Prefer 功能开发时的失误。

额外说明

你可能注意到 CPV 的描述会评论漏洞影响,许多 CPV 描述类似如下:

This vulnerability causes NGINX to crash thus denying service to its clients. The intentional crash of a service is called “Denial of Service” or DoS.

确实,DoS 是一种安全问题。而 CHERI 不会缓解以 DoS 为目标的攻击,因为其“fail-stop”行为会直接崩溃。

致谢

感谢 Robert Watson 教授审阅本文并提出诸多宝贵建议。

参考文献

1Paul Liétar, Theodore Butler, Sylvan Clebsch, Sophia Drossopoulou, Juliana Franco, Matthew J. Parkinson, Alex Shamis, Christoph M. Wintersteiger, and David Chisnall. 2019. Snmalloc: a message passing allocator. In Proceedings of the 2019 ACM SIGPLAN International Symposium on Memory Management (ISMM 2019). Association for Computing Machinery, New York, NY, USA, 122–135. https://doi.org/10.1145/3315573.3329980

2Project Zero. 2014. The poisoned NUL byte, 2014 edition. Retrieved from https://googleprojectzero.blogspot.com/2014/05/the-poisoned-nul-byte-2014-edition.html

3N. Wesley Filardo et al., “Cornucopia: Temporal Safety for CHERI Heaps,” 2020 IEEE Symposium on Security and Privacy (SP), San Francisco, CA, USA, 2020, pp. 608-625, doi: 10.1109/SP40000.2020.00098

4Nathaniel Wesley Filardo, Brett F. Gutstein, Jonathan Woodruff, Jessica Clarke, Peter Rugg, Brooks Davis, Mark Johnston, Robert Norton, David Chisnall, Simon W. Moore, Peter G. Neumann, and Robert N. M. Watson. 2024. Cornucopia Reloaded: Load Barriers for CHERI Heap Temporal Safety. In Proceedings of the 29th ACM International Conference on Architectural Support for Programming Languages and Operating Systems, Volume 2 (ASPLOS ‘24), Vol. 2. Association for Computing Machinery, New York, NY, USA, 251–268. https://doi.org/10.1145/3620665.3640416

5Brooks Davis, Robert N. M. Watson, Alexander Richardson, Peter G. Neumann, Simon W. Moore, John Baldwin, David Chisnall, Jessica Clarke, Nathaniel Wesley Filardo, Khilan Gudka, Alexandre Joannou, Ben Laurie, A. Theodore Markettos, J. Edward Maste, Alfredo Mazzinghi, Edward Tomasz Napierala, Robert M. Norton, Michael Roe, Peter Sewell, Stacey Son, and Jonathan Woodruff. 2019. CheriABI: Enforcing Valid Pointer Provenance and Minimizing Pointer Privilege in the POSIX C Run-time Environment. In Proceedings of the Twenty-Fourth International Conference on Architectural Support for Programming Languages and Operating Systems (ASPLOS ‘19). Association for Computing Machinery, New York, NY, USA, 379–393. https://doi.org/10.1145/3297858.3304042

6Richard Grisenthwaite, Graeme Barnes, Robert N. M. Watson, Simon W. Moore, Peter Sewell, and Jonathan Woodruff. 2023. The Arm Morello Evaluation Platform—Validating CHERI-Based Security in a High-Performance System. IEEE Micro 43, 3 (May-June 2023), 50–57. https://doi.org/10.1109/MM.2023.3264676


Security analysis of AIxCC Nginx with CHERI

免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:securitainment RoundofThree RoundofThree《AIxCC Nginx 的 CHERI 安全分析》

评论:0   参与:  0