混淆的另一重境界

Mess :https://github.com/JackCho/Mess

Mess介绍

众所周知,我们开混淆打包后生成的apk里,Activity、自定义View、Service等出现在xml里的相关Java类默认都会被keep住,那么这对于app的保护是不足够好的,Mess就是来解决这个问题,把即使出现在xml文件中的Java类照样混淆。

使用

dependencies {
   ...
   classpath 'me.ele:mess-plugin:1.0.1'
 }

apply plugin: 'com.android.library'
apply plugin: 'me.ele.mess'

此外,Mess还提供一个可选配置,ignoreProguard,由于有些依赖库本身也配置了相关混淆配置,如com.android.support:recyclerview-v7com.jakewharton:butterknife等,那么这些文件都将会被添加到proguardFiles中,导致依赖库无法被混淆,所以ignoreProguard配置就是来解决这个问题的。

比如忽视com.android.support:recyclerview-v7的混淆配置文件,则直接

mess {
    ignoreProguard 'com.android.support:recyclerview-v7'
}

实现原理

先来看看Android gradle plugin在构建时最后所走的几个task:

:app:processReleaseResources
...
:app:transformClassesAndResourcesWithProguardForRelease
:app:transformClassesWithDexForRelease
:app:transformClassesWithShrinkResForRelease
:app:mergeReleaseJniLibFolders
:app:transformNative_libsWithMergeJniLibsForRelease
:app:validateDebugSigning
:app:packageRelease
:app:zipalignRelease
:app:assembleRelease

其中有几个关键性的task,可以看到:app:transformClassesAndResourcesWithProguardForRelease是走在:app:packageRelease之前的,那么我们就在打包前对混淆的task做些操作来实现我们的目的。

  • hook transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}
  • hook ProcessAndroidResources Task,将生成的aapt_rules.txt中内容清空
  • 如果需要混淆依赖库,则删除依赖库中的proguard.txt文件
  • 遍历一遍mapping.txt获取所有Java类名的的映射关系得到一个Map
  • 拿映射Map替换AndroidManifest.xml里的Java原类名
  • 拿映射Map替换layout、menu和value文件夹下的xml的Java原类名
  • 重新跑ProcessAndroidResources Task
  • 恢复之前删除依赖库中的proguard.txt文件

以上就是Mess干的关键性的东西,接下来依次说明。

hook transformClassesAndResourcesWithProguardFor${variant.name}

这个task是处理类和资源混淆的,也是我们的突破口,Mess中大部分自定义task都是围绕在这个task执行的,之后会有详解。

hook ProcessAndroidResources Task,将生成的aapt_rules.txt中内容清空

这一步是虽说只是把aapt_rules.txt文件中的内容清空,但是确实Mess Plugin能成功的最关键的一步。

ProcessAndroidResources task会生成一个aapt_rules.txt,可见源码ProcessAndroidResources.groovy,aapt_rules.txt里会keep住我们在xml里所书写的那些Activity、自定义View等Java类名部分,还可以看到JackTask.java里的相关代码:

if (config.isMinifyEnabled()) {
    ConventionMappingHelper.map(jackTask, "proguardFiles", new Callable<List<File>>() {
        @Override
        public List<File> call() throws Exception {
            // since all the output use the same resources, we can use the first output
            // to query for a proguard file.
            File sdkDir = scope.getGlobalScope().getSdkHandler().getAndCheckSdkFolder();
            File defaultProguardFile =  new File(sdkDir,
                SdkConstants.FD_TOOLS + File.separatorChar
                    + SdkConstants.FD_PROGUARD + File.separatorChar
                    + TaskManager.DEFAULT_PROGUARD_CONFIG_FILE);

            List<File> proguardFiles = config.getProguardFiles(true /*includeLibs*/,
                ImmutableList.of(defaultProguardFile));
            File proguardResFile = scope.getProcessAndroidResourcesProguardOutputFile();
            proguardFiles.add(proguardResFile);
            // for tested app, we only care about their aapt config since the base
            // configs are the same files anyway.
            if (scope.getTestedVariantData() != null) {
                proguardResFile = scope.getTestedVariantData().getScope()
                    .getProcessAndroidResourcesProguardOutputFile();
                proguardFiles.add(proguardResFile);
            }

            return proguardFiles;
         }
   });

   jackTask.mappingFile = new File(scope.getProguardOutputFolder(), "mapping.txt");
}

其中getProcessAndroidResourcesProguardOutputFile方法所对应的文件就是我们所需要清空的aapt_rules.txt,可以在VariantScope.java中查看。

@NonNull
public File getProcessAndroidResourcesProguardOutputFile() {
    return new File(globalScope.getIntermediatesDir(),
         "/proguard-rules/" + getVariantConfiguration().getDirName() + "/aapt_rules.txt");
}

很明显,aapt_rules.txt所keep住的所有内容都将会添加到最后的混淆配置中,因此,我们需要在ProcessAndroidResources这个Task执行之后清空aapt_rules.txt中的内容,以保证编译出的main.jar中的所有.class都是混淆后的。

相关代码如下:

boolean hasProcessResourcesExecuted = false
output.processResources.doLast {
    if (hasProcessResourcesExecuted) {
    return
    }
    hasProcessResourcesExecuted = true

    def rulesPath = "${project.buildDir.absolutePath}/intermediates/proguard-rules/${variant.dirName}/aapt_rules.txt"
    File aaptRules = new File(rulesPath)
    aaptRules.delete()
    aaptRules << ""
}

如果需要混淆依赖库,则删除依赖库中的proguard.txt文件

这一步就是删除依赖库中所保护的内容,具体proguard.txt文件位于app目录下/build/intermediates/exploded-aar/依赖库maven名/proguard.txt

Mess中直接将proguard.txt文件名最后加上~,如proguard.txt~,在linux中表示备份,以便之后文件的恢复。

相关代码如下:

  public static void hideProguardTxt(Project project, String component) {
    renameProguardTxt(project, component, 'proguard.txt', 'proguard.txt~')
  }

  public static void recoverProguardTxt(Project project, String component) {
    renameProguardTxt(project, component, 'proguard.txt~', 'proguard.txt')
  }

  private static void renameProguardTxt(Project project, String component, String orgName,
      String newName) {
    MavenCoordinates mavenCoordinates = parseMavenString(component)
    File bundlesDir = new File(project.buildDir, "intermediates/exploded-aar")
    File bundleDir = new File(bundlesDir,
        "${mavenCoordinates.groupId}/${mavenCoordinates.artifactId}")
    if (!bundleDir.exists()) return
    bundleDir.eachFileRecurse(FileType.FILES) { File f ->
      if (f.name == orgName) {
        File targetFile = new File(f.parentFile.absolutePath, newName)
        println "rename file ${f.absolutePath} to ${targetFile.absolutePath}"
        Files.move(f, targetFile)
      }
    }
  }

遍历一遍mapping.txt获取所有Java类名的的映射关系得到一个Map

之前第一步已经将生成的main.jar中所有的.class文件做相关混淆了,那么我们之前所在xml里写的还是原来的Java类名,因此,我们想要替换xml里的Java类名,就得先知道原先的类名被替换成什么了,这个时候就得依赖mapping.txt了。

直接遍历:

File mappingFile = apkVariant.mappingFile
println mappingFile.toString()
mappingFile.eachLine { line ->
    //方法名的混淆前面是会有空格的,我们这里只需要拿类名的映射关系
    if (!line.startsWith(" ")) {
        // 如me.ele.mess.SecondActivity -> me.ele.mess.z:
        // -> 作为分割符号
        String[] keyValue = line.split("->")
        // 原始文件名
        String key = keyValue[0].trim()
        // 混淆后文件名,去掉最后一个":"
        String value = keyValue[1].subSequence(0, keyValue[1].length() - 1).trim()
        // 添加进map
        if (!key.equals(value)) {
            map.put(key, value)
        }
    }
}

这样后map里就存有所有类名的映射关系了,但是有个小问题要注意,假如存在这种情况,me.ele.foo -> me.ele.a,me.ele.fooNew -> me.ele.b,也就是恰巧有类名是另一个类名的开始部分,那么这样对我们之后的替换是会有bug的,会导致fooNew被替换成了aNew。因此,拿到map后需要对map做一次原类名长度的降序排序(也就是map中的key),以避免这个bug发生。相关代码如下:

  public static Map<String, String> sortMapping(Map<String, String> map) {
    List<Map.Entry<String, String>> list = new LinkedList<>(map.entrySet());
    Collections.sort(list, new Comparator<Map.Entry<String, String>>() {
      public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) {
        return o2.key.length() - o1.key.length()
      }
    });

    Map<String, String> result = new LinkedHashMap<>();
    for (Iterator<Map.Entry<String, String>> it = list.iterator(); it.hasNext();) {
      Map.Entry<String, String> entry = (Map.Entry<String, String>) it.next();
      result.put(entry.getKey(), entry.getValue());
    }

    return result;
  }

至此,一个正确的map已经拿到,接下来就是靠这个map来对相关的xml文件做替换了。

拿映射Map替换AndroidManifest.xml里的Java原类名

细心活,拿到AndroidManifest.xml一行一行读取,匹配到相关字符串则进行替换,但这里有个小坑,由于Java内部类的类名是用$符号分割的,刚好它又是正则表达式表示匹配字符串的结尾,因此对于内部类,我们应该现将$符号先替换成其他字符串,然后再做类名的替换,Mess中是替换成inner,相关代码如下:

File f = new File(path)
StringBuilder builder = new StringBuilder()
f.eachLine { line ->
    //<me.ele.base.widget.LoadingViewPager -> <me.ele.aaa
    // app:actionProviderClass="me.ele.base.ui.SearchViewProvider" -> app:actionProviderClass="me.ele.bbv"
    if (line.contains("<${oldStr}") || line.contains("${oldStr}>") || line.contains("${oldStr}\"")) {
        if (line.contains("\$") && oldStr.contains("\$")) {
             oldStr = oldStr.replaceAll("\\\$", "inner")
             line = line.replaceAll("\\\$", "inner").replaceAll(oldStr, newStr)
        } else {
            line = line.replaceAll(oldStr, newStr)
        }
    }
    builder.append(line);
    builder.append("\n")
}

f.delete()
f << builder.toString()

拿映射Map替换layout、menu和value文件夹下的xml的Java原类名

前一步已经把AndroidManifest.xml中的对应Java类名替换了,这一步就是替换layout、menu和value这三个文件夹下的xml内容,感谢groovy语法让整件事情变得非常简单。layout、menu文件夹大家能立马理解,那么value呢?其实就是behavior引入后才存在的,所以value文件夹千万别忽视。

相关代码如下:

File layoutDir = new File(getLayoutPath())
File menuDir = new File(getMenuPath())
File valueDir = new File(getValuePath())
[layoutDir, menuDir, valueDir].each {File dir ->
    if (dir.exists()) {
        dir.eachFileRecurse(FileType.FILES) { File file ->
            String orgTxt = file.text
            String newTxt = orgTxt
            map.each { k, v ->
                newTxt = newTxt.replace(k, v)
            }
            if (newTxt != orgTxt) {
            println 'rewrite file: ' + file.absolutePath
            file.text = newTxt
            }
        }
    }
}

至此,整个工程的main.jar中的.class文件以及资源文件都替换成相互匹配的混淆后的名称了。

重新跑ProcessAndroidResources Task

前些步骤hook后ProcessAndroidResources Task之后我们已经把静态的文件都替换好了,那么接下来就还得依靠Android gradle plugin的原有tasks了,于是乎我们重新执行ProcessAndroidResources Task。

ProcessAndroidResources processTask = variantOutput.processResources
processTask.state.executed = false
processTask.execute()

恢复之前删除依赖库中的proguard.txt文件

有头有尾。

尾语

想要写出Mess这样的plugin,对Android整个打包流程是要相当熟悉的,这样才能知道什么时候该hook什么task,平常开发过程中尽量不要直接点击run按钮,应该直接通过gradle assemble** 构建,这样无数次的看构建过程中经历哪些task,然后去阅读相关task源码,这样对整个打包流程才会越来越胸有成竹。

Mess有个小遗憾,那就是ButterKnife这个库在绝大多数app中都使用了,但是ButterKnife的混淆规则中有对使用注解的方法名和变量名做保护,这样就比较尴尬了,会导致Mess对使用ButterKnife库的app而言是没多大作用的。

-keepclasseswithmembernames class * { @butterknife.* <methods>; }
-keepclasseswithmembernames class * { @butterknife.* <fields>; }

但是不要灰心,ButterMess这个Lib就来解决这个问题,接下来会写篇详解ButterMess的文章,先放个ButterMess的链接:https://github.com/peacepassion/ButterMess

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容