Думаю, ты не раз встречал эксплойты, которые классифицируются как Unicode, подыскивал нужную кодировку для отображения страницы, радовался очередным кракозябрам то тут, то там. Да мало ли чего еще! Если ты хочешь узнать, кто заварил всю эту кашу, и расхлебывает по сей день, пристегни ремни и читай дальше.

Как говорится, - «инициатива наказуема» и, как всегда, виноваты во всем оказались американцы.

А дело было так. На заре расцвета компьютерной индустрии и распространения интернета появилась необходимость в универсальной системе представления символов. И в 60 годах прошлого века появляется ASCII - «American Standard Code for Information Interchange» (Американский Стандартный Код для Обмена Информацией), знакомая нам 7-битная кодировка символов. Последний восьмой неиспользованный бит был оставлен как управляющий бит для настройки ASCIIтаблицы под свои нужды каждого заказчика компьютера отдельного региона. Такой бит позволял расширить ASCII-таблицу для использования своих символов каждого языка. Компьютеры поставлялись во многие страны, где уже и использовали свою, модифицированную таблицу. Но позже такая фича переросла в головную боль, так как обмен данными между ЭВМ стал достаточно проблематичным. Новые 8-битные кодовые страницы были несовместимы между собой - один и тот же код мог означать несколько разных символов. Для разрешения этой проблемы ISO («International Organization for Standardization», Международная Организация по Стандартизации) предложила новую таблицу, а именно - «ISO 8859».

Позже этот стандарт переименовали в UCS («Universal Character Set», Универсальный Набор Символов). Однако, к моменту первого выпуска UCS появился Unicode. Но так как цели и задачи обоих стандартов совпадали, было принято решение объединить усилия. Что ж, Unicode взял на себя нелегкую задачу - дать каждому символу уникальное обозначение. На данный момент последняя версия Unicode - 5.2.

Хочу предупредить - на самом-то деле история с кодировками весьма мутная. Разные источники предоставляют разные факты, так что не стоит зацикливаться на одном, достаточно быть в курсе того, как все образовывалось и следовать современным стандартам. Мы ведь, надеюсь, не историки.

Краш-курс unicode

Прежде чем углубляться в тему, хотелось бы пояснить, что представляет собой Unicode в техническом плане. Цели данного стандарта мы уже знаем, осталось лишь матчасть подлатать.

Итак, что есть Unicode? Проще говоря - это способ представить любой символ в виде определенного кода для всех языков мира. Последняя версия стандарта вмещает порядка 1 100 000 кодов, которые занимают пространство от U+0000 до U+10FFFF. Но тут будь внимателен! Unicode строго определяет то, что есть код для символа и то, как этот код будет представлен в памяти. Коды символов (допустим, 0041 для символа «A») не имеют какоголибо значения, а вот для представления этих кодов байтами есть своя логика, этим занимаются кодировки. Консорциум Unicode предлагает следующие виды кодировок, называемые UTF (Unicode Transformation Formats, «Форматы Преобразования Unicode»). А вот и они:

  • UTF-7: данную кодировку не рекомендуется использовать из соображений безопасности и совместимости. Описана в RFC 2152. Не является частью Unicode, однако была представлена данным консорциумом.
  • UTF-8: самая распространенная кодировка в веб-пространстве. Является переменной, шириной от 1 до 4 байт. Обратно совместима со протоколами и программами, использующими ASCII. Занимает пределы от U+0000 до U+007F.
  • UTF-16: использует переменную ширину от 2 до 4 байт. Чаще всего встречается применение 2 байт. UCS-2 является этой же кодировкой, только с фиксированной шириной 2 байта и ограничено пределами BMP.
  • UTF-32: использует фиксированную ширину в 4 байта, то есть 32 бита. Однако используется только 21 бит, остальные 11 забиты нулями. Пусть данная кодировка и громоздка в плане пространства, но считается наиболее эффективной по быстродействию за счет 32-битной адресации в современных компьютерах.

Ближайший аналог UTF-32 - кодировка UCS-4, но сегодня используется реже.

Несмотря на то, что в UTF-8 и UTF-32 можно представить чуть больше двух миллиардов символов, было принято решение ограничиться миллионом с хвостиком - ради совместимости с UTF-16. Все пространство кодов сгруппировано в 17 плоскостей, где в каждой по 65536 символов. Наиболее часто употребляемые символы расположены в нулевой, базовой плоскости. Именуется как BMP - Basic MultiPlane.
Поток данных в кодировках UTF-16 и UTF-32 может быть представлен двумя способами - прямым и обратным порядком байтов, называются UTF-16LE/UTF-32LE, UTF16BE/UTF-32BE, соответственно. Как ты и догадался, LE - это little-endian, а BE - big-endian. Но надо как-то уметь различать эти порядки. Для этого используют метку порядка байтов U+FEFF, в английском варианте - BOM, «Byte Order Mask». Данный BOM может попадаться и в UTF-8, но там он ничего не значит.

Ради обратной совместимости Юникоду пришлось вместить в себя символы существующих кодировок. Но тут возникает другая проблема - появляется много вариантов идентичных символов, которые как-то нужно обрабатывать. Поэтому нужна так называемая «нормализация», после которой уже можно сравнить две строки. Всего существует 4 формы нормализации:

  • Normalization Form D (NFD): каноническая декомпозиция.
  • Normalization Form C (NFC): каноническая декомпозиция + каноническая композиция.
  • Normalization Form KD (NFKD): совместимая декомпозиция.
  • Normalization Form KC (NFKC): совместимая декомпозиция + каноническая композиция.

Теперь подробнее про эти странные слова.

Юникод определяет два вида равенств строки - каноническая и по совместимости.

Первое предполагает разложение сложного символа на несколько отдельных фигур, которые в целом образуют исходный символ. Второе равенство подыскивает ближайший подходящий по виду символ. А композиция - это объединение символов из разных частей, декомпозиция - обратное действие. В общем, глянь на рисунок, все станет на место.

В целях безопасности нормализацию следует делать до того, как строка поступит на проверку каким-либо фильтрам. После данной операции размер текста может меняться, что может иметь негативные последствия, но об этом чуть позже.

В плане теории на этом все, я многое еще не рассказал, но надеюсь ничего важного не упустил. Unicode невообразимо обширен, сложен, по нему выпускают толстые книги, и очень нелегко сжато, доступно и полноценно объяснить основы столь громоздкого стандарта. В любом случае, для более глубокого понимания следует пройтись по боковым ссылкам. Итак, когда картина с Юникодом стала более-менее ясна, можем ехать дальше.

Зрительный обман

Наверняка ты слышал про IP/ARP/DNSспуфинг и хорошо представляешь, что это такое. Но есть еще так называемый «визуальный спуфинг» - это тот самый старый метод, которым активно пользуются фишеры для обмана жертв. В таких случаях применяют использование схожих букв, наподобие «o» и «0», «5» и «s». Это наиболее распространенный и простой вариант, его и легче заметить. Как пример можно привести фишинг-атаку 2000 года на PayPal, о которой даже упомянуто на страницах www.unicode.org . Однако к нашей теме Юникода это мало относится.

Для более продвинутых ребят на горизонте появился Unicode, а точнее - IDN, что является аббревиатурой от «Internationalized Domain Names» (Интернационализованные Доменные Имена). IDN позволяет использовать символы национального алфавита в доменных именах. Регистраторы доменных имен позиционируют это как удобную вещь, мол, набирай доменное имя на своем родном языке! Однако, удобство это весьма сомнительное. Ну да ладно, маркетинг - не наша тема. Зато представь, какое это раздолье для фишеров, сеошников, киберсквоттеров и прочей нечисти. Я говорю об эффекте, что называется IDN-спуфинг. Данная атака относится к категории визуального спуфинга, в английской литературе это еще называют как «homograph attack», то есть, атаки с использованием омографов (слов, одинаковых в написании).

Да, при наборе букв никто не ошибется и не наберет заведомо ложный домен. Но ведь чаще всего пользователи щелкают по ссылкам. Если хочешь убедиться в эффективности и простоте атаки, то посмотри на картинку.

В качестве некоторой панацеи был придуман IDNA2003, но уже в этом, 2010 году, вступил в силу IDNA2008. Новый протокол должен был решить многие проблемы молодого IDNA2003, однако внес новые возможности для спуфинг-атак. Снова возникают проблемы совместимости - в некоторых случаях один и тот же адрес в разных браузерах может вести на разные сервера. Дело в том, что Punycode может преобразован по-разному для разных браузеров - все будет зависеть от того, спецификации какого стандарта поддерживаются.
Проблема зрительного обмана на этом не заканчивается. Unicode и тут приходит на службу спамерам. Речь про спам-фильтры - исходные письма прогоняются спамерами через Unicode-обфускатор, который подыскивает схожие между собой символы разных национальных алфавитов по так называемому UC-Simlist («Unicode Similarity List», список схожих символов Unicode). И все! Антиспам фильтр пасует и уже не может в такой каше символов распознать нечто осмысленное, зато юзер вполне способен прочитать текст. Не отрицаю, что решение для такой проблемы нашли, однако флаг первенства за спамерами. Ну и еще кое-что из этой же серии атак. Ты точно уверен, что открываешь некий текстовый файл, а не имеешь дело с бинарником?

На рисунке, как видишь, имеем файл с названием evilexe. txt. Но это фальш! Файл на самом-то деле называется eviltxt.exe. Спросишь, что это еще за фигня в скобках? А это, U+202E или RIGHT-TO-LEFT OVERRIDE, так называемый Bidi (от слова bidirectional) - алгоритм Юникода для поддержки таких языков, как арабский, иврит и прочих. У последних ведь письменность справа налево. После вставки символа Юникода RLO, все, что идет после RLO, мы увидим в обратном порядке. Как пример данного метода из реальной жизни могу привести спуфинг-атаку в Mozilla Firfox - cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2009-3376 .

Обход фильтров - этап № 1

На сегодня уже известно, что нельзя обрабатывать длинные формы (non-shortest form) UTF-8, так как это является потенциальной уязвимостью. Однако разработчиков PHP этим не вразумить. Давай разберемся, что собой представляет данный баг. Возможно ты помнишь про неправильную фильтрацию и utf8_decode(). Вот этот случай мы и рассмотрим более детально. Итак, мы имеем такой PHP-код: