文章总结: CVE-2026-31431(CopyFail)是Linux内核中的一个本地提权漏洞,源于2017年的一次性能优化(提交72548b093ee3)破坏了algifaead加密接口中源(src)与目标(dst)缓冲区的边界。当结合afalg、splice()、authencesn等机制时,攻击者可利用此漏洞将4个可控字节写入任意文件的pagecache,从而篡改setuid程序内存执行路径实现提权,且该修改对基于磁盘哈希的完整性检查不可见。主线修复(a664bf3d603d)通过恢复源与目标缓冲区的分离解决了此问题。 综合评分: 82 文章分类: 漏洞分析,二进制安全,内核安全
4 个字节,9 年技术债:CVE-2026-31431 / Copy Fail 完整拆解
原创
ckcsec ckcsec
CKCsec安全研究院
2026年5月9日 01:02 美国
在小说阅读器读本章
去阅读
CVE-2026-31431,公开名字是 Copy Fail。
这个洞最容易被一句话概括成“Linux 本地提权”,但这样说太轻了。它真正有意思的地方不在提权结果,而在提权原语:一个本来只应该写到 AEAD 输出缓冲区里的 4 字节,最后被写进了文件的 page cache。
公开披露时间是 2026-04-29。主线修复是 a664bf3d603d,致因提交是 2017 年的 72548b093ee3。公开资料给出的影响窗口覆盖 Linux 4.14 到 6.19.12 一段相当长的周期。攻击条件也并不苛刻:一个非 root 的本地 shell,就够进入这条链。
这个洞从工程角度看有几个不寻常的地方:
- 没有 race,没有玄学重试,触发路径是直线逻辑;
- 不依赖典型内核地址泄露,也不靠某个发行版的固定内核偏移;
- 改的是 page cache,不是磁盘文件,传统基于文件 hash 的完整性检查很容易看不到;
- 它跨了好几层本来各自“看起来没问题”的机制:
AF_ALG、splice()、algif_aead、authencesn(...)。
下面按漏洞链把它拆开。
一、入口:AF_ALG 的 algif_aead
AF_ALG 是 Linux 内核暴露给用户态的 crypto socket family。用户态进程可以通过它直接调用内核里的哈希、对称加密、AEAD 等算法。
大多数业务代码并不会主动写 AF_ALG,但主流发行版内核往往默认带着这条入口。也就是说,它不是一个需要攻击者先加载奇怪模块才能到达的边缘面,而是一个默认在那里的内核接口。
调用形态大概是这样:
int tfm = socket(AF_ALG, SOCK_SEQPACKET, 0);
structsockaddr_algsa = {
.salg_family = AF_ALG,
.salg_type = "aead",
.salg_name = "authencesn(hmac(sha1),rfc3686(ctr(aes)))",
};
bind(tfm, (struct sockaddr *)&sa, sizeof(sa));
setsockopt(tfm, SOL_ALG, ALG_SET_KEY, key, keylen);
int op = accept(tfm, NULL, NULL);
sendmsg(op, &msg, MSG_MORE);
read(op, out, outlen);
这里最关键的是 authencesn(...)。
它是 IPsec ESN 场景里的组合算法模板,用来支持 64-bit 扩展序号。常规业务基本不会主动碰它,但只要内核注册了这个模板,用户态通过 bind() 就能借到这条算法路径。
Copy Fail 的第一层问题就在这里:一个普通用户态进程,可以把请求送进一条内核加密路径,而这条路径后面会碰到 scatterlist、splice 和 page cache。
二、三个机制单独看都合理,串起来就出事
1. scatterlist:内核 crypto API 的输入输出格式
内核加密 API 不喜欢用户态那种“连续 buffer”的思维。它处理的是 struct scatterlist,本质上是一串:
(page, offset, length)
每个 AEAD 请求上通常有两条链:
req->src -> [page1] -> [page2] -> ... 输入:AAD + 密文
req->dst -> [pageA] -> [pageB] -> ... 输出:明文
正常情况下,src 和 dst 是两条独立链。
当然,加密 API 也不是不能做 in-place,也就是输入输出落在同一组 page 上。但前提是:算法要知道自己在 in-place,调用方也要保证不会写到不该写的地方。
Copy Fail 的核心问题,就是这个前提后来被破坏了。
2. splice:把 page cache 直接当 page 交出去
splice() 的危险点不在“能读文件”,而在“它不复制”。
Linux 的 page cache 是磁盘文件在内存中的权威副本。读 /usr/bin/su,很多时候读到的就是 page cache 里的物理页。splice() 这类零拷贝路径可以把文件页的 struct page * 引用直接塞进管道、socket 或下游子系统。
可以把它理解成这样:
/usr/bin/su on disk
|
| open + splice()
v
page cache physical page
|
| zero-copy into pipe / msg_iter
v
algif_aead req->src
这和 Dirty Pipe 那类问题有同一个底层味道: 下游子系统以为自己拿到的是“只读输入”,但如果它哪一步把这张 page 当成输出写了,事情就会立刻变味。
3. authencesn:为了省事,借 dst 放 4 字节
authencesn 在 AEAD 处理前要处理 ESN 高 32 位。它没有单独申请临时缓冲区,而是借用了 dst 上的一小块空间,把 4 字节先放进去,后面再覆盖回去。
简化后就是这类逻辑:
staticintcrypto_authenc_esn_genicv(struct aead_request *req, ...)
{
/*
* 在 dst 链上跳过 assoclen + cryptlen,
* 借后面 4 字节存 ESN 高位。
*/
scatterwalk_map_and_copy(esn_high_bytes, req->dst,
req->assoclen + cryptlen, 4, 1);
...
}
如果 req->dst 真是用户独占的输出 buffer,这 4 字节借用没有什么问题。写进去,算完,盖回去,外界看不见。
问题是:2017 年那次 algif_aead 优化之后,dst 不再总是那个干净的输出 buffer。
三、根因:72548b093ee3 把 src 和 dst 边界改模糊了
2017 年之前,algif_aead 基本是 out-of-place 的。req->src 和 req->dst 各自一条 scatterlist,不会把输入页和输出页混成一个对象。
72548b093ee3 这次优化的目标是性能:少拷贝一次,走 in-place 路径。问题出在 scatterlist 的组织方式上。
简化后的结构大概是这样:
/* crypto/algif_aead.c - 2017 优化后的简化结构 */
sg_init_table(sgl_src, src_nents + dst_nents);
sg_init_table(sgl_dst, dst_nents);
/* src:来自 sendmsg / splice 的 page,可能是 page cache 页 */
af_alg_make_sg(&ctx->rsgl[0], &msg->msg_iter, ...);
/* dst:用户给的输出 buffer page */
af_alg_make_sg(&ctx->rsgl[1], &out_iter, ...);
/* 关键:把 dst 链接到 src 链后面 */
sg_chain(sgl_src, src_nents + 1, sgl_dst);
req->src = sgl_src;
req->dst = sgl_src;
aead_request_set_crypt(req, req->src, req->dst, cryptlen, iv);
问题不是 sg_chain() 这个函数本身坏了,而是这次组织方式改变了 dst 的语义。
原本:
src -> 输入页
dst -> 输出页
优化后,在特定布局下:
src 链尾 -> splice 进来的 page cache 页
dst 起点 -> 接在 src 后面,逻辑上被当成输出
于是 authencesn 那个“借 dst 放 4 字节”的动作,可能落到上一段 src 链尾的 page cache 页上。
这就是整个洞的数学核心:
- 目标页:由攻击者通过
splice()选择; - 写入偏移:由
assoclen + cryptlen间接控制; - 写入内容:来自 ESN 高 32 位,能通过请求字段构造;
- 写入长度:4 字节;
- 写入对象:page cache;
- 稳定性:没有 race。
一句话:拿到了一个任意 page-cache 偏移、4 字节、内容可控的写原语。
这不是“误写了 4 字节”这么简单。真正致命的是,这 4 字节写到了文件页缓存上。
四、官方修复:把 2017 年那次优化退回去
主线修复 a664bf3d603d 的方向很直接:撤回 2017 年那次 in-place 优化,让 src 和 dst 重新分开。
简化成 diff 就是:
- sg_init_table(sgl_src, src_nents + dst_nents);
- ...
- sg_chain(sgl_src, src_nents + 1, sgl_dst);
- req->src = sgl_src;
- req->dst = sgl_src;
+ sg_init_table(sgl_src, src_nents);
+ sg_init_table(sgl_dst, dst_nents);
+ ...
+ req->src = sgl_src;
+ req->dst = sgl_dst;
authencesn 仍然可以借 dst 的 4 字节。
但修复之后,dst 不再可能是那张从文件 splice() 进来的 page cache 页。
这个修复没有玩复杂补丁,也没有在 authencesn 里堆各种特判。它回到最根本的边界:输入是输入,输出是输出。
五、4 字节怎么变成 root
写一次 4 字节能做什么?
如果目标是普通文本,可能什么也不是。 如果目标是 setuid root 二进制的关键跳转,那就完全不一样了。
典型的 setuid 程序里,认证后会有类似这样的逻辑:
call pam_authenticate
test eax, eax
jne auth_failed
...
call setuid
call execve
auth_failed:
...
x86_64 里,长跳转 jne rel32 的典型编码是:
0F 85 xx xx xx xx
前 4 字节如果被改成 NOP:
原始:0F 85 ?? ?? ?? ?? ; jne rel32
修改:90 90 90 90 ?? ?? ; nop nop nop nop + 残留两字节
认证失败后本来应该跳走,现在没有跳。程序就继续往下执行。 如果这段路径后面正好进入提权逻辑,那么普通用户就拿到了 root 语义下的执行。
这就是 Copy Fail 的实际危险:
磁盘上的 /usr/bin/su 没变
page cache 里的 /usr/bin/su 变了
execve() 读到的是 page cache
setuid root 程序按被污染后的代码路径跑
所以很多检测会失灵:
sha256sum /usr/bin/su不变;- 包管理器校验不变;
- IMA / AIDE / Tripwire 这类基于磁盘 hash 的方案不一定报警;
- 写入路径不走普通 VFS write;
- page cache 没有持久落盘,重启后文件本体仍然干净。
这也是 Copy Fail 比普通“改文件提权”麻烦的地方。它改的是执行时内存视图。
六、利用链骨架
完整 EXP 已经有公开 PoC。这里不贴可直接复制执行的版本,只保留原语骨架,用来说明每一步到底在干什么。
缺掉的部分包括:
- 目标 setuid 二进制的具体偏移选择;
- 针对不同 ELF 布局的入口定位;
- page 布局和 sg 链稳定落点;
- 能直接跑通的 payload 拼接。
原语骨架如下:
# CVE-2026-31431 / Copy Fail - primitive skeleton
# only for understanding the chain; not a drop-in exploit
import os
import socket
AF_ALG = 38
ALG_SET_KEY = 1
PAGE = 4096
target_path = "/usr/bin/su"
patch_offset = locate_patch_site(target_path) # find a controlled 4-byte patch site
patch_value = b"\x90\x90\x90\x90"
# 1. Bring the target file page into page cache and pass it through splice.
fd = os.open(target_path, os.O_RDONLY)
r, w = os.pipe()
page_aligned = patch_offset & ~(PAGE - 1)
os.lseek(fd, page_aligned, os.SEEK_SET)
os.splice(fd, w, PAGE)
# 2. Bind AF_ALG to the authencesn AEAD template.
tfm = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)
tfm.bind(("aead", "authencesn(hmac(sha1),rfc3686(ctr(aes)))"))
tfm.setsockopt(socket.SOL_ALG, ALG_SET_KEY, b"\x00" * 20)
op, _ = tfm.accept()
# 3. Build request fields so authencesn writes the desired high32 bytes.
iv = build_iv_with_high32(patch_value)
# 4. Send spliced page-cache backed pages into req->src.
send_spliced_pages_with_aad(op, r, iv, assoclen=...)
# 5. Prepare an output buffer; vulnerable algif_aead chains dst behind src.
out = bytearray(PAGE)
# 6. Trigger AEAD operation; authencesn writes 4 bytes through req->dst.
op.recv_into(out)
# 7. Execute target; mmap observes the modified page cache.
os.execv(target_path, [target_path])
这段骨架里最关键的不是 Python,而是四个动作:
splice()没有复制文件内容,而是把 page cache 页送进后续路径;sendmsg()把这张页挂到了req->src;- 漏洞内核里,
sg_chain()让dst的逻辑落点靠到了src链尾; authencesn往dst写 4 字节时,写到了 page cache。
哪一步需要 race?没有。 哪一步需要反复赌时序?没有。 这就是它工程上稳定的原因。
七、复现过程
复现要放在隔离实验环境里做。这里说的是验证链条,不是拿生产机器试手。
最小环境条件:
- 内核在受影响范围内,且没有合入
a664bf3d603d等价修复; CONFIG_CRYPTO_USER_API_AEAD=y或=m;CONFIG_CRYPTO_AUTHENC=y,authencesn模板可注册;- 普通用户可以创建
AF_ALGsocket; - 测试目标最好是自建的 setuid helper,而不是直接拿生产系统里的真实工具开刀。
实验验证分三层。
第一层,确认路径可达:
socket(AF_ALG, SOCK_SEQPACKET, 0)
bind("aead", "authencesn(...)")
setsockopt(ALG_SET_KEY)
sendmsg / recv
第二层,确认 page cache 被污染:
目标文件磁盘 hash 不变
执行行为发生变化
重启或回收 page cache 后恢复
第三层,确认权限边界被跨越:
普通用户执行 setuid 测试目标
触发前走失败分支
触发后走被污染后的控制流
这三层跑通,就足够证明漏洞链成立。真正的公开 PoC 会继续把这些步骤压缩、泛化、自动定位偏移,但原理并没有变。
八、修复方案
最可靠的方案只有一个:升级到包含修复的内核,或确认发行版已经 backport 等价补丁。
不要只看 uname -r。很多发行版会把上游补丁回补到旧版本号里,云厂商内核也经常有自己的维护分支。判断是否修复,要看发行版安全公告、内核包 changelog,或者确认是否包含 a664bf3d603d 等价修复。
临时缓解可以考虑禁用 algif_aead 模块:
echo"install algif_aead /bin/false" > /etc/modprobe.d/disable-algif.conf
rmmod algif_aead
但这只对模块有效。
如果 algif_aead 是 builtin,modprobe.d 写得再漂亮也挡不住。
容器、CI、在线评测、开发沙箱这类环境还要额外处理 AF_ALG:
- seccomp 拦截
socket(AF_ALG, ...); - pod / container 运行时策略限制不可信 workload;
- 对共享内核环境优先打补丁。
Copy Fail 最该优先处理的不是“看起来最贵”的机器,而是低权限代码执行机会最多的机器:
- CI runner;
- 构建机;
- 跳板机;
- 共享开发机;
- 容器宿主机;
- Notebook / 沙箱 / Serverless 这类跑用户代码的平台。
九、快速自查
先做基础核对:
uname -r
rpm -q --changelog kernel 2>/dev/null | grep -E 'CVE-2026-31431|a664bf3d603d'
dpkg-query -W -f='${Version}\n' linux-image-$(uname -r) 2>/dev/null
再看相关内核能力是否存在:
grep -E 'CONFIG_CRYPTO_USER_API|CONFIG_CRYPTO_USER_API_AEAD|CONFIG_CRYPTO_AUTHENC' /boot/config-$(uname -r) 2>/dev/null
zgrep -E 'CONFIG_CRYPTO_USER_API|CONFIG_CRYPTO_USER_API_AEAD|CONFIG_CRYPTO_AUTHENC' /proc/config.gz 2>/dev/null
lsmod | egrep 'algif_aead|af_alg|authenc'
modinfo algif_aead 2>/dev/null
运行期可以看有没有异常 AF_ALG 使用:
ss -xa | grep AF_ALG
lsof -nP 2>/dev/null | grep AF_ALG
下面是一份更完整的一键初筛脚本。它不是最终裁决,只负责快速判断攻击面是否可能存在。
- 解析内核版本,判断是否落在公开影响窗口
- 查发行版 changelog 里是否有 CVE-2026-31431 / a664bf3d603d 回补线索
- 区分 CONFIG_CRYPTO_USER_API_AEAD=y、=m、禁用、未知
- 检查 authenc/authencesn 相关 crypto 模板
- 读取 /proc/crypto 看运行期 crypto 能力
- 实际测试 AF_ALG / aead / authencesn 是否可绑定
- 检测 modprobe.d 缓解是否是假缓解,尤其是 builtin 情况
- 识别容器环境,提示宿主内核才是关键
- 最后输出 HIGH / NEEDS REVIEW / LOW 分级结论
#!/usr/bin/env bash
# CVE-2026-31431 (Copy Fail) exposure checker
# Purpose: quick local triage. Final patch status must still be confirmed from the vendor advisory/changelog.
set -u
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
NC='\033[0m'
RISK=0
KVER="$(uname -r)"
OS_NAME="unknown"
CONFIG_PATH=""
CONFIG_TMP=""
note() { printf"%b\n""${BLUE}[i]${NC} $*"; }
ok() { printf"%b\n""${GREEN}[OK]${NC} $*"; }
warn() { printf"%b\n""${YELLOW}[!]${NC} $*"; [ "$RISK" -lt 1 ] && RISK=1; }
bad() { printf"%b\n""${RED}[HIGH]${NC} $*"; RISK=2; }
cleanup() {
[ -n "${CONFIG_TMP:-}" ] && [ -f "$CONFIG_TMP" ] && rm -f "$CONFIG_TMP"
}
trap cleanup EXIT
version_ge() {
# Compare major.minor.patch numerically. Missing patch is treated as 0.
awk -v a="$1" -v b="$2"'BEGIN {
split(a, A, "."); split(b, B, ".");
for (i = 1; i <= 3; i++) {
if (A[i] == "") A[i] = 0;
if (B[i] == "") B[i] = 0;
if (A[i] + 0 > B[i] + 0) exit 0;
if (A[i] + 0 < B[i] + 0) exit 1;
}
exit 0;
}'
}
version_lt() {
! version_ge "$1""$2"
}
kernel_base_version() {
printf"%s\n""$KVER" | sed -E 's/^([0-9]+)\.([0-9]+)(\.([0-9]+))?.*/\1.\2.\4/' | sed -E 's/\.$/.0/'
}
load_os_release() {
if [ -r /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
OS_NAME="${PRETTY_NAME:-${NAME:-unknown}}"
fi
}
find_config() {
if [ -r "/boot/config-$KVER" ]; then
CONFIG_PATH="/boot/config-$KVER"
return
fi
if [ -r /proc/config.gz ] && command -v zcat >/dev/null 2>&1; then
CONFIG_TMP="$(mktemp -t copyfail-config.XXXXXX)"
zcat /proc/config.gz > "$CONFIG_TMP" 2>/dev/null || true
if [ -s "$CONFIG_TMP" ]; then
CONFIG_PATH="$CONFIG_TMP"
return
fi
fi
CONFIG_PATH=""
}
config_val() {
key="$1"
if [ -z "$CONFIG_PATH" ]; then
printf"unknown\n"
return
fi
val="$(grep -E "^${key}=" "$CONFIG_PATH" 2>/dev/null | tail -n1 | cut -d= -f2-)"
if [ -n "$val" ]; then
printf"%s\n""$val"
else
if grep -q -E "^# ${key} is not set""$CONFIG_PATH" 2>/dev/null; then
printf"n\n"
else
printf"unknown\n"
fi
fi
}
has_changelog_fix_hint() {
ifcommand -v rpm >/dev/null 2>&1; then
rpm -q --changelog kernel-core 2>/dev/null | grep -qiE 'CVE-2026-31431|a664bf3d603d|Copy Fail' && return 0
rpm -q --changelog kernel 2>/dev/null | grep -qiE 'CVE-2026-31431|a664bf3d603d|Copy Fail' && return 0
fi
ifcommand -v dpkg-query >/dev/null 2>&1; then
pkg="$(dpkg-query -S "/boot/vmlinuz-$KVER" 2>/dev/null | cut -d: -f1 | head -n1)"
if [ -n "$pkg" ]; then
zgrep -hiE 'CVE-2026-31431|a664bf3d603d|Copy Fail'"/usr/share/doc/$pkg/changelog"* 2>/dev/null && return 0
fi
fi
return 1
}
af_alg_probe() {
if ! command -v python3 >/dev/null 2>&1; then
printf"unknown:python3-not-found\n"
return
fi
python3 - <<'PY'
import socket
AF_ALG = 38
tests = [
("aead", "authencesn(hmac(sha1),rfc3686(ctr(aes)))"),
("aead", "gcm(aes)"),
]
try:
s = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)
except PermissionError:
print("blocked:socket-permission")
raise SystemExit
except OSError as e:
print(f"blocked:socket:{e.errno}:{e.strerror}")
raise SystemExit
bound = []
errors = []
for typ, name in tests:
try:
t = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)
t.bind((typ, name))
t.close()
bound.append(name)
except PermissionError:
print("blocked:bind-permission")
raise SystemExit
except OSError as e:
errors.append(f"{name}:{e.errno}:{e.strerror}")
s.close()
if bound:
print("open:" + ",".join(bound))
else:
print("unbound:" + "|".join(errors))
PY
}
echo"==[ Copy Fail / CVE-2026-31431 exposure check ]=="
load_os_release
find_config
echo"[+] OS: $OS_NAME"
echo"[+] Kernel: $KVER"
if [ -n "$CONFIG_PATH" ]; then
echo"[+] Kernel config: $CONFIG_PATH"
else
warn "kernel config not found; built-in/module status may be incomplete"
fi
BASE_VER="$(kernel_base_version)"
echo"[+] Parsed kernel base version: $BASE_VER"
if version_lt "$BASE_VER""4.14.0"; then
ok "kernel base version is older than 4.14; the known in-place algif_aead regression is not expected"
elif version_ge "$BASE_VER""6.19.13"; then
ok "kernel base version is at or above 6.19.13; upstream fixed range should be covered unless vendor tree differs"
else
warn "kernel base version falls in the public 4.14.x - 6.19.12 affected window; vendor backport status must be checked"
fi
if has_changelog_fix_hint; then
ok "vendor changelog contains a Copy Fail/CVE/fix-commit hint"
FIX_HINT="yes"
else
warn "no local vendor changelog hint for CVE-2026-31431 or a664bf3d603d was found"
FIX_HINT="no"
fi
CRYPTO_USER_API="$(config_val CONFIG_CRYPTO_USER_API)"
CRYPTO_USER_API_AEAD="$(config_val CONFIG_CRYPTO_USER_API_AEAD)"
CRYPTO_AUTHENC="$(config_val CONFIG_CRYPTO_AUTHENC)"
CRYPTO_AUTHENC_ESN="$(config_val CONFIG_CRYPTO_AUTHENC_ESN)"
echo"[+] CONFIG_CRYPTO_USER_API=$CRYPTO_USER_API"
echo"[+] CONFIG_CRYPTO_USER_API_AEAD=$CRYPTO_USER_API_AEAD"
echo"[+] CONFIG_CRYPTO_AUTHENC=$CRYPTO_AUTHENC"
echo"[+] CONFIG_CRYPTO_AUTHENC_ESN=$CRYPTO_AUTHENC_ESN"
MODE="unknown"
if grep -q -E '^algif_aead[[:space:]]' /proc/modules 2>/dev/null; then
bad "algif_aead is currently loaded as a module"
MODE="module-loaded"
elif [ "$CRYPTO_USER_API_AEAD" = "y" ]; then
bad "CONFIG_CRYPTO_USER_API_AEAD=y; AF_ALG AEAD support is built into the kernel"
MODE="builtin"
elif [ "$CRYPTO_USER_API_AEAD" = "m" ]; then
warn "CONFIG_CRYPTO_USER_API_AEAD=m; algif_aead may be auto-loadable if not blocked"
MODE="module-available"
elif [ "$CRYPTO_USER_API_AEAD" = "n" ]; then
ok "CONFIG_CRYPTO_USER_API_AEAD is disabled"
MODE="disabled"
else
warn "CONFIG_CRYPTO_USER_API_AEAD status is unknown"
fi
if [ "$CRYPTO_AUTHENC" = "n" ] && [ "$CRYPTO_AUTHENC_ESN" = "n" ]; then
ok "authenc/authencesn template appears disabled in config"
elif [ "$CRYPTO_AUTHENC" = "y" ] || [ "$CRYPTO_AUTHENC" = "m" ] || [ "$CRYPTO_AUTHENC_ESN" = "y" ] || [ "$CRYPTO_AUTHENC_ESN" = "m" ]; then
warn "authenc/authencesn related crypto templates appear available"
else
warn "authenc/authencesn config status is unknown"
fi
if [ -r /proc/crypto ]; then
if grep -qiE 'authencesn|algif_aead|gcm\(aes\)' /proc/crypto; then
warn "/proc/crypto shows AEAD/authencesn-related crypto entries"
else
ok "/proc/crypto does not show tested AEAD/authencesn entries"
fi
else
warn "/proc/crypto is not readable from this context"
fi
PROBE_RESULT="$(af_alg_probe)"
echo"[+] AF_ALG probe: $PROBE_RESULT"
case"$PROBE_RESULT"in
open:*authencesn*)
bad "AF_ALG AEAD and authencesn are bindable from this context"
;;
open:*)
warn "AF_ALG AEAD is bindable from this context; authencesn was not confirmed by the probe"
;;
blocked:*)
ok "AF_ALG appears blocked from this context by permissions/seccomp/LSM or missing socket family"
;;
unbound:*)
warn "AF_ALG socket exists but tested AEAD templates did not bind"
;;
unknown:*)
warn "AF_ALG runtime probe could not run"
;;
esac
MODPROBE_FILES="$(grep -RslE 'algif_aead|af_alg' /etc/modprobe.d 2>/dev/null || true)"
if [ -n "$MODPROBE_FILES" ]; then
echo"[+] modprobe rules mentioning algif_aead/af_alg:"
printf"%s\n""$MODPROBE_FILES" | sed 's/^/ /'
if [ "$MODE" = "builtin" ]; then
bad "modprobe rules cannot disable built-in algif_aead support"
elif [ "$MODE" = "module-loaded" ]; then
warn "modprobe rules exist, but algif_aead is already loaded; unload/reboot and verify"
else
ok "module-blocking rules exist; verify they match the actual module-loading path"
fi
fi
if [ -f /.dockerenv ] || grep -q -E 'docker|kubepods|containerd|libpod' /proc/1/cgroup 2>/dev/null; then
warn "container environment detected; host kernel patch status is what matters"
note "block AF_ALG in container seccomp/runtime policy if the workload does not need it"
fi
if [ "$RISK" -eq 2 ]; then
echo -e "${RED}[RESULT] HIGH exposure: patch immediately or block AF_ALG/algif_aead where possible.${NC}"
elif [ "$RISK" -eq 1 ]; then
echo -e "${YELLOW}[RESULT] NEEDS REVIEW: exposure indicators exist; confirm vendor backport and runtime policy.${NC}"
else
echo -e "${GREEN}[RESULT] LOW exposure from this local check. Still confirm vendor advisory for final status.${NC}"
fi
echo"==[ check complete ]=="
没有本地脚本能 100% 代替发行版安全公告和补丁回补确认
十、最后
Copy Fail 值得记住的不是“4 字节也能提权”这个标题,而是它背后的边界错位。
authencesn 以为自己在写输出缓冲区。
algif_aead 以为自己只是做了一次 in-place 性能优化。
splice() 以为自己只是把 page cache 引用零拷贝交给下游。
三个机制各自都能解释得通,但拼在一起,dst 的身份变了。
最终结果就是:一个普通用户可以通过内核 crypto API,把 4 字节写到文件 page cache 上。
这类漏洞的教训很直接: 内核里的性能优化,最怕的不是少一个边界检查,而是改变了对象的语义;page cache 这层也不能只按“读缓存”理解,它在执行路径里就是实际被消费的数据。
所以修复要回到边界本身:
src 是 src,dst 是 dst。别把 page cache 放进可写输出链。
参考资料
- Copy Fail:https://copy.fail/
- shadowabi PoC:https://github.com/shadowabi/CVE-2026-31431-CopyFail-Universal-LPE
- Linux 主线修复:
a664bf3d603d - 致因提交:
72548b093ee3
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:CKCsec安全研究院 ckcsec ckcsec《4 个字节,9 年技术债:CVE-2026-31431 / Copy Fail 完整拆解》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论