2 poin oleh GN⁺ 2025-02-13 | 1 komentar | Bagikan ke WhatsApp
  • Dengan merangkai variation selector Unicode, dimungkinkan menyembunyikan deretan byte di belakang satu karakter yang tidak terlihat di layar tetapi tetap ikut saat disalin-tempel
  • Ada 256 variation selector, dari VS-1 hingga VS-256, sehingga bisa dibuat pemetaan yang pas dengan rentang 1 byte
  • Bahkan jika byte hello [0x68, 0x65, 0x6c, 0x6c, 0x6f] ditempelkan setelah 😊, tampilannya tetap seperti satu emoji biasa
  • Decoding dilakukan dengan mencari rentang U+FE00..U+FE0F dan U+E0100..U+E01EF, lalu mengubahnya kembali menjadi byte, dan karakter dasar tidak harus berupa emoji
  • Metode ini adalah penyalahgunaan Unicode, dan dapat disalahgunakan untuk melewati filter konten manusia atau menyisipkan watermark ke dalam teks

Cara data tak terlihat menempel pada satu karakter

  • Teks Unicode direpresentasikan sebagai urutan code point, biasanya ditulis dalam format U+XXXX
  • Pada huruf Latin sederhana, code point dan karakter yang terlihat di layar biasanya berkorespondensi 1:1
    • Contoh: U+0067 merepresentasikan karakter g
  • Pada sistem tulisan lain, satu karakter yang tampak di layar bisa tersusun dari beberapa code point
    • Contoh: dalam Devanagari, karakter yang dibaca sebagai direpresentasikan sebagai pasangan berurutan U+0915 dan U+0940

Menggunakan variation selector seperti media penyimpanan data

  • Unicode mendefinisikan 256 code point variation selector, dengan nama dari VS-1 sampai VS-256
  • Variation selector sendiri tidak ditampilkan di layar, melainkan digunakan untuk mengubah cara karakter sebelumnya dirender
  • Sebagian besar karakter Unicode tidak memiliki variasi terkait, tetapi karena Unicode menargetkan kompatibilitas masa depan, kode pemroses yang tidak memahami maknanya tetap harus mempertahankan variation selector
    • Menambahkan U+FE01 (VS-2) setelah U+0067 (g) tetap membuatnya terlihat seperti huruf kecil g di layar
    • Saat disalin-tempel, variation selector juga ikut terbawa
  • Karena 256 variation selector tepat berjumlah cukup untuk merepresentasikan 1 byte, data 1 byte dapat disembunyikan di belakang code point Unicode arbitrer
  • Spesifikasi Unicode tidak secara rinci membahas urutan beberapa variation selector yang beruntun, dan mengisyaratkan bahwa urutan semacam itu seharusnya diabaikan saat rendering
  • Dengan merangkai beberapa variation selector, deretan byte arbitrer bisa direpresentasikan di belakang satu karakter

Menyandikan byte menjadi variation selector

  • Variation selector terbagi ke dalam dua rentang code point
    • U+FE00 .. U+FE0F: 16 yang pertama
    • U+E0100 .. U+E01EF: 240 sisanya
  • Aturan untuk mengubah byte menjadi variation selector cukup sederhana
    • Jika byte lebih kecil dari 16, gunakan 0xFE00 + byte
    • Selain itu, gunakan 0xE0100 + (byte - 16)
  • Proses encoding dimulai dengan memasukkan satu karakter dasar (base character), lalu setiap byte setelah itu diubah menjadi variation selector dan dirangkai di belakangnya
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
}
  • Jika byte yang merepresentasikan hello, yaitu [0x68, 0x65, 0x6c, 0x6c, 0x6f], ditempelkan setelah 😊, hasilnya adalah string yang secara kasatmata tampak seperti emoji biasa
  • Pada output normal, karakter tersembunyi itu tidak terlihat, tetapi jika dicetak dengan format debug Rust, code point tersembunyi seperti \u{e0158} akan tampak
"😊\u{e0158}\u{e0155}\u{e015c}\u{e015c}\u{e015f}"

Cara membaca kembali byte yang tersembunyi

  • Decoding dilakukan dengan mengiterasi karakter dan mengubah kembali code point dalam rentang variation selector menjadi byte
  • Rentang U+FE00..U+FE0F dipulihkan dengan variation_selector - 0xFE00
  • Rentang U+E0100..U+E01EF dipulihkan dengan variation_selector - 0xE0100 + 16
  • Karakter biasa sebelum variation selector pertama dianggap sebagai karakter dasar dan diabaikan
  • Jika menemukan karakter yang bukan variation selector setelah hasil decoding sudah mulai terbentuk, proses decoding dihentikan
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
    }
}
  • Jika hasil encoding yang sama didekode lalu diinterpretasikan sebagai UTF-8, hasilnya adalah "hello"
  • Karakter dasar tidak harus emoji; pada karakter biasa pun variation selector diproses dengan cara yang sama
  • Emoji dipakai hanya karena terasa lebih menyenangkan

Potensi penyalahgunaan

  • Metode ini adalah penyalahgunaan Unicode dan sebaiknya tidak digunakan
  • Karena data tidak terlihat pada hasil render, moderator atau reviewer manusia akan sulit mengetahui keberadaan data tersembunyi
  • Ini bisa disalahgunakan untuk menyembunyikan data sambil lolos dari filter konten manusia
  • Teknik ini juga dapat dipakai untuk watermarking teks
    • Setelah pesan dikirim ke beberapa orang, jika bocor maka penerima aslinya bisa dilacak
    • Urutan variation selector cenderung tetap bertahan dalam sebagian besar proses salin-tempel
    • Kepadatan data arbitrer dimungkinkan, dan jika diinginkan watermark dapat ditanamkan pada setiap karakter

Bisakah LLM memproses data tersembunyi?

  • Setelah diposting ke Hacker News, muncul pertanyaan tentang bagaimana LLM menangani data tersembunyi seperti ini
  • Secara umum, tokenizer tampaknya mempertahankan variation selector sebagai token, sehingga secara teori model dapat mengaksesnya
  • OpenAI tokenizer adalah alat pemeriksaan yang bisa digunakan untuk memverifikasi hal ini
  • Secara keseluruhan, model tampaknya tidak mencoba melakukan decoding langsung secara internal
  • Jika digunakan bersama code interpreter, beberapa model bisa membuka data tersembunyi

1 komentar

 
GN⁺ 2025-02-13
Komentar Hacker News
  • Untuk penyalahgunaan Unicode, ini hanyalah puncak gunung es. Dengan teknik serupa, buffer bisa dibuat overflow di berbagai sistem yang menerima string Unicode; biasanya berakhir sebagai error atau crash, tetapi kalau beruntung bisa menghasilkan perilaku yang cukup menarik
    Saat melakukan penetration testing pada masa sebelum Python 3, saya pernah membuat buffer backend web server overflow dengan memperpanjang satu karakter menjadi beberapa byte hanya memakai tanda diakritik. Waktu itu hanya crash dan restart otomatis, tetapi jika digali cukup dalam, tampaknya bisa dipakai untuk exploit pada sistem atau software tertentu

    • Soal "encrypted runner" di Google CTF quals 2024 didasarkan pada ide ini
    • Benar. Teks Zalgo adalah test case umum untuk field input situs web, tetapi biasanya tidak menghasilkan hal menarik. Kadang hanya menyentuh exception batas panjang database, dan umumnya prosesnya bahkan tidak mati; exception selesai di dalam thread saat ini
      Di form modern pun hal serupa bisa dipicu hanya dengan mematikan JavaScript, dan skenario terbaiknya debug sedang aktif sehingga stack trace atau query tercetak dan sedikit informasi bocor. Kesalahan umum lain adalah salah menghitung panjang \n dan \r\n dalam string teks; JavaScript biasanya menghitung carriage return sebagai 1 byte, tetapi spesifikasi HTTP mensyaratkan 2 byte
      unescape(encodeURIComponent("ç")).length adalah cara kira-kira yang cepat untuk memeriksa panjang byte di JavaScript, dan masalah \r\n bisa diselesaikan dengan membersihkan string sebelum menghitung panjangnya
    • Saya masih pemula, jadi bisa jelaskan lebih lanjut bagaimana ini terjadi atau bagaimana melakukannya? Rasanya seperti celah yang bisa dicoba dites
  • Ini lucu, tetapi sebenarnya tidak perlu. Unicode punya rentang besar bernama PUA (private use area), dan kode di rentang ini tidak dipetakan ke karakter apa pun serta tidak akan dipetakan di masa depan, sehingga digunakan untuk keperluan internal/kustom
    Misalnya di fish-shell, saat mem-parsing token menjadi string dengan aman, karakter khusus yang tidak di-escape diubah seperti code point Unicode lain di dalam string tetapi ditempatkan di area PUA, lalu dicegat belakangan di pipeline. Ini tidak boleh terekspos keluar dari batas API, tetapi saat ditemui, rekomendasinya adalah membiarkannya lewat apa adanya, dan sebagian besar sistem serta library juga begitu. Ini bisa menjadi jalur kebocoran yang jelas, dan sering tetap terbuka karena banyak developer normal tidak tahu banyak tentang Unicode selain “selalu pakai Unicode agar terhindar dari masalah internasionalisasi”

    • Saya coba sendiri, dan karakter private use dirender sebagai kotak di lingkungan saya (󰀀). Poin utamanya di sini adalah mengenkode agar tersembunyi saat disalin-tempel dan diperlakukan sebagai “bagian” dari karakter lain
    • Perbedaannya adalah karakter PUA biasanya dirender cukup terlihat dalam satu atau lain bentuk, sedangkan variation selector tidak begitu
    • Ada konteks yang terlewat: ini adalah ide yang muncul dalam diskusi seputar submission Open Heart Protocol
      https://news.ycombinator.com/item?id=42791378
      Karena API dibatasi hanya menerima emoji, potensi pemanfaatan kriminal langsung dibahas. Untuk kegunaan itu PUA tidak bisa dipakai, dan harus dienkode di dalam emoji
    • Bukankah ini lebih mirip penggunaan noncharacter yang ditetapkan daripada private-use area? PUA juga dipakai untuk encoding tidak resmi sistem tulisan yang belum masuk Unicode atau hal seperti logo Apple, jadi kalau dipakai seperti ini saya khawatir akan terjadi konflik
      Noncharacter yang ditetapkan mencakup 0xFFFF, 0xFFFE, dan dua code point terakhir di setiap plane, serta satu area di tengah Arabic Presentation Forms. Seingat saya, daftar itu kemudian ditambah agar orang punya lebih banyak noncharacter untuk dipakai seperti ini
    • Jujur saja, saya menempelkan komentar ini ke decoder yang disediakan. Saya pikir tidak mungkin komentarnya meleset sejauh ini dan pasti ada pesan tersembunyi di dalamnya, tetapi tampaknya memang meleset atau situs web ini menghapusnya
      Anda tidak bisa membuat watermark karakter arbitrer secara tak terlihat dengan karakter PUA yang tidak dikenali. Sebab karakter itu tidak diperlakukan sebagai combining character. Sebagai gantinya akan muncul kotak placeholder yang dirender terpisah. Contoh: — tentu saja jika Anda memang memakai private use area secara pribadi, mungkin saja bukan kotak
  • Sekitar 10 tahun lalu saya pernah mengagetkan rekan kerja dengan menyisipkan U+202D LEFT-TO-RIGHT OVERRIDE di tengah nama file Windows. funnypicturegnp.exe terlihat seperti funnypictureexe.png
    Jika ditambah ikon kustom yang tampak seperti preview foto, hasilnya cukup meyakinkan

    • Saya pernah bekerja di deteksi phishing, dan ini pola yang sering dipakai penyerang. .exe kebanyakan diblokir otomatis, tetapi ekstensi berbahaya masa kini sering berupa .html, lalu menampilkan halaman login palsu lewat redirect window.location yang diobfuscate
      Penyalahgunaan RTL seperti cute-cat-lmth.png cukup umum, tetapi juga sangat mudah dideteksi, dan email seperti itu langsung kami tandai sebagai phishing
    • Versi source code dari ini adalah CVE-2021-42574, dan ada situs webnya juga
      https://trojansource.codes/
      Pada dasarnya, ini memungkinkan menyembunyikan kode yang tampak seperti komentar tetapi berfungsi sebagai kode saat dikompilasi. Namun, saya ingat status CVE-nya diperdebatkan karena banyak editor teks sudah membuat komentar mencurigakan seperti ini terlihat
    • Saya tidak tahu trik spesifik ini, tetapi senang kebiasaan paranoid saya selama puluhan tahun untuk selalu membuka file media yang berpotensi mencurigakan lewat “klik kanan → Open with” jadi terbukti benar
    • Saya pernah membuat file bat bernama guitar_tab.txt
  • Sebagai contoh penggunaan nyata, Sanity memakai trik ini untuk mengenkode Content Source Maps ke dalam teks aktual yang disajikan di halaman web dalam “mode pratinjau”0. Editor dapat dengan mudah melacak hingga lokasi asal di dalam struktur konten yang dalam hanya dengan mengeklik teks atau konten tersebut
    Ada juga kekurangan dan batasannya. Misalnya, harus dicegah agar ini tidak ditambahkan ke nilai yang perlu diparse atau digunakan apa adanya, seperti tanggal·timestamp, URL, atau ID. Meski begitu, ini trik yang cukup menarik
    0 https://www.sanity.io/docs/stega
    [1] https://github.com/sanity-io/content-source-maps

  • Saya suka ide memakai ini untuk watermarking keluaran LLM. Posisinya pas. Bagaimanapun, 99% generator berkualitas rendah yang cuma copy-paste mau tak mau akan ketahuan, dan hampir tidak berdampak pada use case inti lainnya
    Saya juga penasaran berapa banyak yang akan disisipkan untuk tiap huruf atau token keluaran. Apakah hal-hal seperti ID pengguna, referensi prompt, tanggal, nomor token? Juga penasaran bagaimana ini ditafsirkan di terminal; benar-benar keren

    • Saya tidak mengerti kenapa semua orang berpikir watermarking AI akan berhasil. Watermarking apa pun bisa langsung dan mudah dihapus, jadi tidak akan pernah benar-benar berfungsi dengan baik
      Satu-satunya pertahanan AI yang nyata adalah mewajibkan tanda tangan kunci yang diverifikasi dengan identitas asli untuk semua interaksi manusia, tetapi itu pun A: tidak akan pernah terjadi, dan B: bisa disalahgunakan di negara dengan pemerintah korup atau negara dengan pemerintah korup yang sangat dipengaruhi industri swasta, misalnya AS
    • Karena ada begitu banyak prapemrosesan yang dilakukan sebelum data dimasukkan ke dataset, akan mengejutkan kalau trik seperti ini benar-benar berhasil di praktiknya
    • Di sebagian besar terminal Linux, apa yang dikirimkan akan diteruskan begitu saja sebagai urutan byte. Teknik ini sesuai dengan UTF-8 dan tidak memakai glif tambahan, jadi pada terminal yang mematuhi Unicode, ini tidak terlihat oleh mata manusia. Saya sudah mencobanya di beberapa terminal
      Tentu saja kalau kalimatnya dikirim ke xxd, itu akan terlihat. Berbeda dengan usulan PUA pada komentar teratas saat ini yang langsung terlihat
      Setelah pengujian tambahan, jika ditempelkan ke terminal lalu dilihat dengan xxd, pesannya lewat sepenuhnya tanpa perubahan, tetapi jika dipilih dari terminal lalu ditempelkan lagi, pada X selection di mate terminal dan konsole hanya tersisa beberapa kata dan terpotong. Saya tidak tahu apakah pemotongan itu karena terminal atau karena X. Di xterm, huruf e terakhir berubah dan isi pilihan terpotong lebih banyak
      Di file, kalimat tercatat tanpa perubahan. Jadi sepertinya sebagian data hilang saat disalin keluar dari terminal. Saya mengujinya dengan echo kalimat ke file uji, membukanya di browser, lalu menyalin teksnya
    • Untuk watermarking LLM, ada pendekatan lain yang jauh lebih tangguh dan lebih sulit dideteksi. Pendekatan itu memanfaatkan fakta bahwa LLM membuat distribusi probabilitas yang memberi probabilitas untuk setiap kemungkinan token berikutnya, lalu membuat keluaran dengan mengambil sampel acak dari sana
      Jika saat generasi kita mengutak-atik cara sampling, nantinya kita bisa menjalankan LLM lagi dan mengamati pola keluarannya untuk mendeteksi sidik jari. Misalnya dengan memilih token berprobabilitas tinggi dan token berprobabilitas rendah secara bergantian. Implementasi nyatanya tentu akan jauh lebih canggih, tetapi idenya mengarah ke sana
  • Yang menarik, pembaca layar dapat mendeteksi variation selector seperti ini saat berpindah per karakter. Jika bergerak dengan tombol panah di atas contoh, ia akan membacakan seperti “Smiling face with smiling eyes”, “Symbol e zero one five five”, “Symbol e zero one five c”
    Namun ini bergantung pada speech synthesizer yang digunakan, dan jika dokumen hanya sedang dibaca biasa, kita tidak akan tahu ada karakter semacam itu, jadi secara keseluruhan bukan keuntungan besar

    • Karena teks online secara umum tercemar oleh karakter yang tidak terlihat tetapi menyebalkan untuk didengar, saya memakai skrip di pembaca layar saya untuk menghapus semua karakter non-ASCII
  • StegCloak0 juga satu keluarga yang mirip, dan mendorong ide ini selangkah lebih jauh dengan mengenkripsi payload tersembunyi menggunakan AES-256-CTR. Trik kecil yang cukup bagus
    0 https://github.com/KuroLabs/stegcloak

    • Sepertinya ada plugin Better Discord yang memakai ini atau cara serupa. Anda bisa mengirim pesan terenkripsi penuh yang bagi orang lain terlihat seperti bukan apa-apa
      Namun agar pihak lain dapat mendekodenya, Anda harus berbagi nilai rahasia kata sandi
    • Saya sempat mencoba mengujinya pada TXT record DNS Cloudflare, tetapi Cloudflare dengan pintarnya mendekodekannya saat ditempelkan ke field TXT
  • Judulnya agak menyesatkan. Tertulis bahwa “karakter dasar tidak harus berupa emoji, dan pemrosesan variation selector sama saja pada karakter biasa. Dengan emoji hanya jadi lebih menyenangkan”
    Jika cara ini dipakai pada karakter non-emoji, hasilnya menjadi lebih tersembunyi dan lebih merepotkan

    • Sepertinya tidak sampai semerepotkan itu. Detektor bisa dibuat tanpa banyak kesulitan. Jika ada variant yang menempel pada karakter yang sebenarnya tidak memiliki variasi, tinggal tampilkan saja. Malah sepertinya bisa juga dipakai untuk keperluan tanda tangan
  • Dibanding sekadar watermarking keluaran LLM, ini tampak bisa menjadi cara rapi untuk membungkus data logprobs bersama teks
    Pada dasarnya, ini memasukkan informasi probabilitas dari semua token yang dihasilkan untuk memberi sedikit transparansi pada proses generasi. Ini juga ada di spesifikasi OpenAI API, dan beberapa engine seperti llama.cpp juga menyediakan informasi ini. Biasanya ditempelkan sebagai field terpisah, tetapi ada juga cara visualisasi seperti mikupad0
    Mungkin ini ide buruk, tetapi tetap saja gagasannya menggelitik

  • Teknik yang keren. Ini mencerminkan ASCII dan juga ada karakter Unicode Tag yang jarang terlihat di elemen UI, terutama aplikasi web.
    Keunikan karakter Tag adalah sebagian LLM menafsirkan teks tersembunyi sebagai ASCII dan mengikuti instruksinya, bahkan bisa menuliskannya secara langsung.
    https://embracethered.com/blog/posts/2024/hiding-and-finding...
    Ada juga proof-of-concept eksploit nyata yang telah diperbaiki Microsoft di Copilot.
    https://embracethered.com/blog/posts/2024/m365-copilot-promp...