- Сложный протокол общения между сервером и клиентом. Чтобы отправлять произвольные запросы – патчим;
- Захардкожены настройки. Чтобы поменять – патчим;
- Для демонстрации последствий проблем типа «race condition» – патчим.
Конечно, в каждом случае можно решить проблему, не прибегая к модификации приложения. Но зачастую это самый простой и быстрый способ добиться необходимого результата. В этой статье мы расскажем, как легко и быстро изменить функционал в приложении на Java.
Application
Для рассказа нам понадобится подопытное приложение. Конечно, мы напишем его сами. Его структура на рисунке 1.
рисунок 1
Как видим, оно достаточно простое: два класса, которые запускают приложение и рисуют GUI. Вот их исходники. Main.java:
1 package com.mycompany;
2
3 public class Main
4 {
5 public static void main(String[] args)
6 {
7 javax.swing.SwingUtilities.invokeLater(new Runnable()
8 {
9 public void run() {
10 new Window();
11 }
12 });
13 }
14 }
15
Window.java
1 package com.mycompany;
2
3 import com.mycompany.Targets.*;
4 import javax.swing.*;
5
6 public class Window extends JFrame
7 {
8 private JPanel panel1;
9 private JLabel Level1Value;
10 private JLabel Level2Value;
11 private JLabel Level3Value;
12
13 public Window()
14 {
15 super("Simple example");
16
17 Level1Value.setText(new Level1().getText());
18 Level2Value.setText(new Level2().getText());
19 Level3Value.setText(new Level3().getText());
20
21 getContentPane().add(panel1);
22
23 setSize(300,100);
24 setLocationRelativeTo(null);
25
26 setVisible(true);
27 setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
28 }
29 }
30
Классы очень простые. Main – создаёт окно Window. Window – печатает текст в 3 поля. Текст он берёт из наших целевых классов, на примере которых мы и будем рассматривать способы модификации. Как выглядит запущенное приложение, смотрите на рисунке 2.
рисунок 2
Из инструментов нам понадобится только JDK и JavaDecompiler. Приступаем к патчу.
Level 1
Наше приложение – это jar-архив. Для начала просто вставляем его в JavaDecompiler и получаем исходник интересующего нас класса. Level1.java:
1 package com.mycompany.Targets;
2
3 public class Level1
4 {
5 public String getText()
6 {
7 return "Data";
8 }
9 }
10
Замечательно, теперь мы копируем этот код в текстовый файл с расширением «java». Это самый простой класс, который только можно придумать. Теперь меняем этот исходник, как пожелаем. Мы заменили возвращаемое значение на «My msg». Далее выполняем команду:
В результате мы получим готовый Level1.class. Затем нам нужно просто добавить его в наш jar-архив. Сделаем это следующей командой:
Обратим внимание, что мы создали путь, идентичный названию пакета, в котором расположен наш класс, и переложили туда бинарник.
Результат – на рисунке 3.
рисунок 3
Рассмотрим случай посложнее.
Level 2
Проделываем всё то же самое и получаем исходник. Level2.java:
1 package com.mycompany.Targets;
2
3 public class Level2
4 {
5 public String getText()
6 {
7 return "Super " + (new Level1().getText());
8 }
9 }
В нём я поменяю «Super» на «New». И попытаюсь скомпилировать:
Не получилось, потому что в классе есть ссылка на другой объект из этого пакета. Но java очень дружелюбна и позволяет указать при компиляции любой путь, где лежат классы. Т.е. можно просто выполнить команду:
В результате, у нас опять готовый бинарник. Точно так же обновляем jar-архив и смотрим результат на рисунке 4:
рисунок 4
Ну и рассмотрим последний случай, в котором добавится ещё немного сложностей.
Level 3
Декомпилируем наш класс. Level3.java:
1 package com.mycompany.Targets;
2
3 import jd.core.CoreConstants;
4
5 public class Level3
6 {
7 public String getText()
8 {
9 return CoreConstants.class.getSimpleName() + " "
10 + (new Level2().getText());
11 }
12 }
Здесь добавим строчку "-update-". Как мы можем сразу заметить, добавился сторонний пакет. И, если мы попытаемся собрать, получим ошибку:
И опять же, java выручает своей дружелюбностью. Ей можно просто указать путь, где лежат jar-архивы зависимостей. Делается это так:
(да, тут указание пути класса лишнее, т.к. он запакован в архив и лежит в той же директории, но т.к. это не всегда так, я его всё же указал)
И далее всё так же – результат на рисунке 5.
Рисунок 5
Итог
В итоге мы рассмотрели примеры, как просто и быстро можно модифицировать Java-приложения. Конечно, рассмотрели мы на примере jar-архива, но вы же понимаете, что если классы не запакованы, то всё гораздо проще?
Комментарии (15)
sleeply4cat
05.05.2016 09:47+4Статья, честно говоря, совсем «для начинающих». Даже если говорить только о техническом аспекте, не затрагивая, собственно, тестирования, ради которого всё затевалось, то мы не увидели ни типовых ошибок декомпиляции, коих эта древняя софтина генерирует тонны, ни потери типизации в циклах for-each и дженериках, ни «сборки» в кучу анонимных классов из разных файлов…
Кроме того, не было сказано ровным счётом ничего об модификации обфусцированных приложений, даже простейших вариантов вроде запаковывания в jar A.class и a.class, не уживающихся после распаковки в NTFS или перемещения всех классов в корневой пакет, чтобы код был некомпилируем стандартным JDK.USSCLTD
05.05.2016 09:47Ага, статья совсем «для начинающих». Название, на мой взгляд, полностью это отражает — «Easy Hack».
Этот метод патча ни в коем случае не претендует на универсальность. Он даже не способен обойти простейшие механизмы защиты, как вы правильно заметили.
Однако, этот метод работает очень во многих случаях — далеко не все бинарники обфуцируют.
Думаю, эта инструкция пригодится людям далёким от java, которые не хотят лезть в байткод и разбираться в java-машине
ExplosiveZ
05.05.2016 10:15+2Первые 5-10 страниц любой книги. Боже, куда же катится Habrahabr! Верните мне Habrahabr 2011/2013, когда я ничего не понимал и приходилось гуглить сложные термины, чтобы стать умнее… Или это я уже настолько умный, что всё понимаю?(
Lure_of_Chaos
Для той же задачи можно облегчить себе жизнь и использовать ASM или агенты, т.к. JAD не всегда хорошо справляется с декомпиляцией java 5+ байткода, особенно перед этим обфусцированного, да и вообще декомпилированный код чаще очень сложно читать.
lany
Можно попробовать FernFlower или Procyon. JAD, конечно, мёртвенький.
Stiver
>> Можно попробовать FernFlower
В IntelliJ IDEA он уже встроен, так что даже ничего искать не надо.
USSCLTD
Да, разумеется — существует много путей достижения цели.
Но когда справляется JAD, зачем усложнять себе жизнь?
Lure_of_Chaos
Потому что не всегда справляется, а так — да.
Для еще большей легкости модификации байт-кода в рантайме я лично для себя открыл tapestry-plastic
lany
Можете ещё в сторону Byte Buddy поглядеть.
Lure_of_Chaos
Очень вкусная штучка, спасибо!
dougrinch
Что значит "ASM или агенты"? Первое — про способ преобразования класса, второе — про точку входа.
Lure_of_Chaos
Именно потому, что они позволяют к проблеме подойти с разных сторон.
dougrinch
Так нет же, это ортогональные вещи. Пропатчив байткод класса через asm все еще остается вопрос "как подсунуть этот байткод в jvm". С другой стороны, зайдя в jvm через агента, нужно еще как-то изменить байткод класса.
Lure_of_Chaos
с помощью загрузчиков классов, конечно.
или подсунуть свой класс вместо существующего.
dougrinch
А почему нельзя подсунуть байткод, сделанный асмом, через агента? Или почему нельзя подсунуть свой класс вместо существующего в загрузчик классов?