- Wenn man Unicode-Variation Selectors aneinanderhängt, lässt sich hinter einem Zeichen eine Bytefolge verstecken, die auf dem Bildschirm nicht sichtbar ist, aber beim Kopieren und Einfügen mitwandert
- Es gibt VS-1 bis VS-256, also 256 Variation Selectors, sodass sich ein Mapping erstellen lässt, das exakt dem Bereich von 1 Byte entspricht
- Hängt man hinter
😊 die Bytes von hello ([0x68, 0x65, 0x6c, 0x6c, 0x6f]) an, sieht es nach außen wie ein ganz normales einzelnes Emoji aus
- Beim Decoding werden die Bereiche
U+FE00..U+FE0F und U+E0100..U+E01EF gesucht und wieder in Bytes umgewandelt; das Basiszeichen muss kein Emoji sein
- Dieses Verfahren ist ein Missbrauch von Unicode und kann missbraucht werden, um menschliche Content-Filter zu umgehen oder Wasserzeichen in Text einzubetten
Wie unsichtbare Daten an einem Zeichen hängen
- Unicode-Text wird als Sequenz von Codepoints dargestellt, üblicherweise in der Form
U+XXXX
- Bei einfachen lateinischen Zeichen entsprechen Codepoint und das auf dem Bildschirm sichtbare Zeichen 1:1 einander
- Beispiel:
U+0067 steht für den Buchstaben g
- In anderen Schriftsystemen kann ein sichtbares Zeichen aus mehreren Codepoints bestehen
- Beispiel: Ein Devanagari-Zeichen, das als
ki gelesen wird, wird als aufeinanderfolgendes Paar aus U+0915 und U+0940 dargestellt
Variation Selectors als Datenspeicher verwenden
- Unicode definiert 256 Variation-Selector-Codepoints, benannt von VS-1 bis VS-256
- Ein Variation Selector selbst wird nicht angezeigt; er dient dazu, die Darstellungsweise des vorangehenden Zeichens zu verändern
- Die meisten Unicode-Zeichen haben keine zugehörigen Varianten, doch da Unicode auf Zukunftskompatibilität ausgelegt ist, muss auch Verarbeitungscode, der ihre Bedeutung nicht kennt, Variation Selectors erhalten
- Hängt man
U+FE01 (VS-2) hinter U+0067 (g), sieht es auf dem Bildschirm weiterhin wie ein kleines g aus
- Beim Kopieren und Einfügen wandert der Variation Selector mit
- 256 Variation Selectors sind genau die Anzahl, mit der sich 1 Byte darstellen lässt; damit kann man hinter einem beliebigen Unicode-Codepoint 1 Byte an Daten verstecken
- Die Unicode-Spezifikation behandelt Sequenzen mehrerer aufeinanderfolgender Variation Selectors nicht konkret und impliziert, dass sie beim Rendering ignoriert werden sollten
- Wenn man mehrere Variation Selectors aneinanderhängt, lässt sich hinter einem Zeichen eine beliebige Bytefolge darstellen
Bytes als Variation Selectors codieren
- Variation Selectors sind in zwei Codepoint-Bereiche unterteilt
U+FE00 .. U+FE0F: die ersten 16
U+E0100 .. U+E01EF: die übrigen 240
- Die Regel, um ein Byte in einen Variation Selector umzuwandeln, ist einfach
- Ist das Byte kleiner als 16, dann
0xFE00 + byte
- Andernfalls
0xE0100 + (byte - 16)
- Beim Encoding wird zuerst ein Basiszeichen (base character) eingefügt; danach wird jedes Byte in einen Variation Selector umgewandelt und angehängt
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
}
- Hängt man die Bytes für
hello ([0x68, 0x65, 0x6c, 0x6c, 0x6f]) hinter 😊, entsteht eine Zeichenkette, die äußerlich wie ein normales Emoji aussieht
- In normaler Ausgabe sind die versteckten Zeichen nicht sichtbar; gibt man sie jedoch mit Rusts Debug-Format aus, erscheinen versteckte Codepoints wie
\u{e0158}
"😊\u{e0158}\u{e0155}\u{e015c}\u{e015c}\u{e015f}"
Versteckte Bytes wieder auslesen
- Beim Decoding iteriert man über die Zeichen und wandelt Codepoints im Bereich der Variation Selectors wieder in Bytes um
- Der Bereich
U+FE00..U+FE0F wird mit variation_selector - 0xFE00 rekonstruiert
- Der Bereich
U+E0100..U+E01EF wird mit variation_selector - 0xE0100 + 16 rekonstruiert
- Normale Zeichen vor dem ersten Variation Selector werden als Basiszeichen betrachtet und ignoriert
- Trifft man auf ein Zeichen, das kein Variation Selector ist, und es gibt bereits ein Ergebnis, wird das Decoding beendet
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
}
}
- Decodiert man dasselbe Encoding-Ergebnis und interpretiert es als UTF-8, erhält man
"hello"
- Das Basiszeichen muss kein Emoji sein; die Verarbeitung von Variation Selectors funktioniert bei normalen Zeichen genauso
- Der Grund, Emojis zu verwenden, ist, dass es mehr Spaß macht
Missbrauchspotenzial
- Dieses Verfahren ist Unicode-Missbrauch und sollte nicht verwendet werden
- Im gerenderten Ergebnis sind die Daten nicht sichtbar, sodass menschliche Moderatoren oder Reviewer schwer erkennen können, dass versteckte Daten vorhanden sind
- Es kann missbraucht werden, um Daten so zu verstecken, dass sie menschliche Content-Filter passieren
- Es kann auch für Text-Wasserzeichen verwendet werden
- Sendet man eine Nachricht an mehrere Personen und sie wird später geleakt, lässt sich der ursprüngliche Empfänger zurückverfolgen
- Variation-Selector-Sequenzen überleben die meisten Kopier-und-Einfüge-Vorgänge
- Sie erlauben eine beliebige Datendichte; wenn man will, kann man jedes Zeichen mit einem Wasserzeichen versehen
Können LLMs versteckte Daten verarbeiten?
- Nachdem der Beitrag auf Hacker News erschienen war, kam die Frage auf, wie LLMs mit solchen versteckten Daten umgehen
- Im Allgemeinen scheinen Tokenizer Variation Selectors als Tokens zu erhalten, sodass Modelle theoretisch darauf zugreifen können
- Der OpenAI tokenizer ist ein Prüftool, mit dem sich das verifizieren lässt
- Insgesamt scheinen Modelle intern nicht von sich aus zu versuchen, sie direkt zu decodieren
- Zusammen mit einem Code Interpreter können einige Modelle versteckte Daten entschlüsseln
1 Kommentare
Hacker-News-Kommentare
Was Unicode-Missbrauch angeht, ist das nur die Spitze des Eisbergs. Mit ähnlichen Techniken kann man in vielen Systemen, die Unicode-Strings annehmen, Puffer zum Überlaufen bringen; meistens endet das in Fehlern oder Abstürzen, aber mit etwas Glück kommt dabei auch ziemlich interessantes Verhalten heraus.
Bei Penetrationstests in der Zeit vor Python 3 habe ich einmal allein mit diakritischen Zeichen ein einzelnes Zeichen auf mehrere Bytes aufgebläht und so den Puffer eines Backend-Webservers überlaufen lassen. Damals führte das nur zu einem Crash und automatischen Neustart, aber wenn man tief genug gräbt, könnte es offenbar für Exploits bestimmter Systeme oder Software nutzbar sein.
Auch in modernen Formularen lässt sich Ähnliches oft schon auslösen, indem man JavaScript deaktiviert. Im besten Fall ist Debugging aktiviert, sodass ein Stacktrace oder Queries ausgegeben werden und ein wenig Information durchsickert. Ein weiterer häufiger Fehler ist, die Länge von
\nund\r\nin Textstrings falsch zu zählen: JavaScript zählt einen Carriage Return normalerweise als 1 Byte, die HTTP-Spezifikation verlangt aber 2 Bytes.unescape(encodeURIComponent("ç")).lengthist in JavaScript eine grobe, schnelle Methode, die Byte-Länge zu prüfen; das\r\n-Problem lässt sich lösen, indem man den String vor dem Längenzählen bereinigt.Das ist niedlich, aber eigentlich nicht nötig. Unicode hat einen großen Bereich namens PUA (private use area); die Codes in diesem Bereich sind keinem Zeichen zugeordnet und werden auch künftig keinem zugeordnet, damit sie für interne bzw. benutzerdefinierte Zwecke verwendet werden können.
In fish-shell zum Beispiel werden beim sicheren Parsen von Tokens als String nicht-escapte Sonderzeichen durch andere Unicode-Codepoints im String ersetzt, aber in den PUA-Bereich gelegt und später in der Pipeline abgefangen. Man sollte sie nicht über API-Grenzen hinaus exponieren, aber wenn man ihnen begegnet, wird empfohlen, sie unverändert durchzureichen, und die meisten Systeme und Bibliotheken tun das auch. Das kann ein offensichtlicher Exfiltrationskanal sein, der oft offen bleibt, weil viele ansonsten vernünftige Entwickler über Unicode nicht viel mehr wissen als „immer Unicode verwenden, um Internationalisierungsprobleme zu vermeiden“.
). Der Punkt hier ist, so zu encodieren, dass es beim Kopieren und Einfügen verborgen bleibt und wie ein „Teil“ eines anderen Zeichens behandelt wird.https://news.ycombinator.com/item?id=42791378
Wegen der Einschränkung, dass die API nur Emojis akzeptiert, wurde sofort über mögliche missbräuchliche bzw. kriminelle Nutzung diskutiert. Für diesen Zweck kann man PUA nicht verwenden; man muss innerhalb des Emojis encodieren.
Zu den designierten Nichtzeichen gehören nicht nur
0xFFFF,0xFFFEund die letzten beiden Codepoints jeder Ebene, sondern auch ein Bereich mitten in den Arabic Presentation Forms. Soweit ich weiß, wurden sie später zur Liste hinzugefügt, damit Menschen mehr Nichtzeichen für solche Zwecke zur Verfügung haben.Mit nicht erkannten PUA-Zeichen kann man beliebige Zeichen nicht unsichtbar wasserzeichnen. Sie werden nämlich nicht als kombinierende Zeichen behandelt. Stattdessen erscheint ein separat gerendertes Platzhalter-Kästchen. Beispiel:
— natürlich kann es auch kein Kästchen sein, wenn man die private use area tatsächlich privat nutzt.Vor etwa 10 Jahren habe ich Kolleginnen und Kollegen erschreckt, indem ich mitten in Windows-Dateinamen U+202D LEFT-TO-RIGHT OVERRIDE eingefügt habe.
funnypicturegnp.exesah dann aus wiefunnypictureexe.png.Mit einem benutzerdefinierten Icon, das wie eine Fotovorschau aussah, war das ziemlich überzeugend.
.exewird meist automatisch blockiert, aber heutzutage ist die bösartige Erweiterung oft .html, mit einem verschleiertenwindow.location-Redirect auf eine gefälschte Login-Seite.RTL-Missbrauch wie
cute-cat-lmth.pngwar relativ häufig, aber auch sehr leicht zu erkennen; solche E-Mails habe ich sofort als Phishing markiert.https://trojansource.codes/
Im Grunde kann man Code verstecken, der wie ein Kommentar aussieht, beim Kompilieren aber als Code wirkt. Ich erinnere mich allerdings, dass der CVE-Status umstritten war, weil viele Texteditoren solche verdächtigen Kommentare inzwischen sichtbar machen.
guitar_tab.txterstellt.Als reales Einsatzbeispiel hat Sanity diesen Trick genutzt, um Content Source Maps im „Preview-Modus“ direkt in den tatsächlichen Text zu kodieren, der auf der Webseite ausgeliefert wird0. Redakteure können allein durch Anklicken dieses Textes oder Inhalts die ursprüngliche Stelle tief in der Content-Struktur leicht zurückverfolgen.
Es gibt auch Nachteile und Einschränkungen. Man muss beispielsweise verhindern, dass so etwas zu Werten hinzugefügt wird, die unverändert geparst oder verwendet werden müssen, etwa Datumsangaben, Timestamps, URLs oder IDs. Trotzdem ist es ein ziemlich interessanter Trick.
0 https://www.sanity.io/docs/stega
[1] https://github.com/sanity-io/content-source-maps
Die Idee, das für Watermarking von LLM-Ausgaben zu verwenden, gefällt mir. Das trifft genau den richtigen Punkt. 99 % der minderwertigen Generatoren, die ohnehin nur Copy & Paste machen, werden zwangsläufig erwischt, und andere zentrale Use Cases werden kaum beeinflusst.
Ich frage mich auch, wie viel man pro Zeichen oder Ausgabe-Token hineinpacken würde. Dinge wie Nutzer-ID, Prompt-Referenz, Datum, Token-Nummer? Und ich frage mich, wie das im Terminal interpretiert wird. Wirklich cool.
Die einzige echte AI-Abwehrmaßnahme wäre, für jede menschliche Interaktion eine mit echter Identität verifizierte Key-Signatur zu verlangen. Aber das wird A: nie passieren, und B: könnte es in Ländern mit korrupten Regierungen oder in Ländern mit korrupten Regierungen, die stark von der Privatwirtschaft beeinflusst werden, etwa den USA, missbraucht werden.
Natürlich erscheint es, wenn man den Satz durch
xxdschickt. Der PUA-Vorschlag im derzeit obersten Kommentar ist etwas anderes, das sofort sichtbar ist.Nach weiteren Tests: Wenn ich es ins Terminal einfüge, läuft die Nachricht in
xxdvöllig unverändert durch. Wenn ich sie aber im Terminal auswähle und wieder einfüge, bleiben in der X selection von mate terminal und konsole nur ein paar Wörter übrig, der Rest wird abgeschnitten. Ob das Abschneiden am Terminal oder an X liegt, weiß ich nicht. In xterm wurde das letzteeverändert, und der ausgewählte Inhalt wurde noch stärker abgeschnitten.In Dateien wird der Satz unverändert aufgezeichnet. Es sieht also eher so aus, als gingen beim Kopieren aus dem Terminal heraus Teile der Daten verloren. Ich habe den Satz in eine Testdatei per
echogeschrieben, sie im Browser geöffnet und den Text zum Prüfen kopiert.Wenn man bei der Erzeugung die Sampling-Methode manipuliert, kann man später das LLM erneut laufen lassen, das Ausgabemuster beobachten und einen Fingerprint erkennen. Zum Beispiel, indem abwechselnd Tokens mit hoher und niedriger Wahrscheinlichkeit gewählt werden. Die tatsächliche Implementierung wäre natürlich viel ausgefeilter, aber die Idee geht in diese Richtung.
Interessanterweise können Screenreader solche variation selectors erkennen, wenn man zeichenweise navigiert. Wenn man im Beispiel mit den Pfeiltasten darübergeht, wird etwa „Smiling face with smiling eyes“, „Symbol e zero one five five“, „Symbol e zero one five c“ vorgelesen.
Das hängt allerdings vom verwendeten Sprachsynthesizer ab, und wenn man das Dokument einfach nur liest, merkt man nicht, dass solche Zeichen vorhanden sind. Insgesamt ist das also kein großer Vorteil.
StegCloak0 gehört ebenfalls in eine ähnliche Kategorie und treibt die Idee noch einen Schritt weiter, indem es die versteckte Payload mit AES-256-CTR verschlüsselt. Ein ziemlich netter kleiner Trick.
0 https://github.com/KuroLabs/stegcloak
Allerdings muss man einen Passwort-Secret-Wert teilen, damit die Gegenseite sie dekodieren kann.
Der Titel ist etwas irreführend. Dort steht: „Das Basiszeichen muss kein Emoji sein, und die Verarbeitung von variation selectors ist bei normalen Zeichen identisch. Mit Emojis macht es nur mehr Spaß.“
Wenn man diese Methode mit Nicht-Emoji-Zeichen verwendet, wird sie unauffälliger und zugleich unangenehmer.
Mehr noch als schlichtes Watermarking von LLM-Ausgaben könnte das eine saubere Methode sein, logprobs-Daten mitzuverpacken.
Im Grunde würde man Wahrscheinlichkeitsinformationen für alle erzeugten Tokens einschließen und dem Erzeugungsprozess etwas Transparenz geben. Das ist auch in der OpenAI-API-Spezifikation enthalten, und mehrere Engines wie
llama.cppliefern diese Informationen ebenfalls. Normalerweise hängen sie in einem separaten Feld, es gibt aber auch Visualisierungsmethoden wie mikupad0.Vermutlich ist es eine schlechte Idee, aber trotzdem ein Gedanke, der einem im Kopf herumspukt.
Coole Technik. Sie greift ASCII auf, und es gibt auch Unicode-Tag-Zeichen, die in UI-Elementen, insbesondere in Web-Apps, oft nicht sichtbar sind.
Das Besondere an Tag-Zeichen ist, dass einige LLMs den versteckten Text als ASCII interpretieren und den Anweisungen folgen – und dass sie ihn sogar selbst schreiben können.
https://embracethered.com/blog/posts/2024/hiding-and-finding...
Es gibt auch einen echten Exploit-Proof-of-Concept, den Microsoft in Copilot behoben hat.
https://embracethered.com/blog/posts/2024/m365-copilot-promp...