本文内容所属是小破站啊,没有授权请勿转载
最近很多公司都面临和我们一样的难题,配合网信办进行隐私权限整改。主要涉及到在用户同意隐私权限授权之前,禁止调用敏感的api,具体比如imei
,androidid
,ip
,macaddress
等等。
之前有另外一篇文章介绍了通过python
,用反编译apk
产物的方式对于敏感权限的调用进行搜索,之后再通知调用方进行整改的方式。
但是上述大佬的方法有一个问题,因为项目是会持续迭代的,需要每一段时间对其进行一次检查,之后再提醒业务方改动,实在过于被动了。
因为网络库以及埋点点基础组件依赖了唯一标识符,而在隐私权限给予之前又不允许调用,所以导致了初始化任务错乱了,同时这些基础仓库也要提供给b站其他app使用。一部分是为了隐私权限治理,另外一部分则就是为了梳理我们的初始化任务。
方案其实比较简单,我们会先抽象出一个隐私中间件,当隐私权限没有授予的情况下,所有api调用都返回的是空值。
然后就需要把业务上一个个api的调用更换成隐私中间件就行了。
在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
。
这个就是当前哔哩哔哩一个分支代码推送远端之后,所执行的所有的步骤,之后这些步骤全部通过之后才能允许代码被和入。
当前github是提供一套非常简单的ci/cd接入方案的,有兴趣的大佬可以尝试下。
结果可以参考这个 https://github.com/Leifzhang/AndroidLint/runs/3276855999?check_suite_focus=true
有兴趣学习下lint的基本使用可以参考我之前的文章 Android自定义lint开发 再谈Android Lint
因为b站的代码仓库基本都是源代码的大仓(mono-repo)模式,所以所有源代码都在一起,所以也就给我们提供了便利进行静态代码检查。
同时因为所有的代码和入都要先完成静态扫描的pipline
,所以我们就能确保后续所有的代码和入都是规范的,这样就可以有效并持续性的规避这方面的问题。
我们这次涉及到的api
改动数量比较大,每个提示修改文本也都不一样,如果一个个lint
进行开发就会显得非常麻烦,这个时候我们需要提供一个更简单拓展性更好的方式,把这些简单的lint
变成可配置化的。
这部分并不是我们独创的技术,而是参考之前美团和米忽悠的技术栈,之后对其进行了迭代和改造。还有之前米忽悠大佬也在自己的github上进行了分享。
github 参考链接AndroidLint
首先我们看下这部分简单的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
代码能读取到我们的配置的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机制,基于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,去对第三方的隐私代码进行替换,将他们转包到我们的中间件内。
这样就能做到多重保险,可以极大程度的应对机构的审查。
这部分相对来说也比较简单,我写了个小demo去验证这部分修正,以下指示针对deviceid的尝试修复。
这部分可以定位出更详细的三方库api调用的问题,辅助我们去推进第三方库进行调整。
还会老的方法,通过Asm的Tree api,之后判断当前的方法栈帧是不是"android/telephony/TelephonyManager"
的getDeviceId
方法,如果是则对其进行修改,替换成我们的定义的静态方法。
这里有个小tips,因为之前必然是获取了android/telephony/TelephonyManager
并完成了方法的压栈,所以我们要把上一个方法调用移除。
因为我们这次将这种静态检查能力可配置化了,所以针对于后续的这种需求,我们只需要变更扫描规则就好了。极大的扩充了我们对于被动应付审查的能力,同时也更好的对于我们当前的大仓模式进行了肯定。
这次我们分享到主要目的就是为中国和谐移动生态作出一份我们的贡献,净化网络环境你我都有责任,用户隐私对于当今社会的重要性不言而喻了。因为所有代码的和入都要进行静态检查以及人工审核,所以可以保证所有后续的和入的代码都完成了这部分审查能力。希望文章能对大家有所帮助吧。