ANDROID函数插桩实现隐私合规检测

1. 隐私合规检测的需求

  • 工信部对app个人隐私收集要求严格
  • 应用市场第三方检测中心检测出问题
  • 接入的第三方广告sdk调用敏感函数次数和时机

2. 隐私合规整改的方案

  • 手动排查:耗时、耗力、可能会有遗漏
  • xposed:非root手机需要借助太极等第三方框架
  • Gradle Transform加ASM实现函数插桩:自动化排查、第三方sdk不好解决或者修复周期长的,可以动态拦截相关敏感函数的调用

3. 关键技术点

3.1 Android 打包流程

  • Java文件会先转换成.class文件
  • 在.class文件转换成.dex文件前拦截做插入点
  • 遍历程序中所有的.class文件,做修改和保存

3.2 自定义Gradle插件,Gradle Transform Api

Gradle Transform的功能就是把输入的.class文件转换成目标字节码文件。

  • TransformmInput输入文件接口类包含两部分
    • DirectortyInput集合是指以源码方式参与项目编译的所有目录结构及其目录下的源码文件
    • JarInput集合是指以jar包方式参与项目编译的所有本地jar包和远程jar包
  • TransformOutputProvider 是指Transform的输出,包含输出路径等信息

3.3 ASM字节码插桩

  • ASM是一个Java字节码操作与分析框架,通过使用ASM框架,可以动态生成类或者增强既有类的功能。
  • Java的二进制被存储在严格格式定义的.class文件里,这些字节码文件拥有足够的元数据信息用来表示类中的所有元素,包括类名、方法、属性以及Java字节码指令。
  • ASM从字节码文件中读入这些信息后,能够改变类行为、分析类的信息,甚至能够根据具体的要求生成新的类。
    • ClassReader 解析编译过的.class字节码文件
    • ClassWriter 重新构建编译后的类,修改类名、属性以及方法
    • ClassVisitor 负责拜访类成员信息。包括类注解、类的构造方法、类的字段、类的方法、静态代码块等

4. 实现流程

5. 源码分析

  • plugin-sentry库实现自定义gradle插件,通过ASM获取类信息,收集带有指定注解的方法,以及执行字节码替换代理敏感api的方法

PrivacySentryPlugin: 实现Plugin接口,重写apply方法,注册收集注解信息和字节码替换任务的两个Transform

class PrivacySentryPlugin : Plugin<Project> {  
    override fun apply(target: Project) {  

        //只在application下生效  
        if (!target.plugins.hasPlugin("com.android.application")) {  
            return  
        }  
        target.extensions.create("privacy", PrivacyExtension::class.java)  
        val android = target.extensions.getByType(AppExtension::class.java)  
        // 收集注解信息的任务  
        android.registerTransform(PrivacyCollectTransform(target))  

        // 执行字节码替换的任务  
        android.registerTransform(PrivacySentryTransform(target))  
    }  
}    

PrivacyCollectTransform: Transform类,处理源码文件和jar包的.class文件,收集带有PrivacyMethodProxy的方法

override fun transform(transformInvocation: TransformInvocation?) {  
    super.transform(transformInvocation)  
    ....
    // Transform的inputs有两种类型,一种是目录,一种是jar包,分开遍历  
    transformInvocation?.inputs?.forEach {  
        handleJar(  
            it,  
            transformInvocation.outputProvider,  
            transformInvocation.isIncremental,  
            privacyExtension  
        )  
        handleDirectory(  
            it,  
            transformInvocation.outputProvider,  
            transformInvocation.isIncremental, privacyExtension  
        )  
    }  
}

PrivacyClassProcessor : 处理源码和jar包的.class文件、收集敏感api方法、执行hook函数的实际处理者

fun runCollect(ins: InputStream?, project: Project): ByteArray? {  
    val classReader = org.objectweb.asm.ClassReader(ins)  

    val classWriter = org.objectweb.asm.ClassWriter(org.objectweb.asm.ClassWriter.COMPUTE_MAXS)  
    // 定义类访问者  
    val classVisitor: ClassVisitor =  
        CollectHookMethodClassAdapter(  
            Opcodes.ASM7, classWriter, project.extensions.findByType(  
                PrivacyExtension::class.java  
            )  
        )   
    classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES)  
    return classWriter.toByteArray()  
}

CollectHookMethodAsm:收集待替换的敏感api方法的代理类,包活ClassVistor和AdviceAdapter实现类

override fun visit(version: Int,  access: Int,  name: String,  signature: String?,  
    superName: String?,  interfaces: Array<out String>?  ) {  
    super.visit(version, access, name, signature, superName, interfaces)   
    className = name.replace("/", ".")   
}  

override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {  
    if (descriptor?.equals("Lcom/yl/lib/privacy_annotation/PrivacyClassProxy;") == true){ 
        bHookClass = true  
    }  
    return super.visitAnnotation(descriptor, visible)  
}

override fun visitMethod(access: Int,  name: String?,  descriptor: String?,  signature: String?,   exceptions: Array<out String>?  ): MethodVisitor {  
    if (bHookClass) {  
        val methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions)  
        return CollectHookMethodAdapter(  
            api,  methodVisitor,  access,  name,  descriptor,  privacyExtension,  className  
        )  
    }  
    return super.visitMethod(access, name, descriptor, signature, exceptions)  
}

CollectHookMethodAdapter:

override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {  
    if (descriptor?.equals("Lcom/yl/lib/privacy_annotation/PrivacyMethodProxy;") == true) {  
        var avr = mv.visitAnnotation(descriptor, visible)  
        return CollectHookAnnotationVisitor(  
            api,   avr,  
            HookMethodItem(proxyClassName = className,  proxyMethodName = name, proxyMethodReturnDesc = methodDesc )  
        )  
    }  
    return super.visitAnnotation(descriptor, visible)  
}
  • privacy-annotation: 自定义注解库,定义PrivacyClassProxy类注解和PrivacyMethodProxy方法注解,在ASM解析.class文件时,有这个注解的类才会被解析收集

  • hook-sentry:写入敏感函数调用次数和堆栈日志的库,使用构建者模式提供初始化方法,上层调用做具体配置

  • privacy-proxy:用类注解代理定义一些涉及到用户隐私的敏感函数api代理方法,可以根据具体隐私政策整改要求自定义相关敏感函数做具体检测

@Keep  
open class PrivacyProxyCall {  

    // kotlin里实际解析的是这个PrivacyProxyCall$Proxy 内部类  
    @PrivacyClassProxy  
    @Keep    object Proxy {  
        // 这个方法的注册放在了PrivacyProxyCall2中,提供了一个java注册的例子  
        @PrivacyMethodProxy(  
            originalClass = ActivityManager::class,  
            originalMethod = "getRunningTasks",  
            originalOpcode = MethodInvokeOpcode.INVOKEVIRTUAL  
        )  
        @JvmStatic  
        fun getRunningTasks(  
            manager: ActivityManager,  
            maxNum: Int  
        ): List<ActivityManager.RunningTaskInfo?>? {  
            doFilePrinter("getRunningTasks", methodDocumentDesc = "当前运行中的任务")  
            if (PrivacySentry.Privacy.getBuilder()?.isVisitorModel() == true) {  
                return emptyList()  
            }  
            return manager.getRunningTasks(maxNum)  
        }
        ....
}

5. 如何使用

5.1、工程配置

根目录/build.gradle添加:

buildscript {  
     dependencies {
         // 添加插件依赖
         classpath 'cn.etouch.android.privacy:plugin-sentry:1.0.0'
     }
}

主项目 build.gradle 配置:

apply plugin: 'privacy-sentry-plugin'

dependencies {  
    // aar依赖
    def privacyVersion = "1.0.0"
    implementation "cn.etouch.android.privacy:hook-sentry:$privacyVersion"
    implementation "cn.etouch.android.privacy:privacy-annotation:$privacyVersion"
    //如果不想使用库中本身的代理方法,可以不引入这个类,自己实现
    implementation "cn.etouch.android.privacy:privacy-proxy:$privacyVersion"
}

// 黑名单配置,可以设置这部分包名不会被修改字节码 // 项目里如果有引入高德地图,先加黑 blackList = ["com.loc","com.amap.api"], asm的版本有冲突

privacy {  
    blackList = []
} 

5.2、功能接入

完成功能的初始化

PrivacySentryBuilder builder = new PrivacySentryBuilder()  
            // 自定义文件结果的输出名
            .configResultFileName("zhwnl_privacy")
            // 配置游客模式,true打开游客模式,false关闭游客模式
            .configVisitorModel(false)
            // 配置写入文件日志 , 线上包这个开关不要打开!!!!,true打开文件输入,false关闭文件输入
            .enableFileResult(true)
            // 持续写入文件30分钟
            .configWatchTime(30 * 60 * 1000)
            // 文件输出后的回调
            .configResultCallBack(new PrivacyResultCallBack() {
                @Override
                public void onResultCallBack(@NonNull String s) {

                }
            }); 
PrivacySentry.Privacy.INSTANCE.init(application, builder);  

在隐私协议确认的时候调用

PrivacySentry.Privacy.INSTANCE.updatePrivacyShow();

关闭游客模式

PrivacySentry.Privacy.INSTANCE.closeVisitorModel();

排查结果的日志文件写入在/storage/emulated/0/Android/data/yourPackgeName/cache/xx.xls,需要手动执行下adb pull。

作者介绍

  • 吕游 高级Android开发工程师

微鲤技术团队

微鲤技术团队承担了中华万年历、Maybe、蘑菇语音、微鲤游戏高达3亿用户的产品研发工作,并构建了完备的大数据平台、基础研发框架、基础运维设施。践行数据驱动理念,相信技术改变世界。