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 内容隐藏
成功