Lint-隐私检测利器

2,987 阅读3分钟

参考文章: zhuanlan.zhihu.com/p/307382854 zhuanlan.zhihu.com/p/43609710

demo工程:github.com/TokenYc/Lin… demo中内置了一些用于检测的Detector。

Lint用于检测静态代码和资源,找到其中不符合预定义规则的地方。

最近由于隐私问题抓的很紧,通过自定义lint,可以实现对三方sdk中涉及到用户隐私的方法进行检测。 自定义lint的最大坑在于:项目中的各种依赖版本不和,可能会导致执行lint失败。 所以,如果自己写的话,版本号还是按照demo中的来吧,测试能正常执行以后再结合项目更改版本。 包括:

  • lint-api版本
compileOnly "com.android.tools.lint:lint-api:27.1.3"
  • build tool
classpath "com.android.tools.build:gradle:4.1.3"
  • appcompat 对,你没看错。
implementation 'androidx.appcompat:appcompat:1.2.0'

最终效果: 可以看到哪些sdk中,调用了隐私相关的方法。 image.png

自定义Lint

如果是第一次接触lint,建议先按照步骤走,然后再去理解。

1.创建一个java module

image.png 在java module的build.gradle中添加

dependencies {
    compileOnly "com.android.tools.lint:lint-api:27.1.3"
}

2.创建Detector和Issue

首先是Detector,代表一个检测器。继承Detector,然后实现ClassScanner。 文件放在创建的java module中即可。 为什么是ClassScanner?其实有多个Scanner可以使用,但是目前只测试了这个Scanner是可以扫描三方sdk的。 直接上代码,这里以检测调用获取IMEI方法为例(已省略部分方法内部代码):

package com.qianfanyun.lintlib.detector

import com.android.tools.lint.detector.api.*
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode

class StealImeiDetector : 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) {
       
    }
}

重写了两个方法:

  • getApplicableAsmNodeTypes 这个方法表示,扫描器要扫描哪些指令。我们这边返回的是Method,就是方法相关的指令。
  • checkInstruction 这个方法提供了一些方法的描述,以便于进行筛选。

在理解了整体结构后,添加逻辑相关内容:

package com.qianfanyun.lintlib.detector

import com.android.tools.lint.detector.api.*
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode

/**
 * @date on 2019-12-23  14:31
 * @author ArcherYc
 * @mail  247067345@qq.com
 */
class StealImeiDetector : 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.name == "getDeviceId"&&methodInsn.owner=="android/telephony/TelephonyManager") {
            val message = "SDK 中 $callerMethodSig 调用了 " +
                    "${methodInsn.owner.substringAfterLast('/')}.${methodInsn.name} 的方法,需要注意!"
            context.report(ISSUE, method, methodInsn, context.getLocation(methodInsn), message)
        }
    }
}

逻辑代码还是比较好理解的,就是通过比较方法的名称和方法所在类来找到我们要检测的方法。 然后通过report方法,添加到lint的检测报告中。

然后是Issue。Detector表示检测器,Issure则表示一个问题,可以理解为是对Detector的描述。一般直接作为静态成员,放在Detector中。创建的方法是比较固定的,具体参数表示什么,跑一遍就知道了。

package com.qianfanyun.lintlib.detector

import com.android.tools.lint.detector.api.*
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode

/**
 * @date on 2019-12-23  14:31
 * @author ArcherYc
 * @mail  247067345@qq.com
 */
class StealImeiDetector : Detector(), ClassScanner {

    companion object {
        val ISSUE = Issue.create(
            "StealImei",//问题 Id
            "",//问题的简单描述,会被 report 接口传入的描述覆盖
            "",//问题的详细描述
            Category.CORRECTNESS,//问题类型
            6,//问题严重程度,0~10,越大严重
            Severity.ERROR,//问题严重程度
            //Detector 和 Scope 的对应关系
            Implementation(
                StealImeiDetector::class.java,
                Scope.ALL_CLASSES_AND_LIBRARIES
            )
        )
    }
    /**
     * 返回这个 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.name == "getDeviceId"&&methodInsn.owner=="android/telephony/TelephonyManager") {
            val message = "SDK 中 $callerMethodSig 调用了 " +
                    "${methodInsn.owner.substringAfterLast('/')}.${methodInsn.name} 的方法,需要注意!"
            context.report(ISSUE, method, methodInsn, context.getLocation(methodInsn), message)
        }
    }
}

到这里,最核心的部分就完成了。

3.创建Registry

(1)需要通过一个Registry来讲Issue注册进lint。 代码很简单,需要注册的Issue直接加载list中即可。 api返回固定CURRENT_API即可。

package com.qianfanyun.lintlib

import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.detector.api.Issue
import com.qianfanyun.lintlib.detector.*
import java.util.*


class DangerIssueRegistry : IssueRegistry() {

    override val issues: List<Issue>
        get() {
            return Arrays.asList(
                StealImeiDetector.ISSUE,
            )
        }

    override val api: Int
        get() = CURRENT_API
}

(2)在java module的build.gradle中添加

jar {
    manifest {
        attributes("Lint-Registry-v2": "com.qianfanyun.lintlib.DangerIssueRegistry")
    }
}

4.在app的build.gradle中引入

在app的build.gradle中引入

dependencies {
    lintChecks project(':lintlib')
}

5.执行

在命令行中执行

gradle app:lintDebug

如果你的项目中添加了Varint,则需要在lintDebug之间加上对应的名称。对应的命令变为

gradle app:lintxxxxDebug

执行后,如果一切顺利,会出现两个链接,一个是html格式的,一个是xml格式的。 image.png 点击就可以查看了。html格式的可以在浏览器中查看。 大致就像这样 image.png 这样,就可以找到第三方sdk中涉及到隐私政策的代码了。