混淆的另一重境界

KelBragg 7年前
   <h3>前言</h3>    <p>今天给大家讲解了一个Gradle插件的实现方法和原理,对于想深入了解Android打包编译,gradle插件实现的开发者来说,绝对是一篇不错的案例。</p>    <h3>Mess介绍</h3>    <p>众所周知,我们开混淆打包后生成的apk里,Activity、自定义View、Service等出现在xml里的相关Java类默认都会被keep住,那么这对于app的保护是不足够好的,Mess就是来解决这个问题,把即使出现在xml文件中的Java类照样混淆。</p>    <h3>使用</h3>    <h3 style="text-align: center;"><img src="https://simg.open-open.com/show/c1c0092fd8cd1f96c47a63e1f70334a9.jpg"></h3>    <p>此外,Mess还提供一个可选配置, <strong>ignoreProguard</strong> ,由于有些依赖库本身也配置了相关混淆配置,如 <strong>com.android.support:recyclerview-v7</strong> 、 <strong>com.jakewharton:butterknife</strong> 等,那么这些文件都将会被添加到 <strong>proguardFiles</strong> 中,导致依赖库无法被混淆,所以 <strong>ignoreProguard</strong> 配置就是来解决这个问题的。</p>    <p>比如忽视 <strong>com.android.support:recyclerview-v7</strong> 的混淆配置文件,则直接</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/bd6b60410500f6b4c4ede993c8118364.png"></p>    <h3>实现原理</h3>    <p>先来看看Android gradle plugin在构建时最后所走的几个task:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/489767a4f16646fabac025845edf05f3.jpg"></p>    <p>其中有几个关键性的task,可以看到 :app:transformClassesAndResourcesWithProguardForRelease 是走在 <strong>:app:packageRelease</strong> 之前的,那么我们就在打包前对混淆的task做些操作来实现我们的目的。</p>    <ul>     <li> <p>hook transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}</p> </li>     <li> <p>hook <strong>ProcessAndroidResources</strong> Task,将生成的 <strong>aapt_rules.txt</strong> 中内容清空</p> </li>     <li> <p>如果需要混淆依赖库,则删除依赖库中的 <strong>proguard.txt</strong> 文件</p> </li>     <li> <p>遍历一遍mapping.txt获取所有Java类名的的映射关系得到一个Map</p> </li>     <li> <p>拿映射Map替换AndroidManifest.xml里的Java原类名</p> </li>     <li> <p>拿映射Map替换layout、menu和value文件夹下的xml的Java原类名</p> </li>     <li> <p>重新跑 <strong>ProcessAndroidResources</strong> Task</p> </li>     <li> <p>恢复之前删除依赖库中的 <strong>proguard.txt</strong> 文件</p> </li>    </ul>    <p>以上就是Mess干的关键性的东西,接下来依次说明。</p>    <p>hooktransformClassesAndResourcesWithProguardFor${variant.name}</p>    <p>这个task是处理类和资源混淆的,也是我们的突破口,Mess中大部分自定义task都是围绕在这个task执行的,之后会有详解。</p>    <p>hookProcessAndroidResourcesTask,将生成的aapt_rules.txt中内容清空</p>    <p>这一步是虽说只是把 <strong>aapt_rules.txt</strong> 文件中的内容清空,但是确实Mess Plugin能成功的最关键的一步。</p>    <p>    ProcessAndroidResourcestask会生成一个 <strong>aapt_rules.txt</strong> ,可见源码 ProcessAndroidResources.groovy ,aapt_rules.txt里会keep住我们在xml里所书写的那些Activity、自定义View等Java类名部分,还可以看到 JackTask.java 里的相关代码:</p>    <p><img src="https://simg.open-open.com/show/d688134ba5545fc104e9757b4c8b4153.jpg"></p>    <p>其中getProcessAndroidResourcesProguardOutputFile方法所对应的文件就是我们所需要清空的aapt_rules.txt,可以在 VariantScope.java 中查看。</p>    <p><img src="https://simg.open-open.com/show/dd944fa50ddac175147249b37b4d237b.jpg"></p>    <p>很明显,aapt_rules.txt所keep住的所有内容都将会添加到最后的混淆配置中,因此,我们需要在 <strong>ProcessAndroidResources</strong> 这个Task执行之后清空aapt_rules.txt中的内容,以保证编译出的main.jar中的所有.class都是混淆后的。</p>    <p>相关代码如下:</p>    <p><img src="https://simg.open-open.com/show/d5486b2bd5de42ad610a920810260bfc.jpg"></p>    <p>如果需要混淆依赖库,则删除依赖库中的proguard.txt文件</p>    <p>这一步就是删除依赖库中所保护的内容,具体 <strong>proguard.txt</strong> 文件位于 app目录下/build/intermediates/exploded-aar/依赖库maven名/proguard.txt 。</p>    <p>Mess中直接将 <strong>proguard.txt</strong> 文件名最后加上 <strong>~</strong> ,如 <strong>proguard.txt~</strong> ,在linux中表示备份,以便之后文件的恢复。</p>    <p>相关代码如下:</p>    <p><img src="https://simg.open-open.com/show/a8ef78a7605c34b986ba24a031a03579.jpg"> 遍历一遍mapping.txt获取所有Java类名的的映射关系得到一个Map</p>    <p>之前第一步已经将生成的main.jar中所有的.class文件做相关混淆了,那么我们之前所在xml里写的还是原来的Java类名,因此,我们想要替换xml里的Java类名,就得先知道原先的类名被替换成什么了,这个时候就得依赖mapping.txt了。</p>    <p>直接遍历:</p>    <p><img src="https://simg.open-open.com/show/e6f51fecb1599267a8350ffaa9618019.jpg"></p>    <p>这样后map里就存有所有类名的映射关系了,但是有个小问题要注意,假如存在这种情况,me.ele.foo -> me.ele.a,me.ele.fooNew -> me.ele.b,也就是恰巧有类名是另一个类名的开始部分,那么这样对我们之后的替换是会有bug的,会导致fooNew被替换成了aNew。因此,拿到map后需要对map做一次原类名长度的降序排序(也就是map中的key),以避免这个bug发生。相关代码如下:</p>    <p><img src="https://simg.open-open.com/show/bb468ca1f0c15a3433a53a23fb59c9ff.jpg"></p>    <p>至此,一个正确的map已经拿到,接下来就是靠这个map来对相关的xml文件做替换了。</p>    <p>拿映射Map替换AndroidManifest.xml里的Java原类名</p>    <p>细心活,拿到AndroidManifest.xml一行一行读取,匹配到相关字符串则进行替换,但这里有个小坑,由于Java内部类的类名是用 <strong>$</strong> 符号分割的,刚好它又是正则表达式表示匹配字符串的结尾,因此对于内部类,我们应该现将 <strong>$</strong> 符号先替换成其他字符串,然后再做类名的替换,Mess中是替换成 <strong>inner</strong> ,相关代码如下:</p>    <p><img src="https://simg.open-open.com/show/d3eadee4f9d51626c9389f8ac03608ac.jpg"></p>    <p>拿映射Map替换layout、menu和value文件夹下的xml的Java原类名</p>    <p>前一步已经把AndroidManifest.xml中的对应Java类名替换了,这一步就是替换layout、menu和value这三个文件夹下的xml内容,感谢groovy语法让整件事情变得非常简单。layout、menu文件夹大家能立马理解,那么value呢?其实就是behavior引入后才存在的,所以value文件夹千万别忽视。</p>    <p>相关代码如下:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/a993e81f895a8b34ab9f386bc17f6636.jpg"></p>    <p>至此,整个工程的main.jar中的.class文件以及资源文件都替换成相互匹配的混淆后的名称了。</p>    <p>重新跑ProcessAndroidResourcesTask</p>    <p>前些步骤hook后 <strong>ProcessAndroidResources</strong> Task之后我们已经把静态的文件都替换好了,那么接下来就还得依靠Android gradle plugin的原有tasks了,于是乎我们重新执行 <strong>ProcessAndroidResources</strong> Task。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/b86d3349b71a2e620028cd52669383a4.jpg"></p>    <p>恢复之前删除依赖库中的proguard.txt文件</p>    <p>有头有尾。</p>    <h3>尾语</h3>    <p>想要写出Mess这样的plugin,对Android整个打包流程是要相当熟悉的,这样才能知道什么时候该hook什么task,平常开发过程中尽量不要直接点击run按钮,应该直接通过gradle assemble** 构建,这样无数次的看构建过程中经历哪些task,然后去阅读相关task源码,这样对整个打包流程才会越来越胸有成竹。</p>    <p>Mess有个小遗憾,那就是ButterKnife这个库在绝大多数app中都使用了,但是ButterKnife的混淆规则中有对使用注解的方法名和变量名做保护,这样就比较尴尬了,会导致Mess对使用ButterKnife库的app而言是没多大作用的。</p>    <p>但是不要灰心,ButterMess这个Lib就来解决这个问题,接下来会写篇详解ButterMess的文章,先放个ButterMess的链接: https://github.com/peacepassion/ButterMess</p>    <p>喜欢就点击原文链接,传送Github</p>    <p> </p>    <p> </p>    <p> </p>