2 puntos por GN⁺ 2025-02-13 | 1 comentarios | Compartir por WhatsApp
  • 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

 
GN⁺ 2025-02-13
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.

    • El desafío "encrypted runner" de las clasificatorias de Google CTF 2024 se basaba en esta idea.
    • Correcto. El texto Zalgo es un caso de prueba común para campos de entrada en sitios web, pero por lo general no pasa nada interesante. A veces apenas dispara una excepción por límites de longitud en la base de datos y, normalmente, ni siquiera mata el proceso; la excepción queda dentro del hilo actual.
      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 \n y \r\n en cadenas de texto: JavaScript suele contar el retorno de carro como 1 byte, pero la especificación HTTP exige 2 bytes.
      unescape(encodeURIComponent("ç")).length es una forma aproximada y rápida de comprobar la longitud en bytes en JavaScript, y el problema de \r\n se soluciona normalizando la cadena antes de contar la longitud.
    • Soy principiante, pero ¿podrías explicar un poco más cómo funciona esto o cómo lo hiciste? Suena como una vulnerabilidad que valdría la pena probar.
  • 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.

    • Lo probé directamente y los caracteres private use se renderizan como cajas en mi entorno (󰀀). La clave aquí es codificarlo de modo que quede oculto al copiar y pegar, y que se trate como “parte” de otro carácter.
    • La diferencia es que los caracteres PUA normalmente se renderizan de alguna forma bastante visible, mientras que los variation selectors no.
    • Falta un poco de contexto: esta idea surgió en la discusión alrededor de una propuesta de Open Heart Protocol.
      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.
    • ¿No se parece más al uso de noncharacters designados que al private-use area? El PUA también se usa para codificaciones informales de sistemas de escritura que aún no están en Unicode, o para cosas como el logo de Apple, así que usarlo de esta manera me preocuparía por posibles colisiones.
      Los noncharacters designados incluyen 0xFFFF, 0xFFFE y 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.
    • Honestamente, pegué este comentario en el decodificador proporcionado. Pensé que no podía estar perdiendo tanto el punto y que seguro había un mensaje oculto, pero parece que realmente lo perdió o que este sitio web los está eliminando.
      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.exe se veía como funnypictureexe.png.
    Si además le ponías un ícono personalizado que pareciera una vista previa de foto, quedaba bastante convincente.

    • Trabajé en detección de phishing y era un patrón que los atacantes usaban con frecuencia. Los .exe casi 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ón window.location ofuscada.
      El abuso de RTL tipo cute-cat-lmth.png era relativamente común, pero también era muy fácil de detectar, y esos correos se marcaban de inmediato como phishing.
    • La versión de ese truco para código fuente es CVE-2021-42574, y también tiene sitio web.
      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.
    • No conocía este truco específico, pero me alegra que quedara justificado mi hábito paranoico de abrir durante décadas los archivos multimedia potencialmente sospechosos siempre con “clic derecho → Abrir con”.
    • Una vez hice un archivo bat llamado 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.

    • No entiendo por qué todos creen que el watermarking de IA va a funcionar. Cualquier watermark puede eliminarse de inmediato y con facilidad, así que nunca funcionará de verdad.
      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.
    • Hay tanto preprocesamiento antes de meter datos en un dataset que me sorprendería que este tipo de travesura funcionara en la práctica.
    • En la mayoría de las terminales Linux, lo que pasas simplemente se transmite tal cual como una secuencia de bytes. Esta técnica es compatible con UTF-8 y no usa glifos adicionales, así que en una terminal que cumpla con Unicode no es visible para el ojo humano. Lo probé en algunas.
      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 xxd el 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 última e se 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 echo de la frase a un archivo de prueba, abriéndolo en el navegador y copiando el texto.
    • Para el watermarking de LLM hay otros enfoques mucho más robustos y difíciles de detectar. Aprovechan que el LLM crea una distribución de probabilidad que asigna probabilidades a cada posible siguiente token, y luego genera la salida tomando muestras aleatorias de esa distribución.
      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.

    • Como el texto en línea en general está contaminado con caracteres invisibles pero molestos de escuchar, uso un script en mi lector de pantalla que elimina todos los caracteres no ASCII.
  • 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

    • Parece que hay un plugin de Better Discord que usa esto o un método similar. Permite enviar mensajes completamente cifrados que para otras personas parecen no ser nada.
      Eso sí, para que la otra persona pueda decodificarlos hay que compartir un valor secreto de contraseña.
    • Intenté probarlo en registros TXT de Cloudflare DNS, pero Cloudflare, muy astutamente, los decodifica al pegarlos en el campo TXT.
  • 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.

    • No parece tan incómodo. Se podría hacer un detector sin demasiada dificultad. Bastaría con mostrarlo cuando a un carácter que no tiene una variante real se le adjunta un variant. De hecho, parece que también podría usarse como firma.
  • 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.cpp proporcionan 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...