1. APP加強
1). 原理
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuEGZxETM4cjYyUTOkJTYkN2MzMTNlZTOzUGNzUDO0ITZfdWbp9CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.png)
圖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
Android DEX加殼
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
圖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的四大元件,如何不進行不配置?