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

背景

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

一、两种方案的比较

1、xposed 方式的实现

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

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

插桩的方式通过自定义gradlePlugin,在项目构建过程中扫描项目中的类文件,并使用ASM库定位隐私方法,修改类文件,以达到检查的目的。该方案的优势在于:

  1. 灵活性高,适用于各种手机型号。
  2. 操作简单,无需安装额外软件,可集成到自动化测试系统中进行测试。

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

二、方案实现

1、方案概述

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

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

如图所示,我们自定义了两个关键模块:一个是名为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、功能实现

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)
        }

隐私方法的收集与保存

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

3、插件性能

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

image.png

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

image.png

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

三、插件的使用
1. 引入插件

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

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

android {
    privacyCheck{
        enable = true
    }
}

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

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

2. 手动测试

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

image.png
3. 测试结果

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

image.png

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

image.png

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

image.png
4. 结果处理

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

四、自动化测试的实现

我们采用了 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 的操作并不复杂,有兴趣的同学可以参考相关文档。

五、总结与展望

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容