简体中文 / [English]


Hooking Obfuscated Apps Using Xposed

 

This article is currently an experimental machine translation and may contain errors. If anything is unclear, please refer to the original Chinese version. I am continuously working to improve the translation.

Nowadays, most apps use obfuscation to prevent reverse engineering. However, a few apps still use code protection (app hardening) techniques to increase the difficulty of analysis.

Deobfuscation (or “unpacking”) isn’t actually that hard — you can achieve it using Xposed or even a self-compiled Android ROM.

However, when using Xposed to hook such hardened apps, there’s one extra step required: obtaining the real ClassLoader.

I came across an excellent article online that clearly explains this issue. I’m saving it here for future reference.

The following content is reposted from: https://curz0n.github.io/2018/07/10/xposed_find_classloader/

Using Xposed to Obtain the Real ClassLoader: Common Use Cases

When using Xposed to instrument code and analyze app behavior, under normal circumstances you can simply use the system’s default classloader to meet your needs. However, in special cases — such as when an app is hardened or uses dynamic dex loading — directly using the default classloader for hooking will often result in a ClassNotFoundException. Below is a summary of several common scenarios for obtaining the actual classloader associated with a class, for future reference.

0x01 Using 360 Protection

After being protected by 360, an app’s classloader is replaced with 360’s custom loader. Therefore, during hooking, you must replace the classloader with 360’s version.

Code Characteristics

When you decompile a 360-protected app, the dex file contains only a few classes. The most important one is usually named StubAppxxxx (where “xxxx” is a sequence of digits). Inside this class, you can hook the getNewAppInstance method to obtain the Context parameter. From this context, you can retrieve 360’s classloader. After that, simply use this classloader to perform further hooking operations successfully.

imgimg

Example Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Hook 360 wrapper
XposedHelpers.findAndHookMethod("com.qihoo.util.StubApp579459766", loadPackageParam.classLoader,"getNewAppInstance", Context.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// Obtain the Context object from 360, then get the classloader from it
Context context = (Context) param.args[0];
// Retrieve 360's classloader; use this loader to hook into the actual protected app code
ClassLoader classLoader = context.getClassLoader();
// Replace classloader and hook into the real application logic
XposedHelpers.findAndHookMethod("xxx.xxx.xxx.xxx", classLoader, "xxx", String.class, String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Log.d(TAG, "==>0 " + param.args[0].toString());
Log.d(TAG, "==>1 " + param.args[1].toString());
}
});
}
});

0x02 Using Baidu Protection

The principle is the same as with 360 — the classloader is replaced, so you need to substitute Xposed’s default loader with Baidu’s.

Code Characteristics

For Baidu-protected apps, decompilation only reveals the outer shell code. The package name typically starts with com.baidu.xxx. You can hook the StubApplication.onCreate method to obtain the real classloader, then use it to hook into the actual business logic.

imgimg

Example Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Hook Baidu wrapper
XposedHelpers.findAndHookMethod("com.baidu.protect.StubApplication", loadPackageParam.classLoader, "onCreate", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Application appClz = (Application) param.thisObject;
// After onCreate completes, get the classloader from the application instance
ClassLoader loader = appClz.getClassLoader();
// Replace classloader and hook into the real application code
XposedHelpers.findAndHookMethod("xxx.xxx.xxx", loader, "xxx", String.class, String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.d(TAG, "==>0 " + param.args[0].toString());
Log.d(TAG, "==>1 " + param.args[1].toString());
}
});
}
});

0x03 Using Other Protection Schemes

The idea remains the same: before hooking the target code, you must first obtain the real classloader responsible for loading it. This is typically done by hooking the app’s entry point — either attachBaseContext or onCreate — then retrieving the Context parameter (in before) or the result object (in after), and casting it to obtain the classloader.

Code Characteristics

After decompiling a hardened app, you usually only see a few shell classes. The core logic is hidden. Typical signs of hardening include:

imgimg

Example Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Retrieve classloader from protected app; usually start analysis at attachBaseContext or onCreate
XposedHelpers.findAndHookMethod("com.shell.SuperApplication", lpparam.classLoader, "attachBaseContext",Context.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// Get the shell's Context object and retrieve its classloader
Context context = (Context) param.args[0];
// Get classloader from Context
ClassLoader classLoader = context.getClassLoader();
// Replace classloader and hook into real application code
XposedHelpers.findAndHookMethod("xxx.xxx.xxx", classLoader, "xxx", String.class,String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.d(TAG, "==>0 " + param.args[0].toString());
Log.d(TAG, "==>1 " + param.args[1].toString());
}
});
}
});

0x04 Using DexClassLoader for Dynamic Loading of JAR/APK

During app reverse engineering, you may encounter apps that use dynamic plugin loading. Typically, the app downloads the latest plugin package (usually a JAR file) from a server upon startup. Then, when reaching specific functionality, it uses DexClassLoader to load the dex file inside the JAR/APK and execute its bytecode.

Code Characteristics

When DexClassLoader is used to dynamically load dex, the three arguments passed to the loadParser method in the image below are:

  • arg5: Context
  • arg6: Absolute path to the plugin JAR file
  • arg7: Full class name to be loaded from the JAR

imgimg

The DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) constructor has four parameters:

  • dexPath: Path to the JAR/APK containing the target classes
  • optimizedDirectory: Directory where the extracted dex files are stored
  • libraryPath: Directory for native libraries (C/C++) used by the target classes
  • parent: Parent classloader

Example Code

When dynamically loading plugins via a classloader, the process eventually calls java.lang.ClassLoader.loadClass to load the actual bytecode. Therefore, you can hook the system’s loadClass method to obtain the real classloader. (In theory, this approach also works for hardened apps to retrieve the correct classloader.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
// Hook the system classloader to obtain the real classloader
XposedHelpers.findAndHookMethod("java.lang.ClassLoader", loadPackageParam.classLoader, "loadClass", String.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// Optionally filter by the target class name
// if("xxx.xxx.xx.xx".equals(param.args[0])){
// 1. Get the current object from Xposed's thisObject and cast to ClassLoader
// ClassLoader loader = (ClassLoader) param.thisObject;
// 2. Or get the loaded Class object first, then retrieve its classloader
Class clz = (Class) param.getResult();
ClassLoader loader = clz.getClassLoader();
// Use the obtained classloader to hook methods inside the JAR/APK
XposedHelpers.findAndHookMethod("xxx.xx.xx.xxx", loader, "xxx", String.class, new XC_MethodHook() {

@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.d(TAG, "jar hook success...");
Log.d(TAG, param.args[0].toString());
}
});
// }
}
});
}

0x05 Other Cases of Dynamic Loading

In the previous example, after loadClass is called, the app proceeds to newInstance to instantiate an object, calls a method on it, and returns the result (e.g., a UrlParser object). Since the returned object cannot be cast to ClassLoader, we need to hook loadClass directly.

However, in some apps using plugin architectures, the custom classloader might be created and used within a specific method. In such cases, simply hook that method and retrieve the classloader in the afterHookedMethod block. (This is similar to retrieving the classloader from the outer shell in hardened apps.)

References:

Android Dynamic Loading — DexClassLoader Analysis
Android Plugin System Exploration (1): Class Loader DexClassLoader
ClassLoader Source Code
Using Xposed to Hook Apps Protected by 360

This article is licensed under the CC BY-NC-SA 4.0 license.

Author: lyc8503, Article link: https://blog.lyc8503.net/en/post/xposed-hook-packed/
If this article was helpful or interesting to you, consider buy me a coffee¬_¬
Feel free to comment in English below o/