投稿

ImageMagick の InterpretImageFilename で見つかった off-by-one read overflow

ImageMagick の InterpretImageFilename で見つかった off-by-one read overflow

参考: GHSA-hm4x-r5hc-794f

まとめ

  • ImageMagick の InterpretImageFilename に「連続パーセント記号 %% を処理する箇所」で off-by-one error があった
  • バグの本体は 書き込み ではなく 読み込み 側のポインタが \0 を 1 バイト飛び越えること
  • 1 バイトの read overflow 自体は軽症(クラッシュ or 軽い情報漏洩)だが、パーサーがそのバイトを「次の動作の判断材料」に使うとループが暴走しうる
  • NVD は最悪ケースで 9.8、MITRE/GitHub は現実影響で 3.7 と評価が大きく乖離する典型例

off-by-one とは

ループ・配列・バッファ操作で「1 個ズレる」ミス。境界の判定で <<= を間違えたり、ヌル終端文字 \0 の 1 バイトを忘れたり、ポインタの進め方を 1 つ多く / 少なくしたりすると起きる。

書き込み側でずれれば「バッファ末尾を 1 バイト超えて書く」 = heap/stack buffer overflow になり、悪用すれば任意コード実行に化けることがある。読み込み側でずれれば「確保した領域の外を 1 バイト読む」 = out-of-bounds read になる。今回のケースは後者。

問題のコード

修正前は概ね下記のような構造だった。

for (p = strchr(format, '%'); p != NULL; p = strchr(p + 1, '%'))
{
  q = (char *) p + 1;
  if (*q == '%')
  {
    p = q + 1;   // ← 問題箇所
    continue;
  }
  // ... 他の format specifier 処理
}

format がたとえば "%%"(バイト列 %, %, \0 の 3 バイト)だった場合を追う。

位置 バイト
format[0] %
format[1] %
format[2] \0(文字列終端)
  1. 初回 strchr(format,'%')pformat[0] を指す
  2. q = p + 1format[1](2 番目の %)を指す
  3. *q == '%' が真なので p = q + 1 を実行 → pformat[2]\0 を指す
  4. continue で次の反復へ。条件式 strchr(p + 1, '%')p + 1\0 の 1 バイト先のアドレス
  5. strchr はこのアドレスから先を「次の % を見つけるか \0 に当たるまで」走査する → 確保した文字列バッファの外を読む

%% を消費したら本来 p は 2 番目の % の位置(q)にとどめておけばよかった。修正は 1 文字分の差。

p = q;       // 修正後(p = q + 1 から変更)

なぜ 1 バイト読みすぎるだけで脆弱性なのか

1 バイトの out-of-bounds read 自体の直接影響は以下に限られる。

  • アンマップ領域に当たれば SIGSEGV でクラッシュ(DoS)
  • 隣接ヒープに機密データがあり、それが何らかの出力経路に乗れば情報漏洩
  • 多くの場合は何も起きない

問題は 読んだ値をパーサーが判断材料にする こと。今回のループは strchr(p + 1, '%') が「次の % を見つけるまで」走査を続ける。\0 を飛び越えた先のメモリにたまたま % バイトがあれば、

  1. それを「次の format specifier の開始」と誤認
  2. ループが続行して、さらに先のメモリを「フォーマット文字列」として読みに行く
  3. 1 バイトのズレが雪だるま式に拡大して、本来関係ないヒープを延々と走査
  4. 走査範囲に機密情報が乗っていれば漏洩、アンマップ境界に当たればクラッシュ、運次第で後続処理が意図しない動作に

つまり「1 バイトの read overflow」は 入口 であって、本当の怖さは「正常範囲外で動き続けるパーサーが、隣接メモリを意図しない入力として食う」ところにある。

評価が大きく乖離する理由

評価元 スコア 視点
NVD 9.8 最悪ケース(任意の隣接メモリを走査 → 機密漏洩 / クラッシュ / 後段の脆弱コードパス到達)
MITRE / GitHub 3.7 現実影響(read overflow 主体、AC:H で攻撃成立条件が限定的)

このギャップは write overflow / read overflow を一律に同じ重大度で扱うか、攻撃成立条件まで踏み込んで現実的影響を見るかの違い。脆弱性対応の優先度を決めるときは、両方を見て自分のアプリでの到達経路を加味する必要がある。

アプリ側で到達条件があるか

このバグが発火するのは「InterpretImageFilename が format string テンプレートを処理するパス」に到達したときに限られる。具体的には:

  • mogrify -write "%d.jpg" のように、ImageMagick のテンプレート展開機能を使う
  • そのテンプレート文字列に連続 %% が含まれる

Ruby から ImageMagick を使うラッパー(mini_magick 等)は通常 convert 経由で動作し、出力ファイル名はホスト側のコードで生成する。この場合、ImageMagick のテンプレート展開機能を経由しないため、ユーザーが投稿したファイル名に %% が含まれていても InterpretImageFilename のこのコードパスには入らない。

「CVSS 9.8 だから即対応」ではなく、

  1. 脆弱なコードパスは何か
  2. 自分のアプリからそのコードパスに到達できるか
  3. 到達できるならどの入力経路か

を確認してから対応優先度を判定するのが現実的。

教訓

C 言語のポインタ進行で p = qp = q + 1 は 1 文字の差だが、間違えると out-of-bounds read になりうる。とくに「エスケープシーケンスを処理した直後にポインタを進める」コードは off-by-one を埋め込みやすい。

レビュー時は:

  • \0 を踏み越える経路がないか
  • strchr / strstr のような「終端を見つけるまで走査する関数」に渡すポインタの妥当性
  • continue の直前で進め過ぎていないか

を意識して読むと、この種のバグに気づきやすい。

トレンドのタグ