Как взломать программу java
В природе не существует природе не существует программных продуктов, которые не поддавались бы взлому. И тут совершенно не важно на каком языке программирования написанна эта программа, будь то Java или C . Просто затраты на взлом могут быть различными и поэтому все основные действия по защите от нелегального использования должны быть направлены на затруднение декомпиляции.
В случае с Java дела обстоят достаточно плохо. Дело в том, что в отличии от C или C ++, Java компилятор не создает конечный машинный код, а всего лишь его платформенно независимое представление. Полученный байт-код содержит очень много осмысленной информации, которая может помочь разобраться взломщику программы в принципе её работы.
При создании программы, в подовляющем большенстве, вводятся сомодостаточные названия для классов и их методов. Например, класс ConfigMgr скорее всего бдует представлять сервис для управления конфигруцией программного продукта. А его метод getRootCataloog – будет возвращать корневой коталог, где физически расположена программа. Это, конечно, существенно облегчает понимание программы для программиста, но и одновременно помогает злоумышленнику, ведь при декомпиляции Java байт-кода получаются реальные названия классов и их методов.
1) Использование флагов компиляции
2) Написание двух версий программного продукта
3) “Затемнение” кода
4) Изменение байт-кода
5) Использование JNI
6) Выставление исходников программы по более высокой цене
7) Хранение методов в атрибутах
8) Применение глухих классов
1. Использование флагов декомпиляции
По существу нас интересуют только 3 флага компиляции, это – g , – O и без флага.
Флаг – g говорит компилятору добавлять номер строки и имя локальных переменных в конечный байт код.
Без флага – теряются имена локальных переменных, но сохраняютья номера строк.
Флаг – O – дополнительно удаляются номера строк.
Коментарии
Данный метод был приведен для общности картины и может являться лишь первым шагом на пути защиты java кода от взлома. Единственный вывод отсюда можно сделать, что всегда необходимо компилировать программу тольк с ключом компиляции – O .
2. Написание двух версий программного продукта
Эта достаточно хороший способ защиты. Например если необходимо выпустить демо версию, то берётся полная верися, из неё вырезается например сохранение в файл и затем раздаётся всем бесплатно. Таким образом нет особой нужды защищать программу. Если же клиент покупает программный продукт, то ему выдаётся полная версия программы.
Но этот подход не полностью может удовлетворть потребности в защите, например, если необходимо, чтобы даже полная версия работала определённый период времени (так называемая “временная” лицензия).
Коментарии
Данный метод имеет несравненный плюс при предоставлении кому-либо демонстрации, фактически вручается полурабочий образец, который сложно будет применять в каких либо других целях, кроме как ознакомление с программным продуктом. Да и в добавок в нем будет отсутствовать достаточно важная часть кода, которую невозможно будет получить даже при помощи декомпиляции.
3. “Затемнение” кода
Этот способ является на данный момент самым популярным среди методов защиты программ от декомпиляции.
На практике, если проект переходит определённый предел сложности, то разобраться в логике программы можно только с использованием коментариев в коде и технической подержки. На этом и оновывается данный метод. Во всем проекте происходит замена декларативных названий полей классов и методов на абстрактные, которые не несут какой-либо смысловой нагрузки.
Например был исходный текст :
private void loadPixie( URL url ) throws Exception
URLConnection connection = url.openConnection( );
in = new DataInputStream( connection.getInputStream() );
// Verify file format via magic numbers and version number.
if (in.readInt() != Constants.MAGIC)
throw new Exception( "Bad Pixie header" );
int v = in.readShort();
if (v != FILE_VERSION)
throw new Exception( "Bad Pixie version " + v );
// Skip unused fields.
readUnsignedVInt( ); // Frame table size always 1.
readUnsignedVInt( ); // Skip end of control commands.
// Object table size always 0.
// Skip unused fields.
// Block-read the rest.
int byteCount = readUnsignedVInt();
pixieBody = new byte[ byteCount ];
int maxBlockSize = byteCount/20+1;
Graphics fg = getGraphics( );
int bytesDone = 0;
while (bytesDone < byteCount)
int blockSize = byteCount-bytesDone;
if (blockSize > maxBlockSize)
in.readFully( pixieBody, bytesDone, blockSize );
// Update progress monitor.
progress = bytesDone / (float) byteCount;
fg.fillRect( 0, size().height-4,
( int )( progress * size().width), 4 );
после “затемнения” он принял вид:
private void _mth015E(void 867 % static 931)
void short + = 867 % static 931.openConnection();
private01200126013D = new DataInputStream( short +.getInputStream());
if( private01200126013D.readInt() != 0x5daa749)
throw new Exception("Bad Pixie header");
void do const throws = private01200126013D.readShort();
if( do const throws != 300)
throw new Exception("Bad Pixie version " + do const throws);
_fld015E = _ mth012B( );
short01200129 = _ mth012B( );
_fld013D013D0120import = new byte[ |=];
void void = |= / 20 + 1;
for( void catch 11 final = 0; catch 11 final < |=;)
void while if = |= - catch 11 final;
if( while if > void)
private01200126013D.readFully( _fld013D013D0120import, catch 11 final, while if);
catch 11 final += while if;
const = (float)catch 11 final / (float)|=;
=. fillRect( 0, size().height - 4, (int)(const * size().width), 4);
Как видно из приведённого примера, код стал плохо читаем и поэтому, чтобы разобраться в логике программы необходимо приложить на порядок больше усилий.
java / tools / crema / ). Crema рапространяется бесплатно (с незначительными ограничениями), а ее аналог интегрирован в Jbuilder начиная с версии 2.0.
Коментарии
Данный метод очень сложно реализуем, если в программном продукте необходимо предоставлять открытый API или если в проект взодят EJB (которые тоже должны иметь строго определённые методы и открытый API ). Компромисом может служить не тотальное переименование классов и методов, а только частичное, но для этого необходимо изначально проектировать систему с учётом дальнейшего её “затемнения”.
4. Изменение байт-кода
Основным недостатком предыдущего метода является то, что “затемнённый” код все равно может быть успешно компилирован, после декомпиляции. Против простеньких декомпиляторов можно побороться следующим способом. Всавлять лишние инструкции в байт-код. Например если после return в методе класса всавить java инструкцию pop , то многие декомпиляторы воспримут это как ошибку и не смогут коректно декомпилировать этот код. Хотя при этом JVM его сможет исполнять без ошибок.
Пример исходного файла:
void parse_fig_pointline( DataInputStream f,
int npoints, int xpoints[], int ypoints[] )
if (debug) editor.consoleMessage(
"parse_fig_pointline: npoints= " + npoints ) ;
String line = null;
int n_tokens = 0; /* number of tokens in current line */
int i =0; /*current index into point arrays */
while( i < npoints )
editor.consoleMessage( "parse_fig_pointline: " + line );
st = new StringTokenizer( line, " \n\r\t" );
for( int j=0; j < n_tokens; j+=2)
xpoints[ i] = fig_scale( Integer.parseInt(
ypoints[ i] = fig_scale( Integer.parseInt(
> catch( IOException e )
editor.consoleMessage( "Error: Not a valid FIG3.1 file
editor.consoleMessage( "on line " + line_number + ": " +line );
После применения DashO и декомпиляции JAD ’ом получился следующий код:
void a(DataInputStream datainputstream, int i1, int ai[], int ai1[])
d.a( "parse_fig_pointline: npoints= " + i1);
boolean flag = false;
stringtokenizer = new StringTokenizer(s1, " \n\r\t");
int i2 = Integer.parseInt(stringtokenizer.nextToken());
h < 30 ? (i2 +1)*g :i2 *g;
i2 = Integer.parseInt( stringtokenizer.nextToken());
h < 30 ? (i2 +1)*g :i2 *g;
if( l1 < j1) goto _L4; else goto _L3
if( k1 < i1) goto _L6; else goto _L5
d.a( "Error: Not a valid FIG3.1 file (pointline)");
Подобный код уже не откомпилируется.
Коментарии
Этот метод не универсален и это собственно его единственный недостаток, а в остальном он может бэффективно применяться наряду с другими методами защиты программных продуктов.
5. Использование JNI
Этот может принести ощутимый эффект, например в следующем случае. У нас есть определённая программа. Часть её функционального кода без которого она не будет работать переводится на C . А затем использую JNI API эта часть кода связывается с остальным java байт-кодом.
Ломаем Java программу
Несмотря на столь громкое название в данном тексте речь не пойдет о том, как ломать программы или аплеты на Java, речь пойдет как раз о другом, как защитить такие уязвимые объекты от посягательств "нехороших" людей. Данная статья основана на результатах проводимого конкурса в рамках проекта JavaPower. Те из читателей, кто не желает узнать результаты конкурса или же не желает знать, как можно защитить программу от ее взлома могут смело пролистать данный документ до конца и проголосовать, не забыв при этом нажать пару раз на баннеры.
Итак, в начале о самом конкурсе и о том, о чем собственно будет идти речь далее. Специально для конкурса была создана простая программа. В ее задачу входила всего лишь одна функция - получение и вывод на экран файла с FTP сервера. Однако программа может читать файлы только с того сервера, который указан в ее специальном ключевом файле. Другими словами, в файле ключа было зашифровано доменное имя, с которого и только с которого программа может получать файлы.
Задача участников конкурса состояла в том, что бы заставить программу читать файлы с других FTP серверов, с других доменных имен. С чем собственно и справилось несколько человек.
И что же наша конкурсная программа? Она была обработана наиболее продвинутым обфускатором 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 для программы наследника выглядит следующим образом:
Для работы программы естественно, нужно запускать наследника, который будет уже вызывать саму программу конкурса. Хотя программа и ругается, она все же читает файл. Если бы конкурсная программа имела бы модификатор 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 благодарит всех участников конкурса за предоставленные материалы.
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
В заметке «В обход стражи. Отлаживаем код на PHP, упакованный SourceGuardian» мы рассматривали программу, реализованную в виде локального веб‑интерфейса. Работает она так: под Windows запускается локальный сервер Apache c набором PHP-модулей, а пользователь взаимодействует с приложением через браузер, в котором набирает адрес localhost . Программа, взломом которой мы займемся сегодня, действует похожим образом, только написана она на Java и поставляется в виде файла . JAR . Наша задача — отучить приложение от деморежима.
По счастью, нам известно, где лежат стартующие в виде сервиса исполняемые модули программы в формате . EXE и соответствующий JAR-файл. По своей сути JAR — это обычный ZIP-архив, в который упакованы части проекта. Поскольку мы собираемся править код, нас интересуют модули *. CLASS , содержащие откомпилированный JVM-байт‑код. Декомпиляторов и способов их применения множество, существуют даже инструменты вроде JD-GUI, способные полностью восстановить проект из исполняемого файла. Чаще всего взломщики используют общеизвестный JAD, который из‑за его распространенности ловкие обфускаторы давно научились обманывать, что, в свою очередь, стало причиной появления более продвинутых декомпиляторов вроде CFR. Эта война щитов и мечей, пуль и бронежилетов обещает быть долгой, нам остается только запастись попкорном. Но не будем тут останавливаться, а вместо этого предположим, что мы декомпилировали проект одним из описанных способов до Java-исходников и даже проанализировали полученный код.
Применительно к нашему подопытному приложению это выглядело примерно так. Декомпилировав все‑все‑все CLASS-файлы, мы так и не обнаружили ничего похожего на обращение к лицензии, однако в подкаталоге BOOT-INF/ lib нашего JAR-архива нашлось множество упакованных JAR-библиотек, среди которых сразу бросилась в глаза библиотека license-1. 2. 12. jar . Распаковав и декомпилировав ее, мы наткнулись на два CLASS-модуля, содержащих две любопытные функции. Одна возвращает демонстрационный режим, вторая активирует опцию 1 по умолчанию:
Iterator iter = this . modulesItems . entrySet () . iterator () ; log . info ( "Default module loaded <> " , ( Object ) mod . getName ()) ;Наша задача — сделать так, чтобы функция isDemo всегда возвращала false , а в функции setDefault нужно заменить опцию 1 опцией 256 . Вот здесь и начинается самое интересное, то, ради чего и написана эта статья.
Ты спросишь: раз у нас имеются в наличии все исходники и код, то почему бы просто не перекомпилировать весь проект, поменяв эти две процедуры на нужные? К сожалению, прямой метод не всегда самый простой. В нашем случае в интересующих нас модулях много зависимостей, а проект очень большой, многие модули сильно обфусцированы. Кроме того, код восстановился частично с кучей ошибок, из‑за чего проект полностью не соберется. Можно, конечно, покопать обфускацию и попробовать руками вытащить исходный текст программы, но решать эту (возможно, даже, гораздо более сложную) задачу ради двух простых патчей в коде как‑то лень. Вдобавок, пересборке проекта может помешать отсутствие установленного JDK на компьютере. Устанавливать его и разбираться в особенностях компиляции Java-проектов мне тоже неохота. Поэтому мы, как обычно, ищем самый простой путь — патч откомпилированного JVM-кода.
В этом нам поможет интересная, но малоизвестная утилита dirtyJOE. Открываем в ней наш CLASS-модуль, на вкладке Methods видим полный список методов класса. Находим в нем искомую isDemo и тыкаем в нее, открывая окно редактирования.
Окно редактирования dirtyJOE
Это, конечно, не исходник на Java, но здесь хотя бы можно редактировать байт‑код, сверяясь с логикой исходника. Возможности программы минималистичны: редактировать можно только в виде hex-значений кодов инструкций. По счастью, мнемоника и описание текущей исправленной инструкции отображается в окошке над окном кода, а сам список инструкций с описанием каждой имеется в хелпе (причем только список, без опкодов: явно, чтобы хакерам жизнь медом не казалась и пришлось искать шестнадцатеричные опкоды инструкций самостоятельно). По сути, нам надо закоротить данную функцию, сделав возвращаемым значением 0 ( false) . Находим в таблице инструкцию помещения 0 на стек ( iconst_0 ), ее опкод ( 3 ) и ставим ее в самое начало метода, а после нее — сразу возврат ( ireturn ).
Исправляем инструкцию
Закрываем окно редактирования, сохраняем CLASS-модуль, затем меняем исправленный модуль в архиве license-1. 2. 12. jar , который, в свою очередь, копируем на место старого в основном JAR-модуле. С предвкушением перезапускаем программу и обнаруживаем, что она не работает. Мы что‑то сделали не так.
Продолжение доступно только участникам
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Надоело платить за мобильные игры? Заколебала череда бесконечных смертей? Хочется переделать спрайты/тексты/заставку под свой вкус? Нет ничего проще. Мобильные игры весьма компактны и потому просты для взлома и анализа. Надругаться над ними сможет даже начинающий хакер, а эта статья поможет ему сделать первые шаги, после которых он будет оттачивать хакерское мастерство уже самостоятельно.
Введение
Популярность мобильных игр стремительно растет. Они прочно оккупировали рынок сотовых телефонов, коммуникаторов, смартфонов, карманных компьютеров и других аналогичных устройств. Большинство игр распространяется на условно-бесплатной основе, то есть требуют деньги, иначе блокируют часть возможностей и/или ограничивают количество запусков. Но даже полностью бесплатные игры не лишены недостатков. Неудобное управление, быстро кончающиеся жизни. Да мало ли существует причин, побуждающих хакера дорабатывать код в соответствии со своими предпочтениями? Этические проблемы взлома нас не волнуют, поэтому мы немедленно переходим к технической части, благо хвост уже зудит, чешется и рвется в бой. О взломе мобильных игр написано много, но все как-то неконкретно и не в тему. Не так-то просто обобщить свой опыт и передать его другим. Но мыщъх все же попробовал.
Мобильные платформы
Основная масса мобильных игр (по некоторым оценкам аж до
70%) пишется на Java, а точнее - J2ME, что расшифровывается как Java 2 Micro Edition. Это урезанная версия языка Java, ориентированная на маломощные системы и поддерживающая огромное множество мобильных устройств. Вместо "живого" машинного кода, сотовому телефону подсовывают так называемый "байт-код", исполняющийся на виртуальной Java-машине (Java Virtual Machine или сокращенно JVM). Теоретически, игра, написанная для одного сотового телефона, будет работать на любом другом, независимо от особенностей его аппаратного обеспечения, что очень хорошо (хотя на практике переносимость намного хуже). Расплачиваться за это приходится драматическим падением производительности в условиях и без того маломощных микропроцессоров.
Продвинутые игры (наподобие Fight Hard 3D и RiderX 3D) пишутся на чистом машинном коде и потому могут исполняться только на микропроцессорах одного семейства (например, ARM 6), что ограничивает сферу их применения. В настоящей статье они не рассматриваются. Поскольку нельзя объять необъятное, мы сосредоточимся исключительно на взломе Java-приложений, а до Fight Hard'а доберемся не раньше, чем мыщъх купит соответствующий сотовый телефон.
- Прежде всего нам потребуется спецификация на байт-код виртуальной Java-машины, выложенная на официальном сайте корпорации Sun (на английском языке), при этом знать сам язык Java совершенно необязательно, хоть и желательно. Во всяком случае, мыщъх несколько лет успешно хачил Java-приложения непосредственно в JVM, пока, наконец, не купил "Горький вкус Java" Брюса Тейта, разобравшись с основными языковыми концепциями, которые, кстати сказать, ничуть не облегчили ни дизассемблирование байт-кода, ни его анализ;
- Лучшим дизассемблером Java-программ была и остается легендарная IDA Pro, распространяющаяся на коммерческой основе за нехилые деньги, однако при желании можно обойтись и без нее, воспользовавшись штатным дизассемблером, входящим в бесплатный Java SDK или любой другой утилитой аналогичного назначения, коих в последнее время развелось как грибов (см. JavaBite, описанный ниже);
- Чтобы не корячиться над анализом байт-кода, имеет смысл прогнать ломаемое приложение через Java-декомпилятор, выдающий вполне читабельные и структурированные листинги. Java-декомпиляторов существует много. Хороших и разных. Мыщъх рекомендует бесплатный avaDec by wl, которым пользуется сам. Еще стоит попробовать JDecompiler. Он также неплох и бесплатен. декомпилированный код можно хачить прямо в исходных текстах с последующей рекомпиляцией, но мыщъх этого делать не рекомендует, поскольку декомпилятор не всегда работает корректно и повторная компиляция зачастую ведет к краху программы, поэтому лучше патчить непосредственно сам байт-код;
Рисунок 1. Внешний вид Java-декомпилятора. - Для модификации байт-кода (то бишь "бит-хака") подойдет любой hex-редактор, например, всем известный hiew, однако лучше использовать специализированные инструменты, лучим из которых является бесплатный JavaBite by BitArts, наглядно отображающий дерево классов, а также включающий в себя дизассемблер и ассемблер байт-кода;
- Мобильные игры как правило распространяются в виде упакованных jar-файлов, создаваемые одноименной утилитой, входящей в состав Java SDK, однако это не единственно возможный вариант. Архиваторы 7-Zip (бесплатный) и WinAce (условно-бесплатный) справляются со своей задачей ничуть не хуже. Исключение составляют Java-приложения, снабженные цифровыми подписями. Ни 7-Zip, ни WinAce их создавать не умеют, да этого и не требуется. Любой сотовый телефон загрузит jar-архив и без подписи;
- Иногда рядом с jar-архивом лежит jad-файл, без которого некоторые модели телефонов откажутся загружать java-приложение и тут приходится прибегать к помощи бесплатной утилиты JADgen, генерирующий jad-файлы на основе jar-архивов;
- Некоторые хакеры рекомендуют использовать эмулятор сотового телефона для проверки работоспособности взломанных игр. Другие же (к числу которых принадлежит и мыщъх) предпочитают живое "железо", тем более что закачать приложение на телефон - не проблема. Правда, при этом существует угроза "завесить" аппарат так, что придется вынимать батарею или даже делать полный reset, удерживая определенные клавиши при включении (у каждой модели телефона - разные), описание которых можно найти в сервисной документации. Впрочем, страхи угробить телефон некорректным взломом очень сильно преувеличены. В 90% случаях некорректный хак пресекается жутко матерящимся Java-верификатором. В 9% случаях игра просто зависает, подвешивая за собой весь телефон. И только в 1% происходит разрушение содержимого энергонезависимой памяти и прочий бэд. Так что, неуверенным в себе хакерам все-таки стоит пользоваться эмулятором.
Поставим себе задачу - обессмертить колобка, чтобы игра никогда не кончалась. Главное - разобраться с техникой и стратегией взлома, освоив основные хакерские трюки и приемы. Остальные программы ломаются аналогичным образом и неважно, что это - вечная жизнь или снятие ограничения с количества запусков.
Короче, кончай курить, мужики! Курить мы будет потом, а сейчас глотнем пива и возьмемся за дело.
Как мы будем ломать
Пускаем мы, значит, Macroman'а и даем ему умереть на зубах зловредных существ (типа приведений), агрессивно бегающих по лабиринту. На экране появляется надпись: "1 Life Left" (осталась одна жизнь). Очевидно, что код, выводящий эту строку, так или иначе связан с кодом, уменьшающим количество жизней при каждом акте поедания колобка. Во всяком случае, во всех императивных языках программирования (к которым принадлежит и Java) ситуация обстоит именно так.
Вот эту строку мы и будем искать. Прямым текстом. Но сначала распакуем jar-архив, пропустив его через 7-Zip (предварительно изменив расширение с .jar на .zip). И вот что мы получим в результате:
Листинг 1. Содержимое распакованного jar-архива с ломаемой игрой.
Берем FAR (или любой другой файл-менеджер), давим (Search), вводим маску файлов "*" (все файлы) и строку для поиска "Life Left", которую и обнаруживаем через секунду поиска в файле "e.class", занимающим всего 19 Кбайт.
Рисунок 2. Поиск строки "Life" в декомпилированном листинге.
Прогнав "e.class" через JDec (или любой другой декомпилятор) мы получаем текстовой файл "e.java" размером порядка 36 Килобайт, который тут же открываем в FAR'е по (Edit), давим (Search) и вновь ищем строку "Life Left", затаившуюся в окрестностях следующего кода (см. листинг 2):
Листинг 2. Декомпилированный фрагмент Java-программы, найденный поиском строки "Live Left".
Машинная логика вполне стандартна и особых пояснений не требует. Если переменная "ax" становится меньше нуля - мы получаем "Game Over", в противном случае на экран выводится количество оставшихся жизней.
Следовательно, чтобы взломать программу, необходимо найти код, уменьшающий переменную "ax" на единицу при каждом акте смерти. А как мы его найдем? Да все тем же контекстным поиском! Просто ищем "ax" контекстным поиском, анализируя прилегающий к ней код. Довольно быстро мы найдем строку инициализации, устанавливающую начальный счетчик жизней равный двум (на самом деле - трем, с учетом того, что смерть наступает, только если ax Листинг 3. Фрагмент кода, отвечающего за начальное количество жизней.
Можно, конечно, заменить строку "ax = 2" на "ax =69" (например), но это плохой и порочный путь. Во-первых, вечной жизни мы все равно не обретем, а во-вторых, еще не известно, как программа отреагирует на такие издевательства (поскольку количество оставшихся жизней отображается в виде "колобков" внизу экрана, то при слишком большом их числе поведение программы рискует стать непредсказуемым и крышесрывательным).
Ладно, идем дальше и. Видим заветную команду "ax--" в методе "f()", уменьшающую значение переменной "ax" на единицу (см. листинг 4).
Листинг 4. Декомпилированный фрагмент метода f(), уменьшающего переменную "ax" (счетчик жизней) на единицу.
Вот это - то, что нужно. Остается найти байт-код, соответствующей данной конструкции языка высокого уровня. Вот тут-то нам и пригодится IDA Pro, ну или утилита JavaBite. Открыв файл "e.class" в любой из этих программ, переходим к методу "f()" и внимательно исследуем код на предмет обращений к переменной "ax".
Рисунок 3. Дизассемблированный байт-код в IDA Pro.
Как легко увидеть, в методе "f()" обращение к переменной "ax" встречается дважды (см. листинг 5):
042 aload_0 ; var016_0
089 dup
180 001 199 getfield ax B ; читаем переменную ax, закидывая ее на стек
004 iconst_1 ; закидываем на стек константу 1
100 isub ; стягиваем со стека две ячейки и вычитаем их
145 int2byte ; преобразуем в int и забрасываем на стек
181 001 199 putfield ax B ; обновляем содержимое переменной ax
042 aload_0 ; var016_0
180 001 199 getfield ax B
156 000 047 ifge met016_106
Листинг 5. Фрагмент дизассемблированного байт-кода метода f(), уменьшающего переменную "ax" (счетчик жизней) на единицу.
А что, если заменить команду "isub" (опкод 64h/100) на "парную" ей команду "iadd" (опкод 60h/96)? Эту операцию легко осуществить в любом hex-редакторе, например, в hiew'е. Просто ищем последовательность "042/089/180 001 199/004/100/145/181 001 199" (окружающую инструкцию "isub") и заменяем 100 на 96. Тогда при каждом столкновении со злобными приведениями количество жизней будет увеличиваться на единицу и. в конце концов мы получим незапланированное переполнение и тогда - трындец. А нам трынденца не надо! Нам надо корректный взлом.
Рисунок 4. С каждой смертью количество жизней увеличивается на единицу.
Хорошо! Попробуем заменить инструкцию "isub" на команду "nop" (опкод 00h). Кстати, говоря, это можно сделать прямо в JaveBite, не прибегая к помощи hiew'а. Достаточно подвести курсор к "isub", щелкнуть правой кнопкой мыши и в появившемся контекстном меню выбрать пункт "Edit Instuction". Откроется диалоговое окно со списком всех возможных команд. Находим "nop", жмем на "OK" и давим (Save Class), чтобы сохранить результаты правки на диск.
Рисунок 5. Модификация байт-кода в Java Bite.
Вот только результаты эти. мягко говоря, довольно удручающие. И при запуске программы Java-верификатор завершает ее выполнение в принудительном порядке. Это в x86-процессорах с их регистровой архитектурой инструкцию SUB можно безболезненно менять на NOP. Виртуальная машина Java исповедует иной принцип и аргументы команды "isub" предварительно забрасываются на вершину стека, в расчете на то, что она стащит их оттуда. Замена "isub" на "nop" вызывает дисбаланс стека и чтобы восстановить статус-кво необходимо так же "занопить" и команду "iconst_1". Инструкцию "int2byte" можно не трогать, т.к. она имеет нулевой побочный эффект, сохраняя стек в том состоянии, в каком он был до ее вызова.
Короче говоря, корректно хакнутый байт-код выглядит так:
042 aload_0 ; var016_0
089 dup
180 001 199 getfield ax B ; читаем переменную ax, закидывая ее на стек
000 nop ; ничего не делаем
000 nop ; ничего не делаем
145 int2byte ; преобразуем в int и забрасываем на стек
181 001 199 putfield ax B ; обновляем содержимое переменной ax
Листинг 6. Байт-код, получивший "бессмертие" (хакнутые байты выделены полужирным шрифтом).
Сохранив изменения в класс-файле по (или если мы работаем в hiew'е), нам остается только упаковать все файлы обратно в jar-архив и залить его на сотовый телефон. Для тестирования, так сказать.
Рисунок 6. Создание jar-архива с помощью WinAce.
При использовании WinAce достаточно выделить все файлы (включая каталоги), в типе архива указать "JavaSoft-Jar" и плюхнуться на "ОК" (см. рис. 8). А вот среди выходных форматов, поддерживаемых архиватором 7-Zip никакого jar'а нет! То есть, он, конечно, есть, просто называется Zip'ом. В "Archive format" указываем: "ZIP", в "Compression level" - "Normal", поле "Compression method" выставляем в "Deflate". Остальные параметры оставляем по умолчанию - как есть. Главное, не забыть вместо расширения ".zip" указать ".jar". Ну, а имя файла может быть каким угодно.
Рисунок 7. Создание jar-архива в 7-Zip.
Итак, игра залита на телефон и. Дрожащими от волнения руками (все-таки наш первый взлом, как-никак) мы едва попадаем по клавишам, запускаем игру и. о чудо. Она работает! (В смысле, не падает) И самое главное - счетчик жизней навечно застыл на отметке двух. Мы обрели бессмертие, а вместе с ним утратили весь игровой азарт и интерес. Но какой интерес играть в игры? Вот ломать их - настоящий кайф.
Рисунок 8. Счетчик жизней, навечно застывший на отметке двух.
Заключение или что еще можно сделать
Вот мы и совершили свой первый взлом! Как видно, ничего сложного и сверхъестественного в этом нет. Не маги программы ломают. Это доступно каждому! Главное - сделай свой первый шаг, а уж там. поле деятельности практически безгранично. Можно заменить все текстовые строки, в том числе относящиеся к копирайту компании-создателя. Не то, чтобы это было законно, зато очень приятно и прикольно показывать друзьям мобилу с надписью "hacked by. ".
Более творческие настроенные кодокопатели наверняка уже загружают спрайты в графический редактор, коверкая их в готическом хакерском стиле. Ну, или меняют логотип на заставке, который также находится в png-файлах, собранных в директории image.
Конечно, мы рассмотрели простейший случай взлома незащищенной программы. Некоторые игры и приложения тем или иным образом проверяют целостность байт-кода, а также пропускаются через обфускаторы, добавляющие "мусорные" инструкции, отвлекающие внимание и затрудняющие анализ, но базовая техника взлома при этом все равно остается неименной. Найти-и-обезвредить - вот наш девиз. Знания приходят с опытом, а опыт - со временем, проведенным за ломанием игр. Так что, дерзайте. Да. и еще. не слишком-то распространяйтесь о своих хакерских наклонностях. А то ведь и повязать могут, хотя взлом "для себя" закон не запрещает, но это уже по-любому тема совсем другого разговора.
Учу java. Дошёл до внутренних классов. Написал такой код:
Таким образом компилятор добавляет в класс A метод:
(то, что имя метода именно такое, проверил с помощью рефлексии).
Допустим, я злоумышленник и хочу получить доступ к скрытому полю а класса А . Добавляю я в папку a -файл Hacker.java, содержащий следующий код:
Компилирую его уже не через Eclipse, а через cmd вот так:
(т.е. пакет a у меня находится в D:\6. ФПМИ\Информатика\JavaProjects\Internal\bin ), на что мне компилятор сообщает:
Почему? Почему из меня такой плохой хакер?) В общем, объясните, пожалуйста, как надо. Но только доступно объясните, ибо я ещё просто студентик. )
11 1 1 золотой знак 2 2 серебряных знака 8 8 бронзовых знаков 723 1 1 золотой знак 7 7 серебряных знаков 23 23 бронзовых знака Возможно, метод access$0 приватный. Попробуйте вызвать его с помощью рефлексии. Нет, он не приватный, а "пакетный", иначе компилятору не было бы смысла его создавать. Я компилирую в два этапа. Сначала просто "приложение", а потом хочу внедрить новый класс "Hacker" в тот же пакет. Т.е. второй раз я компилирую только класс Hacker, указывая где искать нужные (уже скомпилированные) классы ("-classpath Internal\bin").Вероятно, компилятор Java достаточно умён, чтобы не позволить вам напрямую вызывать синтетические методы. А вот уже JVM можно обмануть. Для этого произведём следующие манипуляции:
- Скомпилируем класс A и в декомпиляторе посмотрим сигнатуру метода, сгенерированного для доступа к приватному полю. Пусть она будет static int access$0(A a) .
- Забэкапим скомпилированные классы A.class и A$Internal.class . В исходнике класса A удалим внутренний класс и добавим метод access$0 :
- Скомпилируем эту версию класса A . Скомпилируем класс Hacker , указав в параметре -classpath компилятора путь к модифицированной версии класса A .
- Теперь можно заменить модифицированный класс A на оригинальный и запустить программу.
На уровне байт-кода не будет никакой принципиальной разницы между вызовами синтетического метода access$0 и написанного нами метода с таким же именем. JVM не будет задумываться о корректности предоставления доступа к этому методу классу Hacker , она считает, что об этом позаботился компилятор (как видите, он не позволяет вам даже скомпилировать код, содержащий прямой вызов синтетического метода).
Теперь осталось придумать хоть одно практическое применение такого подхода. Ведь всё это можно было сделать без танцев с бубнами при помощи рефлексии.
Читайте также: