
このREADME.mdは、Davidのブログの翻訳です。DavidはLinuxカーネルでCVE-1015および1016を発見しました。原文を読むには彼のウェブサイトをご覧ください。
彼のソーシャルメディアはこちら:
2022年4月2日公開
これらの問題は、最新のUbuntuおよびRHELのデフォルト設定で悪用可能であると考えられます。CVE-2022-1015のPoC(Proof of Concept)は、Arch Linuxのカーネルバージョン5.16-rc3をターゲットとして作成しました。
この文書は、Linuxカーネルの機能とセキュリティに関する基礎知識を持つ読者を対象としています。ネットワークスタックに詳しくない方にも理解しやすいよう、できるだけ平易に記述するよう努めました。
以下は読み進め方のガイドです:
2月中旬、GoogleのセキュリティプログラムはkCTF報奨金プログラムの継続を発表し、nsjailサンドボックス内の非特権プロセスからrootユーザーへの権限昇格を可能にするLinuxカーネルのエクスプロイトに対して、31,337ドルから91,337ドルの報奨金を提供するとしました。
貧乏な学生である私は、明らかにこれに惹かれました。これが「現実世界」の脆弱性を探す初めての試みでしたが、CTFで自分のチームと冒険する中で、セキュリティの観点からLinuxカーネルに親しんでいました。何時間も(ほとんど進歩せずに、しかしLinuxに関する知識は増えて)過ごした後、nf_tablesモジュールにいくつかの脆弱性を発見することができました。
残念ながら、結局このモジュールはGoogleのkCTFルールには含まれていなかったため、これらの2つの脆弱性に対して報奨金は得られませんでした。しかしもちろん、それでも報告し、CVE-2022-1015用のLPE(ローカル特権昇格)エクスプロイトを作成しました。
さて、あなたはLinuxにいくつかの脆弱性を見つけようと決意しました。次はどうする? Linuxは巨大なプロジェクトであり、木を見て森を見ず(細部に集中しすぎて本当に重要なことを見失う)になりがちです。さらに悪いことに、多くの部分は文書化されておらず、何が起こっているのか理解するために大量のコードを読む必要があります。
私はまず、Linuxのセキュリティモデルを詳細に把握しようと試みました。バグを見つけることと、良いバグを見つけることは別物です。結局のところ、すべてのバグが同じように作られているわけではありません:
FS_USERNS_MOUNTを指定するvfsで、その場合ユーザー名前空間でマウントできます。CAP_SYS_ADMINまたはCAP_NET_ADMINを必要とします。
/proc/config.gzからアクセスできます。モジュールはカーネルに組み込む(=y)か、別途コンパイルして実行時にロード(=m)できます。/proc/modulesや/proc/kallsymsを使用できますが、モジュールはカーネルに動的にロードされる(例:request_module)ため、常に信頼できるとは限りません。これらの制約は、脆弱性を探すファイルシステムの範囲を絞り込むのに役立ちます。時間をかけて、狙うターゲットへの攻撃計画を立てるのは良い考えだと思います。
私は前述の点から教訓を得ました。前述したように、nf_tablesモジュールはkCTFで提供されたインスタンスにはロードされていませんでした。最初からこれに気づいていれば、失望を避けられたでしょう:p。一方で、もし気づいていたら、おそらく今頃このブログを読んでいることはなかったでしょう。結局うまくいったのだから、良しとしましょう。
COS(Googleのコンテナ最適化Linuxフォーク)にnf_tablesが含まれていなかった理由の説明は、こちらとこちらにあります。
上記の点を評価した後、最善の道はおそらくネットワークソースコードを見ることだと判断しました。そこにある多くの興味深い機能はCAP_NET_ADMINを必要としますが、前述したように実際には問題になりません。むしろ、特別な権限を必要とするコンポーネントは、カーネル開発者が誤った安心感を持つ可能性があるため、一般的に安全性が低いのではないかと推測しています。
さらに、もっと詳しく知りたいファイルシステムを選ぶ努力もしました。そうすれば、たとえバグが見つからなくても、多くの興味深いことを学べます。
私は多くのネットワークファイルシステムを調査しましたが、重要なものは見つかりませんでした。net/サブディレクトリをナビゲートした後、nf_tablesモジュールに出会いました。少し複雑そうに見えたので、時間をかけて理解することにしました。
Netfilter(net/netfilter)は、カーネル内のかなり大きなネットワークサブシステムです。簡単に言えば、netfilterはネットワークスタック全体にフックを設置し、他のモジュールがハンドラを登録できるようにします。フックに到達すると、制御はそれらのハンドラに委譲され、それぞれのネットワークパケット構造を操作できます。ハンドラはパケットを許可、ドロップ、変更することができます。
nf_tables API(net/netfilter/nf_tables_api.c)を何時間か探索して、正確な動作を理解し始めた後、ユーザーが送信するレジスタに対する論理検証を調べてみることにしました。すると、いくつかの疑わしい動作を発見しました。自分が正気を失っているのかどうか考えた後、見つけた脆弱性をトリガーする小さなPoC(概念実証)を書きました。それは、スタックメモリの読み取りと書き込みを可能にするOOB(out-of-bounds)脆弱性です。
カーネルアドレスを漏洩させる方法を見つけた後、プログラムカウンタを乗っ取るのは非常に簡単でした。少しのROP(Return-Oriented Programming)を経て、rootシェルが現実のものとなりました。
式のinitルーチンがnetlinkユーザーメッセージからレジスタをパースするたびに、ソースレジスタかデスティネーションレジスタかに応じて、nft_parse_register_loadまたはnft_parse_register_storeルーチンが呼び出されます。いくつかコメントを追加しました:```c
int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
/* Given a netlink attribute and the length
* that is required to read the requested data,
* write a register index to `sreg` or return
* an error on failure. */
u32 reg;
int err;
reg = nft_parse_register(attr);
err = nft_validate_register_load(reg, len);
if (err < 0)
return err;
/* Write resulting index to the nft_expr.data structure. */
*sreg = reg;
return 0;
}
static unsigned int nft_parse_register(const struct nlattr attr) { / Convert a register to an index in nft_regs */
unsigned int reg;
/* Get specified register from netlink attribute */
reg = ntohl(nla_get_be32(attr));
switch (reg) {
/* If it's 0 to 4 inclusive,
* it's an OG 16-byte register and we need to
* multiply the index by 4 (4*4=16) */
case NFT_REG_VERDICT...NFT_REG_4:
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
/* Else we subtract 4, since we need to account
* for the OG registers above. */
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}
/* So supplied values of 1, 2, 3, 4 map to
* OG 16-byte registers, with indices 4, 8,
* 12, 16
* Supplied values of 5, 6, 7 overlap the verdict,
* 8,9,10,11 overlap with OG register 1
* 12,13,14,15 overlap with OG register 2
* etc. */
}
static int nft_validate_register_load(enum nft_registers reg, unsigned int len) { /* We can never read from the verdict register, * so bail out if the index is 0,1,2,3 */ if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) return -EINVAL;
/* Invalid operation, bail out */
if (len == 0)
return -EINVAL;
/* If there would be an OOB access whenever
* `reg` is taken as index and `len` bytes are read,
* bail out.
* sizeof_field(struct nft_regs, data) == 0x50 */
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
return -ERANGE;
return 0;
}
`*_store` のバリアントは実質的に同一ですが、いくつかの条件下で *verbdict* への書き込みを許可する点が異なります。最後のバリデーションを確認した後、ここで何かが本当におかしいです:```c
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
これは integer overflow のように見えますね。reg に、len を加えたときにオーバーフローを起こすような値を4倍したものを含ませることができれば、条件を満たせます。nft_parse_register_load では、reg の最後の貴重なバイトがポインタ u8 *sreg に書き込まれ、後でインデックスとして使用される nft_expr に落ち込みます。```c
*sreg = reg;
本当にできるのか?`reg` はルーチンのバリデーションにおける `enum nft_registers` です。いずれにせよ、`nft_parse_register` の範囲である `0x00000001` から `0xfffffffb` までの値を渡すことができます。しかし、`nft_validate_register_load` において `reg` は32ビット値なのでしょうか?コンパイラは、より小さな型で全ての値を表現できる場合、*enum types* を縮小することが知られています。別の意見も聞いてみましょう。
GCC マニュアルより引用:```
The integer type compatible with each enumerated type (C90 6.5.2.2, C99 and C11 6.7.2.2).
Normally, the type is unsigned int if there are no negative values
in the enumeration, otherwise int. If -fshort-enums is specified,
then if there are negative values it is the first
of signed char, short and int that can represent all the values,
otherwise it is the first of unsigned char, unsigned short and unsigned int
that can represent all the values.
On some targets, -fshort-enums is the default; this is determined by the ABI.
¿TL;DR? ABIと最適化の程度に依存します。このオプションがLinuxビルドでデフォルトで有効になっているかどうかについて、具体的な証拠は見つかりませんでした。