您的位置:首页 > 移动开发 > Android开发

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类的代码如下:

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的过程,大家如果想深入了解的话,可以去学习。

     希望本博客能给大家带来帮助,也希望大家关注我的博客,谢谢!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  makefile apk 源码 界面