Hook Android Gradle插件修改编译后的dex

本文着重介绍如何Hook Android Gradle插件的实现,涉及到的Gradle以及Groovy基础会稍微提下,具体可以参考文末给出的博客。

本文分为两部分:

1. Hook Android Gradle插件

2. 使用dexlib2修改编译中的dex文件--修改dex中的函数名字

1. Hook Android Gradle插件

Android Gradle Plugin可以简单的理解为是由好多task组成的,这么多task组成一个task graph。在build apk的时候有的task负责编译每个class,有的task负责组成dex,有的task负责打包生成apk。这些task按照task graph上的顺序依次执行生成一个apk。

Executing tasks: [:app:assembleDebug]
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:compileDebugAidl UP-TO-DATE
:app:compileDebugRenderscript UP-TO-DATE
:app:checkDebugManifest UP-TO-DATE
:app:generateDebugBuildConfig UP-TO-DATE
:app:prepareLintJar UP-TO-DATE
:app:mainApkListPersistenceDebug UP-TO-DATE
:app:generateDebugResValues UP-TO-DATE
:app:generateDebugResources UP-TO-DATE
:app:mergeDebugResources UP-TO-DATE
:app:createDebugCompatibleScreenManifests UP-TO-DATE
:app:processDebugManifest UP-TO-DATE
:app:splitsDiscoveryTaskDebug UP-TO-DATE
:app:processDebugResources UP-TO-DATE
:app:generateDebugSources UP-TO-DATE
:app:javaPreCompileDebug
:app:compileDebugJavaWithJavac
:app:compileDebugNdk NO-SOURCE
:app:compileDebugSources
:app:mergeDebugShaders
:app:compileDebugShaders
:app:generateDebugAsset
:app:mergeDebugAssets
:app:transformClassesWithDexBuilderForDebug
:app:transformDexArchiveWithExternalLibsDexMergerForDebug
:app:transformDexArchiveWithDexMergerForDebug
:app:mergeDebugJniLibFolders
:app:transformNativeLibsWithMergeJniLibsForDebug
:app:processDebugJavaRes NO-SOURCE
:app:transformResourcesWithMergeJavaResForDebug
:app:validateSigningDebug
:app:packageDebug
:app:assembleDebug
BUILD SUCCESSFUL in 8s
26 actionable tasks: 13 executed, 13 up-to-date

我们的目的是在Android gradle plugin自己build apk的时候hook transformDexArchiveWithDexMergerForDebug这个task,这个task可以在上图中找到,Android Gradle Plugin会把各个class编译生成dex,transformDexArchiveWithDexMergerForDebug这个task的作用就是把生成的各个dex组合成一个dex。所以我们看中了这个task,只要hook它,可以在它生成整体的dex后使用dexlib2更改其中的class或者method。

首先要先掌握如何自定义Android Gradle插件,这样把它打包上传后就可以在其他Android工程中直接使用了,很方便,这一步可以直接参考这个博客,写的很详细,一步一步按着做即可。在AndroidStudio中自定义Gradle插件 这个同学写的几篇关于Gradle插件的文章都可以看看,写的不错的。这个链接给出的教程只是开发一个独立的Android gradle插件,这个插件里面只有一个task,就是输出一句话而已,我们在直接工程中加入apply plugin:'XXXXX',再指定这个plugin的路径,build的时候就会输出这句话。这个插件的作用就是在Android gradle插件build project的时候加上了一个task。但是这个task是一个独立的task,并没有对Android gradle插件中的task作用,Android插件该怎么build怎么build,没有更改它的task graph,也谈不上hook。

下面直接上代码了,上面链接中已经讲述了如何生成一个Android gradle plugin。直接在上面代码的基础上改:

public class MyPlugin implements Plugin<Project> {

    void apply(Project project1) {
        System.out.println("========================");
        System.out.println("Hello gradle plugin!");
        System.out.println("========================");
        project1.afterEvaluate { project ->
            project.tasks.transformDexArchiveWithDexMergerForDebug << {
                println 'add my own step from plugin'
                //Get the inputs of this task
                project.tasks.transformDexArchiveWithDexMergerForDebug.getInputs().getFiles().collect().each { element1 ->
                    println "inputs " + element1
                }
                //Get the outputs of this task
                project.tasks.transformDexArchiveWithDexMergerForDebug.getOutputs().getFiles().collect().each() { element ->
                    def file = new File(element.toString())
                    def files = file.listFiles()
                    def files2 = files[0].listFiles()
                    String dexfilepath = files2[0]
                    println "Outputs Dex file's path: "+dexfilepath
                    //Modify the dex 可先注释掉
                    testRewrite(dexfilepath)
                }
            }
        }
    }
}

上述代码的作用很简单,里面的变量project就是Android Gradle plugin的project,然后它执行到transformDexArchiveWithDexMergerForDebug这个task的时候就加上自己的一个action,这个action很简单就是打印这个task的输入和输出,然后testRewrite()函数就是用来更改这个task生成的dex的。看一下我们hook的结果。

Executing tasks: [:app:assembleDebug]
========================
Hello gradle plugin!
========================
The Task.leftShift(Closure) method has been deprecated and is scheduled to be removed in Gradle 5.0. Please use Task.doLast(Action) instead.
:app:preBuild UP-TO-DATE
//省略了部分task
:app:transformClassesWithDexBuilderForDebug
:app:transformDexArchiveWithExternalLibsDexMergerForDebug
//这个task就是被我们hook的task
:app:transformDexArchiveWithDexMergerForDebug
add my own step from plugin
//很多inputs 这里只列出了一部分
inputs /Users/xxx/AndroidStudioProjects/gradleTest/app/build/intermediates/transforms/dexBuilder/debug/7.jar
inputs /Users/xxx/AndroidStudioProjects/gradleTest/app/build/intermediates/transforms/dexBuilder/debug/6.jar
inputs /Users/xxx/AndroidStudioProjects/gradleTest/app/build/intermediates/transforms/dexBuilder/debug/19/com/example/dean/gradletest/R$bool.dex
inputs /Users/xxx/AndroidStudioProjects/gradleTest/app/build/intermediates/transforms/dexBuilder/debug/19/com/example/dean/gradletest/R$integer.dex
inputs /Users/xxx/AndroidStudioProjects/gradleTest/app/build/intermediates/transforms/externalLibsDexMerger/debug/0/classes.dex
//output只是一个dex
Outputs Dex file's path: /Users/xxxAndroidStudioProjects/gradleTest/app/build/intermediates/transforms/dexMerger/debug/0/classes.dex
:app:mergeDebugJniLibFolders
:app:transformNativeLibsWithMergeJniLibsForDebug
:app:processDebugJavaRes NO-SOURCE
:app:transformResourcesWithMergeJavaResForDebug
:app:validateSigningDebug
:app:packageDebug
:app:assembleDebug
BUILD SUCCESSFUL in 4s
26 actionable tasks: 13 executed, 13 up-to-date

通过这种方式我们可以hook Android Gradle Plugin中的任意一个task。

2. 修改dex文件--修改dex中的函数名字

这里就需要用到dexlib2这个工具了,直接在自定义插件的build.gradle中的dependency中加入compile group: 'org.smali', name: 'dexlib2', version: '2.2.4',如下所示。注:不要自行下载jar包加载,这样这个jar包无法打包到自己的插件里,会出现bug。

dependencies {
    // https://mvnrepository.com/artifact/org.smali/dexlib2
    compile group: 'org.smali', name: 'dexlib2', version: '2.2.4'
    //gradle sdk
    compile gradleApi()
    //groovy sdk
    compile localGroovy()
}

在自定义插件类的统一文件中加入以下代码

 static void testRewrite(String dexfilepath){
        DexFile dexFile
        try {
            //把要修改的dex load进来
            dexFile = DexFileFactory.loadDexFile(dexfilepath, Opcodes.getDefault())
            println "dexFile: " + dexFile.getClass().getName()
            DexRewriter rewriter = new DexRewriter(new RewriterModule() {
                @Override
                Rewriter<org.jf.dexlib2.iface.Method> getMethodRewriter(
                        @Nonnull Rewriters rewriters) {
                    return new MyMethod()
                }
            })
            //删除原dex
            DexFile rewrittenDexFile = rewriter.rewriteDexFile(dexFile);
            File olddex = new File(dexfilepath)
            if(olddex.exists()){
                println "delete original dex"
                olddex.delete()
            }
            //生成新dex
            DexFileFactory.writeDexFile(dexfilepath, rewrittenDexFile);
        } catch (IOException e) {
            println "failed"
            e.printStackTrace();
        }
    }
}
//修改dex中的method
class MyMethod implements Rewriter<org.jf.dexlib2.iface.Method> {
    @Nonnull
    @Override
    org.jf.dexlib2.iface.Method rewrite(@Nonnull final org.jf.dexlib2.iface.Method value) {
       //找到 helloMethod 
        if (value.getName().contains("helloMethod")) {
            println "rewrite: "+value.getName()
            return new org.jf.dexlib2.iface.Method() {
                @Override
                public int compareTo(@Nonnull MethodReference o) {
                    return value.compareTo(o);
                }
                @Nonnull
                @Override
                public List<? extends CharSequence> getParameterTypes() {
                    return value.getParameters();
                }
                @Nonnull
                @Override
                public String getDefiningClass() {
                    return value.getDefiningClass();
                }
                @Nonnull
                @Override
                public List<? extends MethodParameter> getParameters() {
                    return value.getParameters();
                }
                @Nonnull
                @Override
                public String getReturnType() {
                    return value.getReturnType();
                }
                @Override
                public int getAccessFlags() {
                    return value.getAccessFlags();
                }
                @javax.annotation.Nullable
                @Override
                public MethodImplementation getImplementation() {
                    return value.getImplementation();
                }
                @Nonnull
                @Override
                public Set<? extends org.jf.dexlib2.iface.Annotation> getAnnotations() {
                    return value.getAnnotations();
                }
                @Nonnull
                @Override
                //将helloMethod重命名为MyMethod
                public String getName() {
                    return "MyMethod"
                }
            };
        }
        return value;
    }
}

Bingo!!!
最后, 同学点个赞吧!!! 加个关注好么
Gradle for Android(一) 使用Gradle和Android Studio 这个人写了一系列,可以看看。
晚点有时间我会上传到git上。

推荐阅读更多精彩内容