阅读 3268

一步步治理隐私权限 | 安卓黑魔法

背景

本文内容所属是小破站啊,没有授权请勿转载

最近很多公司都面临和我们一样的难题,配合网信办进行隐私权限整改。主要涉及到在用户同意隐私权限授权之前,禁止调用敏感的api,具体比如imei,androididipmacaddress等等。

之前有另外一篇文章介绍了通过python,用反编译apk产物的方式对于敏感权限的调用进行搜索,之后再通知调用方进行整改的方式。

教你如何高效的检查APK中使用敏感权限的地方以及检查某系统方法被调用的地方

但是上述大佬的方法有一个问题,因为项目是会持续迭代的,需要每一段时间对其进行一次检查,之后再提醒业务方改动,实在过于被动了。

因为网络库以及埋点点基础组件依赖了唯一标识符,而在隐私权限给予之前又不允许调用,所以导致了初始化任务错乱了,同时这些基础仓库也要提供给b站其他app使用。一部分是为了隐私权限治理,另外一部分则就是为了梳理我们的初始化任务。

方案其实比较简单,我们会先抽象出一个隐私中间件,当隐私权限没有授予的情况下,所有api调用都返回的是空值。

然后就需要把业务上一个个api的调用更换成隐私中间件就行了。

Demo工程

pipeline

在jenkins官方文档是这样介绍pipeline的:Jenkins Pipeline (or simply "Pipeline") is a suite of plugins which supports implementing and integrating continuous delivery pipelinesinto Jenkins.它的意思就是pipeline是一套jenkins官方提供的插件,它可以用来在jenkins中实现和集成连续交付。

pipeline是一个流程,这个流程定义了完成过一个CI/CD流程的步骤,通过执行这个流程代替手工自动去完成CI/CD,这个流程是由使用者自己定义的。

而在gitlab上的pipeline对应的就是.gitlab-ci.yaml

image.png

这个就是当前哔哩哔哩一个分支代码推送远端之后,所执行的所有的步骤,之后这些步骤全部通过之后才能允许代码被和入。

GithubActions

当前github是提供一套非常简单的ci/cd接入方案的,有兴趣的大佬可以尝试下。

image.png

image.png

image.png

结果可以参考这个 github.com/Leifzhang/A…

静态检查

有兴趣学习下lint的基本使用可以参考我之前的文章 Android自定义lint开发 再谈Android Lint

因为b站的代码仓库基本都是源代码的大仓(mono-repo)模式,所以所有源代码都在一起,所以也就给我们提供了便利进行静态代码检查。

同时因为所有的代码和入都要先完成静态扫描的pipline,所以我们就能确保后续所有的代码和入都是规范的,这样就可以有效并持续性的规避这方面的问题。

我们这次涉及到的api改动数量比较大,每个提示修改文本也都不一样,如果一个个lint进行开发就会显得非常麻烦,这个时候我们需要提供一个更简单拓展性更好的方式,把这些简单的lint变成可配置化的。

这部分并不是我们独创的技术,而是参考之前美团和米忽悠的技术栈,之后对其进行了迭代和改造。还有之前米忽悠大佬也在自己的github上进行了分享。

github 参考链接AndroidLint

Json格式

首先我们看下这部分简单的json定义,因为我们要根据这些json来去做动态化的json匹配。

因为构造函数和方法调用其实是两种不同的lint写法,所以我们在这里定义了两个不同

{
  "methods": [
    {
      "name_regex": "android.net.wifi.WifiManager.getSSID",
      "message": "gie gie 这个是隐私api 请使用PrivacyUtil.getWifiName 替换哦",
      "excludes": [
        "com.xxxxxx.privacy.PrivacyImp"
      ]
    },
    {
      "name_regex": "android.net.wifi.WifiManager.getBSSID",
      "message": "gie gie 这个是隐私api 请使用PrivacyUtil.getWifiName 替换哦",
      "excludes": [
        "com.xxxxx.privacy.PrivacyImp"
      ]
    },
    {
      "name_regex": "Settings.Secure.getString",
      "message": "gie gie 这个是隐私api 请使用PrivacyUtil.getAndroidId 替换哦",
      "excludes": [
        "com.bilibili.privacy.PrivacyImp",
        "com.bilibili.adcommon.util.LocationUtil"
      ]
    },
    {
      "name_regex": "android.telephony.TelephonyManager.getImei",
      "message": "gie gie 这个是隐私api 请使用PrivacyUtil.getDeviceId 替换哦",
      "excludes": [
        "com.xxxx.privacy.PrivacyImp"
      ]
    },
    {
      "name_regex": "android.telephony.TelephonyManager.getDeviceId",
      "message": "gie gie 这个是隐私api 请使用PrivacyUtil.getDeviceId 替换哦",
      "excludes": [
        "com.xxxx.privacy.PrivacyImp"
      ]
    },
    {
      "name_regex": "android.telephony.TelephonyManager.getDeviceId",
      "message": "gie gie 这个是隐私api 请使用PrivacyUtil.getDeviceId 替换哦",
      "excludes": [
        "com.xxxx.privacy.PrivacyImp"
      ]
    },
    {
      "name_regex": "java.net.getHostAddress",
      "message": "gie gie 这个是隐私api 请使用PrivacyUtil.getIpAddress 替换哦",
      "excludes": [
        "com.xxxxx.privacy.PrivacyImp"
      ]
    },
    {
      "name_regex": "android.content.pm.PackageManager.getInstalledApplications",
      "message": "gie gie 这个是隐私api 请使用PrivacyUtil.getPackageList 替换哦",
      "excludes": [
        "com.Xxxxxx.privacy.PrivacyImp"
      ]
    },
    {
      "name_regex": "android.content.pm.PackageManager.getInstalledPackages",
      "message": "gie gie 这个是隐私api 请使用PrivacyUtil.getAppList 替换哦",
      "excludes": [
        "com.xxxxx.privacy.PrivacyImp"
      ]
    }
  ],
  "constructions": [
    {
      "name_regex": "",
      "message": ""
    }
  ]
}
复制代码

以上是我们当前整理的隐私相关的json名单,剔除的则是我们中间件相关的代码,我们希望一次性能将所有隐私相关的api整改到中间件上去。

因为这次诉求比较简单,我们只定义了方法和构造函数两个数组。name_regex 代表规则匹配,message则标示的是提示文案,excludes代表的是白名单列表。因为我们的诉求其实是统一调用我们定义的中间件,素有中间件都在我们的白名单列表上。

动态可配置化Lint

这个地方的难点就在于如何让lint代码能读取到我们的配置的json文件。

class DynamicLint : Detector(), Detector.UastScanner {

    lateinit var globalConfig: DynamicConfigEntity


    override fun beforeCheckRootProject(context: Context) {
        super.beforeCheckRootProject(context)
        globalConfig = GsonUtils.inflate(context.project.dir)
    }

}
复制代码

Detector提供了一个beforeCheckRootProject方法。这个方法会把当的目录信息之类的传入,我们就是要通过这个Context上下文,去获取我们的可配置化的json文件信息。

另外这里有个小细节,因为我们的项目采取的是compose building的模式,而这个Context正常传入的只有Module路径,所以这里要进行一个简单的递归查找。

private fun findCodeQuality(projectDir: File): File? {
     if (projectDir.parent != null) {
         val parent = projectDir.parentFile
         val file = parent.listFiles()?.firstOrNull {
             it.name == ".codequality" && it.isDirectory
         }
         return file ?: findCodeQuality(parent)
     }
     return null
 }
复制代码

一个简单的递归调用寻址。我会的为数不多的辣鸡算法题,哈哈哈哈。

      /**
       * name是完全匹配,nameRegex是正则匹配,匹配优先级上name > nameRegex
       * inClassName是当前需要匹配的方法所在类
       * exclude是要排除匹配的类(目前以类的粒度去排除)
       */
      private fun match(
          nameRegex: String?,
          qualifiedName: String?,
          inClassName: String? = null,
          exclude: List<String> = emptyList(),
          excludeRegex: String? = null
      ): Boolean {
          qualifiedName ?: return false

          //排除

          if (inClassName != null && inClassName.isNotEmpty()) {
              if (exclude.contains(inClassName)) return false

              if (excludeRegex != null &&
                  excludeRegex.isNotEmpty() &&
                  Pattern.compile(excludeRegex).matcher(inClassName).find()
              ) {
                  return false
              }
          }

          if (nameRegex != null && nameRegex.isNotEmpty() &&
              Pattern.compile(nameRegex).matcher(qualifiedName).find()
          ) {//在匹配nameRegex
              return true
          }
          return false
      }
  }
复制代码

代码检查匹配就是通过上述代码进行的,这部分逻辑也比较简单,各位有兴趣看看就行了。

如何验证

虽然我们通过lint将项目内的代码进行了一次约束,但是已经编译成.class并不会被这段UastScanner所识别。

可以通过ClassScanner进行class的lint扫描,但是逻辑相对来说比较复杂,写完这个我其实asm都写完了。

而且我们如果将这个需求提测的情况下,对于测试同学来说,这个需求也就没有办法测试了。所以我们需要另外一种方式能在运行时提供一部分hook能力,当这些隐私api被调用的情况下,或是产生一条文件记录或者是直接崩溃都行。

基于Epic动态hook

epic的hook机制,基于art的elft文件格式,所以可以hook所有代码内的方法调用,虽然是被动了点,但是可以避免反射等极端情况导致的隐私权限调用问题,以及第三方sdk内的调用情况。

首先我们可以沿用之前项目内定义好的那份动态json文件,之后通过软连接的方式直接复制到debug的assets文件夹下面。

软连接是linux中一个常用命令,它的功能是为某一个文件在另外一个位置建立一个同不的链接。 具体用法是:ln -s 源文件 目标文件。


    fun hookManager(context: Context) {
        val steam = context.resources.assets.open("dynamic.json")
        val configEntity = GsonUtils.inflate(steam)
        configEntity.methods.forEach {
            start(it)
        }
    }


    fun start(entity: DynamicEntity) {
        if (entity.name_regex.isNotEmpty()) {
            val methodName = entity.name_regex.substring(entity.name_regex.lastIndexOf(".") + 1)
            val className = entity.name_regex.substring(0, entity.name_regex.lastIndexOf("."))
            val lintClass = Class.forName(className)
               DexposedBridge.hookAllMethods(lintClass, methodName, object : XC_MethodHook() {
                   override fun beforeHookedMethod(param: MethodHookParam?) {
                       super.beforeHookedMethod(param)

                       Log.i("EpicHook", "EpicHook")
                   }
               })
        }
    }
复制代码

之后在debug包的情况下,通过反序列化json,同样生成好对应的hook文件配置,之后调用DexposedBridge.hookAllMethods方法。

温馨小提示,因为动态hook框架极为不稳定,所以请不要把这个功能发布到线上,同时最好带上版本控制的逻辑,因为在安卓10版本会崩溃。

切记debug工具一定不要带到线上去,因为一般为debug所设计的功能都是一些有风险的操作,所以这部分变种一定要加上。

第三方库内的隐私调用

虽然我们已经有了动态Hook的能力,但是因为动态hook一定要等到方法被调用的情况下才会执行异常,对于一些调用逻辑比较深的页面,可能会出现覆盖不到的情况。

而更好的方案就是通过asm,去对第三方的隐私代码进行替换,将他们转包到我们的中间件内。

这样就能做到多重保险,可以极大程度的应对机构的审查。

通过Transform + Asm 定位敏感权限

这部分相对来说也比较简单,我写了个小demo去验证这部分修正,以下指示针对deviceid的尝试修复。

这部分可以定位出更详细的三方库api调用的问题,辅助我们去推进第三方库进行调整。

还会老的方法,通过Asm的Tree api,之后判断当前的方法栈帧是不是"android/telephony/TelephonyManager"getDeviceId方法,如果是则对其进行修改,替换成我们的定义的静态方法。

这里有个小tips,因为之前必然是获取了android/telephony/TelephonyManager并完成了方法的压栈,所以我们要把上一个方法调用移除。

总结

因为我们这次将这种静态检查能力可配置化了,所以针对于后续的这种需求,我们只需要变更扫描规则就好了。极大的扩充了我们对于被动应付审查的能力,同时也更好的对于我们当前的大仓模式进行了肯定。

这次我们分享到主要目的就是为中国和谐移动生态作出一份我们的贡献,净化网络环境你我都有责任,用户隐私对于当今社会的重要性不言而喻了。因为所有代码的和入都要进行静态检查以及人工审核,所以可以保证所有后续的和入的代码都完成了这部分审查能力。希望文章能对大家有所帮助吧。

文章分类
Android
文章标签