运用AsmClassVisitorFactory完结安全整改

前言

前面写了一篇文章(《利用ASM完结第三方SDK安全整改》)对项目中的安全缝隙做了些修正,里边有提到Transform在AGP7.0被标记为抛弃,作为一个好奇的安卓开发,我觉得还是有必要学学被抛弃后的新办法的-_-||,所以花了点时间,找了下材料,尝试了下,顺便记录下。

Gradle版别要求

这儿gradle版别当然需求升级到7.x才能运用,计划升级并且想用kts的话能够看下我之前的文章:

《记录迁移gradle到kts》

不想升级还想运用ASM修正字节码的话,能够看下Transform那种办法(这儿也要求gradle升级到6.1.1,AGP版别4.0):

《利用ASM完结第三方SDK安全整改》


这儿说下我的版别装备: Gradle Version 7.5.1,AGP 7.4.2。

编写插件

关于Gradle插件编写的内容,我之前也写了一篇文章,有需求的能够看下:

《Gradle自界说插件实践与总结》

挑选运用buildSrc编写插件的话,能够越过这节,直接看AsmClassVisitorFactory部分,代堆放buildSrc里边就行。


运用Transform办法的那篇文章里,我用的是发布到本地maven仓库的形式运用插件,当时没搞懂Composing build里边的插件,又学了学,这篇文章就用Composing build来做吧。

Composing build编写插件

这儿从头说清楚吧,Composing build实际便是多项目构建,咱们先创立一个项目,在根目录下新建一个build-plugins目录,里边创立两个文件以及代码目录,结构如下:

// 用我代码举例了,包名自己界说
build-plugins
|--src/main/java/com/silencefly96/plugins/privacy
|--|--PrivacyPlugin.kt
|--build.gradle.kts
|--settings.gradle.kts

在settings.gradle.kts填入如下代码:

@file:Suppress("UnstableApiUsage")
pluginManagement {
    repositories {
        // 是用于从 Gradle 插件门户下载插件的默许仓库。
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "build-plugins"
include (":build-plugins")

留意下这儿把rootProject指向了build-plugins,这样就不分项目的build.gradle.kts和模块的build.gradle.kts了,两个放一起了。

下面便是两个放一起的build.gradle.kts,代码如下:

buildscript {
    // 我这不加有问题,按道理repositories是不必的
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:7.4.2")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.0")
    }
}
plugins {
    `kotlin-dsl`
}
// 插件的依靠联系
dependencies {
    implementation(gradleApi())
    implementation("com.android.tools.build:gradle:7.4.2")
    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.0")
}
gradlePlugin {
    // 注册插件
    plugins.register("privacyPlugin") {
        id = "privacy-plugin"
        implementationClass = "com.silencefly96.plugins.privacy.PrivacyPlugin"
    }
}

仔细的可能会发现gradle和kotlin-gradle-plugin咱们引入了两次,留意下buildscript里边的是给gradle脚本用的(classpath),下面dependencies里边的是给自己代码运用的(implementation)。

装备好这些咱们就来写PrivacyPlugin的代码:

package com.silencefly96.plugins.privacy
import org.gradle.api.Plugin
import org.gradle.api.Project
class PrivacyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        println("PrivacyPlugin")
    }
}

这儿就随意打印了下名称,sync一下就能在项目的module中运用了。

在住项目的根目录的settings.gradle.kts(和build-plugins区分开来)中引入build-plugins模块,要运用includeBuild:

...
include(":app")
...
includeBuild("build-plugins")

然后在要运用的模块的build.gradle.kts中依据插件id装备:

plugins {
    id("privacy-plugin")
    ...
}

build一下,控制台应该就会打印”PrivacyPlugin”了,至此插件咱们就写好了,接下来便是重点的AsmClassVisitorFactory环节。

AsmClassVisitorFactory运用

我觉得嘛,其实AsmClassVisitorFactory便是咱们之前的Transform,这儿在上面PrivacyPlugin同目录下新建一个PrivacyTransform(命名随意),里边来写AsmClassVisitorFactory代码:

package com.silencefly96.plugins.privacy;
import com.android.build.api.instrumentation.*
import org.objectweb.asm.ClassVisitor
// 留意这儿需求一个抽象类!
abstract class PrivacyTransform: AsmClassVisitorFactory<InstrumentationParameters.None> {
    override fun createClassVisitor(
        classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor {
        // 创立自界说的ClassVisitor并回来
        return PrivacyClassVisitor(nextClassVisitor, classContext.currentClassData.className)
    }
    // 过滤处理的class
    override fun isInstrumentable(classData: ClassData): Boolean {
        // 处理className: com.silencefly96.module_base.base.BaseActivity
        val className = with(classData.className) {
            val index = lastIndexOf(".") + 1
            substring(index)
        }
        // 筛选要处理的class
        return !className.startsWith("R$")
                && "R" != className
                && "BuildConfig" != className
                // 这两个我加的,替代的类小心无限迭代
                && !classData.className.startsWith("android")
                && "AsmMethods" != className
    }
}

这儿就两步,一个是创立自界说的ClassVisitor,里边实现ASM代码逻辑,第二个是对class的过滤,看自己需求吧,直接回来true也行。

写好AsmClassVisitorFactory后,需求在上面的PrivacyPlugin里边注册下:

package com.silencefly96.plugins.privacy
import com.android.build.api.instrumentation.FramesComputationMode
import com.android.build.api.instrumentation.InstrumentationScope
import com.android.build.api.variant.AndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
class PrivacyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val androidComponents =
            project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.onVariants { variant ->
            // 控制是否需求扫描依靠库代码, ALL / PROJECT
            variant.instrumentation.transformClassesWith(
                PrivacyTransform::class.java,
                InstrumentationScope.ALL
            ) {}
            // 可设置不同的栈帧核算形式
            variant.instrumentation.setAsmFramesComputationMode(
                FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
            )
        }
    }
}

这儿能够侧重看下InstrumentationScope.ALL和InstrumentationScope.PROJECT,之前的Transform的Scope可是有七种啊,这儿只有两了,如果要对SDK修正的话就设置为ALL吧。

PrivacyClassVisitor编写

上面自界说的ClassVisitor传入了一个PrivacyClassVisitor,下面就写下它的代码:

package com.silencefly96.plugins.privacy
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
class PrivacyClassVisitor(nextVisitor: ClassVisitor, private val className: String)
    : ClassVisitor(Opcodes.ASM7, nextVisitor) {
    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
        val newMethodVisitor = object: MethodVisitor(Opcodes.ASM7, methodVisitor) {
            override fun visitMethodInsn(
                opcode: Int,
                owner: String,
                name: String,
                descriptor: String,
                isInterface: Boolean
            ) {
                // 替换说明:
                // 1. 途径以”/“分割,而不是包名里边的”.“
                // 2. owner前不带”L“字符,descriptor内都要加上”L“字符
                // 3. descriptor里边参数及回来值类型后的”;“不能省,特别是参数列表最终一个参数后的”;“
                // 4. descriptor里边根本类型(比如V、Z)后不能添加”;“,不然匹配不上
                // 5. 办法签名一定要写对,参数及回来值的类型,抛出的异常不算办法签名
                // 6. 替换办法前后变量一定要对应,实例办法0位置是this,改为静态办法时,要用第一个参数去接收;
                // 7. 替换办法前后,参数加回来值的数量要持平
                // 替换调用 Environment.getExternalStorageDirectory() 的地方为应用程序的本地目录
                if (opcode == Opcodes.INVOKESTATIC && owner == "android/os/Environment" && name == "getExternalStorageDirectory" && descriptor == "()Ljava/io/File;") {
                    println("处理SD卡数据泄漏危险: $className")
                    mv.visitMethodInsn(
                        Opcodes.INVOKESTATIC,
                        "com/silencefly96/module_base/utils/AsmMethods",
                        "getExternalDir",
                        "()Ljava/io/File;",
                        false
                    )
                } else if (opcode == Opcodes.INVOKEVIRTUAL && name == "registerReceiver" && descriptor == "(Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;") {
                    // && owner.equals("android/content/Context")
                    println("处理动态注册播送: $className")
                    // 调用你自界说的办法,并传递 Context 和参数
                    mv.visitMethodInsn(
                        Opcodes.INVOKESTATIC,
                        "com/silencefly96/module_base/utils/AsmMethods",
                        "registerZxyReceiver",
                        "(Landroid/content/Context;Landroid/content/BroadcastReceiver;Landroid/content/IntentFilter;)Landroid/content/Intent;",
                        false
                    )
                } else if (opcode == Opcodes.INVOKEVIRTUAL && owner == "android/database/sqlite/SQLiteDatabase" && name == "rawQuery" && descriptor == "(Ljava/lang/String;[Ljava/lang/String;)Landroid/database/Cursor;") {
                    println("处理SQL数据库注入缝隙 rawQuery: $className")
                    mv.visitMethodInsn(
                        Opcodes.INVOKESTATIC,
                        "com/silencefly96/module_base/utils/AsmMethods",
                        "rawZxyQuery",
                        "(Landroid/database/sqlite/SQLiteDatabase;Ljava/lang/String;[Ljava/lang/String;)Landroid/database/Cursor;",
                        false
                    )
                } else if (opcode == Opcodes.INVOKEVIRTUAL && owner == "android/database/sqlite/SQLiteDatabase" && name == "execSQL" && descriptor == "(Ljava/lang/String;)V") {
                    println("处理SQL数据库注入缝隙 execSQL: $className")
                    mv.visitMethodInsn(
                        Opcodes.INVOKESTATIC,
                        "com/silencefly96/module_base/utils/AsmMethods",
                        "execZxySQL",
                        "(Landroid/database/sqlite/SQLiteDatabase;Ljava/lang/String;)V",
                        false
                    )
                } else if (opcode == Opcodes.INVOKEVIRTUAL && owner == "java/util/zip/ZipEntry" && name == "getName" && descriptor == "()Ljava/lang/String;") {
                    println("处理ZipperDown缝隙: $className")
                    mv.visitMethodInsn(
                        Opcodes.INVOKESTATIC,
                        "com/silencefly96/module_base/utils/AsmMethods",
                        "getZipEntryName",
                        "(Ljava/util/zip/ZipEntry;)Ljava/lang/String;",
                        false
                    )
                } else if (opcode == Opcodes.INVOKESTATIC && owner == "android/util/Log" && descriptor == "(Ljava/lang/String;Ljava/lang/String;)I") {
                    println("处理日志函数泄露危险 $name: $className")
                    if (name == "e") {
                        // 错误日志还是有用的
                        mv.visitMethodInsn(
                            Opcodes.INVOKESTATIC,
                            "com/silencefly96/module_base/utils/AsmMethods",
                            "optimizeLogE",
                            "(Ljava/lang/String;Ljava/lang/String;)I",
                            false
                        )
                    } else {
                        mv.visitMethodInsn(
                            Opcodes.INVOKESTATIC,
                            "com/silencefly96/module_base/utils/AsmMethods",
                            "optimizeLog",
                            "(Ljava/lang/String;Ljava/lang/String;)I",
                            false
                        )
                    }
                } else if (opcode == Opcodes.INVOKEVIRTUAL && owner == "android/webkit/WebSettings" && name == "setJavaScriptEnabled" && descriptor == "(Z)V") {
                    println("处理Webview组件跨域拜访危险: $className")
                    mv.visitMethodInsn(
                        Opcodes.INVOKESTATIC,
                        "com/silencefly96/module_base/utils/AsmMethods",
                        "setZxyJsEnabled",
                        "(Landroid/webkit/WebSettings;Z)V",
                        false
                    )
                } else if (opcode == Opcodes.INVOKEVIRTUAL && owner == "com/tencent/smtt/sdk/WebSettings" && name == "setJavaScriptEnabled" && descriptor == "(Z)V") {
                    println("处理X5Webview组件跨域拜访危险: $className")
                    mv.visitMethodInsn(
                        Opcodes.INVOKESTATIC,
                        "com/silencefly96/module_base/utils/AsmMethods",
                        "setZxyX5JsEnabled",
                        "(Lcom/tencent/smtt/sdk/WebSettings;Z)V",
                        false
                    )
                } else {
                    super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
                }
            }
        }
        return newMethodVisitor
    }
}

还是原来ASM替代的代码,就不多解释了,不过这儿明显比之前简单多了啊,不错!

仅有需求留意的是ASM的版别,我这要求Opcodes.ASM7,低了会报错,这问题遇到好多次了-_-||

关于用来替换的AsmMethods类,读者能够自己编写,需求要留意的是这个类里边别被替代搞得无限迭代了,另外一个便是kotlin静态办法记住加上JvmStatic注解:

// 留意包名一致啊!
package com.silencefly96.module_base.utils
object AsmMethods {
    // ASM替换代码勿动: 替换获取外部文件
    @JvmStatic
    fun getExternalDir(): File {
        var result = File("")
        // ...
        return result
    }

运用

上面代码写好的话,目录全体结构如下(忽略我剩余的文件):

运用AsmClassVisitorFactory完结安全整改

在要运用的地方参加插件,比如我这是app模块:

plugins {
    id("privacy-plugin")
}

app的MainActivity放了个测试用的代码:

fun onTestRegisterZxyReceiver() {
    val cw: ContextWrapper = object : ContextWrapper(this) {
        override fun registerReceiver(
            receiver: BroadcastReceiver?,
            filter: IntentFilter
        ): Intent? {
            Log.d("TAG", "ContextWrapper registerReceiver: ")
            return super.registerReceiver(receiver, filter)
        }
    }
    val receiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            Log.d("TAG", "onReceive: " + intent.action)
        }
    }
    val intentFilter = IntentFilter()
    intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
    Log.d("TAG", "registerZxyReceiver: invoke before")
    cw.registerReceiver(receiver, intentFilter)
}

在AS中挑选rebuild,一会在控制台就能看到ASM处理的输出了,速度比之前Transform方法还更快(这个是有增量更新的):

运用AsmClassVisitorFactory完结安全整改

看下输出,打印了很多,瞄一眼咱们在MainActivity内的有打印,如果说你觉得打印不能证明ASM修正成功,咱们能够继续看下APK包:

运用AsmClassVisitorFactory完结安全整改

点开MainActivity的字节码看一下:

运用AsmClassVisitorFactory完结安全整改

依据字节码对应的代码行数,对比下源码位置:

运用AsmClassVisitorFactory完结安全整改

第46行对日志的替换,第47行对动态注册播送的替换是不是生效了,(●∀●)

文章参考及源码

参考文章:

现在准备好离别Transform了吗? | 拥抱AGP7.0

android官方文档


Demo源码(可能随时有改动,练手的项目)

总结

这篇文章用了Composing build的方法编写了gradle的插件,并运用gradle7.x的AsmClassVisitorFactory来对项目及SDK的代码进行整改,学习了!