科学・技術
MS Paint と Photos はローカル生成出力にも不可視の GUID ウォーターマークを付与する
MS Paint and Photos inivisibly watermark even locally generated output with GUID (xusheng.dev)
要約
Microsoft Paint と Photos アプリが、ローカルで生成されたAI画像に、サーバー発行のGUIDを不可視のウォーターマークとして埋め込んでいることがリバースエンジニアリングにより明らかになりました。このウォーターマークは、ユーザーが設定できる可視ウォーターマークとは独立しており、画像生成時にリモートサーバーから取得したGUIDがピクセルデータにエンコードされる仕組みです。Copilot+ PCでは画像生成はローカルで行われますが、プロンプトのモデレーションとGUIDの取得はリモートで行われます。
全文翻訳
Microsoft Paint と Photos は、ローカル生成画像にサーバー発行の GUID を不可視ウォーターマークとして埋め込む
リバースエンジニアリングにより、Paint と Photos がサーバー発行の GUID をローカル生成 AI 画像のピクセルにどのように埋め込んでいるかが明らかになりました。
TL;DR
Microsoft Paint はローカルおよびクラウドでの画像生成の両方をサポートしています。
Paint と Photos はローカル AI モデルも出荷しています。
両アプリは、モデレーションのためにプロンプトをリモートサーバーに送信します。
サーバーは、モデレーションされたプロンプトと共に GUID を返します。
GUID は、不可視ウォーターマークとしてローカル生成画像に埋め込まれます。
個別の可視ウォーターマーク設定は、この不可視ウォーターマークを制御しません。
Copilot+ PC では、画像生成はローカルですが、プロンプトモデレーションはリモートのままです。
Microsoft は、Paint が AI 生成画像に C2PA メタデータを追加すると開示しています。
AI 生成画像の保存は、C2PA 互換フォーマット(PNG、JPEG、GIF、.paint)に限定されます。
A curious look at Microsoft Paint
この研究は、Paint に関する私の好奇心から始まりました。最近、UCPD や WHESCVC のようなあまり探求されていない Windows の機能について調べることに成功し、Microsoft が Paint アプリに多くの AI 機能を added していることは以前から知っていました。Paint + AI を使って画像を生成する人がいるかどうかは知りませんが、画像生成がどのように機能するかを正確に知りたいと思っていました。始める前は、単にリモート API を呼び出して画像生成を行うだけだろうと予想していました。しかし、Binary Ninja MCP を Codex と共にセットアップして分析を開始した後、Microsoft が Copilot の一部として Windows にローカルモデルを出荷していることにすぐに気づきました。
Paint アプリは以下のパスにあります(はい、これらはすべて Windows Apps です):
C:\Program Files\WindowsApps\Microsoft.Paint_11.2605.71.0_x64__8wekyb3d8bbwe\PaintApp\
そして、.onnxe 拡張子を持つ4つのモデルファイルがあります:
seg.onnxe 23.1 MB
inseg_enc.onnxe 28.0 MB
inseg_dec.onnxe 16.5 MB
mager.onnxe 302.4 MB
seg.onnxe の形式は以前から知られており、つまり、文字列 Microsoft_2023 と XOR すると、通常の ONNX ファイルになります。しかし、他の3つの .onnxe ファイルの形式は当初異なって見えました。Microsoft はアルゴリズムを変更したのではなく、キーを変更しただけであることが判明しました。
segapi.dll には小さなキーレジストリが含まれています:
ps_enc_key.1.0.80-main -> "Microsoft_2023"
ps_enc_key.1.0.81-main -> 4,096 バイトの英数字文字列
復号後、onnx.checker.check_model() はすべてで機能します:
Model Graph
seg.onnx 1,094 ノード、入力 input_image、出力 output
inseg_enc.onnx 1,014 ノード、出力 image_embeddings
inseg_dec.onnx 1,133 ノード、入力は embeddings、points、masks;出力は masks
mager.onnx 15,284 ノード、image/mask 入力;出力は output
A visible watermark
これらのファイルを歩き回っていると、Watermarker.dll を見つけました:
これは私にとってそれほど驚くことではありませんでした。Paint アプリを操作している間に、生成する画像に可視ウォーターマークを埋め込む設定があることを発見していたからです。
可視ウォーターマークは、画像の右下にある小さな Copilot ロゴであり、これは完全に正常です。
そして、突然、AI に DLL を分析させて、不可視ウォーターマークも埋め込んでいるかどうかを尋ねることにしました。これは私のリバースエンジニアとしての直感の一部です。なぜなら、ファイルサイズが 1.67 MB であり、このような些細な機能(議論の余地がありますが、可視ウォーターマークは個別の DLL を必要としないかもしれません)にしては異常に大きいからです。明らかに、最近の Claude Code テキストウォーターマークの発表も、この可能性について考えるきっかけとなりました。
An invisible watermark
まず、可視ウォーターマークは AddPerceptibleWatermark によって追加されます:
CPBDoc::Save(...)
|-- perceptible-watermark save helper(bitmap, WatermarkSetting)
| +-- WatermarkSetting::Never
| `-- return the original bitmap
| +-- WatermarkSetting::AskEveryTime
| `-- show the Yes / No confirmation popup
| +-- No: return the original bitmap
| `-- Yes: continue
| +-- Always or confirmed Yes
+-- Paint::AI::GetPerceptibleWatermarkSvg()
`-- Paint::AI::AddPerceptibleWatermark(bitmap, SVG stream)
`-- composite the visible Copilot logo
そして、別の WmkWriteWatermark 関数もあります:
Watermarker.dll!WmkWriteWatermark( output_pixels, payload, payload_length, width, height, stride, input_pixels, pixel_format);
コールツリーをトレースすると、ローカル Stable Diffusion 画像生成後に WmkWriteWatermark が呼び出されていることがわかります。そして、WmkWriteWatermark が失敗した場合、Paint は生成全体をエラーに変換します。画像なしで返すのではなく:
CocreatorViewModel::GenerateImageAsync(...)
|-- Paint::AI::StableDiffusionHelpers::GenerateAsync(..., watermarkId, ...)
|-- Microsoft.ImageCreation.ImageGenerator
|-- NPU-generated image result
| +-- output safety/moderation checks
| +-- Paint::AI::AddWatermark(bitmap, watermarkId)
| | | `-- Watermarker.dll!WmkWriteWatermark(...)
| | | +-- success: return the watermarked bitmap
| `-- failure: turn generation into an error
`-- construct successful StableDiffusionResult
次に、入力ペイロードが実際には何であるかを尋ねるのが自然です。それは 16 バイトであるとすぐに明らかになります:
if (payload_length < 16) return -6;
if (payload_length > 16) return -5;
コードがペイロードが短すぎるか長すぎる場合に2つの異なるエラーコードを使用しているのは面白いです。その後、関数は長さパラメータを無視し、ペイロードをコピーする際にハードコードされたループ境界を使用します:
for (size_t i = 0; i < 16; i++) message.push_back(payload[i]);
16 バイトのペイロードが何であるかはまだわかりませんが、後述するように、それは GUID です!
WmkWriteWatermark は GUID を直接埋め込みません。そのラッパーは、次の 18 バイト(144 ビット)メッセージを構築します:
0x4c || GUID[0..15] || (16 GUID バイトの合計を modulo 256 した値)
コアエンコーダーは、使用可能な画像寸法を 8 の倍数に丸め、144 個のカウンター(各ビットに対応)を保持します。各ビットは少なくとも 3 回配置される必要があります。エンコーダー自体は次のように要約できます:
WmkWriteWatermark(output, guid, 16, width, height, stride, input, format)
|-- validate pointers, format, stride, and payload length
+-- require width >= 192 and height >= 192
+-- construct payload
| `-- 0x4c || GUID || byte-sum checksum
+-- expand 18 bytes into 144 individual bits
+-- round usable dimensions down to 8-pixel boundaries
+-- scan/select suitable image blocks
+-- quantize selected block/matrix values according to each bit
+-- require at least three successful placements per bit
| | | `-- insufficient capacity -> return -8
`-- reconstruct RGB pixels into the output buffer
埋め込みループは、選択された画像ブロックに対して小さな量子化された変更を実行します。3x5 の行列演算と行列分解ルーチンが含まれており、24.0、0.25、0.5、0.2 などの定数を使用します。これは、コンテンツ適応型ブロックドメイン、SVD スタイルのウォーターマークのように見えます。私は画像ウォーターマーキングの専門家ではありませんが、1つのことは明らかです – これは不可視ウォーターマークです!
AI はこの関数を直接呼び出すコードを書き、合成された 512x512 BGRA 画像でテストしました – ウォーターマークを追加した後、262,144 ピクセルのうち 193,376 ピクセルが変更されました。これが次の質問につながりました。
ウォーターマークの入力はどこから来るのか?
a GUID from remote prompt moderation
WmkWriteWatermark の境界では、ペイロードはポインタと長さのみです。16 バイトである必要があることを知っていたことは手がかりでしたが、多くのものが 16 バイトになり得ます。そのため、呼び出し元を逆にたどることにしました。
PaintAIManager.dll の直接のラッパーには、次のようなシンボリックなシグネチャがあります:
Paint::AI::AddWatermark( Gdiplus::Bitmap& image, winrt::guid const& watermarkId);
winrt::guid、やばい!
これで、16 バイトのウォーターマークペイロードが実際には GUID であることがわかりました。
ソースをさらに追跡すると、GUID は実際にはネットワークリクエストから来ていることがわかります。Paint がローカル画像モデルを実行する前に、AIServices.dll はプロンプトとスタイルを以下に送信します:
https://apsaiservices-a0fqcjc6bzbhgdcd.b02.azurefd.net/ v1/paint-cocreator/moderate-prompt
リクエストは JSON で、少なくとも次のフィールドが含まれています:
{
"prompt": "...",
"style": "...",
"lastPromptGenerationId": "..."
}
レスポンス p