div.main {margin-left: 20pt; margin-right: 20pt}
Ломаем Java программу
Несмотря на столь громкое название в данном тексте речь не пойдет о
том, как ломать программы или аплеты на Java, речь пойдет как раз о другом, как
защитить такие уязвимые объекты от посягательств "нехороших" людей. Данная
статья основана на результатах проводимого конкурса в рамках проекта JavaPower. Те из читателей, кто не желает узнать результаты
конкурса или же не желает знать, как можно защитить программу от ее взлома могут
смело пролистать данный документ до конца и проголосовать, не забыв при этом
нажать пару раз на баннеры.
Итак, в начале о самом конкурсе и о том, о чем собственно будет
идти речь далее. Специально для конкурса была создана простая программа. В ее
задачу входила всего лишь одна функция - получение и вывод на экран файла с FTP
сервера. Однако программа может читать файлы только с того сервера, который
указан в ее специальном ключевом файле. Другими словами, в файле ключа было
зашифровано доменное имя, с которого и только с которого программа может
получать файлы.
Задача участников конкурса состояла в том, что бы заставить
программу читать файлы с других FTP серверов, с других доменных имен. С чем
собственно и справилось несколько человек.
Почему безопасности программ на Java уделяется столько внимания?
Именно безопасности, поскольку "взломанная" программа может нанести не только
финансовый вред автору, но и при нарушениях ее работы обычным пользователям
(хотя, к слову сказать, это может проделать и не взломанная программа). Для
непосвященных читателей следует сделать небольшое разъяснение - изначально Java
программы были интерпретируемыми и выполнялись в среде специальной виртуальной
машины Java шаг за шагом. Однако исполнялся не исходный текст, а байткод.
Байткод это инструкции виртуальной машины, скомпилированные из исходного кода,
компилятором Java. Хочется заметить, что компилировать в байткод в настоящее
время может не только компилятор Java с языка Java, а еще и множество других
компиляторов с языков отличных от Java. До поры до времени все шло хорошо,
начали появляться популярные программы на Java и аплеты, некоторые из них стоили
денег. Авторы придумывали множество систем позволяющих ограничить
несанкционированное использование их творений. Но.... Как оказалось, получить
исходный код программы по ее байткоду не так уж и сложно. Причем полученный
исходный код не намного отличается от первоначального. Авторы схватились за
голову - "Что же делать!". Ведь любой школьник может декомпилировать их
программу в понятный исходный код, подправить его и получить бесплатную
полностью рабочую копию программы! И тут появились специальные программы -
обфускаторы или по-русски запутыватели. В их задачу входит затруднение
правильной декомпиляции исходного кода или полную его невозможность
восстановления. Тут уже все зависит от декомпилятора.
И что же наша конкурсная программа? Она была обработана наиболее
продвинутым обфускатором Zelix
KlassMaster version 2.6, доступном автору программы. Что собственно и
отразилось на результатах. Ни один из участников не выполнил вышеописанную
процедуру, а именно не декомпилировал программу, не поправил полученный исходный
текст и не скомпилировал обратно. И дело тут вовсе не в том, что нет в природе
декомпиляторов, которые не могли бы декомпилировать предложенные классы. Нет,
если воспользоваться опять же наиболее продвинутыми декомпиляторами, то можно
все-таки получить исходный текст, но толку от него будет мало, поскольку
выглядеть он будет примерно следующим образом:
if(flag4) goto _L2; else goto _L1 _L1: JVM INSTR ifne
214; goto _L3 _L4 _L3: JVM INSTR ifle 181; goto _L5 _L6 _L5: s
= s.substring(0, s.indexOf(zkmToString(zkmToString0(zkmToString1())))) +
s.substring(s.indexOf(zkmToString(zkmToString0(zkmToString1( )))) + 6,
s.length()); flag3; if(flag4) goto _L8; else goto _L7 _L7: JVM INSTR
ifeq 239; goto _L6 _L9
Т.е. декомпилировалось-то оно декомпилировалось, но вот
скомпилировать это обратно или даже хотя бы понять что тут к чему весьма
затруднительно. Вообще между декомпиляторами и обфускаторами идет постоянный
незримый бой, как между мечем и щитом. Кто кого?
Итак, первый совет - всегда обфусцируйте (запутывайте) свою
программу при помощи специальных программных средств.
Как известно Java это очень открытая среда. Широкой общественности
доступна полная информация о всех внутренностях виртуальной машины, в добавок
она имеет еще и широкие возможности по отладке запускаемого в ней кода. Такая
открытость означает в первую очередь безопасность, а конкретно безопасность
аплетов. Но с другой стороны она может нанести и вред.
Первый участник, приславший правильное решение, применил
традиционный метод для взлома программ. Битовый (байтовый) хак. Сущность этого
метода заключается в том, что бы изменить уже откомпилированный код программы
так, что бы программа даже при неверных условиях все равно выполняла те
действия, которые она выполняет при верных условиях. Или же не выполняла
какие-то деструктивные или нежелательные действия (как-то, вывод рекламного
окна, завершение работы и т.д.). Как обычно поступают авторы программ,
пытающиеся ограничить какой-то функционал своей программы при определенных
условиях? Создают проверку этих условий в своей программе. И если условия
удовлетворяют некоторому значению, то программа продолжает свою нормальную
работу.
Организовать проверку условия можно несколькими способами. Можно
сделать отдельную функцию, которая будет возвращать результат проверки. Не
делайте проверку на правильность условий в отдельной функции, методе или
процедуре. Почему не стоит этого делать? Если у Вас всего лишь одна
функция-процедура-метод проверки на всю программу и она возвращает одно
значение, то взломщику не составит большого труда изменить указанную процедуру
так, что бы она при любых условиях всегда возвращала значение, которое
соответствовало бы верным условиям проверки. По-другому организовать проверку
условий можно следующим образом - можно делать это в самом теле программы, т.е.
без использования каких-либо функций или процедур. Попытайтесь сделать
несколько проверок в теле программы. Для чего нужно делать несколько
проверок? Если у Вас будет сделана проверка условий всего лишь в одном месте
программы, то сломать ее будет так же легко, как и вариант выполненный в виде
функции. Избегайте ветвления программы. Что под этим подразумевается? Как
обычно организуется логика программы? При помощи логических операторов if
что-то = чему-то then делаем то-то else делаем что-то другое. В таком
случае, нужно изменить всего лишь адрес перехода при неверном варианте на адрес
перехода верного варианта. Однако сделать логику программы без логических
операторов нельзя (в случае Java). Ну или почти нельзя. Во всяком случае,
применяемые приемы будут достаточно нетривиальны и сломать их самих по себе
будет достаточно трудно.
Итак, в конкурсной программе не использовалась функция проверки
правильности предложенных значений, а использовалось как минимум две
проверки в самом теле программы. Но первый автор прислал CRK для конкурсной
программы состоящий всего лишь из одной строчки. Что же произошло? Почему в
программе использовалось несколько проверок, а оказалось достаточным изменить
всего лишь одно место в программе (байт коде), для того, что бы программа
работала как полнофункциональная? На самом деле все произошло достаточно просто
- в теле программы действительно было несколько проверок, но они были полностью
идентичны. Далее либо компилятор Java произвел оптимизацию полученного байткода,
либо обфускатор сделал практически то же самое. Другими словами, то, что в
исходном коде выглядело как несколько различных групп операторов, в байткоде
стало одной группой операторов. Попытайтесь использовать несколько различных
последовательностей проверки значений или различные алгоритмы. Это, конечно,
не обезопасит Вашу программу полностью от взлома, но по крайней мере затруднит
ее взлом хакеру.
Итак, первый участник сперва применил декомпилятор JAD, что бы понять сам
алгоритм работы программы, но поскольку программа была обфусцирована, то эта
процедура была затруднена. Далее участник при помощи отладчика байткода JDBG нашел тот самый оптимизированный переход и изменил его
значение на нормальное при помощи CafeBabe:
action(): 700: ifeq -> 753 //host not allowed 703: ... //
normal execution
Результат очевиден - программа сломана. Т.е. будет читать файлы с любого FTP
сервера.
Второй участник прислал сразу же два решения. Для начала
остановимся на первом из них, поскольку он получился весьма оригинальным. Как
уже было описано выше - Java очень открытая система. О скомпилированной
программе можно узнать все, ну или почти все. Второй участник так и поступил,
при помощи самодельной небольшой программы он через Java Reflection API узнал все о методах и переменных
конкурсной программы. И что же он там увидел? А то, что у программы есть только
два своих собственных метода action и read_ftp. Судя по названиям (обфускатор не
доработал), первый из них отвечает за собственно проверку значений и вызов
чтения файла (второй метод). Но самое главное, что оба этих метода доступны для
наследования. Не забывайте о модификаторах доступа. В результате
достаточно было только создать наследника от класса и перекрыть в нем метод
action с принудительным в нем вызовом метода чтения файла, так программа
оказалась сломанной. Метод action для программы наследника выглядит следующим
образом:
void
action() { super.action(); read_ftp(); }
Для работы программы естественно, нужно запускать наследника,
который будет уже вызывать саму программу конкурса. Хотя программа и ругается,
она все же читает файл. Если бы конкурсная программа имела бы модификатор final,
то это решение не прошло бы. Следует так же заметить, что, по всей видимости,
можно воспользоваться битовым (байтовым) хаком, что бы изменить модификатор
доступа на требуемый.
По результатам первых двух взломов программы, можно лишь
посоветовать - используйте оригинальные алгоритмы для вычисления контрольных
сумм программ. Это хоть как-то позволит защитить программу от модификации ее
методом битового (байтового) хака.
Второй вариант взлома программы от второго участника и вариант от
третьего это уже настоящие ключеделалки. В конкурсной программе тем условием,
что программа может читать файл с указанного FTP служил специальный ключевой
файл, в котором лежал зашифрованный ключ. Вернее даже и не ключ, а его
хеш-функция, полученная при помощи пакета MD5. В файле, однако, хранилась не
просто хеш в чистом виде, он была так же обработан нехитрым алгоритмом, т.е.
растворен в наборе других случайных чисел. Далее при работе программы
проверялись хеш-функции полученные из ключевого файла и полученные для
запрашиваемого сервера. Оба участника основывались на анализе полученного
декомпилированного кода:
_L36: String s1; md5 md5_1 = new md5(url); md5_1.calc(); s1
= String.valueOf(String.valueOf(md5_1)); flag3; if(flag4) goto _L38; else
goto _L37 _L37:
Но был использован не только анализ исходного кода. Анализировался
так же и сам ключевой файл. Анализ участников был основан на том, что
сравниваемая хеш-функция из файла будет сравниваться с хеш функцией вычисляемой
динамически на основе адреса сервера, с которого предполагается получить файл. В
результате оба участника создали новую, не зашифрованную, как в ключевом файле,
хеш-функцию для того же самого адреса, для которого уже имелся ключ и сравнили
их. Так был вскрыт даже не алгоритм, а формат шифрования хеша в ключевом файле.
Ясно, что здесь не совсем удачно шифровался (или растворялся) хеш в ключевом
файле, поэтому если уж Вы прячете ключевые данные в каком-то массиве данных,
то делайте это наиболее тщательно, не кладите их открытым кодом, создавайте
обманки, которые будут похожи на Ваши данные, но на самом деле таковыми не будут
являться.
Больше на момент написания этого документа вариантов решения
поставленной задачи по взлому программы не поступало, однако хочется дать еще
несколько советов по защите программы от взлома.
Используйте шифрование методов или данных. Если бы в
программе использовались некоторые данные, которые нужны были бы для нормального
чтения файлов, но они были бы зашифрованы, то конкурсантам пришлось бы
повозиться намного дольше с программой.
Используйте свои загрузчики классов. Использование своих
собственных загрузчиков классов, так же затруднит взлом Вашей программы. А если
загружаемые классы будут зашифрованы, а расшифровываться будут уже динамически в
памяти, то программа будет достаточно устойчивой к взлому.
Основное правило защиты программ - затраты на взлом программы
должны быть больше, чем предполагаемая ее легальная стоимость. Это означает,
что Вашу программу не будут ломать, если это будет стоить дороже, чем
просто-напросто купить на нее лицензию. Хотя к популярным продуктам это не
относится.
Администрация проекта JavaPower благодарит всех участников конкурса за
предоставленные материалы.
|