Copy Fail 和 DirtyFrag:野生的 Linux 页面缓存错误

本研究分析了 Linux 内核权限升级漏洞 Copy Fail 和 DirtyFrag,这两个漏洞利用了微妙的页面缓存损坏漏洞,为 root 访问创建了可靠路径。此外,Elastic Security Labs 正在发布针对这些漏洞的检测逻辑。

4 分钟阅读探测工程

简介

最近的 Linux 内核权限升级漏洞、Copy Fail(CVE-2026-31431)、Copy Fail 2 和 DirtyFrag 凸显了微妙的页面缓存损坏漏洞是如何成为通向 root 的实用可靠路径的。由于利用涉及合法内核接口、本地执行和简短的概念验证代码,因此这些问题与防御者尤为相关。Copy Fail 已被报告为在野外被利用,并被添加到 CISA 的已知被利用漏洞目录中。

为了帮助减轻这些威胁,Elastic Security Labs 开发了检测逻辑,重点是围绕这些漏洞的利用模式,而不仅仅是匹配特定的概念验证实现。

复制失败

复制失败是 Linux 内核authencesn 加密模板中的一个逻辑错误。该漏洞通过AF_ALGsplice() 向任何可读文件的页面缓存创建受控的 4 字节写入。实际上,这会破坏/usr/bin/su 等 setuid 二进制文件的内存视图,并在不更改磁盘文件的情况下提升权限。公开的漏洞利用程序是一个 732 字节的 Python 脚本,适用于 Ubuntu、Amazon Linux、RHEL 和 SUSE。

脏污

DirtyFrag 通过两个页面缓存写入变体将同一错误类扩展到网络堆栈中。ESP 路径通过AF_NETLINK 使用 XFRM 安全关联,对拼接页面执行就地加密操作,用最小的根壳 ELF 覆盖/usr/bin/su 。RxRPC 回退路径使用AF_RXRPCpcbc(fcrypt) 破坏/etc/passwd ,清除 root 的密码字段。这两种路径都要求unshare(CLONE_NEWUSER | CLONE_NEWNET) 在触发页面缓存写入之前获得命名空间能力。

DirtyFrag 不依赖于algif_aead 模块,这意味着只应用了 Copy Fail 缓解功能的系统仍有可能被暴露。

检测

对于这些漏洞,我们重点检测的是底层基元和行为,而不仅仅是具体的漏洞利用实现。这种区别很重要,Copy Fail 已经有了多个公开的重新实现(Python、Go、Rust、C、Metasploit),而 DirtyFrag 则是一个公开的 C 概念验证。试图只检测特定的 PoC 会让维护者落后一步。

系统调用级原语(Auditd)

Copy Fail 和 DirtyFrag 都依赖socket(AF_ALG) 访问内核加密子系统,并依赖splice() 将只读文件页注入网络缓冲区,在网络缓冲区中进行就地加密操作会破坏页面缓存。当AF_ALG 不可用时,DirtyFrag 还会使用socket(AF_RXRPC) 作为备用。通过 auditd 系统调用审计socketa0 十六进制值为26 (AF_ALG) 或21 (AF_RXRPC) 的调用,以及来自非 root 进程的splice 调用,可以看到这些基元。我们将这些作为早期信号,通过 EQL 序列与最终权限升级步骤(从非根调用者处获得有效 uid 0 )相关联:

sequence with maxspan=60s
  [any where host.os.type == "linux" and    
   (
    (event.category == "process" and auditd.data.syscall == "socket" and auditd.data.a0 in ("26", "21")) or 
    (event.category == "process" and auditd.data.syscall == "splice") or 
    (event.category == "network" and event.action == "bound-socket" and data_stream.dataset == "auditd_manager.auditd" and ?auditd.data.socket.family == "38") 
    )  
   and user.id != "0"]  by process.pid, host.id, user.id with runs=10
  [process where host.os.type == "linux"  and event.action == "executed" and 
   (
     (user.effective.id == "0" and user.id != "0") or 
     (process.name in ("bash", "sh", "zsh", "dash", "fish", "ksh", "busybox") and 
      process.args in ("-c", "--command", "-ic", "-ci", "-cl", "-lc", "-bash", "-sh", "-zsh", "-dash", "-fish", "-ksh"))
    )] by process.parent.pid, host.id, user.id

Example of matches :

命名空间创建(DirtyFrag 专用)

DirtyFrag 的漏洞利用链还依靠unshare(CLONE_NEWUSER | CLONE_NEWNET) 获得命名空间能力。我们将此事件与根进程执行或随后不久的setuid(0) syscall 相关联:

sequence by host.id, process.parent.pid with maxspan=30s
 [process where host.os.type == "linux" and 
  (
   (auditd.data.syscall == "unshare" and auditd.data.class == "namespace" and auditd.data.a0 in ("10000000", "50000000", "70000000", "10020000", "50020000", "70020000")) or 

   (process.name == "unshare" and  
    (process.args in ("--user", "--map-root-user", "--map-current-user") or process.args like ("-*U*", "-*r*")))
   ) and user.id != "0" and user.id != null]
 [process where host.os.type == "linux" and 
  user.id == "0" and user.id != null and 
  (
   process.name in ("su", "sudo", "pkexec", "passwd", "chsh", "newgrp", "doas", "run0", "sg", "dash", "sh", "bash", "zsh", "fish", 
                    "ksh", "csh", "tcsh", "ash", "mksh", "busybox", "rbash", "rzsh", "rksh", "tmux", "screen", "node") or 
   process.name like ("python*", "perl*", "ruby*", "php*", "lua*")
  )]

通用 SUID 二进制滥用(进程执行事件)

我们还评估了仅使用进程执行事件的检测选项,因为与 auditd 系统调用审计相比,这些选项往往在更多环境中启用。这两种漏洞利用的最后一个共同步骤是破坏或影响 SUID 二进制文件(如su,sudo,pkexec,passwd,chshnewgrp )的内存执行,使其以 root 身份运行攻击者控制的代码。

检测会查找可疑的执行,其中进程以有效 UID 0 的身份运行,真实用户为非 root,父进程也是非 root,SUID 二进制进程以最小参数启动,父进程是脚本运行时、shell 单行程序或来自用户可写路径的可执行文件:

process where event.type == "start" and event.action == "exec" and (
  (process.user.id == 0 and process.real_user.id != 0) or
  (process.group.id == 0 and process.real_group.id != 0)
) and (
  (process.name == "su" and process.args_count <= 2) or
  (process.name == "sudo" and process.args_count == 1) or
  (process.name == "pkexec" and process.args_count == 1) or
  (process.name == "passwd" and process.args_count <= 2)
) and
(
  process.parent.name like (".*", "python*", "perl*", "ruby*", "lua*", "php*", "node", "deno", "bun", "java") or
  process.parent.executable like ("./*", "/tmp/*", "/var/tmp/*", "/dev/shm/*", "/run/user/*", "/var/run/user/*", "/home/*/*") or
  (
    process.parent.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish", "mksh") and
    process.parent.args in ("-c", "-cl", "-lc", "--command", "-ic", "-ci", "-bash", "-sh", "-zsh", "-dash", "-fish", "-ksh") and
    process.parent.args_count <= 4
  )
)

无需依赖子进程的生成,我们也可以使用 ES|QL 主动猎取利用活动。Copy Fail 和 DirtyFrag 都会从同一进程中产生一系列独特的交错socket(AF_ALG)splice() 系统调用。Copy Fail 会迭代 48 次以写入 192 字节,而 DirtyFrag 在其 ESP 和 RxRPC 路径中也遵循类似的模式。

下面的查询按进程汇总了这些系统调用,并在卷中列出了任何将AF_ALGAF_RXRPC 套接字与splice 调用相结合的非根进程:

FROM logs-auditd_manager.auditd-default*
| WHERE host.os.type == "linux" AND user.id != "0" AND
  (
    (event.category == "process" AND auditd.data.syscall == "socket" AND auditd.data.a0 IN ("26", "21")) OR
    (event.category == "process" AND auditd.data.syscall == "splice") OR
    (event.category == "network" AND event.action == "bound-socket" AND auditd.data.socket.family == "38")
  )
| EVAL
    is_af_alg   = CASE(auditd.data.syscall == "socket" AND auditd.data.a0 == "26", 1, 0),
    is_af_rxrpc = CASE(auditd.data.syscall == "socket" AND auditd.data.a0 == "21", 1, 0),
    is_splice   = CASE(auditd.data.syscall == "splice", 1, 0),
    is_bind_alg = CASE(event.action == "bound-socket" AND auditd.data.socket.family == "38", 1, 0)
| STATS
    socket_af_alg   = SUM(is_af_alg),
    socket_af_rxrpc = SUM(is_af_rxrpc),
    splice_count    = SUM(is_splice),
    bind_af_alg     = SUM(is_bind_alg),
    total_calls     = COUNT(*),
    first_seen      = MIN(@timestamp),
    last_seen        = MAX(@timestamp)
  BY host.name, user.name, process.executable, process.pid
| EVAL
    duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
    distinct_syscalls = CASE(
      socket_af_alg > 0 AND splice_count > 0 AND bind_af_alg > 0, "af_alg+splice+bind",
      socket_af_alg > 0 AND splice_count > 0, "af_alg+splice",
      socket_af_rxrpc > 0 AND splice_count > 0, "af_rxrpc+splice",
      socket_af_alg > 0, "af_alg_only",
      socket_af_rxrpc > 0, "af_rxrpc_only",
      splice_count > 0, "splice_only",
      "other"
    )
| WHERE total_calls >= 10 AND
  (socket_af_alg > 0 OR socket_af_rxrpc > 0) AND
  splice_count > 0
| SORT total_calls DESC
| LIMIT 50

审核规则:

可将以下规则添加到Auditd集成配置中,以启用对这些漏洞利用原语的可见性:

-a always,exit -F arch=b64 -S socket -k socket_syscall
-a always,exit -F arch=b32 -S socketcall -k socket_syscall
-a always,exit -F arch=b64 -S splice -k splice-syscall
-a always,exit -F arch=b32 -S splice -k splice-syscall
-a always,exit -F arch=b64 -S bind -k socket_bound
-a always,exit -F arch=b32 -S bind -k socket_bound

检测规则 :

减轻

检测应与加固和打补丁相结合。这两个漏洞的主要补救措施是,一旦发布补丁,就更新 Linux 内核。

在无法立即打补丁的情况下,有针对性地封堵模块可以减少攻击面。对于 Copy Fail,禁用algif_aead 模块可防止漏洞利用程序使用 AF_ALG AEAD 路径:

echo "install algif_aead /bin/false" > /etc/modprobe.d/copyfail.conf
rmmod algif_aead 2>/dev/null

对于 DirtyFrag,禁用受影响的网络模块可阻止 ESP 和 RxRPC 攻击路径:

printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf
rmmod esp4 esp6 rxrpc 2>/dev/null

应用这两种缓解措施后,丢弃页面缓存可确保之前损坏的内存页面被丢弃:

echo 3 > /proc/sys/vm/drop_caches

由于禁用内核模块可能会影响 IPsec VPN、加密应用程序或其他服务(具体取决于受影响的子系统),因此在生产部署前应在暂存环境中测试这些缓解措施。丢弃页面缓存会导致短暂的 I/O 峰值,因此应避免在负载高峰期丢弃页面缓存。

限制非特权用户命名空间的创建还能有效防范 DirtyFrag 和类似的漏洞利用:

sysctl -w kernel.unprivileged_userns_clone=0

在 RHEL/Fedora 上,请使用user.max_user_namespaces=0 。此设置可能会影响依赖非特权命名空间的应用程序,如某些容器运行时和浏览器沙箱。申请前评估兼容性。

参考资料:

分享这篇文章