微软画图和照片应用会给本地生成的图片偷偷嵌入 GUID 隐形水印
文章摘要
逆向工程师 Xusheng Li 出于好奇拆解了 Windows 自带的画图(Paint)应用,想搞清楚它的 AI 生图功能到底怎么工作。他原本以为不过是调个远程 API,结果发现微软真的把本地模型塞进了 Windows:画图目录下有四个 .onnxe 扩展名的模型文件(seg、inseg_enc、inseg_dec、mager,最大的 302MB)。这些文件的加密方式已知一半——用字符串 Microsoft_2023 做 XOR 就能还原成正常 ONNX,而另外三个用的是不同的密钥;他在 segapi.dll 里找到了一个小小的密钥表,第二个密钥是一段 4096 字节的字母数字串。解密后所有模型都能通过 ONNX 的模型校验。
真正的发现来自一个叫 Watermarker.dll 的文件。画图确实有一个用户可见的水印设置——在图片右下角叠一个小小的 Copilot 徽标,这很正常。但作者的逆向直觉告诉他不对劲:这个 DLL 有 1.67MB,对于「贴个小图标」这种琐碎功能来说大得离谱(何况叠图标根本不需要单独一个 DLL)。他坦言,Claude Code 前不久宣布文本水印的新闻也促使他往这个方向想。
拆开之后,可见水印走的是 AddPerceptibleWatermark,受用户设置(从不 / 每次询问 / 总是)控制。但同一个 DLL 里还有一个完全不同的函数 WmkWriteWatermark,调用链显示它在本地 Stable Diffusion 生成图片之后被调用——而且如果它失败了,画图会把整次生成直接判定为错误,而不是把没水印的图返回给用户。这个函数要求载荷严格等于 16 字节(短了返回 -6,长了返回 -5,作者觉得用两个不同错误码挺有意思),然后忽略长度参数、用硬编码的循环边界拷贝 16 字节。包装层构造出一条 18 字节(144 位)的消息:0x4c + 16 字节 GUID + 16 个字节之和对 256 取模的校验和。编码器把可用图像尺寸向下取整到 8 的倍数,维护 144 个计数器(每位一个),要求每一位至少被放置三次;嵌入循环包含 3×5 矩阵运算和矩阵分解例程,用到 24.0、0.25、0.5、0.2 这些常数——看起来是一种内容自适应的块域 SVD 式水印。作者让 AI 写代码直接调用这个函数,用合成的 512×512 BGRA 图测试,加水印后 262,144 个像素里有 193,376 个发生了变化。这毫无疑问是隐形水印。
那这 16 字节从哪来?顺着调用者往回走,包装函数的签名是 Paint::AI::AddWatermark(Gdiplus::Bitmap&, winrt::guid const&)——所以确实是个 GUID。再往上走就是全文最关键的发现:这个 GUID 来自一次网络请求。在运行本地图像模型之前,AIServices.dll 会把提示词和风格发到微软 Azure 前门的一个「提示词审核」端点(/v1/paint-cocreator/moderate-prompt),请求体含 prompt、style 和上一次的 lastPromptGenerationId;服务器返回 revisedPrompt、promptGenerationId、watermarkId 和 containsHumanReference。作者复用画图自己的已认证会话实际发了一次请求验证,服务器返回 HTTP 200 和一对真实 GUID;他换成含人物的提示词时,containsHumanReference 变成了 true,说明这是服务端对提示词是否涉及人物的分类。换句话说,「本地生成」并不意味着整个操作都在本地:微软接收并审核你的提示词,然后签发那个被嵌进本地生成图片的唯一 GUID。更进一步,画图会把上一次的 promptGenerationId 随下一次审核请求一起发出去,让连续的多次请求可以被显式串联起来。
故事还有下半段。画图不只改像素,还会给保存的文件附上 C2PA「内容凭证」。作者保存了一张真实生成的图并检查 PNG 分块,发现紧跟 IHDR 之后有一个 18,979 字节的 caBX 块,里面是签名过的 C2PA 清单。清单中的 c2pa.soft-binding 字段写着算法 com.microsoft.invismark.1,作用域是整张图,而它的值——正是服务器返回的那个 watermarkId,与嵌进像素里的完全一致。C2PA 把这称为「软绑定」:一个从内容中派生或嵌入内容的值,使得即便文件级清单被剥掉,内容仍能与其溯源记录匹配上。微软对这条断言做了密码学签名。
作者随后解释了为什么画图需要本地水印实现:画图有两条生成路径。Image Creator 走 Azure OpenAI ImageGen,生成、加水印、打包溯源全在微软云端完成,画图收到的已是成品;而 Cocreator 在 Copilot+ PC 上由 NPU 本地生成——云端生成器可以在返回前加水印,本地生成器不行,所以画图必须自己动手改本地像素。这也解释了为什么水印失败会导致整次生成失败。另一个佐证是保存格式:从 Image Creator 面板直接保存只提供 PNG;把 AI 结果应用到画布后,可选格式仍被限制为 PNG、JPEG、GIF 和画图自有的 .paint——经典的 BMP 明显缺席。这与 C2PA 支持的格式完全吻合,因为 C2PA 规范明确指出 BMP 无法内嵌任意清单数据。作者还提出一个安全问题:如果云端生图端点能被诱导在加水印和打包溯源之前返回图片,就可能拿到两种标记都没有的云端生成图;这究竟算预期行为、产品 bug 还是安全漏洞,取决于微软自己的信任边界设计,三种可能都还开着。
最后作者发现微软照片应用(Photos)里有同名的 Watermarker.dll,其 Image Creator 和 Restyle Image 两个功能同样在本地 Stable Diffusion 之后调用同一个水印函数。一个微妙差别是失败行为:照片应用在编码器报错时只记一句日志「水印将不会被应用」然后照常返回图片,而画图会把整次生成算作失败。至于微软的披露,官方支持页确实提到会做内容过滤、生成的图片会带 C2PA 清单、Image Creator 使用 Azure 在线服务且会收集用户与设备标识符用于滥用防范——但页面没有说明的是:这个 C2PA 清单里含有一个标识隐形像素水印的 GUID,而画图的本地生成路径的水印 GUID 来自远程提示词审核。作者的结论是,把这个功能叫作「内容凭证」并没有说谎,但它没让 Windows 用户意识到这里有一个与提示词绑定的标识符。
HN 评论精华
这条帖子拿到 859 分,是当周最热的技术帖之一。讨论几乎一边倒地把焦点从「AI 检测」转向了「唯一标识符与匿名性」,同时也有一支不小的声音在纠正标题的误导性。
- 提交者 ComputerGuru 在置顶评论里给出了本次讨论的基调(他注明帖子是 AI 辅助写作,且他只是提交者不是作者):「我更大的担忧不是『这张图能被识别为 AI 生成』,而是『数字版的打印机黄点被强加给了我们,只不过这次它能识别并检索出确切的用户、设备、时间、地点、文档』,把仅剩的隐私幻觉彻底摧毁了。」他提出的下一步是:能否把 watermarker.dll 换成一个(已签名的)空操作垫片,或者中间人劫持那次 API 调用,至少换上一个与自己设备账号无关的(全零?)GUID。
- weberer 的一条被广泛认同:「AI 这个角度是个障眼法。真正的问题是他们在你创建的每张图里偷偷加了一个唯一标识符。如果有人不喜欢你的表情包,他只要向微软发一份版权传票,就能立刻拿到你的全名、地址、邮箱、电话和微软账号关联的所有数据。跟年龄验证一样,这是对互联网匿名性开战的又一件武器。」luciana1u 一句话总结:「GUID 就是破绽所在——这不是为了保护艺术家,是为了日后能证明来源。没人会为了用户的利益在本地文件里嵌一个唯一 ID。」ozereray1 说这是重大的隐私越界,让人怀疑还有多少本地工具在悄悄给用户生成的内容打标。
- 打印机追踪点的类比被反复提起。fchicken 把它推得更远:「很快就会变成打印机那套『青色不足,无法打印黑白文档』——『无网络连接,无法运行本地 AI 模型』。越想越觉得是一回事。」petjuh 想起苏联时期的打字机会加入可追溯到具体机器和购买者的标识。lesspassiveobse 讽刺道:朝鲜的 RedStar OS 十年前就这么干了,「很高兴看到西方赶上来了」。
- 「本地却要联网审核」这一点引发了最直接的愤怒。aucisson_masque:「这是你的电脑,在本地运行,但微软可以对你说不。就像你想打开一个文件夹却要向微软申请许可。」userbinator 指出本地 SD(尤其早期版本)本来就没有审查,微软等于给它加上了会向总部汇报的间谍软件,「隐形水印还不是最糟的,最糟的是微软在记录你与模型的每一次交互,而这些数据本来根本不需要离开你的机器」。tgsovlerkhgsel 认为这像是数据保护机构该介入的隐私违规,编辑补充说「据说某处有披露,但这仍然彻底摧毁了本地生成的意义」。
- 标题误导的纠正也很集中。red_admiral、ranger_danger 和 phendrenad2 都指出:水印只作用于 AI 生成或 AI 编辑的图片(包括本地模型跑的),并不是你手工在画图里画的每一张图。phendrenad2 还给出了验证方法:存一张纯黑图,看保存后是否突然多出别的像素值。不过 JoeBOFH 报告了误判:他粘贴了一张截图只想缩放一下,就收到了「此图由 AI 制作,将被相应标记」的横幅,于是他转去装了 Paint.net。VCFundedGenYer 提醒这不是孤例——几个月前微软曾错误地给所有 Azure DevOps 提交打 Copilot 标记,无论有没有 LLM 参与,被大量 GitHub issue 轰炸后才撤掉。
- 也有为水印辩护的声音。Delphiza 说:「我理解隐私顾虑,也认为我们有权要求微软说明它的工具在做什么。但我担心有一天我们回头会想,当初为什么没有更努力地去签名和保存人类的创作真实性。『AI 修改过』这个戳应该成为数字血统工具链的一部分。」imnotr0b0t 认为从对抗深度伪造和满足欧盟要求的角度看是说得通的,令人不安的只是用户没被告知。sixothree 希望所有相机都能给照片做数字签名,好证明照片是真实的。hojinkoh 说微软其实处在一个能为某些机构提供价值的位置上。
- 技术细节的追问:burnoutdv 问这个指纹到底有多鲁棒——能不能扛住 JPEG 压缩?是在整图重复还是只在一个区域?如果那块区域碰巧全黑,JPEG 不就把它抹掉了吗?dagaci 贴出微软自己的开源实现 github.com/microsoft/InvisMark,可以对照源码看水印算法。gigel82 说他反而惊讶于微软没有把整张图上传到服务端去打水印——把水印生成器发到用户机器上,会让有动机的人很容易搞清楚它怎么工作并写出「水印去除器」。angry_octet 提醒截图工具(Snipping Tool)很可能也在做同样的事,并给出反制建议:不要分享你无法在字节层面验证的图片,对低位噪声做滤波,不要信任复杂的容器格式,尽量用 PPM 这类简单格式。
- 大量评论的落点是「换系统 / 换软件」:megous「不用闭源软件,完了」,globular-toast「用自由软件,专有软件被其所有者用来控制你,这不是新鲜事」,kibwen 的表述更谨慎也更悲观:「在有开源审计和可复现构建证明之前,你应该假设所有商业图像编辑软件都在能塞的地方塞水印,包括你花大价钱买许可证的专业软件;而且即便今天没有,它们最终也会像打印机一样被迫这么做。」也有人推荐替代品:jspaint.app、Lazpaint、LibreOffice Draw、Paint.net。clickety_clack 则是纯粹的错愕:「我很久没用 Windows 了,听说画图已经不是一个点点鼠标涂像素的应用,实在震惊。他们完全可以让画图保持纯粹,把花哨功能放进新的图像编辑器里。」
- 还有几条元评论值得一提:claiir 抱怨「现在这些技术博客都散发着 Claude 的味道,明显被 LLM 灌水,读起来很痛苦」;naniel 则从相反角度感叹「有了 AI,逆向工程变得太疯狂了,知识产权已死」——这篇文章本身正是用 Binary Ninja MCP 配合 Codex 完成的。