您现在的位置是:网站首页> Android

Android中的Apk的加固(加壳)原理解析和实现

  • Android
  • 2021-05-06
  • 899人已阅读
摘要

Android中的Apk的加固(加壳)原理解析和实现

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

1.png

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


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.png



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


1、源程序项目(需要加密的Apk)


2、脱壳项目(解密源Apk和加载Apk)


3、对源Apk进行加密和脱壳项目的Dex的合并


三、项目案例

下面先来看一下源程序

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

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);  

    }  

  

}  

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.png


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


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


[java] view plain copy

 

//修改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.png

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


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


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

1.png

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


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


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


 


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


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


 


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

1.png



 


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


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.进行解密,然后加载


[java] view plain copy

 

//这是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,并且进行解密操作


[java] view plain copy

 

//创建两个文件夹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文件


 


[java] view plain copy

 

/** 

 * 从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文件


[java] view plain copy

 

/** 

 * 释放被加壳的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


[java] view plain copy

 

////直接返回数据,读者可以添加自己解密方法  

private byte[] decrypt(byte[] srcdata) {  

    for(int i=0;i<srcdata.length;i++){  

        srcdata[i] = (byte)(0xFF ^ srcdata[i]);  

    }  

    return srcdata;  

}  

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


 


2>、加载解密之后的源程序Apk


[java] view plain copy

 

//配置动态加载环境  

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程序,让其运行


[java] view plain copy

 

@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文件中的内容:

1.png

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


 


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


 


四、运行程序

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


运行步骤:


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

1.png

2.png

   


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


1.png


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


 


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


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

1.png



 


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


2.png


工具下载: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一下吧:


1.png


 


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


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

2.png



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


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

1.png



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


 


五、遇到的问题

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


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

1.png



 


六、技术要点

1、对Dex文件格式的了解


2、动态加载技术的深入掌握


3、Application的执行流程的了解


4、如何从Apk中得到Dex文件


5、如何从新签名一个Apk程序


 


七、综合概述

我们通过上面的过程可以看到,关于Apk加固的工作还是挺复杂的,涉及到的东西也挺多的,下面就在来总结一下吧:


1、加壳程序


任务:对源程序Apk进行加密,合并脱壳程序的Dex文件 ,然后输入一个加壳之后的Dex文件


语言:任何语言都可以,不限于Java语言


技术点:对Dex文件格式的解析


 


2、脱壳程序


任务:获取源程序Apk,进行解密,然后动态加载进来,运行程序


语言:Android项目(Java)


技术点:如何从Apk中获取Dex文件,动态加载Apk,使用反射运行Application


 


八、总结

Android中的Apk反编译可能是每个开发都会经历的事,但是在反编译的过程中,对于源程序的开发者来说那是不公平的,那么Apk加固也是应运而生,但是即使是这样,我们也还是做不到那么的安全,现在网上也是有很多文章在解析梆梆加固的原理了。而且有人破解成功了,那么加固还不是怎么安全。最后一句话:逆向和加固是一个永不停息的战争。


Top