
Proof-of-concept-Exploit und detaillierte Analyse der Linux-Kernel-Privilege-Escalation-Schwachstelle CVE-2022-0847 (Dirty Pipe), einschließlich einer Docker-Umgebung zum Testen und Debuggen.
[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.