Skip to content
NeonOrbit edited this page Jun 10, 2023 · 5 revisions

Dexplore

With Dexplore, you can locate any obfuscated classes and methods in apk/dex/odex files. You have the option to either find classes at runtime to hook with Xposed or perform static searches using the Dexplore CLI tool.

Dependency

Add the dependency to your build.gradle file:

repositories {
    mavenCentral()
}
dependencies {
    implementation 'io.github.neonorbit:dexplore:1.4.5'
}

Search Sample

It's worth noting that Dexplore generates a Reference Pool for each class, which contains all the strings used within that class.
(Reference Pool actually holds other types of references as well, but we'll discuss that later.)

public class Example {
  public void find(String apkPath) {
    // Suppose we want to find a class that contains the string "Hello Dex".

    // Create a class filter to locate the desired class.
    ClassFilter classFilter = new ClassFilter.Builder()
            // To narrow down the search, limit it to string references only.
            .setReferenceTypes(ReferenceTypes.STRINGS_ONLY)
            // Check if the class contains the string "Hello Dex" in its reference pool.
            .setReferenceFilter(pool ->
                    pool.contains("Hello Dex")
            ).build();

    // Load the apk into Dexplore
    Dexplore dexplore = DexFactory.load(apkPath);

    // Perform a dex search
    MethodData result = dexplore.findClass(classFilter);

    // This is our target class
    if (result != null) Util.log("Found: " + result.clazz);  // Print the class name
  }
}

ReferencePool

Think of a ReferencePool as a container that holds all the References found in a dex class. These References primarily consist of string literals and identifiers (such as the names of classes, methods, and fields) used in the source code of that particular class.

For instance, this is a Sample java class:

public class Sample {
  static final String TAG = "AppInit";  // String literal
  Context context;
  public void init(Activiy activiy) {
    context = (Context) activiy;        // Type: Context
    if (Build.VERSION.SDK_INT >= 30) {  // Field: SDK_INT
      Util.log(getMessage());           // Method: log() and getMessage()
    }
  }
  public static String getMessage() { 
    return "Congratulations";           // String literal
  }
}

The ReferencePool object of the Sample class will contain the follwing references:

Field References:   SDK_INT
Method References:  log, getMessage
Type References:    android.content.Context
String References:  "AppInit", "Congratulations"

The ReferencePool object of the init method will contain the follwing references:

Field References:   SDK_INT
Method References:  log, getMessage
Type References:    android.content.Context
String References:  [EMPTY]

The ReferencePool object of the getMessage method will contain the follwing references:

Field References:   [EMPTY]
Method References:  [EMPTY]
Type References:    [EMPTY]
String References:  "Congratulations"

Dexplore creates a ReferencePool object for each class, making it easier to identify a class by examining its reference pool. This simplifies the process of finding classes based on the references they contain.

If you want to see the references of a class without having to decompile it, use the CLI tool:

java -jar Dexplore.jar search input.apk -cls com.app.Sample -pool a  # print all the references of a class
java -jar Dexplore.jar search input.apk -cls com.app.Sample -pool s  # print only the string references of a class
# Refer to the CommandLine section of this wiki for further details.

Find Classes

Now, let's search for an obfuscated class.
For example, we'll try to find a class that contains a specific string within its source code.

package xy.z;  // obfuscated package
public final class ObfClassAaa {  // obfuscated class
  public void A0X() {
    Processor.process("appx_unique_token");  // Assuming no other classes have this exact string.
    // ... ... ...
  }
}

Note: Our target class has a unique string in its ReferencePool.

Find the ObfClassAaa class:

public class Example {
  public void find(String apkPath) {
    // Create a ClassFilter to match the ObfClassAaa class
    ClassFilter classFilter = new ClassFilter.Builder()
            // Since we will check String references only
            .setReferenceTypes(ReferenceTypes.STRINGS_ONLY)
            // Check whether the reference pool contains the unique string
            .setReferenceFilter(pool -> pool.stringsContain("appx_unique_token"))
            // Search in public final classes only (our target class is 'public final')
            .setModifiers(Modifier.PUBLIC | Modifier.FINAL)
            // It doesn't have any interfaces (skip all classes with interfaces)
            .setInterfaces(Collections.emptyList())
            // It doesn't have an explicit super class, which means 'Object' is the super class.
            .setSuperClass(Object.class.getName())
            // To make the filter more precise, you can specify additional conditions.
            .build();

    // Load the apk into Dexplore
    Dexplore dexplore = DexFactory.load(apkPath);

    // We will talk about 'DexFilter' later
    ClassData result = dexplore.findClass(DexFilter.MATCH_ALL, classFilter);

    // This is our target class
    if (result != null) Util.log("Found: " + result.clazz);  // In this case: xy.z.ObfClassAaa


    // -------------------------- Additional -------------------------- //

    // If you want to find all the classes that match the filter
    List<ClassData> results = dexplore.findClasses(DexFilter.MATCH_ALL, classFilter, -1);  // find all

    // If you want to ensure that only one class has the string "appx_unique_token".
    List<ClassData> results = dexplore.findClasses(DexFilter.MATCH_ALL, classFilter, 2);  // find the first 2 matches
    if (results.size() > 1) Util.log("Not unique");  // not unique
  }
}

ClassFilter API:

Note: The filter will match if and only if all the specified conditions are satisfied.

Find Methods

Consider our obfuscated class has a method that looks like BtZ(),

package xy.z;
public final class ObfClassAaa {  // obfuscated class
  public void A0X() {
    Processor.process("appx_unique_token");  // Assuming no other classes have this exact string.
  }
  public static int BtZ(ActivityManager am, boolean flag) {  // obfuscated method
    if (am.isLowRamDevice()) {  // A method reference: 'isLowRamDevice'
      return Something.get();
    }
    return 0;
  }
}

Note: Our target method has a method reference 'isLowRamDevice' and the class also has a unique string.

Let's find the BtZ() method:

public class Example {
  public void find(String apkPath) {
    // Create a ClassFilter to match ObfClassAaa
    ClassFilter classFilter = new ClassFilter.Builder()
            // Check in string and method references from the reference pool
            .setReferenceTypes(ReferenceTypes.builder().addString().addMethod().build())
            .setReferenceFilter(pool ->
                    // Check whether the reference pool contains the unique string
                    pool.stringsContain("appx_unique_token") &&
                    // Check whether it contains 'isLowRamDevice' method reference too (to search more accurately)
                    pool.methodsContain("isLowRamDevice")
            )
            // Search in public final classes only (our class is 'public final')
            .setModifiers(Modifier.PUBLIC | Modifier.FINAL)
            // It doesn't have any interfaces (skip all classes with interface)
            .setInterfaces(Collections.emptyList())
            // It doesn't have an explicit super class, which means 'Object' is the super class.
            .setSuperClass(Object.class.getName())
            // Build
            .build();

    // Create a MethodFilter to match BtZ() method
    MethodFilter methodFilter = new MethodFilter.Builder()
            // Check in method references only
            .setReferenceTypes(ReferenceTypes.builder().addMethod().build())
            // Check whether the target method contains this reference
            .setReferenceFilter(pool -> pool.contains("isLowRamDevice"))
            // Method return type is "int"
            .setReturnType(int.class.getName())
            // We know the parameters of BtZ()
            .setParamList(Arrays.asList("android.app.ActivityManager", "boolean"))
            // OR set parameter size instead (in case parameters are also obfuscated)
            .setParamSize(2)
            // Search in public static methods only (our method is 'public static')
            .setModifiers(Modifier.PUBLIC | Modifier.STATIC)
            // Build
            .build();
    //  If you are confused, take a look at ReferencePool section again.

    // Load the apk into Dexplore
    Dexplore dexplore = DexFactory.load(apkPath);

    // Search method
    MethodData result = dexplore.findMethod(DexFilter.MATCH_ALL, classFilter, methodFilter);

    // This is our target method
    if (result != null) {
      Util.log("Method: " + result.method);  // In this case: BtZ
      Util.log("Declaring class: " + result.clazz);  // xy.z.ObfClassAaa
      Util.log("Parameters: " + result.params[0] + result.params[1]);
    }

    // Read javadocs of all filter methods
  }
}

MethodFilter API:

Build DexFilter

DexFilter example:

public class Example {
  public void find(String apkPath) {
    // If the input apk has multiple dex files, we can provide a dex filter to speed up the search
    DexFilter dexFilter = new DexFilter.Builder()
            // Check string references only
            .setReferenceTypes(ReferenceTypes.STRINGS_ONLY)
            // Analyze only the dex file that contains this string in its reference pool.
            // Checking whether a dex file contains a reference is much faster than checking each classes.
            .setReferenceFilter(ReferenceFilter.contains("appx_unique_token"))
            // If we know which dex file contains the class, we can specify it. (it doesn't change often)
            // Setting a preferred dex file will analyze it first. 
            .setPreferredDexNames("classes4.dex") // Analyze classes4.dex first.
            // Build
            .build();

    // Additionally, we can specify to load root dex files only. (classes.dex, classes2.dex, ...)
    // This will avoid verifying (dex or not) thousands of non-dex file from the apk. 
    DexOptions options = new DexOptions();
    options.rootDexOnly = true;
    DexFactory.load(apkPath, options).findClass(dexFilter, classFilter);
  }
}

DexFilter API:

Advanced Search

What if the class we are trying to find doesn't have any unique properties?
In that case, we need to find out all the usages of that class manually. If another unique class uses our target class, we will find that unique class first and then extract the target class.

Say we are trying to find a class named ObfClassBBB, which doesn't have any unique properties. But another obfuscated class ObfClassAaa that uses our target class does have unique properties.

package xy.z;
public class ObfClassAaa { // a random obfuscated class
  public ObfClassAaa(ObfClassBBB param) {  // ObfClassBBB: our target class
    // ...
  }
  public void A0X() {
    Processor.process("appx_unique_token");
    ObfClassBBB obj = new ObfClassBBB("id", 1);  // ObfClassBBB() our target class constructor
  }
}

Let's find it:

public class Example {
  public void find(String apkPath) {
    // Our goal is to find ObfClassBBB, but it doesn't have any unique properties.
    // So, let's find the ObfClassAaa first.

    Dexplore dexplore = DexFactory.load(apkPath);

    // Create a ClassFilter to find ObfClassAaa
    ClassFilter classFilter = new ClassFilter.Builder()
            .setReferenceTypes(ReferenceTypes.STRINGS_ONLY)
            .setReferenceFilter(pool -> pool.stringsContain("appx_unique_token"))
            .build();

    // helperClass: ObfClassAaa 
    ClassData helperClass = dexplore.findClass(DexFilter.MATCH_ALL, classFilter);
    if (helperClass == null) return; // failed

    // First approach, the parameter of ObfClassAaa constructor is the target class (ObfClassBBB)
    List<MethodData> constructors = helperClass.getConstructors() // find the constructor 
            .stream().filter(m ->
                    m.params.length == 1  // in case there are multiple constructors
            ).collect(Collectors.toList());
    if (constructors.size() != 1) return; // further confirmation

    // target: ObfClassBBB
    Util.log("Result: " + constructors.get(0).params[0]);  // the parameter of the constructor is the target class (ObfClassBBB)

    // ------------------------------------------------------------------------------------- //

    // Alternative approach: (This one is little confusing, read carefully)
    // A0X() method creates an instance of ObfClassBBB class, 
    // which means A0X() ReferencePool contains the constructor of ObfClassBBB.

    // A0X() is also obfuscated, so extract the A0X() method by checking its reference pool
    MethodData A0X = helperClass.getMethods()
            .stream().filter(m ->
                    m.getReferencePool().contains("appx_unique_token") // A0X() contains this string
            ).findFirst().orElse(null);
    if (A0X == null) return; // failed

    // Now extract the target class (ObfClassBBB) from the reference pool of A0X()

    // new ObfClassBBB("id", 1);   
    // This is a constructor, which means A0X() ReferencePool contains it in the form of a method reference.

    // Extract the method reference of the constructor first.
    List<MethodRefData> mRefs = A0X.getReferencePool() // ReferencePool of A0X() method
            .getConstructorSection().stream()  // find in method references (constructors only)
            .filter(ref ->
                    // match only if it has 'String' and 'int' parameters. (target class constructor has these two params)
                    ref.getParameterTypes().equals(Arrays.asList("java.lang.String", "int"))
            ).collect(Collectors.toList());
    // let's hope there is only one constructor with such signature
    if (mRefs.size() != 1) return; // assumption failed

    // Target: ObfClassBBB
    // Declaring class of the constructor 'ObfClassBBB()' is the target class 'ObfClassBBB'
    Util.log("Result: " + mRefs.get(0).getDeclaringClass());  
  }
}

This is just an example. You can find your target class in so many creative ways.

Batch Operation

public class Example {
  public void find(String apkPath) {
    // Create a batch of queries
    QueryBatch batch = new QueryBatch.Builder()
            // A class query with 'item1' id
            // (id is just an arbitrary string to identify it later)
            .addClassQuery("item1", dexFilter1, classFilter1)
            // Another class query with 'item2' id
            .addClassQuery("item2", dexFilter2, classFilter2)
            // A method query with 'itemX' id
            .addMethodQuery("itemX", dexFilter3, classFilter3, methodFilter3)
            // To speed up the process
            .setParallel(true)
            // Build
            .build();

    Dexplore dexplore = DexFactory.load(apkPath);

    // Example #1: Store the first result from each query
    Map<String> results = new ConcurrentHashMap<>();  // for thread safety (since parallel flag is enabled)
    dexplore.onQueryResult(batch, (key, result) -> {
      results.put(key, result);  // 'key' is the unique id used to create the query.
      // If you only need the first result from each query, return true for each callback.
      return true; // stop further search for the given key.
    });

    // Example #2: Examine each result manually
    dexplore.onQueryResult(batch, (key, result) -> {
      if (key.equals("item1")) {  // a result from the 'item1' query
        ClassData cls = (ClassData) result;  // 'item1' was a class query
        if (cls /* is your desired result? */) {
          store = cls; // store it
          return true; // found?  stop further search for 'item1'
        } else {
          return false; // not the result you expected? continue searching for 'item1'
        }
        // Handle others ...
        // ................
      }
    });
  }
}

Xposed Samples

public class XposedModule implements IXposedHookLoadPackage {
  @Override
  public void handleLoadPackage(XC_LoadPackage.LoadPackageParam param) {
    if (!param.packageName.equals("com.pkg.app")) return;

    // First, find all the necessary classes/methods using Dexplore.
    String apk = param.appInfo.sourceDir;
    Dexplore dexplore = DexFactory.load(apk);
    MethodData result = dexplore.findMethod(/* query */); // for example: find an obfuscated method

    // Write it to Preferences
    preferences.edit()
            .putString("obfMethod", result.serialize()) // the obfuscated method
            .putLong("appVersion", currentVersionCode) // app version code (NOT version name).
            .apply();

    // Load the method
    ClassLoader cl = param.classLoader;
    // Either load directly (easy)
    Method method = result.loadMethod(cl);
    // Or load with xposed (if you want)
    XposedHelpers.findMethodExact(result.clazz, cl, result.method, (Object[]) result.params);
    // Hook with xposed
    XposedBridge.hookMethod(method /* , hook */);
    // ------------------------ . ------------------------ //

    // Next time, retrieve it from Preferences
    long code = preferences.getLong("appVersion", 0);
    if (code == currentVersionCode) {
      String raw = preferences.getString("obfMethod", null);
      MethodData retrieved = MethodData.deserialize(raw);  // deserialize
      // Load and hook with xposed
      // .........
    } else {
      // App version code changed, find your necessary classes/methods again
      dexplore.findMethod(/* query */); // find again using the same query
      // Save new data to Preferences and ...................
    }
  }
}

CommandLine

Requires: JDK 8+

Available Commands:

  • s,search : Search classes and methods
  • d,decode : Decompile java, smali and resource files
  • m,mapver : Map classes from one version to another

Usage details:

java -jar Dexplore.jar --help
java -jar Dexplore.jar --help search
java -jar Dexplore.jar --help decode
java -jar Dexplore.jar --help mapver

Let's create an alias for convenience,

alias Dexplore='java -jar Dexplore-1.4.5.jar'

Static Analysis

Usage:
java -jar Dexplore.jar --help search

Find classes:

Dexplore search input.apk --mode c --sources 'AClassName.java' 'AnotherClassName.java'
Dexplore search input.apk --mode c --cls-names 'AClassName' 'AnotherClassName'
Dexplore search input.apk --mode c --classes 'com.any.AClassName' 'com.any.AnotherClassName'
Dexplore search input.apk --mode c --numbers '101' '201' '301.0f' '401.5d'
Dexplore search input.apk --mode c --ref-type s  --references 'A unique string'
Dexplore search input.apk --mode c --ref-type s  --references 'A unique string' 'AnotherString'
Dexplore search input.apk --mode c --ref-type sm --references 'A unique string' 'aMethodRefName'
Dexplore search input.apk --mode c --ref-type f  --signatures 'java.lang.Byte.SIZE:int'  # field signature
Dexplore search input.apk --mode c --ref-type m  --signatures 'com.util.Time.setNow(int,java.lang.String,int):int'
# Advanced Search
# Format: 'm:public+..., s:superclass, i:interface+..., a:annotation+...'
Dexplore search input.apk --mode c --class-advanced 'm:public+final, s:java.lang.Object, i:com.any.AnInterface, a:com.any.SomeAnnotation'

Find methods:

Dexplore search input.apk --mode m --ref-type s --references 'A_unique_string'
# Advanced Search
# Format: 'm:public+..., n:name+..., p:param+..., r:return, a:annot+..., z:paramSize'
Dexplore search input.apk --mode c --class-advanced 'm:public+final, n:AMethodName, p:boolean+java.lang.String, a:com.any.SomeAnnotation'

Find all the matching classes from multiple versions of an app:

Dexplore search input-v1.apk input-v2.apk input-v3.apk --mode c --ref-type s --references 'A_unique_string'

Print the references of classes from the search result:

Dexplore search input.apk --classes 'java.lang.String' --print-pool a   # Print all references
Dexplore search input.apk --classes 'java.lang.String' --print-pool sf  # Print only string and field references
Dexplore search input.apk --mode c --ref-type s --ref 'A_unique_string' --print-pool a  # Print references from the search results

Print the references of methods from the search result:

Dexplore search input.apk --mode m --ref-type s --ref 'A_unique_string' --print-pool a

Generate java and smali source files of classes from the search result:

Dexplore search input.apk --mode c --ref-type s -ref 'A_unique_string' -gen
Dexplore search input.apk -cls 'app.pkg.ClassA' 'app.pkg.ClassB' 'app.pkg.ClassC' -gen

Full Decompiler

Usage:
java -jar Dexplore.jar --help decode

Decompile an app:

Dexplore decode input.apk            # Default: --mode j
Dexplore decode input.apk --mode j   # Decompile java sources
Dexplore decode input.apk --mode s   # Decompile smali sources
Dexplore decode input.apk --mode r   # Decompile manifest and resources
Dexplore decode input.apk --mode js  # Decompile java and smali files
Dexplore decode input.apk --mode jr  # Decompile java and resource files

Decompile partially:

Dexplore decode input.apk  -cls 'ClassA' 'ClassB'  # Decompile a list of classes by names
Dexplore decode input.apk  -cls 'app.pkg.ClassA' 'app.pkg.ClassB'  # Decompile a list of classes
Dexplore decode input.apk  -pkg 'app.pkg.ds.any' 'app.pkg.d.util'  # Decompile a list of packages
Dexplore decode input.apk --mode r -res 'manifest'                 # Decompile android manifest
Dexplore decode input.apk --mode r -res 'values' 'drawable'        # Decompile specific resources

Decompiler tweaks:

Dexplore decode input.apk  -eps     # Enable pause capability (ENTER key to pause/resume)
Dexplore decode input.apk  -dmem    # Disable In-Memory cache
Dexplore decode input.apk  -dren    # Disable class names renaming
Dexplore decode input.apk --jobs 4  # The number of threads to use (for better performance)

Troubleshooting

Out of memory issue:

  • Solution-1: Reduce the number of threads

    java -jar Dexplore.jar decode input.apk --jobs 1    # 1 Thread
    java -jar Dexplore.jar decode input.apk --jobs 2    # 2 Threads
  • Solution-2: Increase the maximum RAM for JVM

    java -jar -Xmx6g  Dexplore.jar decode input.apk     # 6GB Memory
    java -jar -Xmx8g  Dexplore.jar decode input.apk     # 8GB Memory