Disabling Samsung Android System Services – A post-mortem

I’m currently working in a project where we build an Android App that gets installed on an EMM (Enterprise Mobility Managed) devices work profile. The devices are mostly Samsung devices, running Android 9 or Android 10.

More recently we got a big influx of crashes that left us back puzzled. Apparently when people took screenshots in the private profile, opened some app (like the browser) and then returned immediately to the work profile to our app, the application crashed as soon as they set the focus on an EditText field, with this:

Uncaught exception thrown in the UI: java.lang.SecurityException: No access to content://com.sec.android.semclipboardprovider/images: neither user 1010241 nor current process has android.permission.INTERACT_ACROSS_USERS_FULL or android.permission.INTERACT_ACROSS_USERS
at android.os.Parcel.createException(Parcel.java:2088)
at android.os.Parcel.readException(Parcel.java:2056)
at android.os.Parcel.readException(Parcel.java:2004)
at android.sec.clipboard.IClipboardService$Stub$Proxy.getClipData(IClipboardService.java:959)
at com.samsung.android.content.clipboard.SemClipboardManager.getLatestClip(SemClipboardManager.java:609)
at android.widget.EditText.updateClipboardFilter(EditText.java:316)
at android.view.inputmethod.InputMethodManager.startInputInner(InputMethodManager.java:2131)
... 

A quick Google search came back with almost nothing, only one stack overflow post suggested that one should simply add the missing permission with protectionLevel="signature" – which is of course non-sense for a non-system app that is not signed with the same key as the rest of the system framework. So, what do?

Staring at the stacktrace I fired up the Google Android CodeSearch and checked the sources of EditText – just to find a possible way to somehow disable / prevent the call to updateClipboardFilter. However, to my surprise, this API was completely nonexistant in AOSP!

So, apparently we’ve had to deal with a completely proprietary Samsung API. Firing up Google for SemClipboardManager pointed me to a several years old repository that partially disassembled the said class, so I could have a closer look of what is actually going on.

From what I saw there, the manager’s functionality could be disabled if I somehow found a way to overwrite the isEnabled method in this class to permanently return false – which the method usually only does if the device is in “emergency” or “ultra low power” mode. Ok, we have an attack vector!

From my usual Android trickery I knew the easiest way to fumble with system services is to create a custom ContextWrapper and wrap any given base context in my Activitys attachBaseContext method, like so:

class SomeActivity : AppCompatActivity {
  ...
  override fun attachBaseContext(newBase: Context) {
    super.attachBaseContext(FixSamsungStuff(newBase))
  }
  ...
}

Now, one could think “why deal with the internal service workings at all, wouldn’t it be enough to simply disable / null the service instead”, i.e. like this?

class FixSamsungStuff(base: Context): ContextWrapper(base) {
  override fun getSystemService(name: String): Any {
    // the name is from adb shell service list
    return if (name == "semclipboard") {
      null
    } else {
      super.getSystemService(name)
    }
  }
}

But the fine folks at Samsung of course don’t check for the non-existance of their service and instead of receiving the above SecurityException I was presented a NullPointerException instead.

So, now it got interesting – how would I actually proxy a method of a class to return a different value? From my testing adventures I knew this must be possible, because Mockito.spy(instance) exactly allows to do that, on the JVM and on ART.

So I came across ByteBuddy for Android, by the fantastic Rafael Winterhalter. His example on his front page of the repo was easy enough to adapt for my use case:

class FixSamsungStuff(base: Context): ContextWrapper(base) {
  override fun getSystemService(name: String): Any {
    val service = super.getSystemService(name)
    return if (name == "semclipboard") {
      interceptClipboardService(service)
    } else {
      service
    }
  }
  //
  private fun interceptClipboardService(service: Any): Any {
    val strategy = new AndroidClassLoadingStrategy.Wrapping(
      getDir("generated", Context.MODE_PRIVATE)
    )
    val dynamicType: Class<Any> = new ByteBuddy()
      .subclass(service.javaClass)
      .method(ElementMatchers.named("isEnabled"))
      .intercept(FixedValue.value(false))
      .make()
      .load(service.javaClass.classLoader, strategy)
      .getLoaded()
    // constructor definition from the decompiled sources
    val constructor = dynamicType.getConstructor(
      Context::class.java, Handler::class.java
    )
    return constructor.newInstance(this, Handler())
  }
}

But when I tried to ran this, I got a NoSuchFieldException because the given constructor was unknown. Hrm… well, I thought, maybe the decompiled sources where just too old, so I debugged into the code and checked for service.javaClass.getConstructors() and service.javaClass.getDeclaredConstructors(), but both returned an empty list! How on earth could a Java class be instantiated without a constructor?!

I learned that there are possibilities and that the JVM spec itself does actually not dictate the existance of a constructor for a class! So I contacted Rafael Winterhalter and he told me that there was probably some native code trickery going on, so my best bet should be to use sun.reflect.ReflectionFactory on my JVM. But this – of course – was not available on Android.

A hint in the Android Study Group slack then pointed me into the right direction – objenesis! This magic wand apparently allows you to create an instance of any class, regardless whether it has a constructor or not. So instantiating my ByteBuddy fake instance was as easy as doing

val objenesis = ObjenesisStd()
return objenesis.newInstance(dynamicType)

And as awesome as it is, that worked instantly!

This was a struggle-some, but in the end very worthy journey and I learned quite a few things on my way.

Thanks for reading!

Leave a Reply

Your email address will not be published.