- PVSM.RU - https://www.pvsm.ru -

Статический анализ → уязвимость → профит

В статьях про PVS-Studio [1] всё чаще говорят об уязвимостях и дефектах безопасности, которые можно найти с помощью статического анализа. Авторов этих статей критикуют (и я в том числе [2]), что не каждая ошибка является дефектом безопасности. Возникает однако интересный вопрос, можно ли пройти весь путь от сообщения статического анализатора до эксплуатации найденной проблемы и получения какой-то выгоды. В моём случае выгода всё же осталась теоретической, но эксплуатировать ошибку удалось, не особо вникая в код проекта.

Представьте, что вы разрабатываете обфускатор для Java-классов. Ваш бизнес в том, чтобы затруднить извлечение исходного кода из .class-файлов, в том числе с использованием имеющихся на рынке декомпиляторов. Помимо стандартных техник обфускации вполне разумный подход — искать баги в известных декомпиляторах и эксплуатировать их. Если на сгенерированном вами коде популярный декомпилятор просто упадёт, клиенты будут очень рады.

Один из популярных декомпиляторов — Fernflower от JetBrains, который входит в состав IntelliJ IDEA. JetBrains не очень заботится о том, чтобы его распространять отдельно, но его можно собрать из исходников, выкачав из репозитория [3] IntelliJ Community Edition. Ещё проще стянуть с неофициального зеркала [4]: тут не придётся выкачивать всю IDEA. Я возьму недавний коммит d706718 [5]. Собирается Fernflower запуском ant, внешних зависимостей не требует и производит fernflower.jar, который можно использовать как приложение командной строки:

$ java -jar fernflower.jar
Usage: java -jar fernflower.jar [-<option>=<value>]* [<source>]+ <destination>
Example: java -jar fernflower.jar -dgs=true c:mysource c:my.jar d:decompiled

После свежих улучшений статический анализатор IDEA поумнел и стал выдавать предупреждение в методе ConverterHelper::getNextClassName [6]:

int index = 0;
while (Character.isDigit(shortName.charAt(index))) {
  index++;
}

if (index == 0 || index == shortName.length()) { // <<==
  return "class_" + (classCounter++);
}
else { ... }

Предупреждение звучит так:

Condition 'index == shortName.length()' is always 'false' when reached

Такие предупреждения про всегда истинное или всегда ложное условие очень интересны. Часто они свидетельствуют о баге не в данном условии, а в каком-то другом месте выше. С непривычки бывает сложно даже разобраться, почему такой вывод был сделан. Здесь перед условием был цикл while, условие выхода в котором содержит shortName.charAt(index): получить символ строки по индексу. Существенно то, что индекс не может быть больше длины строки или равен ей: иначе charAt выпадет с исключением IndexOutOfBoundsException. Таким образом если цикл дошёл до index == shortName.length(), то выйти из цикла нормально мы не сможем, а гарантировано упадём. А если вышли из цикла нормально, то условие index == shortName.length() действительно всегда ложно.

Далее следует разобраться, действительно ли исключение может произойти или просто условие лишнее. В рамках данного метода такой ситуации ничего не противоречит, достаточно лишь, чтобы вся строка shortName состояла из одних цифр. Отлично, пахнет реальным багом. Но может ли в этот метод попасть строка, состоящая из одних цифр? Смотрим две точки вызова этого метода: ClassesProcessor::new [7] и IdentifierConverter::renameClass [8]. В обоих случаях в качестве shortName передаётся имя класса без пакета, которое по правилам виртуальной машины Java вполне может состоять из цифр. И в обоих же случаях этот код выполняется под условием ConverterHelper::toBeRenamed [9]. Условие немного мутное, но видно, что оно сработает, если имя класса начинается с цифры.

Судя по всему, этот код отвечает за переименование классов, если их имя допустимо для виртуальной машины, но недопустимо для языка Java. Замечательно, давайте сгенерируем корректный класс с именем из цифр. Возьмём любимый ASM [10] и вперёд. Классу желательно иметь конструктор [11]. Напечатаем в нём что-нибудь:

String className = "42";
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// public class 42 extends Object {
cw.visit(Opcodes.V1_6, ACC_PUBLIC | ACC_SUPER, className, null, "java/lang/Object", new String[0]);
// private 42() {
MethodVisitor ctor = cw.visitMethod(ACC_PRIVATE, "<init>", "()V", null, null);
// super();
ctor.visitIntInsn(ALOAD, 0);
ctor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
// System.out.println("In constructor!");
callPrintln(ctor, "In constructor!");
// return;
ctor.visitInsn(RETURN);
ctor.visitMaxs(-1, -1);
ctor.visitEnd(); // }

Ну и чтобы проверить, что класс действительно нормальный, сделаем ему main с честным Hello World:

// public static void main(String[] args) {
MethodVisitor main = cw.visitMethod(ACC_PUBLIC|ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
// System.out.println("Hello World!");
callPrintln(main, "Hello World!");
// return;
main.visitInsn(RETURN);
main.visitMaxs(-1, -1);
main.visitEnd(); // }

cw.visitEnd(); // }

Метод callPrintln несложный, вот он:

private static void callPrintln(MethodVisitor mv, String string) {
    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    mv.visitLdcInsn(string);
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}

Сохраняем класс в файл:

Files.write(Paths.get(className+".class"), cw.toByteArray());

Отлично, класс генерируется и успешно запускается:

$ java 42
Hello World!

Теперь попытаемся его декомпилировать:

$ java -jar fernflower.jar 42.class dest
INFO:  Decompiling class 42
INFO:  ... done

Незадача, не упал. Посмотрим содержимое полученного файла:

public class 42 {
   private _2/* $FF was: 42*/() {
      System.out.println("In constructor!");
   }

   public static void main(String[] var0) {
      System.out.println("Hello World!");
   }
}

Непохоже, чтобы переименование вообще работало. Класс по-прежнему называется 42 и, конечно, не является правильным Java-классом. Более того, конструктор переименовался и вообще перестал быть конструктором. Конечно, хорошо, что декомпилятор не смог создать валидный Java-файл, но хотелось большего.

Может переименование можно как-то включить? Есть некоторые опции, которые описаны прямо в README.md [12]. И среди них опция ren:

  • ren (0): rename ambiguous (resp. obfuscated) classes and class elements

Ну-ка попробуем:

$ java -jar fernflower.jar -ren=1 42.class dest
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 2
        at java.lang.String.charAt(Unknown Source)
        at ...renamer.ConverterHelper.getNextClassName(ConverterHelper.java:58)
        at ...renamer.IdentifierConverter.renameClass(IdentifierConverter.java:187)
        at ...renamer.IdentifierConverter.renameAllClasses(IdentifierConverter.java:169)
        at ...renamer.IdentifierConverter.rename(IdentifierConverter.java:63)
        at ...main.Fernflower.decompileContext(Fernflower.java:46)
        at ...main.decompiler.ConsoleDecompiler.decompileContext(ConsoleDecompiler.java:135)
        at ...main.decompiler.ConsoleDecompiler.main(ConsoleDecompiler.java:96)

Бдыщь! Отлично, упали ровно там, где надо. Причём даже не особо вникая в исходный текст декомпилятора. Что интересно, если пользователь декомпилирует целый jar-файл, в котором попадётся такой класс, то падает вся декомпиляция до того как хоть один файл декомпилируется. И по сообщению совершенно неясно, из-за какого конкретно класса ошибка. Достаточно припаковать такой класс где-нибудь в глубине обфусцированного jar, и такой jar не декомпилировать. Да, к сожалению, надо запускать с опцией, отключенной по умолчанию, но другие механизмы обфускации могут сделать использование этой опции очень желанной.

Так как я работаю в компании, которая производит декомпилятор, а не обфускатор, то, конечно, вместо эксплуатации уязвимости я сообщил о ней [13], и её закрыли. А чтобы воспользоваться обновлённым статическим анализатором IDEA и найти подобные ошибки в своём коде, вы можете собрать IntelliJ Community Edition из исходников [14] или дождаться EAP-программы 2017.2. И не надо недооценивать статический анализ. Если вы не проанализируете свой код, это сделают конкуренты или злоумышленники и найдут там что-нибудь, что испортит вам жизнь.

Автор: Тагир Валеев

Источник [15]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/252730

Ссылки в тексте:

[1] PVS-Studio: https://habrahabr.ru/company/PVS-studio/

[2] в том числе: https://habrahabr.ru/company/pvs-studio/blog/324114/#comment_10124664

[3] репозитория: https://github.com/JetBrains/intellij-community/tree/master/plugins/java-decompiler

[4] зеркала: https://github.com/fesh0r/fernflower

[5] d706718: https://github.com/fesh0r/fernflower/commit/d706718b1b22dfe2e378bd06c21c2cd8a8b194c7

[6] ConverterHelper::getNextClassName: https://github.com/fesh0r/fernflower/blob/d706718b1b22dfe2e378bd06c21c2cd8a8b194c7/src/org/jetbrains/java/decompiler/modules/renamer/ConverterHelper.java#L62

[7] ClassesProcessor::new: https://github.com/fesh0r/fernflower/blob/d706718b1b22dfe2e378bd06c21c2cd8a8b194c7/src/org/jetbrains/java/decompiler/main/ClassesProcessor.java#L84

[8] IdentifierConverter::renameClass: https://github.com/fesh0r/fernflower/blob/d706718b1b22dfe2e378bd06c21c2cd8a8b194c7/src/org/jetbrains/java/decompiler/modules/renamer/IdentifierConverter.java#L187

[9] ConverterHelper::toBeRenamed: https://github.com/fesh0r/fernflower/blob/d706718b1b22dfe2e378bd06c21c2cd8a8b194c7/src/org/jetbrains/java/decompiler/modules/renamer/ConverterHelper.java#L42

[10] ASM: http://asm.ow2.org/

[11] иметь конструктор: https://habrahabr.ru/post/250029/#comment_8272049

[12] README.md: https://github.com/fesh0r/fernflower/blob/d706718b1b22dfe2e378bd06c21c2cd8a8b194c7/README.md#command-line-options

[13] сообщил о ней: https://youtrack.jetbrains.com/issue/IDEABKL-7547

[14] исходников: https://github.com/JetBrains/intellij-community

[15] Источник: https://habrahabr.ru/post/326384/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best