缘起上周四早上通勤时我瞥了一眼“兰舍”微信群有兰友提到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 Toolssudo 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 kernel5.18.1 archx86_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 bPWND (AAD seqno_lo) landed in the spliced page-cache page at offset 0.[!] Surrounding bytes: 50574e444641494c2d53454e (bPWNDFAIL-SEN)[!] Apply the upstream fix or block algif_aead immediately.接着运行利用脚本注意必须带上 --shell 参数否则看不到效果 [steveaiyun17735 cve_2026_31431]$ python3.10 exploit_cve_2026_31431.py --shell[*] CVE-2026-31431 LPE usersteve uid1000[*] /etc/passwd: steve UID field at offset 1188 1000[*] Patching 1000 - 0000 in page cache...[*] Page cache now reads b0000 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:[rootaiyun177350 cve_2026_31431]#命令提示符变成了root提权成功几小时的折腾总算没有白费。举一反三这时我又想起幽兰本上尚未成功于是在兰舍群里发了一条消息询问群友有兰友说内核老其实不对这个漏洞号称隐藏十年了啊。我继续与AI聊天向AI提出了一串问题让AI给我解答疑惑1该漏洞对云服务器有影响吗如果云服务器是容器环境呢2Android手机手机受影响吗?3algif_aead是什么是驱动吗?4如何判断是否在容器中?5云主机一般是什么环境?6这篇文章中提及的Copy Fail是什么术语https://xint.io/blog/copy-fail-linux-distributions7”Copy Fail“这个词背景及来源其中第3个问题的回答顺带解决了幽兰本不成功的原因我在幽兰本上检索编译选项CRYPTO_USER_API_AEAD确实没有设置(is not set):看来幽兰本上没有这个漏洞的原因是没有把有漏洞的代码编译进来。漏洞原理仍难于理解此时我对漏洞的原理还是模糊的下面这样的概括你能看懂吗截取自https://github.com/rootsecdev/cve_2026_31431Vulnerability summaryalgif_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 files 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 AADs 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 kernels 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.像scatterlistthe files 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_VERBOSE2 V1 quiet -j1 ARCHarm64编译成功后通过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_decrypt3.ssh连接上gdk8,输入python3.10 test_cve_2026_31431.py此时下图内容输出了一半停下4.此时nanocode断点命中敲入kn可以看到调用栈如下图5.继续敲r及dt lk!aead_request 0xffffffc0e30fd3006.可以看到src与dst如文档所说是同一个类型为struct scatterlist7.继续观察dt lk!scatterlist 0xffffffc0e30fd010这里page_link字段是多用途字段它可以指向另一个scatterlist(如bit0置1)注意dst(0xffffffc0e30fd010)指向数组通过SG_END标记它是最后一个当前bit1没有置1显然它不是最后一个。#define SG_CHAIN 0x01UL#define SG_END 0x02UL/** We overload the LSB of the page pointer to indicate whether its* 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 0xffffffc0e30fd010 0x20(为sizeof(lk!scatterlist))第二个scatterlist9.继续下断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_srcNone, offset_dstNone, flags0)scatterList布局图splice系统调用就像一个魔法把文件缓存引用给传递了下去在内核端实际是数组scatterlist dst[]有两个元素AAD占一个元素剩下的占另一元素。结语找资料看代码上调试器五天假期忙得不亦乐乎感觉时间过得好快明天又到上班的时间了时间又不属于自己了。今天把几天忙碌的过程写下来作为自己学习的记录也分享给格友和同行们写的不好的地方欢迎大家批评指正。这个五一假期过的好充实。参考资料https://github.com/rootsecdev/cve_2026_31431https://xint.io/blog/copy-fail-linux-distributionshttps://mp.weixin.qq.com/s/HO_kS4OSIMFAMdvNKxxFqA***正心诚意格物致知以人文情怀审视软件以软件技术改变人生扫描下方二维码或者在微信中搜索“盛格塾”小程序可以文章和有声读物也欢迎关注格友公众号