随着政府部门对用户隐私的保护越来越严格,对移动应用来说怎样查找和避免应用中出现非法的用户隐私调用成为我们开发的一个新课题,为创造长期价值,形成以引擎驱动长期自我检查的能力,我们希望以我们的技术优势带动业务的进步。
经过技术调研,我们发现使用Gradle插件+ASM的方式可以很好的解决我们的困难。ASM是AOP编程中一种比较成熟的框架,在现在的开发中得到了越来越多的应用。但在ASM的调研和学习中发现目前可找到的资料和教程或侧重原理,或侧重核心API介绍,没有一个完整的教程,从原理、API介绍到实战进行整体系统的讲解,这给学习ASM的同学造成了困扰,无形之中抬高了学习的门槛。本文将结合自己学习的经验和学习中的疑难点从入门者的角度讲解ASM入门涉及的知识点和流程,并通过具体实例讲解如何使用ASM,希望能给读者带来不错的收益。
ASM实际开发中多和Gradle Transform结合使用,在介绍ASM之前我们先来简单说下Gradle Transform的使用。
Android官方从Gradle1.5.0开始提供了Tranform API,Gradle Transform是Android官方提供给开发者在项目构建时期(class -> dex转换阶段)用来修改class文件的一套标准API,即把输入的class文件转变成目标字节码文件。
自定义Transform需要继承Transform抽象类,实现自己的字节码处理逻辑,然后通过自定义的Gradle插件来注册自定义的Transform,注册后的Transform会被包装成一个Gradle Task,这个Task将在App的所有Task执行列表中被执行。
实现自定义Gradle插件和自定义Transform的步骤主要包括如下几步:
1)、新建Gradle Plugin工程
2)、实现自定义Gradle Plugin
3)、实现自定义Transform
4)、注册自定义Transform
5)、发布工程
下面来逐步讲解整个插件工程的实现过程。
这里推荐使用IDEA来开发Gradle Plugin,不用手动处理工程的目录结构。具体步骤如下 :
1)、依次点击File->New ->Project选项,弹出界面如图1所示:
图1
2)、左侧选择Gradle,右侧勾选Groovy,点击下一步,页面如图2所示
图2
3)、输入工程名称,点击完成,Gradle Plugin工程创建完毕,新建的工程目录如图3所示。
图3
工程创建完成后,在src/main目录下有groovy、java、resources三个子目录,其中groovy目录就是用来开发gradle plugin的,我们在目录下新建自己的包名,然后新建文件,选择Groovy Class类型的文件,在这里我们新建的类为HelloGradlePlugin类,新建后让它实现Plugin<Project>接口,重写其中的apply方法,实现后的结果如图4:
图4
这样整个gradle plugin的创建就告一段落,下面我们开始下一步开发任务。
Transform是个抽象类,从官方文档中我们知道Transform核心API如图5所示
图5
String getName():定义了Transform的名称
Set<ContentType> getInputTypes():返回当前Transform处理的数据的类型
Set<? super Scope> getScopes():返回当前Transform处理的数据的范围
boolean isIncremental():放回当前Transform是否开启增量编译
void transform(TransformInvocation transformInvocation):对字节码进行处理的核心类,我们的插桩操作都要在这个类里进行。
Transform是一个链式处理流程,第一个Transform接收来自javac编译的结果,以及已经拉取到本地的第三方依赖(jar和aar),处理完后传给下一个Transform,此后每一个Transform接收上一个Transform处理的结果,处理完后在将处理结果传给下一个Transform,直至最后一个。而transform(TransformInvocation transformInvocation)方法就是来完成这一操作的。TransformInvocation对象包含了传入的每个文件信息和Transform的输出信息。
public interface TransformInvocation {
Collection<TransformInput> getInputs();
TransformOutputProvider getOutputProvider();
}
TransformInvocation的两个核心方法就是getInputs()和getOutputProvider()。
TransformInput指输入文件的一个抽象,包括:
DirectoryInput集合
是指以源码的方式参与项目编译的所有目录结构及其目录下的源码文件
JarInput集合
是指以jar包方式参与项目编译的所有本地jar包和远程jar包(此处的jar包包括aar)
DirectoryInput集合和JarInput集合就是我们要遍历处理的class文件集合,后面会讲到这块和ASM的结合来实现我们插桩的目的。
4)、注册自定义Transform
自定义了Transform后需要在Gradle中注册才能运行使用,Transform的注册比较简单,具体代码如图6所示:
图6
5)、发布工程
到此为止一个空的Gradle 插件工程基本开发完成,下面我们把这个工程发布到maven服务器上。工程的发布用到了现成的gradle插件 maven-publish,将maven-publish添加到build.gradle,然后调用publishing方法进行maven url、groupId、artifactId、version等参数配置,配置后的结果如图7所示:
图7
maven url可以根据情况填入自己的私服地址(如JCenter),我自己用到的就不贴出来了,username和password是私服对应的用户名和密码。groupId、artifactId、version根据自己的情况设置就可以,我设置的是: com.jd.plugin:helloplugin:1.0.2-SNAPSHOT
这个后面会用到,这里先贴出来。
同时需要在项目的resourcs目录下新建一个properties文件,告诉插件工工程插件的入口类是哪个,具体实现如下:
a、 在resourcs新建META-INF/gradle-plugins目录
b、 在gradle-plugins下新建一个properties文件,这里我们新建的文件为hellogradleplugin.properties,这里的hellogradleplugin是插件的id,使用插件时会用到
c、 在hellogradleplugin.properties写入实现插件的路径,如:implementation-class=com.jd.plugin.HelloGradlePlugin
实现后的如图8所示:
图8
所有参数配置好之后,打开gradle界面,点击reload all gradle projects按钮,待reload完成后,在tasks列表界面会看到新增了publishing task,双击其中的publishMavenPublicationToMavenRepository task即可完成工程的发布,如图9。
图9
至此我们所有的准备工作都准备完成了,如果一个Android工程想使用我们的插件需要如下配置:
1、 在工程的根目录build.gradle添加插件的classpath:
classpath "com.jd.plugin:helloplugin:1.0.0-SNAPSHOT"
2、 在工程的app module引入插件
plugins {
id 'hellogradleplugin'
}
3、sync代码
ASM是一个通用的Java字节码操作和分析框架。它可以用于修改现有类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从中构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但侧重于性能。由于它的设计和实现尽可能小和快,因此非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。
从ASM官网(https://asm.ow2.io/)中我们可以看到其代码主要包含在以下六个包中:
org.objectweb.asm
org.objectweb.asm.commons
org.objectweb.asm.signature
org.objectweb.asm.tree
org.objectweb.asm.tree.analysis
org.objectweb.asm.util
其中org.objectweb.asm是核心包。它定义了ASM Visitor API,并提供ClassReader和ClassWriter类来读写编译后的Java class文件,此软件包不依赖于其他软件包,可以单独使用。我们后面的demo中主要用到了这个包提供的能力。
org.objectweb.asm.tree在核心包提供的类似SAX的API之上提供了类似DOM的API。在核心包实现太复杂而无法使用的情况下,它可以用来实现复杂的类转换操作。
org.objectweb.asm.commons提供了一些基于核心包和树包的有用的类适配器。这些适配器可以按原样使用,也可以扩展以实现更具体的类转换。
其它包的使用情况大家可以在官网自行阅读学习,这里就不做过多介绍了。此次Demo中我们主要用到了org.objectweb.asm和org.objectweb.asm.commons包下的内容,用到的类包括:
ClassReader:一个解析器,可以让ClassVisitor像访问jvm中的Java文件一样访问class文件的结构
ClassVisitor:Java class文件的访问器,可以访问class文件的结构
MethodVisitor:Java 方法的访问器
AdviceAdapter:一个MethodVisitor,可以在方法或构造器之前、之后替换代码或替换这些方法
ClassWriter:一个类访问器ClassVisitor,可以生成相应的class文件结构。
从这些类的介绍中我们知道ClassReader是一个入口类,只有它读取了.class文件才能让后面的Visitor访问每个类的结构,那么class文件我们从哪获得呢?
现在我们就要到前面自定义的Transform中去寻找答案了,前面说过我们可以在Transform类的void transform(TransformInvocation transformInvocation)方法中通过DirectoryInput集合和JarInput集合拿到所有的.class文件集合,如果把这些class文件作为ASM的输入那Transform和ASM就可以无缝衔接起来了。先上代码:
图10
图11
图12
图10主要是拿到DirectoryInput集合和JarInput集合后对两个集合分别进行遍历拿到集合下的所有.class文件,图11是对DirectoryInput集合的具体操作,主要是对集合下的文件判断是否符合想要遍历的文件要求(比如文件后缀、文件名等),如果符合要求则读取字节码,使用Visitor访问.class文件从而进行插桩操作,具体的插桩操作在我们自定义的HelloVisitor类中进行。
下面我们以扫描TelephonyManager类的getNetworkType()方法并上报的过程为例,向大家介绍插桩操作的具体流程。
1)、新建一个App工程作为我们的验证工程,创建一个Activity或其它类并写好getNetworkType()方法的调用,如图13所示
图13
定义好进行上报的方法,这里用PrivacyReportUtil.reportPrivacyApi()方法进行上报,如图14所示。
图14
回到前面讲到的ClassVisitor,通过API文档我们知道成员函数MethodVisitor visitMethod()是对字节码文件中所有方法进行遍历访问的函数,对需要插桩的方法的判断和操作需要在这个方法中进行。所以我们在这个方法中判断如果当前访问的指令的类TelephonyManager,方法是getNetworkType()则进行插入上报方法。
我们这里要插入的是PrivacyReportUtil.reportPrivacyApi()的字节码不是源码,对于不熟悉字节码的同学来说这是一个困难点,对于字节码的学习大家可以后续自己补充,这里介绍个简单的方法。借助工具:ASM Bytecode Viewer(Android Studio)或Show Bytecode outline(IDEA),这是两款插件,通过这些工具可以方便的从Java源码得到对应的字节码,在开发工具的插件库中搜索并安装对应插件。插件工具的具体使用可以自行网上搜索。
新增HelloMethodVisitor类对项目工程中的所有方法进行遍历,在void visitMethodInsn()方法中判断符合我们要求的方法并完成插桩,visitMethodInsn()方法表示在访问这个方法体的内容。
图15
如图15所示我们判断当前访问的被调用方法的所属类是android/telephony/TelephonyManager,并且方法的方法名是getNetworkType则判断符合插桩要求,进行插桩操作。
mv.visitLdcInsn()方法是向后面的上报方法传入参数,
mv.visitMethodInsn()方法是向当前方法体中插入方法,第一个参数是方法的可访问性,这里是静态方法传入INVOKESTATIC,第二个参数是插入方法的全路径类名,第三个参数是插入的方法名,第四个参数是对插入方法的参数和返回值描述,第五个参数是这个方法是否是接口。
以上插入的方法我们可以通过Bytecode工具直接获取到,直接复制过来就可以了。
至此我们的插桩代码编写完成,后面开始来验证代码的可用性。
重新按照发布插件工程步骤中说的再发布一次代码,然后在测试Demo中配置插件,如图16和图17所示:
图16
图17
Sync后重新编译代码,在生成的apk的dex文件中或build/../javac文件下找到对应的类,我们发现上报方法已经插入了到了getNetworkType()被调用的地方,如图18所示。
图18
技术方案调研完成后我们迅速在实际场景中进行了落地实验,为趋于接近实际场景运行,我们增加了一个判断逻辑让扫描插桩操作只在debug编译环境下进行,最大程度减少对Release版本的影响,判断逻辑如图19。
图19
此外我们还与部门推出的SunGlasses工具相结合,使我们的扫描结果可以上报到服务后台中,从而可以在可视化页面中查看所有隐私api的调用情况。从可视化页面中我们可以清晰的看到当前APP中有哪些隐私api被调用和被哪些方法调用,从而可以快速的定位负责人进行修改,图19是Sunglasses平台展示的部分上报信息。
图20
动态扫描工具的开发很好的助力了隐私合规检测整改工作,因为做到了快速定位隐私api调用链,从而可以轻松的确定修改负责人,极大提高了整改时效,是一次很好的技术助力业务的落地实践。
随着隐私监管政策越来越严格,每次扫描-排查-整改的低效率方式必然不是一个可取的整改策略,目前我们在做隐私api整体收归的调研工作,通过ASM具有的替换方法的能力实现对APP全部隐私api的整体收归,从而避免隐私API被无序使用、频繁调用等问题,从根本上解决隐私整改问题。
AOP开发是降低项目耦合度,提高程序开发效率的有效方式,但由于ASM入门门槛较高,且相关的入门资料比较少,所以ASM推广起来比较困难,希望更多的优秀人士加入到ASM的阵营中舔砖加瓦,助力更多ASM成果的产出。
横冲直撞总比坐以待毙高明的多!
本文只是通过简单的例子讲解了ASM的使用过程,但ASM中还有许多更神奇强大的功能等待我们去挖掘,期待大家忍受横冲直撞的痛苦,享受云消雾散后的快乐!