ImageMagick の InterpretImageFilename で見つかった off-by-one read overflow
まとめ
- 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(文字列終端) |
- 初回
strchr(format,'%')でpはformat[0]を指す q = p + 1でformat[1](2 番目の%)を指す*q == '%'が真なのでp = q + 1を実行 →pはformat[2]の\0を指すcontinueで次の反復へ。条件式strchr(p + 1, '%')のp + 1は\0の 1 バイト先のアドレスstrchrはこのアドレスから先を「次の%を見つけるか\0に当たるまで」走査する → 確保した文字列バッファの外を読む
%% を消費したら本来 p は 2 番目の % の位置(q)にとどめておけばよかった。修正は 1 文字分の差。
p = q; // 修正後(p = q + 1 から変更)なぜ 1 バイト読みすぎるだけで脆弱性なのか
1 バイトの out-of-bounds read 自体の直接影響は以下に限られる。
- アンマップ領域に当たれば SIGSEGV でクラッシュ(DoS)
- 隣接ヒープに機密データがあり、それが何らかの出力経路に乗れば情報漏洩
- 多くの場合は何も起きない
問題は 読んだ値をパーサーが判断材料にする こと。今回のループは strchr(p + 1, '%') が「次の % を見つけるまで」走査を続ける。\0 を飛び越えた先のメモリにたまたま % バイトがあれば、
- それを「次の format specifier の開始」と誤認
- ループが続行して、さらに先のメモリを「フォーマット文字列」として読みに行く
- 1 バイトのズレが雪だるま式に拡大して、本来関係ないヒープを延々と走査
- 走査範囲に機密情報が乗っていれば漏洩、アンマップ境界に当たればクラッシュ、運次第で後続処理が意図しない動作に
つまり「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 だから即対応」ではなく、
- 脆弱なコードパスは何か
- 自分のアプリからそのコードパスに到達できるか
- 到達できるならどの入力経路か
を確認してから対応優先度を判定するのが現実的。
教訓
C 言語のポインタ進行で p = q と p = q + 1 は 1 文字の差だが、間違えると out-of-bounds read になりうる。とくに「エスケープシーケンスを処理した直後にポインタを進める」コードは off-by-one を埋め込みやすい。
レビュー時は:
\0を踏み越える経路がないかstrchr/strstrのような「終端を見つけるまで走査する関数」に渡すポインタの妥当性continueの直前で進め過ぎていないか
を意識して読むと、この種のバグに気づきやすい。