A recent internal thread about detecting hooking frameworks in native code (C/C++) got me thinking about the different ways that a Java Android application can detect the presence of either Cydia Substrate or the Xposed framework.

Disclaimer: All of these anti-hooking techniques are easy to bypass by any experienced reverse engineer. I’m just exploring how one might go about detecting that their Java application has been hooked using Substrate or the Xposed framework because at some point we will need to be able to bypass these techniques to do our jobs just like how we bypass root detection on a daily basis. The last time I looked at DexGuard and Arxan’s Java protection product (GuardIT) they did not support detection of either hooking framework. I would expect similar anti-hooking techniques will be added to these Java obfuscation/protection products in the future.

Stab 1: Check what is installed on the device.

The first thought that comes to mind is simply detecting whether or not Substrate or the Xposed framework is installed on the device. We can ask the PackageManager for a list of installed packages and flag any suspicious packages, which is a common technique used in root detection.

PackageManager packageManager = context.getPackageManager();
List applicationInfoList  = packageManager.getInstalledApplications(PackageManager.GET_META_DATA);
		
for(ApplicationInfo applicationInfo : applicationInfoList) {
	if(applicationInfo.packageName.equals("de.robv.android.xposed.installer")) {
		Log.wtf("HookDetection", "Xposed found on the system.");
	}
	if(applicationInfo.packageName.equals("com.saurik.substrate")) {
		Log.wtf("HookDetection", "Substrate found on the system.");
	}
}

Stab 2: Check the stack trace for suspicious method calls.

The next technique that I thought of was inspecting stack traces for suspicious method calls. Consider the following Java class that contains a method that throws an exception, catches it, and then prints out the stack trace.

public class DoStuff {
	public static String getSecret() {
		try {
			throw new Exception("blah");
		}
		catch(Exception e) {
			for(StackTraceElement stackTraceElement : e.getStackTrace()) {
				Log.wtf("HookDetection", stackTraceElement.getClassName() + "->" + stackTraceElement.getMethodName());
			}
		}
		return "ChangeMePls!!!";
	}
}

Assuming that our application is not hooked, the following stack trace is produced.

com.example.hookdetection.DoStuff->getSecret
com.example.hookdetection.MainActivity->onCreate
android.app.Activity->performCreate
android.app.Instrumentation->callActivityOnCreate
android.app.ActivityThread->performLaunchActivity
android.app.ActivityThread->handleLaunchActivity
android.app.ActivityThread->access$800
android.app.ActivityThread$H->handleMessage
android.os.Handler->dispatchMessage
android.os.Looper->loop
android.app.ActivityThread->main
java.lang.reflect.Method->invokeNative
java.lang.reflect.Method->invoke
com.android.internal.os.ZygoteInit$MethodAndArgsCaller->run
com.android.internal.os.ZygoteInit->main
dalvik.system.NativeStart->main

Assuming that the Xposed framework is used to hook the com.example.hookdetection.DoStuff.getSecret method, the following stack trace is produced. There are a few abnormal method calls worth noting.

  • When the Xposed framework is active you will notice a call to the de.robv.android.xposed.XposedBridge.main method after the dalvik.system.NativeStart.main method.
  • When the Xposed framework hooks a specific method included in the stack trace you will notice calls to the de.robv.android.xposed.XposedBridge.handleHookedMethod and de.robv.android.xposed.XposedBridge.invokeOriginalMethodNative methods.
  • The hooked method appears twice in the stack trace.
com.example.hookdetection.DoStuff->getSecret
de.robv.android.xposed.XposedBridge->invokeOriginalMethodNative
de.robv.android.xposed.XposedBridge->handleHookedMethod
com.example.hookdetection.DoStuff->getSecret
com.example.hookdetection.MainActivity->onCreate
android.app.Activity->performCreate
android.app.Instrumentation->callActivityOnCreate
android.app.ActivityThread->performLaunchActivity
android.app.ActivityThread->handleLaunchActivity
android.app.ActivityThread->access$800
android.app.ActivityThread$H->handleMessage
android.os.Handler->dispatchMessage
android.os.Looper->loop
android.app.ActivityThread->main
java.lang.reflect.Method->invokeNative
java.lang.reflect.Method->invoke
com.android.internal.os.ZygoteInit$MethodAndArgsCaller->run
com.android.internal.os.ZygoteInit->main
de.robv.android.xposed.XposedBridge->main
dalvik.system.NativeStart->main

Ok, now lets look at the stack trace when Substrate is used to hook the com.example.hookdetection.DoStuff.getSecret method. Again, there are some abnormal method calls worth noting.

  • When Substrate is active you will notice two calls to the com.android.internal.os.ZygoteInit.main method after the dalvik.system.NativeStart.main method as opposed to just one call.
  • When Substrate hooks a specific method included in the stack trace you will notice calls to the com.saurik.substrate.MS$2.invoked method, com.saurik.substrate.MS$MethodPointer.invoke method, and a method call associated with the Substrate extension (com.cigital.freak.Freak$1$1.invoked in this case).
  • The hooked method appears twice in the stack trace.
com.example.hookdetection.DoStuff->getSecret
com.saurik.substrate._MS$MethodPointer->invoke
com.saurik.substrate.MS$MethodPointer->invoke
com.cigital.freak.Freak$1$1->invoked
com.saurik.substrate.MS$2->invoked
com.example.hookdetection.DoStuff->getSecret
com.example.hookdetection.MainActivity->onCreate
android.app.Activity->performCreate
android.app.Instrumentation->callActivityOnCreate
android.app.ActivityThread->performLaunchActivity
android.app.ActivityThread->handleLaunchActivity
android.app.ActivityThread->access$800
android.app.ActivityThread$H->handleMessage
android.os.Handler->dispatchMessage
android.os.Looper->loop
android.app.ActivityThread->main
java.lang.reflect.Method->invokeNative
java.lang.reflect.Method->invoke
com.android.internal.os.ZygoteInit$MethodAndArgsCaller->run
com.android.internal.os.ZygoteInit->main
com.android.internal.os.ZygoteInit->main
dalvik.system.NativeStart->main

Now that we know the difference between a normal stack trace and a stack trace of a hooked Android application, we can write some Java code that can detect Substrate or the Xposed framework based on a stack trace.

try {
	throw new Exception("blah");
}
catch(Exception e) {
	int zygoteInitCallCount = 0;
	for(StackTraceElement stackTraceElement : e.getStackTrace()) {
		if(stackTraceElement.getClassName().equals("com.android.internal.os.ZygoteInit")) {
			zygoteInitCallCount++;
			if(zygoteInitCallCount == 2) {
				Log.wtf("HookDetection", "Substrate is active on the device.");
			}
		}
		if(stackTraceElement.getClassName().equals("com.saurik.substrate.MS$2") && 
				stackTraceElement.getMethodName().equals("invoked")) {
			Log.wtf("HookDetection", "A method on the stack trace has been hooked using Substrate.");
		}
		if(stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") && 
				stackTraceElement.getMethodName().equals("main")) {
			Log.wtf("HookDetection", "Xposed is active on the device.");
		}
		if(stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") && 
				stackTraceElement.getMethodName().equals("handleHookedMethod")) {
			Log.wtf("HookDetection", "A method on the stack trace has been hooked using Xposed.");
		}

	}
}

Stab 3: Check for native methods that shouldn’t be native.

The Xposed framework works by changing the method type of hooked methods to “native” and replacing the method within its own code (calls hookedMethodCallback instead). Check out the XposedBridge_hookMethodNative function, which is part of the modified app_process, to see how this works.

Given that the Xposed framework, and Substrate which works in a somewhat similar fashion, changes the properties of the hooked method, we can use this to our advantage to detect the presence of hooking. Note that this hooking detection technique will not work for the ART version of Xposed since altering the method type to native is unnecessary.

Consider the following class, which declares a single public method.

public class DoStuff {
	public static String getSecret() {
		return "ChangeMePls!!!";
	}
}

Assuming that the getSecret method is hooked, at runtime the class definition actually looks like the following.

public class DoStuff {
        // calls hookedMethodCallback if hooked using Xposed
	public native static String getSecret(); 
}

So given this abnormal behavior we can do the following to detect hooking within our own package.

  • Find the location of our application’s DEX file.
  • Enumerate all the classes within the DEX file.
  • For each class in the DEX file, use reflection to check for the existence of native methods that shouldn’t be native.

The following Java code demonstrates this technique. Note that we are assuming that the Android classes that we have written do not use native methods to invoke code in shared objects via JNI, which is the case for the vast majority of Android applications. If your application utilizes JNI then those native methods could be whitelisted. In theory we could extend this technique to check for hooks into core Java methods or third party library methods, but that would require mapping out which third party methods are implemented natively to avoid false positives (automate this step).

	
for (ApplicationInfo applicationInfo : applicationInfoList) {
	if (applicationInfo.processName.equals("com.example.hookdetection")) {		
		Set classes = new HashSet();
		DexFile dex;
		try {
			dex = new DexFile(applicationInfo.sourceDir);
			Enumeration entries = dex.entries();
			while(entries.hasMoreElements()) {
				String entry = entries.nextElement();
				classes.add(entry);
			}
			dex.close();
		} 
		catch (IOException e) {
			Log.e("HookDetection", e.toString());
		}
		for(String className : classes) {
			if(className.startsWith("com.example.hookdetection")) {
				try {
					Class clazz = HookDetection.class.forName(className);
					for(Method method : clazz.getDeclaredMethods()) {
						if(Modifier.isNative(method.getModifiers())){
							Log.wtf("HookDetection", "Native function found (could be hooked by Substrate or Xposed): " + clazz.getCanonicalName() + "->" + method.getName());
						}
					}
				}
				catch(ClassNotFoundException e) {
					Log.wtf("HookDetection", e.toString());
				}
			}
		}
	}
}

Stab 4: Use /proc/[pid]/maps to detect suspicious shared objects or JARs loaded into memory.

Within Linux, the /proc/[pid]/maps file contains the currently mapped memory regions and their access permissions. Lets look at part of the maps file for one of our Android applications. Note that the first row shows the starting and ending address of the region in the process’s address space and the sixth row shows the pathname if the region was mapped from a file.

#cat /proc/5584/maps

40027000-4002c000 r-xp 00000000 103:06 2114      /system/bin/app_process
4002c000-4002d000 r--p 00004000 103:06 2114      /system/bin/app_process
4002d000-4002e000 rw-p 00005000 103:06 2114      /system/bin/app_process
4002e000-4003d000 r-xp 00000000 103:06 246       /system/bin/linker
4003d000-4003e000 r--p 0000e000 103:06 246       /system/bin/linker
4003e000-4003f000 rw-p 0000f000 103:06 246       /system/bin/linker
4003f000-40042000 rw-p 00000000 00:00 0 
40042000-40043000 r--p 00000000 00:00 0 
40043000-40044000 rw-p 00000000 00:00 0 
40044000-40047000 r-xp 00000000 103:06 1176      /system/lib/libNimsWrap.so
40047000-40048000 r--p 00002000 103:06 1176      /system/lib/libNimsWrap.so
40048000-40049000 rw-p 00003000 103:06 1176      /system/lib/libNimsWrap.so
40049000-40091000 r-xp 00000000 103:06 1237      /system/lib/libc.so
... Lots of other memory regions here ...

So using the maps file we can look for any suspicious pathnames associated with the Xposed framework or Substrate.

try {
	Set libraries = new HashSet();
	String mapsFilename = "/proc/" + android.os.Process.myPid() + "/maps";
	BufferedReader reader = new BufferedReader(new FileReader(mapsFilename));
	String line;
	while((line = reader.readLine()) != null) {
		if (line.endsWith(".so") || line.endsWith(".jar")) {
			int n = line.lastIndexOf(" ");
			libraries.add(line.substring(n + 1));
		}
	}
	for (String library : libraries) {
		if(library.contains("com.saurik.substrate")) {
			Log.wtf("HookDetection", "Substrate shared object found: " + library);
		}
		if(library.contains("XposedBridge.jar")) {
			Log.wtf("HookDetection", "Xposed JAR found: " + library);
		}
	}
	reader.close();
}
catch (Exception e) {
	Log.wtf("HookDetection", e.toString());
}

When the Substrate framework is used we will notice multiple regions of memory mapped from shared objects associated with the framework.

Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libAndroidBootstrap0.so
Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libAndroidCydia.cy.so
Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libDalvikLoader.cy.so
Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libsubstrate.so
Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libsubstrate-dvm.so
Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libAndroidLoader.so

When the Xposed framework is used, then we will notice a region of memory mapped from the XposedBridge.jar file.

Xposed JAR found: /data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar

Debug.isDebuggerConnected() -> false

It is possible to detect whether or not an Android application is being hooked by Cydia Substrate or the Xposed framework from Java code and I’ve demonstrated a few different techniques (some novel and some not), but I’m sure that other techniques exist. Granted subverting these detection techniques isn’t difficult, so expect a Xposed cloaker that does the following soon. :)

  • Hook the PackageManager’s getInstalledApplications method and remove suspicious packages from the list.
  • Hook the Exception’s getStackTrace method and remove suspicious StackTraceElement objects from the array.
  • Hook the Method’s getModifiers method and tweak the flags to make it seem like the hooked method isn’t native.
  • Hook any file opens and return /dev/null instead of /proc/[PID]/maps or return a bogus maps file.

Android, take the blue pill.