Android DEX加壳

1. APP加固

1). 原理
图1.png

加密过程的三个对象:

  • 1、需要加密的Apk(源Apk)
  • 2、壳程序Apk(负责解密Apk工作)
  • 3、加密工具(将源Apk进行加密和壳Dex合并成新的Dex)
2). DEX头内容
图2.png

需要关注的字段:

  • checksum 文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误 。
  • signature 使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。
  • fileSize Dex 文件的大小 。
  • 在文件的最后,我们需要标注被加密的apk的大小,因此需要增加4个字节。


    图3.png
3). 解密过程

宿主Apk启动 -> 宿主Application中解密Apk -> 替换ClassLoader -> 替换资源路径 -> 替换Application对象

2. 源程序Module(source)

1). SourceApplication
/**
 * 源Apk的全局Application
 * Created by mazaiting on 2018/6/26.
 */

public class SourceApplication extends Application {
  private static final String TAG = SourceApplication.class.getSimpleName();
  @Override
  public void onCreate() {
    super.onCreate();
    Log.d(TAG, "onCreate: --------");
  }
}
2). MainActivity
/**
 * 应用主入口
 */
public class MainActivity extends AppCompatActivity {
  private static final String TAG = MainActivity.class.getSimpleName();
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    TextView tvContent = new TextView(this);
    tvContent.setText("I am Source Apk");
    tvContent.setOnClickListener(new View.OnClickListener(){
      @Override
      public void onClick(View arg0) {
        Intent intent = new Intent(MainActivity.this, SecondActivity.class);
        startActivity(intent);
      }});
    setContentView(tvContent);
    Log.i(TAG, "onCreate:app:"+getApplicationContext());
  }
}
3). 第二个页面
/**
 * 第二个页面
 */
public class SecondActivity extends AppCompatActivity {
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    TextView tv_content = new TextView(this);
    tv_content.setText("I am Second Activity");
    setContentView(tv_content);
  }
}
4). AndroidManifest.xml文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.mazaiting.reinforcement">

  <application
      android:name=".SourceApplication"
      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>
    <activity android:name=".SecondActivity">
    </activity>
  </application>

</manifest>
5). 签名

Build -> Generate Signed APK,对应用进行签名(如果没有keystore, 可以创建一个新的),将签名后的apk文件放置在项目根目录下的force文件夹下,并更名为source.apk


图3.png

图4.png

3. 脱壳Module(reforceapk)

1). 替换步骤
 * 代理Application
 * 步骤:
 *    ------- 在attachBaseContext -------
 *    1. 从当前APK中拿到classes.dex文件,拿到classes.dex文件的二进制数据
 *    2. 从dex的二进制数据中分离出解密后的apk,及so文件
 *    3. 反射获取主线程对象,并从中获取所有已加载的package信息,找到当前LoadApk的弱引用
 *    4. 创建一个新的DexClassLoader,从指定路径加载apk资源
 *    5. 加载被加密的apk主Activity入口
 *    -------  在onCreate方法   ----------
 *    6. 获取配置在清单文件的源apk的Application
 *    7. 替换原有的Application
 *    8. 调用被加密app的Application
2). 代码
/**
 * 代理Application
 * 步骤:
 *    ------- 在attachBaseContext -------
 *    1. 从当前APK中拿到classes.dex文件,拿到classes.dex文件的二进制数据
 *    2. 从dex的二进制数据中分离出解密后的apk,及so文件
 *    3. 反射获取主线程对象,并从中获取所有已加载的package信息,找到当前LoadApk的弱引用
 *    4. 创建一个新的DexClassLoader,从指定路径加载apk资源
 *    5. 加载被加密的apk主Activity入口
 *    -------  在onCreate方法   ----------
 *    6. 获取配置在清单文件的源apk的Application
 *    7. 替换原有的Application
 *    8. 调用被加密app的Application
 * Created by mazaiting on 2018/6/26.
 */

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;
  /**
   * 加载资源
   */
  protected AssetManager mAssetManager;
  protected Resources mResources;
  protected Resources.Theme mTheme;
  
  /**
   * 最先执行的方法
   */
  @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
      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");
        srcApkFile.createNewFile();
        // 拿到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);
      
      // 创建一个新的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.mazaiting.reinforcement.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);
    
    // 写入源APK文件
    File file = new File(mSrcApkFilePath);
    try {
      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文件中获取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);
        }
      }
      zis.closeEntry();
    }
    zis.close();
    return baos.toByteArray();
  }
  
  
  @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();
    
  }
3). RefInvoke类
/**
 * 反射类
 * Created by mazaiting on 2018/6/26.
 */

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();
    }
  }
  
}
4). MainActivity
public class MainActivity extends AppCompatActivity {
  
  private static final String TAG=MainActivity.class.getSimpleName();
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Log.d(TAG,"-------------onCreate");
  }
}
5). AndroidManifest.xml文件

在这个文件中,需要配置meta-data结点和将源apk中四大组件进行配置,否则无法运行源apk中的内容

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

  <application
      android:name=".ProxyApplication"
      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">
    <meta-data
        android:name="APPLICATION_CLASS_NAME"
        android:value="com.mazaiting.reinforcement.SourceApplication"/>
    <activity android:name="com.mazaiting.reinforcement.MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

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

</manifest>
6). 打包

与源apk相同,对此Module进行打包,打包完成后,更改文件后缀名apk为zip,使用压缩工具解压,将解压后文件夹中的classes.dex文件复制到项目根目录force文件夹下


图5.png

图6.png

图7.png

4. 加密工程

1). 新建一个Java Module
2). 加密步骤
 * 步骤:
 *    1. 获取待加密的APK, 并对其二进制数据加密
 *    2. 取出壳DEX, 并获取其二进制数据
 *    3. 计算拼接后的DEX应用的大小, 并创建二进制数组
 *    4. 依次将解壳DEX,加密后的源APK,加密后的源APK大小,拼接出新的DEX
 *    5. 修改DEX的头,fileSize字段
 *    6. 修改DEX的头,SHA1字段
 *    7. 修改DEX的头,CheckNum字段
 *    8. 输出新的DEX文件
3). DexShellTool
/**
 * 加密APK
 * 步骤:
 *    1. 获取待加密的APK, 并对其二进制数据加密
 *    2. 取出壳DEX, 并获取其二进制数据
 *    3. 计算拼接后的DEX应用的大小, 并创建二进制数组
 *    4. 依次将解壳DEX,加密后的源APK,加密后的源APK大小,拼接出新的DEX
 *    5. 修改DEX的头,fileSize字段
 *    6. 修改DEX的头,SHA1字段
 *    7. 修改DEX的头,CheckNum字段
 *    8. 输出新的DEX文件
 */
public class DexShellTool {
  public static void main(String[] args) {
    try {
      // 需要加壳的源APK, 以二进制形式读取,并进行加密处理
      File srcApkFile = new File("force/source.apk");
      System.out.println("apk path: " + srcApkFile.getAbsolutePath());
      System.out.println("apk size: " + srcApkFile.length());
      // 加密并返回元apk数据
      byte[] enSrcApkArray = encrypt(readFileBytes(srcApkFile));
      
      // 需要解壳的dex, 以二进制形式读出dex
      File unShellDexFile = new File("force/shell.dex");
      byte[] unShellDexArray = readFileBytes(unShellDexFile);
      
      // 将源APK长度和需要解壳的DEX长度相加并加上存放源APK大小的四位得到总长度
      int enSrcApkLen = enSrcApkArray.length;
      int unShellDexLen = unShellDexArray.length;
      // 多出的四位存放加密后的dex长度
      int totalLen = enSrcApkLen + unShellDexLen + 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);
      // 赋值加壳后的dex大小
      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/classes.dex";
      File file = new File(str);
      if (!file.exists()) {
        file.createNewFile();
      }
      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 srcData 字节数组
   * @return 加密后的二进制数组
   */
  private static byte[] encrypt(byte[] srcData) {
    for (int i = 0; i < srcData.length; i++) {
      srcData[i] ^= 0xFF;
    }
    return srcData;
  }
  
  /**
   * 以二进制读出文件内容
   *
   * @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;
  }
}
4). 运行main函数

在项目的根目录的force文件夹下,生成一个classes.dex文件


图8.png

5. 合并

1). 项目结构
图9.png
2). 收集文件

将force/classes.dex与reforceapk Module生成的apk放在桌面


图10.png
3). 替换

将reforceapk-release.apk直接使用压缩工具打开,将classes.dex复制并替换reforceapk-release.apk中原有的classes.dex文件.


图11.png
4). 重签名
jarsigner -verbose -keystore E:\android\key\release-key.keystore -storepass mazaiting -keypass mazaiting -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar Reforce_des.apk reforceapk-release.apk key-alias
del reforceapk-release.apk

参数说明:

jarsigner -verbose -keystore 签名文件 -storepass 密码  -keypass alias的密码 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA  签名后的文件 签名前的apk alias名称
5). 安装运行
adb install C:\Users\mazaiting\Desktop\Reforce_des.apk

7. 参考文章及代码

8. 残留的问题

  • 源APP中Activity中的界面组件是由代码构建,xml文件中如何加载?
  • 宿主APP中ProxyApplication中使用到了源APP中的入口Application及MainActivity,如何动态获取?
  • 宿主APP中AndroidManifest.xml文件中需要配置源APP的四大组件,如何不进行不配置?

代码下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容