Microsoft Paint en Foto's voegen server-geïssueerde GUID's toe als onzichtbare watermerken in lokaal gegenereerde afbeeldingen

Samenvatting (TL;DR)

  • Microsoft Paint ondersteunt zowel lokale als cloud-gebaseerde beeldgeneratie.
  • Paint en Foto's worden geleverd met lokale AI-modellen.
  • Beide apps sturen de prompt naar een externe server voor moderatie.
  • De server stuurt een GUID terug, samen met de gemodereerde prompt.
  • De GUID wordt als onzichtbaar watermerk in de lokaal gegenereerde afbeelding geplaatst.
  • Een aparte instelling voor zichtbare watermerken heeft geen invloed op dit onzichtbare watermerk.
  • Op Copilot+ PC's is de beeldgeneratie lokaal, maar blijft de prompt-moderatie extern.
  • Microsoft meldt dat Paint C2PA-metadata toevoegt aan AI-gegenereerde afbeeldingen.
  • Het opslaan van AI-gegenereerde afbeeldingen is beperkt tot formaten die C2PA ondersteunen: PNG, JPEG, GIF en .paint.

Een nieuwsgierige blik op Microsoft Paint

Dit onderzoek begon vanuit nieuwsgierigheid naar Paint. Na succesvolle analyses van minder bekende Windows-functies zoals UCPD en WHESCVC, wilde ik onderzoeken hoe de recent toegevoegde AI-functies in Paint precies werken.

Aanvankelijk verwachtte ik dat de app simpelweg een remote API aanroept voor de beeldgeneratie. Echter, na analyse met Binary Ninja MCP en Codex, bleek dat Microsoft lokale modellen in Windows heeft opgenomen als onderdeel van Copilot.

De Paint-app bevindt zich in het volgende pad: C:\Program Files\WindowsApps\Microsoft.Paint11.2605.71.0x64__8wekyb3d8bbwe\PaintApp\

Hierin bevinden zich vier modelbestanden met de extensie .onnxe:

  • seg.onnxe (23,1 MB)
  • inseg_enc.onnxe (28,0 MB)
  • inseg_dec.onnxe (16,5 MB)
  • mager.onnxe (302,4 MB)

Het formaat van seg.onnxe was al bekend: wanneer dit bestand wordt XORed met de string Microsoft_2023, wordt het een normaal ONNX-bestand. Voor de andere drie bestanden bleek Microsoft het algoritme niet te hebben gewijzigd, maar enkel de sleutel. In segapi.dll bevindt zich een klein register met sleutels:

  • psenckey.1.0.80-main → "Microsoft_2023"
  • psenckey.1.0.81-main → een alfanumerieke string van 4.096 bytes

Na decryptie werken de modellen als volgt:

ModelGraphInput/Output
seg.onnx1.094 nodesinput: input_image, output: output
inseg_enc.onnx1.014 nodesoutput: image_embeddings
inseg_dec.onnx1.133 nodesinputs: embeddings, points en masks; output: masks
mager.onnx15.284 nodesinputs: image/mask; output: output

Een zichtbaar watermerk

Tijdens het bestuderen van deze bestanden stuitte ik op Watermarker.dll. Dit was niet verrassend, aangezien Paint een instelling heeft om een zichtbaar watermerk (een klein Copilot-logo rechtsonder) in de afbeelding te plaatsen.

Vanuit mijn intuïtie als reverse engineer vroeg ik mij af of de DLL ook onzichtbare watermerken kon embedden. De bestandsgrootte van 1,67 MB is namelijk ongebruikelijk groot voor een eenvoudige functie als het plaatsen van een logo.

Een onzichtbaar watermerk

Het zichtbare watermerk wordt toegevoegd via AddPerceptibleWatermark. De logica is als volgt: CPBDoc::Save(...)perceptible-watermark save helper → (indien ingesteld op 'Always' of na bevestiging) → Paint::AI::GetPerceptibleWatermarkSvg()Paint::AI::AddPerceptibleWatermark(bitmap, SVG stream).

Er is echter ook een andere functie: WmkWriteWatermark. Watermarker.dll!WmkWriteWatermark(outputpixels, payload, payloadlength, width, height, stride, inputpixels, pixelformat);

Tracing van de call tree laat zien dat WmkWriteWatermark wordt aangeroepen na een lokale Stable Diffusion-beeldgeneratie. Als deze functie faalt, zet Paint de gehele generatie om in een foutmelding in plaats van de afbeelding zonder watermerk terug te geven.

De payload moet exact 16 bytes zijn. Als de payload te kort of te lang is, geeft de code verschillende foutcodes terug. De functie negeert vervolgens de lengteparameter en gebruikt een hard-coded loop om de payload te kopiëren.

De wrapper construeert een bericht van 18 bytes (144-bit): 0x4c || GUID[0..15] || (som van de 16 GUID-bytes modulo 256)

De encoder werkt als volgt:

  1. Valideert pointers, formaat, stride en payload-lengte.
  2. Vereist een minimale breedte en hoogte van 192 pixels.
  3. Construeert de payload (0x4c + GUID + checksum).
  4. Zet de 18 bytes om in 144 individuele bits.
  5. Rondt bruikbare dimensies af naar grenzen van 8 pixels.
  6. Selecteert geschikte image-blocks en kwantiseert waarden op basis van elke bit.
  7. Vereist dat elke bit minstens drie keer is geplaatst; zo niet, dan volgt een foutmelding (-8).
  8. Reconstrueert de RGB-pixels in de output-buffer.

Dit lijkt op een content-adaptive block-domain, SVD-style watermerk. Tests met een synthetische 512x512 BGRA-afbeelding toonden aan dat 193.376 van de 262.144 pixels veranderden na het toevoegen van het watermerk, wat bevestigt dat het onzichtbaar is voor het menselijk oog.

Een GUID via remote prompt-moderatie

De payload van 16 bytes bleek een GUID te zijn. De bron van deze GUID is een netwerkverzoek. Voordat Paint het lokale beeldmodel uitvoert, stuurt AIServices.dll de prompt en stijl naar: https://apsaiservices-a0fqcjc6bzbhgdcd.b02.azurefd.net/v1/paint-cocreator/moderate-prompt

Het JSON-verzoek bevat:

{
  "prompt": "...",
  "style": "...",
  "lastPromptGenerationId": "..."
}

De server reageert met:

{
  "revisedPrompt": "...",
  "promptGenerationId": "...",
  "watermarkId": "...",
  "containsHumanReference": false
}

Een test met de prompt "a cobalt blue circle above a tiny orange square" leverde een HTTP 200 response op met een specifieke watermarkId. Wanneer een prompt verwijst naar een mens, wordt containsHumanReference op true gezet.

De workflow is als volgt: PaintUI.dllIPromptModerationServicePaintAIManager.dllAIServices.dll!ModerateAsync(...) → HTTPS POST → ParseModerateResponsePaintUI slaat WatermarkId op → StableDiffusionHelpers::GenerateAsync → lokale Stable Diffusion resultaat → Paint::AI::AddWatermarkWmkWriteWatermark → gewijzigde RGB-pixels.

Dit betekent dat "lokaal gegenereerd" niet betekent dat de operatie volledig lokaal is. Microsoft ontvangt en modereert de prompt, waarna een unieke GUID wordt uitgegeven die Paint in de lokaal gegenereerde afbeelding embedt.

Dezelfde watermark-GUID in C2PA-metadata

Paint voegt daarnaast C2PA Content Credentials toe via ProvenanceHelper.dll en provenancesdk.dll. Voor de lokale Stable Diffusion-route ziet het proces er zo uit:

  1. Lokaal Stable Diffusion resultaat.
  2. Paint::AI::AddWatermark (voegt watermarkId toe aan pixels).
  3. AIServices.dll!SignIngredientOnlineAsync (verstuurt promptGenerationId en de afbeelding naar de server voor ondertekening).
  4. Ontvangst van server-geleverd C2PA-manifest.
  5. Manifest wordt ingevoegd in de uiteindelijke afbeelding.

In een PNG-bestand van de Image Creator bevindt zich een caBX chunk met een signed C2PA-manifest. Hierin staat:

{
  "c2pa.soft-binding": {
    "alg": "com.microsoft.invismark.1",
    "blocks": [
      {
        "scope": "the entire image",
        "value": "83424621-03cb-40e3-9808-a9fae837156d"
      }
    ]
  },
  "c2pa.actions.v2": {
    "actions": [
      {
        "action": "c2pa.watermarked",
        "description": "Content watermarked by Microsoft Responsible AI"
      }
    ]
  }
}

De watermarkId die in de pixels is geplaatst en de c2pa.soft-binding.value zijn identiek. C2PA noemt dit een "soft binding": een waarde die is afgeleid van of ingebed in de content, zodat de content nog steeds gekoppeld kan worden aan het provenance-record als het bestand-niveau manifest wordt verwijderd.

Waarom voegt Paint lokaal watermerken toe?

Paint heeft twee verschillende generatie-paden:

  1. Image Creator: Gebruikt Azure OpenAI ImageGen. Generatie, watermarking en provenance gebeuren in de cloud. Paint ontvangt een kant-en-klaar bestand.
  2. Cocreator (op Copilot+ PC's): De NPU genereert de afbeelding lokaal, maar Azure-services voeren de veiligheidschecks uit.

Omdat een lokale generator geen cloud-watermark kan ontvangen, moet Paint de pixels zelf aanpassen met de server-geïssueerde GUID. Dit verklaart ook waarom een fout in WmkWriteWatermark leidt tot een volledige generatiefout.

Ook de beperking in bestandsformaten is hierop gebaseerd. Bij het opslaan van AI-resultaten biedt Paint enkel PNG, JPEG, GIF en .paint aan. BMP is afwezig omdat dit formaat geen arbitraire manifest-data kan embedden zonder extern manifest, waardoor het C2PA-manifest zou verdwijnen.

Dit roept een beveiligingsvraag op: als het remote endpoint beelden kan teruggeven vóór de watermarking-stap, zou het mogelijk zijn cloud-gegenereerde beelden te verkrijgen zonder deze signalen.

De Foto's-app doet hetzelfde

De Watermarker.dll is ook aanwezig in de Microsoft Foto's-app: C:\Program Files\WindowsApps\Microsoft.Windows.Photos...\Watermarker.dll

Zowel de Image Creator als de "Restyle Image"-functie in Foto's maken gebruik van dezelfde watermark-wrapper. Een klein verschil is dat Foto's bij een fout in de encoder de afbeelding alsnog teruggeeft aan de gebruiker, terwijl Paint de generatie volledig blokkeert.

Wat Microsoft openbaart

Op de supportpagina van Image Creator meldt Microsoft dat er content-filtering wordt toegepast en dat afbeeldingen een C2PA-manifest bevatten om ze als AI-gegenereerd te identificeren. Ook wordt vermeld dat prompts en apparaat-identifiers worden verzameld voor misbruikpreventie.

Wat echter niet wordt uitgelegd, is dat het C2PA-manifest een GUID bevat die verwijst naar een onzichtbaar pixel-watermerk, of dat het lokale generatie-pad deze GUID ontvangt via remote prompt-moderatie.

Conclusie

Dit is het eerste onderzoek dat het onzichtbare watermerk-gedrag van Paint en Foto's documenteert. Hoewel Microsoft open is over C2PA-metadata, blijkt uit dit onderzoek dat deze metadata niet slechts een label op bestandsniveau is; de signed c2pa.soft-binding koppelt het manifest direct aan de identifier in de pixels.

Het onderscheid tussen cloud- en lokale paden verklaart de taakverdeling: cloud-generatie is vooraf watermarked, terwijl Cocreator de identifier na lokale NPU-inferentie moet embedden. In beide gevallen is "lokaal" niet offline: de prompt gaat naar Microsoft voor moderatie en het resultaat gaat via een online signing-proces.

Dit zou kunnen samenhangen met Artikel 50 van de EU AI Act, dat vereist dat AI-gegenereerde content een detecteerbaar, machine-leesbaar kenmerk draagt. Microsoft onthult de C2PA-metadata, maar niet de server-geïssueerde GUID of de koppeling met prompt-moderatie. Dit heeft privacy-implicaties.

Hoewel het mogelijk is om Paint of Foto's aan te passen om moderatie en watermarking te omzeilen, biedt dit geen nieuwe mogelijkheden; iedereen kan Stable Diffusion immers al direct draaien zonder deze mechanismen.