Фингерпринтинг текст непечатаемыми символами

Фингерпринтинг текст непечатаемыми символами

}{@kep


Символы нулевой ширины — это непечатаемые управляющие символы, которые не отображаются большинством приложений. Н​апример, в э​то пред​ложение я вст​авил де​сять про​​белов н​улевой ширины, вы эт​о замет​или? (Подсказка: вставьте предложение в Diff Checker, чтобы увидеть местоположение символов!). Эти символы можно использовать как уникальные «отпечатки» текста для идентификации пользователей.


Зачем?

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


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


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


Как?

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


Фингерпринтинг текста

1. Получить имя пользователя, вошедшего в систему, и преобразовать его в двоичный файл.


Здесь мы просто преобразуем каждую букву имени пользователя в двоичный эквивалент.


const zeroPad = num => ‘00000000’.slice(String(num).length) + num;
const textToBinary = username => (
  username.split('').map(char =>
    zeroPad(char.charCodeAt(0).toString(2))).join(' ')
);


2. Взять имя пользователя в бинарном формате и преобразовать его в непечатаемые символы


Следующий скрипт перебирает двоичную строку и преобразует каждый бит 1 в непечатаемый пробел, каждый 0 — в непечатаемый символ запрета лигатур (non-joiner). После преобразования каждой буквы вставляем непечатаемый символ разрешения лигатур (joiner) — и переходим к следующей.


const binaryToZeroWidth = binary => (
  binary.split('').map((binaryNum) => {
    const num = parseInt(binaryNum, 10);
    if (num === 1) {
      return '​'; // zero-width space
    } else if (num === 0) {
      return '‌'; // zero-width non-joiner
    }
    return '‍'; // zero-width joiner
  }).join('') // zero-width no-break space
);


3. Вставка «имени пользователя» в непечатаемый конфиденциальный текст


Здесь просто вставляем блок непечатаемых символов в конфиденциальный текст.


Извлечение имени пользователя из помеченного текста


Те же действия в обратном порядке.


1. Извлечь непечатаемое «имя пользователя» из конфиденциального текста


Удалить конфиденциальный текст из строки, оставив только непечатаемые символы.


2. Преобразовать непечатаемое «имя пользователя» обратно в двоичный файл


Здесь мы разбиваем строку на фрагменты, с учётом добавленных межбуквенных разделителей. Это даёт эквивалент в управляющих символах для каждой буквы имени пользователя! Перебираем символы и возвращаем 1 или 0, чтобы воссоздать двоичную строку. Если не находим соответствующий 1 или 0, то значит попали на межбуквенный разделитель (символ разрешения лигатур) и, таким образом, завершиили двоичное преобразование для символа: можно добавить к строке один пробел и переходить к следующему символу.


const zeroWidthToBinary = string => (
  string.split('').map((char) => { // zero-width no-break space
    if (char === '​') { // zero-width space
      return '1';
    } else if (char === '‌') {  // zero-width non-joiner
      return '0';
    }
    return ' '; // add single space
  }).join('')
);


3. Преобразование имени пользователя из двоичного формата обратно в текст


В конце концов, анализируем двоичную строку и преобразуем каждую серию 1 и 0 в соответствующий символ.


const binaryToText = string => (
  string.split(' ').map(num =>
    String.fromCharCode(parseInt(num, 2))).join('')
);


Заключение

Компании как никогда много внимания уделяют утечкам информации и поиску инсайдеров. Этот лишь один из многих трюков, которые можно использовать. В зависимости от направления вашей работы, может быть жизненно важно понимать риски, связанные с копированием текста. Очень немногие приложения отображают непечатаемые символы. Например, вы можете предположить, что ваш терминал попытается их отобразить (мой нет!).


Если вернуться к секретной доске объявлений, то план сработал как надо. Вскоре после внедрения скрипта вышло новое объявление. В течение нескольких часов текст распространили в другом месте с прикрепленной непечатаемой строкой. Имя пользователя виновника успешно идентифицировали, и его забанили: хэппи-энд!


Конечно, есть определённые оговорки по использованию этого метода. Например, если пользователь знает о скрипте, то теоретически может заменить непечатаемые символы, чтобы подставить другого человека. Так что лучше вместо имени пользователя вставлять уникальный секретный ID.


Чтобы поиграться со скриптом, запускайте демо или смотрите исходный код.


Report Page