apk加壳去壳

  • Post author:
  • Post category:其他




1.1原理



(1)基础原理

在这里插入图片描述

在整个加固过程中,我们会涉及到四个文件(其实最终要实现加固,需要涉

及的文件不止四个,这里我们仅包含了主要文件):

  • originalAPK.apk:APK 文件,也就是我们要加固的 APP
  • shellDex.dex:dex 文件,该文件的程序功能是将 originalAPK.apk 的密文

    进行解密,并将 originalAPK.apk 中的 dex 文件进行加载,启动被加固的 APP。

    该 dex 文件来自于一个 APK 文件,我们称为 shellAPK.apk。注意,在

    shellAPK.apk 中,shellDex.dex 文件的名字为 classes.dex。这里,我们为了区

    别,将其改名为 shellDex.dex。
  • addShell.java:java 程序,用来加密 originalAPK.apk,并将

    originalAPK.apk 密文与 shellDex.dex 文件合并,生成 classes.dex 文件,并将该

    文件加入到 shellAPK.apk 中替换原来的 classes.dex 文件。
  • classes.dex:dex 文件,最终到用户手里的 shellAPK.apk 中的 dex 文件,

    该文件本质上包含 shellAPK.apk 中原 classes.dex 文件和 originalAPK.apk 的密

    文。


一般加固流程


1.生成 originalAPK.apk

2.编写 addShell.java 程序(注:虽然这里没有所要操作的 shellDex.dex 文件,

但其文件格式都已知晓,不会影响程序的实现)

3.编写 shellAPK.apk,并从中提取 classes.dex 文件改名为 shellDex.dex(注:

虽然这里 originalAPK.apk 都没有被加密,也没有与 shellDex.dex 合并,但解密

算法和文件格式都已知晓,不会影响程序的实现)

4.运行 addShell.java 程序,生成 classes.dex

5.修改相关文件,如 Manifest 文件,将修改后的文件和第四步生成的

classes.dex 文件替换 shellAPK.apk 中相应的文件。最终获得的 shellAPK.apk 就

是我们加固后的 originalAPK.apk。



(2)DEX 头内容

在这里插入图片描述

需要关注的字段:

  • checksum 文件校验码 ,使用 alder32 算法校验文件除去 maigc ,

    checksum 外余下的所有文件区域 ,用于检查文件错误 。
  • signature 使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余

    下的所有文件区域 ,用于唯一识别本文件 。

    ·- ileSize Dex 文件的大小 。

    在文件的最后,我们需要标注被加密的 apk 的大小,因此需要增加 4 个字

    节。

简单来说,我们将数据拼接后,还需要修改这三个字段,以保证文件正常运

行。最后,这个新 DEX 的结构大概如下

在这里插入图片描述

因此拼接步骤如下:

• 获取脱壳 DEX (二进制数据)

• 计算新 DEX 的大小(脱壳 DEX 的大小 + 加密 APK 的大小 + 4)

• 依次拼接脱壳 DEX、加密 APK、加密 APK 的大小,得到新 DEX

• 修改新 DEX 头的三个字段

• 输出新 DEX



1.2操作过程



(1)源apk

首先新建一个HelloWorld

MainAcitivity

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("helloPack", "I am coming");
    }
}



(2)加密和拼接工具(函数)

使用 Android Studio 的 Java

Moudle 来编辑 Java 函数。

再明确一下功能:对源 APK 进行加密、拼接到脱壳 DEX,得到新 DEX。



1、新建 Java Moudle

新建工程,默认模板即可,然后右键 app,New -> Moudle -> Java Library:

在这里插入图片描述

此时,会生成一个 lib 文件夹和一个默认的 MyClass.java,我们可以在里面编辑 Java 代码。

在这里插入图片描述

这里测试一下:

public class MyClass {
 public static void main(String[] args) {
 System.out.println("hello Java");
 }
}

右键 MyClass,或者点击对应文件旁边的按钮,选择 Run”MyClass.main()”:

在这里插入图片描述



2、 加密和拼接

package com.example.lib;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.Adler32;

public class MyClass {
    public static void main(String[] args){
        try {
            // 获取源APK(项目根目录下)
            File srcApkFile = new File("force/orignal.apk");
            System.out.println("apk path: " + srcApkFile.getAbsolutePath());
            System.out.println("apk size: " + srcApkFile.length());
            // 加密并返回数据
            byte[] enSrcApkArray = encrypt(readFileBytes(srcApkFile));
            // 脱壳DEX, 以二进制形式读出DEX
            File unShellDexFile = new File("force/classes.dex");
            System.out.println("unShellDexFile path: " + unShellDexFile.getAbsolutePath());
            System.out.println("unShellDexFile size: " + unShellDexFile.length());
            byte[] unShellDexArray = readFileBytes(unShellDexFile);

            // 将脱壳DEX长度和加密APK长度相加并加上存放加密APK大小的四位得到总长度
            int unShellDexLen = unShellDexArray.length;
            int enSrcApkLen = enSrcApkArray.length;
            // 多出的四位存放加密APK长度
            int totalLen = unShellDexLen + enSrcApkLen + 4;

            // 依次将脱壳DEX,加密APK,加密APK大小,拼接出新的DEX
            byte[] newDex = new byte[totalLen];
            // 复制加壳数据
            System.arraycopy(unShellDexArray, 0, newDex, 0, unShellDexLen);
            // 复制加密APK数据
            System.arraycopy(enSrcApkArray, 0, newDex, unShellDexLen, enSrcApkLen);
            // 赋值加密APK大小
            System.arraycopy(intToByte(enSrcApkLen), 0, newDex, totalLen - 4, 4);

            // 修改DEX file size 文件头
            fixFileSizeHeader(newDex);
            // 修改DEX SHA1 文件头
            fixSHA1Header(newDex);
            // 修改DEX CheckNum文件头
            fixCheckSumHeader(newDex);

            // 新DEX文件名
            String str = "force/classesShell.dex";
            File file = new File(str);
            if (!file.exists()) {
                if (!file.createNewFile()) {
                    System.out.println("File create error");
                    return;
                }
            }
            // 导出文件
            FileOutputStream fos = new FileOutputStream(str);
            fos.write(newDex);
            fos.flush();
            fos.close();
        } catch (IOException | NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }

    /**
     * 修改DEX头,checkSum校验码
     * @param dexBytes 要修改的二进制数据
     */
    private static void fixCheckSumHeader(byte[] dexBytes) {
        Adler32 adler = new Adler32();
        // 从12到文件末尾计算校验码
        adler.update(dexBytes, 12, dexBytes.length - 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("fixCheckSumHeader:" + Integer.toHexString(newCs[i]));
        }
        // 校验码赋值(8-11)
        System.arraycopy(reCs, 0, dexBytes, 8, 4);
        System.out.println("fixCheckSumHeader:" + Long.toHexString(value));
    }

    /**
     * 修改DEX头, sha1值
     * @param dexBytes 要修改的二进制数组
     */
    private static void fixSHA1Header(byte[] dexBytes) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        // 从32位到结束计算sha-1
        md.update(dexBytes, 32, dexBytes.length - 32);
        byte[] newDt = md.digest();
        // 修改sha-1值(12-21)
        System.arraycopy(newDt, 0, dexBytes, 12, 20);
        // 输出sha-1值
        StringBuilder hexStr = new StringBuilder();
        for (byte aNewDt : newDt) {
            hexStr.append(Integer.toString((aNewDt & 0xFF) + 0x100, 16).substring(1));
        }
        System.out.println("fixSHA1Header:" + hexStr.toString());
    }

    /**
     * 修改DEX头, file_size值
     * @param dexBytes 二进制数据
     */
    private static void fixFileSizeHeader(byte[] dexBytes) {
        // 新文件长度
        byte[] newFs = intToByte(dexBytes.length);
        System.out.println("fixFileSizeHeader: " + 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("fixFileSizeHeader: " + Integer.toHexString(newFs[i]));
        }
        // 修改32-35
        System.arraycopy(reFs, 0, dexBytes, 32, 4);
    }

    /**
     * int 转 byte[]
     * @param number 整型
     * @return 返回字节数组
     */
    private 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;
    }



    /**
     * 读出文件的二进制数据
     * @param file 文件
     * @return 二进制数据
     */
    private static byte[] readFileBytes(File file) throws IOException {
        byte[] bytes = new byte[1024];
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        FileInputStream fis = new FileInputStream(file);
        while (true) {
            int len = fis.read(bytes);
            if (-1 == len) break;
            baos.write(bytes, 0, len);
        }
        byte[] byteArray = baos.toByteArray();
        fis.close();
        baos.close();
        return byteArray;
    }

    /**
     * 加密二进制数据
     * @param srcData 源数据
     * @return 加密后的数据
     */
    private static byte[] encrypt(byte[] srcData) {
        for (int i = 0; i < srcData.length; i++) {
            srcData[i] ^= 0xFF;
        }
        return srcData;
    }
}

项目根目录下新建文件夹 force,将源 APK 放进其中



( 3 )脱壳 DEX

编辑脱壳 APK 的解密部分和内容替换部分,取出其中的 DEX 文件,即脱壳DEX

在这里插入图片描述



( 1 )自定义 Application

首先,自定义 Application,在 AndroidManifest.xml 指定名称为

ProxyApplication,利用 Android Studio 可以自动生成

ProxyApplication.java 文件。同时,将源 APK 的 Activity 注册进来,

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

    <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">
        <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>



( 2 )attachBaseContext 方法实现

package com.exampie.myapplication;

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

public class RefInvoke {
    /**
     * 反射执行类的静态函数(public)
     *
     * @param className  类名
     * @param methodName 方法名
     * @param pareTypes  函数的参数类型
     * @param pareValues 调用函数时传入的参数
     * @return
     */
    public static Object invokeStaticMethod(String className, String methodName, Class[] pareTypes, Object[] pareValues) {
        try {
            Class objClass = Class.forName(className);
            Method method = objClass.getMethod(methodName, pareTypes);
            return method.invoke(null, pareValues);
        } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 反射执行的函数(public)
     *
     * @param className  类名
     * @param methodName 方法名
     * @param obj        对象
     * @param pareTypes  参数类型
     * @param pareValues 调用方法传入的参数
     * @return
     */
    public static Object invokeMethod(String className, String methodName, Object obj, Class[] pareTypes, Object[] pareValues) {
        try {
            Class objClass = Class.forName(className);
            Method method = objClass.getMethod(methodName, pareTypes);
            return method.invoke(obj, pareValues);
        } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 反射得到类的属性(包括私有和保护)
     *
     * @param className 类名
     * @param obj       对象
     * @param fieldName 属性名
     * @return
     */
    public static Object getFieldObject(String className, Object obj, String fieldName) {
        try {
            Class objClass = Class.forName(className);
            Field field = objClass.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(obj);
        } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 反射得到类的静态属性(包括私有和保护)
     *
     * @param className 类名
     * @param fieldName 属性名
     * @return
     */
    public static Object getStaticFieldObject(String className, String fieldName) {
        try {
            Class objClass = Class.forName(className);
            Field field = objClass.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(null);
        } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 设置类的属性(包括私有和保护)
     *
     * @param className  类名
     * @param fieldName  属性名
     * @param obj        对象
     * @param fieldValue 字段值
     */
    public static void setFieldObject(String className, String fieldName, Object obj, Object fieldValue) {
        try {
            Class objClass = Class.forName(className);
            Field field = objClass.getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(obj, fieldValue);
        } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * 设置类的静态属性(包括私有和保护)
     *
     * @param className  类名
     * @param fieldName  属性名
     * @param fieldValue 属性值
     */
    public static void setStaticObject(String className, String fieldName, String fieldValue) {
        try {
            Class objClass = Class.forName(className);
            Field field = objClass.getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(null, fieldValue);
        } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}



( 3 )onCreate 方法实现

在 ProxyApplication.java :

package com.exampie.myapplication;

import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;


import androidx.annotation.RequiresApi;

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

import dalvik.system.DexClassLoader;

public class ProxyApplication extends Application {
    private static final String TAG = ProxyApplication.class.getSimpleName();
    /**
     * APP_KEY获取Activity入口
     */
    private static final String APP_KEY = "APPLICATION_CLASS_NAME";
    /**ActivityThread包名*/
    private static final String CLASS_NAME_ACTIVITY_THREAD = "android.app.ActivityThread";
    /**LoadedApk包名*/
    private static final String CLASS_NAME_LOADED_APK = "android.app.LoadedApk";
    /**
     * 源Apk路径
     */
    private String mSrcApkFilePath;
    /**
     * odex路径
     */
    private String mOdexPath;
    /**
     * lib路径
     */
    private String mLibPath;

    /**
     * 最先执行的方法
     */
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Log.d(TAG, "attachBaseContext: --------onCreate");

        try {
            // 创建payload_odex和payload_lib文件夹,payload_odex中放置源apk即源dex,payload_lib放置so文件
            File odex = this.getDir("payload_odex", MODE_PRIVATE);
            File libs = this.getDir("payload_lib", MODE_PRIVATE);
            // 用于存放源apk释放出来的dex
            mOdexPath = odex.getAbsolutePath();
            // 用于存放源apk用到的so文件
            mLibPath = libs.getAbsolutePath();
            // 用于存放解密后的apk即源apk
            mSrcApkFilePath = mOdexPath + "/payload.apk";

            File srcApkFile = new File(mSrcApkFilePath);
            Log.d(TAG, "attachBaseContext: apk size: " + srcApkFile.length());

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

            // 配置动态加载环境
            // 反射获取主线程对象,并从中获取所有已加载的package信息,找到当前LoadApk的弱引用
            // 获取主线程对象
            Object currentActivityThread = RefInvoke.invokeStaticMethod(
                    CLASS_NAME_ACTIVITY_THREAD, "currentActivityThread",
                    new Class[]{}, new Object[]{}
            );
            // 获取当前报名
            String packageName = this.getPackageName();
            // 获取已加载的所有包
            ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldObject(
                    CLASS_NAME_ACTIVITY_THREAD, currentActivityThread,
                    "mPackages"
            );
            // 获取LoadApk的弱引用
            WeakReference wr = (WeakReference) mPackages.get(packageName);
            Log.d(TAG,"获取LoadApk的弱引用");
            // 创建一个新的DexClassLoader用于加载源Apk
            // 传入apk路径,dex释放路径,so路径,及父节点的DexClassLoader使其遵循双亲委托模型
            // 反射获取属性ClassLoader
            Object mClassLoader = RefInvoke.getFieldObject(
                    CLASS_NAME_LOADED_APK, wr.get(), "mClassLoader"
            );
            // 定义新的DexClassLoader对象,指定apk路径,odex路径,lib路径
            DexClassLoader dLoader = new DexClassLoader(
                    mSrcApkFilePath, mOdexPath, mLibPath, (ClassLoader) mClassLoader
            );
            // getClassLoader()等同于 (ClassLoader) RefInvoke.getFieldOjbect()
            // 但是为了替换掉父节点我们需要通过反射来获取并修改其值
            Log.d(TAG, "attachBaseContext: 父ClassLoader: " + mClassLoader);

            // 将父节点DexClassLoader替换
            RefInvoke.setFieldObject(
                    CLASS_NAME_LOADED_APK,
                    "mClassLoader",
                    wr.get(),
                    dLoader
            );

            Log.d(TAG, "attachBaseContext: 子ClassLoader: " + dLoader);

            // 测试
            try {
                // 尝试加载源apk的MainActivity
                Object actObj = dLoader.loadClass("com.example.apkreinforcement.MainActivity");
                Log.d(TAG, "attachBaseContext: SrcApk_MainActivity: " + actObj);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
                Log.d(TAG, "attachBaseContext: LoadSrcActivityErr: " + Log.getStackTraceString(e));
            }

        } catch (IOException e) {
            e.printStackTrace();
            Log.d(TAG, "attachBaseContext: error: " + Log.getStackTraceString(e));
        }

    }

    /**
     * 从DEX中分割出资源
     * @param dexData dex资源
     */
    private void splitPayLoadFromDex(byte[] dexData) throws IOException {
        // 获取dex数据长度
        int len = dexData.length;
        // 存储被加壳apk的长度
        byte[] dexLen = new byte[4];
        // 获取最后4个字节数据
        System.arraycopy(dexData, len - 4, dexLen, 0, 4);
        ByteArrayInputStream bais = new ByteArrayInputStream(dexLen);
        DataInputStream in = new DataInputStream(bais);
        // 获取被加密apk的长度
        int readInt = in.readInt();
        // 打印被加密apk的长度
        Log.d(TAG, "splitPayLoadFromDex: Integer.toHexString(readInt): " + Integer.toHexString(readInt));

        // 取出apk
        byte[] enSrcApk = new byte[readInt];
        // 将被加密apk内容复制到二进制数组中
        System.arraycopy(dexData, len - 4 - readInt, enSrcApk, 0, readInt);

        // 对源apk解密
        byte[] srcApk = decrypt(enSrcApk);
        Log.d(TAG,"原APK解密成功");

        // 写入源apk文件
        File file = new File(mSrcApkFilePath);
        try {
            if(file.createNewFile()){
                FileOutputStream fos = new FileOutputStream(file);
                fos.write(srcApk);
                fos.close();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // 分析源apk文件
        ZipInputStream zis = new ZipInputStream(
                new BufferedInputStream(
                        new FileInputStream(file)
                )
        );

        // 遍历压缩包
        while (true) {
            ZipEntry entry = zis.getNextEntry();
            // 判断是否有内容
            if (null == entry) {
                zis.close();
                break;
            }

            // 依次取出被加壳的apk用到的so文件,放到libPath中(data/data/包名/paytload_lib)
            String name = entry.getName();
            if (name.startsWith("lib/") && name.endsWith(".so")) {
                // 存储文件
                File storeFile = new File(
                        mLibPath + "/" + name.substring(name.lastIndexOf('/'))
                );
                storeFile.createNewFile();
                FileOutputStream fos = new FileOutputStream(storeFile);
                byte[] bytes = new byte[1024];
                while (true) {
                    int length = zis.read(bytes);
                    if (-1 == length) break;
                    fos.write(bytes);
                }
                fos.flush();
                fos.close();
            }
            zis.closeEntry();
        }
        zis.close();
    }

    /**
     * 解密二进制
     * @param srcData 二进制数
     * @return 解密后的二进制数据
     */
    private byte[] decrypt(byte[] srcData) {
        for (int i = 0; i < srcData.length; i++) {
            srcData[i] ^= 0xFF;
        }
        return srcData;
    }

    /**
     * 从新APK中获取加密APK的DEX文件
     * @return dex字节数组
     */
    private byte[] readDexFileFromApk() throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ZipInputStream zis = new ZipInputStream(
                new BufferedInputStream(
                        new FileInputStream(this.getApplicationInfo().sourceDir)
                )
        );
        // 遍历压缩包
        while (true) {
            ZipEntry entry = zis.getNextEntry();
            if (null == entry) {
                zis.close();
                break;
            }
            // 获取dex文件
            if ("classes.dex".equals(entry.getName())) {
                byte[] bytes = new byte[1024];
                while (true) {
                    int len = zis.read(bytes);
                    if (len == -1) break;
                    baos.write(bytes, 0, len);
                }
                Log.d(TAG,"classes.dex读取成功");
            }
            zis.closeEntry();
        }
        zis.close();
        return baos.toByteArray();
    }

    // 后续实现
    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate: ---------------");

        // 获取配置在清单文件的源apk的Application路径
        String appClassName = null;
        try {
            // 创建应用信息对象
            ApplicationInfo ai = this.getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA);
            // 获取metaData数据
            Bundle bundle = ai.metaData;
            if (null != bundle && bundle.containsKey(APP_KEY)) {
                appClassName = bundle.getString(APP_KEY);
            } else {
                Log.d(TAG, "onCreate: have no application class name");
                return;
            }
        } catch (PackageManager.NameNotFoundException e) {
            Log.d(TAG, "onCreate: error: " + Log.getStackTraceString(e));
            e.printStackTrace();
        }

        // 获取当前Activity线程
        Object currentActivityThread = RefInvoke.invokeStaticMethod(CLASS_NAME_ACTIVITY_THREAD,
                "currentActivityThread", new Class[]{}, new Object[]{});
        // 获取绑定的应用
        Object mBoundApplication = RefInvoke.getFieldObject(CLASS_NAME_ACTIVITY_THREAD,
                currentActivityThread, "mBoundApplication");
        // 获取加载apk的信息
        Object loadedApkInfo = RefInvoke.getFieldObject(
                CLASS_NAME_ACTIVITY_THREAD + "$AppBindData",
                mBoundApplication, "info"
        );
        // 将LoadedApk中的ApplicationInfo设置为null
        RefInvoke.setFieldObject(CLASS_NAME_LOADED_APK, "mApplication", loadedApkInfo, null);
        // 获取currentActivityThread中注册的Application
        Object oldApplication = RefInvoke.getFieldObject(
                CLASS_NAME_ACTIVITY_THREAD, currentActivityThread, "mInitialApplication"
        );
        // 获取ActivityThread中所有已注册的Application, 并将当前壳Apk的Application从中移除
        ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke.getFieldObject(
                CLASS_NAME_ACTIVITY_THREAD, currentActivityThread, "mAllApplications"
        );
        mAllApplications.remove(oldApplication);
        // 从loadedApk中获取应用信息
        ApplicationInfo appInfoInLoadedApk = (ApplicationInfo) RefInvoke.getFieldObject(
                CLASS_NAME_LOADED_APK, loadedApkInfo, "mApplicationInfo"
        );
        // 从AppBindData中获取应用信息
        ApplicationInfo appInfoInAppBindData = (ApplicationInfo) RefInvoke.getFieldObject(
                CLASS_NAME_ACTIVITY_THREAD + "$AppBindData", mBoundApplication, "appInfo"
        );
        // 替换原来的Application
        appInfoInLoadedApk.className = appClassName;
        appInfoInAppBindData.className = appClassName;

        // 注册Application
        Application app = (Application) RefInvoke.invokeMethod(
                CLASS_NAME_LOADED_APK, "makeApplication", loadedApkInfo,
                new Class[]{boolean.class, Instrumentation.class},
                new Object[]{false, null}
        );
        // 替换ActivityThread中的Application
        RefInvoke.setFieldObject(CLASS_NAME_ACTIVITY_THREAD, "mInitialApplication",
                currentActivityThread, app);
        ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldObject(
                CLASS_NAME_ACTIVITY_THREAD, currentActivityThread, "mProviderMap"
        );

        // 遍历
        for (Object providerClientRecord : mProviderMap.values()) {
            Object localProvider = RefInvoke.getFieldObject(
                    CLASS_NAME_ACTIVITY_THREAD + "$ProviderClientRecord",
                    providerClientRecord, "mLocalProvider"
            );
            RefInvoke.setFieldObject("android.content.ContentProvider", "mContext",
                    localProvider, app);
        }

        Log.d(TAG, "onCreate: SrcApp: " + app);
        // 调用新的Application
        app.onCreate();
    }

}

至此脱壳目录如下

在这里插入图片描述



( 4 )新 DEX

使用加密和拼接工具(函数)对源 APK 进行加密、拼接到脱壳 DEX,得到新DEX

这一步开始,实际就是测试部分。

1、打包源 APK,这里是 helloWorld.apk。

在这里插入图片描述

2、打包脱壳 APK,这里是 默认.apk,使用解压软件打开,将其中的 classes.dex,即脱壳 DEX,拷贝出来:

在这里插入图片描述

在这里插入图片描述

运行加密和拼接函数,即 MyClass.main(),此时会生成 classesShell.dex:

在这里插入图片描述



( 5 )新 APK

对新 DEX 打包,生成新 APK

使用新 DEX 替换掉apk 里的 classes.dex

在这里插入图片描述

为 apk 被修改,所以要对其重新签名。同样打开新 APK,将其 METAIMF 文件夹删除,然后使用 jarsigner 重新签名:

jarsigner -verbose -keystore IS.keystore xx.apk IS1601

弹出日志

在这里插入图片描述

可见已经加载出了源 APK 内容。

在这里插入图片描述

该源 APK 可以直接安装运行,前提是给予相关权限。

再观察一下 AndroidKiller 逆向情况,并无 MainActivity,源 APK 内容隐藏

成功

在这里插入图片描述



版权声明:本文为Cloud_Player原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。