区伯肺癌病重:一位逐渐被遗忘的广州公民

前外交部副部长傅莹:一旦中美闹翻,有没有国家会站在中国一边

Weixin Official Accounts Platform

去泰国看了一场“成人秀”,画面尴尬到让人窒息.....

多年来,中国有个省,几乎每一个村庄都在偷偷“虎门销烟”

生成图片,分享到微信朋友圈

自由微信安卓APP发布,立即下载! | 提交文章网址
查看原文

一文学会字节码替换,再也不用担心隐私合规审核

徐公 2022-09-24

作者:miaowmiaow
来源:https://juejin.cn/post/7121985493445083149

前言

随着各平台对隐私合规愈加严格, APP 上架也变得愈加困难。大厂SDK有反馈都会有解决,但很多第三方库作者已经不维护,想想就让人脑壳疼😿。

ASM

ASM是一种通用Java字节码操作和分析框架。它可以用于修改现有的class文件或动态生成class文件。

Core API 和 Tree API

ASM分成两部分,Core APITree API
如何描述两者之间的关系呢?
Tree API是在Core API的基础上构建而来。直白点就是Core API非常节约内存,但是编程难度较大,Tree API消耗内存多,但是编程比较简单。

原理

通过ASMTree 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}
按照下图新建模块并创建文件
企业微信截图_20220719145946.png
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`生成插件
企业微信截图_20220719151548.png
在根目录`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编写如下测试代码:

企业微信截图_20220719152533.png

打包并反编译APK的源码,发现目标代码已经替换成功

企业微信截图_20220719152514.png

再看看coil的源码,发现目标代码也替换成功

企业微信截图_20220719152635.png

Thanks

以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。如果喜欢的话希望点个赞吧,您的鼓励是我前进的动力。谢谢~~

项目地址

  • github: fragmject

推荐阅读:

Android IO监控 | 性能监控系列

程序员该如何写好自己的简历,一位 5 年中大厂老哥跟你聊聊

面试官:简历上最好不要写Glide,不是问源码那么简单

Android 快速适配 64 位架构

ConstraintLayout最详细使用,减少嵌套优化ui,提升app性能

Android 代码覆盖率如何实现


文章有问题?点此查看未经处理的缓存