- Ao concatenar variation selectors do Unicode, é possível esconder uma sequência de bytes atrás de um único caractere; ela não aparece na tela, mas acompanha o texto ao copiar e colar
- Existem 256 variation selectors, do VS-1 ao VS-256, o que permite criar um mapeamento exato para o intervalo de 1 byte
- Mesmo anexando os bytes de
hello — [0x68, 0x65, 0x6c, 0x6c, 0x6f] — após 😊, visualmente ainda parece apenas um emoji comum
- A decodificação procura os intervalos
U+FE00..U+FE0F e U+E0100..U+E01EF e os converte de volta em bytes; o caractere base nem precisa ser um emoji
- Esse método é um abuso do Unicode e pode ser usado indevidamente para contornar filtros de conteúdo humanos ou embutir marca d’água em texto
Como dados invisíveis ficam anexados a um caractere
- Texto Unicode é representado como uma sequência de codepoints e normalmente é escrito no formato
U+XXXX
- Em caracteres latinos simples, costuma haver uma correspondência 1:1 entre codepoint e o caractere visível na tela
- Ex.:
U+0067 representa o caractere g
- Em outros sistemas de escrita, um único caractere visual pode ser composto por vários codepoints
- Ex.: em devanágari, um caractere lido como
키 é representado pelo par sequencial U+0915 e U+0940
Usando variation selectors como depósito de dados
- O Unicode define 256 codepoints de variation selector, chamados de VS-1 até VS-256
- O variation selector em si não é exibido na tela; ele serve para alterar a forma de exibição do caractere anterior
- A maioria dos caracteres Unicode não possui variações associadas, mas o Unicode busca compatibilidade futura, então até código que não entende o significado deve preservar os variation selectors
- Mesmo colocando
U+FE01 (VS-2) após U+0067 (g), a tela continua mostrando apenas a letra minúscula g
- Ao copiar e colar, o variation selector também vai junto
- Como 256 variation selectors correspondem exatamente à quantidade necessária para representar 1 byte, é possível esconder 1 byte de dados após qualquer codepoint Unicode arbitrário
- A especificação do Unicode não trata de forma específica sequências com vários variation selectors em série e dá a entender que elas devem ser ignoradas durante a renderização
- Ao concatenar vários variation selectors, é possível representar uma sequência arbitrária de bytes atrás de um único caractere
Codificando bytes em variation selectors
- Os variation selectors são divididos em dois intervalos de codepoints
U+FE00 .. U+FE0F: os primeiros 16
U+E0100 .. U+E01EF: os 240 restantes
- A regra para converter bytes em variation selectors é simples
- Se o byte for menor que 16, use
0xFE00 + byte
- Caso contrário, use
0xE0100 + (byte - 16)
- A codificação primeiro insere um caractere base e, em seguida, converte cada byte em variation selector e o concatena
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
}
- Ao anexar os bytes de
hello — [0x68, 0x65, 0x6c, 0x6c, 0x6f] — após 😊, o resultado visual é uma string que parece apenas um emoji comum
- Em saída normal, os caracteres ocultos não aparecem, mas no formato de depuração do Rust surgem codepoints escondidos como
\u{e0158}
"😊\u{e0158}\u{e0155}\u{e015c}\u{e015c}\u{e015f}"
Como ler os bytes ocultos novamente
- A decodificação percorre os caracteres e converte de volta em bytes os codepoints que estiverem nos intervalos de variation selector
- O intervalo
U+FE00..U+FE0F é restaurado com variation_selector - 0xFE00
- O intervalo
U+E0100..U+E01EF é restaurado com variation_selector - 0xE0100 + 16
- Caracteres comuns antes do primeiro variation selector são tratados como o caractere base e ignorados
- Se aparecer um caractere que não seja variation selector depois que já houver resultado, a decodificação termina
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
}
}
- Ao decodificar o mesmo resultado da codificação e interpretá-lo como UTF-8, obtém-se
"hello"
- O caractere base não precisa ser um emoji; o tratamento de variation selector é o mesmo em caracteres comuns
- O motivo para usar emoji é apenas porque fica mais divertido
Possibilidades de uso indevido
- Esse método é um abuso do Unicode e não deve ser usado
- Como os dados não aparecem no resultado renderizado, moderadores ou revisores humanos dificilmente percebem que existe informação oculta
- Isso pode ser explorado para esconder dados e passar por filtros de conteúdo humanos
- Também pode ser usado para marca d’água em texto
- Após enviar a mesma mensagem para várias pessoas, um vazamento permitiria rastrear o destinatário original
- A sequência de variation selectors sobrevive à maioria das operações de copiar e colar
- A técnica permite densidade arbitrária de dados e, se quiser, dá para colocar marca d’água em todos os caracteres
LLMs conseguem processar os dados ocultos?
- Depois que o tema apareceu no Hacker News, surgiram perguntas sobre como LLMs lidam com esse tipo de dado escondido
- Em geral, tokenizers parecem preservar os variation selectors como tokens, então, em teoria, o modelo pode acessá-los
- O tokenizer da OpenAI pode ser usado como ferramenta de verificação disso
- No geral, os modelos não parecem tentar decodificar esses dados diretamente por conta própria
- Com um interpretador de código, alguns modelos conseguem extrair os dados ocultos
1 comentários
Comentários do Hacker News
Em termos de abuso de Unicode, isso é só a ponta do iceberg. Com técnicas parecidas, dá para estourar buffers em vários sistemas que recebem strings Unicode e, normalmente, isso termina em erro ou crash, mas com sorte pode gerar uns comportamentos bem interessantes
Fazendo testes de intrusão na época anterior ao Python 3, já consegui estourar o buffer de um servidor web backend expandindo um único caractere em vários bytes só com diacríticos. Naquele caso, o resultado foi só um crash com reinicialização automática, mas, se cavar o suficiente, parece algo que poderia ser usado em exploits de sistemas ou softwares específicos
Mesmo em formulários modernos, dá para provocar algo parecido só desativando o JavaScript, e, no melhor caso, o modo de depuração está ligado e exibe stack trace ou queries, vazando um pouco de informação. Outro erro comum é contar errado o comprimento de
\ne\r\nem strings de texto: o JavaScript normalmente conta o carriage return como 1 byte, mas a especificação HTTP exige 2 bytesunescape(encodeURIComponent("ç")).lengthé uma forma rápida e aproximada de verificar o comprimento em bytes no JavaScript, e o problema de\r\npode ser resolvido normalizando a string antes de contar o tamanhoIsso é bonitinho, mas não é realmente necessário. O Unicode tem uma grande faixa chamada PUA(private use area). Os códigos dessa faixa não são mapeados para caractere nenhum, nem serão no futuro, então são usados para fins internos ou definidos pelo usuário
Por exemplo, no fish-shell, ao fazer o parsing seguro de tokens como string, caracteres especiais não escapados são trocados por outros code points Unicode dentro da string, mas colocados na área PUA, e depois interceptados adiante no pipeline. Eles não deveriam vazar para fora do limite da API, mas, quando aparecem, a recomendação é apenas deixá-los passar, e a maioria dos sistemas e bibliotecas faz isso. Isso pode virar um caminho óbvio de vazamento, mas muitos desenvolvedores perfeitamente normais não sabem sobre Unicode muito além de “use sempre Unicode para evitar problemas de internacionalização”, então muitas vezes isso fica totalmente exposto
). O ponto aqui é codificar de forma que, ao copiar e colar, isso fique escondido e seja tratado como “parte” de outro caracterehttps://news.ycombinator.com/item?id=42791378
Como havia a restrição de que a API só aceitava emoji, logo se discutiu o potencial de uso criminoso. Para esse caso, PUA não serve; tem que ser codificado dentro do emoji
Os noncharacters designados incluem
0xFFFF,0xFFFEe os dois últimos code points de cada plano, além de uma área no meio de Arabic Presentation Forms. Pelo que sei, eles foram adicionados depois à lista justamente para garantir mais noncharacters que as pessoas pudessem usar desse jeitoNão dá para fazer watermark invisível de caracteres arbitrários com caracteres PUA não reconhecidos. Eles não são tratados como caracteres combinantes. Em vez disso, aparece uma caixa de placeholder renderizada separadamente. Ex.:
— claro, se você estiver usando private use area de forma realmente privada, pode ser que não apareça como caixaUns 10 anos atrás, eu costumava assustar colegas colocando U+202D LEFT-TO-RIGHT OVERRIDE no meio de nomes de arquivos no Windows.
funnypicturegnp.exepassava a parecerfunnypictureexe.pngCom um ícone personalizado que parecesse pré-visualização de foto, ficava bem convincente
.execostuma ser bloqueado automaticamente, mas hoje em dia extensões maliciosas muitas vezes são .html, que acabam abrindo páginas falsas de login com redirecionamentoswindow.locationofuscadosAbusos de RTL como
cute-cat-lmth.pngeram relativamente comuns, mas também muito fáceis de detectar, então esses e-mails eram marcados imediatamente como phishinghttps://trojansource.codes/
Basicamente, dá para esconder código que parece comentário, mas que ao compilar é tratado como código executável. Ainda assim, lembro que o status de CVE foi controverso, porque muitos editores de texto já passaram a destacar esse tipo de comentário suspeito
guitar_tab.txtComo caso de uso real, a Sanity usou esse truque para codificar Content Source Maps dentro do próprio texto real entregue à página web no “modo de pré-visualização”0. Assim, os editores podem rastrear facilmente a localização original dentro de uma estrutura profunda de conteúdo apenas clicando naquele texto ou conteúdo
Também há desvantagens e limitações. Por exemplo, é preciso impedir que isso seja adicionado a valores que precisam ser analisados ou usados literalmente, como datas e timestamps, URLs e IDs. Ainda assim, é um truque bem divertido
0 https://www.sanity.io/docs/stega
[1] https://github.com/sanity-io/content-source-maps
Gosto da ideia de usar isso para watermarking de saída de LLM. É um ponto excelente. Afinal, 99% dos geradores de baixa qualidade que só vivem de copiar e colar inevitavelmente seriam pegos, e quase não haveria impacto em outros casos de uso importantes
Também fico curioso sobre quanto seria inserido em cada caractere ou token de saída. Seriam coisas como ID do usuário, referência ao prompt, data e número do token? Também queria saber como isso seria interpretado no terminal; é muito legal
A única defesa real contra IA seria exigir assinaturas de chave verificadas por identidade real em toda interação humana, e mesmo isso A: nunca vai acontecer, e B: pode ser abusado em países com governos corruptos, ou governos corruptos fortemente influenciados pela iniciativa privada, como os EUA
Claro, se você passar a frase para
xxd, ela aparece. A sugestão de PUA no comentário principal atual é diferente, porque fica visível imediatamenteEm testes adicionais, depois de colar no terminal, no
xxda mensagem passa completamente sem modificação, mas se eu seleciono no terminal e colo de novo, na seleção X do mate terminal e do konsole só algumas palavras permanecem e o resto é cortado. Não sei se isso é culpa do terminal ou do X. No xterm, o últimoefoi alterado e o conteúdo selecionado foi truncado ainda maisEm arquivos, a frase é gravada sem alteração. Então parece mais que alguns dados se perdem ao copiar para fora do terminal. Testei dando
echoda frase para um arquivo de teste e abrindo no navegador para copiar o textoSe você mexer na forma de amostragem durante a geração, depois pode rodar o LLM novamente e observar o padrão da saída para detectar a impressão digital. Por exemplo, alternando entre tokens de alta probabilidade e de baixa probabilidade. A implementação real obviamente seria muito mais sofisticada, mas a ideia vai nessa direção
Como observação interessante, leitores de tela conseguem detectar esses variation selectors quando se navega caractere por caractere. Se você usar as setas no exemplo acima, ele lê algo como “Smiling face with smiling eyes”, “Symbol e zero one five five”, “Symbol e zero one five c”
No entanto, isso varia conforme o sintetizador de voz usado e, se o documento estiver apenas sendo lido normalmente, não dá para saber que esses caracteres estão ali, então no geral não é uma grande vantagem
O StegCloak0 é da mesma família e leva essa ideia um passo adiante ao criptografar a carga oculta com AES-256-CTR. É um truque pequeno, mas bem interessante
0 https://github.com/KuroLabs/stegcloak
Só que, para a outra pessoa decodificar, vocês precisam compartilhar um segredo de senha
O título é um pouco enganoso. A ideia é que “o caractere base não precisa ser emoji, e o tratamento de variation selectors é o mesmo em caracteres comuns. Com emoji só fica mais divertido”
Se você usar esse método com caracteres não emoji, ele fica mais discreto e mais inconveniente
Mais do que um simples watermarking de saída de LLM, isso parece poder ser uma forma elegante de empacotar também os dados de logprobs
Basicamente, incluiria as informações de probabilidade de todos os tokens gerados, dando um pouco de transparência ao processo de geração. Isso também está incluído na especificação da API da OpenAI, e vários motores como
llama.cpptambém fornecem essa informação. Normalmente ela vem em um campo separado, mas também existem formas de visualização como o mikupad0Provavelmente é uma má ideia, mas ainda assim é um conceito que instiga
Técnica muito interessante. Ela reflete ASCII e também existem caracteres Unicode Tag em elementos de UI, especialmente em apps web, que muitas vezes não ficam visíveis
O ponto peculiar dos caracteres Tag é que alguns LLMs interpretam texto oculto como ASCII e seguem as instruções, e até conseguem escrevê-lo diretamente
https://embracethered.com/blog/posts/2024/hiding-and-finding...
Também há uma prova de conceito de exploit real que a Microsoft corrigiu no Copilot
https://embracethered.com/blog/posts/2024/m365-copilot-promp...