Android Apk资源混淆流程全解析
2016-12-30 14:01
260 查看
随着Android的发展,各种技术涌现出来,各种方案层出不穷,在Android OS这个平台上真是出现了非常多的新技术,时刻关注技术市场的变化,才能让我们跟上市场的脚步。作为应用开发商,前端最重要的就是那个apk了,自己所有的实现都在这个文件当中,所以为了保护自己的知识产权,也相应的出现了各种混淆方案,有代码混淆,有资源混淆,今天我们就来实现一个简单的资源混淆的例子,并大概分析一下资源混淆的实现原理。
在开始分析之前,我们需要了解一下apk中的资源是个什么东西,它是如何构成的,我们可以随便打包一个apk,然后将它后缀名改为.rar,用解压程序将它打开,大概结果如下图:
这是一个我自己打包的demo,非常简单,我们在项目中使用到的资源文件最后都打包成为一个resources.arsc文件,那么我们今天要来处理的,就是这个resources.arsc文件了,先通过一张图来认识一下resources.arsc文件的构成:
自己完整的实现已完成,代码下载地址如下:
点击下载完整代码
当然这个方案其实也是别人已经实现好的,自己只不过拿来用而已,原文如下:
安装包立减1M--微信Android资源混淆打包工具
手把手教你解析Resources.arsc
我们本例中的实现非常简单,主类为ProguardMain,根据我们自己的需要构造一个InputParam参数,然后直接调用Main.gradleRun(inputParam)就完成了。
1为我们要混淆的文件,包括apk和mapping文件,2为实现混淆的入口类,3为参数封装,4为执行结果,输入的文件全部在当前项目的根目录的out文件夹中。我们打开out文件夹来看一下:
res就是混淆之后的资源文件目录,FFmpeg_unsigned.apk就是我们最终要的混淆后的apk,resource_mapping_FFmpeg.txt是对应混淆的扰码表,我们自己需要清楚混淆前后的文件对应关系。分别打开可以看到结果如下:
当然我这个demo混淆参数非常简单,实际项目中的文件应该是比较复杂的,大家可以根据自己的需要去封装参数就可以了。好了,下面我们来分析一下资源混淆的实现原理。前面的参数封装就不说了,最后调用InputParam inputParam = builder.create(),根据我们的参数创建一个InputParam对象,然后调用Main.gradleRun(inputParam),Main类的代码如下:
gradleRun方法是直接调用run方法来完成的,可以看到run方法中实际执行的就三步:1、loadConfigFromGradle(inputParam)加载配置文件;2、resourceProguard(new File(inputParam.outFolder), inputParam.apkPath)执行资源混淆并完成打包;3、clean()执行最后的清理工作。
1、loadConfigFromGradle(inputParam)加载配置文件
这一步的目的就是根据我们传入的参数创建一个Configuration对象,然后赋值给Main的成员变量config,创建过程我们就不分析了,都比较简单,就是把我们传入的参数一一解析,解析一个就给Configuration的成员变量赋值一个。
2、resourceProguard(new File(inputParam.outFolder), inputParam.apkPath)执行资源混淆并完成打包
这一步就是我们要分析的重点了。首先根据上一步创建好的Configuration创建一个ApkDecoder对象,然后进行参数检查,如果我们传入的源apk文件不存在,就直接返回失败,因为源文件都没有了,那还处理什么呢?参数检查完成后,调用FileOperation.getFileSizes(apkFile)获取文件大小,然后执行混淆,最后重新打包成apk。
我们先来看一下decodeResource的实现。它当中开始先进行一些参数处理,最重要的就是最后一步decoder.decode(),该方法的实现如下:
第一个判断hasResources()也是参数检查,就是判断有没有resources.arsc文件,如果没有就直接抛出异常;如果有,就先调用ensureFilePath()进行前期处理,这里就是创建一个预备目录,比如temp临时目录,res的资源目录,resources_temp.arsc的临时文件等等。FileOperation.unZipAPk(mApkFile.getAbsoluteFile().getAbsolutePath(),
unZipDest)句代码会将我们的apk解压出来,放到temp临时目录中,大家可以对比一下,使用是的java提供的ZipFile。
预处理完成后,接着调用RawARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc")),mApkFile.getDirectory().getFileInput("resources.arsc")句代码是获取到resources.arsc文件并将它以流的方式读取到内存,然后调用RawARSCDecoder.decode方法对读入的流数据进行处理,这里请注意,读入的流数据作为参数构造了一个ExtDataInput和一个LEDataInputStream对象,ExtDataInput只是一个装饰件,实质上的数据都保存在LEDataInputStream当中,后边的流程大家可以看到,每次调用ExtDataInput的方法取数据时,其实都是在执行LEDataInputStream对象的方法。接下来调用ARSCDecoder.decode方法进一步处理,这里会构造一个ARSCDecoder对象,在它的构造方法中,调用proguardFileName(),这里会生成一个ProguardStringBuilder对象,大家可以看一下它这个对象的mAToZ和mAToAll属性,全部是a-z、0-9,我们最终的扰码表和打包好的文件名就是从这里取出来的,它是生成是通过reset方法中的三个for循环,不断的将数字和字母组合,然后将组合后的结果添加到成员变量mReplaceStringBuffer当中,要用的时候直接从这里取就可以了;又接着调用generalFileResMapping,我们最终看到的扰码表就是在这里生成的。
好了,后边的流程我们就不深入了,就是将apk文件中的所有东西取出来,然后一个个判断,需要混淆就执行,接着调用ARSCDecoder.write(mApkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs)将所有数据全部写回去,最后调用buildApk(decoder, apkFile)重新打包。
3、clean()执行最后的清理工作
这一步的清理工作非常简单,就是将config设置为空,然后清除到ARSCDecoder类的成员变量mTableStringsProguard的值。
本博客限于时间原因,中间有一些解析的过程未作分析,请大家见谅,开头也有引用别人的博客,里边有非常详细的解析Resources.arsc的过程,大家如果想深入了解的话,可以去学习。
希望本博客能给大家带来帮助,也希望大家关注我的博客,谢谢!
在开始分析之前,我们需要了解一下apk中的资源是个什么东西,它是如何构成的,我们可以随便打包一个apk,然后将它后缀名改为.rar,用解压程序将它打开,大概结果如下图:
这是一个我自己打包的demo,非常简单,我们在项目中使用到的资源文件最后都打包成为一个resources.arsc文件,那么我们今天要来处理的,就是这个resources.arsc文件了,先通过一张图来认识一下resources.arsc文件的构成:
自己完整的实现已完成,代码下载地址如下:
点击下载完整代码
当然这个方案其实也是别人已经实现好的,自己只不过拿来用而已,原文如下:
安装包立减1M--微信Android资源混淆打包工具
手把手教你解析Resources.arsc
我们本例中的实现非常简单,主类为ProguardMain,根据我们自己的需要构造一个InputParam参数,然后直接调用Main.gradleRun(inputParam)就完成了。
1为我们要混淆的文件,包括apk和mapping文件,2为实现混淆的入口类,3为参数封装,4为执行结果,输入的文件全部在当前项目的根目录的out文件夹中。我们打开out文件夹来看一下:
res就是混淆之后的资源文件目录,FFmpeg_unsigned.apk就是我们最终要的混淆后的apk,resource_mapping_FFmpeg.txt是对应混淆的扰码表,我们自己需要清楚混淆前后的文件对应关系。分别打开可以看到结果如下:
当然我这个demo混淆参数非常简单,实际项目中的文件应该是比较复杂的,大家可以根据自己的需要去封装参数就可以了。好了,下面我们来分析一下资源混淆的实现原理。前面的参数封装就不说了,最后调用InputParam inputParam = builder.create(),根据我们的参数创建一个InputParam对象,然后调用Main.gradleRun(inputParam),Main类的代码如下:
package com.tencent.mm.resourceproguard; import com.tencent.mm.androlib.AndrolibException; import com.tencent.mm.androlib.ApkDecoder; import com.tencent.mm.androlib.ResourceApkBuilder; import com.tencent.mm.androlib.res.decoder.ARSCDecoder; import com.tencent.mm.directory.DirectoryException; import com.tencent.mm.util.FileOperation; import com.tencent.mm.util.Utils; import java.io.File; import java.io.IOException; import java.util.ArrayList; /** * @author shwenzhang * @author simsun */ public class Main { public static final int ERRNO_ERRORS = 1; public static final int ERRNO_USAGE = 2; protected static long mRawApkSize; protected static String mRunningLocation; protected static long mBeginTime; /** * 是否通过命令行方式设置 */ public boolean mSetSignThroughCmd = false; public boolean mSetMappingThroughCmd = false; public String m7zipPath = null; public String mZipalignPath = null; protected Configuration config; protected File mOutDir; public static void gradleRun(InputParam inputParam) { Main m = new Main(); m.run(inputParam); } private void run(InputParam inputParam) { loadConfigFromGradle(inputParam); System.out.println("resourceprpguard begin"); resourceProguard(new File(inputParam.outFolder), inputParam.apkPath); System.out.printf("resources proguard done, you can go to file to find the output %s\n", mOutDir.getAbsolutePath()); clean(); } protected void clean() { config = null; ARSCDecoder.mTableStringsProguard.clear(); } private void loadConfigFromGradle(InputParam inputParam) { try { config = new Configuration(inputParam); } catch (IOException e) { e.printStackTrace(); } } protected void resourceProguard(File outputFile, String apkFilePath) { ApkDecoder decoder = new ApkDecoder(config); File apkFile = new File(apkFilePath); if (!apkFile.exists()) { System.err.printf("the input apk %s does not exit", apkFile.getAbsolutePath()); goToError(); } mRawApkSize = FileOperation.getFileSizes(apkFile); try { decodeResource(outputFile, decoder, apkFile); buildApk(decoder, apkFile); } catch (AndrolibException | IOException | DirectoryException | InterruptedException e) { e.printStackTrace(); goToError(); } } private void decodeResource(File outputFile, ApkDecoder decoder, File apkFile) throws AndrolibException, IOException, DirectoryException { decoder.setApkFile(apkFile); if (outputFile == null) { mOutDir = new File(mRunningLocation, apkFile.getName().substring(0, apkFile.getName().indexOf(".apk"))); } else { mOutDir = outputFile; } decoder.setOutDir(mOutDir.getAbsoluteFile()); decoder.decode(); } private void buildApk(ApkDecoder decoder, File apkFile) throws AndrolibException, IOException, InterruptedException { ResourceApkBuilder builder = new ResourceApkBuilder(config); String apkBasename = apkFile.getName(); apkBasename = apkBasename.substring(0, apkBasename.indexOf(".apk")); builder.setOutDir(mOutDir, apkBasename); builder.buildApk(decoder.getCompressData()); } public double diffApkSizeFromRaw(long size) { return (mRawApkSize - size) / 1024.0; } protected void goToError() { System.exit(ERRNO_USAGE); } }
gradleRun方法是直接调用run方法来完成的,可以看到run方法中实际执行的就三步:1、loadConfigFromGradle(inputParam)加载配置文件;2、resourceProguard(new File(inputParam.outFolder), inputParam.apkPath)执行资源混淆并完成打包;3、clean()执行最后的清理工作。
1、loadConfigFromGradle(inputParam)加载配置文件
这一步的目的就是根据我们传入的参数创建一个Configuration对象,然后赋值给Main的成员变量config,创建过程我们就不分析了,都比较简单,就是把我们传入的参数一一解析,解析一个就给Configuration的成员变量赋值一个。
2、resourceProguard(new File(inputParam.outFolder), inputParam.apkPath)执行资源混淆并完成打包
这一步就是我们要分析的重点了。首先根据上一步创建好的Configuration创建一个ApkDecoder对象,然后进行参数检查,如果我们传入的源apk文件不存在,就直接返回失败,因为源文件都没有了,那还处理什么呢?参数检查完成后,调用FileOperation.getFileSizes(apkFile)获取文件大小,然后执行混淆,最后重新打包成apk。
我们先来看一下decodeResource的实现。它当中开始先进行一些参数处理,最重要的就是最后一步decoder.decode(),该方法的实现如下:
public void decode() throws AndrolibException, IOException, DirectoryException { if (hasResources()) { ensureFilePath(); // read the resources.arsc checking for STORED vs DEFLATE compression // this will determine whether we compress on rebuild or not. System.out.printf("decoding resources.arsc\n"); RawARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc")); ResPackage[] pkgs = ARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc"), this); //把没有纪录在resources.arsc的资源文件也拷进dest目录 copyOtherResFiles(); ARSCDecoder.write(mApkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs); } }
第一个判断hasResources()也是参数检查,就是判断有没有resources.arsc文件,如果没有就直接抛出异常;如果有,就先调用ensureFilePath()进行前期处理,这里就是创建一个预备目录,比如temp临时目录,res的资源目录,resources_temp.arsc的临时文件等等。FileOperation.unZipAPk(mApkFile.getAbsoluteFile().getAbsolutePath(),
unZipDest)句代码会将我们的apk解压出来,放到temp临时目录中,大家可以对比一下,使用是的java提供的ZipFile。
预处理完成后,接着调用RawARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc")),mApkFile.getDirectory().getFileInput("resources.arsc")句代码是获取到resources.arsc文件并将它以流的方式读取到内存,然后调用RawARSCDecoder.decode方法对读入的流数据进行处理,这里请注意,读入的流数据作为参数构造了一个ExtDataInput和一个LEDataInputStream对象,ExtDataInput只是一个装饰件,实质上的数据都保存在LEDataInputStream当中,后边的流程大家可以看到,每次调用ExtDataInput的方法取数据时,其实都是在执行LEDataInputStream对象的方法。接下来调用ARSCDecoder.decode方法进一步处理,这里会构造一个ARSCDecoder对象,在它的构造方法中,调用proguardFileName(),这里会生成一个ProguardStringBuilder对象,大家可以看一下它这个对象的mAToZ和mAToAll属性,全部是a-z、0-9,我们最终的扰码表和打包好的文件名就是从这里取出来的,它是生成是通过reset方法中的三个for循环,不断的将数字和字母组合,然后将组合后的结果添加到成员变量mReplaceStringBuffer当中,要用的时候直接从这里取就可以了;又接着调用generalFileResMapping,我们最终看到的扰码表就是在这里生成的。
好了,后边的流程我们就不深入了,就是将apk文件中的所有东西取出来,然后一个个判断,需要混淆就执行,接着调用ARSCDecoder.write(mApkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs)将所有数据全部写回去,最后调用buildApk(decoder, apkFile)重新打包。
3、clean()执行最后的清理工作
这一步的清理工作非常简单,就是将config设置为空,然后清除到ARSCDecoder类的成员变量mTableStringsProguard的值。
本博客限于时间原因,中间有一些解析的过程未作分析,请大家见谅,开头也有引用别人的博客,里边有非常详细的解析Resources.arsc的过程,大家如果想深入了解的话,可以去学习。
希望本博客能给大家带来帮助,也希望大家关注我的博客,谢谢!
相关文章推荐
- Android apk文件资源混淆原理及实现
- Android Apk 代码混淆与资源文件混淆实战
- 资源访问机制之资源定义与解析流程
- Android apk 签名及代码混淆、资源文件混淆、加固整套流程
- 资源访问机制之资源定义与解析流程
- Webkit学习 --- 解析HTML获取网页子资源流程
- android代码混淆压缩、资源压缩全解析
- J0ker的CISSP之路:CISSP复习流程和资源 推荐
- jbpm解析流程定义的三种方式
- 全面解析Sbo业务审批流程与结构
- 解析进程为何不能访问网络资源(作者Bingle)
- 解析进程为何不能访问网络资源(作者Bingle)
- 由浅至深 谈谈.NET混淆原理 (三)-- 流程混淆
- 解析进程为何不能访问网络资源(作者Bingle)
- Java初学者容易混淆的几个问题详细解析
- CISSP的成长之路(六):CISSP复习流程及资源
- 解析入侵3389的全部流程
- 解析Asp.net中资源本地化的实现
- [转载]解析Asp.net中资源本地化的实现
- 由浅至深 谈谈.NET混淆原理(三)-- 流程混淆