- Al encadenar variation selectors de Unicode, se puede ocultar detrás de un carácter una secuencia de bytes que no se ve en pantalla, pero que sigue presente al copiar y pegar
- Hay 256 variation selectors, de VS-1 a VS-256, por lo que se puede crear un mapeo que coincide exactamente con el rango de 1 byte
- Aunque se agreguen los bytes de
hello [0x68, 0x65, 0x6c, 0x6c, 0x6f] después de 😊, a simple vista se ve como un emoji normal
- Para decodificar, se buscan los rangos
U+FE00..U+FE0F y U+E0100..U+E01EF y se convierten de nuevo a bytes; el carácter base no tiene que ser un emoji
- Este método es un abuso de Unicode y puede usarse indebidamente para eludir filtros de contenido humanos o insertar marcas de agua en textos
Cómo datos invisibles se adjuntan a un carácter
- El texto Unicode se representa como una secuencia de codepoints (puntos de código), normalmente escritos en el formato
U+XXXX
- En caracteres latinos simples, el codepoint y el carácter visible en pantalla tienen una correspondencia 1:1
- Ej.:
U+0067 representa la letra g
- En otros sistemas de escritura, un carácter visible puede estar compuesto por varios codepoints
- Ej.: en devanagari, el carácter que se lee como
ki se representa con el par consecutivo U+0915 y U+0940
Usar variation selectors como almacenamiento de datos
- Unicode define 256 codepoints de variation selectors, llamados de VS-1 a VS-256
- Un variation selector no se muestra por sí mismo en pantalla, sino que se usa para cambiar la forma en que se muestra el carácter anterior
- La mayoría de los caracteres Unicode no tienen variantes asociadas, pero como Unicode busca compatibilidad futura, el código de procesamiento que no entienda su significado también debe preservar los variation selectors
- Si se agrega
U+FE01 (VS-2) después de U+0067 (g), en pantalla se ve como una g minúscula
- Al copiar y pegar, el variation selector también se conserva
- Como los 256 variation selectors son exactamente la cantidad necesaria para representar 1 byte, se puede ocultar 1 byte de datos detrás de cualquier codepoint Unicode
- La especificación Unicode no trata en detalle las secuencias con varios variation selectors seguidos, e implica que deben ignorarse durante el renderizado
- Al encadenar varios variation selectors, se puede representar una secuencia arbitraria de bytes detrás de un solo carácter
Codificar bytes como variation selectors
- Los variation selectors se dividen en dos rangos de codepoints
U+FE00 .. U+FE0F: los primeros 16
U+E0100 .. U+E01EF: los 240 restantes
- La regla para convertir un byte en un variation selector es simple
- Si el byte es menor que 16,
0xFE00 + byte
- En caso contrario,
0xE0100 + (byte - 16)
- La codificación primero inserta un carácter base (base character) y luego agrega cada byte convertido en variation selector
fn byte_to_variation_selector(byte: u8) -> char {
if byte < 16 {
char::from_u32(0xFE00 + byte as u32).unwrap()
} else {
char::from_u32(0xE0100 + (byte - 16) as u32).unwrap()
}
}
fn encode(base: char, bytes: &[u8]) -> String {
let mut result = String::new();
result.push(base);
for byte in bytes {
result.push(byte_to_variation_selector(*byte));
}
result
}
- Si se agregan los bytes que representan
hello, [0x68, 0x65, 0x6c, 0x6c, 0x6f], después de 😊, se obtiene una cadena que por fuera parece un emoji común
- En la salida normal, los caracteres ocultos no se ven, pero si se imprime con el formato de depuración de Rust aparecen codepoints ocultos como
\u{e0158}
"😊\u{e0158}\u{e0155}\u{e015c}\u{e015c}\u{e015f}"
Cómo volver a leer los bytes ocultos
- La decodificación recorre los caracteres y convierte de nuevo a bytes los codepoints que estén en los rangos de variation selectors
- El rango
U+FE00..U+FE0F se restaura como variation_selector - 0xFE00
- El rango
U+E0100..U+E01EF se restaura como variation_selector - 0xE0100 + 16
- Los caracteres normales antes del primer variation selector se consideran el carácter base y se ignoran
- Si se encuentra un carácter que no es variation selector y ya hay resultados, se termina la decodificación
fn variation_selector_to_byte(variation_selector: char) -> Option<u8> {
let variation_selector = variation_selector as u32;
if (0xFE00..=0xFE0F).contains(&variation_selector) {
Some((variation_selector - 0xFE00) as u8)
} else if (0xE0100..=0xE01EF).contains(&variation_selector) {
Some((variation_selector - 0xE0100 + 16) as u8)
} else {
None
}
}
- Al decodificar el mismo resultado de codificación e interpretarlo como UTF-8, se obtiene
"hello"
- El carácter base no tiene por qué ser un emoji; el procesamiento de variation selectors es igual en caracteres comunes
- La razón para usar emojis es que es más divertido
Posibles abusos
- Este método es un abuso de Unicode y no debería usarse
- Como los datos no se ven en el resultado renderizado, a moderadores o revisores humanos les resulta difícil saber que existen datos ocultos
- Puede usarse indebidamente para ocultar datos y pasar filtros de contenido humanos
- También puede usarse para marcas de agua en texto
- Si se envía un mensaje a varias personas y luego se filtra, se puede rastrear al destinatario original
- Las secuencias de variation selectors sobreviven en la mayoría de operaciones de copiar y pegar
- Permite una densidad arbitraria de datos y, si se quiere, se podría poner una marca de agua en cada carácter
¿Pueden los LLM procesar datos ocultos?
- Después de que el tema apareció en Hacker News, surgió la pregunta de cómo tratan los LLM este tipo de datos ocultos
- En general, los tokenizers parecen preservar los variation selectors como tokens, por lo que en teoría el modelo puede acceder a ellos
- El tokenizer de OpenAI es una herramienta de comprobación que puede usarse para verificarlo
- En general, los modelos no parecen intentar decodificarlos directamente de forma interna
- Cuando se usan junto con un intérprete de código, algunos modelos pueden extraer los datos ocultos
1 comentarios
Opiniones en Hacker News
En cuanto al abuso de Unicode, esto es apenas la punta del iceberg. Con técnicas similares se pueden desbordar búferes en varios sistemas que aceptan cadenas Unicode y, aunque normalmente termina en errores o crashes, con algo de suerte también pueden aparecer comportamientos bastante interesantes.
En la época anterior a Python 3, haciendo pruebas de penetración, una vez hice que un solo carácter creciera a varios bytes usando únicamente signos diacríticos, hasta desbordar el búfer del servidor web backend. En ese momento solo provocó un crash y reinicio automático, pero si se investiga lo suficiente, parece que podría usarse para explotar sistemas o software específicos.
Incluso en formularios modernos se puede provocar algo similar con solo desactivar JavaScript y, en el mejor de los casos, si el debug está activado, se imprimen trazas de pila o consultas y se filtra un poco de información. Otro error común es contar mal la longitud de
\ny\r\nen cadenas de texto: JavaScript suele contar el retorno de carro como 1 byte, pero la especificación HTTP exige 2 bytes.unescape(encodeURIComponent("ç")).lengthes una forma aproximada y rápida de comprobar la longitud en bytes en JavaScript, y el problema de\r\nse soluciona normalizando la cadena antes de contar la longitud.Esto es tierno, pero no es realmente necesario. Unicode tiene un rango grande llamado PUA (private use area), cuyos códigos no están mapeados a ningún carácter y nunca lo estarán, por lo que se usan para fines internos o definidos por el usuario.
Por ejemplo, en fish-shell, al parsear tokens de forma segura como cadenas, los caracteres especiales sin escapar se reemplazan por otros puntos de código Unicode dentro de la cadena, pero se colocan en el área PUA, y luego se interceptan más adelante en el pipeline. No deberían exponerse fuera de los límites de la API, pero cuando aparecen se recomienda dejarlos pasar tal cual, y la mayoría de los sistemas y bibliotecas hacen eso. Puede ser un canal de fuga evidente, pero muchos desarrolladores normales no saben mucho sobre Unicode más allá de “usa siempre Unicode para evitar problemas de internacionalización”, así que muchas veces queda abierto.
). La clave aquí es codificarlo de modo que quede oculto al copiar y pegar, y que se trate como “parte” de otro carácter.https://news.ycombinator.com/item?id=42791378
Como la API tenía la restricción de aceptar solo emojis, enseguida se discutió su posible uso delictivo. Para ese caso no se puede usar PUA; hay que codificar dentro de los emojis.
Los noncharacters designados incluyen
0xFFFF,0xFFFEy los dos últimos puntos de código de cada plano, además de una zona en medio de Arabic Presentation Forms. Tengo entendido que se agregaron más tarde a la lista para que la gente tuviera más noncharacters disponibles para usarlos de esta forma.No se pueden usar caracteres PUA no reconocidos para poner marcas de agua invisibles en texto arbitrario, porque no se tratan como caracteres combinantes. En su lugar aparece una caja de marcador de posición renderizada por separado. Ejemplo:
— claro que, si estás usando el private use area de forma privada y directa, podría no verse como una caja.Hace unos 10 años asusté a algunos colegas poniendo U+202D LEFT-TO-RIGHT OVERRIDE en medio de nombres de archivo de Windows.
funnypicturegnp.exese veía comofunnypictureexe.png.Si además le ponías un ícono personalizado que pareciera una vista previa de foto, quedaba bastante convincente.
.execasi siempre se bloquean automáticamente, pero hoy en día la extensión maliciosa suele ser .html, que abre una página falsa de inicio de sesión mediante una redirecciónwindow.locationofuscada.El abuso de RTL tipo
cute-cat-lmth.pngera relativamente común, pero también era muy fácil de detectar, y esos correos se marcaban de inmediato como phishing.https://trojansource.codes/
Básicamente permite ocultar código que parece un comentario, pero que al compilarse funciona como código. Aunque recuerdo que su estatus de CVE fue polémico porque muchos editores de texto ya hacían visibles ese tipo de comentarios sospechosos.
guitar_tab.txt.Como caso de uso real, Sanity usó este truco para codificar Content Source Maps dentro del texto real que se entrega en una página web en “modo de vista previa”0. Con solo hacer clic en ese texto o contenido, los editores pueden rastrear fácilmente hasta la ubicación original dentro de una estructura de contenido profunda.
También tiene desventajas y limitaciones. Por ejemplo, hay que evitar que se agregue a valores que deben analizarse o usarse tal cual, como fechas, timestamps, URL o ID. Aun así, es un truco bastante interesante.
0 https://www.sanity.io/docs/stega
[1] https://github.com/sanity-io/content-source-maps
Me gusta la idea de usar esto para marcar con watermark la salida de LLM. Es un punto justo. De todos modos, el 99% de los generadores de baja calidad que solo copian y pegan van a quedar atrapados sin poder hacer mucho, y casi no afecta otros casos de uso importantes.
También me da curiosidad cuánto se insertaría por cada carácter o token de salida. ¿Cosas como ID de usuario, referencia del prompt, fecha, número de token? También me intriga cómo se interpretaría en la terminal; realmente está genial.
La única defensa real contra la IA sería exigir firmas de clave verificadas con identidad real para toda interacción humana, pero eso A: nunca va a ocurrir, y B: podría ser abusado en países con gobiernos corruptos o en países con gobiernos corruptos fuertemente influenciados por la industria privada, como Estados Unidos.
Claro que si mandas la frase a
xxd, aparece. La propuesta de PUA del comentario principal actual es distinta porque se ve de inmediato.Haciendo pruebas adicionales, después de pegarlo en la terminal, en
xxdel mensaje pasa completamente sin cambios, pero si lo seleccionas en la terminal y lo vuelves a pegar, en la selección X de mate terminal y konsole se cortó y solo quedaron unas pocas palabras. No sé si el corte se debe a la terminal o a X. En xterm, la últimaese transformó y la selección quedó todavía más recortada.En un archivo, la frase se guarda sin cambios. Por lo tanto, parece más bien que algunos datos se pierden al copiar hacia fuera de la terminal. Lo comprobé haciendo
echode la frase a un archivo de prueba, abriéndolo en el navegador y copiando el texto.Si durante la generación se manipula la forma de muestreo, más tarde se puede volver a ejecutar un LLM y observar el patrón de la salida para detectar una huella. Por ejemplo, elegir alternadamente tokens de alta y baja probabilidad. La implementación real, por supuesto, sería mucho más sofisticada, pero la idea va por ahí.
Algo interesante: los lectores de pantalla pueden detectar estos variation selectors al desplazarse carácter por carácter. Si te mueves con las flechas sobre el ejemplo, lee cosas como “Smiling face with smiling eyes”, “Symbol e zero one five five”, “Symbol e zero one five c”.
Sin embargo, depende del sintetizador de voz que se use, y si simplemente estás leyendo el documento no puedes saber que esos caracteres están ahí, así que en general no es una gran ventaja.
StegCloak0 también es de una familia parecida, y lleva esta idea un paso más allá cifrando el payload oculto con AES-256-CTR. Es un truquito bastante bueno.
0 https://github.com/KuroLabs/stegcloak
Eso sí, para que la otra persona pueda decodificarlos hay que compartir un valor secreto de contraseña.
El título es un poco engañoso. Dice: “El carácter base no tiene por qué ser un emoji, y el manejo de los variation selectors es igual en caracteres normales. Solo que con emojis es más divertido”.
Si se usa este método en caracteres que no son emoji, se vuelve más sigiloso y más incómodo.
Más que un simple watermarking de salida de LLM, esto parece que podría ser una forma prolija de empaquetar también los datos de logprobs.
Básicamente, incluiría la información de probabilidad de todos los tokens generados para aportar algo de transparencia al proceso de generación. También está incluido en la especificación de la API de OpenAI, y varios motores como
llama.cppproporcionan esta información. Normalmente se adjunta como un campo separado, pero también hay métodos de visualización como mikupad0.Probablemente sea una mala idea, pero igual es una idea que me da vueltas en la cabeza.
Es una técnica genial. Refleja ASCII y también hay caracteres Unicode Tag, que no suelen verse con frecuencia en elementos de UI, especialmente en apps web.
Lo particular de los caracteres Tag es que algunos LLM interpretan el texto oculto como ASCII y siguen las instrucciones, e incluso pueden escribirlos directamente.
https://embracethered.com/blog/posts/2024/hiding-and-finding...
También hay una prueba de concepto de un exploit real que Microsoft corrigió en Copilot.
https://embracethered.com/blog/posts/2024/m365-copilot-promp...