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上。