
このケーススタディは、ECE 9069: Introduction to Hacking の課題の成果物です: https://whisperlab.org/introduction-to-hacking/
CVE-2022-0185 は、Linux カーネルの Filesystem Context 機能の legacy_parse_param 関数が、指定されたパラメータの長さを検証する方法に存在するヒープベースのバッファオーバーフローの脆弱性です。特権のない(非特権ユーザーネームスペースが有効な場合。それ以外の場合は、ネームスペース内の CAP_SYS_ADMIN 権限が必要)ローカルユーザーが、Filesystem Context API をサポートしておらず(したがってレガシー処理にフォールバックする)ファイルシステムを開くことができる場合、この欠陥を利用してシステム上の権限を昇格させることができました。[1]
この脆弱性が報告された後、このバグを修正するパッチがリリースされました:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=722d94847de2
https://ubuntu.com/security/CVE-2022-0185#impact-score
発見者による詳細な解説があります: https://www.hackthebox.com/blog/CVE-2022-0185:_A_case_study
このリポジトリでは、この脆弱性を再現するための基本的な手順と関連する背景情報を説明します。また、不明な点があれば、[email protected] までメールを送ってください。喜んでお答えします。
CVE-2022-0185 の脆弱性は、02/11/2022 に公開され、CVSS 3.x の基本スコアは 8.4(高)です。[1] この脆弱性は、符号なし整数のアンダーフローによって引き起こされるヒープベースのバッファオーバーフローです。
この脆弱性は Linux v5.1 カーネルで導入され、5.1 より高いカーネルバージョンを搭載するすべての Linux ディストリビューションに影響を与えました。例えば、Ubuntu 20.04 LTS(focal)はこのバグに対して脆弱でした。ただし、パッチはリリースされており、バージョン 5.4.0-96.109 以降で利用可能です。[3]
この脆弱性を悪用すると、特権のないローカルユーザーがシステム上の権限を昇格させ、システム全体を危険にさらす可能性があります。[1] [2] CVSS スコアの詳細な分析は次のとおりです: 基本スコア: 8.4。これは、即時の対応が必要な重大なセキュリティリスクを示しています。 影響スコア: 5.9。悪用された場合、かなりの潜在的な被害が想定されます。高い機密性、完全性、可用性の値がこのスコアに寄与しています。 悪用可能性スコア: 2.5。比較的高い悪用可能性を示しています。ローカルの攻撃ベクトル、高い完全性、高い可用性の値がこのスコアに寄与しています。
表 1.1 と表 1.2 は、これらのスコアとその構成要素に関する詳細情報を示しています。
| CVSS v3.1 深刻度 | 値 |
|---|---|
| 基本スコア | 8.4 高 |
| 影響スコア | 5.9 |
| 悪用可能性スコア | 2.5 |
表 1.1 CVSS 深刻度スコア[1]
表 1.2 CVSS ベクター[1]
現代のコンピュータには、符号付きと符号なしの2種類の整数型があります。符号付き数の表現には、一般に2の補数と呼ばれる操作が関与します。[4] 「2の補数は、最上位の桁の2進数ビットを符号として使用し、2進数が正であるか負であるかを示します」[4]
2の補数を導入することで、減算の計算を加算に変換できるため、CPUの設計と実装が簡素化されます。整数の2の補数を生成するには、次の3つのステップがあります:[4]
図 2.1.1.1 は、「-6」を2の補数形式に変換する実際の例を図で示しています。

図 2.1.1.2 2の補数を用いた加算
図 2.1.1.2 は、'-6' の2の補数を '+6' に加算するプロセスを示しています。これは、2の補数を使用することで加算を減算の代用として使用できることを示しています。

図 2.1.1.2 2の補数を用いた加算
セクション 2.1.1 で、2の補数が何であるかを理解しました。次に、コンピュータにおける符号なし整数のアンダーフローのシナリオを見てみましょう。現代のコンピュータでは、符号なし数を使用する場合、最上位ビットは符号ビットとして扱われず、符号なし数自体の一部となります。つまり、符号なし数で減算を実行するときは注意が必要です。符号なし整数のアンダーフローとして知られる状態につながる可能性があるためです。[5]
図 2.1.2.1 は、8ビットの符号なし数で5から6を減算する状況を示しています。符号なし数がラップアラウンドするため、最終結果は255になります。このアンダーフローが条件文内で発生すると、その文の機能を破壊する可能性があります。

図 2.1.2.1 符号なし整数のアンダーフロー
Linux カーネルでは、Slab アロケータは小さなメモリチャンクを効率的に割り当ておよび解放するためのメモリ管理メカニズムです。それぞれが固定サイズのメモリブロックを含む複数のスラブキャッシュを維持することで、パフォーマンスを提供します。通常、kmalloc-32 は32バイトのメモリを割り当てるため kmalloc-32 スラブであり、一方 kmalloc-4k は4096バイトのメモリを割り当てるため kmalloc-4k スラブです。[6]
さらに、Linux カーネルのスラブ割り当ては、通常、カーネルのヒープメモリ領域内の連続したアドレス空間からメモリを割り当てることを伴います。この連続したアドレスはカーネルによって管理され、さまざまなカーネルオブジェクトやデータ構造にメモリを割り当てるために使用されます。図 2.2.1.1 は、Linux カーネルメモリ内のスラブのレイアウトを示しています。

図 2.2.1.1 Linux の Slab アロケータ [7] (この図の作者は https://leviathan.vip/ です)
自分でコンパイルした Linux カーネルでこのプロセスを再現したい場合は、以下のマークダウンファイルを読んで背景情報を入手してください:
注意:
このリポジトリのマークダウンファイル、コード、スクリプトはすべて異なるフォルダにあります。各フォルダには専用のマークダウンファイルがあるので、何かを試す前にそれを読んでください!
セクション 2.1 では、符号なしアンダーフローの仕組みを説明しました。次に、この脆弱性を含むカーネル関数を調べます。
ユーザー「clubby789」は、カーネル関数 legacy_parse_param に脆弱性を発見しました。この関数は主に、カーネルに渡されるパラメータの解析を担当します。CVE-2022-0185 では、fsopen を使用してファイル記述子を開いた後、fsconfig 関数を使用して設定のキーと値のペアをカーネルに渡すことで、この関数が呼び出されました。legacy_parse_param の簡略化されたバージョンを次のコードに示します。[2]```c static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param) { struct legacy_fs_context *ctx = fc->fs_private; // [1] unsigned int size = ctx->data_size; // [2] size_t len = 0; int ret; [ ... ] switch (param->type) { case fs_value_is_string: len = 1 + param->size; // [3] case fs_value_is_flag: len += strlen(param->key); break; default: return invalf(fc, "VFS: Legacy: Parameter type for '%s' not supported", param->key); } if (len > PAGE_SIZE-2-size) return invalf(fc, "VFS: Legacy: Cumulative options too large"); // [4] [ ... ] if (!ctx->legacy_data) { ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL); // [5] if (!ctx->legacy_data) return -ENOMEM; } ctx->legacy_data[size++] = ','; // [6] len = strlen(param->key); memcpy(ctx->legacy_data + size, param->key, len); size += len; if (param->type == fs_value_is_string) { ctx->legacy_data[size++] = '='; memcpy(ctx->legacy_data + size, param->string, param->size); size += param->size; } ctx->legacy_data[size] = '\0'; ctx->data_size = size; ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS; return 0; }
上記のコードスニペットから、行 [1] と [2] がコードのコンテキストを設定していることがわかります。一方、行 [4] には、符号なしアンダーフローが発生するステートメントが含まれています。行 [5] はヒープスラブの割り当てを処理し、行 [6] と [7] は割り当てられたスラブへのデータの投入を担当します。特に、行 [6] では区切り文字としてカンマ (',') が追加され、等号 ('=') も追加されるため、実際のデータサイズより2バイト多く消費されます。
行 [4] では、if 文内の変数に PAGE_SIZE (4096 に設定されたマクロ) と size (符号なし 64 ビット数) が含まれています。符号なし数値が 4095 まで累積するとアンダーフローが発生し、if 文が常に false と評価されるようになります。これにより、隣接するスラブへの領域外書き込みが可能になります。このアンダーフローは、符号なし数値の減算によって引き起こされ、4096 - 4095 の結果として、符号なし数値 18446744073709551615 が得られます。[2]
`18446744073709551615` の結果については、以下の本文で説明されています。```c
if (len > PAGE_SIZE-2-size) return invalf(fc, "VFS: Legacy: Cumulative options too large");
ここで、PAGE_SIZE は 4096 バイトに等しく、2 は各 key-value ペアを区切るために追加される文字 , と = に相当します。
問題は、size が 符号なし整数値 であることです。したがって、size が 4095 に達すると、PAGE_SIZE-2-size という式は singned value: -1 に等しくなります。しかし、符号なし整数値: 18446744073709551615 として扱うと、次の図に示すように2の補数表現のため、この値になります! [3]

したがって、上記の if 文は常に false になります。つまり、残りのデータは、割り当てたスラブを超えてヒープにコピーされることになります!
この符号なし整数のアンダーフローがどのように発生するかを理解したら、脆弱性を実証するための概念実証 (POC) コードの作成に進むことができます。
ユーザー “clubby789” が、以下のコードスニペットに示す詳細な POC コードを提供しています。このコードは簡潔です。まず ext4 というファイルディスクリプタを開き、次に fsconfig を複数回使用してカーネルにデータを投入します。
ここで注意すべき点は2つあります:
セクション3.1.1で述べたように、境界外書き込みを観測する前に4095バイトのデータを投入する必要があります。各サイクルで35バイトしか投入されないため、ヒープメモリを観測して概念実証を完了するには、この操作を117回(4095 / 35)実行する必要があります。
### 3.1.3 QEMU を使用した POC
このリポジトリ: GitHub - dcheng69/CVE-2022-0185-Case-Study では、デバッグプロセスを容易にするためのシェルスクリプト poc.sh を提供しています。セットアップを開始する前に、Poc フォルダ内の Markdown ファイルを読んでください。
legacy_parse_param はカーネル関数であるため、カーネル関数のデバッグが必要になります。これを行うには、必要なシンボルとソースコードを取得するためにカーネルソースをコンパイルする必要があります。このプロセスを案内する詳細な Markdown ファイルも提供しています。詳細については、Compile_linux フォルダを参照してください。
図3.1.3.1では、カーネルヒープに4095バイトのデータを投入した後、符号なしアンダーフローを悪用して境界外書き込みを正常にトリガーできることを示しました。さらに、合計4130バイトのデータを kmalloc-4k スラブに投入し、隣接するスラブの破壊に成功しました。この例では隣接するスラブに情報が含まれていませんが(すべてゼロ)、コードを慎重に構築することで、この機能を悪用して悪意のあるデータを書き込むことができます。これを実現する方法については、セクション3.2 Exploit で説明します。

**図 3.1.3.1 QEMU を使用した POC**
詳細については、このリポジトリの `Poc` フォルダにある https://github.com/dcheng69/CVE-2022-0185-Case-Study/blob/main/Poc/poc.md を参照してください。
## 3.2 エクスプロイト
この脆弱性を理解したら、それを利用してエクスプロイトを作成できます。詳細は `exploit-ubuntu` フォルダと、以下の Markdown ファイルに記載されています: https://github.com/dcheng69/CVE-2022-0185-Case-Study/blob/main/explot-ubuntu/exploit.md
簡単に言うと、Ubuntu のソースコードをコンパイルして deb ファイルを取得し、仮想マシン上でテストして特定のカーネルバージョンを特定します。次に、`System.map` ファイルから見つけた情報を使用して、エクスプロイトコードのオフセットをそのカーネルバージョンに合わせて修正します。最後に grub を更新して再起動し、エクスプロイトを実行します。
### 3.2.1 エクスプロイトの概要
ユーザー 'clubby789' が詳細なエクスプロイトコードを提供しています。まず概要を説明し、続いて図を使用していくつかの主要な概念を説明します。最後に、仮想マシン(VirtualBox)上で実行されている Ubuntu を使用したエクスプロイトの結果を示します。
この脆弱性の概念実証を示した後、実際にエクスプロイトを進めることができます。図3.2.1.1に、この脆弱性をエクスプロイトする方法の概要を示します。
- 左側のセクションは、Linux カーネルベースアドレスの取得に焦点を当てています。これは、符号なしアンダーフローを悪用して msg_msg 構造体の m_ts を上書きし、境界外読み取りを可能にして、事前にスプレーしたカーネル構造体にアクセスすることで実現されます。
- 右側のセクションは、root 権限の取得を目的としています。これは、符号なしアンダーフローを使用して msg_msg 構造体の next ポインタを上書きし、modprobe_path を指すようにすることで実現されます。次に、ページフォールトをトリガーして構築した FUSE コードを呼び出し、カーネル空間での任意書き込みを可能にします。

**図 3.2.1.1 エクスプロイトの概要**
### 3.2.2 Linux カーネルベースアドレスの取得
先ほど分析したように、このパートでは符号なしアンダーフローを悪用して msg_msg 構造体の m_ts フィールドを上書きし、境界外読み取りを可能にします。カーネルポインタを含む構造体でヒープをスプレーすることで、メモリリークを取得できる可能性があります。
struct msg_msg は、System V メッセージキューを実装するために使用される Linux カーネルのデータ構造です。このセクションでは、struct msg_msg の内部構造と、メッセージの送信・受信・割り当てに関連する関数のロジックに焦点を当てます。図3.2.2.1に示すように、これらが理解する必要のある関数です。
struct msg_msg は、System V メッセージキューを実装するために使用される Linux カーネルのデータ構造です。[8] この議論では、struct msg_msg の内部構造と、メッセージの送信・受信・割り当てに関連する関数のロジックに焦点を当てます。図3.2.2.1に示すように、これらが理解する必要がある関数です。
メッセージ送信の実装は msg.c ファイルにあり、メッセージの最大長は 8192 バイトと定義されています。alloc_msg 関数では、メッセージはその長さに基づいてセグメントに分割されます。メッセージの長さがメッセージヘッダーと合わせて 1 ページ(4096 バイト)を超える場合、メッセージはポインタで連結された複数のセグメントに格納されます。

**図 3.2.2.1 struct msg_msg の送信と受信**
図3.2.2.2から、struct msg_msg がメッセージヘッダーとして機能し、0x30 バイトのメモリを占めていることがわかります。メッセージに残りのデータがある場合、それはメッセージセグメントに格納され、struct msg_msgseg にリンクされます。したがって、カーネルが最大 8192 バイトのメッセージを許可する場合、データは最大 3 つのメッセージセグメントに格納されます。

**図 3.2.2.2 struct msg_msg**
図3.2.2.3では、struct msg_msg の構造を示しています。コードから、m_ts フィールドが境界外読み取りのために上書きする必要があるフィールドであることがわかります。(必要であれば、`res` フォルダに draw.io ソースファイルがあります。)

**図 3.2.2.2 struct msg_msg の構造**
struct msg_msg の構造を理解したところで、カーネルリークを取得する方法を学ぶ必要があります。Linux カーネルには、カーネルアドレス空間配置のランダム化(KASLR)機能があり、カーネルコードは起動フェーズで決定されたランダムなアドレスにロードされます。ただし、カーネルの開始点から任意の関数アドレスまでのオフセットは一定であるため、特定の操作を実行して、特定のカーネル関数を含む構造体でヒープ空間を埋めることができます。オフセットを減算することで、カーネル開始アドレスを見つけることができます。
幸いなことに、/proc/self/stat を開くことで、kmalloc-32 スラブに存在する seq_operations 構造体でヒープを簡単にスプレーできます。seq_operations の定義を図3.2.2.3に示します。

**図 3.2.2.3 カーネルリーク用の構造体**
最後に、全体的なプロセスを図3.2.2.4に示します。まず、上書きに備えて legacy_data に 4095 バイトのデータを投入します。次に、struct msg_msg を使用してメッセージを構築します。ヒープメモリは連続的に割り当てられるため、構築したメッセージは隣接する kmalloc-4k スラブに隣接する可能性が高くなります。legacy_data に書き込むデータを制御することで、m_ts フィールドを上書きします。
次に、複数の kmalloc-32 seq_operations 構造体でヒープをスプレーします。その後、メッセージキューからデータを受信すると、境界外読み取りがトリガーされます。カーネル関数からのオフセットを調整することで、カーネルベースアドレスを取得できます。

**図 3.2.2.3 境界外読み取りの概要**
### 3.2.3 root 権限の取得
先ほどの分析と同様に、このパートでは FUSE ファイルシステムをセットアップする必要があります。これにより、ユーザー空間のコードがカーネル空間からのページフォールトを処理できるようになります。同時に、msg_msgseg *next ポインタを上書きして modprobe_path を指すようにし、カーネル空間での任意書き込みを可能にします。
まず、図3.2.3.1に示す FUSE コールスタックを見てみましょう。一般に、FUSE を使用すると、ユーザー空間にファイルシステムを実装できます。新しい操作が発生すると、システムは FUSE 用に定義したコードを呼び出します。

**図 3.2.3.1 FUSE の概要**
FUSE を使用してカーネル空間で任意書き込みを実現する方法を分析しましょう。まず、struct msg_msg のメッセージ送信操作を考えます。この操作では、データをカーネル空間に書き込みます。メッセージが 2 つのセグメントを持つように構成すると、ポインタを上書きして任意のカーネルアドレスに書き込むことができます。この概念を図3.2.3.2に示します。
さらに、キューへのメッセージ送信のロジックを調べると、このプロセスがユーザー空間からカーネル空間へのバッファのコピーを伴うことがわかります。メッセージが十分に長い場合、セグメントごとにコピーされます。このプロセス中にページフォールトをトリガーできれば、図3.2.3.2に示すシナリオを実現できます。
幸いなことに、FUSE は必要な機能を提供します。ページを FUSE にマップでき、ページフォールトがトリガーされると、システムは FUSE の read 関数を呼び出してページフォールトを処理します。符号なしアンダーフローが msg_msg_seg *next ポインタを上書きするまで読み取りプロセスを一時停止し、その後メッセージ送信プロセスを再開することで、任意書き込みを実現できます。プロセス全体を図3.2.3.3に示します。

**図 3.2.3.2 メッセージ送信の使用**

**図 3.2.3.3 FUSE とメッセージ送信**
### 3.2.4 VirtualBox を使用したエクスプロイト
以下の Markdown ファイルに従ってください: https://github.com/dcheng69/CVE-2022-0185-Case-Study/blob/main/explot-ubuntu/exploit.md
コンパイルが完了すると、図3.2.4.1でエクスプロイトの結果を確認できます。

**図 3.2.4.1 VirtualBox でのエクスプロイト**
# 4. 脆弱性の緩和策
## 4.1 公式パッチ
この脆弱性が報告された後、Linux とその多くのディストリビューションはこのバグを修正するパッチを統合しました。[9] [10] [11]
次のスクリーンショットに、Linus Torvalds が統合したパッチを示します。このアンダーフロー問題の緩和策は、減算演算を加算に変換するだけです。

# 5. 現実世界への影響
このレポートでは、この脆弱性を悪用して Ubuntu システムを侵害する方法を実証します。さらに、定期的なセキュリティ更新が行われていない古いシステムも標的になる可能性があります。
Kubernetes に関しては、この脆弱性により、権限昇格、コンテナエスケープ、またはサービス拒否攻撃が発生する可能性があります。
ニュースでこの脆弱性による損失が報告されたことはありませんが、重要なセキュリティ更新を一貫して適用することの重要性が浮き彫りになります。この脆弱性を報告した研究者は、私たち全員が努めるべき倫理的ハッキングの実践を体現しています。[12]
# 参考文献
[1] https://nvd.nist.gov/vuln/detail/CVE-2022-0185
[2] https://www.hackthebox.com/blog/CVE-2022-0185:_A_case_study
[3] https://ubuntu.com/security/CVE-2022-0185#impact-score
[4] [https://en.wikipedia.org/wiki/Two%27s_complement](https://en.wikipedia.org/wiki/Two's_complement)
[5]https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Unsigned-Overflow.html
[6] https://www.kernel.org/doc/gorman/html/understand/understand011.html
[7] https://leviathan.vip/
[8] https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html
[9] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=722d94847de2
[10] https://ubuntu.com/security/CVE-2022-0185
[11] https://access.redhat.com/security/cve/CVE-2022-0185
[12]https://jfrog.com/blog/the-impact-of-cve-2022-0185-linux-kernel-vulnerability-on-popular-kubernetes-engines/
[14] https://github.com/chenaotian/CVE-2022-0185?tab=readme-ov-file
[15] https://www.tutorialspoint.com/two-s-complement
| CVSS v3.1 メトリクス | 値 |
|---|
| 攻撃ベクトル (AV) | ローカル |
| 必要な権限 (PR) | なし |
| ユーザー操作 (UI) | なし |
| 機密性 (C) | 高 |
| 完全性 (I) | 高 |
| 可用性 (A) | 高 |