CVE_2026_31431漏洞复现与分析纪实
缘起
上周四早上通勤时,我瞥了一眼“兰舍”微信群,有兰友提到Linux的提权漏洞(如下图),当时并未太在意。
但到了下午,看雪安全公众号推送了一篇相关文章:《732字节,通杀所有Linux!一个潜伏十年的“隐形杀手”终曝光》,这个推送瞬间勾起了我的兴趣。
接下来正好五一假期有空,我便决定深入研究一番。
所谓通杀,其实不然
看雪的文章出于安全考虑没有直接给出相关链接,这时回想起兰舍群里曾有人发过GitHub及xint的链接,于是认真翻看起来(顺便也练练英文),
附仓库(https://github.com/rootsecdev/cve_2026_31431)截图如下
。
看到有PoC脚本,我便想亲自试一下。用什么环境呢?我手边正好有一台“幽兰本”(格蠹3588 Linux笔记本),尝试运行检测脚本,结果如下:
并没有成功,这与“通杀所有Linux!一个潜伏十年的漏洞”的结论不太相符啊。
再试云主机
于是我又想到用云主机试试,但这次栽在了Python版本上——该漏洞的利用脚本要求Python 3.10及以上版本,我云主机上的是3.6.8。
python3 test_cve_2026_31431.pyTraceback (most recent call last): File "test_cve_2026_31431.py", line 65, in <module> def attempt_trigger(target_path: str) -> tuple[bool, bytes]:TypeError: 'type' object is not subscriptablepython3 --versionPython 3.6.8sudo yum install python310Loaded plugins: fastestmirrorLoading mirror speeds from cached hostfilebase | 3.6 kB 00:00:00extras | 2.9 kB 00:00:00updates | 2.9 kB 00:00:00No package python310 available.Error: Nothing to do反复尝试通过yum安装Python 3.10均告失败,真是考验耐心(欲速则不达)。求助AI后,找到了一种源码编译安装的方法:
wget https://www.python.org/ftp/python/3.10.16/Python-3.10.16.tgztar -xzf Python-3.10.16.tgzcd Python-3.10.16./configure --enable-optimizations --prefix=/usr/localmake -j $(nproc)sudo make altinstall然而又遇到了编译错误:
反复询问AI,对方给出了一堆安装选项,但都不在点上。后来终于发现关键是要禁用链接优化(--disable-lto):
#如果使用yum (RHEL 7)
sudo yum groupinstall "Development Tools"
sudo yum install gcc openssl-devel bzip2-devel libffi-devel zlib-devel readline-devel sqlite-devel tk-devel
最终瞄到了关键点是禁用链接优化(--disable-lto):
./configure --prefix=/usr/local --disable-optimizations --disable-lto
加上--disable-lto后,Python 3.10终于编译成功。但此时已经过去了好几个小时,兴趣难免有所消退。
在云主机(内核版本5.18.1)上再次尝试检测脚本,结果显示确实存在漏洞:
# python3.10 test_cve_2026_31431.py[*] CVE-2026-31431 detector kernel=5.18.1 arch=x86_64[i] Kernel 5.18.1 predates the affected 6.12/6.17/6.18 lines; trigger may not apply even if prerequisites match.[+] AF_ALG + 'authencesn(hmac(sha256),cbc(aes))' loadable - precondition met.[!] VULNERABLE to CVE-2026-31431.[!] Marker b'PWND' (AAD seqno_lo) landed in the spliced page-cache page at offset 0.[!] Surrounding bytes: 50574e444641494c2d53454e (b'PWNDFAIL-SEN')[!] Apply the upstream fix or block algif_aead immediately.接着运行利用脚本(注意必须带上 --shell 参数,否则看不到效果): [steve@aiyun17735 cve_2026_31431]$ python3.10 exploit_cve_2026_31431.py --shell[*] CVE-2026-31431 LPE user=steve uid=1000[*] /etc/passwd: steve UID field at offset 1188 = '1000'[*] Patching '1000' -> '0000' in page cache...[*] Page cache now reads b'0000' at offset 1188[*] getpwnam('steve').pw_uid = 0[+] /etc/passwd page cache now lists steve as UID 0.[+] Run: su steve[+] Enter your own password. su will setuid(0) and drop a root shell.[i] Cleanup after testing (from the root shell):[i] echo 3 > /proc/sys/vm/drop_caches[+] Executing `su steve` now...Password:[root@aiyun177350 cve_2026_31431]#命令提示符变成了root,提权成功!几小时的折腾总算没有白费。
举一反三
这时我又想起幽兰本上尚未成功,于是在兰舍群里发了一条消息询问群友:
有兰友说内核老,其实不对,这个漏洞号称隐藏十年了啊。我继续与AI聊天,向AI提出了一串问题,让AI给我解答疑惑:
1.该漏洞对云服务器有影响吗?如果云服务器是容器环境呢?
2.Android手机手机受影响吗?
3.algif_aead是什么,是驱动吗?
4.如何判断是否在容器中?
5.云主机一般是什么环境?
6.这篇文章中提及的Copy Fail是什么术语:https://xint.io/blog/copy-fail-linux-distributions
7.”Copy Fail“这个词背景及来源
其中第3个问题的回答顺带解决了幽兰本不成功的原因::
我在幽兰本上检索编译选项CRYPTO_USER_API_AEAD,确实没有设置(is not set):
看来幽兰本上没有这个漏洞的原因是没有把有漏洞的代码编译进来。
漏洞原理仍难于理解
此时,我对漏洞的原理还是模糊的,下面这样的概括,你能看懂吗(截取自https://github.com/rootsecdev/cve_2026_31431)?
Vulnerability summary
algif_aead runs AEAD operations in-place (req->src == req->dst). When the source data is fed in via splice() from a regular file, the destination scatterlist contains references to the file's page-cache pages — i.e. the kernel will write into them. The authencesn(hmac(sha256), cbc(aes)) algorithm then performs a 4-byte "scratch" write of the AAD's seqno_lo field (bytes 4–7 of the sendmsg-supplied AAD) into that destination, corrupting the page-cache copy of the file.
Because the on-disk file is never modified, there is no on-disk signature; the corruption is observed only by readers that share the page cache. /etc/passwd and /usr/bin/su are both world-readable, so an unprivileged local user can corrupt the running kernel's view of either.
Affected: kernels carrying commit 72548b093ee3 (in-place AEAD, 2017) without the upstream revert. The disclosure confirmed Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3, and SUSE 16, but the underlying primitive predates that range.
像scatterlist,the file's page-cache pages和splice这术语超出我的知识,splice是个关键的概念,看雪有篇文章(https://mp.weixin.qq.com/s/HO_kS4OSIMFAMdvNKxxFqA)提供了中文解释。
把漏洞代码编译为内核模块
我深知纸上得来还是不深刻(古人云:纸上得来终觉浅,绝知此事要躬行),我想在幽兰或者gdk8上调试。虽然漏洞代码没有编译进内核,但可以手工编译啊。
我在crypto/Makefile下找到了对应源码algif_aead.oóalgif_aead.c(后来发现在gdk8上,还要另一个模块af_alg.oóaf_alg.c),如下图:
整一个Makefile文件(我参考的是llaolao驱动),内容如下:
WFLAGS := -Wstrict-prototypes -Wno-trigraphs -Wunused-resultLDFLAGS = -Map /var/tmp/algif_aead.txtEXTRA_CFLAGS := $(WFLAGS)# EXTRA_CFLAGS += -g -Wa,-adhln=$(<:.c=.lst)EXTRA_CFLAGS += -D_DEBUG -g3-fno-stack-protectorMODULE = algif_aeadMODULE2 = af_algKERNELDIR?=/lib/modules/$(shell uname -r)/buildobj-m := $(MODULE).oobj-m += $(MODULE2).oall:make -C $(KERNELDIR) M=$(shell pwd) modulesmake $(MODULE).lst $(MODULE2).lst#$(MODULE)-objs := $(MODULE).o%.lst:%.koobjdump --source --line-numbers --all-headers --demangle --disassemble $^ > $@clean:rm -rf *.o *~ .*.cmd *.ko *.mod *.mod.c *.order *.symvers .tmp_versions built-in.o如果要为gdk8编译ko(我是在幽兰本上为其编译),则需要export KERNELDIR目录,我编写一个mybuildgdk8.sh,内容如下:
export KERNELDIR=/gewu/home/geduer/gedulab/gdk8ktime bear --output compile_commands.json -- make KBUILD_VERBOSE=2 V=1 quiet="" -j1 ARCH=arm64编译成功后,通过insmod ./algif_aead.ko加载,试了下,不出意外exploit是成功的。
gdk8上的python是3.6.9,我还是把python3.10源码编译了一番(注意加上--disable-lto)。
在gdk8上,insmod ./algif_aead.ko和insmod ./af_alg.ko, 执行python3.10 exploit_cve_2026_31431.py –shell竟然出现异外情况,输出su: Cannot determine your user name.
cat /etc/passwd可以看到geduer的UID由1002变成了0000了,至少内存是被改掉的,那可能su版本有差异。
使用挥码枪调试
这里我选择gdk8作为调试目标,当然选择幽兰本也可以,选择gdk8主要它更轻便些,幽兰用来构建驱动或者看文档。
操作步骤:
1.按上面左图连好挥码枪与gdk8盒子,打开nanocode软件进入调内核调试并设置好符号路径等
2.对文档中提及的函数下断点bp crypto_authenc_esn_decrypt
3.ssh连接上gdk8,输入python3.10 test_cve_2026_31431.py,此时下图内容输出了一半停下
4.此时nanocode断点命中,敲入kn可以看到调用栈,如下图
5.继续敲r及dt lk!aead_request 0xffffffc0e30fd300
6.可以看到src与dst如文档所说是同一个,类型为struct scatterlist
7.继续观察dt lk!scatterlist 0xffffffc0`e30fd010
这里page_link字段是多用途字段,它可以指向另一个scatterlist(如bit0置1),注意dst(0xffffffc0`e30fd010)指向数组,通过SG_END标记它是最后一个,当前bit1没有置1,显然它不是最后一个。
#define SG_CHAIN 0x01UL#define SG_END 0x02UL/** We overload the LSB of the page pointer to indicate whether it's* a valid sg entry, or whether it points to the start of a new scatterlist.* Those low bits are there for everyone! (thanks mason :-)*/#define sg_is_chain(sg) ((sg)->page_link & SG_CHAIN)#define sg_is_last(sg) ((sg)->page_link & SG_END)#define sg_chain_ptr(sg) \ ((struct scatterlist *) ((sg)->page_link & ~(SG_CHAIN | SG_END)))8.继续观察dt lk!scatterlist 0xffffffc0`e30fd010 +0x20(为sizeof(lk!scatterlist))第二个scatterlist
9.继续下断bp lk!scatterwalk_map_and_copy,跳过前3次的下断,直接看改写内存那个断点(即这行scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);)
10.继续下断lk!scatterwalk_copychunks并g,断下我们看到PWND字串,这个要写到目标缓存的内容
11.我们继续对memcpy_dir下断,可惜它是内联函数,我们打开汇编窗口找到lk__memcpy这行,bp/1 ffffff800862442c下断
12.我们观察改写的即将发生,
x1指向4字节将改写x0指向内存。(x0指向的就是文件缓存地址)
13.接下来是关键的splice操作,另起一章吧。
splice系统调用
splice()是Linux内核提供的一个高效的零拷贝系统调用,其核心价值在于在内核空间直接移动数据,避免数据在“内核缓冲区”与“用户空间缓冲区”之间不必要的来回拷贝。但它有一个严格的使用限制:两个文件描述符中至少有一个必须是管道(pipe)。
函数原型:
ssize_t splice(int fd_in, off_t *off_in, int fd_out, off_t *off_out, size_t len, unsigned int flags);调用成功时返回实际传输的字节数,失败则返回-1并设置errno。参数看似复杂,但存在固定的配对关系,可以分三组理解:
1.基础I/O与长度控制
int fd_in & int fd_out:数据流入和流出的文件描述符(文件、套接字、设备或管道的句柄)。
size_t len:想要移动的最大字节数。
2.偏移量操作
| 文件描述符类型 | off_in/off_out 参数值 | 具体行为 |
| 管道 | 必须为 NULL | 从管道的当前读写位置操作,管道不支持随机访问。 |
| 非管道 | NULL | 使用文件内核维护的当前文件偏移量(操作后自动更新)。 |
| 非管道 | 非 NULL | 使用指针指向的值作为起始偏移量,但不更新文件本身的文件偏移量,实现独立的偏移量控制。 |
3. 行为控制标志
SPLICE_F_MOVE:性能优化提示,指示内核尽可能通过移动内存页面取代复制数据(从Linux 2.6.21起该标志暂时是空操作,但传递它合法)。
SPLICE_F_NONBLOCK:使splice的管道操作非阻塞(注意:若fd_in或fd_out本身没有O_NONBLOCK标志,整个调用仍可能阻塞)。
SPLICE_F_MORE:性能优化提示,常用于网络传输,建议内核后续还有更多数据到来。若fd_out是套接字,则与send(2)中的MSG_MORE标志效果类似,有助于减少网络数据包。
Python对splice封装:
os.splice(src, dst, count, offset_src=None, offset_dst=None, flags=0)
scatterList布局图
splice系统调用就像一个魔法,把文件缓存引用给传递了下去,在内核端实际是数组scatterlist dst[],有两个元素,AAD占一个元素,剩下的占另一元素。
结语
找资料,看代码,上调试器,五天假期忙得不亦乐乎,感觉时间过得好快,明天又到上班的时间了(时间又不属于自己了)。今天把几天忙碌的过程写下来,作为自己学习的记录,也分享给格友和同行们,写的不好的地方,欢迎大家批评指正。这个五一假期过的好充实。
参考资料:
https://github.com/rootsecdev/cve_2026_31431
https://xint.io/blog/copy-fail-linux-distributions
https://mp.weixin.qq.com/s/HO_kS4OSIMFAMdvNKxxFqA
***
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以文章和有声读物
也欢迎关注格友公众号
