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

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

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

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

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CVE-2022-1015-1016 — Davidによって発見され文書化されたCVE-2022-1015およびCVE-2022-1016のスペイン語翻訳 | Kitploit
ツール/GitHubGitHub/zanezhub/cve-2022-1015-1016
特権昇格脆弱性分析エクスプロイト学習と教育バイナリエクスプロイト
GitHubzanezhub/cve-2022-1015-1016

CVE-2022-1015-1016

Davidによって発見され文書化されたCVE-2022-1015およびCVE-2022-1016のスペイン語翻訳

リポジトリを見る
124年前未レビュー

人気

すべて見る →

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

すべてのツールを探索

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

すべてのツールを見る →
共有
ウェブサイト

CVE-2022-1015 & CVE-2022-1026

このREADME.mdは、Davidのブログの翻訳です。DavidはLinuxカーネルでCVE-1015および1016を発見しました。原文を読むには彼のウェブサイトをご覧ください。

彼のソーシャルメディアはこちら:

  • Twitter
  • Github

nf_tablesにおけるLinuxの2つの新たな脆弱性の分析

2022年4月2日公開

  • CVE-2022-1015は、入力引数の検証不足に起因するout-of-boundsアクセスを引き起こし、リモートコード実行やローカル特権昇格につながる可能性があります。
  • CVE-2022-1016は、スタック上に割り当てられた変数の初期化不良に関連しており、カーネルからユーザースペースへの広範なデータ漏洩に利用される可能性があります。

これらの問題は、最新のUbuntuおよびRHELのデフォルト設定で悪用可能であると考えられます。CVE-2022-1015のPoC(Proof of Concept)は、Arch Linuxのカーネルバージョン5.16-rc3をターゲットとして作成しました。

この文書は、Linuxカーネルの機能とセキュリティに関する基礎知識を持つ読者を対象としています。ネットワークスタックに詳しくない方にも理解しやすいよう、できるだけ平易に記述するよう努めました。

以下は読み進め方のガイドです:

  • 脆弱性そのものについて読みたいだけなら、セクション4から始めてください。
  • カーネルサブシステムの背景知識も欲しい場合は、セクション2から始めてください。
  • さらに詳細な背景に興味があるなら、文書全体をお読みください。

1. 背景

2月中旬、GoogleのセキュリティプログラムはkCTF報奨金プログラムの継続を発表し、nsjailサンドボックス内の非特権プロセスからrootユーザーへの権限昇格を可能にするLinuxカーネルのエクスプロイトに対して、31,337ドルから91,337ドルの報奨金を提供するとしました。

貧乏な学生である私は、明らかにこれに惹かれました。これが「現実世界」の脆弱性を探す初めての試みでしたが、CTFで自分のチームと冒険する中で、セキュリティの観点からLinuxカーネルに親しんでいました。何時間も(ほとんど進歩せずに、しかしLinuxに関する知識は増えて)過ごした後、nf_tablesモジュールにいくつかの脆弱性を発見することができました。

残念ながら、結局このモジュールはGoogleのkCTFルールには含まれていなかったため、これらの2つの脆弱性に対して報奨金は得られませんでした。しかしもちろん、それでも報告し、CVE-2022-1015用のLPE(ローカル特権昇格)エクスプロイトを作成しました。

1.1 ターゲットの特定と監査戦略

さて、あなたはLinuxにいくつかの脆弱性を見つけようと決意しました。次はどうする? Linuxは巨大なプロジェクトであり、木を見て森を見ず(細部に集中しすぎて本当に重要なことを見失う)になりがちです。さらに悪いことに、多くの部分は文書化されておらず、何が起こっているのか理解するために大量のコードを読む必要があります。

私はまず、Linuxのセキュリティモデルを詳細に把握しようと試みました。バグを見つけることと、良いバグを見つけることは別物です。結局のところ、すべてのバグが同じように作られているわけではありません:

  • バグがroot権限を必要とする場合、セキュリティ上の境界線は意味をなさない(カーネルモジュール署名が有効でない限り)。
    • 思い浮かぶのは、多くの(仮想)ファイルシステムモジュールです。これらのファイルシステムをマウントできるのは初期rootユーザーのみです。例外は、FS_USERNS_MOUNTを指定するvfsで、その場合ユーザー名前空間でマウントできます。
  • バグにシステムコール経由でアクセスできない場合、おそらく悪用は不可能でしょう。
    • これは多くのハードウェアドライバーに当てはまります。物理的なアクセスがないためです。ただし、低レベルのネットワークドライバーは、Bluetoothや802.11.acなどを通じてデータを送信できる場合、依然として良いターゲットとなり得ます。
    • 明らかに、これは状況に依存します。
  • 多くのバグはCAP_SYS_ADMINまたはCAP_NET_ADMINを必要とします。
    • ユーザー名前空間はデフォルトで有効なので、これは問題になりません。
    • そうでなければ、まずコンテナ内でrootユーザー名前空間への権限昇格を行う必要があります。
  • すべてのモジュールがターゲットに存在するとは限りません。
    • Linuxは高度に設定可能な例外的なソフトウェアであり、すべての構成は多様な形で変化します。
    • カーネル構成は通常、/proc/config.gzからアクセスできます。モジュールはカーネルに組み込む(=y)か、別途コンパイルして実行時にロード(=m)できます。
    • /proc/modulesや/proc/kallsymsを使用できますが、モジュールはカーネルに動的にロードされる(例:request_module)ため、常に信頼できるとは限りません。
    • 確信が持てない場合は、モジュールと対話する小さなプログラムを作成してください。

これらの制約は、脆弱性を探すファイルシステムの範囲を絞り込むのに役立ちます。時間をかけて、狙うターゲットへの攻撃計画を立てるのは良い考えだと思います。

私は前述の点から教訓を得ました。前述したように、nf_tablesモジュールはkCTFで提供されたインスタンスにはロードされていませんでした。最初からこれに気づいていれば、失望を避けられたでしょう:p。一方で、もし気づいていたら、おそらく今頃このブログを読んでいることはなかったでしょう。結局うまくいったのだから、良しとしましょう。

COS(Googleのコンテナ最適化Linuxフォーク)にnf_tablesが含まれていなかった理由の説明は、こちらとこちらにあります。

1.2 nf_tables:なぜか?

上記の点を評価した後、最善の道はおそらくネットワークソースコードを見ることだと判断しました。そこにある多くの興味深い機能はCAP_NET_ADMINを必要としますが、前述したように実際には問題になりません。むしろ、特別な権限を必要とするコンポーネントは、カーネル開発者が誤った安心感を持つ可能性があるため、一般的に安全性が低いのではないかと推測しています。

さらに、もっと詳しく知りたいファイルシステムを選ぶ努力もしました。そうすれば、たとえバグが見つからなくても、多くの興味深いことを学べます。

私は多くのネットワークファイルシステムを調査しましたが、重要なものは見つかりませんでした。net/サブディレクトリをナビゲートした後、nf_tablesモジュールに出会いました。少し複雑そうに見えたので、時間をかけて理解することにしました。

2. netfilter入門

Netfilter(net/netfilter)は、カーネル内のかなり大きなネットワークサブシステムです。簡単に言えば、netfilterはネットワークスタック全体にフックを設置し、他のモジュールがハンドラを登録できるようにします。フックに到達すると、制御はそれらのハンドラに委譲され、それぞれのネットワークパケット構造を操作できます。ハンドラはパケットを許可、ドロップ、変更することができます。


4. CVE-2022-1015

nf_tables API(net/netfilter/nf_tables_api.c)を何時間か探索して、正確な動作を理解し始めた後、ユーザーが送信するレジスタに対する論理検証を調べてみることにしました。すると、いくつかの疑わしい動作を発見しました。自分が正気を失っているのかどうか考えた後、見つけた脆弱性をトリガーする小さなPoC(概念実証)を書きました。それは、スタックメモリの読み取りと書き込みを可能にするOOB(out-of-bounds)脆弱性です。

カーネルアドレスを漏洩させる方法を見つけた後、プログラムカウンタを乗っ取るのは非常に簡単でした。少しのROP(Return-Oriented Programming)を経て、rootシェルが現実のものとなりました。

4.1 ルート

式のinitルーチンがnetlinkユーザーメッセージからレジスタをパースするたびに、ソースレジスタかデスティネーションレジスタかに応じて、nft_parse_register_loadまたはnft_parse_register_storeルーチンが呼び出されます。いくつかコメントを追加しました:```c int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len) {

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

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

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

}

root@kitploit:~
`*_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;

root@kitploit:~
本当にできるのか?`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ビルドでデフォルトで有効になっているかどうかについて、具体的な証拠は見つかりませんでした。

しかし、アセンブラは決して嘘をつきません。見てみましょう:```objdump.x86asm 0000000000001b60 <nft_parse_register_load>: 1b60: e8 00 00 00 00 call 1b65 <nft_parse_register_load+0x5> 1b65: 55 push rbp 1b66: 8b 47 04 mov eax,DWORD PTR [rdi+0x4] 1b69: 0f c8 bswap eax 1b6b: 89 c7 mov edi,eax 1b6d: 8d 48 fc lea ecx,[rax-0x4] 1b70: c1 e7 04 shl edi,0x4 1b73: 48 89 e5 mov rbp,rsp 1b76: c1 ef 02 shr edi,0x2 1b79: 83 f8 04 cmp eax,0x4 1b7c: 89 f8 mov eax,edi 1b7e: 0f 47 c1 cmova eax,ecx 1b81: 85 d2 test edx,edx 1b83: 74 13 je 1b98 <nft_parse_register_load+0x38> 1b85: 83 f8 03 cmp eax,0x3 1b88: 76 0e jbe 1b98 <nft_parse_register_load+0x38> 1b8a: 8d 14 82 lea edx,[rdx+rax*4] 1b8d: 83 fa 50 cmp edx,0x50 1b90: 77 0d ja 1b9f <nft_parse_register_load+0x3f> 1b92: 88 06 mov BYTE PTR [rsi],al 1b94: 5d pop rbp 1b95: 31 c0 xor eax,eax 1b97: c3 ret
1b98: b8 ea ff ff ff mov eax,0xffffffea 1b9d: 5d pop rbp 1b9e: c3 ret
1b9f: b8 de ff ff ff mov eax,0xffffffde 1ba4: 5d pop rbp 1ba5: c3 ret

root@kitploit:~
関数呼び出しはかなりよく整列されています。重要な操作は `1b8a` にあります:```objdump.x86asm
lea    edx, [rdx+rax*4]
cmp    edx, 0x50
ja     1b9f <nft_parse_register_load+0x3f>
mov    BYTE PTR [rsi], al

rax は ntf_parse_register の結果、rdx は提供された len、rsi はポインタ sreg です。これで疑問は解消されました。

nft_parse_register_store も同じ動作を示します。レジスタが stack 上に存在する限り、私たちの OOB 脆弱性は明らかに stack に関連したものになります。これは良いことで、運が良ければメモリを直接上書きしてリターンできる可能性があります。

脆弱な入力の例として、レジスタが 0xfffffffb で長さが 0x20 の場合、0xfffffffb * 4 + 0x20 = 0x0c < 0x50 と評価されます。検証後、(u8)0xfffffffb = 0xfb が *sreg に書き込まれます。

しかし問題があります: 加算時に overflow を引き起こす可能性のある長さを使用できる式は存在するのでしょうか?少し調査した結果、nft_bitwise と nft_payload は 0x00 から 0xff までの独自の長さを入力として与えることを許可していることがわかりました。他の多くの式は、非常に小さな固定長を持っているようです。

現時点では有望に見えます。次のステップは、これらの exploit primitives(exploit 中に得られる汎用的な能力)を取得し、それらを使用することです。

4.2 exploit primitives の調査

私たちの exploit が与えることができる力のタイプを定義できれば、この脆弱性を悪用するのはより簡単になるはずです。それでは、少しお待ちください。少し算術を見ていきます。

レジスタの乗算に使用できる overflow のポイントは3つあります。これは 4 = 2^2 で乗算されるためです: 2^32 - 1, 2^31 - 1, 2^30 - 1 (それぞれ 0xffffffff, 0x7fffffff, 0x3fffffff)。これらの値は、許可された最大長を加算するまで減少する可能性があり、4倍した後は overflow になりません。もう一つの注意点は、前述のように 0xfffffffb より大きい値は使用できないことです。

特定の長さが与えられた場合、この長さを使用して overflow を可能にする最下位バイト値が、使用可能なOOBインデックスの範囲を形成します。

結局のところ、どの overflow ポイントが使用されるかは重要ではありません。例として、LSB (最下位ビット) が 0xf0 の次の値を考えてみましょう:``` 0xfffffff0 * 4 = 0xffffffc0 0x7ffffff0 * 4 = 0xffffffc0 0x3ffffff0 * 4 = 0xffffffc0

root@kitploit:~
これからは、`0x7fffffff` に近いレジスタ値を使用します。

以前に `nft_payload` と `nft_bitwise` について説明しました。これらの式のいくつかの特性は次のとおりです。

* `nft_payload` は *OOB* 書き込みのみ実行できますが、`nft_bitwise` は *OOB* 書き込みと読み取りの両方を実行できます。

* `nft_payload` は最大 0xff バイトの任意のデータを *OOB* 書き込みできます。

* `nft_bitwise` は実際には最大 `0x40` バイトの任意のデータを書き込むことができ、レジスタ空間のスタックにあるデータのうち最大 `0x40` バイトのみ読み取ることができます。
  
  * `nft_bitwise` には `sreg` と `dreg` が必要で、これらは同じ長さの値で検証を通過する必要があります。
  
  * レジスタ空間は `0x40` バイトしかないため、空間レジスタから読み取りまたは書き込みを行いたいのですが、`0x40` を超える長さでは検証を通過できません。

`nft_bitwise` により大きな長さの値を使用することもできますが、その場合 `sreg` と `dreg` が範囲外になる必要があり、それは我々の目的にはあまり役立ちません。したがって、今のところは長さ `0x40` で作業します。

これらすべてを考慮すると、どのような種類の *exploit* を使用できるでしょうか?

`nft_bitwise` の最大長は `0x40` です。これは、レジスタ値に4を掛けた値が少なくとも `0xffffffc0` である必要があることを意味します。4倍して得られる最大の値は `0xfffffffb` であり、`0xfffffffb + 0x40 = 0x3b <= 0x50` となるため、これは検証を通過します。

`0x7ffffff0 * 4 = 0xffffffc0`: 下限は `0xf0` です。
`0x7fffffff * 4 = 0xfffffffb`: 上限は `0xff` です。

[*バイトオフセット*](https://es.wikipedia.org/wiki/Offset_(inform%C3%A1tica)) に変換すると:```
0xc1 * 4        = 0x304
0xeb * 4 + 0xff = 0x4ab

nft_payload は offsets [0x304, 0x4ab] を使って struct nft_regs から境界外書き込みを行うことができる。

これがすべて明らかになったところで、実際にこれらの offsets にある stack の中身は何だろうか?

nft_do_chain ルーチンは多くのコードパスを通じて呼び出される可能性がある。nft_do_chain の stack frame (スタックフレーム) の前に stack の形状を変える多くの要因がある。

  • chain hook が input か output か。

    • chain hook が input に設定されている場合、hook は該当するネットワークデバイスの softirq コンテキストで softirq スタックとともにアクティブになる。
    • chain hook が output に設定されている場合、hook は syscall (システムコール) コンテキストで send* スタックとともにアクティブになる。
  • 使用しているプロトコル。

    • RAW IP パケットを送信すると、call stack が例えばUDPパケットとはかなり異なる。

プロトコル、インターフェース、hook の場所のさまざまな組み合わせを使用して、多くの call stacks のバリエーションを得られると思う。現在は、UDPパケットを使用して output に設定された chain hook を使用している。

output と UDP のスタック図

送信されたUDPパケットが output に設定されたフックに到達したときの、nft_do_chain におけるスタックのレイアウトと境界外到達範囲

4.3 サイドチャネル情報漏洩 (side-channel)

安定したエクスプロイトを作成するには、まずカーネルイメージのアドレスを漏洩させる必要がある。

カーネルイメージのアドレスは9ビットのエントロピー(メッセージの集合から1つだけを受信する際に存在する不確かさの尺度)を持ち、つまりカーネルがロードされ得る512の異なる位置がある。攻撃シナリオによっては、攻撃が正しく機能する確率は512分の1である。しかし、これよりも安定したエクスプロイトを得られればもっと良い。

最も簡単な方法は、nft_bitwise によって得られた境界外読み取り機能を使って、stack 内のいくつかのデータをレジスタにコピーすることだ。読み取り可能な合計区間の長さが 0x7c バイトであるため、カーネルアドレスがそこにある可能性は十分にある。

nft_bitwise の境界外到達範囲

nft_bitwise の境界外到達範囲

今日は私たちの日だ!二つ存在する:``` gef➤ x/bx 0xffffffff815b49c1 0xffffffff815b49c1 <import_iovec+49>: 0xc9 gef➤ x/bx 0xffffffff819ac3ec 0xffffffff819ac3ec <copy_msghdr_from_user+92>: 0xba

root@kitploit:~
これをレジスタに書き込むことは一つのことですが、抽出することは別のことです。調査の結果、`nft_do_chain` の実行中にレジスタを直接読み取る簡単な方法は存在しないようです。

[email protected] への最初の報告では、netfilter メンテナから `nft_dynset` 式について教えられました。これは [*動的セット*](https://en.wikipedia.org/wiki/Dynamic_set) をサポートしており、異なる `nft_do_chain` 実行間で読み書き可能な一種のデータベースとして機能します。どうやら `nft_payload` もパケット自体に書き込むことができるようですが、私はこれに気づいていませんでした。

代わりに、私は [*サイドチャネル攻撃*](https://es.wikipedia.org/wiki/Ataque_de_canal_lateral) を続行することにしました。`nf_tables` の性質上、副作用を引き起こすことができます。実際、これらは副作用ではなく、むしろ主な効果であると言えるでしょう。

コピーしているカーネルメモリアドレスの値に基づいてパケットをドロップまたは許可するルールを作成することで、送信したパケットが受信されたかどうかを調べることにより、少しずつその値を推測できます。

1. `127.0.0.1:9999` でパケットを受信する UDP *ソケット* を作成します:
* 別のスレッドでパケットを受信する必要があります。

* 受信したパケットごとにメッセージを返送する必要があります。
2. 次のようなルールを追加します:
   
   1. `nft_bitwise` を使用してカーネルアドレスをレジスタにコピーします。
   
   2. `nft_cmp_expr` を使用してアドレスを定数と比較します。
   
   3. 比較結果が真の場合、パケットをドロップします。

3. `127.0.0.1:9999` に UDP パケットを送信します
   
   1. 返信メッセージを受信したかどうかに基づいて、カーネルアドレスに関する情報を少し決定できます。

4. 十分な情報が得られてアドレスを単独で特定できるまで、適切な値で2と3を繰り返します。

![](https://assets.kitploit.com/production/public/readmes/35022/71fabf101d1c962c5434bff7babccd3f294cd3d278765f1b03121429c5417b12.png)

まだいくつかの注意点があります。例えば、受信したパケットも予告なくドロップされる可能性があります。これを軽減するために、ノイズ低減を追加できます。そのためには、*ベースチェーン* と *補助通常チェーン* が必要です。

*ベースチェーン内のルール:*

| #   | 式                  | 引数                                                                                                    | コメント                                                                                                               |
| --- | ------------------- | ------------------------------------------------------------------------------------------------------- |:---------------------------------------------------------------------------------------------------------------------- |
| 0   | `nft_payload`       | base=NFT_PAYLOAD_TRANSPORT_HEADER<br/>offset=offsetof(udphdr, dport)<br/>len=sizeof_field(udphdr, dport) | パケットの宛先ポートをレジスタ8に書き込みます。                                                                         |
| 1   | `nft_cmp_expr`      | op=NFT_CMP_EQ<br/>sreg=8<br/>data=9999                                                                  | 宛先ポートを `9999` と比較し、結果が等しくない場合は `NFT_BREAK` を返します。                                         |
| 2   | `nft_payload`       | base=NFT_PAYLOAD_INNER_HEADER<br/>offset=0<br/>len=8                                                    | パケットの最初の8バイトをレジスタ8に書き込みます。                                                                     |
| 3   | `nft_cmp_expr`      | op=NFT_CMP_EQ<br/>sreg=8<br/>data=0xdeadbeef0badc0de                                                    | 最初の8バイトをマジック値と比較し、等しくない場合は `NFT_BREAK` を返します。                                         |
| 4   | `nft_immediate_expr`| verdict=NFT_JUMP<br/>chain=aux_chain                                                                    | ルールがまだ評価中であるため、条件が一致し、私たちの *補助チェーン* を呼び出します。                                   |

*補助チェーン内のルール:*

| #   | 式               | 引数                                                                     | コメント                                                                                                                                                 |
| --- | ---------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0   | `nft_bitwise`    | op=NFT_BITWISE_RSHIFT<br/>data=SHIFT_AMT<br/>dreg=OOB_OFFSET<br/>sreg=8  | 境界外読み取りを使用してカーネルアドレスをレジスタに書き込み、`SHIFT_AMT` ビットだけシフトして目的のアドレスのバイトを正しいレジスタに取得します。 |
| 1   | `nft_cmp`        | op=NFT_CMP_GT<br/>sreg=ADDRESS_OFFSET<br/>data=COMPARAND                  | カーネルアドレスのバイトと `COMPARAND` を比較し、結果が等しくない場合は `NFT_BREAK` を返します。                                                        |
| 2   | `nft_immediate`  | verdict=NFT_DROP                                                         | アドレスバイトが `COMPARAND` より大きい場合、パケットをドロップします。                                                                                  |

宛先ポートを確認し、内部 *ヘッダー* の最初の8バイトをマジック値と比較することで、任意のパケットに対して副作用をトリガーできます。

`COMPARAND` を動的に変更することで、二分探索を行い、`O(log(n))` 時間でカーネルアドレスのバイトを見つけることができます。`SHIFT_AMT` を次の8の倍数に動的に変更することで、次のメモリバイトに移動し、再度開始できます。

#### 4.3.1 疑似コードのフィルタリング

メモリアドレスをフィルタリングするためのPythonコードです。面白いことに、これは簡単にPythonで実装できました。カーネルエクスプロイトを必ずしもCで書く必要はないことを覚えておいてください :p```python
'''
Asumimos que un hilo secundario está recibiendo
paquetes UDP en 127.0.0.1:9999 y todo lo relacionado
con nf_tables ya está configurado
p. ej. table, base y auxiliary chain
'''

def leak_byte(pos):
    s = socket.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
    s.settimeout(200) # 200ms debería ser más que suficiente
    s.bind(("127.0.0.1", 1234))

    # buscar los límites
    low = 0, high = 255

    while True:
        mid = (low + high) // 2

        # si encontramos el valor, lo regresamos 
        if low == high:
            s.close()
            return mid

        set_leak_rule(SHIFT_AMT=pos*8, COMPARAND=mid)

        # Enviar el paquete y activar la auxiliary chain
        s.sendto(pack(0xdeadbeef0badc0de), ("127.0.0.1", 9999))

        # El hilo secundario regresa a 127.0.0.1:1234
        res = s.recvfrom(0x2000)

        if not res:
            '''
            nuestro paquete fue soltado
            ya que no se regresó nada en los 200ms
            lo que significa que 

            byte to leak >= mid
            el byte a filtrar es mayor o igual a mid (127)
            '''
            low = mid
        else:
            '''
            [sanity check o prueba de cordura]

            se usa para evaluar rápidamente si 
            el valor a calcular es siquiera posible

            https://es.wikipedia.org/wiki/Prueba_de_cordura
            '''

            if res != b"MSG_OK":
                print("Something went wrong")
                return None

            '''
            Nuestro paquete fue aceptado, lo que
            significa que 

            byte to leak < mid
            byte a filtrar es menor a mid (127)
            '''
            high = mid - 1

leak_bytes = lambda: [leak_byte(i*8) for i in range(4)]

4.4 任意コード実行(Arbitrary code execution)

リークを取得したので、任意コード実行は非常に簡単なはずです。nft_payloadの境界外書き込みは、スタックに対するチェーン化された RoP 攻撃を書き込めるはずですよね?

いいえ、少なくともこの特定のカーネルではあまり運がありませんでした。nft_payloadの境界外書き込みは、ほぼ完全にudp_sendmsgルーチンのスタックフレームと一致しています。udp_sendmsgのアドレスはレジスタ相対でオフセット+0x2f8にありますが、この位置はnft_payloadやnft_bitwiseで到達するには低すぎます(オフセット+0x304から書き込みを開始できますが、非常に近い...)。inet_sendmsgのアドレスはオフセット+0x4a8にあります。技術的には到達可能(下位3バイトを上書きできます)ですが、これを実現するにはアドレス+0x0458にある stack canary(スタックバッファオーバーフローを検出し、悪意のあるコードの実行を防ぐ手法)も上書きする必要があります。これによりカーネルが明らかにクラッシュするため、この方法は選択肢になりません。

別のカーネルビルドではこの方法を使用できましたが、このブログで使用しているカーネルに対して同じことを行おうとすると、もう少し難しくなるようです。

さて、もしかすると少し凝ったスタックフレームハッキングを行って、udp_sendmsg内のローカル変数を上書きできるかもしれません。また、verdict chain pointerを上書きすることも試せます。例えば、レジスタの値 0x7fffff00 を使用します(これは素晴らしいテクニックになるかもしれません。課題を考慮すると)。

使用するベースチェーンフックを変更してみましょう。これまでは output チェーンを使用していましたが、input チェーンに変更した場合はどうなるでしょうか。

送信されたUDPパケットがinputフックに到達した場合の、nft_do_chainにおける境界外の範囲の図

これは少し良さそうです!__netif_receive_skb_one_coreのフレームの戻り先アドレス(オフセット+0x328、__netif_receive_skbに戻る)を上書きできます。これはnft_payloadの境界外の範囲に比較的近いので、OOBインデックスをこの戻り先アドレスに直接指すように設定でき、オフセット+0x310のスタックカナリーを回避できます。オフセット+0x328はインデックス0xcaに相当します。

戻り先アドレスの上書きを有効にするには、テーブルに新しいinputチェーンを作成し、パケットの内部ヘッダーからインデックス0xcaに向けて0xffバイトを書き込むnft_payloadを含むルールを追加します。その後、ペイロードを含むパケットを送信すると、ブームです。

🥳 🥳 🥳 🥳 🥳

ツールをダウンロード