Apk源码的加固(加壳)原理解析和实现

栏目: IOS · Android · 发布时间: 7年前

内容简介:Apk源码的加固(加壳)原理解析和实现
好久没写博客了,要深刻检讨下!

前言:

在Android中没有经过加密的Apk给人的感觉就是在裸奔,通过apktool,dex2jar,AndroidKill等各式各样的反编译 工具 就可以轻松的获取其smail代码,如这个叫SourceProject的helloworld程序被apktool反编译后,对于懂smail语法的逆向工程师来说就一览无余了。破解与反破解是相对的,所以我们尽可能的给自己的Apk多穿点衣服。

Apk源码的加固(加壳)原理解析和实现

原理解析

首先我们先来看下Apk加壳的步骤:

Apk源码的加固(加壳)原理解析和实现
  • 源Apk:需要加壳的Apk
  • 加密的Apk:源Apk经过加密算法加密后的Apk
  • 加壳程序Apk:是有解密源Apk和动态加载启动源Apk的外壳

首先我们拿到需要加壳的源Apk,通过加密算法加密源Apk然后与加壳Apk的dex文件组合成新的Dex文件,然后将加壳程序Apk的Dex文件替换成新的Dex,生成新的Apk重新签名。

我们先来看下Dex文件的结构:

Apk源码的加固(加壳)原理解析和实现
  • Magic
    Magic数是为了方便虚拟机识别目标文件是否是合格的Dex文件,在Dex文件中magic的值固定值
  • checksum
    文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误
  • signature
    使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。
  • file_size
    当前Dex 文件的大小 。

所以我们在将Dex与加密算法加密后的Apk合并生成新的Dex后需要修改新Dex文件的这三个值,为了方便从新Dex中获得加密的Apk,我们需要知道加密的Apk的大小,为了方便以后获得,我们将其大小放置在新Dex的后四位,新生成的Dex文件结构:

Apk源码的加固(加壳)原理解析和实现

生成新Dex后,将加壳程序Apk的Dex文件替换,重新签名后加壳的Apk即完成了。如果觉得步骤理清了,我们来看下其具体的实现。

具体实现:

这过程一共要创建三个项目。

首先我们先创建一个需要加密的Apk项目,结构非常简单只有几个Log的打印,结构

Apk源码的加固(加壳)原理解析和实现

SourceApplication.java

package com.jju.yuxin.sourceproject;
import android.app.Application;
import android.util.Log;

public class SourceApplication extends Application {
    private static final String TAG=SourceApplication.class.getSimpleName();
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG,"-------------onCreate");
    }
}

MainActivity.java

package com.jju.yuxin.sourceproject;

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

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

SubActivity.java

package com.jju.yuxin.sourceproject;
import android.app.Activity;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

public class SubActivity extends Activity {
    private static final String TAG=SubActivity.class.getSimpleName();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView tv_content = new TextView(this);
        tv_content.setText("I am SubActivity");
        setContentView(tv_content);
        Log.i(TAG, "SubActivity:app:"+getApplicationContext());
    }
}

然后将其打包生成Apk。

第二个项目是一个 JAVA 项目用于将源Apk加密,并合并加壳程序Dex与加密后的源Apk。在贴出这个代码时我们先不用管加壳程序Dex从何而来,假设已经有了这样一个文件。我们来看下具体实现:

package com.forceapk;

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 { 
    public static void main(String[] args) {  
        try {  
        	//需要加壳的源APK  ,以二进制形式读出,并进行加密处理
            File srcApkFile = new File("force/SourceAPK.apk");   
            System.out.println("apk size:"+srcApkFile.length()); 
            byte[] enSrcApkArray = encrpt(readFileBytes(srcApkFile));
            
            //需要解壳的dex  以二进制形式读出dex  
            File unShellDexFile = new File("force/shelldex.dex");    
            byte[] unShellDexArray = readFileBytes(unShellDexFile);

            //将源APK长度和需要解壳的DEX长度相加并加上存放源APK大小的四位得到总长度
            int enSrcApkLen = enSrcApkArray.length;  
            int unShellDexLen = unShellDexArray.length;  
            int totalLen = enSrcApkLen + unShellDexLen +4;
            
            //依次将解壳DEX,加密后的源APK,加密后的源APK大小,拼接出新的Dex
            byte[] newdex = new byte[totalLen];  
            System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);
            System.arraycopy(enSrcApkArray, 0, newdex, unShellDexLen, enSrcApkLen);
            System.arraycopy(intToByte(enSrcApkLen), 0, newdex, totalLen-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();  
            }  
        }  
    }  
}

我们可以看到程序比较简单和我们之前描述的一样,核心部分就在文件拼接部分和修改三个头信息的部分。

//依次将解壳DEX,加密后的源APK,加密后的源APK大小,拼接出新的Dex
byte[] newdex = new byte[totalLen];  
System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);
System.arraycopy(enSrcApkArray, 0, newdex, unShellDexLen, enSrcApkLen);
System.arraycopy(intToByte(enSrcApkLen), 0, newdex, totalLen-4, 4);
//修改DEX file size文件头  
fixFileSizeHeader(newdex);  
//修改DEX SHA1 文件头  
fixSHA1Header(newdex);  
//修改DEX CheckSum文件头  
fixCheckSumHeader(newdex);

我们再来看下第三个项目:

加壳程序,这个程序主要负责将在JAVA项目中加密的源Apk获取及解密,以及动态加载源Apk。项目结构

Apk源码的加固(加壳)原理解析和实现

我们看下代码:

package com.jju.yuxin.reforceapk;

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.res.AssetManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;

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.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import dalvik.system.DexClassLoader;

/**
 * =============================================================================
 * Copyright (c) 2017 yuxin All rights reserved.
 * Packname com.jju.yuxin.reforceapk
 * Created by yuxin.
 * Created time 2017/6/18 0018 下午 5:03.
 * Version   1.0;
 * Describe :
 * History:
 * ==============================================================================
 */
public class ProxyApplication extends Application{

    private static final String appkey = "APPLICATION_CLASS_NAME";
    private  static final String TAG=ProxyApplication.class.getSimpleName();
    private String srcApkFilePath;
    private String odexPath;
    private String libPath;
    //以下是加载资源
    protected AssetManager mAssetManager;
    protected Resources mResources;
    protected Resources.Theme mTheme;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Log.d(TAG,"----------onCreate");
        try {

            File odex = this.getDir("payload_odex", MODE_PRIVATE);
            File libs = this.getDir("payload_lib", MODE_PRIVATE);
            //用于存放源apk释放出来的dex
            odexPath = odex.getAbsolutePath();
            //用于存放源Apk用到的so文件
            libPath = libs.getAbsolutePath();
            //用于存放解密后的apk
            srcApkFilePath = odex.getAbsolutePath() + "/payload.apk";

            File srcApkFile= new File(srcApkFilePath);
            Log.i("demo", "apk size:"+srcApkFile.length());

            //第一次加载
            if (!srcApkFile.exists())
            {
                Log.i("demo", "isFirstLoading");
                srcApkFile.createNewFile();
                //拿到dex文件
                byte[] dexdata = this.readDexFileFromApk();
                //取出源APK解密后放置在/payload.apk,及其so文件放置在payload_lib/下
                this.splitPayLoadFromDex(dexdata);
            }

            // 配置动态加载环境
            //反射获取主线程对象,并从中获取所有已加载的package信息,并中找到当前的LoadApk对象的弱引用
            Object currentActivityThread = RefInvoke.invokeStaticMethod(
                    "android.app.ActivityThread", "currentActivityThread",
                    new Class[] {}, new Object[] {});
            String packageName = this.getPackageName();
            ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mPackages");
            WeakReference wr = (WeakReference) mPackages.get(packageName);

            //创建一个新的DexClassLoader用于加载源Apk,
            // 传入apk路径,dex释放路径,so路径,及父节点的DexClassLoader使其遵循双亲委托模型
            DexClassLoader dLoader = new DexClassLoader(srcApkFilePath, odexPath,
                    libPath, (ClassLoader) RefInvoke.getFieldOjbect(
                    "android.app.LoadedApk", wr.get(), "mClassLoader"));

            //getClassLoader()等同于 (ClassLoader) RefInvoke.getFieldOjbect()
            //但是为了替换掉父节点我们需要通过反射来获取并修改其值
            Log.i(TAG,"父classloader:"+(ClassLoader) RefInvoke.getFieldOjbect(
                    "android.app.LoadedApk", wr.get(), "mClassLoader"));
            //将父节点DexClassLoader替换
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
                    wr.get(), dLoader);

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

            try{
                //尝试加载源Apk的MainActivity
                Object actObj = dLoader.loadClass("com.jju.yuxin.sourceproject.MainActivity");

                Log.i(TAG, "SrcApk_MainActivity:"+actObj);
            }catch(Exception e){
                Log.i(TAG, "LoadSrcActivityErr:"+Log.getStackTraceString(e));
            }


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


    public void onCreate() {

            //加载源apk资源
            //loadResources(srcApkFilePath);

            Log.i(TAG, "--------onCreate");
            //获取配置在清单文件的源Apk的Application路劲
            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(TAG, "have no application class name");
                    return;
                }
            } catch (PackageManager.NameNotFoundException e) {
                Log.i(TAG, "error:"+Log.getStackTraceString(e));
                e.printStackTrace();
            }

            //获取当前壳Apk的ApplicationInfo
            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");
            //将LoadedApk中的ApplicationInfo设置为null
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
                    loadedApkInfo, null);

            //获取currentActivityThread中注册的Application
            Object oldApplication = RefInvoke.getFieldOjbect(
                    "android.app.ActivityThread", currentActivityThread,
                    "mInitialApplication");

            //获取ActivityThread中所有已注册的Application,并将当前壳Apk的Application从中移除
            ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
                    .getFieldOjbect("android.app.ActivityThread",
                            currentActivityThread, "mAllApplications");
            mAllApplications.remove(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");
            //替换原来的Application
            appinfo_In_LoadedApk.className = appClassName;
            appinfo_In_AppBindData.className = appClassName;

            //注册Application
            Application app = (Application) RefInvoke.invokeMethod(
                    "android.app.LoadedApk", "makeApplication", loadedApkInfo,
                    new Class[] { boolean.class, Instrumentation.class },
                    new Object[] { false, null });

            //替换ActivityThread中的Application
            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(TAG, "Srcapp:"+app);

            app.onCreate();

    }


    private void splitPayLoadFromDex(byte[] shelldexdata) throws IOException {
        int sdlen = shelldexdata.length;
        //取被加壳apk的长度
        byte[] dexlen = new byte[4];
        System.arraycopy(shelldexdata, sdlen - 4, dexlen, 0, 4);
        ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
        DataInputStream in = new DataInputStream(bais);
        int readInt = in.readInt();
        Log.d(TAG,"Integer.toHexString(readInt):"+Integer.toHexString(readInt));

        //取出apk
        byte[] ensrcapk = new byte[readInt];
        System.arraycopy(shelldexdata, sdlen - 4 - readInt, ensrcapk, 0, readInt);

        //对源程序Apk进行解密
        byte[]  srcapk = decrypt(ensrcapk);

        //写入源apk文件
        File file = new File(srcApkFilePath);
        try {
            FileOutputStream localFileOutputStream = new FileOutputStream(file);
            localFileOutputStream.write(srcapk);
            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文件
     * @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;
            }
            //拿到dex文件
            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 void loadResources(String srcApkPath) {
        //创建一个AssetManager放置源apk的资源
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, srcApkPath);
            mAssetManager = assetManager;
        } catch (Exception e) {
            Log.i(TAG, "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 Resources.Theme getTheme() {
        return mTheme == null ? super.getTheme() : mTheme;
    }

}

这个文件比较长我们来依次分析:

//第一次加载
if (!srcApkFile.exists())
{
    Log.i("demo", "isFirstLoading");
    srcApkFile.createNewFile();
    //拿到dex文件
    byte[] dexdata = this.readDexFileFromApk();
    //取出源APK解密后放置在/payload.apk,及其so文件放置在payload_lib/下
    this.splitPayLoadFromDex(dexdata);
}

通过判断用于存放解密后的源Apk文件是否存在来判断是否是第一次加载。第一次加载时通过readDexFileFromApk()来获取当前Apk的Dex文件

/**
 * 拿到自己apk文件中的dex文件
 * @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;
        }
        //拿到dex文件
        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();
}

然后通过this.splitPayLoadFromDex();将当前Dex分解,从中获取源Apk并将其解密,以及源Apk的so库

private void splitPayLoadFromDex(byte[] shelldexdata) throws IOException {
       int sdlen = shelldexdata.length;
       //取被加壳apk的长度
       byte[] dexlen = new byte[4];
       System.arraycopy(shelldexdata, sdlen - 4, dexlen, 0, 4);
       ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
       DataInputStream in = new DataInputStream(bais);
       int readInt = in.readInt();
       Log.d(TAG,"Integer.toHexString(readInt):"+Integer.toHexString(readInt));

       //取出apk
       byte[] ensrcapk = new byte[readInt];
       System.arraycopy(shelldexdata, sdlen - 4 - readInt, ensrcapk, 0, readInt);

       //对源程序Apk进行解密
       byte[]  srcapk = decrypt(ensrcapk);

       //写入源apk文件
       File file = new File(srcApkFilePath);
       try {
           FileOutputStream localFileOutputStream = new FileOutputStream(file);
           localFileOutputStream.write(srcapk);
           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();
   }

然后通过动态加载机制将加壳程序的ClassLoader替换成他的子ClassLoader这样确保既能加载自己的Class又能加载源Apk的Class

核心代码,如果这段代码不是很懂,你可能需要去了解Java反射,Android中Classloader的双亲委托模型,以及动态加载机制

DexClassLoader dLoader = new DexClassLoader(srcApkFilePath, odexPath,
        libPath, (ClassLoader) RefInvoke.getFieldOjbect(
        "android.app.LoadedApk", wr.get(), "mClassLoader"));

然后在Application的onCreate()中替换LoadApk以及ActivityThread中的Application,希望更清楚的明白这点需要了解下Android中Activity以及Application的启动流程,其中在加壳程序的清单文件中我们配置了源Apk的相关信息以便能找到他们

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:name=".ProxyApplication"
    android:theme="@style/AppTheme">
    <meta-data
        android:name="APPLICATION_CLASS_NAME"
        android:value="com.jju.yuxin.sourceproject.SourceApplication"/>
    
    <activity android:name="com.jju.yuxin.sourceproject.MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>

            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>
    <activity android:name="com.jju.yuxin.sourceproject.SubActivity">
    </activity>
</application>

实现操作流程:

先将源Apk的项目生成Apk,放置到JAVA项目中

Apk源码的加固(加壳)原理解析和实现

将加壳程序也生成Apk,通过直接将apk后缀名改成zip的方式获取到classes.dex,(最好复制一份,这个后面还要用)。将classes.dex改名成shelldex.dex放置到JAVA项目中

Apk源码的加固(加壳)原理解析和实现

运行JAVA项目,将生成新的Dex文件classes.dex,将新生成的classes.dex替换加壳Apk的classes.dex(通过解压软件直接拖放进去替换即可)

Apk源码的加固(加壳)原理解析和实现

最后cd到apktool的目录下,使用apktool中的jarsigner对应用重新签名即可,重新签名指令

jarsigner -verbose -keystore 签名文件路径 -storepass 密码 -keypass 密码 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar 签名后生成Apk路径 需要签名Apk路径 签名文件别名
//例如:jarsigner -verbose -keystore yuxin.jks -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk reforce.apk yuxin

这三个项目的Github地址( 项目地址

常见错误:

  • ClassNotFoundException:

    这个错误主要注意:Class路径拼写有没错,加密Apk能否正确的转为源Apk,还有就是ClassLoader有没用错

  • Class ref in pre-verified class resolved to unexpected

    implementation:

    这是类被重复加载的错误,需要检查报错的类是否被别的ClassLoder已经加载过了,我的Activity在继承android.support.v7.app.AppCompatActivity时候报了这个错误,改成Activity就好了,原因还没去找。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

痛点

痛点

马丁·林斯特龙 / 陈亚萍 / 中信出版集团股份有限公司 / 2017-4-1 / CNY 49.00

互联网经济迅猛发展,大数据成为分析用户需求的一种惯性路径。世界首席品牌营销专家林斯特龙则指出,大数据连接了千百万的数据点,可以准确地产生相互关系。但是,当人类按照自己的习惯行动时,大数据分析通常不会十分准确。所以挖掘用户需求时,在大数据之外,更重要的是通过对一个小群体的亲身观察和小数据常识,捕捉到这个社会群体所体现出的文化欲望。满足这些用户需求,击中痛点,则意味着将掌握无限的商机。一起来看看 《痛点》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

URL 编码/解码
URL 编码/解码

URL 编码/解码