Рубрики
Без рубрики

Взлом сторонних API-интерфейсов на JVM

Экосистема JVM является зрелой и предлагает множество библиотек, так что вам не нужно изобретать велосипед…. Помеченный как java, hack, api.

Экосистема JVM является зрелой и предлагает множество библиотек, так что вам не нужно изобретать велосипед. Базовые – и не очень базовые – функциональные возможности находятся всего в нескольких шагах от вас. Иногда, однако, зависимость и ваш вариант использования немного смещены.

Правильным способом исправить это было бы создать запрос на извлечение. Но ваш крайний срок – завтра: вам нужно заставить это сработать сейчас ! Пришло время взломать предоставленный API.

В этом посте мы рассмотрим некоторые альтернативы, которые позволяют вам заставить сторонние API вести себя так, как не предполагали их разработчики.

Отражение

Представьте, что API был разработан в соответствии с принципом “открыто-закрыто”:

В объектно-ориентированном программировании принцип открытого–закрытого гласит: “программные объекты (классы, модули, функции и т.д.) Должны быть открыты для расширения, но закрыты для модификации”; то есть такая сущность может разрешить расширение своего поведения без изменения исходного кода.

Принцип “открыто-закрыто”

Представьте, что общедоступный API зависимостей не подходит для вашего варианта использования. Вам нужно расширить его, но это невозможно, потому что дизайн запрещает это – специально.

Чтобы справиться с этим, самый старый трюк с JVM в книге, вероятно, заключается в отражении.

Отражение – это функция языка программирования Java. Это позволяет исполняемой Java-программе исследовать или “анализировать” саму себя и манипулировать внутренними свойствами программы. Например, класс Java может получать имена всех своих членов и отображать их.

Использование отражения Java

В нашей области отражение позволяет вам получать доступ к состоянию, которое не предназначалось для доступа, или вызывать методы, которые не предназначались для вызова.

public class Private {

  private String attribute = "My private attribute";

  private String getAttribute() {
    return attribute;
  }
}

public class ReflectionTest {

  private Private priv;

  @BeforeEach
  protected void setUp() {
    priv = new Private();
  }

  @Test
  public void should_access_private_members() throws Exception {
    var clazz = priv.getClass();
    var field = clazz.getDeclaredField("attribute");                             // 1
    var method = clazz.getDeclaredMethod("getAttribute");                        // 2
    AccessibleObject.setAccessible(new AccessibleObject[]{field, method}, true); // 3
    field.set(priv, "A private attribute whose value has been updated");         // 4
    var value = method.invoke(priv);                                             // 5
    assertThat(value).isEqualTo("A private attribute whose value has been updated");
  }
}
  1. Получить ссылку на частное поле класса Private
  2. Получить ссылку на частный метод класса Private
  3. Разрешить использовать личные участники
  4. Установите значение поля private
  5. Вызовите метод private

Тем не менее, рефлексия имеет некоторые ограничения:

  • “Волшебство” происходит с AccessibleObject.setAccessible . Это можно запретить в runtime с помощью правильно настроенного менеджера безопасности. Я признаю, что за всю свою карьеру я никогда не видел, чтобы Менеджер по безопасности использовался.
  • Модульная система ограничивает использование API отражения. Например, и вызывающий, и целевые классы должны находиться в одном модуле, целевой элемент должен быть public и т.д. Обратите внимание, что многие библиотеки не используют модульную систему.
  • Отражение хорошо, если вы напрямую используете класс с закрытыми членами. Но это бесполезно, если вам нужно изменить поведение зависимого класса: если ваш класс использует сторонний класс A который сам по себе требует класса B и вам нужно изменить B .

Затенение пути к классу

Длинный пост мог бы быть посвящен исключительно механизму загрузки классов Java. Для этого поста мы сузим его до classpath . Путь к классу – это упорядоченный список папок и JAR-файлов, которые JVM будет просматривать для загрузки ранее выгруженных классов.

Давайте начнем со следующей архитектуры:

Самая простая команда для запуска приложения заключается в следующем:

java -cp=.:thirdparty.jar Main

По какой-то причине представьте, что нам нужно изменить поведение класса B . Его конструкция этого не допускает.

Независимо от этого дизайна, мы могли бы взломать его в любом случае с помощью:

  1. Получение исходного кода класса B
  2. Изменение его в соответствии с нашими требованиями
  3. Компилируя его
  4. Помещаем скомпилированный класс перед JAR, содержащим исходный класс, в classpath

При запуске той же команды, что и выше, загрузка класса будет происходить в следующем порядке: Main , B из файловой системы и A из JAR; B в JAR будет пропущен.

Этот подход также имеет некоторые ограничения:

  • Вам нужен исходный код B – или, по крайней мере, способ получить его из скомпилированного кода.
  • Вы должны уметь компилировать B из источника. Это означает, что вам нужно заново создать все необходимые зависимости B .

Таковы технические требования. Возможно ли это юридически – это совершенно другая проблема, выходящая за рамки этого поста.

Аспектно-ориентированное программирование

В отличие от C++, язык Java предлагает единое наследование: класс может наследовать от одного суперкласса.

Однако в некоторых случаях множественное наследование является обязательным. Например, мы хотели бы иметь методы ведения журнала для разных уровней журнала в иерархии классов. Некоторые языки придерживаются принципа единого наследования, но предлагают альтернативу для сквозных задач, таких как ведение журнала: Scala предоставляет черты, в то время как интерфейсы Java и Kotlin могут иметь свойства.

” В те времена” AOP был довольно популярен для добавления сквозных функций к классам, которые не были частью одной и той же иерархии.

В вычислительной технике аспектно-ориентированное программирование (AOP) – это парадигма программирования, направленная на повышение модульности за счет разделения сквозных задач. Он делает это, добавляя дополнительное поведение к существующему коду (совет) без изменения самого кода, вместо этого отдельно указывая, какой код изменяется с помощью спецификации “pointcut”, такой как “регистрировать все вызовы функций, когда имя функции начинается с “set””. Это позволяет добавлять в программу поведения, которые не являются центральными для бизнес-логики (например, ведение журнала), не загромождая ядро кода функциональными возможностями. AOP формирует основу для аспектно-ориентированной разработки программного обеспечения.

Аспектно-ориентированное программирование

В Java/| AspectJ является предпочтительной библиотекой AOP. Он опирается на следующее GitHub :

  • /| Точка соединения определяет определенную четко определенную точку в выполнении программы, например , выполнение методов A
  • pointcut выбирает конкретные точки соединения в потоке программы, например, , выполнение любого метода , аннотированного с помощью @Loggable
  • Совет объединяет pointcut (для выбора точек соединения) и основной код (для запуска в каждой из этих точек соединения)

Существует два класса: один представляет общедоступный API и делегирует его реализацию другому.

public class Public {

  private final Private priv;

  public Public() {
    this.priv = new Private();
  }

  public String entryPoint() {
    return priv.implementation();
  }
}

final class Private {

  final String implementation() {
    return "Private internal implementation";
  }
}

Представьте, что нам нужно изменить частную реализацию.

public aspect Hack {

  pointcut privateImplementation(): execution(String Private.implementation()); // 1

  String around(): privateImplementation() {                                    // 2
    return "Hacked private implementation!";
  }
}
  1. Pointcut, который перехватывает выполнение Private.implementation()
  2. Совет, который завершает вышеупомянутое выполнение и заменяет исходное тело метода своим собственным

AspectJ предлагает различные реализации:

  1. Время компиляции: байт-код обновляется во время сборки
  2. Время после компиляции: байт-код обновляется сразу после сборки. Это позволяет обновлять не только классы проекта, но и зависимые банки.
  3. Время загрузки: байт-код обновляется во время выполнения при загрузке классов

Вы можете настроить первый вариант в Maven следующим образом:


  
    
      maven-surefire-plugin
      2.22.2
    
    
      com.nickwongdev
      aspectj-maven-plugin
      1.12.6
      
        ${java.version}
        ${java.version}
        ${java.version}
        ${project.encoding}
      
      
        
          
            compile
          
        
      
    
  


  
    org.aspectj
    aspectjrt
    1.9.5
  

AOP в целом и AspectJ, в частности, представляют собой ядерный вариант. У них практически нет ограничений, хотя, должен признаться, я не проверял, как это работает с модулями Java.

Однако официальный плагин AspectJ Maven от Codehaus обрабатывает JDK только до версии 8 (входит в комплект), поскольку никто не обновлялся с 2018 года. Кто-то разветвил код на GitHub который обрабатывает более поздние версии. Форк может обрабатывать JDK до версии 13 и библиотеку AspectJ до версии 1.9.5.

Агент Java

AOP предлагает высокоуровневую абстракцию, когда вы хотите взломать. Но если вы хотите изменить код мелкозернистым способом, нет другого способа, кроме как изменить сам байт-код . Интересно, что JVM предоставляет нам стандартный механизм для изменения байт-кода при загрузке класса.

Вы, вероятно, уже сталкивались с этой особенностью в своей карьере: они называются Java-агентами. Агенты Java могут быть установлены статически в командной строке при запуске JVM или динамически подключены к уже запущенной JVM после этого. Для получения дополнительной информации об агентах Java, пожалуйста, проверьте этот пост (раздел “Краткое введение в агенты Java”).

Вот код простого Java-агента:

public class Agent {

    public static void premain(                      // 1
            String args,                             // 2
            Instrumentation instrumentation){        // 3
        var transformer = new HackTransformer();
        instrumentation.addTransformer(transformer); // 4
    }
}
  1. remain – это точка входа для статически заданных агентов Java, точно так же, как main для обычных приложений
  2. Мы тоже получаем аргументы, как и в случае с main
  3. Инструментовка является “волшебным” классом
  4. Установите преобразователь, который может изменять байт-код до того, как JVM загрузит его

Агент Java работает на уровне байт-кода. Агент предоставляет вам массив байтов, в котором хранится определение класса в соответствии со спецификацией JVM и, точнее, в формате файла class . Необходимость менять байты в байтовом массиве – это не весело. Хорошей новостью является то, что другие уже сталкивались с этим требованием раньше. Следовательно, экосистема предоставляет готовые к использованию библиотеки, которые предлагают абстракцию более высокого уровня.

В следующем фрагменте трансформатор использует Javassist :

public class HackTransformer implements ClassFileTransformer {

  @Override
  public byte[] transform(ClassLoader loader,
              String name,
              Class clazz,
              ProtectionDomain domain,
              byte[] bytes) {                                            // 1
    if ("ch/frankel/blog/agent/Private".equals(name)) {
      var pool = ClassPool.getDefault();                                 // 2
      try {
        var cc = pool.get("ch.frankel.blog.agent.Private");              // 3
        var method = cc.getDeclaredMethod("implementation");             // 4
        method.setBody("{ return \"Agent-hacked implementation!\"; }");  // 5
        bytes = cc.toBytecode();                                         // 6
      } catch (NotFoundException | CannotCompileException | IOException e) {
        e.printStackTrace();
      }
    }
    return bytes;                                                        // 7
  }
}
  1. Байтовый массив класса
  2. Точка входа в API Javassist
  3. Получите класс из пула
  4. Получить метод из класса
  5. Замените тело метода, установив новое
  6. Замените исходный массив байтов на обновленный
  7. Возвращает обновленный массив байтов для загрузки JVM

Вывод

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

С их помощью вы сможете решить любую проблему, с которой столкнетесь. Просто помните, что библиотеки – и JVM – были разработаны таким образом по уважительной причине: чтобы вы не совершали ошибок.

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

Полный исходный код этого поста можно найти на Github:

java-гик/взлом-jvm-api

Этот проект содержит 2 модуля: hack-agent и basic-hack .

Для тестовой фазы последнего требуется пакет первого.

Следовательно, для успешного использования этого проекта требуется выполнить следующие команды для того, чтобы:

  1. mvn install -pl '! базовый взлом'

  2. mvn test -pl basic-взлом

Идти дальше:

Первоначально опубликовано на Фанат Java 23 мая рд , 2021

Оригинал: “https://dev.to/nfrankel/hacking-third-party-apis-on-the-jvm-579o”