Статьи

Исправление Java во время выполнения

В этой статье будет немного освещено, как исправить проблемы со сторонними библиотеками, которые

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

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

В качестве мотивации для этого сценария рассмотрим атаки на структуры данных с хэш-индексированием, такие как java.util.Hashtable и java.util.HashMap (для тех, кто не знаком с подобными атаками, я бы порекомендовал посмотреть следующий доклад 28C3: атака типа « отказ в обслуживании» на веб-приложения стала проще ).

Короче говоря, основной проблемой является использование некриптографических хеш-функций (где легко найти коллизии). Основная причина скрыта в функции java.lang.String.hashCode () . Очевидный подход заключается в исправлении класса java.lang.String, что сложно по двум причинам:

  1. он содержит нативный код
  2. он принадлежит к базовым классам Java, которые поставляются с установкой Java и, таким образом, находятся вне нашего контроля

Первый пункт заставит нас патчить архитектуру и специфичные для ОС библиотеки, которые мы должны обойти, когда это возможно. Второй пункт верен, но он немного более гибкий, как мы увидим далее.

Итак, давайте пересмотрим: исправление нативных патчей грязно, и мы не хотим идти по этому пути — мы должны сделать некоторую работу для других (в данном случае патчей SDK libs), которые не желают исправлять свой код.

Попытка:
Классы java.util.Hashtable и java.util.HashMap обеспокоены проблемой хеширования и не используют какой-либо нативный код. Патчировать эти классы намного проще, поскольку достаточно предоставить один скомпилированный класс для всех архитектур и ОС.
Мы могли бы использовать одно из предоставленных решений для ошибки и настроить (или заменить) исходные классы с фиксированными версиями. Сложность заключается в исправлении виртуальной машины, не затрагивая основные библиотеки — я думаю, пользователи будут очень разочарованы, если им придется менять части своей установки JVM или, что еще хуже, наше приложение делает это автоматически во время установки. Дальнейшее введение новых пользовательских загрузчиков классов может быть затруднено в некоторых случаях.

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

Это можно легко сделать, используя API Java Instrumentation . Чтобы процитировать JavaDoc :

«Предоставляет сервисы, которые позволяют агентам языка программирования Java программным инструментам, работающим на JVM».

И это именно то, что нам нужно!

Доказательство концепции
Сначала нам понадобится пример приложения для демонстрации концепции:

01
02
03
04
05
06
07
08
09
10
11
12
public class StringChanger {
  public static void main(String[] args) {
    System.out.println(A.shout());
  }
 
}
 
public class A {
  public static String shout() {
    return "A";
  }
}

Когда этот класс запускается, он просто выводит: A
После применения нашего «патча» мы бы хотели получить следующий вывод: Apatched

«Запатентованный код выглядит так:

1
2
3
4
5
public class A {
  public static String shout() {
    return "Apatched";
  }
}

Далее нам нужен «Агент», который управляет используемыми классами и исправляет нужные:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
final public class PatchingAgent implements ClassFileTransformer {
 
  private static byte[] PATCHED_BYTES;
  private static final String PATH_TO_FILE = "Apatched.class";
  private static final String CLASS_TO_PATCH = "stringchanger/A";
 
  public PatchingAgent() throws FileNotFoundException, IOException {
    if (PATCHED_BYTES == null) {
      PATCHED_BYTES = readPatchedCode(PATH_TO_FILE);
    }
  }
 
  public static void premain(String agentArgument,
    final Instrumentation instrumentation) {
    System.out.println("Initializing hot patcher...");
    PatchingAgent agent = null;
 
    try {
     agent = new PatchingAgent();
    } catch(Exception e) {
      System.out.println("terrible things happened....");
    }
 
    instrumentation.addTransformer(agent);
  }
 
  @Override
  public byte[] transform(final ClassLoader loader, String className,
    final Class classBeingRedefined, final ProtectionDomain protectionDomain,
    final byte[] classfileBuffer) throws IllegalClassFormatException {
    byte[] result = null;
 
    if (className.equals(CLASS_TO_PATCH)) {
     System.out.println("Patching... " + className);
     result = PATCHED_BYTES;
    }
 
    return result;
  }
 
  private byte[] readPatchedCode(final String path)
    throws FileNotFoundException, IOException {
    ...
  }
}

Не волнуйтесь — я не буду беспокоить вас деталями реализации, поскольку это всего лишь код PoC, далеко не красивый, умный, быстрый и аккуратный. За исключением того факта, что я ловлю Exception только потому, что мне сейчас лень, я не фильтрую входные данные, а создаю глубокие копии (защитное программирование как модное слово), это действительно не должно восприниматься как производственный код.

public PatchingAgent ()
Инициализирует агент (в этом случае выбирает байты пропатченного файла A.class. Запатентованный класс был скомпилирован и хранится где-то, где мы можем получить к нему доступ.

public static void premain (…)
Этот метод вызывается после инициализации JVM и подготовки агента.

public byte [] transform (…)
Всякий раз, когда класс определен (например, ClassLoader.defineClass (…)), эта функция вызывается и может преобразовывать обработанный класс byte [] ( classfileBuffer ). Как видно, мы сделаем это для нашего класса A в пакете stringchanger. Вы не ограничены в том, как вы собираетесь преобразовывать класс (если он остается действительным классом Java) — например, вы можете использовать каркасы модификации байт-кода… — для простоты мы предполагаем, что мы заменим старый байт [] на один из пропатченного класса (путем простой буферизации полного пропатченного файла A.class в байт []).

Это все, что касается кодирующей части патчера … В заключение мы должны создать jar агента со специальным файлом manifest.mf, который сообщает JVM, как агент может быть вызван.

Манифест-Версия: 1.0
X-COMMENT: Main-Class будет добавлен автоматически при сборке
Premain-Class: stringchanger.PatchingAgent

После создания этой банки мы можем попробовать наше приложение PoC. сначала мы будем вызывать его без необходимых аргументов JVM для вызова агента:

бег:

СТРОИТЬ УСПЕШНО (общее время: 0 секунд)

Он ведет себя как ожидалось и печатает вывод, как определено непатчированным классом.

А теперь мы попробуем с волшебными аргументами JVM вызвать агент — javaagent: StringChanger.jar:

бег:
Инициализация горячего патчера…
Чтение пропатченного файла.
Патчинг… Стрингченджер / А
Apatched
СТРОИТЬ УСПЕШНО (общее время: 0 секунд)

Вуаля, код был успешно исправлен на лету!

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

Чтобы было понятно, это не элегантное решение — по крайней мере, оно очень грязное! Лучшим способом было бы исправить основную причину, но пока нет поставщиков исправлений, разработчики могут помешать своему программному обеспечению путем оперативного исправления, не переписывая каждую строку, где используются уязвимые классы.

Наконец, я хотел бы попросить комментарии, улучшения или просто лучшие решения. Большое спасибо Юраю Соморовскому, который вместе со мной работает над этим вопросом.

Ссылка: исправление Java во время выполнения от нашего партнера JCG