Static Analysis → Vulnerability → Profit

    Articles about PVS-Studio increasingly talk about vulnerabilities and security flaws that can be found using static analysis. The authors of these articles are criticized ( including myself ) that not every mistake is a security defect. However, an interesting question arises whether it is possible to go all the way from the message of a static analyzer to the operation of the problem found and obtaining any kind of benefit. In my case, the benefit still remained theoretical, but I managed to exploit the error without really delving into the project code.


    Imagine you are developing an obfuscator for Java classes. Your business is making it difficult to extract the source code from .class files, including using the decompilers available on the market. In addition to standard obfuscation techniques, it is quite reasonable to look for bugs in well-known decompilers and exploit them. If the popular decompiler just crashes on the code you generated, customers will be very happy.


    One of the popular decompilers is JetBrains Fernflower, which is part of IntelliJ IDEA. JetBrains doesn't really care about distributing it separately, but you can build it from source by downloading it from the IntelliJ Community Edition repository . It’s even easier to pull off an unofficial mirror : you don’t have to pump out the entire IDEA. I'll take the recent commit d706718 . It is assembled by Fernflower by running ant, it does not require external dependencies and produces fernflower.jar, which can be used as a command-line application:


    $ java -jar fernflower.jar
    Usage: java -jar fernflower.jar [-

    After fresh improvements, the IDEA static analyzer became wiser and began to issue a warning in the ConverterHelper :: getNextClassName method :


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

    The warning sounds like this:


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

    Such warnings about an always true or always false condition are very interesting. Often they indicate a bug not in this condition, but in some other place above. It’s hard to even figure out why this conclusion has been made. Here before the condition there was a while loop, the exit condition in which contains shortName.charAt(index): get the character of the string by index. It is significant that the index cannot be greater than or equal to the length of the string: otherwise it charAtwill drop out with an exception IndexOutOfBoundsException. Thus, if the cycle has reached index == shortName.length(), then we will not be able to exit the cycle normally, but we will fall guaranteedly. And if you exit the loop normally, then the condition is index == shortName.length()really always false.


    Next, you need to find out whether an exception can really occur or just an extra condition. Within the framework of this method, nothing contradicts such a situation, it is enough that the whole line shortNameconsists of only digits. Great, it smells like a real bug. But can a string consisting of only digits get into this method? We look at two call points for this method: ClassesProcessor :: new and IdentifierConverter :: renameClass . In both cases, the shortNamename of the class is passed without a package, which, according to the rules of the Java virtual machine, may well consist of numbers. And in both cases, this code is executed under the condition ConverterHelper :: toBeRenamed . The condition is a little muddy, but it’s clear that it will work if the class name starts with a digit.


    Apparently, this code is responsible for renaming classes if their name is valid for the virtual machine, but invalid for the Java language. Great, let's generate the correct class with a name from numbers. Take your favorite ASM and go. It is desirable for the class to have a constructor . Let's print something in it:


    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, "", "()V", null, null);
    // super();
    ctor.visitIntInsn(ALOAD, 0);
    ctor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false);
    // System.out.println("In constructor!");
    callPrintln(ctor, "In constructor!");
    // return;
    ctor.visitInsn(RETURN);
    ctor.visitMaxs(-1, -1);
    ctor.visitEnd(); // }

    Well, to check that the class is really normal, let's make it mainhonest 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(); // }

    The method is callPrintlnsimple, here it is:


    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);
    }

    Save the class to a file:


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

    Great, the class is generated and runs successfully:


    $ java 42
    Hello World!

    Now try to decompile it:


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

    Bad luck, did not fall. Let's see the contents of the resulting file:


    public class 42 {
       private _2/* $FF was: 42*/() {
          System.out.println("In constructor!");
       }
       public static void main(String[] var0) {
          System.out.println("Hello World!");
       }
    }

    Renaming doesn't seem to work at all. The class is still called 42 and, of course, is not a valid Java class. Moreover, the constructor was renamed and generally ceased to be a constructor. Of course, it’s good that the decompiler could not create a valid Java file, but I wanted more.


    Maybe you can somehow turn on the renaming? There are some options that are described directly in README.md . And among them the option ren:


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

    Well, let's try:


    $ 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)

    Whatever! Well, they fell exactly where necessary. And not even delving into the source code of the decompiler. Interestingly, if the user decompiles an entire jar file that contains such a class, then the entire decompilation crashes before at least one file is decompiled. And according to the message, it is completely unclear which class is the cause of the error. It is enough to pack such a class somewhere deep in the obfuscated jar, and not decompile such a jar. Yes, unfortunately, you need to start with the option disabled by default, but other obfuscation mechanisms can make using this option very desirable.


    Since I work for a company that produces a decompiler, not an obfuscator, of course, instead of exploiting a vulnerability, I reported it and closed it. And to use the updated IDEA static analyzer and find similar errors in your code, you can compile IntelliJ Community Edition from the sources or wait for the 2017.2 EAP program. And don't underestimate static analysis. If you do not analyze your code, competitors or cybercriminals will do it and find something there that ruins your life.


    Also popular now: