Компрессия/шифрование кода программы

Самый распространённый метод защиты под Win32. Под Linuxом этот метод имеет некоторые особенности. Самое большое отличие в том, как разрешаются ссылки на внешние модули. 

Статическая линковка

Не имеющий аналогов под Win32 метод. При сборке программы все используемые ею библиотеки просто статически линкуются в один большой толстый модуль. Т.е. для запуска такой программы ничего дополнительно делать не нужно - такая программа самодостаточна. Преимущества: 

  • Большую программу ломать труднее, чем маленькую. Впрочем, это несерьёзно 🙂 
  • Независимость от среды исполнения. В самом деле, довольно часто возникают конфликты версий установленных на машине конечного пользователя библиотек, из-за чего
    ваша программа может просто не работать. В случае статически слинкованной программы такие проблемы устраняются (а точнее, просто не возникают). 
  • Устранение (хотя и не 100%) возможности перехвата и эмуляции библиотечных вызовов (см. также далее по тексту). В такой программе Вы будете несколько более уверены, что при вызове, скажем, strcmp будет вызвана Ваша статически слинкованная функция strcmp, а не неизвестно что. Хотя в Linuxе никогда и ни в чём нельзя быть уверенным до конца 🙂 

Недостатки: 

  • Увеличение размера программ. Впрочем, я уже рассуждал об это выше. 
  • Если вы думаете, что таким образом сможете затруднить жизнь крякеру,
    вы глубоко ошибаетесь. С помощью технологии FLAIR, используемой в IDA Pro, а также применив любой инструмент для создания сигнатур исходных библиотек (использованных Вами при статической линковке), все Ваши библиотечные функции
    могут быть легко опознаны. Более того, обычно не составляет большого труда выяснить, какие именно библиотеки используются, собрать их, и сделать файлы сигнатур. 

Rating: 0.1%.

Также возможно, что кто-нить станет бросать в меня кирпичи, утверждая, что статическая линковка де увеличивает необходимые ресурсы, и что якобы разделяемые библиотеки разделяют сегмент кода между всеми процессами, использующими их. Я тоже так думал, более того, то же самое утверждают большинство виденных мною учебников по Unix, но это не так. Самое простое доказательство - посмотрите атрибуты сегментов памяти, занимаемых разделяемыми библиотеками (файл maps в файловой системе /proc). Вы почти никогда не увидите атрибута s(hared). Почему ? Короткий ответ звучит так - из-за ELF. Дело в том, что при загрузке ELF файла происходит настройка его перемещаемых адресов - relocations. При этом сегменту памяти (даже если это сегмент кода) присваиваются атрибуты Read/Write, и если он при этом разделялся несколькими процессами, происходит копирование памяти при записи. Таким образом, разделение сегментов кода между процессами возможно только между родителем и его потомками (как результат функции fork). За подробностями обращайтесь к исходникам
kernelа. Кстати, эти же аргументы применимы и к утверждению, что якобы "упаковка кода программы приводит к увеличению ресурсов, необходимых для запуска такой программы". Как видите, господа-слюниксоиды, как минимум в управлении виртуальной памятью Linux ничем не лучше поделок от M$

Декомпрессор/декриптор для статически слинкованных файлов ничем не отличаются от своих DOSовых предков, собственно, тут даже и говорить не о чем. Я в настоящий момент занят написанием такого зверя - просто добавляется код распаковки и изменяется точка входа на него. 

Динамическая линковка

Под Win32 все программы являются динамически слинкованными, как минимум, с системными .DLL, реализующими API. Более того, такая линковка осуществляется опять же самой системой с помощью всё тех же функций API. Под Linuxом всё по-другому. Во-первых, программе совершенно необязательно быть динамически слинкованной. Во-вторых, программа сама должна заботиться о загрузке всех необходимых модулей и динамическом разрешении ссылок. Это, правда, вовсе не означает, что
вам каждый раз придётся писать код загрузки модуля в память. Среда исполнения (а не
kernel - как в Win32) предоставляет реализацию динамического загрузчика по умолчанию - в терминах Linux его называют ELF interpretor, или просто interpretor (я не знаю точного перевода и вообще сомневаюсь в его наличии, так что буду просто использовать оригинальный термин). При линковке в программу помещается информация, что для её запуска необходимо после загрузки самой программы также загрузить interpretor, и передать ему управление. На этом загрузка файла на исполнение с точки зрения кернела заканчивается. Но Ваши головные боли только начинаются! Итак, чем плох стандартный ELF interpretor? 

  1. Тем же, чем и весь Linux - его исходники доступны, соответственно, он может быть легко изменён для достижения неизвестных Вам целей 
  2. Он поддерживает уникальный для Unixов механизм предварительной загрузки (я лично не знаю аналогов под Win32). Рассмотрим его подробнее. Итак, предположим, что
    ваша программа импортирует функцию strcmp. Злобный крякер может написать собственную реализацию этой функции, создать объектный модуль и использовать её вместо той, что
    вы ожидали ! Для этого всего лишь нужно определить переменную среды LD_PRELOAD, чтобы она содержала список модулей, подлежащих загрузке ПЕРЕД импортируемыми Вашей программой. Логика работы interpretorа такова, что если некая импортируемая функция уже разрешена, то она больше не разрешается (в Linux импорт происходит по имени, а не по имени и имени библиотеки, как в Win32). Таким образом, можно штатными средствами внедрить в адресное пространство Вашей программы всё что угодно, заменив при этом любую импортируемую функцию. Похоже на ночной кошмар, не так ли ? Вы всё ещё думаете, что Linux написали не крякеры :-)? 
  3. С стандартным interpretorом есть и ещё одна проблема. Дело в том, что его легко можно переписать для универсального инструмента, отслеживающего все вызовы импортируемых функций. У меня была идея встроить script engine в ELF interpretor, так что больше не нужно будет переписывать его под каждую конкретную программу, а всего лишь заменить script, который и сделает всё что нужно. Ведь ELF interpretor работает в адресном пространстве Вашей программы, и он отвечает за начальную загрузку импортируемых функций, т.е. фактически такой script будет иметь над Вашей программой полный контроль (под Win32 мне вообще неизвестны подобные инструменты. BoundsChecker конечно может отслеживать все вызовы импортируемых функций, но пока никому не пришла мысль дописать к нему script engine и использовать, например, для memory patching). Пока Вы можете вздохнуть свободно - из-за недостатка времени эта идея уже третий месяц лежит в долгом ящике. Но ведь если она пришла в мой извращённый мозг - она может посетить и кого-нть менее занятого 🙂 

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

  • Собственный загрузчик в кернеле.
    Можно переписать стандартный загрузчик ELF файлов (файл binfmt_elf.c из директории fs исходников ядра Linux), чтобы сделать жизнь крякера несколько тяжелее. Например, сбрасывать флаг трассировки процесса, иметь собственный формат исполнимых файлов (естественно вместе с перекодировщиком обычных ELFов в этот формат), декриптовать/декомпрессировать куски файла перед загрузкой их в память - у меня довольно богатая фантазия.

Недостатки:

  • Как ни странно, самым большим недостатком является необходимость иметь собственный код в kernelе. Поскольку у конечного пользователя время от времени возникает необходимость в пересборке кернела,
    вы должны будете предоставить либо объектный модуль (для насмерть прибитого гвоздями в кернел кода) или опять объектный модуль (для LKM - Linux Kernal Module). И то и другое легко можно дизассемблировать/пропатчить и история повторяется... 
  • Как насчёт смены версии кернела ? Например, грядёт смена ядра 2.4 на 2.5 - нужно будет иметь (и поддерживать!) как минимум код для обоих версий ядра... 
  • А если Вы допустите ошибку ? Если для userlevel программ Ваши ошибки скорее всего не смертельны, то ошибки в коде кернела могут иметь весьма плачевные последствия. Кроме того, отладка такого кода является сущим кошмаром, поверьте мне... 
  • Субъективная причина - сложность установки. Если Вам скажут, что для использования, скажем, Photoshopом
    вам придётся загрузить модуль ядра, а при сборке ядра
    вам необходимо сделать такие-то действия - я просто хочу знать Вашу реакцию :-)... 

При всех недостатках это вполне приемлемый вариант для серверов, работающих в режиме 7x24x365, где кернел модернизируется раз в год (при обязательном условии, что Ваш встраиваемый в кернел код глючит относительно редко). Так что для серверных приложений (каких-нть баз данных, например), real-time приложений, и приложений, всё равно требующих изменений в кернеле (например драйверов какого-нть заказного железа, см. ниже), этот вариант вполне может использоваться...

Rating: 1-20%, в зависимости от реализации. 

Собственный ELF interpretor

Только подумайте о том, чего можно добиться,
используя собственную версию ELF interpretorа! 

  • Скажите "прощай" предварительной загрузке, поддержке отладчика (кстати, разве я не говорил, что для нормальной работы всё того же GDB стандартный ELF interpretor должен иметь кое-какие функции ?) и тому подобным прелестям... 
  • А ведь почти всё, что было описано выше для загрузчика в ядре применимо также и для ELF interpretorа. Он может перед передачей управления основной программе распаковать/декриптовать её части, проверить наличие/отсутствие лицензии, проверить наличие отладчика в памяти и много ещё чего. Причём, в отличие от LKM, он может использовать стандартную C runtime library, его написание (и самое главное отладка) намного легче, и ошибка в нём (в общем-то все программы содержат ошибки. Любое тестирование может выявить лишь их наличие, но никак не их отсутствие) не приведёт к необходимости перезагрузки системы и порче системных файлов (ну, если Вы не работаете как root :-). 
  • Он может снять ещё одну Вашу головную боль - проверять подлинность разделяемых библиотек. Допустим, что Вы подписываете все используемые Вами библиотеки (скажем, посредством MD5 или SHA hash). Теперь при загрузке такой библиотеки можно повторно вычислить цифровую подпись - и если фактическая не совпадает с имеющейся - ба, да нас ломают! Т.е. с некоторой вероятностью Вы будете уверены после этой процедуры, что вызов несчастной функции strcmp будет вызывать Вашу реализацию strcmp из подписанной Вами библиотеки... 

Мой извращённый мозг может генерировать довольно много применений собственной версии ELF interpretorа, но этот способ также имеет недостатки: 

  • ELF interpretor - всего лишь программа. Поэтому она также может быть дизассемблирована/пропатчена... 
  • Как насчёт memory dump/memory patch? Конечно, можно сделать так, чтобы перед передачей управления главной программе ELF interpretor затирал в памяти все данные, использованные для её загрузки (символьную и строковую таблицы, информацию о relocations и т.д.), но тем не менее всё равно
    ваша программа в памяти находится в боевой готовности к выполнению, полностью распакованной, расшифрованной, с широко раскинутыми ногами, в общем 🙂
  • Вполне реально написать memory dumper, который будет формировать на диске статически "слинкованный" исполнимый файл прямо из образа Вашей программы в памяти. Более того, этот файл будет работоспособен (ведь в памяти уже однажды всё было готово к исполнению, все библиотеки загружены, relocations выполнены...) - просто размерчик у него будет больше среднего :-). Нда, надо будет как-нть заняться этим...

Также весьма многообещающим выглядит модификация куска кода в кернеле, отвечающая за формирование core dumps. 

Rating: 1-20%, сильно зависит от реализации. 


Применение виртуальной среды исполнения

Да, что-то вроде старой недоброй Java - свой ассемблер, свой процессор, свой язык. Необязательно писать всю программу на таком чудовище - достаточно написать на нём макросы, и добавить к ним парочку, отвечающую за защиту (если я ещё не выжил из ума окончательно, есть такой неплохой редактор CRiSP, хорошо попивший у меня крови в своё время - в нём использовалась именно такая схема защиты. Ему правда всё равно не помогло :-). Если с достоинствами всё ясно, то недостатки несколько менее очевидны: 

  • Нельзя использовать готовые script engines, чьи исходники доступны. Мне однажды пришлось написать дизассемблер с байткодов Pythonа (для поискового сервера UltraSeek ) - это оказалось на удивление простой задачей - ведь у меня были исходные коды! 
  • Поэтому Вам нужно написать собственную среду исполнения. Это довольно нетривиальная и дорогостоящая задача... 

Применение аппаратных средств защиты

Я лично считаю, что если некая привинченная к порту железка (dongle) не несёт в себе часть функциональности самой программы, то она просто бесполезна, и программа, "защищённая" таким образом, легко может, и более того, просто обязана быть взломана. Такая затычка занимает целый порт, коих и так обычно недостаточно (я видел однажды гирлянду из пяти dongles, нанизанных друг на друга. Как говорил ослик Иа, "байтораздирающее зрелище" :-). Обратитесь к парням из UCL, они намного лучше меня разбираются в данном вопросе.

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

  • Аппаратно реализованные модули криптографии, например, для зашифрованной файловой системы. Где-то я уже видел нечто аналогичное, если я не ошибаюсь, был использован
    IBM 4758 PCI Cryptographic Coprocessor
  • Прога для сопряжения с уникальным (в смысле заказным, не массовым) оборудованием, всяческими DSP, измерительными приборами etc. Кстати, я нашёл неплохую ссылку на Linux-драйверы для подобных устройств:
    http://www.llp.fu-berlin.de/pool/software/. Создание драйвера под Linux не намного отличается от создания драйвера под любую другую операционную систему. 

Т.е. без такой железки Ваша программа просто не имеет смысла. Можно вообще не заботиться об её защите (возможно даже её распространение под GPL) - если такие железки производите только Вы, и больше никто. Впрочем, всегда помните о такой страшной штуке, как послойное копирование (особенно этим пробавлялись Япония и не так давно СССР)... 

Клиент-сервер

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

И в заключение хочу ещё раз напомнить основное правило разработчика ВОПРОС НЕ В ЗАЩИТЕ, А
В ЕЁ НЕОБХОДИМОСТИ. Если вы разработали действительно ценную вещь – её сломают в любом случае, даже если вы продадите всё своё имущество и купите СверхМегаГиперНовую Защиту. Если же ваша программа не имеет ценности – то и защита ей вряд ли нужна. Вот такое вот неприятное окончание…

Happy cracking !

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии