zoukankan      html  css  js  c++  java
  • android 加固防止反编译-重新打包

    http://blog.csdn.net/u010921385/article/details/52505094

    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:UsersiDesktopforceapks  
    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的执行流程的了解

  • 相关阅读:
    SaveFileDialog
    在SQL Server 2008中配置文件流(FILESTREAM)
    C#中图片转二进制到存储数据库
    OpenFileDialog
    WPF中自定义只能输入数字的TextBox
    QL Server 2008新特性:FILESTREAM
    ERROR 2003 (HY000): Can't connect to MySQL server on 'localhost' (10061)
    winxp+Apache+Mysql+Python+Django安装配置
    django最佳实践
    Sphinx 在 windows 下安装使用
  • 原文地址:https://www.cnblogs.com/jukan/p/6519119.html
Copyright © 2011-2022 走看看