加壳VS脱壳(第一代)

2020-09-01  本文已影响0人  M_天河

原理:
首先有下面三个工程
1、源程序项目(需要加密的Apk)
2、脱壳项目(解密源Apk和加载Apk)
3、对源Apk进行加密和脱壳项目的Dex的合并
以byte数组格式读取源apk,并按自己设计的算法进行加密,然后直接拼接到脱壳apk的dex文件后面,然后按照标准dex文件的格式对拼接后的dex进行修改文件头中的文件大小位、SHA-1校验位、sum校验位,

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

然后在脱壳apk中重写Application方法,在方法运行之初去读取dex文件按照指定拼接位置读取出加密apk文件,然后解密出源apk文件,再通过动态加载技术将当前apk的dexClassLoader替换为源apk的dexClassLoader,

来看下具体代码:

一,源apk(随便写一个应用)

应用只有一个MainActivity和MyApplication



AndroidManifest.xml,这里没什么需要注意的,正常来写就行了

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.smm.forceapkobj">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:name="com.smm.forceapkobj.MyApplication">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

MainActivity.java,也没啥需要注意的,需要的话打个Log就行了

package com.smm.forceapkobj;

import android.support.v7.app.AppCompatActivity;
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;
import android.widget.Toast;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.i("测试记录","进入到了源程序MainActivityOnCreate");
        super.onCreate(savedInstanceState);

        TextView content = new TextView(this);
        content.setText("I am Source Apk");
        content.setOnClickListener(new OnClickListener(){
            @Override
            public void onClick(View arg0){
                Toast myToast = Toast.makeText(getApplicationContext(),"this is MainActivity!!!",Toast.LENGTH_SHORT);
                myToast.show();
            }
        });
        setContentView(content);
    }
}

MyApplication.java

package com.smm.forceapkobj;

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

public class MyApplication extends Application{

    @Override
    public void onCreate(){
        Log.i("测试记录","进入到了源程序Application");
        super.onCreate();
        Log.i("测试记录","source apk onCreate:" + this);
    }
}

二,编写脱壳程序

结构也很简单,主要这里不要有其他组件,如果有MainActivity的话执行完了源程序的Application之后就会执行这里的MainActivity。


AndroidManifest.xml,这里除了要注册ProxyApplication之外还要注册源程序中的组件,另外还要用<meta-data>标签获取源程序中的Application对象。
标签如下:<meta-data android:name="APPLICATION_CLASS_NAME" android:value="com.smm.forceapkobj.MyApplication"/>,后面会在ProxyApplication的onCreate代码中看到具体获取方式。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.smm.reforceapk">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:name="com.smm.reforceapk.ProxyApplication">
        <meta-data android:name="APPLICATION_CLASS_NAME" android:value="com.smm.forceapkobj.MyApplication"/>
        <activity android:name="com.smm.forceapkobj.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

ProxyApplication.java

package com.smm.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.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.os.Debug;
import android.util.ArrayMap;
import android.util.Log;
import dalvik.system.DexClassLoader;

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;

public class ProxyApplication extends Application{
    private static final String appkey = "APPLICATION_CLASS_NAME";
    private String apkFileName;
    private String odexPath;
    private String libPath;

    @Override
    protected void attachBaseContext(Context base){
        //Debug.waitForDebugger();
        super.attachBaseContext(base);
        try{
            //从dex中读取apk并解密,还原出源apk,payload.apk
            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();
                byte[] dexdata = this.readDexFileFromApk();
                this.splitPayLoadFromDex(dexdata);
            }
            //配置动态加载环境
            //获取主线程对象ActivityTread
            Object currentActivityTread = RefInvoke.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",new Class[]{},new Object[]{});
            String packageName = this.getPackageName();
            //当前app的包名传递给mPachages来获得当前app的弱引用,从而找到当前app的类加载器mClassLoader
            ArrayMap mPackages = (ArrayMap)RefInvoke.getFieldObject("android.app.ActivityThread",currentActivityTread,"mPackages");
            WeakReference wr = (WeakReference)mPackages.get(packageName);
            //创建被加壳apk的DexClassLoader对象,加载apk内的类和native代码
            DexClassLoader dLoader = new DexClassLoader(apkFileName,odexPath,libPath,
                    (ClassLoader)RefInvoke.getFieldObject("android.app.LoadedApk",wr.get(),"mClassLoader"));
            //把当前进程的DexClassLoader设置成被加壳apk的DexClassLoader
            RefInvoke.setFieldObject("android.app.LoadedApk","mClassLoader",wr.get(),dLoader);
            Log.i("demo","classloader:" + dLoader);
        }catch (Exception e){
            Log.i("demo","error:" + Log.getStackTraceString(e));
            e.printStackTrace();
        }
    }

    @Override
    public void onCreate(){
        String appClassName = null;
        try{
            //获取am文件中的meta标签,通过检测是否包含```APPLICATION_CLASS_NAME```字符来判断是否获取源apk的Application对象
            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");
            }else{
                Log.i("demo","have no application class name");
                return;
            }
        }catch(NameNotFoundException e){
            Log.i("demo","error:" + Log.getStackTraceString(e));
            e.printStackTrace();
        }
        //调用该Application
        Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",
                new Class[]{},new Object[]{});
        Object mBoundApplication = RefInvoke.getFieldObject("android.app.ActivityThread",currentActivityThread,"mBoundApplication");
        Object loadedApkInfo = RefInvoke.getFieldObject("android.app.ActivityThread$AppBindData",mBoundApplication,"info");
        //把当前进程的Application设置为null
        RefInvoke.setFieldObject("android.app.LoadedApk","mApplication",loadedApkInfo,null);
        Object oldApplication = RefInvoke.getFieldObject("android.app.ActivityThread",currentActivityThread,"mInitialApplication");
        ArrayList<Application> mAllApplications = (ArrayList<Application>)RefInvoke.getFieldObject("android.app.ActivityThread",
                currentActivityThread,"mAllApplications");
        mAllApplications.remove(oldApplication);

        ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo)RefInvoke.getFieldObject("android.app.LoadedApk",loadedApkInfo,"mApplicationInfo");
        ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo)RefInvoke.getFieldObject("android.app.ActivityThread$AppBindData",
                mBoundApplication,"appInfo");
        //将源apk的application对象名赋给LoadedApk
        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.setFieldObject("android.app.ActivityThread","mInitialApplication",currentActivityThread,app);
        ArrayMap mProviderMap = (ArrayMap)RefInvoke.getFieldObject("android.app.ActivityThread",currentActivityThread,"mProviderMap");
        Iterator it = mProviderMap.values().iterator();
        while(it.hasNext()){
            Object providerClientRecord = it.next();
            Object localProvider = RefInvoke.getFieldObject("android.app.ActivityThread$ProviderClientRecord",
                    providerClientRecord,"mLocalProvider");
            RefInvoke.setFieldObject("android.content.ContentProvider","mContext",localProvider,app);
        }
        Log.i("demo","app:" + app);
        app.onCreate();
    }

    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 void splitPayLoadFromDex(byte[] apkdata)throws IOException{
        int ablen = apkdata.length;
        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];
        System.arraycopy(apkdata,ablen - 4 - readInt,newdex,0,readInt);

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

    private byte[] decrypt(byte[] srcdata){
        for(int i = 0;i < srcdata.length;i++){
            srcdata[i] = (byte)(0xFF ^srcdata[i]);
        }
        return srcdata;
    }
}

RefInvoke.java,反射类,主要通过java.lang.reflect.Methodinvoke方法定义了几类调用和赋值方放。

package com.smm.reforceapk;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class RefInvoke {
    public static Object invokeStaticMethod(String class_name,String method_name,Class[] pareTyple,Object[] pareVaules){
        try{
            Class obj_class = Class.forName(class_name);
            Method method = obj_class.getMethod(method_name,pareTyple);
            return method.invoke(null,pareVaules);
        }catch(SecurityException e){
            e.printStackTrace();
        }catch(IllegalArgumentException e){
            e.printStackTrace();
        }catch(IllegalAccessException e){
            e.printStackTrace();
        }catch(NoSuchMethodException e){
            e.printStackTrace();
        }catch(InvocationTargetException e){
            e.printStackTrace();
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
        return null;
    }

    public static Object invokeMethod(String class_name,String method_name,Object obj,Class[] pareTyple,Object[] pareVaules){
        try{
            Class obj_class = Class.forName(class_name);
            Method method = obj_class.getMethod(method_name,pareTyple);
            return method.invoke(obj,pareVaules);
        }catch(SecurityException e){
            e.printStackTrace();
        }catch(IllegalArgumentException e){
            e.printStackTrace();
        }catch(IllegalAccessException e){
            e.printStackTrace();
        }catch(NoSuchMethodException e){
            e.printStackTrace();
        }catch(InvocationTargetException e){
            e.printStackTrace();
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
        return null;
    }

    public static Object getFieldObject(String class_name,Object obj,String fieldName){
        try{
            Class obj_class = Class.forName(class_name);
            Field field = obj_class.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(obj);
        }catch(SecurityException e){
            e.printStackTrace();
        }catch(IllegalArgumentException e){
            e.printStackTrace();
        }catch(NoSuchFieldException e){
            e.printStackTrace();
        }catch(IllegalAccessException e){
            e.printStackTrace();
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
        return null;
    }

    public static Object getStaticFileObject(String class_name,String fieldName){
        try{
            Class obj_class = Class.forName(class_name);
            Field field = obj_class.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(null);
        }catch(SecurityException e){
            e.printStackTrace();
        }catch(NoSuchFieldException e){
            e.printStackTrace();
        }catch(IllegalArgumentException e){
            e.printStackTrace();
        }catch(IllegalAccessException e){
            e.printStackTrace();
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
        return null;
    }

    public static void setFieldObject(String classname,String fieldName,Object obj,Object fieldVaule){
        try{
            Class obj_class = Class.forName(classname);
            Field field = obj_class.getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(obj,fieldVaule);
        }catch(SecurityException e){
            e.printStackTrace();
        }catch(NoSuchFieldException e){
            e.printStackTrace();
        }catch(IllegalArgumentException e){
            e.printStackTrace();
        }catch(IllegalAccessException e){
            e.printStackTrace();
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
    }

    public static void setStaticObject(String class_name,String fieldName,Object fieldVaule){
        try{
            Class obj_class = Class.forName(class_name);
            Field field = obj_class.getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(null,fieldVaule);
        }catch(SecurityException e){
            e.printStackTrace();
        }catch(NoSuchFieldException e){
            e.printStackTrace();
        }catch(IllegalArgumentException e){
            e.printStackTrace();
        }catch(IllegalAccessException e){
            e.printStackTrace();
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}

然后通过buildAPK编译应用,注意此时编译出的apk是不能运行的。

三,加壳程序

加壳程序是个java工程,原理很简单如下:

package shellApk;
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 DexShellTool {
    public static void main(String[] args) {
        try {
            File payloadSrcFile = new File("D:\\shellProject\\p1\\force.apk");
            System.out.println("apk size:" + payloadSrcFile.length());
            File unShellDexFile = new File("D:\\shellProject\\p1\\reforce.dex");
            byte[] payloadArray = encrypt(readFileBytes(payloadSrcFile));
            byte[] unShellDexArray = readFileBytes(unShellDexFile);
            int payloadLen = payloadArray.length;
            System.out.println("payloadLen:" + payloadLen);
            int unShellDexLen = unShellDexArray.length;
            System.out.println("unShellLen:" + unShellDexLen);
            int totalLen = payloadLen + unShellDexLen + 4;
            byte[] newdex = new byte[totalLen];
            
            System.arraycopy(unShellDexArray,0,newdex,0,unShellDexLen);
            System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);
            System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen - 4, 4);
            
            fixFileSizeHeader(newdex);
            fixFileSHA1Header(newdex);
            fixCheckSumHeader(newdex);
            
            String str = "D:\\shellProject\\p1\\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 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);
    }
    
    private static void fixFileSHA1Header(byte[] dexBytes)throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        md.update(dexBytes, 32, dexBytes.length - 32);
        byte[] newdt = md.digest();
        System.arraycopy(newdt, 0, dexBytes, 12, 20);
        String hexstr = "";
        for(int i = 0;i < newdt.length;i++) {
            hexstr += Integer.toString((newdt[i] & 0xff) + 0x100,16).substring(1);
        }
        System.out.println(hexstr);
        
    }
    
    private static void fixCheckSumHeader(byte[] dexBytes) {
        Adler32 adler = new Adler32();
        adler.update(dexBytes,12,dexBytes.length - 12);
        long value = adler.getValue();
        int va = (int)value;
        byte[] newcs = intToByte(va);
        System.out.println("newcs Len:" + newcs.length);
        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);
        System.out.println(Long.toHexString(value));
        System.out.println();
        
    }
        
    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;
    }
    
    private static byte[] encrypt(byte[] srcdata) {
        for(int i = 0;i < srcdata.length;i++) {
            srcdata[i] = (byte)(0xFF ^ srcdata[i]);
        }
        return srcdata;
    }
    
    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();
            }
        }
    }
}

其中的force.apk是源apk,reforce.dex是脱壳apk的dex文件,运行后生成一个新的classes.dex,用新得到的dex替换掉reforce.apk的dex文件,然后重签名即可。
最后运行界面可见就是源apk的运行界面;


最后运行界面

Tips:
过程中遇到的几个坑:
1,7-zip对reforce.apk解压失败,提示文件格式错误,改用winRAR后正常,原因暂时未知。
2,应用按流程从reforce.apk中的ProxyApplication中运行到源apk中的MyApplication中后又返回到的reforce.apk中的MainActivity,原因是脱壳程序不能有MainActivity,删除后恢复。
3,自签名和用Androidkiller签名的apk往4.4的手机上安装时出现错误Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES]Failure [INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION],原因是重打包时要删掉原来的cert文件,删掉后再用自签名工具签名即可。

参考https://github.com/l123456789jy/Lazy/blob/master/lazylibrary/src/main/java/com/github/lazylibrary/util/AntiEmulatorUtiles.java
https://blog.csdn.net/androidsecurity/article/details/8809542
http://www.520monkey.com/archives/553

上一篇下一篇

猜你喜欢

热点阅读