- Bằng cách nối các variation selector của Unicode, có thể giấu một chuỗi byte phía sau một ký tự; chúng không hiển thị trên màn hình nhưng vẫn đi theo khi sao chép/dán
- Có 256 variation selector, từ VS-1 đến VS-256, nên có thể tạo ánh xạ khớp chính xác với phạm vi 1 byte
- Ngay cả khi gắn các byte của
hello là [0x68, 0x65, 0x6c, 0x6c, 0x6f] sau 😊, bề ngoài nó vẫn trông như một emoji bình thường
- Việc giải mã tìm các phạm vi
U+FE00..U+FE0F và U+E0100..U+E01EF rồi chuyển lại thành byte; ký tự cơ sở không nhất thiết phải là emoji
- Cách này là sự lạm dụng Unicode và có thể bị lợi dụng để vượt qua bộ lọc nội dung của con người hoặc cài watermark vào văn bản
Cách dữ liệu vô hình được gắn vào một ký tự
- Văn bản Unicode được biểu diễn bằng một chuỗi codepoint và thường được ghi theo dạng
U+XXXX
- Với các chữ cái Latin đơn giản, codepoint và ký tự hiển thị trên màn hình tương ứng 1:1
- Ví dụ:
U+0067 biểu diễn ký tự g
- Trong các hệ chữ khác, một ký tự hiển thị trên màn hình có thể được cấu thành từ nhiều codepoint
- Ví dụ: trong Devanagari, chữ đọc là
ki được biểu diễn bằng cặp liên tiếp U+0915 và U+0940
Dùng variation selector như kho lưu trữ dữ liệu
- Unicode định nghĩa 256 codepoint variation selector, được đặt tên từ VS-1 đến VS-256
- Bản thân variation selector không được hiển thị trên màn hình, mà được dùng để thay đổi cách hiển thị của ký tự đứng trước
- Phần lớn ký tự Unicode không có biến thể liên kết, nhưng vì Unicode hướng đến khả năng tương thích trong tương lai, mã xử lý không hiểu ý nghĩa cũng phải bảo toàn variation selector
- Ngay cả khi gắn
U+FE01 (VS-2) sau U+0067 (g), trên màn hình nó vẫn trông như chữ g thường
- Khi sao chép/dán, variation selector cũng đi kèm
- Vì 256 variation selector đúng bằng số lượng có thể biểu diễn 1 byte, nên có thể giấu dữ liệu 1 byte sau một codepoint Unicode bất kỳ
- Đặc tả Unicode không xử lý cụ thể các chuỗi có nhiều variation selector liên tiếp, và ngụ ý rằng chúng nên bị bỏ qua trong quá trình render
- Nếu nối nhiều variation selector, có thể biểu diễn một chuỗi byte tùy ý phía sau một ký tự
Mã hóa byte thành variation selector
- Variation selector được chia thành hai phạm vi codepoint
U+FE00 .. U+FE0F: 16 cái đầu tiên
U+E0100 .. U+E01EF: 240 cái còn lại
- Quy tắc chuyển byte thành variation selector rất đơn giản
- Nếu byte nhỏ hơn 16 thì
0xFE00 + byte
- Ngược lại thì
0xE0100 + (byte - 16)
- Khi mã hóa, trước tiên đặt một ký tự cơ sở (base character), rồi chuyển từng byte thành variation selector và nối phía sau
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
}
- Nếu gắn các byte biểu diễn
hello là [0x68, 0x65, 0x6c, 0x6c, 0x6f] sau 😊, chuỗi tạo ra bề ngoài sẽ trông như một emoji bình thường
- Trong kết quả xuất thông thường, các ký tự ẩn không thấy được, nhưng nếu in bằng định dạng debug của Rust thì các codepoint ẩn như
\u{e0158} sẽ lộ ra
"😊\u{e0158}\u{e0155}\u{e015c}\u{e015c}\u{e015f}"
Cách đọc lại các byte ẩn
- Việc giải mã duyệt qua các ký tự và chuyển các codepoint nằm trong phạm vi variation selector trở lại thành byte
- Phạm vi
U+FE00..U+FE0F được khôi phục bằng variation_selector - 0xFE00
- Phạm vi
U+E0100..U+E01EF được khôi phục bằng variation_selector - 0xE0100 + 16
- Các ký tự thường trước khi gặp variation selector đầu tiên được xem là ký tự cơ sở và bị bỏ qua
- Nếu gặp ký tự không phải variation selector và đã có kết quả, quá trình giải mã sẽ kết thúc
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
}
}
- Sau khi giải mã cùng kết quả mã hóa đó và diễn giải theo UTF-8, sẽ thu được
"hello"
- Ký tự cơ sở không nhất thiết phải là emoji; việc xử lý variation selector cũng giống nhau với ký tự thông thường
- Lý do dùng emoji là vì vui hơn
Khả năng bị lạm dụng
- Cách này là lạm dụng Unicode và không nên sử dụng
- Trong kết quả đã render, dữ liệu không hiển thị, nên các moderator hoặc reviewer là con người khó biết rằng có dữ liệu ẩn tồn tại
- Nó có thể bị lạm dụng như một cách giấu dữ liệu để vượt qua bộ lọc nội dung của con người
- Cũng có thể dùng cho watermarking văn bản
- Sau khi gửi thông điệp cho nhiều người, nếu bị rò rỉ thì có thể truy vết người nhận ban đầu
- Chuỗi variation selector sống sót qua hầu hết thao tác sao chép/dán
- Cho phép mật độ dữ liệu tùy ý, và nếu muốn có thể chèn watermark vào từng ký tự
LLM có thể xử lý dữ liệu ẩn không
- Sau khi bài được đăng lên Hacker News, đã có câu hỏi về việc LLM xử lý loại dữ liệu ẩn này như thế nào
- Nhìn chung, tokenizer có vẻ bảo toàn variation selector dưới dạng token, nên về lý thuyết mô hình có thể truy cập được
- OpenAI tokenizer là công cụ kiểm tra có thể dùng để xác nhận điều này
- Nhìn chung, các mô hình dường như không tự cố gắng giải mã trực tiếp ở bên trong
- Khi dùng cùng code interpreter, một số mô hình có thể giải dữ liệu ẩn
1 bình luận
Các ý kiến trên Hacker News
Về việc lạm dụng Unicode thì đây chỉ là phần nổi của tảng băng. Với các kỹ thuật tương tự, có thể làm tràn bộ đệm trong nhiều hệ thống nhận chuỗi Unicode; thường thì chỉ kết thúc bằng lỗi hoặc crash, nhưng nếu may mắn cũng có thể tạo ra vài hành vi khá thú vị
Thời trước Python 3, khi làm pentest, tôi từng dùng riêng các dấu phụ để kéo một ký tự thành nhiều byte và làm tràn bộ đệm của web server backend. Khi đó chỉ là crash và tự động khởi động lại, nhưng nếu đào đủ sâu thì có vẻ có thể dùng để khai thác một hệ thống hay phần mềm cụ thể
Ngay cả với các form hiện đại, chỉ cần tắt JavaScript cũng có thể gây ra chuyện tương tự; trường hợp tốt nhất là debug đang bật nên stack trace hoặc truy vấn được in ra, rò rỉ một chút thông tin. Một lỗi phổ biến khác là đếm sai độ dài
\nvà\r\ntrong chuỗi văn bản: JavaScript thường tính carriage return là 1 byte, nhưng đặc tả HTTP yêu cầu 2 byteunescape(encodeURIComponent("ç")).lengthlà một cách đại khái để kiểm tra nhanh độ dài theo byte trong JavaScript, còn vấn đề\r\nthì chỉ cần chuẩn hóa chuỗi trước khi đếm độ dàiCái này dễ thương, nhưng không thật sự cần thiết. Unicode có một vùng lớn gọi là PUA (private use area); các mã trong vùng này không được ánh xạ tới ký tự nào và cũng sẽ không được ánh xạ trong tương lai, nên được dùng cho mục đích nội bộ/tùy biến của người dùng
Ví dụ trong fish-shell, khi phân tích token thành chuỗi một cách an toàn, các ký tự đặc biệt chưa được escape sẽ được đổi thành các code point Unicode khác trong chuỗi nhưng đặt vào vùng PUA, rồi bị chặn lại ở giai đoạn sau trong pipeline. Không nên để chúng lộ ra ngoài ranh giới API, nhưng khi gặp thì khuyến nghị là cứ truyền nguyên trạng, và phần lớn hệ thống cùng thư viện cũng làm vậy. Đây có thể là một kênh rò rỉ rõ ràng, nhưng nhiều lập trình viên bình thường không biết nhiều về Unicode ngoài mức “để tránh vấn đề quốc tế hóa thì luôn dùng Unicode”, nên thường để hở như thế
). Điểm chính ở đây là mã hóa sao cho khi copy-paste nó bị ẩn đi và được coi như “một phần” của ký tự kháchttps://news.ycombinator.com/item?id=42791378
Vì API bị ràng buộc chỉ nhận emoji, khả năng dùng vào mục đích phạm pháp đã lập tức được bàn tới. Trong trường hợp đó không thể dùng PUA mà phải mã hóa bên trong emoji
Các noncharacter được chỉ định bao gồm
0xFFFF,0xFFFEvà hai code point cuối của mỗi mặt phẳng, cũng như một vùng ở giữa Arabic Presentation Forms. Tôi hiểu là danh sách này về sau đã được bổ sung để người ta có thêm noncharacter dùng theo kiểu nàyKhông thể watermark vô hình các ký tự tùy ý bằng các ký tự PUA không được nhận dạng. Chúng không được xử lý như ký tự kết hợp. Thay vào đó sẽ xuất hiện một ô placeholder được render riêng. Ví dụ:
— tất nhiên nếu bạn đang tự dùng private use area theo cách riêng thì có thể nó không phải là ô vuôngKhoảng 10 năm trước, tôi từng làm đồng nghiệp giật mình bằng cách chèn U+202D LEFT-TO-RIGHT OVERRIDE vào giữa tên tệp trên Windows.
funnypicturegnp.exetrông nhưfunnypictureexe.pngNếu thêm cả icon tùy chỉnh trông như ảnh preview thì khá thuyết phục
.exehầu hết bị chặn tự động, nhưng phần mở rộng độc hại ngày nay thường là .html, rồi dùngwindow.locationredirect đã bị làm rối để mở trang đăng nhập giảLạm dụng RTL kiểu
cute-cat-lmth.pngtương đối phổ biến, nhưng cũng rất dễ phát hiện, và những email như vậy bị đánh dấu phishing ngay lập tứchttps://trojansource.codes/
Về cơ bản có thể giấu mã trông như comment nhưng khi biên dịch lại hoạt động như code. Tuy nhiên tôi nhớ tình trạng CVE của nó từng gây tranh cãi vì nhiều trình soạn thảo văn bản đã hiển thị các comment đáng ngờ kiểu này rồi
guitar_tab.txtTrong một trường hợp sử dụng thực tế, Sanity đã dùng mẹo này để mã hóa Content Source Maps vào trong chính văn bản thật được cung cấp cho trang web ở “chế độ xem trước”0. Biên tập viên có thể dễ dàng truy ngược đến vị trí gốc sâu bên trong cấu trúc nội dung chỉ bằng cách nhấp vào văn bản hoặc nội dung đó
Cũng có nhược điểm và giới hạn. Ví dụ, cần ngăn không cho nó được thêm vào các giá trị phải được phân tích cú pháp hoặc sử dụng nguyên trạng như ngày tháng/dấu thời gian, URL, ID. Dù vậy, đây vẫn là một mẹo khá thú vị
0 https://www.sanity.io/docs/stega
[1] https://github.com/sanity-io/content-source-maps
Tôi thích ý tưởng dùng cái này cho watermark đầu ra LLM. Nó chạm đúng điểm. Dù sao thì 99% các trình tạo chất lượng thấp chỉ biết sao chép/dán cũng sẽ bị phát hiện, còn các trường hợp sử dụng cốt lõi khác hầu như không bị ảnh hưởng
Cũng tò mò là sẽ chèn bao nhiêu vào mỗi ký tự hoặc mỗi token đầu ra. Có thể là ID người dùng, tham chiếu prompt, ngày tháng, số thứ tự token? Tôi cũng tò mò terminal sẽ diễn giải nó thế nào; thật sự rất hay
Biện pháp phòng vệ AI thực sự duy nhất là yêu cầu mọi tương tác của con người phải có chữ ký khóa đã được xác minh bằng danh tính thật, nhưng điều đó A: sẽ không bao giờ xảy ra, và B: có thể bị lạm dụng ở các nước có chính phủ tham nhũng, hoặc ở những nước có chính phủ tham nhũng chịu ảnh hưởng mạnh của ngành tư nhân, chẳng hạn như Mỹ
Tất nhiên nếu đưa câu vào
xxdthì nó sẽ hiện ra. Đề xuất PUA trong bình luận cấp cao nhất hiện tại thì khác ở chỗ nó sẽ lộ ngayThử thêm thì thấy sau khi dán vào terminal, qua
xxdthông điệp đi qua hoàn toàn không biến đổi, nhưng nếu chọn lại trong terminal rồi dán lại, ở X selection của mate terminal và konsole thì nó bị cắt, chỉ còn vài từ. Tôi không biết việc cắt này là do terminal hay do X. Trong xterm, chữecuối cùng bị biến đổi và nội dung được chọn còn bị cắt nhiều hơnKhi ghi vào file thì câu được ghi lại không biến đổi. Vì vậy có vẻ giống trường hợp một phần dữ liệu bị rơi mất khi sao chép ra khỏi terminal hơn. Tôi đã
echocâu vào một file thử nghiệm, mở bằng trình duyệt rồi sao chép văn bản để kiểm traNếu can thiệp vào cách lấy mẫu khi sinh văn bản, sau này có thể chạy lại LLM để quan sát dạng đầu ra và phát hiện dấu vân tay. Ví dụ như luân phiên chọn token có xác suất cao và token có xác suất thấp. Tất nhiên triển khai thực tế sẽ tinh vi hơn nhiều, nhưng ý tưởng là theo hướng đó
Một điểm thú vị là trình đọc màn hình có thể phát hiện các variation selector này khi di chuyển theo từng ký tự. Nếu dùng phím mũi tên di chuyển trên ví dụ, nó sẽ đọc kiểu “Smiling face with smiling eyes”, “Symbol e zero one five five”, “Symbol e zero one five c”
Tuy nhiên điều này còn tùy vào bộ tổng hợp giọng nói đang dùng, và nếu chỉ đọc tài liệu bình thường thì không thể biết có những ký tự đó hay không, nên nhìn chung cũng không phải lợi ích lớn
StegCloak0 cũng thuộc nhóm tương tự, và đẩy ý tưởng này xa hơn một bước bằng cách mã hóa payload ẩn bằng AES-256-CTR. Một mẹo nhỏ khá hay
0 https://github.com/KuroLabs/stegcloak
Tuy nhiên để phía kia giải mã thì phải chia sẻ giá trị bí mật mật khẩu
Tiêu đề hơi dễ gây hiểu lầm. Nội dung nói rằng “ký tự cơ sở không nhất thiết phải là emoji, và cách xử lý variation selector cũng giống với ký tự thông thường. Dùng emoji chỉ khiến nó thú vị hơn thôi”
Nếu dùng cách này với ký tự không phải emoji thì nó sẽ kín đáo hơn và phiền phức hơn
Hơn cả watermark đầu ra LLM đơn thuần, đây có vẻ có thể là một cách gọn gàng để đóng gói kèm dữ liệu logprobs
Về cơ bản là bao gồm thông tin xác suất của mọi token đã được sinh ra, nhằm đem lại một chút minh bạch cho quá trình tạo. Nó cũng có trong đặc tả OpenAI API, và nhiều engine như
llama.cppcũng cung cấp thông tin này. Thường thì nó được gắn dưới dạng một trường riêng, nhưng cũng có các cách trực quan hóa như mikupad0Có lẽ đây là một ý tưởng tồi, nhưng vẫn là một ý tưởng khiến người ta thấy ngứa ngáy trong đầu
Đây là một kỹ thuật rất hay. Nó phản ánh ASCII và còn có ký tự Unicode Tag, một thành phần UI, đặc biệt là trong ứng dụng web, thường không thấy xuất hiện
Điểm độc đáo của ký tự Tag là một số LLM diễn giải văn bản ẩn thành ASCII và làm theo chỉ thị, thậm chí còn có thể viết trực tiếp chúng
https://embracethered.com/blog/posts/2024/hiding-and-finding...
Cũng có một proof-of-concept exploit thực tế mà Microsoft đã sửa trong Copilot
https://embracethered.com/blog/posts/2024/m365-copilot-promp...)