禁用三星系统服务 - crash 剖析
本文翻译自网络《Disabling Samsung Android System Services – A post-mortem》,原作者: Thomas Keller.
为了译文流畅,部分表述有所改变,如有勘误敬请指正。
我(原作者)目前正在一个项目中,我们在该项目中构建一个Android应用,该应用已安装在EMM(企业移动性托管)设备的工作资料中。 这些设备主要是运行Android 9或Android 10的三星设备。
最近,我们发现了大量很迷的 crash 事件。当用户在 private profile 中截图,打开某个应用(比如浏览器),然后立即返回到 work profile 我们的应用中时,一旦将焦点设置到 EditText 字段上,程序就会崩溃。崩溃的 log 如下:
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)
...
快速地 Google 了一下,几乎没有什么相关的内容,只找到了一个 stackoverflow 的帖子,其中只有一个非常简单的建议,就是设置 protectionLevel=“signature”
来添加缺少的权限。这对于非系统的应用程序来说毫无意义,因为我们的 App 与其余的系统框架用的不是相同 key 来签名。
盯着 stacktrace,我想看看能不能禁用或者阻止对 updateClipboardFilter
方法的调用。我搜了一下 Android EditText.java
的源码。然而这个 API 在 AOSP(Android Open Source Project) 中完全不存在!
所以,很明显这完全是一个三星专有的API。搜索 stacktrace 中的关键字 SemClipboardManager
,我在 github 上找到了一个几年前的 repo,里面部分拆解了这个类,因此我可以更详细地了解实际发生了什么。
我发现,如果我能以某种方式或者找到一种方法来覆盖这个类中的 isEnabled()
方法,让它永远返回false,那么这个 Manager 的功能就会被禁用(译者注:这样就可以避免 crash 的产生)。这个方法通常只在设备处于“紧急”模式,或者“超低功耗”模式时才会这样做,不过我们总算有一个可以 hack 的突破口了。
根据我惯用的 Android 技巧,摸索系统服务的最简单方法是创建一个自定义的 ContextWrapper
,并将任何给定的 base context 包装到我自己 Activity 的 attachBaseContext
方法中,如下所示:
class SomeActivity : AppCompatActivity {
...
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(FixSamsungStuff(newBase))
}
...
}
那么,有人可能会问:“不能单独禁用这个服务吗?或者把它设为 null?为什么还要处理服务内部的逻辑?”,比如这样:
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)
}
}
}
如果这么做,虽然不会再报上面的 SecurityException了,但是会收到 NPE(NullPointerException)。因为,三星的“优秀员工”当然不会检查他们的服务是否为空。(注:讽刺🙂️,原作者发现三星的服务中没有做空指针检查,因此上述做法仍会导致 crash)
所以,现在事情实际上变成:怎么代理一个类的方法来返回一个不同的值?这肯定是可行的,因为在测试中完全允许 Mockito.spy(instance)这种用法,在 JVM 和 ART 上都可以这样做。
然后,我找到了一个工具 ByteBuddy。大神 Rafael Winterhalter 在首页 README 上的例子正好很适合我的用例:
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())
}
}
但是,当我尝试运行这段代码时,由于没有给定的构造函数,所以出现了NoSuchFieldException异常。好吧,或许是反编译的源码太旧了,所以我调试了一下代码并检查了 service.javaClass.getConstructors()
和 service.javaClass.getDeclaredConstructors()
,但是都返回了一个空列表!在没有构造函数的情况下,如何实例化 Java 类呢?
我觉得还是有成功的可能性的,况且 JVM 规范本身实际上并不要求类的构造函数必须存在。因此,我联系了Rafael Winterhalter,他告诉我可能是客户端代码进行了一些微妙的操作。因此,我最好的选择是在 JVM 上使用 sun.reflect.ReflectionFactory
,但这在 Android 上没法用。
最后,slack 上 Android 学习小组的提示给我指明了正确的方向 —— 使用 objenesis!这个神奇的 repo 可以帮助创建任何类的实例,无论它是否具有构造函数。因此,实例化我的ByteBuddy 类变得非常容易,只需要这样:
val objenesis = ObjenesisStd()
return objenesis.newInstance(dynamicType)
太棒了,这样就可以用了!
Debug 真是一个艰难的过程,不过我在途中学到了很多东西,还是很值的。