Как стать автором
Обновить

Стеганография в .NET приложениях или водяные знаки

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

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

Мы рассмотрим здесь только Watermark'и предназначение которых – ни при каких условиях не быть удалёнными, чтобы создатель приложения имел возможность считать их после любых атак потенциального злоумышленника, а пользователи приложения о них не догадывались. Есть и другие виды вотермарок, предназначенные, например, для отслеживания изменений в приложении, эдакие скрытые чексуммы, и они также должны быть сложно удаляемы, но это уже другая история.

Самый лучший Watermark


Прекрасный способ внедрения Watermark в приложение – это пофантазировать и придумать место, где никакой хакер Вашу вотермарку искать не будет: просто побоится потонуть в тоннах кода и забросит это дело. Если Вы разрабатываете визуальное приложение, то ничего не мешает менять цвет пикселя спрятанного в углу какой-нибудь кнопки в Богом забытом диалоговом окне. Цвет пикселя и будет вотермаркой. К сожалению такой случай не всегда приемлем и разработчикам удобнее воспользоваться каким-нибудь универсальным решением для внедрения вотермарки в уже скомпилированное приложение. Традиционно такую функцию встраивают в обфускаторы.

Как хакер будет бороться с Watermark?


Итак, нам необходимо универсальное решения для вставки вотермарки в приложение. Универсальность накладывает серьёзные ограничения ведь мы не собираемся писать искусственный интеллект, который определит какой пиксель можно подкрасить, например. На первый взгляд можно подумать: «Так ведь так много есть места, где можно незаметно записать какие-то данные. Выбирай любое!» Но не будем торопиться.

Представим, что хакер – умный хакер и он точно знает, что вотермарки в приложении есть и их надо найти и обезвредить, оставив приложение работающим. Пусть после очередной попытки удаления вотермарки он каким-то неведомым образом может узнать смогли ли разработчики прочитать вотермарку или нет (а что, нам не жалко). Последовательно хакер будет выполнять следующие атаки:

  • Дизассемблировать/ассемблировать приложение. Здесь обломаются все методы, впихивающие вотермарку в разные потаённые места файла, в надежде, что никто не догадается. Да, не догадается, но удалит.
  • Запаковать/обфусцировать приложение. Здесь обломаются все методы не требующие реального исполнения программы, так как полно протекторов, полностью пересобирающих все заголовки и затирающих все исходные данные, а универсальными анпакерами обладают только крупные антивирусные компании и писать такое мы не будем.
  • Будет задействован обфускатор, активно мешающий вмешательству дебагера в ход исполнения программы. Так что подключиться к приложению и ставить в нём точки останова и т.д. не получится.
  • Хакер найдёт нашу впихивалку вотермарок (обязательно найдёт!) и понавставляет кучу своих вотермарок, чтобы затереть существующие.
  • Хакер вставит свою вотермарку в своё типовое приложение, состоящее из тысячи нопов и раскусит наш алгоритм.
  • Хакер приступит к ручному дизассемблированию и, так как он этим многие годы занимается, раскусит алгоритм. Предлагаю думать, что от этого нет панацеи, хакеры они такие.
  • Хакер напишет статический анализатор, ищущий все вотермарки и выдирающий их.

Как можно заметить от последнего пункта защиты нет, но хотелось бы сделать так, чтобы немногие хакеры до него добрались.

Как же быть?


И что же делать? Есть ли панацея? Окончательного ответа на этот вопрос нет, проблема эта очень объемная и давно изучается. Посмотрите, например, следующий обзор академических исследований на эту тему: citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.103.8892&rep=rep1&type=pdf.

Всё же есть два места относительно стабильные к изменениям, которым приложение нещадно подвергает хакер.

  • Первое место: данные приложения. Само собой не строки, так как их любой обфускатор зашифрует и в памяти Вы их увидите лишь на мгновенье.
  • Второе место: логика кода приложения. Но заметьте: именно логика, так как хакерские обфускаторы сам код исказят.

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

Стратегия


Из всего вышесказанного про хакерские обфускаторы и протекторы (хотя бы из хакерской атаки номер два) ясно, что для считывания вотермарки придётся запустить приложение и как-то подключиться к нему во время работы. Мы предполагаем, что хакер может навесить протектор, запрещающий дебаг, а значит для извлечения вотермарки надо очень нежно касаться приложения. На ум приходят следующие варианты.

  • Вставить код, генерирующий какой-нибудь глобально доступный объект, пайп, например, и по нему переслать секретное сообщение. Многие приложения имеют единицы подобных объектов и хакеру не составит труда выяснить какой из них вотермарк (помните: хакер обязательно уже раскусил наш алгоритм и знает где искать!).
  • Вставить код, генерирующий где-нибудь в системе скрытый файл, или запись в реестре, или ещё что-то. Имеет недостатки предыдущего метода плюс легко понять, что добавление подобной логики в приложение чревато всякими нехорошими последствиями.
  • Вставить код, генерирующий в памяти приложения неудаляемый сборщиком мусора массив, который мы потом найдём, просканировав память приложения. Причём в качестве массива могут выступать многие объекты (MemoryWriter, например), ведь при слепом считывании из памяти никто не разберёт что это такое было изначально. Так что это наш метод.

Алгоритм в общих чертах прост: в случайные типы добавляем статические «массивы» (напомню, что это не обязательно массивы) выбираем случайные методы и вставляем в них код, генерирующий вотермарк сообщение в «массиве» (делаем их несколько, чтобы хакеру пришлось попотеть, находя их все). Но нужно решить несколько важных проблем.

  • Код, генерирующий массив, является мёртвым, так как массив после этого не используется и поэтому статически, то есть без запуска программы, может быть обнаружен хакером. Массив надо «оживить».
  • Методы с вотермаркой выявляются статистически по наличию большого числа присваиваний констант в массив. Решение: некоторые куски вотермарка заполнять с помощью циклов.
  • Если вставить код, генерирующий вотермарку, в критический для производительности метод, то скорость работы приложения может существенно упасть. Решение: вставляем вотермарк только в относительно большие методы.
  • Нужно гарантировать, что при слепом чтении памяти мы не примем за вотермарку какой-то случайный блок данных.
  • Как-то надо противодействовать попыткам хакера перетереть вотермарку наложением хакерской вотермарки.
  • Если хакер найдёт одну вотермарку надо сделать так, чтобы он не смог замусорить приложение псевдовотермарками с мусорным текстом.

Проблем много, приступим.

Неподделываемость сообщений


В нашем вотермарк кодере/декодере мы храним массив статичных сигнатур sig[i] (сто штук, например) – случайных массивов по 100 байт. Они не меняются от версии к версии и секретом не являются. Мы полагаемся на хэш алгоритм SHA256 (это как MD5 и SHA1 только лучше), он обеспечит отсутствие коллизий в памяти и решит ещё несколько проблем. Напомню, не известно быстрых алгоритмов для того, чтобы по хэшу HASH найти строку STR такую, что SHA256(STR) = HASH. Вторым столпом, на котором будет держаться вотармарк, является алгоритм Rijndael (AES). Напомню, если KEY – секретный ключ для шифрования/дешифровки, то неизвестно эффективных алгоритмов как не зная KEY из строки AES(STR, KEY) получить STR.

От разработчика защищаемого приложения потребуется пароль, который является секретным и подлежит бережному хранению в сейфе. Когда пользователь вводит пароль PASS и сообщение MSG для создания вотермарки, мы генерируем последовательно:

  • Индивидуальные сигнатуры: indSig[i] = SHA256(sig[i] + PASS + salt), где sig[i] – i-я сигнатура, а salt – немного соли для усиления пароля;
  • Генерируем короткую сигнатуру сообщения: MsgSig = SHA256(PASS + salt)[0..3] (только четыре первых байта хэша).
  • Вычисляем индивидуальный ключ KEY[i] = SHA256(indSig[i] + PASS + salt);
  • Для каждой индивидуальной сигнатуры indSig[i] подготавливаем зашифрованное с помощью алгоритма AES сообщение indMsg[i] = AES(MsgSig + MSG, KEY[i]);
  • Генерируем вотермарки Watermark[i] = indSig[i] + <indMsg[i].Length> + indMsg[i], где <indMsg[i].Length> – четыре байта длины indMsg[i].

Всё! Вотермарки Watermark[i] готовы к употреблению в случайно выбранных местах программы.

Для считывания вотермарок из памяти мы также генерируем indSig[i] по паролю и ищем в памяти вотермарканого процесса indSig[i]. Затем с помощью ключа KEY[i] = SHA256(indSig[i] + PASS + salt) расшифровываем секретное сообщение, следующее за indSig[i]. Не забываем проверить, что первые четыре байта сообщения равны MsgSig = SHA256(PASS + salt)[0..3].

Алгоритм SHA256 со своей способностью противостоять коллизиям гарантирует нам, что случайный блок данных не будет принят за вотермарку. Не зная пароль, хакер не сможет перетереть вотермарку, что опять же гарантируется антиколлизионностью SHA256. Найдя одну вотермарку, не имея пароля хакер не сможет создать мусорное сообщение или даже прочитать вотермарканое сообщение, что гарантируется алгоритмом Rijndael и короткой сигнатурой сообщения. По найденной вотермарке хакер не сможет найти без пароля остальные вотермарки, для чего как раз и генерируются индивидуальные сигнатуры и ключи. В итоге мы решили проблемы 4, 5 и 6, описанные ранее.

«Оживление» массивов


Для того, чтобы вотермарки-массивы перестали быть мёртвыми данными мы вовлекаем их в вычисления, проводимые в случайных методах. В этом случае, даже найдя вотермарк, его нельзя просто удалить, не повлияв на работоспособность приложения. Например пусть где-то в некоем методе было:

return baseOffset + 55;
Пусть вотермарк-массив MyClass.WatermarkArr содержит число 55 в ячейке MyClass.WatermarkArr[42]. Тогда код выше превратится в:

return baseOffset + MyClass.WatermarkArr[42];
Выглядит всё хорошо, но кто может гарантировать, что на момент выполнения кода вотермарк-массив уже создан? Для выяснения этого мы строим контролфлоу граф методов. Такое построение сопряжено со множеством сложностей, ведь методы вызываются через Wpf, Reflection, с помощью виртуальных вызовов, статических конструкторах и так далее. Мы стараемся анализировать как можно больше случаев и перестраховываться везде, где это возможно, применяя для этого собственный эмулятор .NET кода.

Заключение


Watermark является весьма эффективным методом идентификации пользователя и/или конкретной сборки. Разумеется у этого метода, как и у любого другого, есть свои ограничения. В частности Вы не можете заранее собрать один дистрибутив для всех Ваших пользователей (хотя в случае с SaaS обфускатором эта проблема менее актуальна — можно обфусцировать программу на лету, непосредственно перед скачиванием). С другой стороны, для крупных индивидуальных продуктов использование такой защиты более чем оправдано, ведь пользователи, заранее зная, что за ними следят, намного более неохотно станут передавать Вашу интеллектуальную собственность третьим лицам.

Автор публикации: Дмитрий Косолобов, разработчик Appfuscator.
Теги:obfuscationobfuscatorwatermark
Хабы: Информационная безопасность Криптография .NET
Всего голосов 33: ↑26 и ↓7 +19
Просмотры14.3K

Похожие публикации

Факультет разработки игр
16 июня 2021180 000 ₽GeekBrains
Профессия Data Science
16 июня 2021114 000 ₽SkillFactory
Факультет веб-разработки
16 июня 2021270 000 ₽GeekBrains
Профессия Веб-разработчик
16 июня 202166 000 ₽SkillFactory
Полный курс по Data Science
16 июня 2021114 000 ₽SkillFactory

Лучшие публикации за сутки