Back

Java Obfuscation Tutorial with Zelix Klassmaster

For high-level languages such as .NET or Java, it is quite trivial to reverse-engineer application binaries to recover the original source code if the application doesn’t employ any type of encryption or obfuscation. Some popular decompilers are: Dotpeek, Reflector, JustDecompile (.NET), Java Decompiler, Mocha, DJ Java Decompiler (Java). In a penetration test, program’s source code is valuable information that can be used to attack application and trusted resources. To help prevent the recovery of source code, obfuscation is often being used. Obfuscation is the intentional act of converting original source code to a form that is difficult for human to understand. The main goal of obfuscation is to provide “security through obscurity” in order to prevent attackers from:

  • Recovering original source code (Intellectual Property (IP) theft)
  • Tampering with business logic by modifying source code

Obfuscators (programs that help obfuscating source code), generally achieve its purposes by:

  • Name obfuscation: renaming classes, methods, packages, fields, etc.
  • String encryption
  • Code and Data Flow obfuscation: alternating program logic or adding gibberish instructions that may not be decompiled into valid source code
  • Inject Unnecessary Code
  • Remove debug information

This blog post aims to provide a simple example of Java Obfuscation. Hopefully it will be helpful for programmers wanting to protect their applications.

Choosing Java Obfuscator

If you just want to simply do name/data flow obfuscation for your Java program, free obfuscators such as ProGuard or yGuard will suit your needs. For stronger protection and extra capabilities such as String Encryption, Incremental Obfuscation, Stack Trace Transplantation, you may need to choose a commercial product. This article provides a great comparison among popular Java Obfuscators I am usually a proponent of open source programs; however for this tutorial I will be using Zelix KlassMaster to obfuscate a custom server/client Java program. In my opinion, it provides decent feature set for a reasonable price point. For complete feature listing visit Zelix Klassmaster

Example

Let’s take a look at this client/server program. On the Server side, we have following classes:

  • ServerRun.java: Main Program
  • Person.java, SearchObject.java,SearchResult.java: shared classes between client/server

Img E C Ddfb

Compiling server classes give us Java Archive file: Server.jar On the Client side, we have following classes:

  • ClientRun.java: Main Program
  • Person.java, SearchObject.java,SearchResult.java: shared classes between client/server

Img E Ca B

As mentioned above, it is trivial to recover original source code with a Decompiler Program such as Java Decompiler (JD-GUI). Here is what it looks like when decompiling Client.jar with JD-GUI:  Img E Dee D D

The original source code has been recovered with great accuracy. With this information, attackers can learn the business logic of the program or import decompiled project into an IDE, modify code flow to bypass program restrictions.

Bad Obfuscation Example

The most common mistake when dealing with obfuscation is to involve it later in the game, after the implementation phase, when the binaries are made and ready for testing. In this bad example below, Client.jar is obfuscated with Zelix KlassMaster with the following settings:

Img E Ea Dc

After obfuscation the result binary will have the following structure:

Img E Fb E

Decompiling it will result in an empty class file as shown below:

Img E D B

The command to run Client program will be the same as before: java -classpath Client.jar com.client.ClientRun However it will generate exception when trying to communicate with Server Program. The reason is: Server and Client programs share some common classes: Person, SearchObject and SearchResult. When Server program attempts to read com.commons.Person object, for example, in Client program it is transformed into com.client.a, causing the Exception in the screenshot below. For both of the programs to work, Client and Server programs must be using the exact same classes, including package name.

Img E Deaa

General Obfuscation Procedures

Below is any overview of common obfuscation procedures.

  1. Identify common public interfaces and methods that will be used either within the same program or by another components
  2. Identify serialized classes that will be shared among different components
  3. Refactor classes in step 1 and step 2 into separate project
  4. Compile them into Java Archive (.jar) file
  5. Obfuscate common libraries (optional)
  6. Import necessary library into relevant components
  7. Identify main entry points
  8. Obfuscate the rest of the code
  9. Test
  10. Maintenance

Obfuscation Example

  1. Identify common public interfaces and methods that will be used either within the same program or by another componentsRemote Procedure Call (RPC) Applications typically communicate with each other through some common interfaces. They can use those interfaces to invoke functions on another machine or network. Those interfaces must be made available to all involved components. Some examples:
    • RMI Interfaces
    • JBoss Remoting Interfaces
    • Apache XML RPC Interfaces
    • COBRA Interfaces

    This principle can also be applied for programs that provide Application Programmable Interface (API) for other software components.The example doesn’t have any of the above interfaces so this step can be ignored.

  2. Identify serialized classes that will be shared among different componentsSerialization allows programmers to save and restore state of objects. In case of transmitting objects between different components, serializable classes must be defined the same in all components. In Java, Serializable classes are easily recognized because they must implement Serializable interface. Example:public class Person implements Serializable. In this example, serializable classes are: Person, SearchObject and SearchResult
  3. Refactor aforementioned classes into separate projectsIn this step, the excluded classes/interfaces mentioned in step 1 and step 2 are refactored or moved into separate projects. It is also a good design practice to refactor common classes into new components, since it will eliminate redundancy, keep the code base clean and easy to maintain.In Eclipse, I created a new project called ServletCommons and import serializable classes in step 2:

    Img E A D A

  4. Compile them into Java Archive (.jar) librariesThe purpose of this step is to generate jar files that can later be imported into components that will reuse/extend them. The following command will generate servlet-commons.jar file for project ServletCommons. C:ServletCommonsbin> jar cvf servlet-commons.jar *.*
  5. Obfuscate common libraries (optional)If you follow good software design practice, step 1-4 may have already been done. It is now up to you to decide if these libraries should be obfuscated. If your software is well-designed, RPC Interfaces/ API should not publicly expose sensitive methods, and consequently need no obfuscation. The same principle applies for Serializable classes: they should not contain any application logic, other than object’s data. Obfuscate common libraries will provide greater protection, but with a cost. Your vanilla code base will now contain original source code and obfuscated libraries. It will probably add more overhead and introduce more efforts into your development and testing environment.
  6. Import libraries into relevant components, when necessaryNow you will need to import those libraries to projects that need them. This can be done with an IDE, ant or maven build script. You may also need to deploy them on the server, depending on your setup. The example in the screenshot below show how to do it in Eclipse:

    Img E B

    If you choose to obfuscate common libraries in step 5, this is the time to fix potential compilation errors due to class/package/method renaming.

  7. Identify main entry points, retain mapping of entry pointsFor complex applications that contain native code (C, C++) call out to Java component, the main entry points to Java components need to be clearly identified and kept consistent. For this reason you should keep track of the mapping between entry points and obfuscate code, and update components that are calling now-obfuscated entry points. For more information check out this documentation: https://www.zelix.com/klassmaster/docs/tutorials/incrementalObfuscation.html
  8. Import necessary libraries and obfuscateFirst you will need to set the classpath of the project. As mentioned in step 4, servlet-common.jar is required for both Client and Server program

    Img E E A

    The following settings are used to obfuscate Client Program. Note that I excluded public static void main(String[] args) as it is required to run any Java application:

    Img E Deb D

    Specify change log location:

    Img E F

    Save obfuscated classes into a new jar file:

    Img E Eeec F

    Decompiling it with JD-GUI:

    Img E De

    Review change log for the new names of entry points. Update all components that are using those entry points. Sample change log will look like: Class: public com.client.ClientRun => d Source: “ClientRun.java” Now move on to obfuscate Server program. To keep it simple, I opt to exclude servlet class name from obfuscation:

    Img E Aeb

    Save obfuscated classes into new jar file:

    Img E Faa

    Decompiling it with JD-GUI:

    Img E Bfc

  9. TestDeploy Server.jar and servlet-commons.jar to Tomcat. Run the client with the newly obfuscated entry point: java -classpath Client.jar;servlet-commons.jar d

    Img E D B

  10. MaintenanceTroubleshooting live applications: Zelix provides Stack Trace Translate tool to help with de-obfuscating stack traces and debugging log. For more information visit: https://www.zelix.com/klassmaster/featuresStackTraceTranslate.htmlIncremental Obfuscation: there are cases you may want to keep consistent mapping between your old code and new code. Some examples are software updates, patches or minor releases. It is good practice to keep change logs in safe place and reuse them if you want that consistency. For more information visit: https://www.zelix.com/klassmaster/docs/tutorials/incrementalObfuscation.html

Obfuscation, in general, makes reverse-engineering a time-consuming process for most attackers. Obfuscation cannot prevent other attack vectors such as bytecode manipulation or communication manipulations. To protect your application from such threats, consider implementing code signing in your application. Regarding Zelix Klassmaster itself, Security Researcher Yiannis Pavlosoglou has discovered that the its String Encryption routine is actually composing of five XOR operations, with encryption keys hard-coded in the class file. More information can be viewed here: https://www.defcon.org/images/defcon-15/dc15-presentations/dc-15-subere.pdf

References

Discover how the NetSPI BAS solution helps organizations validate the efficacy of existing security controls and understand their Security Posture and Readiness.

X