In the articles about PVS-Studio , they are increasingly talking about vulnerabilities and security defects 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 reporting a static analyzer to operating a found problem and obtaining some benefit. In my case, the benefit still remained theoretical, but it was possible to exploit the error, not really delving into the project code.
Imagine that you are developing an obfuscator for Java classes. Your business is to make it difficult to extract 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 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 Fernflower from JetBrains, which is part of IntelliJ IDEA. JetBrains doesn’t really care about distributing it separately, but it can be collected from source files by downloading IntelliJ Community Edition from the repository . It is even easier to pull off the unofficial mirror : there’s no need to pump out the entire IDEA. I'll take a recent commit d706718 . Fernflower is going to run ant, 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 [-<option>=<value>]* [<source>]+ <destination> Example: java -jar fernflower.jar -dgs=true c:\my\source\ c:\my.jar d:\decompiled\
After recent improvements, the static IDEA analyzer has grown wiser and started issuing 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 is:
Condition 'index == shortName.length ()' is always 'false' when reached
Such warnings about always true or always false condition are very interesting. Often they indicate a bug not in this condition, but in some other place above. Unaccustomed to even difficult to understand why such a conclusion was made. Here before the condition was a while loop, the exit condition in which contains shortName.charAt(index)
: get the character of the string at the index. It is significant that the index cannot be greater than or equal to the length of the string: otherwise, charAt
will be charAt
with the exception IndexOutOfBoundsException
. Thus, if the cycle has reached index == shortName.length()
, then we will not be able to get out of the cycle normally, and we are guaranteed to fall. And if you index == shortName.length()
loop normally, then the condition index == shortName.length()
really always false.
Next, you need to figure out whether an exception can occur or just an extra condition. In the framework of this method, this situation does not contradict anything, it suffices that the entire shortName
string shortName
of only digits. Great, it smells like a real bug. But can a line consisting of one numbers get into this method? We look at two points of calling this method: ClassesProcessor :: new and IdentifierConverter :: renameClass . In both cases, the name of a class without a package is passed as shortName
, 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 bit muddy, but it can be seen that it will work if the class name begins with a digit.
Apparently, this code is responsible for renaming classes if their name is valid for the virtual machine, but not valid for the Java language. Great, let's generate a correct class with a name from numbers. Take your favorite ASM and go ahead. It is desirable for a class to have a constructor . We 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, "<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(); // }
Well, to check that the class is really normal, let's make it main
with honest 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 callPrintln
method callPrintln
simple, 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 class to file:
Files.write(Paths.get(className+".class"), cw.toByteArray());
Great, the class is generated and starts successfully:
$ java 42 Hello World!
Now we will try to decompile it:
$ java -jar fernflower.jar 42.class dest INFO: Decompiling class 42 INFO: ... done
Bad luck, did not fall. Let's look at 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!"); } }
It’s unlikely that renaming works at all. The class is still called 42 and, of course, is not a valid Java class. Moreover, the designer was renamed and in general ceased to be a designer. Of course, it's good that the decompiler could not create a valid Java file, but I wanted more.
Maybe renaming can somehow be included? There are some options that are described directly in README.md . And among them is the ren
option:
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)
Bydisch! Ok, fell exactly where necessary. And not even really delving into the source code of the decompiler. What is interesting is that if a user decompiles an entire jar file in which such a class falls, then the whole decompilation falls before at least one file is decompiled. And according to the report, it is completely unclear because of what particular class the error is. It is enough to pack such a class somewhere deep in the obfuscated jar, and such a jar should not be decompiled. Yes, unfortunately, it is necessary to start with the option disabled by default, but other obfuscation mechanisms can make the use of this option very desirable.
Since I work for a company that produces a decompiler, not an obfuscator, then, of course, instead of exploiting the vulnerability, I reported it , and it was closed. And in order 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 do not underestimate the static analysis. If you do not analyze your code, competitors or intruders will do it and find there something that will ruin your life.
Source: https://habr.com/ru/post/326384/
All Articles