【译】一个三星专有的crash剖析 - 禁用三星系统服务

禁用三星系统服务 - 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 真是一个艰难的过程,不过我在途中学到了很多东西,还是很值的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值