一文学会字节码替换,再也不用担心隐私合规审核
作者:miaowmiaow
来源:https://juejin.cn/post/7121985493445083149
前言
随着各平台对隐私合规愈加严格, APP 上架也变得愈加困难。大厂SDK有反馈都会有解决,但很多第三方库作者已经不维护,想想就让人脑壳疼😿。
ASM
ASM是一种通用Java字节码操作和分析框架。它可以用于修改现有的class文件或动态生成class文件。
Core API 和 Tree API
ASM分成两部分,Core API和Tree API。
如何描述两者之间的关系呢?Tree API是在Core API的基础上构建而来。直白点就是Core API非常节约内存,但是编程难度较大,Tree API消耗内存多,但是编程比较简单。
原理
通过ASM的Tree API去查找并替换class文件中的目标字段或方法。
编码实现
这边尽量使用简单的语言描述,方便没有ASM基础的读者阅读,如有不正确的地方欢迎指出。
1class ScanClassNode(
2 private val classVisitor: ClassVisitor,
3 private val scans: List<ScanBean>, //配置的对象(包含目标信息和替换信息)
4) : ClassNode(Opcodes.ASM9) { //ASM Tree API 会把 class 文件包装成 ClassNode 方便我们操作
5
6 override fun visitEnd() { //顾名思义访问完成的回调,我们在这里可以获取 class 文件的所有字段和方法
7 //遍历所有方法
8 methods.forEach { methodNode ->
9 val instructions = methodNode.instructions
10 //遍历方法内的每一行代码
11 val iterator = instructions.iterator()
12 while (iterator.hasNext()) {
13 val insnNode = iterator.next()
14 //ASM Tree API 会把字段包装成 FieldInsnNode ,方法包装成 MethodInsnNode
15 //查找目标字段或方法
16 if (insnNode is FieldInsnNode) {
17 //以Build.BRAND举例,对应的 owner = "android/os/Build",name = "BRAND",desc = "Ljava/lang/String;"
18 scans.find {
19 it.owner == insnNode.owner && it.name == insnNode.name && it.desc == insnNode.desc
20 }?.let {
21 //通过 instructions.set 替换目标字段
22 instructions.set(insnNode, newInsnNode(it))
23 }
24 }
25 if (insnNode is MethodInsnNode) {
26 //以OnClickListener.onClick(View v)举例,对应的 owner = "Landroid/view/View$OnClickListener;",name = "onClick",desc = "(Landroid/view/View;)V"
27 scans.find {
28 it.owner == insnNode.owner && it.name == insnNode.name && it.desc == insnNode.desc
29 }?.let {
30 //通过 instructions.set 替换目标方法
31 instructions.set(insnNode, newInsnNode(it))
32 }
33 }
34 }
35 }
36 super.visitEnd()
37 //将 ClassNode 类中字段的值传递给下一个 ClassVisitor 类实例
38 accept(classVisitor)
39 }
40
41 //构建替换的字段或方法
42 private fun newInsnNode(bean: ScanBean): AbstractInsnNode {
43 val opcode = bean.replaceOpcode
44 val owner = bean.replaceOwner
45 val name = bean.replaceName
46 val descriptor = bean.replaceDesc
47 return if (!bean.replaceDesc.startsWith("(")) { //根据"("判断字段或方法
48 FieldInsnNode(opcode, owner, name, descriptor)
49 } else {
50 MethodInsnNode(opcode, owner, name, descriptor, false)
51 }
52 }
53}
至此我们的核心代码已经编写完毕,接下来便是介绍如何通过AGP7.0生成插件及依赖和使用 AGP并不是本文的重点这边就以贴代码为主配以少量的注释
AGP7.0 编写插件
开发环境
Android Studio Bumblebee (2021.1.1) 🐝和Android Gradle 7.1.2。
首先在`settings.gradle`添加如下代码:
1pluginManagement {
2 repositories {
3 maven {
4 url uri('repo')
5 }
6 }
7}
8dependencyResolutionManagement {
9 repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
10}
按照下图新建模块并创建文件
build.gradle
1plugins {
2 id 'kotlin'
3 id 'kotlin-kapt'
4 id 'maven-publish'
5}
6
7dependencies {
8 implementation gradleApi() // 需要在 settings.gradle 设置 RepositoriesMode.PREFER_PROJECT
9 implementation localGroovy()
10
11 implementation 'org.ow2.asm:asm:9.3'
12 implementation 'org.ow2.asm:asm-commons:9.3'
13 implementation 'org.ow2.asm:asm-analysis:9.3'
14 implementation 'org.ow2.asm:asm-util:9.3'
15 implementation 'org.ow2.asm:asm-tree:9.3'
16 implementation "com.android.tools.build:gradle:$gradle_version", {
17 exclude group: 'org.ow2.asm'
18 }
19}
20
21publishing {
22 publications {
23 mavenJava(MavenPublication) {
24 groupId = 'com.example.miaow'
25 artifactId = 'plugin'
26 version = '1.0.0'
27
28 from components.java
29 }
30 }
31 repositories {
32 maven {
33 //输出路径
34 url = parent.layout.projectDirectory.dir('repo') // settings.gradle 记得配置
35 }
36 }
37}
miaow.properties
1implementation-class=com.example.miaow.plugin.MiaowPlugin
ScanBean
1class ScanBean(
2 var owner: String = "",
3 var name: String = "",
4 var desc: String = "",
5 var replaceOpcode: Int = 0,
6 var replaceOwner: String = "",
7 var replaceName: String = "",
8 var replaceDesc: String = "",
9) : Cloneable, Serializable {
10
11 public override fun clone(): ScanBean {
12 return try {
13 super.clone() as ScanBean
14 } catch (e: CloneNotSupportedException) {
15 e.printStackTrace()
16 ScanBean()
17 }
18 }
19
20}
ScanClassVisitorFactory
1//定义 ScanClassVisitorFactory 需要的参数(AGP的语法不需要纠结)
2interface ScanParams : InstrumentationParameters {
3 @get:Input
4 val ignoreOwner: Property<String>
5
6 @get:Input
7 val listOfScans: ListProperty<ScanBean>
8}
9
10abstract class ScanClassVisitorFactory : AsmClassVisitorFactory<ScanParams> {
11
12 override fun createClassVisitor(
13 classContext: ClassContext,
14 nextClassVisitor: ClassVisitor
15 ): ClassVisitor {
16 return ScanClassNode(
17 nextClassVisitor,
18 parameters.get().listOfScans.get(),
19 )
20 }
21
22 override fun isInstrumentable(classData: ClassData): Boolean {
23 return !classData.className.startsWith(parameters.get().ignoreOwner.get().replace("/", "."))
24 }
25
26}
MiaowPlugin
1class MiaowPlugin : Plugin<Project> {
2
3 override fun apply(project: Project) {
4 val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
5 androidComponents.onVariants { variant ->
6 variant.transformClassesWith(
7 ScanClassVisitorFactory::class.java,
8 InstrumentationScope.ALL
9 ) {
10 //配置忽略路径
11 it.ignoreOwner.set("com/example/fragment/library/common/utils/BuildUtils")
12 //配置目标信息和替换信息
13 it.listOfScans.set(
14 listOf(
15 ScanBean(
16 "android/os/Build",
17 "BRAND",
18 "Ljava/lang/String;",
19 Opcodes.INVOKESTATIC,
20 "com/example/fragment/library/common/utils/BuildUtils",
21 "getBrand",
22 "()Ljava/lang/String;"
23 ),
24 ScanBean(
25 "android/os/Build",
26 "MODEL",
27 "Ljava/lang/String;",
28 Opcodes.INVOKESTATIC,
29 "com/example/fragment/library/common/utils/BuildUtils",
30 "getModel",
31 "()Ljava/lang/String;"
32 ),
33 ScanBean(
34 "android/os/Build",
35 "SERIAL",
36 "Ljava/lang/String;",
37 Opcodes.INVOKESTATIC,
38 "com/example/fragment/library/common/utils/BuildUtils",
39 "getSerial",
40 "()Ljava/lang/String;"
41 ),
42 ScanBean( //传感器检测
43 "android/hardware/SensorManager",
44 "getSensorList",
45 "(I)Ljava/util/List;"
46 ),
47 )
48 )
49 }
50 variant.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
51 }
52 }
53
54}
按照下图执行`publish`生成插件
在根目录`build.gradle`添加插件依赖
1buildscript {
2 dependencies {
3 classpath 'com.example.miaow:plugin:1.0.0'
4 }
5}
在app目录 `build.gradle` apply插件
1plugins {
2 id 'miaow'
3}
测试
在MainActivity编写如下测试代码:
打包并反编译APK的源码,发现目标代码已经替换成功
再看看coil的源码,发现目标代码也替换成功
Thanks
以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。如果喜欢的话希望点个赞吧,您的鼓励是我前进的动力。谢谢~~
项目地址
github: fragmject
推荐阅读:
ConstraintLayout最详细使用,减少嵌套优化ui,提升app性能