android 加固防止反编译-重新打包

1、需要加密的Apk(源Apk)
2、壳程序Apk(负责解密Apk工作)
3、加密工具(将源Apk进行加密和壳Dex合并成新的Dex)

主要步骤:

我们拿到需要加密的Apk和自己的壳程序Apk,然后用加密算法对源Apk进行加密在将壳Apk进行合并得到新的Dex文件,最后替换壳程序中的dex文件即可,得到新的Apk,那么这个新的Apk我们也叫作脱壳程序Apk.他已经不是一个完整意义上的Apk程序了,他的主要工作是:负责解密源Apk.然后加载Apk,让其正常运行起来。

在这个过程中我们可能需要了解的一个知识是:  如何将源Apk和壳Apk进行合并成新的Dex

这里就需要了解Dex文件的格式了。下面就来简单介绍一下Dex文件的格式

具体Dex文件格式的详细介绍可以查看这个文件:  http://download.csdn.net/detail/jiangwei0910410003/9102599

主要来看一下Dex文件的头部信息,其实Dex文件和Class文件的格式分析原理都是一样的,他们都是有固定的格式,我们知道现在反编译的一些工具:

1、jd-gui:可以查看jar中的类,其实他就是解析class文件,只要了解class文件的格式就可以
2、dex2jar:将dex文件转化成jar,原理也是一样的,只要知道Dex文件的格式,能够解析出dex文件中的类信息就可以了

当然我们在分析这个文件的时候,最重要的还是头部信息,应该他是一个文件的开始部分,也是索引部分,内部信息很重要。

我们今天只要关注上面红色标记的三个部分:

1) checksum 
文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误 。
2) signature 
使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。
3) file_size
Dex 文件的大小 。

为什么说我们只需要关注这三个字段呢?

因为我们需要将一个文件(加密之后的源Apk)写入到Dex中,那么我们肯定需要修改文件校验码(checksum).因为他是检查文件是否有错误。那么signature也是一样,也是唯一识别文件的算法。还有就是需要修改dex文件的大小。

不过这里还需要一个操作,就是标注一下我们加密的Apk的大小,因为我们在脱壳的时候,需要知道Apk的大小,才能正确的得到Apk。那么这个值放到哪呢?这个值直接放到文件的末尾就可以了。

所以总结一下我们需要做:修改Dex的三个文件头,将源Apk的大小追加到壳dex的末尾就可以了。

我们修改之后得到新的Dex文件样式如下:

那么我们知道原理了,下面就是代码实现了。所以这里有三个工程:

1、源程序项目(需要加密的Apk)
2、脱壳项目(解密源Apk和加载Apk)
3、对源Apk进行加密和脱壳项目的Dex的合并

三、项目案例 

下面先来看一下源程序

1、需要加密的源程序Apk项目:ForceApkObj 

需要一个Application类,这个到后面说为什么需要:

MyApplication.java
package com.example.forceapkobj;

import android.app.Application;
import android.util.Log;

public class MyApplication extends Application{
  
  @Override
  public void onCreate() {
    super.onCreate();
    Log.i("demo", "source apk onCreate:"+this);
  }

}

就是打印一下onCreate方法。

MainActivity.java
package com.example.forceapkobj;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;

public class MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    TextView content = new TextView(this);
    content.setText("I am Source Apk");
    content.setOnClickListener(new OnClickListener(){
      @Override
      public void onClick(View arg0) {
        Intent intent = new Intent(MainActivity.this, SubActivity.class);
        startActivity(intent);
      }});
    setContentView(content);
    
    Log.i("demo", "app:"+getApplicationContext());
    
  }

}

也是打印一下内容。

2、加壳程序项目:DexShellTools 

加壳程序其实就是一个Java工程,因为我们从上面的分析可以看到,他的工作就是加密源Apk,然后将其写入到脱壳Dex文件中,修改文件头,得到一个新的Dex文件即可。

看一下代码:

package com.example.reforceapk;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.Adler32;
public class mymain {
  /**
   * @param args
   */
  public static void main(String[] args) {
    // TODO Auto-generated method stub
    try {
      File payloadSrcFile = new File("force/ForceApkObj.apk");   //需要加壳的程序
      System.out.println("apk size:"+payloadSrcFile.length());
      File unShellDexFile = new File("force/ForceApkObj.dex");	//解客dex
      byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
      byte[] unShellDexArray = readFileBytes(unShellDexFile);//以二进制形式读出dex
      int payloadLen = payloadArray.length;
      int unShellDexLen = unShellDexArray.length;
      int totalLen = payloadLen + unShellDexLen +4;//多出4字节是存放长度的。
      byte[] newdex = new byte[totalLen]; // 申请了新的长度
      //添加解壳代码
      System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷贝dex内容
      //添加加密后的解壳数据
      System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex内容后面拷贝apk的内容
      //添加解壳数据长度
      System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最后4为长度
  //修改DEX file size文件头
      fixFileSizeHeader(newdex);
      //修改DEX SHA1 文件头
      fixSHA1Header(newdex);
      //修改DEX CheckSum文件头
      fixCheckSumHeader(newdex);
      String str = "force/classes.dex";
      File file = new File(str);
      if (!file.exists()) {
        file.createNewFile();
      }
      FileOutputStream localFileOutputStream = new FileOutputStream(str);
      localFileOutputStream.write(newdex);
      localFileOutputStream.flush();
      localFileOutputStream.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
  //直接返回数据,读者可以添加自己加密方法
  private static byte[] encrpt(byte[] srcdata){
    for(int i = 0;i<srcdata.length;i++){
      srcdata[i] = (byte)(0xFF ^ srcdata[i]);
    }
    return srcdata;
  }
  /**
   * 修改dex头,CheckSum 校验码
   * @param dexBytes
   */
  private static void fixCheckSumHeader(byte[] dexBytes) {
    Adler32 adler = new Adler32();
    adler.update(dexBytes, 12, dexBytes.length - 12);//从12到文件末尾计算校验码
    long value = adler.getValue();
    int va = (int) value;
    byte[] newcs = intToByte(va);
    //高位在前,低位在前掉个个
    byte[] recs = new byte[4];
    for (int i = 0; i < 4; i++) {
      recs[i] = newcs[newcs.length - 1 - i];
      System.out.println(Integer.toHexString(newcs[i]));
    }
    System.arraycopy(recs, 0, dexBytes, 8, 4);//效验码赋值(8-11)
    System.out.println(Long.toHexString(value));
    System.out.println();
  }
  /**
   * int 转byte[]
   * @param number
   * @return
   */
  public static byte[] intToByte(int number) {
    byte[] b = new byte[4];
    for (int i = 3; i >= 0; i--) {
      b[i] = (byte) (number % 256);
      number >>= 8;
    }
    return b;
  }
  /**
   * 修改dex头 sha1值
   * @param dexBytes
   * @throws NoSuchAlgorithmException
   */
  private static void fixSHA1Header(byte[] dexBytes)
      throws NoSuchAlgorithmException {
    MessageDigest md = MessageDigest.getInstance("SHA-1");
    md.update(dexBytes, 32, dexBytes.length - 32);//从32为到结束计算sha--1
    byte[] newdt = md.digest();
    System.arraycopy(newdt, 0, dexBytes, 12, 20);//修改sha-1值(12-31)
    //输出sha-1值,可有可无
    String hexstr = "";
    for (int i = 0; i < newdt.length; i++) {
      hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16)
          .substring(1);
    }
    System.out.println(hexstr);
  }
  /**
   * 修改dex头 file_size值
   * @param dexBytes
   */
  private static void fixFileSizeHeader(byte[] dexBytes) {
    //新文件长度
    byte[] newfs = intToByte(dexBytes.length);
    System.out.println(Integer.toHexString(dexBytes.length));
    byte[] refs = new byte[4];
    //高位在前,低位在前掉个个
    for (int i = 0; i < 4; i++) {
      refs[i] = newfs[newfs.length - 1 - i];
      System.out.println(Integer.toHexString(newfs[i]));
    }
    System.arraycopy(refs, 0, dexBytes, 32, 4);//修改(32-35)
  }
  /**
   * 以二进制读出文件内容
   * @param file
   * @return
   * @throws IOException
   */
  private static byte[] readFileBytes(File file) throws IOException {
    byte[] arrayOfByte = new byte[1024];
    ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream();
    FileInputStream fis = new FileInputStream(file);
    while (true) {
      int i = fis.read(arrayOfByte);
      if (i != -1) {
        localByteArrayOutputStream.write(arrayOfByte, 0, i);
      } else {
        return localByteArrayOutputStream.toByteArray();
      }
    }
  }
}

下面来分析一下:

红色部分其实就是最核心的工作:

1>、加密源程序Apk文件
byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作

加密算法很简单:

//直接返回数据,读者可以添加自己加密方法
private static byte[] encrpt(byte[] srcdata){
  for(int i = 0;i<srcdata.length;i++){
    srcdata[i] = (byte)(0xFF ^ srcdata[i]);
  }
  return srcdata;
}

对每个字节进行异或一下即可。

(说明:这里是为了简单,所以就用了很简单的加密算法了,其实为了增加破解难度,我们应该使用更高效的加密算法,同事最好将加密操作放到native层去做)
2>、合并文件:将加密之后的Apk和原脱壳Dex进行合并
int payloadLen = payloadArray.length;
int unShellDexLen = unShellDexArray.length;
int totalLen = payloadLen + unShellDexLen +4;//多出4字节是存放长度的。
byte[] newdex = new byte[totalLen]; // 申请了新的长度
//添加解壳代码
System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷贝dex内容
//添加加密后的解壳数据
System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex内容后面拷贝apk的内容
3>、在文件的末尾追加源程序Apk的长度
//添加解壳数据长度
System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最后4为长度
4>、修改新Dex文件的文件头信息:file_size; sha1; check_sum
//修改DEX file size文件头
fixFileSizeHeader(newdex);
//修改DEX SHA1 文件头
fixSHA1Header(newdex);
//修改DEX CheckSum文件头
fixCheckSumHeader(newdex);

具体修改可以参照之前说的文件头格式,修改指定位置的字节值即可。

这里我们还需要两个输入文件:

1>、源Apk文件:ForceApkObj.apk
2>、脱壳程序的Dex文件:ForceApkObj.dex

那么第一个文件我们都知道,就是上面的源程序编译之后的Apk文件,那么第二个文件我们怎么得到呢?这个就是我们要讲到的第三个项目:脱壳程序项目,他是一个Android项目,我们在编译之后,能够得到他的classes.dex文件,然后修改一下名称就可。

3、脱壳项目:ReforceApk 

在讲解这个项目之前,我们先来了解一下这个脱壳项目的工作:

1>、通过反射置换android.app.ActivityThread 中的mClassLoader为加载解密出APK的DexClassLoader,该DexClassLoader一方面加载了源程序、另一方面以原mClassLoader为父节点,这就保证了即加载了源程序又没有放弃原先加载的资源与系统代码。

关于这部分内容,不了解的同学可以看一下ActivityThread.java的源码:

或者直接看一下这篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/48104455

如何得到系统加载Apk的类加载器,然后我们怎么将加载进来的Apk运行起来等问题都在这篇文章中说到了。

2>、找到源程序的Application,通过反射建立并运行。 

这里需要注意的是,我们现在是加载一个完整的Apk,让他运行起来,那么我们知道一个Apk运行的时候都是有一个Application对象的,这个也是一个程序运行之后的全局类。所以我们必须找到解密之后的源Apk的Application类,运行的他的onCreate方法,这样源Apk才开始他的运行生命周期。这里我们如何得到源Apk的Application的类呢?这个我们后面会说道。使用meta标签进行设置。

下面来看一下整体的流程图:

所以我们看到这里还需要一个核心的技术就是动态加载。关于动态加载技术,不了解的同学可以看这篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/48104581

下面来看一下代码:

package com.example.reforceapk;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import dalvik.system.DexClassLoader;
public class ProxyApplication extends Application{
  private static final String appkey = "APPLICATION_CLASS_NAME";
  private String apkFileName;
  private String odexPath;
  private String libPath;
  //这是context 赋值
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    try {
      //创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录
      File odex = this.getDir("payload_odex", MODE_PRIVATE);
      File libs = this.getDir("payload_lib", MODE_PRIVATE);
      odexPath = odex.getAbsolutePath();
      libPath = libs.getAbsolutePath();
      apkFileName = odex.getAbsolutePath() + "/payload.apk";
      File dexFile = new File(apkFileName);
      Log.i("demo", "apk size:"+dexFile.length());
      if (!dexFile.exists())
      {
        dexFile.createNewFile();  //在payload_odex文件夹内,创建payload.apk
        // 读取程序classes.dex文件
        byte[] dexdata = this.readDexFileFromApk();
        // 分离出解壳后的apk文件已用于动态加载
        this.splitPayLoadFromDex(dexdata);
      }
      // 配置动态加载环境
      Object currentActivityThread = RefInvoke.invokeStaticMethod(
          "android.app.ActivityThread", "currentActivityThread",
          new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493
      String packageName = this.getPackageName();//当前apk的包名
      //下面两句不是太理解
      ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
          "android.app.ActivityThread", currentActivityThread,
          "mPackages");
      WeakReference wr = (WeakReference) mPackages.get(packageName);
      //创建被加壳apk的DexClassLoader对象  加载apk内的类和本地代码(c/c++代码)
      DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
          libPath, (ClassLoader) RefInvoke.getFieldOjbect(
              "android.app.LoadedApk", wr.get(), "mClassLoader"));
      //base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?
      //把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader  ----有点c++中进程环境的意思~~
      RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
          wr.get(), dLoader);
      Log.i("demo","classloader:"+dLoader);
      try{
        Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");
        Log.i("demo", "actObj:"+actObj);
      }catch(Exception e){
        Log.i("demo", "activity:"+Log.getStackTraceString(e));
      }
    } catch (Exception e) {
      Log.i("demo", "error:"+Log.getStackTraceString(e));
      e.printStackTrace();
    }
  }
  @Override
  public void onCreate() {
    {
      //loadResources(apkFileName);
      Log.i("demo", "onCreate");
      // 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
      String appClassName = null;
      try {
        ApplicationInfo ai = this.getPackageManager()
            .getApplicationInfo(this.getPackageName(),
                PackageManager.GET_META_DATA);
        Bundle bundle = ai.metaData;
        if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {
          appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml文件中的。
        } else {
          Log.i("demo", "have no application class name");
          return;
        }
      } catch (NameNotFoundException e) {
        Log.i("demo", "error:"+Log.getStackTraceString(e));
        e.printStackTrace();
      }
      //有值的话调用该Applicaiton
      Object currentActivityThread = RefInvoke.invokeStaticMethod(
          "android.app.ActivityThread", "currentActivityThread",
          new Class[] {}, new Object[] {});
      Object mBoundApplication = RefInvoke.getFieldOjbect(
          "android.app.ActivityThread", currentActivityThread,
          "mBoundApplication");
      Object loadedApkInfo = RefInvoke.getFieldOjbect(
          "android.app.ActivityThread$AppBindData",
          mBoundApplication, "info");
      //把当前进程的mApplication 设置成了null
      RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
          loadedApkInfo, null);
      Object oldApplication = RefInvoke.getFieldOjbect(
          "android.app.ActivityThread", currentActivityThread,
          "mInitialApplication");
      //http://www.codeceo.com/article/android-context.html
      ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
          .getFieldOjbect("android.app.ActivityThread",
              currentActivityThread, "mAllApplications");
      mAllApplications.remove(oldApplication);//删除oldApplication
      ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
          .getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
              "mApplicationInfo");
      ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
          .getFieldOjbect("android.app.ActivityThread$AppBindData",
              mBoundApplication, "appInfo");
      appinfo_In_LoadedApk.className = appClassName;
      appinfo_In_AppBindData.className = appClassName;
      Application app = (Application) RefInvoke.invokeMethod(
          "android.app.LoadedApk", "makeApplication", loadedApkInfo,
          new Class[] { boolean.class, Instrumentation.class },
          new Object[] { false, null });//执行 makeApplication(false,null)
      RefInvoke.setFieldOjbect("android.app.ActivityThread",
          "mInitialApplication", currentActivityThread, app);
      ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(
          "android.app.ActivityThread", currentActivityThread,
          "mProviderMap");
      Iterator it = mProviderMap.values().iterator();
      while (it.hasNext()) {
        Object providerClientRecord = it.next();
        Object localProvider = RefInvoke.getFieldOjbect(
            "android.app.ActivityThread$ProviderClientRecord",
            providerClientRecord, "mLocalProvider");
        RefInvoke.setFieldOjbect("android.content.ContentProvider",
            "mContext", localProvider, app);
      }
      Log.i("demo", "app:"+app);
      app.onCreate();
    }
  }
  /**
   * 释放被加壳的apk文件,so文件
   * @param data
   * @throws IOException
   */
  private void splitPayLoadFromDex(byte[] apkdata) throws IOException {
    int ablen = apkdata.length;
    //取被加壳apk的长度   这里的长度取值,对应加壳时长度的赋值都可以做些简化
    byte[] dexlen = new byte[4];
    System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);
    ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
    DataInputStream in = new DataInputStream(bais);
    int readInt = in.readInt();
    System.out.println(Integer.toHexString(readInt));
    byte[] newdex = new byte[readInt];
    //把被加壳apk内容拷贝到newdex中
    System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);
    //这里应该加上对于apk的解密操作,若加壳是加密处理的话
    //?
    //对源程序Apk进行解密
    newdex = decrypt(newdex);
    //写入apk文件   
    File file = new File(apkFileName);
    try {
      FileOutputStream localFileOutputStream = new FileOutputStream(file);
      localFileOutputStream.write(newdex);
      localFileOutputStream.close();
    } catch (IOException localIOException) {
      throw new RuntimeException(localIOException);
    }
    //分析被加壳的apk文件
    ZipInputStream localZipInputStream = new ZipInputStream(
        new BufferedInputStream(new FileInputStream(file)));
    while (true) {
      ZipEntry localZipEntry = localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的
      if (localZipEntry == null) {
        localZipInputStream.close();
        break;
      }
      //取出被加壳apk用到的so文件,放到 libPath中(data/data/包名/payload_lib)
      String name = localZipEntry.getName();
      if (name.startsWith("lib/") && name.endsWith(".so")) {
        File storeFile = new File(libPath + "/"
            + name.substring(name.lastIndexOf('/')));
        storeFile.createNewFile();
        FileOutputStream fos = new FileOutputStream(storeFile);
        byte[] arrayOfByte = new byte[1024];
        while (true) {
          int i = localZipInputStream.read(arrayOfByte);
          if (i == -1)
            break;
          fos.write(arrayOfByte, 0, i);
        }
        fos.flush();
        fos.close();
      }
      localZipInputStream.closeEntry();
    }
    localZipInputStream.close();
  }
  /**
   * 从apk包里面获取dex文件内容(byte)
   * @return
   * @throws IOException
   */
  private byte[] readDexFileFromApk() throws IOException {
    ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
    ZipInputStream localZipInputStream = new ZipInputStream(
        new BufferedInputStream(new FileInputStream(
            this.getApplicationInfo().sourceDir)));
    while (true) {
      ZipEntry localZipEntry = localZipInputStream.getNextEntry();
      if (localZipEntry == null) {
        localZipInputStream.close();
        break;
      }
      if (localZipEntry.getName().equals("classes.dex")) {
        byte[] arrayOfByte = new byte[1024];
        while (true) {
          int i = localZipInputStream.read(arrayOfByte);
          if (i == -1)
            break;
          dexByteArrayOutputStream.write(arrayOfByte, 0, i);
        }
      }
      localZipInputStream.closeEntry();
    }
    localZipInputStream.close();
    return dexByteArrayOutputStream.toByteArray();
  }
  // //直接返回数据,读者可以添加自己解密方法
  private byte[] decrypt(byte[] srcdata) {
    for(int i=0;i<srcdata.length;i++){
      srcdata[i] = (byte)(0xFF ^ srcdata[i]);
    }
    return srcdata;
  }
  //以下是加载资源
  protected AssetManager mAssetManager;//资源管理器  
  protected Resources mResources;//资源  
  protected Theme mTheme;//主题  
  protected void loadResources(String dexPath) {  
    try {  
      AssetManager assetManager = AssetManager.class.newInstance();  
      Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
      addAssetPath.invoke(assetManager, dexPath);  
      mAssetManager = assetManager;  
    } catch (Exception e) {  
      Log.i("inject", "loadResource error:"+Log.getStackTraceString(e));
      e.printStackTrace();  
    }  
    Resources superRes = super.getResources();  
    superRes.getDisplayMetrics();  
    superRes.getConfiguration();  
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());  
    mTheme = mResources.newTheme();  
    mTheme.setTo(super.getTheme());
  }  
  @Override  
  public AssetManager getAssets() {  
    return mAssetManager == null ? super.getAssets() : mAssetManager;  
  }  
  @Override  
  public Resources getResources() {  
    return mResources == null ? super.getResources() : mResources;  
  }  
  @Override  
  public Theme getTheme() {  
    return mTheme == null ? super.getTheme() : mTheme;  
  } 
}

首先我们来看一下具体步骤的代码实现:

1>、得到脱壳Apk中的dex文件,然后从这个文件中得到源程序Apk.进行解密,然后加载
//这是context 赋值
@Override
protected void attachBaseContext(Context base) {
  super.attachBaseContext(base);
  try {
    //创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录
    File odex = this.getDir("payload_odex", MODE_PRIVATE);
    File libs = this.getDir("payload_lib", MODE_PRIVATE);
    odexPath = odex.getAbsolutePath();
    libPath = libs.getAbsolutePath();
    apkFileName = odex.getAbsolutePath() + "/payload.apk";
    File dexFile = new File(apkFileName);
    Log.i("demo", "apk size:"+dexFile.length());
    if (!dexFile.exists())
    {
      dexFile.createNewFile();  //在payload_odex文件夹内,创建payload.apk
      // 读取程序classes.dex文件
      byte[] dexdata = this.readDexFileFromApk();

      // 分离出解壳后的apk文件已用于动态加载
      this.splitPayLoadFromDex(dexdata);
    }
    // 配置动态加载环境
    Object currentActivityThread = RefInvoke.invokeStaticMethod(
        "android.app.ActivityThread", "currentActivityThread",
        new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493
    String packageName = this.getPackageName();//当前apk的包名
    //下面两句不是太理解
    ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
        "android.app.ActivityThread", currentActivityThread,
        "mPackages");
    WeakReference wr = (WeakReference) mPackages.get(packageName);
    //创建被加壳apk的DexClassLoader对象  加载apk内的类和本地代码(c/c++代码)
    DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
        libPath, (ClassLoader) RefInvoke.getFieldOjbect(
            "android.app.LoadedApk", wr.get(), "mClassLoader"));
    //base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?
    //把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader  ----有点c++中进程环境的意思~~
    RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
        wr.get(), dLoader);

    Log.i("demo","classloader:"+dLoader);

    try{
      Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");
      Log.i("demo", "actObj:"+actObj);
    }catch(Exception e){
      Log.i("demo", "activity:"+Log.getStackTraceString(e));
    }


  } catch (Exception e) {
    Log.i("demo", "error:"+Log.getStackTraceString(e));
    e.printStackTrace();
  }
}
这里需要注意的一个问题,就是我们需要找到一个时机,就是在脱壳程序还没有运行起来的时候,来加载源程序的Apk,执行他的onCreate方法,那么这个时机不能太晚,不然的话,就是运行脱壳程序,而不是源程序了。查看源码我们知道。Application中有一个方法:   attachBaseContext

这个方法,他在Application的onCreate方法执行前就会执行了,那么我们的工作就需要在这里进行

1)、从脱壳程序Apk中找到源程序Apk,并且进行解密操作
//创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录
File odex = this.getDir("payload_odex", MODE_PRIVATE);
File libs = this.getDir("payload_lib", MODE_PRIVATE);
odexPath = odex.getAbsolutePath();
libPath = libs.getAbsolutePath();
apkFileName = odex.getAbsolutePath() + "/payload.apk";
File dexFile = new File(apkFileName);
Log.i("demo", "apk size:"+dexFile.length());
if (!dexFile.exists())
{
  dexFile.createNewFile();  //在payload_odex文件夹内,创建payload.apk
  // 读取程序classes.dex文件
  byte[] dexdata = this.readDexFileFromApk();

  // 分离出解壳后的apk文件已用于动态加载
  this.splitPayLoadFromDex(dexdata);
}

这个脱壳解密操作一定要和我们之前的加壳以及加密操作对应,不然就会出现Dex加载错误问题

A) 从Apk中获取到Dex文件

/**
 * 从apk包里面获取dex文件内容(byte)
 * @return
 * @throws IOException
 */
private byte[] readDexFileFromApk() throws IOException {
  ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
  ZipInputStream localZipInputStream = new ZipInputStream(
      new BufferedInputStream(new FileInputStream(
          this.getApplicationInfo().sourceDir)));
  while (true) {
    ZipEntry localZipEntry = localZipInputStream.getNextEntry();
    if (localZipEntry == null) {
      localZipInputStream.close();
      break;
    }
    if (localZipEntry.getName().equals("classes.dex")) {
      byte[] arrayOfByte = new byte[1024];
      while (true) {
        int i = localZipInputStream.read(arrayOfByte);
        if (i == -1)
          break;
        dexByteArrayOutputStream.write(arrayOfByte, 0, i);
      }
    }
    localZipInputStream.closeEntry();
  }
  localZipInputStream.close();
  return dexByteArrayOutputStream.toByteArray();
}

其实就是解压Apk文件,直接得到dex文件即可

B) 从脱壳Dex中得到源Apk文件

/**
 * 释放被加壳的apk文件,so文件
 * @param data
 * @throws IOException
 */
private void splitPayLoadFromDex(byte[] apkdata) throws IOException {
  int ablen = apkdata.length;
  //取被加壳apk的长度   这里的长度取值,对应加壳时长度的赋值都可以做些简化
  byte[] dexlen = new byte[4];
  System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);
  ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
  DataInputStream in = new DataInputStream(bais);
  int readInt = in.readInt();
  System.out.println(Integer.toHexString(readInt));
  byte[] newdex = new byte[readInt];
  //把被加壳apk内容拷贝到newdex中
  System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);
  //这里应该加上对于apk的解密操作,若加壳是加密处理的话
  //?
  
  //对源程序Apk进行解密
  newdex = decrypt(newdex);
  
  //写入apk文件   
  File file = new File(apkFileName);
  try {
    FileOutputStream localFileOutputStream = new FileOutputStream(file);
    localFileOutputStream.write(newdex);
    localFileOutputStream.close();
  } catch (IOException localIOException) {
    throw new RuntimeException(localIOException);
  }
  
  //分析被加壳的apk文件
  ZipInputStream localZipInputStream = new ZipInputStream(
      new BufferedInputStream(new FileInputStream(file)));
  while (true) {
    ZipEntry localZipEntry = localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的
    if (localZipEntry == null) {
      localZipInputStream.close();
      break;
    }
    //取出被加壳apk用到的so文件,放到 libPath中(data/data/包名/payload_lib)
    String name = localZipEntry.getName();
    if (name.startsWith("lib/") && name.endsWith(".so")) {
      File storeFile = new File(libPath + "/"
          + name.substring(name.lastIndexOf('/')));
      storeFile.createNewFile();
      FileOutputStream fos = new FileOutputStream(storeFile);
      byte[] arrayOfByte = new byte[1024];
      while (true) {
        int i = localZipInputStream.read(arrayOfByte);
        if (i == -1)
          break;
        fos.write(arrayOfByte, 0, i);
      }
      fos.flush();
      fos.close();
    }
    localZipInputStream.closeEntry();
  }
  localZipInputStream.close();


}

C) 解密源程序Apk

////直接返回数据,读者可以添加自己解密方法
private byte[] decrypt(byte[] srcdata) {
  for(int i=0;i<srcdata.length;i++){
    srcdata[i] = (byte)(0xFF ^ srcdata[i]);
  }
  return srcdata;
}

这个解密算法和加密算法是一致的

2>、加载解密之后的源程序Apk
//配置动态加载环境
Object currentActivityThread = RefInvoke.invokeStaticMethod(
    "android.app.ActivityThread", "currentActivityThread",
    new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493
String packageName = this.getPackageName();//当前apk的包名
//下面两句不是太理解
ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
    "android.app.ActivityThread", currentActivityThread,
    "mPackages");
WeakReference wr = (WeakReference) mPackages.get(packageName);
//创建被加壳apk的DexClassLoader对象  加载apk内的类和本地代码(c/c++代码)
DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
    libPath, (ClassLoader) RefInvoke.getFieldOjbect(
        "android.app.LoadedApk", wr.get(), "mClassLoader"));
//base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?
//把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader  ----有点c++中进程环境的意思~~
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
    wr.get(), dLoader);

Log.i("demo","classloader:"+dLoader);

try{
  Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");
  Log.i("demo", "actObj:"+actObj);
}catch(Exception e){
  Log.i("demo", "activity:"+Log.getStackTraceString(e));
}
2)、找到源程序的Application程序,让其运行
@Override
public void onCreate() {
  {
    //loadResources(apkFileName);
    
    Log.i("demo", "onCreate");
    // 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
    String appClassName = null;
    try {
      ApplicationInfo ai = this.getPackageManager()
          .getApplicationInfo(this.getPackageName(),
              PackageManager.GET_META_DATA);
      Bundle bundle = ai.metaData;
      if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {
        appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml文件中的。
      } else {
        Log.i("demo", "have no application class name");
        return;
      }
    } catch (NameNotFoundException e) {
      Log.i("demo", "error:"+Log.getStackTraceString(e));
      e.printStackTrace();
    }
    //有值的话调用该Applicaiton
    Object currentActivityThread = RefInvoke.invokeStaticMethod(
        "android.app.ActivityThread", "currentActivityThread",
        new Class[] {}, new Object[] {});
    Object mBoundApplication = RefInvoke.getFieldOjbect(
        "android.app.ActivityThread", currentActivityThread,
        "mBoundApplication");
    Object loadedApkInfo = RefInvoke.getFieldOjbect(
        "android.app.ActivityThread$AppBindData",
        mBoundApplication, "info");
    //把当前进程的mApplication 设置成了null
    RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
        loadedApkInfo, null);
    Object oldApplication = RefInvoke.getFieldOjbect(
        "android.app.ActivityThread", currentActivityThread,
        "mInitialApplication");
    //http://www.codeceo.com/article/android-context.html
    ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
        .getFieldOjbect("android.app.ActivityThread",
            currentActivityThread, "mAllApplications");
    mAllApplications.remove(oldApplication);//删除oldApplication
    
    ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
        .getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
            "mApplicationInfo");
    ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
        .getFieldOjbect("android.app.ActivityThread$AppBindData",
            mBoundApplication, "appInfo");
    appinfo_In_LoadedApk.className = appClassName;
    appinfo_In_AppBindData.className = appClassName;
    Application app = (Application) RefInvoke.invokeMethod(
        "android.app.LoadedApk", "makeApplication", loadedApkInfo,
        new Class[] { boolean.class, Instrumentation.class },
        new Object[] { false, null });//执行 makeApplication(false,null)
    RefInvoke.setFieldOjbect("android.app.ActivityThread",
        "mInitialApplication", currentActivityThread, app);


    ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(
        "android.app.ActivityThread", currentActivityThread,
        "mProviderMap");
    Iterator it = mProviderMap.values().iterator();
    while (it.hasNext()) {
      Object providerClientRecord = it.next();
      Object localProvider = RefInvoke.getFieldOjbect(
          "android.app.ActivityThread$ProviderClientRecord",
          providerClientRecord, "mLocalProvider");
      RefInvoke.setFieldOjbect("android.content.ContentProvider",
          "mContext", localProvider, app);
    }
    
    Log.i("demo", "app:"+app);
    
    app.onCreate();
  }
}

直接在脱壳的Application中的onCreate方法中进行就可以了。这里我们还可以看到是通过AndroidManifest.xml中的meta标签获取源程序Apk中的Application对象的。

下面来看一下AndoridManifest.xml文件中的内容:

在这里我们定义了源程序Apk的Application类名。

项目下载: http://download.csdn.net/detail/jiangwei0910410003/9102741

四、运行程序

那么到这里我们就介绍完了,这三个项目的内容,下面就来看看如何运行吧:

运行步骤:

第一步:得到源程序Apk文件和脱壳程序的Dex文件

运行源程序和脱壳程序项目,之后得到这两个文件(记得将classes.dex文件改名ForceApkObj.dex),然后使用加壳程序进行加壳:

这里的ForceApkObj.apk文件和ForceApkObj.dex文件是输入文件,输出的是classes.dex文件。

第二步:替换脱壳程序中的classes.dex文件

我们在第一步中得到加壳之后的classes.dex文件之后,并且我们在第一步运行脱壳项目的时候得到一个ReforceApk.apk文件,这时候我们使用解压缩软件进行替换:

第三步:我们在第二步的时候得到替换之后的ReforceApk.apk文件,这个文件因为被修改了,所以我们需要从新对他签名,不然运行也是报错的。

工具下载:  http://download.csdn.net/detail/jiangwei0910410003/9102767

下载之后的工具需要用ReforeceApk.apk文件替换ReforceApk_des.apk文件,然后运行run.bat就可以得到签名之后的文件了。

run.bat文件的命令如下:

cd C:\Users\i\Desktop\forceapks  
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk.apk jiangwei  
del ReforceApk.apk 

这里最主要的命令就是中间的一条签名的命令,关于命令的参数说明如下:

jarsigner -verbose -keystore 签名文件 -storepass 密码  -keypass alias的密码 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA  签名后的文件 签名前的apk alias名称  
eg:  
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk_src.apk jiangwei  
签名文件的密码:123456  
alais的密码:123456 

所以这里我们在得到ReforceApk.apk文件的时候,需要签名,关于Eclipse中如何签名一个Apk的话,这里就不多说了,自己google一下吧:

那么通过上面的三个步骤之后我们得到一个签名之后的最终文件:  ReforceApk_des.apk

我们安装这个Apk,然后运行,效果如下:

看到运行结果的那一瞬间,我们是多么的开心,多么的有成就感,但是这个过程中遇到的问题,是可想而知的。

我们这个时候再去反编译一下源程序Apk(这个文件是我们脱壳出来的payload.apk,看ReforeceApk中的代码,就知道他的位置了)

发现dex文件格式是不正确的。说明我们的加固是成功的。

五、遇到的问题

1、研究的过程中遇到签名不正确的地方,开始的时候,直接替换dex文件之后,就直接运行了Apk,但是总是提示签名不正确。

2、运行的过程中说找不到源程序中的Activity,这个问题其实我在动态加载的那篇文章中说道了,我们需要在脱壳程序中的AndroidManifest.xml中什么一下源程序中的Activiity:

六、技术要点

1、对Dex文件格式的了解
2、动态加载技术的深入掌握
3、Application的执行流程的了解
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值