
유니코드 변형 선택자를 주입하여 LLM 텍스트 워터마크를 우회합니다. SynthID 생성기, mean-g 탐지기, 정규화 방어 및 엔트로피 실험을 포함합니다.
안녕하세요, Alosh입니다 ✌🏻
네, 하지만 작동하는 공격 계열은 하나뿐이고, 모두가 짐작하는 그 공격은 아닙니다.
유니코드 변형 선택자(variation selectors)(카테고리 Mn, U+FE00U+FE0F 및 U+E0100U+E01EF)는 검출기를 임계값 아래로 몰아넣고 그 상태를 유지합니다. 제가 시도한 다른 모든 보이지 않는 문자 공격은 입력 정규화 한 줄로 완전히 되돌려졌습니다. 변형 선택자는 그렇지 않은데, 이는 의미 있는 코드포인트(이모지 표현, CJK 변형)라서 NFKC가 접어 없애지도 않고, 또 없애서도 안 되기 때문입니다.
세 모델, 두 도메인에서 재현됐습니다:
| 모델 | 도메인 | 기준 z | vs16_30 이후 | 편집률 |
|---|---|---|---|---|
| gpt-oss-20b | 산문 | 45.03 | 0.72 | 57% |
| gpt-oss-20b | 코드 | 37.24 | 0.68 | 58% |
| Qwen3.8-27B | 산문 | 35.50 | -0.67 | 57% |
임계값은 z = 2.33입니다. 세 경우 모두 그 아래에 도달하며, 정규화 후에도 아래에 머뭅니다(각각 0.09, 0.45, -0.78). 사람 눈에는 텍스트가 동일하게 렌더링됩니다.
두 번째 실제 발견은 공격이 전혀 필요 없습니다: 낮은 엔트로피 텍스트는 애초에 워터마크가 거의 묻지 않는다는 것입니다. Qwen3.8-27B의 코드 생성은 아무것도 하지 않았는데도 깨끗한 기준 z = 4.31을 가지며, 이미 임계값에 가깝습니다.
Anthropic(그리고 그 이전의 Google DeepMind, SynthID-Text 논문)은 키 기반 토너먼트로 토큰 샘플링에 편향을 주는 방식으로 생성 텍스트에 워터마크를 새겼습니다. 신호는 숨겨진 문자가 아니라 어떤 토큰이 선택되었는지에 있습니다. 저는 이것이 찾기 및 바꾸기 도구를 가진 동기 부여된 사람에게 얼마나 취약한지 알고 싶어서, 생성기와 훈련되지 않은 mean-g 검출기를 만들고 공격을 퍼부었습니다.
검출기 임계값은 z = 2.33(오탐률 1%)입니다. 이보다 높으면 워터마크로 검출됩니다. 저는 워터마크가 있는 텍스트를 생성하고, 공격하고, 다시 토큰화한 뒤 점수를 매겼습니다. 모든 결과 테이블에는 roundtrip 대조군(공격하지 않은 워터마크 텍스트)이 있어 해당 행에서 검출기가 실제로 작동한다는 것을 확인할 수 있습니다. 이 연구에서 두 번, 버그로 모든 z가 0으로 붕괴했고 대조군만이 그것을 잡아냈습니다.
공격 표면은 유니코드 범주에 따라 깔끔하게 나뉘는데, 그것이 사실상 전부였습니다.
효과 있음(카테고리 Mn, 비간격 결합 부호):
효과 없음(카테고리 Cf, 형식 문자):
Cf 문자는 의미 내용이 없는 순수한 형식 문자이므로, 정규화기가 그것들을 통째로 제거해도 잃을 것이 없습니다. Mn 변형 선택자는 이모지와 CJK 텍스트에서 실제 의미를 지니므로, 무차별적으로 제거하면 정당한 문서가 손상됩니다. 이러한 비대칭성 때문에 한 계열은 방어가 가능하고 다른 계열은 그렇지 않습니다.
현실적 범위: 강한 기준값을 임계값 아래로 밀어내려면 대략 20% 이상의 문자를 건드려야 합니다. 하지만 삽입된 문자는 보이지 않으므로 어떤 편집률에서도 텍스트는 정상적으로 읽힙니다. 시각적으로 동일함이 검증되어 있습니다(src/fidelity_check.py 참조). 네 계열은 아무것도 렌더링하지 않는 문자만 삽입합니다. homoglyph는 솔직한 예외입니다(글꼴에 따라 다르며, 보이지 않음이 보장되지 않습니다).
제가 처음에 가졌던 두려움은 지루한 것이었습니다. 엠 대시를 하이픈으로 바꾸고, "and"를 "&"로 바꾸고, markdown을 제거하면 그냥 통하지 않을까, 그러면 워터마크가 보안 연극(security theater)에 불과한 게 아닐까였습니다. 온라인의 많은 사람들이 정확히 그렇게 가정합니다.
그것은 작동하지 않습니다. gpt-oss-20b에서 32k 토큰까지 확장한 전체 표면 편집 래더는 다음과 같습니다:
세 모델(0.8B, 4B, 20B = 294개 셀)에서 정확히 하나의 셀만 임계값을 넘었습니다: 4B에서 가장 짧은 길이일 때 모든 단어의 39%를 삭제한 경우입니다. 그건 공격이 아니라 문서를 분쇄하는 것입니다.
저를 놀라게 한 두 가지가 있습니다:
전체 메커니즘과 공격별 테이블은 docs/FINDINGS.md에 있습니다.
대화형 버전 사용해 보기 → 실제 연구 샘플에 before/after 표시 토글이 있고, 자신의 텍스트에 공격 변형을 적용해 볼 수 있는 플레이그라운드도 있습니다. 임의로 붙여넣은 텍스트가 정말로 워터마크인지 여부는 알려주지 않으며(그러려면 우리가 갖고 있지 않은 키가 필요합니다), 그 점도 명시되어 있습니다. 생성 스크립트는 site/를 보십시오.
워터마크는 모델의 토큰별 불확실성에 실려 있습니다. 모델이 다음 토큰에 대해 확신하는 곳에서는 토너먼트가 그것을 편향시킬 여지가 없으므로 신호가 들어가지 않습니다. 즉, 워터마크는 낮은 엔트로피 텍스트에서 약하고, 코드는 엔트로피가 낮습니다.
Qwen3.5-4B, 산문 vs 코드, 512토큰 샘플, 아무 공격도 없음:
| 도메인 | 엔트로피 | z |
|---|---|---|
| 산문 | 1.19 bits/tok | 11.1 |
| 코드 | 0.55 bits/tok | 5.0 |
z 비율은 0.45배, 엔트로피 비율은 0.46배로 함께 움직였는데, 이것은 메커니즘이 그대로 드러나는 것입니다. 코드 샘플 8개 중 3개는 아무 공격 없이 탐지 임계값 이하로 떨어졌습니다. 가장 빠듯한 샘플(뼈대만 있는 알고리즘, 0.2 bits/token)은 1.7을 기록해 미탐지였습니다.
규모가 커질수록 더 극단적이 됩니다. 아무 공격도 없는 기준 z는 다음과 같습니다:
| 모델 | 산문 | 코드 | 비율 |
|---|---|---|---|
| gpt-oss-20b | 45.03 | 37.24 | 0.83 |
| Qwen3.8-27B | 35.50 | 4.31 | 0.12 |
Qwen3.8-27B의 코드 출력은 너무 템플릿화되어 있어서, 공격받지 않은 깨끗한 워터마크가 z = 4.31, 즉 2.33 임계값을 간신히 웃도는 수준에 머뭅니다. 적대자가 필요 없습니다.
즉, 도메인 전체에 단일 신뢰도 임계값을 적용하는 것은 안전하지 않으며, 짧은 코드 스니펫은 워터마크를 새기기가 거의 불가능합니다. JSON, 설정, 구조화 추출, 보일러플레이트까지 일반화됩니다.
이런 검출기를 하나라도 배포한다면, 입력 정규화만으로 대부분 해결되지만 전부는 아닙니다:
3번과 4번이 순진한 정규화기가 놓치는 부분입니다.
빠진 부분에 대해 솔직히 말하자면.
src/synthid_robustness.py generator + attack ladder + mean-g detector + normalizer
src/code_vs_prose.py the entropy experiment (with per-token entropy tap)
src/fidelity_check.py proves the stego attacks are visually identical
src/synthid_mlx.py watermarking bridge for Apple Silicon (MLX), validated vs HF
src/prompts_code.py prose / code / mixed prompt sets
src/build_report_data.py assembles results/ into the tables in FINDINGS.md
results/ the JSON this is all computed from
docs/FINDINGS.md every table, the defense hierarchy, the bugs I caught
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
# generate watermarked docs + run the full attack ladder on a model
MODEL=Qwen/Qwen3.5-4B LENGTHS=1024,2048,4096,8192 N_DOCS=2 \
DOCS=docs_4b.json OUT=res_4b.json python src/synthid_robustness.py
# the entropy experiment (code vs prose)
python src/code_vs_prose.py --model Qwen/Qwen3.5-4B --out res_cvp_4b.json
# the variation-selector / stego attacks, scored raw AND post-normalization
ATTACK_SET=desync DEFENSE=1 PROMPT_SET=prose MODEL=Qwen/Qwen3.5-4B \
DOCS=docs_4b.json OUT=res_defense.json python src/synthid_robustness.py
PROMPT_SET에는 prose, code, mixed 중 하나를 씁니다. FAST_WM=1은 numpy 워터마크 브리지를 사용합니다(어휘 수가 적고 CPU가 강력한 모델에서 더 빠름). FAST_WM=0은 HF의 GPU 프로세서를 사용합니다(어휘 수가 많은 모델에서 훨씬 빠름; H100에서는 GPU 사용률 0%와 46%의 차이였습니다).
워터마킹에는 전체 다음 토큰 분포가 필요하므로 transformers(CUDA 네이티브 MXFP4, 또는 MPS/CPU)를 통해 실행됩니다. Ollama와 llama.cpp는 생성 중간에 logits을 노출하지 않기 때문에 그렇게 할 수 없습니다. Apple Silicon에서는 src/synthid_mlx.py가 MLX 생성과 워터마크 계산을 연결하며, HF 레퍼런스와 비트 단위로 동일함을 검증했습니다.

(이 모든 것을 시작한 밈. 결국 필요한 건 찾아바꾸기가 아니라 변형 선택자였다.)
참고: 저는 구현과 네 대의 머신(Mac, 제 4090, 대여한 3090, H100)에서 실험을 다시 실행하는 데 Claude Code를 많이 사용했습니다. 실험 설계, 시도해보고 싶었던 공격, 프레이밍 결정은 모두 제 것입니다. Claude는 모든 공격을 그에 대응하는 방어와 함께 측정하라고 주장했고, 그래서 stego 테이블에는 raw 열만 있는 것이 아니라 raw 열과 normalized 열이 함께 있습니다. 그것이 "보이지 않는 문자가 워터마크를 깨뜨린다"는 말을, 정규화기를 살아남는 것은 Mn 범주 문자뿐이라는 실제 발견으로 바꿔 놓았습니다.
| 공격 | 하는 일 | 편집률 | 정규화 후에도 생존? |
|---|
vs16_30 | 약 30%의 문자 뒤에 변형 선택자 삽입 | 57% | 예 |
vs16 | 약 10%의 문자 뒤에 변형 선택자 삽입 | 23% | 예 (z 3.46) |
vs_supp | 보충 평면 선택자(U+E0100+) 삽입 | 24% | 예 (z 3.40) |
homoglyph | 라틴 문자 a 대신 키릴 문자 а 사용 (카테고리 Ll) | 9% | 예, 하지만 효과 약함 |
| 공격 | 원본 z | 정규화 z | 결과 |
|---|
zwsp_30 | -0.09 | 35.68 | 완전히 되돌려짐 |
combo | 0.92 | 35.68 | 완전히 되돌려짐 |
bidi | 24.37 | 35.68 | 완전히 되돌려짐 |
nbsp | 41.04 | 46.51 | 거의 변화 없음 |
| 공격 | 편집률 | z @ 1k | z @ 32k |
|---|
| roundtrip (대조군) | 0% | 25.7 | 104.3 |
| 엠 대시 → 하이픈 | ~0% | 26.4 | 113.7 |
| 모든 markdown 제거 | 13.6% | 27.2 | 103.3 |
| AmE → BrE + 약어화 | 1.3% | ~28 | ~100 |
| 모든 단어의 40% 삭제 | 38% | 4.9 | 25.4 |