로컬에서 만든 이미지에 서버가 발급한 번호가 박힌다
프롬프트는 매번 마이크로소프트를 거쳤다
Windows의 Paint와 Photos는 기기에서 이미지를 생성할 때도 프롬프트를 마이크로소프트 서버로 보낸다. 서버가 돌려준 GUID가 픽셀 속 비가시 워터마크로 들어간다. 보이는 워터마크를 꺼도 이쪽은 안 꺼진다.
Paint 설정에는 AI 이미지에 워터마크를 넣을지 고르는 항목이 있다. 안 함 / 항상 / 매번 묻기 셋 중 하나다. 이걸 안 함으로 두고 그림을 만들어도 워터마크는 들어간다. 눈에 안 보이는 쪽이 따로 있다.
리버스 엔지니어 Xusheng Li가 8월 20일 공개한 분석이 Windows의 Paint와 Photos를 뜯어 이 경로를 끝까지 따라갔다. Hacker News에서 하루 만에 456점이 붙었다.
한 줄 정리
기기 안에서 그림을 만들어도 프롬프트는 마이크로소프트 서버를 한 번 거치고 서버가 내준 16바이트 GUID가 결과 이미지의 픽셀에 박힌다.
한눈에 보기
| 로컬 모델 | Paint 폴더에 .onnxe 파일 4개, 가장 큰 것이 302.4MB |
| 생성 전 통신 | 프롬프트와 스타일을 moderate-prompt 엔드포인트로 전송 |
| 서버 응답 | 수정된 프롬프트, promptGenerationId, watermarkId, 사람 등장 여부 |
| 픽셀 삽입 | 0x4c + GUID 16바이트 + 체크섬 1바이트, 총 144비트 |
| 변경 규모 | 512×512 시험에서 262,144픽셀 중 193,376개가 바뀜 |
| 보이는 워터마크 설정 | 이 삽입을 제어하지 않음 |
로컬 모델은 진짜로 들어 있다
Paint 설치 폴더에는 확장자가 .onnxe인 파일이 넷 있다. 문자열 하나로 XOR을 풀면 평범한 ONNX 모델이 된다. 분할용 모델 셋과 15,284개 노드짜리 생성 모델 하나다. Copilot+ PC에서 Cocreator를 쓰면 이 모델들이 NPU에서 돈다. 마이크로소프트가 "기기에서 생성한다"고 적어 둔 부분은 사실이다.
문제는 그 앞뒤다.
프롬프트는 생성 전에 나간다
Paint는 로컬 모델을 돌리기 전에 프롬프트와 스타일을 Azure Front Door 뒤의 검열 엔드포인트로 보낸다. 돌아오는 JSON은 이렇게 생겼다.
{
"revisedPrompt": "...",
"promptGenerationId": "74d9e06b-...",
"watermarkId": "83424621-...",
"containsHumanReference": false
}
watermarkId가 그림에 박히는 값이다. 워터마크 인코더는 이 GUID 앞에 0x4c를 붙이고 뒤에 바이트 합 체크섬을 붙여 18바이트를 만든 다음, 144개 비트를 이미지 블록에 최소 세 번씩 나눠 심는다. 512×512 시험 이미지에서 26만 픽셀 중 19만 개가 값이 달라졌다.
같은 GUID가 파일에 붙는 C2PA 매니페스트의 c2pa.soft-binding 항목에도 들어간다. 알고리즘 이름은 com.microsoft.invismark.1이다. 메타데이터를 지워도 픽셀 쪽 값으로 다시 맞출 수 있게 설계된 구조다.
Paint는 워터마크 삽입이 실패하면 생성 전체를 실패로 처리한다. 이미지를 안 준다. Photos는 로그만 남기고 그냥 넘어간다. 같은 회사 안에서도 두 앱의 판단이 다르다.
Photos도 같은 길을 쓴다
분석자가 Watermarker.dll을 찾다가 Photos 폴더에서 같은 이름의 파일을 발견했다. Photos의 Image Creator와 Restyle Image 두 기능이 로컬 Stable Diffusion을 돌린 다음 같은 인코더로 GUID를 심는다.
Paint와 다른 부분이 하나 있다. Photos는 promptGenerationId 쪽을 워터마크 값으로 쓴다. 서버가 한 번의 요청에 두 개의 GUID를 발급하는데 어느 쪽을 픽셀에 박을지가 앱마다 다르다는 뜻이다. 클라우드로 만든 이미지는 마이크로소프트 쪽에서 워터마크와 서명을 다 붙여 완성본으로 돌아온다. 로컬 생성은 그게 안 되니 앱이 직접 심는다. Watermarker.dll이 사용자 PC에 깔려 있는 이유가 여기에 있다.
두 갈래가 만나는 지점도 흥미롭다. 서명 요청은 promptGenerationId를 보내고 이미지 안에는 watermarkId가 이미 들어 있다. 서버가 두 값을 같은 요청에서 함께 발급했으니 나중에 둘을 이어 붙일 수 있다. 파일에서 메타데이터를 지워도 픽셀에 남은 값으로 원래 기록을 다시 찾는 구조다. C2PA는 이걸 소프트 바인딩이라고 부른다.
저장 형식까지 좁혀 놨다
AI로 만든 결과를 저장할 때 고를 수 있는 형식은 PNG, JPEG, GIF, 그리고 Paint 전용 포맷뿐이다. Paint의 상징이던 BMP가 목록에서 빠졌다. C2PA 규격이 BMP에 매니페스트를 못 넣기 때문이다. 파일 형식 선택지가 출처 표시를 유지하는 쪽으로 정리됐다.
마이크로소프트는 지원 문서에서 원격 콘텐츠 필터링과 C2PA 부착을 밝혀 뒀다. 프롬프트와 함께 사용자·기기 식별자를 수집한다는 문장도 있다. 밝히지 않은 부분은 그 매니페스트 안의 값이 픽셀에 심긴 워터마크의 식별자라는 점, 그리고 그 식별자가 프롬프트 검열 응답에서 나온다는 점이다.
"콘텐츠 자격 증명"이라는 이름은 정확하다. 다만 그 이름을 읽고 자기 프롬프트마다 다른 번호가 발급된다고 짐작할 사용자는 거의 없다. 공개된 문장과 실제 동작 사이의 거리가 이번 분석이 드러낸 부분이다.
anyAX 관점
고객 이름이나 캠페인 문구를 프롬프트에 적는 순간 그 문장은 남의 로그로 간다. Paint는 다음 요청에 직전 promptGenerationId를 lastPromptGenerationId로 함께 실어 보낸다. 흩어진 기록이 아니라 이어 붙은 줄이다. 동네 카페 신메뉴 포스터를 열 번 고쳐 뽑았다면 그 열 번이 한 줄로 꿰인다.
여기서 볼 건 워터마크의 옳고 그름이 아니다. 제품 설명에 "기기에서 처리"라고 적혀 있어도 그 문장이 "인터넷을 안 쓴다"는 뜻은 아니다. Cocreator는 NPU로 그림을 만들면서 마이크로소프트 계정과 인터넷 연결을 요구한다. 요구한다는 줄이 이미 답이었는데 아무도 그걸 근거로 읽지 않았다.
팀에서 도구를 고를 때 질문은 하나로 줄어든다. 이 기능이 오프라인에서 도는가. 비행기 모드로 바꾸고 버튼을 눌러 보면 5초에 끝난다. 마케팅 문구를 읽는 것보다 빠르다.
민감한 원고나 미공개 제품 이미지를 다루는 팀이라면 여기서 한 발 더 간다. "로컬"이라 적힌 기능도 계정 로그인을 요구하면 로컬이 아니라고 보고 시작하는 편이 안전하다. 뒤집힌 쪽을 확인하는 비용이 훨씬 싸다.
반대 방향도 있다. 이 구조는 이미지가 어디서 왔는지를 되짚을 수 있게 만든다. 외주 디자이너가 넘긴 시안이 AI 생성물인지, 경쟁사 광고 소재에 어떤 흔적이 남았는지 같은 질문에 기계가 답할 근거가 생긴다. 콘텐츠를 납품하거나 검수하는 쪽이라면 계약서에 넣을 문장이 하나 늘어난 셈이다. 어느 도구로 만들었는지를 묻는 대신 파일에 무엇이 남아 있는지를 확인하면 된다.
EU AI법 50조가 8월 2일부터 요구하는 건 기계가 읽을 수 있는 표시까지다. 프롬프트별로 다른 번호와 앞선 요청을 잇는 연결까지는 규정이 시키지 않았다. 규제가 요구한 최소치와 벤더가 실제로 심은 것 사이의 간격, 그 간격이 지금 우리가 못 보고 지나가는 자리다. 다음에 어떤 도구가 "규제 대응 완료"라고 적어 두면 그 문장이 최소치를 뜻하는지 그 이상을 뜻하는지부터 갈라 봐야 맞다.