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

1,872 阅读3分钟

前言

随着各平台对隐私合规愈加严格,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基础的读者阅读,如有不正确的地方欢迎指出。

class ScanClassNode(
    private val classVisitor: ClassVisitor,
    private val scans: List<ScanBean>, //配置的对象(包含目标信息和替换信息)
) : ClassNode(Opcodes.ASM9) { //ASM Tree API 会把 class 文件包装成 ClassNode 方便我们操作

    override fun visitEnd() { //顾名思义访问完成的回调,我们在这里可以获取 class 文件的所有字段和方法
	//遍历所有方法
        methods.forEach { methodNode ->
            val instructions = methodNode.instructions
            //遍历方法内的每一行代码
            val iterator = instructions.iterator()
            while (iterator.hasNext()) {
                val insnNode = iterator.next()
		//ASM Tree API  会把字段包装成 FieldInsnNode ,方法包装成 MethodInsnNode
		//查找目标字段或方法
                if (insnNode is FieldInsnNode) {
                    //以Build.BRAND举例,对应的 owner = "android/os/Build",name = "BRAND",desc = "Ljava/lang/String;"
                    scans.find {
                        it.owner == insnNode.owner && it.name == insnNode.name && it.desc == insnNode.desc
                    }?.let {
			//通过 instructions.set 替换目标字段
                        instructions.set(insnNode, newInsnNode(it))
                    }
                }
                if (insnNode is MethodInsnNode) {
                    //以OnClickListener.onClick(View v)举例,对应的 owner = "Landroid/view/View$OnClickListener;",name = "onClick",desc = "(Landroid/view/View;)V"
                    scans.find {
                        it.owner == insnNode.owner && it.name == insnNode.name && it.desc == insnNode.desc
                    }?.let {
			//通过 instructions.set 替换目标方法
                        instructions.set(insnNode, newInsnNode(it))
                    }
                }
            }
        }
        super.visitEnd()
	//将 ClassNode 类中字段的值传递给下一个 ClassVisitor 类实例
        accept(classVisitor)
    }

    //构建替换的字段或方法
    private fun newInsnNode(bean: ScanBean): AbstractInsnNode {
        val opcode = bean.replaceOpcode
        val owner = bean.replaceOwner
        val name = bean.replaceName
        val descriptor = bean.replaceDesc
        return if (!bean.replaceDesc.startsWith("(")) { //根据"("判断字段或方法
            FieldInsnNode(opcode, owner, name, descriptor)
        } else {
            MethodInsnNode(opcode, owner, name, descriptor, false)
        }
    }
}

至此我们的核心代码已经编写完毕,接下来便是介绍如何通过AGP7.0生成插件及依赖和使用 AGP并不是本文的重点这边就以贴代码为主配以少量的注释

AGP7.0 编写插件

开发环境

Android Studio Bumblebee (2021.1.1) 🐝Android Gradle 7.1.2

首先在settings.gradle添加如下代码:
pluginManagement {
    repositories {
        maven {
            url uri('repo')
        }
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
}
按照下图新建模块并创建文件

企业微信截图_20220719145946.png

build.gradle
plugins {
    id 'kotlin'
    id 'kotlin-kapt'
    id 'maven-publish'
}

dependencies {
    implementation gradleApi() // 需要在 settings.gradle 设置 RepositoriesMode.PREFER_PROJECT
    implementation localGroovy()

    implementation 'org.ow2.asm:asm:9.3'
    implementation 'org.ow2.asm:asm-commons:9.3'
    implementation 'org.ow2.asm:asm-analysis:9.3'
    implementation 'org.ow2.asm:asm-util:9.3'
    implementation 'org.ow2.asm:asm-tree:9.3'
    implementation "com.android.tools.build:gradle:$gradle_version", {
        exclude group: 'org.ow2.asm'
    }
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            groupId = 'com.example.miaow'
            artifactId = 'plugin'
            version = '1.0.0'

            from components.java
        }
    }
    repositories {
        maven {
            //输出路径
            url  = parent.layout.projectDirectory.dir('repo') // settings.gradle 记得配置
        }
    }
}
miaow.properties
implementation-class=com.example.miaow.plugin.MiaowPlugin
ScanBean
class ScanBean(
    var owner: String = "",
    var name: String = "",
    var desc: String = "",
    var replaceOpcode: Int = 0,
    var replaceOwner: String = "",
    var replaceName: String = "",
    var replaceDesc: String = "",
) : Cloneable, Serializable {

    public override fun clone(): ScanBean {
        return try {
            super.clone() as ScanBean
        } catch (e: CloneNotSupportedException) {
            e.printStackTrace()
            ScanBean()
        }
    }

}
ScanClassVisitorFactory
//定义 ScanClassVisitorFactory 需要的参数(AGP的语法不需要纠结)
interface ScanParams : InstrumentationParameters {
    @get:Input
    val ignoreOwner: Property<String>

    @get:Input
    val listOfScans: ListProperty<ScanBean>
}

abstract class ScanClassVisitorFactory : AsmClassVisitorFactory<ScanParams> {

    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        return ScanClassNode(
            nextClassVisitor,
            parameters.get().listOfScans.get(),
        )
    }

    override fun isInstrumentable(classData: ClassData): Boolean {
        return !classData.className.startsWith(parameters.get().ignoreOwner.get().replace("/", "."))
    }

}
MiaowPlugin
class MiaowPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.onVariants { variant ->
            variant.transformClassesWith(
                ScanClassVisitorFactory::class.java,
                InstrumentationScope.ALL
            ) {
                //配置忽略路径
                it.ignoreOwner.set("com/example/fragment/library/common/utils/BuildUtils")
                //配置目标信息和替换信息
                it.listOfScans.set(
                    listOf(
                        ScanBean(
                            "android/os/Build",
                            "BRAND",
                            "Ljava/lang/String;",
                            Opcodes.INVOKESTATIC,
                            "com/example/fragment/library/common/utils/BuildUtils",
                            "getBrand",
                            "()Ljava/lang/String;"
                        ),
                        ScanBean(
                            "android/os/Build",
                            "MODEL",
                            "Ljava/lang/String;",
                            Opcodes.INVOKESTATIC,
                            "com/example/fragment/library/common/utils/BuildUtils",
                            "getModel",
                            "()Ljava/lang/String;"
                        ),
                        ScanBean(
                            "android/os/Build",
                            "SERIAL",
                            "Ljava/lang/String;",
                            Opcodes.INVOKESTATIC,
                            "com/example/fragment/library/common/utils/BuildUtils",
                            "getSerial",
                            "()Ljava/lang/String;"
                        ),
                        ScanBean(  //传感器检测
                            "android/hardware/SensorManager",
                            "getSensorList",
                            "(I)Ljava/util/List;"
                        ),
                    )
                )
            }
            variant.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
        }
    }

}
按照下图执行publish生成插件

企业微信截图_20220719151548.png

在根目录build.gradle添加插件依赖
buildscript {
    dependencies {
        classpath 'com.example.miaow:plugin:1.0.0'
    }
}
在app目录 build.gradle apply插件
plugins {
    id 'miaow'
}

测试

MainActivity编写如下测试代码:

企业微信截图_20220719152533.png

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

企业微信截图_20220719152514.png

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

企业微信截图_20220719152635.png

Thanks

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

项目地址