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 Activity
s 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!
Really helpful article!!!
Hi there, do you have a GIST that you can share of the completed code? Also, does this not break the clipboard service?