
CVE-2020-8597 (pppd EAP) の合成 CWE-120 スタックバッファオーバーフローバリアント(CodeQL 静的解析のターゲット)
pppd の関数名、呼び出し深度、ディスパッチ構造を一切共有しないプログラムにおいて、CVE-2020-8597 — pppd EAP の rhostname スタックバッファオーバーフロー (CWE-120) — のバグクラスを再現する、意図的に脆弱な約170行の C プログラム。CodeQL 静的解析ターゲットとして使用される。
目的は汎用性テストである。pppd のバグを捕捉するために書かれた CodeQL クエリは、編集なしでこのプログラムに対しても発火しなければならない。もし発火すれば、そのクエリは元のコードの形ではなくバグクラスを表現していることになる。
このプログラムは意図的に安全でなく、解析のみを目的として存在する。デプロイしないこと。これが模倣するバグは公開されている (CVE-2020-8597、2020年開示)。
攻撃者由来の長さが固定サイズのバッファにコピーされるが、その長さをバッファのサイズに関連付けるガードが存在しない。
いずれの場合も境界チェックは存在する — ただ、重要な2つの量を関連付けることに失敗しているだけである。これを間違える方法はちょうど2通りあり、このプログラムにはそれぞれ1つずつ含まれている:
sizeof(dest) ではなく受信フレームに対してチェックされる。オーバーリードは防ぐが、オーバーライトには何もしない。(handle_hello)sizeof(dest) を名指しする — バッファ境界にそっくりに見える — が、コピー長として使用される変数とは別の変数を制約している。(handle_stat)2つ目はより難しく、これこそが pppd の死んだ vallen >= len + sizeof(rhostname) チェックの正体である: 宛先サイズに言及しながら、コピー長ではない何かを制約している比較。「ここで何らかの比較が sizeof(dest) に言及しているか?」 とだけ問うクエリは、これによって黙らされる。
| pppd / CVE-2020-8597 | このプロジェクト | |
|---|---|---|
| ソース | PPP fd に対する read() | UDP ソケットに対する recvfrom() |
| ディスパッチ | グローバルな struct protent *protocols[]、プロトコル番号による線形マッチ | ファイルローカルな const struct frame_op ops[]、1バイトタグによる線形マッチ |
| シンクまでの深度 | get_input → (*input) → eap_input → eap_request | dispatch_frame → (*handle) → handle_hello |
| 宛先 | char rhostname[256] | char name[64] |
| 誤ったガード | vallen がパケット len で制約される | vlen がフレーム plen で制約される |
どちらも、これを grep ではなくデータフローにしている唯一の性質を保っている: ソースとシンクの間の関数ポインタテーブルを介した間接呼び出しである。
| ハンドラ | 行 | 存在するチェック | 判定 |
|---|---|---|---|
handle_hello() | シンクは :80 | vlen > plen - 2 — 正しい値、誤った境界 | 発火しなければならない |
handle_echo() | コピーは :106 | vlen >= sizeof(buf) — 両方正しい | 沈黙を保たなければならない — ネガティブコントロール |
handle_stat() | シンクは :145 | hlen >= sizeof(report) — 正しい境界、誤った値 | 発火しなければならない |
handle_stat が判別ケースである。そのチェックは sizeof(report) を名指しするため、宛先サイズに言及するあらゆる比較を受け入れるクエリは、これをガード済みとみなしてバグを見逃す。これを捕捉するには、チェックされている値とコピー長として使用される値を比較する必要がある — グローバル値番号付けである。これをクエリから削除すると、このハンドラは偽陰性となり、他のすべてのサイトは判定を保つ。
1つの UDP データグラム = 1つのフレーム:
[ type : 1 ] [ length : 2, big-endian ] [ value : length bytes ]
type 0x01 → hello、0x02 → echo、0x03 → stat。宣言された長さが65から約2045の間の hello フレームは name[64] をオーバーフローさせる。stat フレームは代わりに2つの1バイト長を運ぶ — ヘッダ長とボディ長 — そしてヘッダ長が何を言おうと、32を超えるボディ長は report[32] をオーバーフローさせる。
make # gcc -Wall -Wextra -O0 -g -o tlv_server tlv_server.c
Linux/POSIX (BSD ソケット)。警告なしでクリーンにビルドされる。
CodeQL は実際のコンパイルをトレースするので、クリーンな状態からビルドする:
make clean
codeql database create db --language=cpp --command="make"
# or, without the clean step:
codeql database create db --language=cpp --command="make -B"
次に db に対して Part 3 クエリを実行する; handle_hello の memcpy と handle_stat の memcpy を報告し、handle_echo では沈黙を保つはずである。クエリとその実行手順は codeql/ にある。
ハンドラが追加されるたびにソースは変わるので、データベースを再ビルドすること — CodeQL は database create 時点のコードをスナップショットし、既存の db/ は新しいコードを見ない。