
Evade las marcas de agua de texto de LLM inyectando selectores de variación Unicode; incluye el generador SynthID, el detector mean-g, defensas de normalización y experimentos de entropía.

Sí, pero solo funciona una familia de ataques, y no es la que todo el mundo asume.
Los selectores de variación Unicode (categoría Mn, U+FE00 a U+FE0F y U+E0100 a U+E01EF) llevan al detector por debajo del umbral y permanecen ahí. Todos los demás ataques con caracteres invisibles que probé se revierten por completo con una sola línea de normalización de entrada. Los selectores de variación no, porque son puntos de código con significado (presentación de emoji, variantes CJK) que NFKC no va a eliminar ni debería hacerlo.
Replicado en distintos modelos y dominios:
| modelo | dominio | z base | tras vs16_30 | tasa de edición |
|---|---|---|---|---|
| gpt-oss-20b | prosa | 45.03 | 0.72 | 57% |
| gpt-oss-20b | código | 37.24 | 0.68 | 58% |
| Qwen3.8-27B | prosa | 35.50 | -0.67 | 57% |
El umbral es z = 2.33. Los tres quedan por debajo tras el ataque, y permanecen por debajo tras la normalización (0.09, 0.45, -0.78 respectivamente). El texto se muestra idéntico para un lector humano.
El segundo hallazgo real no necesita ningún ataque: el texto de baja entropía apenas está marcado de entrada. Con el razonamiento desactivado, la salida de puro código de Qwen3.8-27B puntúa z = 4.31 sin atacar, cerca del umbral de 2.33 sin haberle hecho nada. Vuelve a activar el razonamiento y el mismo modelo, mismo dominio, misma longitud alcanza z = 25.53, porque el preámbulo de razonamiento es prosa ordinaria y porta la marca con normalidad.
Todo en este repositorio se midió en estos, y en ningún otro:
| modelo | parámetros | dónde | qué se ejecutó |
|---|---|---|---|
| Qwen3.5-0.8B | 0.8B | Mac (MPS) | escalera de ataques completa, 1k-8k |
| Qwen3.5-4B | 4B | Mac / 4090 | escalera de ataques completa + el experimento de entropía + estego en crudo/normalizado |
| DeepSeek-R1-Distill-Qwen-14B | 14B | Mac (MLX, 4-bit) | marca de agua + detección, trazas de razonamiento |
| gpt-oss-20b | 20B | 3090 / 4090 / RunPod | escalera de ataques completa hasta 32k, prosa + código, selectores de variación, ley de escala de tasa de inserción hasta 131k |
| Qwen3.8-27B | 27B | RunPod H100 / Modal H100 | prosa + código, selectores de variación, contraste de modo de razonamiento |
| gpt-oss-120b | 120B | Modal H100 (MXFP4) / RunPod H100 | prosa + código + razonamiento a 2k / 8k / 32k, escala parcial de tasa de inserción hasta 65k |
| DeepSeek-V4-Flash | 284B MoE | RunPod 2x H200 (FP8) | prosa + código a 2k / 8k |
No todos los experimentos se ejecutaron en todos los modelos, y las tablas siguientes indican cuáles se ejecutaron dónde. Los modelos más grandes se añadieron a medida que las preguntas interesantes se acotaban, así que la escalera de ataques es amplia en los pequeños y el trabajo de longitud y dominio está concentrado en los grandes.
Anthropic (y Google DeepMind antes que ellos, el artículo SynthID-Text) marca el texto generado sesgando el muestreo de tokens con un torneo con clave. La señal reside en qué tokens fueron elegidos, no en ningún carácter oculto. Quería saber cuán frágil es eso ante una persona motivada con un buscar-y-reemplazar, así que construí el generador más un detector de media-g sin entrenar y le lancé ataques.
El umbral del detector es z = 2.33 (1% de tasa de falsos positivos). Por encima de eso = marca de agua detectada. Generé texto marcado, lo ataqué, lo re-tokenicé y lo puntué. Cada tabla de resultados incluye un control roundtrip (texto marcado sin atacar) para que puedas ver que el detector funciona realmente en esa fila. Dos veces en este estudio un bug hizo que todos los z colapsaran a cero, y el control es lo único que lo detectó.
La superficie de ataque se divide limpiamente por categoría Unicode, que resultó ser toda la historia.
Funciona (categoría Mn, marcas sin espaciado):
| ataque | qué hace | tasa de edición | ¿sobrevive a la normalización? |
|---|---|---|---|
vs16_30 | selector de variación tras ~30% de los caracteres | 57% | sí |
vs16 | selector de variación tras ~10% de los caracteres | 23% | sí (z 3.46) |
vs_supp | selectores del plano suplementario (U+E0100+) | 24% | sí (z 3.40) |
homoglyph | а cirílica por a latina (categoría Ll) | 9% | sí, pero efecto débil |
No funciona (categoría Cf, caracteres de formato):
| ataque | z bruto | z normalizado | veredicto |
|---|---|---|---|
zwsp_30 | -0.09 | 35.68 | totalmente revertido |
combo | 0.92 | 35.68 | totalmente revertido |
bidi | 24.37 | 35.68 | totalmente revertido |
nbsp | 41.04 | 46.51 | apenas lo mueve |
Los caracteres Cf son puro formato sin contenido semántico, por lo que un normalizador puede eliminarlos en bloque sin perder nada. Los selectores de variación Mn conllevan un significado real en emoji y texto CJK, por lo que eliminarlos de forma indiscriminada corrompería documentos legítimos. Esa asimetría es la razón por la que una familia es defendible y la otra no.
El alcance práctico: esto requiere que se toque aproximadamente el 20% o más de los caracteres para empujar una línea base sólida por debajo del umbral, pero los caracteres insertados son invisibles, por lo que el texto se lee con normalidad a cualquier tasa de edición. Se verifica que es visualmente idéntico, véase src/fidelity_check.py. Cuatro familias insertan únicamente caracteres que no renderizan nada; los homoglifos son la excepción honesta (dependen de la fuente, no se garantiza que sean invisibles).
Mi miedo inicial era el aburrido: que los guiones largos y "and" por "&" y eliminar el markdown simplemente funcionarían, y que la marca de agua resultaría ser teatro de seguridad. Mucha gente en internet asume exactamente eso.
No funciona. Toda la escalera de ediciones superficiales sobre gpt-oss-20b, hasta 32k tokens:
| ataque | tasa de edición | z @ 1k | z @ 32k |
|---|---|---|---|
| roundtrip (control) | 0% | 25.7 | 104.3 |
| guion largo a guion | ~0% | 26.4 | 113.7 |
| eliminar todo el markdown | 13.6% | 27.2 | 103.3 |
| AmE a BrE + abreviaturas | 1.3% | ~28 | ~100 |
| borrar el 40% de cada palabra | 38% | 4.9 | 25.4 |
En los tres modelos sobre los que se ejecutó esta escalera (0.8B, 4B, 20B = 294 celdas), exactamente una celda cruzó el umbral: borrar el 39% de cada palabra en la longitud más corta en el 4B. Eso no es un ataque, eso es triturar el documento.
Total de celdas medidas en todo el estudio: 440 (294 de la escalera de ataques anterior, más 146 del barrido de leyes de escala de abajo que extiende la prosa hasta 131072 tokens).
Dos cosas me sorprendieron:
Mecanismo completo y tablas por ataque en docs/FINDINGS.md.