Apk加壳实现

 

前几天在网上看到一篇不错的介绍关于apk加壳的介绍,Android中的Apk的加固(加壳)原理解析和实现,针对里面关于资源加载这块自己研究了下,给出了一个方案,下面结合那篇文章的内容做一下apk加壳流程介绍 

一、将目标apk加密放进壳apk的classes.dex里面,代码如下

package com.example;

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 MyClass {
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            File payloadSrcFile = new File("C:\\Users\\Desktop\\force\\ForceApkObj.apk");   //需要加壳的程序
            System.out.println("apk size:"+payloadSrcFile.length());
            File unShellDexFile = new File("C:\\Users\\Desktop\\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 = newbyte[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为长度
            //修改DEXfile size文件头
            fixFileSizeHeader(newdex);
            //修改DEXSHA1 文件头
            fixSHA1Header(newdex);
            //修改DEXCheckSum文件头
            fixCheckSumHeader(newdex);

            String str = "C:\\Users\\jalen_yang\\Desktop\\force\\classes.dex";
            File file = new File(str);
            if (!file.exists()){
                file.createNewFile();
            }

            FileOutputStreamlocalFileOutputStream = new FileOutputStream(str);
           localFileOutputStream.write(newdex);
           localFileOutputStream.flush();
           localFileOutputStream.close();


        } catch (Exceptione) {
            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 = newbyte[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(intnumber) {
        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];
        ByteArrayOutputStreamlocalByteArrayOutputStream = 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();
            }
        }
    }
}

 

二、     壳apk动态加载目标apk,注意的是目前发现android studio2.2开始编译的debug apk里面会出现两个dex,然后导致动态加载失败,这是因为studio2.2开始默认打开了 Instant Run选项,所以测试动态加载时不要打开这个开关,2.2开始需要自己手动去Settings里面关掉

 

 
 
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.Field;
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;
         try {
            Field mAssets = Resources.class
                  .getDeclaredField("mAssets");
            mAssets.setAccessible(true);
            mAssets.set(super.getResources(), assetManager);
            Log.i("demo", "mAssets  exist, is "+mAssets);
         } catch (Throwable ignore) {
            Log.i("demo", "mAssets don't exist ,search mResourcesImpl:");
            Field mResourcesImpl = Resources.class
                  .getDeclaredField("mResourcesImpl");
            mResourcesImpl.setAccessible(true);
            Object resourceImpl = mResourcesImpl.get(super.getResources());
            Log.i("demo", "mResourcesImpl  exist, is "+resourceImpl);
            Field implAssets = resourceImpl.getClass()
                  .getDeclaredField("mAssets");
            implAssets.setAccessible(true);
            implAssets.set(resourceImpl, assetManager);
         }

      } catch (Exception e) {
         Log.i("demo", "loadResource error:"+Log.getStackTraceString(e));
         e.printStackTrace();
      }
     }
}

这边重点解释一下壳apk动态加载资源这一块,首先解释一下apk加载资源这块的大概流程:

1、 Resources对象包含一个AssetManager对象,在7.0以前这个变量直接是Resources的一个属性,因此要替换AssetManager就要找它的mAssets成员变量,7.0开始Resources对象不再有mAssets属性,取代的是mResourcesImpl属性,这个属性其实就是一个Resources的实现类,它里面包含了一个AssetManager属性,所以在7.0上面就要多一个步骤来替换AssetManager了,也就是上面代码中那样

2、 android系统加载apk资源时主要通过AssetManager来解析Resources.arsc文件,AssetManagerc++类的成员变量mResources指向的是一个ResTable对象,这个ResTable就是解析后的资源索引表,每次加载一个apk就会将这个apkResources.arsc解析后放到这个ResTable里面,以及字符串资源池变量里面

3、 在android5.0以前AssetManager对于相同的package id,比如0x7f,搜索资源时是按照逆序,也就是从后往前,第一个找到的就是返回的对象,因此要先加载当前应用的资源,再加载patch的资源,才能实现覆盖,不过如果找到最后一个发现不存在,就会抛异常,因为android系统不允许新增资源,只允许覆盖已有资源;android 5.0以后就不一样了,搜索相同package id,按照从前往后,因此你要先加载patch资源,再加载当前应用资源,这样你就得重新创建一个AssetManager了

4、 我们平常看到的R.java里面的每个id都是由三部分组成的。分别是:mpackage、type、configurelist,mpackage代表的是资源包,由一个字节表示,比如系统资源包、当前应用资源包、第三方资源包等等,默认情况下系统资源包用0x01表示,当前应用资源包用0x7f,在这两个值范围内的都是合法的id,否则是不合法的。Type代表的是资源类型,同样是一个字节,比如layout、drawable、string等等;最后两个字节代表的是次序,也就是偏移,用来定位具体的资源位置。所以搜索资源时首先分解id成上面三个类型,然后在资源索引表里搜索,注意的是相同的mpackage可能存在多个组,也就是上面提到的覆盖资源问题,每个package对应的是一个PakcageGroup,而这个PakcageGroup包含多个Package,这里的Package不是包名,也和mpackage不同,可以看看5.0以前的系统源码:

ssize_tResTable::getResource(uint32_t resID, Res_value* outValue, bool mayBeBag, 
        uint32_t* outSpecFlags,ResTable_config* outConfig) const 
{ 
    ...... 
 
    const ssize_t p =getResourcePackageIndex(resID); 
    const int t = Res_GETTYPE(resID); 
    const int e = Res_GETENTRY(resID); 
 
    ...... 
 
    const Res_value* bestValue = NULL; 
    const Package* bestPackage = NULL; 
    ResTable_config bestItem; 
    memset(&bestItem, 0, sizeof(bestItem));// make the compiler shut up 
 
    if (outSpecFlags != NULL) *outSpecFlags =0; 
 
    // Look through all resource packages,starting with the most 
    // recently added. 
    const PackageGroup* const grp =mPackageGroups[p]; 
    ...... 
 
    size_t ip = grp->packages.size(); 
    while (ip > 0) { 
        ip--; 
        int T = t; 
        int E = e; 
 
        const Package* const package =grp->packages[ip]; 
        if (package->header->resourceIDMap){ 
            uint32_t overlayResID = 0x0; 
            status_t retval =idmapLookup(package->header->resourceIDMap, 
                                         package->header->resourceIDMapSize, 
                                          resID, &overlayResID); 
            if (retval == NO_ERROR &&overlayResID != 0x0) { 
                // for this loop iteration,this is the type and entry we really want 
                ...... 
                T =Res_GETTYPE(overlayResID); 
                E =Res_GETENTRY(overlayResID); 
            } else { 
                // resource not present inoverlay package, continue with the next package 
                continue; 
            } 
        } 
 
        const ResTable_type* type; 
        const ResTable_entry* entry; 
        const Type* typeClass; 
        ssize_t offset = getEntry(package, T,E, &mParams, &type, &entry, &typeClass); 
        if (offset <= 0) { 
            // No {entry, appropriate config}pair found in package. If this 
            // package is an overlay package(ip != 0), this simply means the 
            // overlay package did not specifya default. 
            // Non-overlay packages are stillrequired to provide a default. 
            if (offset < 0 && ip ==0) { 
                ...... 
                return offset; 
            } 
            continue; 
        } 
 
        if((dtohs(entry->flags)&entry->FLAG_COMPLEX) != 0) { 
            ...... 
            continue; 
        } 
 
        ...... 
 
        const Res_value* item = 
            (const Res_value*)(((constuint8_t*)type) + offset); 
        ResTable_config thisConfig; 
       thisConfig.copyFromDtoH(type->config); 
 
        if (outSpecFlags != NULL) { 
            if (typeClass->typeSpecFlags !=NULL) { 
                *outSpecFlags |=dtohl(typeClass->typeSpecFlags[E]); 
            } else { 
                *outSpecFlags = -1; 
            } 
        } 
 
       if (bestPackage != NULL && 
           (bestItem.isMoreSpecificThan(thisConfig) || bestItem.diff(thisConfig) ==0)) { 
            // Discard thisConfig not only ifbestItem is more specific, but also if the two configs 
            // are identical (diff == 0), oroverlay packages will not take effect. 
            continue; 
        } 
 
        bestItem = thisConfig; 
        bestValue = item; 
        bestPackage = package; 
    } 
 
    ...... 
 
    if (bestValue) { 
        outValue->size =dtohs(bestValue->size); 
        outValue->res0 =bestValue->res0; 
        outValue->dataType =bestValue->dataType; 
        outValue->data =dtohl(bestValue->data); 
        if (outConfig != NULL) { 
            *outConfig = bestItem; 
        } 
        ...... 
        returnbestPackage->header->index; 
    } 
 
    return BAD_VALUE; 
}  


猜你喜欢

转载自blog.csdn.net/u012292247/article/details/65936735