ArchSummit全球架构师峰会门票9折倒计时中~ 了解详情
写点什么

知乎安卓端第三方库敏感代码扫描机制:FindDanger

  • 2020 年 3 月 26 日
  • 本文字数:4792 字

    阅读完需:约 16 分钟

知乎安卓端第三方库敏感代码扫描机制:FindDanger

背景

知乎非常重视用户隐私数据的保护,安全团队一直在为此提供各种保护机制;另外国内外一些知名的 Android 商店,如 Google Play 等也会针对用户隐私数据进行一系列的上架审核,一旦出问题会被严重警告甚至下架。为全面保护用户的隐私数据,同时避免被商店拒审或下架的风险,Android 移动平台团队建立了一套敏感代码扫描机制,取名为 FindDanger。


FindDanger 简介

在 App 开发过程中,对于自己的代码中涉及用户隐私的部分很容易约束,但对于 App 中引用的第三方库是否会有此类行为就无法知晓了。对于这种情况,FindDanger 机制应运而生,FindDanger 是一套敏感代码扫描机制,主要用于扫描第三方库是否存在获取用户隐私之类的高风险行为。


机制介绍


整个机制的运行过程如下图所示:



由于我们使用 GitLab + Jenkins 进行代码管理和持续集成,所以普通的工程师也可以很快上手,具体的流程如下:


首先,工程师提交检测 jar 包的 merge request 后,会触发 Jenkins 上的扫描任务,扫描结束将报告链接发送给 GitLab 的 merge request 页面,如下图所示:



然后,点击页面中的链接打开报告,如下图所示:



最后,移动平台的工程师对报告进行分析,并给出使用意见。


规则支持


目前 FindDanger 支持下列检测规则,其中危险级别的数值越大越危险:



FindDanger 原理之自定义 Lint 规则

从生成的报告可以看出,FindDanger 是利用 Android Lint 进行代码扫描的,而这之中最关键的就是如何自定义 Lint 规则以满足我们的需求。


Android Lint 规则会打包到一个 Jar 包中,所以自定义规则主要有五步:


  1. 创建 Java Library 工程

  2. 实现自己的 Detector

  3. 创建对应的 Issue

  4. 实现自己的 Registry 以便将规则注册到 Lint

  5. 在 App 中使用


第一步,创建 Java Library 工程


为了方便开发和调试,建议创建一个空的 Android App 工程,然后添加 Java Module 用于编写规则代码。


之后在 Java Module 中添加 lint-api 依赖:


dependencies {    compileOnly "com.android.tools.lint:lint-api:26.1.4"}
复制代码


第二步,实现自己的 Detector


创建 Java 工程后,就要开始写规则代码了,每个规则都要实现一个 Detector,首先继承抽象类 Detector,然后根据需求实现一个或多个 Scanner 接口。


Scanner 类有如下几种:


  • SourceCodeScanner 扫描 Java 或符合 JVM 规范的源码文件(如 kotlin)

  • ClassScanner 扫描编译后的 class 文件

  • BinaryResourceScanner 扫描二进制资源文件(如.png)

  • ResourceFloderScanner 扫描资源目录

  • XmlScanner 扫描 xml 文件

  • GradleScanner 扫描 Gradle 文件

  • OtherFileScanner 扫描其他文件


最常用的就是 SourceCodeScanner 和 ClassScanner,由于目标文件的差异(源码文件和 class 文件),这两个 Scanner 的实现方式完全不同,下面分别介绍。(当前 lint 最新版本是 26.1.4。从源码看到 Detector 类被标记为 Beta,里面还声明了所有 Scanner 的方法,其实这些方法我们无法使用,个人猜测 Google 未来可能会用 Detector 封装所有 Scanner 以达到简化接口的目的)


先看 SourceCodeScanner,从源码看到这个接口声明了很多 getApplicableXXX 和 visitXXX 方法,这些方法是成对使用的,比如我想扫描方法调用就实现 getApplicableMethodNames() 和 visitMethod() 方法,getApplicableMethodNames() 返回一个列表,包含了所有关心的方法名字,当扫描到关心的方法调用时 visitMethod() 会被回调,在 visitMethod() 里实现具体检测逻辑,下面看个 Android 的例子。


如果我们在 App 中不正确的使用 AlarmManager.setRepeating 方法,会有 lint 提示:



我们看下 Android 是如何检测的:


class AlarmDetector : Detector(), SourceCodeScanner {    ...    // AlarmDetector 只关心 setRepeating 方法调用    override fun getApplicableMethodNames(): List<String>? = listOf("setRepeating")    // 扫描到方法名字为 setRepeating 的方法调用    override fun visitMethod(context: JavaContext, node: UCallExpression, method: PsiMethod) {        val evaluator = context.evaluator        // 判断此方法是否是 android.app.AlarmManager 类中的,并且有 4 个参数        if (evaluator.isMemberInClass(method, "android.app.AlarmManager") &&                evaluator.getParameterCount(method) == 4) {            // 判断索引为1的参数是否小于5000            ensureAtLeast(context, node, 1, 5000L)            // 判断索引为2的参数是否小于60000            ensureAtLeast(context, node, 2, 60000L)        }    }    // 如果参数小于最小值,上报错误    private fun ensureAtLeast(context: JavaContext, node: UCallExpression, parameter: Int, min: Long) {        val argument = node.valueArguments[parameter]        val value = getLongValue(context, argument)        if (value < min) {            val message = "Value will be forced up to $min as of Android 5.1; " +                    "don't rely on this to be exact"            context.report(ISSUE, argument, context.getLocation(argument), message)        }    }}
复制代码


可以看到在 visitMethod 里,根据三个参数 JavaContext、UCallExpression、PsiMethod 可以很方便的获取方法的参数列表、所在类等信息。


(这里的 UCallExpression 和 PsiMethod 其实是源码抽象语法树 AST 的两种实现,最早 Android Lint 用 Lombok 解析 AST,由于 Lombok 只能支持到 Java6 并且功能有限,在 AndroidStudio 2.2 之后 Lint 改用 PSI 解析语法树,但 PSI 也只是过渡方案,因为他不支持 kotlin,后面 Lint 会使用 UAST 作为抽象语法树解析库,PSI 和 UAST 都是 JetBrains 为 IDEA 开发的,UAST 是基于 PSI 扩展而来所以很容易移植,UAST 的优势是不止支持 Java 源码,理论上能够支持任何 JVM 类型的语言)


同样的,ClassScanner 也声明了一些 getApplicableXXX 和 checkXXX 方法,在 FindDanger 中用的最多的就是 getApplicableAsmNodeTypes() 和 checkInstruction() 方法,getApplicableAsmNodeTypes() 方法返回我们关心的指令列表,当扫描到关心的指令时会调用 checkInstruction() 方法,下面看个 FindDanger 中的例子:


class StealNFCInfoDetector : Detector(), ClassScanner {    /**     * 返回这个 Detector 适用的 ASM 指令     */    override fun getApplicableAsmNodeTypes(): IntArray? {        //这里关心的是与方法调用相关的指令,其实就是以 INVOKE 开头的指令集        return intArrayOf(AbstractInsnNode.METHOD_INSN)    }    /**     * 扫描到 Detector 适用的指令时,回调此接口     */    override fun checkInstruction(context: ClassContext, classNode: ClassNode, method: MethodNode, instruction: AbstractInsnNode) {        if (instruction.opcode != Opcodes.INVOKEVIRTUAL) {            return        }        val callerMethodSig = classNode.name + "." + method.name + method.desc        val methodInsn = instruction as MethodInsnNode        // 这里逻辑是:调用 NfcAdapter 中的任何方法都会报告异常        if (methodInsn.owner == "android/nfc/NfcAdapter") {            val message = "SDK 中 $callerMethodSig 调用了 " +                    "${methodInsn.owner.substringAfterLast('/')}.${methodInsn.name} 的方法来获取 NFC 信息,需要注意!"            context.report(ISSUE, method, methodInsn, context.getLocation(methodInsn), message)        }    }}
复制代码


通过 checkInstruction() 方法的四个参数可以方便的获取当前指令的上下文环境。


有了这些信息便可知道源码或者字节码是否存在问题代码,当发现问题时可以用 ClassContext、JavaContext 的 report 接口上报。不同接口的 report 参数不同,我们只要自定义好 message 即可。report 还有个可选的 LintFix 参数,这个参数作用是提供一个快速修复的功能,这里不做介绍,感兴趣的同学可以自行查看源码。


第三步,创建对应的 Issue


Issue 代表具体的问题对象,对象包含问题的类型、描述、级别,还有上报问题的 Detector 和对应的 Scope。


下面看个 Issue 的例子:


val ISSUE = Issue.create(        "StealNFCInfo",//问题 Id        "",//问题的简单描述,会被 report 接口传入的描述覆盖        "",//问题的详细描述        Category.CORRECTNESS,//问题类型        6,//问题严重程度,0~10,越大严重        Severity.ERROR,//问题严重程度        //Detector 和 Scope 的对应关系        Implementation(StealNFCInfoDetector::class.java, EnumSet.of(Scope.CLASS_FILE, Scope.JAVA_LIBRARIES)))
复制代码


通常情况下 Issue 和 Detector 是一一对应的,所以将 Issue 声明为 Detector 的静态属性会比较直观。


第四步,创建 Registry


有了 Issue 之后还要手动注册,首先实现一个 IssueRegistry,然后复写 getIssues 返回我们的 Issue 列表:


class DangerIssueRegistry : IssueRegistry() {    override val issues: List<Issue>        get() {            return Arrays.asList(                    StealAPPListClazzDetector.ISSUE,                    StealRunningListDetector.ISSUE,                    ...                    StealWifiInfoDetector.ISSUE)        }    override val api: Int        get() = CURRENT_API}
复制代码


接下来需要在 manifest 中声明 Registry,在 build.gradle 里添加:


jar {    manifest {        attributes("Lint-Registry-v2": "com.zhihu.android.findDanger.DangerIssueRegistry")    }}
复制代码


大功告成,编译打包后就会生成包含自定义规则的 Jar 包。


第五步,在 App 中应用


最后一步就是将自定义规则应用到 App 中了,最新版的 Android Gradle 插件提供了简便的方式(不用再自己包装 AAR 了),在 App 的 dependencies 中添加 lintChecks,和使用 implementation 没有任何区别:


dependencies {    implementation fileTree(include: ['*.jar'], dir: 'libs')    lintChecks 'zhihu.find.danger:FindDanger:local'    //或者    lintChecks project(':findDangerRules')}
复制代码


然后执行:


./gradlew app:lintDebug
复制代码


即可看到输出的报告文件。


Android Lint 执行原理

为了便于大家理解,我们可以看看 Android Lint 是如何使用我们定义的规则的,下图是简单的 Android Lint 处理流程:



简单介绍一下上图中的几个流程:


  1. 启动 Lint 后会先解析参数并做一些准备工作

  2. 之后我们自定义的规则和系统自己的规则会统一放到了一个 Map 里

  3. 然后根据项目的文件类型按顺序扫描文件,扫描过程中报告的问题对象都存在内存中,

  4. 全部扫描结束会使用具体 Reporter(如 HtmlReporter、XMLReporter 等)创建报告文件。


成果展示

FindDanger 上线后,我们对正在评审中的一个第三方库进行扫描,发现其有两处上报用户数据的行为:


  1. 上报用户已安装的应用列表

  2. 上报用户正在运行的应用列表


以上两个问题的风险较大,不但有可能被 Google Play 商店拒审,更重要的是可能造成我们的用户数据泄露,这个是我们的底线绝对不能被触碰。


另外还有若干个地方也存在一定的风险,不过由于影响不大,所以就不在这里一一列举了。


最终我们的处理结果是:直接移除了此库,改用别的方案。


FindDanger 后续计划

目前最新的 Android Lint 版本是 26.1.4,从注释看还处于 Beta 版,接口可能会发生变化,并且第一版 FindDanger 支持的功能有限,后续还会有一些改进计划,如:


  1. 添加对 aar 包的支持(官方暂不支持,但需求较大)

  2. 丰富检测规则

  3. 增加更复杂的逻辑检测,比如发现存在读取隐私的代码后还能进一步检测到将隐私泄露的代码

  4. 随着 Android Lint 版本变化,持续维护相关 Api


最后

由于本人的水平有限,如有错误和疏漏,欢迎各位同学指正。


2020 年 3 月 26 日 19:00465

评论

发布
暂无评论
发现更多内容

QA进阶成长感悟录

homber

成长 内容合集 签约计划第二季

基于HTML、CSS和JS的年龄计算器

海拥(haiyong.site)

html 大前端 28天写作 签约计划第二季 12月日更

【讲坛实录】知识图谱的探索与应用

星环科技

知识图谱

2021商业评论管理行动力峰会

大咖说

商业 直播

ipvs localhost 为何不正常

Geek_f24c45

k8s IPVS kube-proxy

服务端质量保证体系(二) 流水线标准化建设

homber

服务端 CI/CD 流程 质量保证 签约计划第二季

换个角度思考勒索攻击事件

华为云开发者联盟

漏洞 勒索 攻击 安全检测 蜜罐检测

一文讲透数仓临时表的用法

华为云开发者联盟

数据库 sql Local GaussDB(DWS) 临时表

基于HTML、CSS、JS的小游戏/工具制作过程及完整源码

海拥(haiyong.site)

28天写作 内容合集 签约计划第二季 12月日更 技术专题合集

Redis 的事务支持 ACID 么?

码哥字节

redis 事务 ACID 签约计划第二季

偷天换日,用JavaAgent欺骗你的JVM

码农参上

字节码插桩 代理 探针 签约计划第二季

Go语言学习查缺补漏ing Day3

恒生LIGHT云社区

Go 编程语言

Linux一学就会之Centos8软件包的管理和安装之yum管理软件包

学神来啦

Linux centos 运维 rpm yum

为什么要做团建TB?(6/28)

赵新龙

28天写作

服务端质量保证体系(三) CI原子能力建设

homber

ci 服务端 质量保证 签约计划第二季

2021 China DevOpsDays演讲实录

homber

DevOps DevOpsDays 签约计划第二季

Python代码阅读(第67篇):获取列表中的去重后的元素

Felix

Python 编程 列表 阅读代码 Python初学者

【分布式技术专题】「OSS中间件系列」Minio的Server端服务的架构和实战搭建

浩宇の天尚

OSS Minio Minio 集群 12月日更 FS

Apache ShenYu源码阅读系列-注册中心实现原理之Http注册

子夜2104

少儿春晚表演

Tiger

28天写作

企业如何做好员工安全意识提升

腾讯安全云鼎实验室

使用Harbor作为Rainbond默认容器镜像仓库,扩展Rainbond镜像管理能力

北京好雨科技有限公司

和合共赢,DataPipeline与麒麟软件完成产品兼容性互认证

DataPipeline数见科技

中间件 数据库中间件

网易应用创新开发者大赛成功在杭举办,十强队伍现场比拼

网易云信

人工智能 音视频 直播

浅谈Java编译优化之常量折叠技术

码农参上

编译器优化 签约计划第二季

春松客服入驻Rainbond开源应用商店

北京好雨科技有限公司

会用泛型,但你知道什么是泛型的类型擦除吗?

码农参上

Java泛型 签约计划第二季

Smack库 XMPP Tigase异常SASLErrorException

Changing Lin

12月日更

「Oracle」Oracle 数据库备份还原

恒生LIGHT云社区

数据库 oracle

星环科技 TDH8.1.0:全新升级为用户带来极致体验

星环科技

大数据

【混合云】部分混合云管理平台大汇总

行云管家

云计算 公有云 混合云 云管平台

AI在游戏反外挂中的应用与实践

AI在游戏反外挂中的应用与实践

知乎安卓端第三方库敏感代码扫描机制:FindDanger_文化 & 方法_柯文_InfoQ精选文章