cover_image

动态隐私检查方案优化与实现

狐友张乙龙 搜狐技术产品
2025年02月19日 23:31

01

背景

在移动应用开发过程中,隐私保护是一项至关重要的工作。以往我们采用了一种动态隐私检查工具,通过xposed方式实现,然而,这种方案存在着诸多限制。需要特定型号的手机和复杂的安装操作,不适用于集成到自动化测试系统中。

02

两种方案的比较

2.1 xposed 方式的实现

以往我们采用了xposed框架实现了动态隐私检查功能。该方案通过修改Android系统的运行时环境,拦截应用程序的方法调用,实现隐私检查。但该方案在使用过程中发现存在一些局限性:

  1. 对手机型号要求高,仅适用于部分支持xposed框架的手机型号;
  2. 操作复杂,需要用户安装多个软件并进行繁琐配置;
  3. 不适用于集成到自动化测试系统中进行测试。
2.2 插桩方式的实现

插桩的方式通过自定义gradlePlugin,在项目构建过程中扫描项目中的类文件,并使用ASM库定位隐私方法,修改类文件,以达到检查的目的。该方案的优势在于:
  1. 灵活性高,适用于各种手机型号;
  2. 操作简单,无需安装额外软件,可集成到自动化测试系统中进行测试。

综上所述,我们选择通过插桩方式来优化动态隐私检查,以解决xposed方式存在的限制和不足。

03

方案实现

3.1 方案概述

在本方案中,我们采用新的 TransformApi扫描项目中的所有类文件,利用ASM库在这些class文件中定位隐私方法。随后,在隐私方法执行结束时,我们通过插入一段代码来收集堆栈数据。一旦用户同意隐私政策,收集到的数据将被输出到JSON文件中,以供进一步分析和处理。

3.2 方案设计

从  AGP1.5 开始,Transform API 一直是一个常用的工具,但在 AGP7.0 中已经被标记为废弃,并且在AGP8.0将被移除。官方的替代方案没有一个统一的替代 API。根据官方建议,有两种主要的方式来处理class文件:一种是通过自定义 Task 结合使用 ASM或者Javassist,另一种是使用 AsmClassVisitorFactory

AsmClassVisitorFactory 集成了 ASM 操作,并根据官方说法,能够提高大约18% 的编译速度,并且能够减少约 5 倍的代码量。它已经处理了增量逻辑,因此无需手动处理,只需进行一次   IO 操作,从而有效减少了IO操作的次数。然而,需要注意的是,它与 ASM 字节码工具紧密绑定,不支持其他字节码修改工具。在实践中,我们最初尝试了自定义TaskASM的方式,但需要处理增量编译的问题。后来我们转向了使用 AsmClassVisitorFactory 来处理,因为它能够更简单地对class文件进行修改。

image.png
image.png

如图所示,我们自定义了两个关键模块:一个是名为gradle_pluginGradle 插件,另一个是privacy_check功能模块。gradle_plugin 主要负责对class文件进行插桩操作,而 privacy_check 则定义了需要插入的代码。

具体而言,gradle_plugin 任务的工作流程如下:

  1. 首先,它会将privacy_check模块中的检测隐私方法的 collect 方法插入到需要检查的类文件中;

  2. 然后,经过插桩处理的类文件会生成 transformedClass

  3. 接下来,修改后的 transformedClass 将用于生成或更新 class/jar 文件;

  4. class/jar 文件随后将被转换为 .dex 文件,以便最终打包成 APK 文件。

在应用运行时,用户同意隐私政策后,会调用 privacy_check 模块中的 save 方法。该方法的目的是收集检测到的隐私方法调用,并将结果生成为 result.json 文件。

3.3 功能实现

3.3.1 gradle_pllugin中的主要实现

gradle_plugin 模块中,我们实现了一个名为 PrivacyClassVisitorFactoryclass文件处理类,主要用于实现插桩功能,以下是该插件的关键功能类:

  1. PrivacyPlugin.  PrivacyPlugin是自定义的一个插件,主要功能就是为编译中的每个变体应用class文件处理功能类:PrivacyClassVisitorFactory,主要代码如下:
         val androidComponents =
                project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)//
            // Registers a callback to be called, when a new variant is configured
            val extension = project.extensions.create("privacyCheck", PrivacyExtension::class.java)
            androidComponents.onVariants { variant ->
                if (!extension.enable) {
                    println("privacyCheck disable")
                    return@onVariants
                }

                variant.instrumentation.transformClassesWith(
                    PrivacyClassVisitorFactory::class.java,
                    InstrumentationScope.ALL
                ) {

                }
                variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)

  
            }

  1. PrivacyClassVisitorFactory. PrivacyClassVisitorFactory继承自AsmClassVisitorFactory,是class文件的处理类,它不用处理输入和输出,并且适配了增量编译,只需要定义ClassVisitor和是否需要处理当前class文件:
 override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        return PrivacyClassVisotor(
            instrumentationContext.apiVersion.get(),
            nextClassVisitor
        )
    }

    //isInstrumentable:io.reactivex.internal.operators.observable.ObservableDetach$DetachObserver
    //isInstrumentable:io.reactivex.internal.operators.observable.ObservableLastMaybe$LastObserver
    override fun isInstrumentable(classData: ClassData): Boolean {
        return !PrivacyPluginUtil.ignoreClass(classData.className)
    }


  1. PrivacyClassVisitor.  PrivacyClassVisitor继承了ClassVisitor,访问项目中class文件中的每个方法,并对每一个方法在PrivacyMethodVisitor类中进行处理,PrivacyMethodVisitor继承了AdviceAdapter,通过visitMethodInsn方法收集隐私方法的调用,并在相应的位置插入收集到的隐私方法调用堆栈信息。下面是PrivacyMethodVisitor中的关键代码:
   override fun visitMethodInsn(
            opcodeAndSource: Int,
            owner: String?,
            nameInsn: String?,
            descriptor: String?,
            isInterface: Boolean
        ) {

            privacyMethodList.forEach { classMethod ->
                if (owner == classMethod.className && nameInsn == classMethod.methodName) {
                    mv.visitLdcInsn("${className}#$name")
                    mv.visitLdcInsn("${owner.replace("/", ".")}#$nameInsn")
                    mv.visitMethodInsn(
                    INVOKESTATIC,
                    "com/xx/privacycheck/PrivacyCollectUtil",
                    "appendData",
                    "(Ljava/lang/String;Ljava/lang/String;)V",
                    false
                    )
                }
            }
            super.visitMethodInsn(opcodeAndSource, owner, nameInsn, descriptor, isInterface)
        }

    }

其中privacyMethodList是本地保存的待检测的隐私方法列表, 这里如果检测到了隐私方法的调用就把当前调用堆栈信息插入相应位置。

下面是 PrivacyMethodVisitor 类中的 visitFieldInsn 方法。该方法的主要作用是在类中查找每个方法的属性调用。当发现隐私方法调用时,会将其替换为代理类和方法。在代理方法中会插入代码,用于收集隐私方法信息,从而实现检测隐私属性调用的功能。其中ProxyBuild类代理了系统Build类。

  override fun visitFieldInsn(
            opcode: Int,
            owner: String?,
            nameField: String?,
            descriptor: String?
        ) {
            var filedOwner = owner
            var filedName = nameField
            privacyFieldList.forEach { field ->
                if (owner == field.className && nameField == field.methodName) {
                    filedOwner = "com/xx/privacycheck/proxy/ProxyBuild"
                    filedName = field.methodName
                }
            }
            super.visitFieldInsn(opcode, filedOwner, filedName, descriptor)
        }

3.3.2 隐私方法的收集与保存

privacy_check 模块中,我们定义了隐私方法的收集和保存功能。正如前文所述,TransformClassWork 负责将检测隐私的方法插入相应的 class 文件中。当用户同意隐私政策后,调用 save 方法将检测结果输出为.json文件,以供后续展示和保存使用。

3.4 插件性能

使用插件后,通过BuildAnalyzer可以查看一次全量编译的时间如下所示:transformClassWithAsm任务的执行时间为5s

image.png

在修改了一个类并且修改了一个布局视图的ID后,增量编译的时间如下所示:transformClassWithAsm任务的执行时间为2.2simage.png

由于我们的自动化测试只有在需要测试时才会启用该插件,因此在平时开发过程中,不会启用该插件,因此这些时间在实际开发中并不算长。

04

插件的使用

4.1 引入插件

在使用过程中,需在 app/build.gradle.kts 中引用插件并开启检测开关。由于隐私检测不是频繁需要的功能,建议仅在需要测试时打开,以避免对实际用户的干扰和影响。同时隐私检测插件和插桩操作可能会对应用的性能和稳定性产生一定影响。因此,通过在测试环境中进行详细的检测和调整,可以确保在上线前解决潜在的隐私权限审核问题。

plugins {
    id("com.xx.privacycheck")
}

android {
    privacyCheck{
        enable = true
    }
}

在代码中,用户点击同意后,只有debug环境才会输出检测结果:

 if(BuildConfig.DEBUG){
       PrivacyCollectUtil.stopCollect(this@PreProcessingActivity)
   }

4.2 手动测试

运行应用程序,进入应用后点击“同意”。如果检测到隐私方法的调用,会在 Logcat 中输出相关信息,并将结果保存为 JSON 文件存储在存储卡中。

image.png
image.png
4.3 测试结果

若仅作为上线前的测试,通过Logcat输出即可判断是否存在隐私方法调用的问题。下图就是Logcat中输出的检测到的隐私方法的调用:

image.png若需具体查看隐私方法的调用位置,需要检查存储卡中的详细数据。下图中的 monitorStackTrace 显示了具体的调用堆栈,可以看到该测试 APK 在点击“同意”前调用了 Build.MODEL 等属性。

image.png
image.png

如果集成到 APM 中进行自动测试,则可以通过 APM 后台获取存储卡中的数据,并在 APM 后台展示页面输出结果。APM后台结果显示如下图所示,检测出来的每个结果,点击后是堆栈信息。

image.png
image.png
4.4 结果处理

通过查看堆栈信息,找到调用隐私方法的位置,把隐私方法的调用放到点击同意隐私政策后初始化。

05

自动化测试的实现

我们采用了 Python 结合 uiautomator2 实现自动化测试。Python 程序负责实现待检测应用的安装和用户同意隐私政策的操作。我们的 APM 后台通过调用 Python 程序来执行隐私方法的检测和数据的保存和展示。

##需要开启开发者模式中的可模拟点击功能
if __name__ == '__main__':
    print(sys.argv)
    global adbPath
    global apkPath
    if len(sys.argv) == 3:
        adbPath = sys.argv[1]
        apkPath = sys.argv[2]
    else :
        print("python params error")
    installApk()
    startCheck()
    loadJson()

上面是Python程序的主要代码,执行这段python程序需要在手机开发者模式中打开可模拟点击的功能,以便uiautomator2可以执行自动化操作。installApk方法执行了卸载和安装apk的操作,startCheckuiautomator2对手机的操作,loadJosn方法会把结果传递给APM后台。这部分 Python 功能与 uiautomator2 的操作并不复杂,有兴趣的同学可以参考相关文档。

06

总结与展望

以上方案实现了动态隐私方法的检测,并且通过代理替换属性调用的方式,实现了系统隐私属性的监测,比如 android.os.Build.SERIAL。未来,我们的插件还可以进一步优化,例如增加白名单功能,以排除某些功能模块不插入检测代码,从而提升插桩速度。尽管与之前的 xposed 方式相比,新方案具有更广泛的适用性和更好的扩展性,但仍有改进的空间。我们希望本文介绍的方案能够对大家的移动应用开发工作有所帮助。



继续滑动看下一个
搜狐技术产品
向上滑动看下一个