天天看點

Android DEX加殼

1. APP加強

1). 原理

Android DEX加殼

圖1.png

加密過程的三個對象:

  • 1、需要加密的Apk(源Apk)
  • 2、殼程式Apk(負責解密Apk工作)
  • 3、加密工具(将源Apk進行加密和殼Dex合并成新的Dex)

2). DEX頭内容

Android DEX加殼

圖2.png

需要關注的字段:

  • checksum 檔案校驗碼 ,使用alder32 算法校驗檔案除去 maigc ,checksum 外餘下的所有檔案區域 ,用于檢查檔案錯誤 。
  • signature 使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外餘下的所有檔案區域 ,用于唯一識别本檔案 。
  • fileSize Dex 檔案的大小 。
  • 在檔案的最後,我們需要标注被加密的apk的大小,是以需要增加4個位元組。
    Android DEX加殼
    圖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

Android DEX加殼
Android DEX加殼

圖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檔案夾下

Android DEX加殼

圖5.png

Android DEX加殼

圖6.png

Android DEX加殼

圖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檔案

Android DEX加殼

圖8.png

5. 合并

1). 項目結構

Android DEX加殼

圖9.png

2). 收集檔案

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

Android DEX加殼

圖10.png

3). 替換

将reforceapk-release.apk直接使用壓縮工具打開,将classes.dex複制并替換reforceapk-release.apk中原有的classes.dex檔案.

Android 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的四大元件,如何不進行不配置?

代碼下載下傳

繼續閱讀