Apk原始碼的加固(加殼)原理解析和實現
好久沒寫部落格了,要深刻檢討下!
前言:
在Android中沒有經過加密的Apk給人的感覺就是在裸奔,通過apktool,dex2jar,AndroidKill等各式各樣的反編譯工具就可以輕鬆的獲取其smail程式碼,如這個叫SourceProject的helloworld程式被apktool反編譯後,對於懂smail語法的逆向工程師來說就一覽無餘了。破解與反破解是相對的,所以我們儘可能的給自己的Apk多穿點衣服。
原理解析
首先我們先來看下Apk加殼的步驟:
- 源Apk:需要加殼的Apk
- 加密的Apk:源Apk經過加密演算法加密後的Apk
- 加殼程式Apk:是有解密源Apk和動態載入啟動源Apk的外殼
首先我們拿到需要加殼的源Apk,通過加密演算法加密源Apk然後與加殼Apk的dex檔案組合成新的Dex檔案,然後將加殼程式Apk的Dex檔案替換成新的Dex,生成新的Apk重新簽名。
我們先來看下Dex檔案的結構:
- Magic
Magic數是為了方便虛擬機器識別目標檔案是否是合格的Dex檔案,在Dex檔案中magic的值固定值 - checksum
檔案校驗碼 ,使用alder32 演算法校驗檔案除去 maigc ,checksum 外餘下的所有檔案區域 ,用於檢查檔案錯誤 - signature
使用 SHA-1 演算法 hash 除去 magic ,checksum 和 signature 外餘下的所有檔案區域 ,用於唯一識別本檔案 。 - file_size
當前Dex 檔案的大小 。
所以我們在將Dex與加密演算法加密後的Apk合併生成新的Dex後需要修改新Dex檔案的這三個值,為了方便從新Dex中獲得加密的Apk,我們需要知道加密的Apk的大小,為了方便以後獲得,我們將其大小放置在新Dex的後四位,新生成的Dex檔案結構:
生成新Dex後,將加殼程式Apk的Dex檔案替換,重新簽名後加殼的Apk即完成了。如果覺得步驟理清了,我們來看下其具體的實現。
具體實現:
這過程一共要建立三個專案。
首先我們先建立一個需要加密的Apk專案,結構非常簡單隻有幾個Log的列印,結構
SourceApplication.java
package com.jju.yuxin.sourceproject;
import android.app.Application;
import android.util.Log;
public class SourceApplication extends Application {
private static final String TAG=SourceApplication.class.getSimpleName();
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG,"-------------onCreate");
}
}
MainActivity.java
package com.jju.yuxin.sourceproject;
import android.app.Activity;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends Activity {
private static final String TAG=MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv_content = new TextView(this);
tv_content.setText("I am Source Apk");
tv_content.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View arg0) {
Intent intent = new Intent(MainActivity.this, SubActivity.class);
startActivity(intent);
}});
setContentView(tv_content);
Log.i(TAG, "onCreate:app:"+getApplicationContext());
}
}
SubActivity.java
package com.jju.yuxin.sourceproject;
import android.app.Activity;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
public class SubActivity extends Activity {
private static final String TAG=SubActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv_content = new TextView(this);
tv_content.setText("I am SubActivity");
setContentView(tv_content);
Log.i(TAG, "SubActivity:app:"+getApplicationContext());
}
}
然後將其打包生成Apk。
第二個專案是一個JAVA專案用於將源Apk加密,併合並加殼程式Dex與加密後的源Apk。在貼出這個程式碼時我們先不用管加殼程式Dex從何而來,假設已經有了這樣一個檔案。我們來看下具體實現:
package com.forceapk;
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 mymain {
public static void main(String[] args) {
try {
//需要加殼的源APK ,以二進位制形式讀出,並進行加密處理
File srcApkFile = new File("force/SourceAPK.apk");
System.out.println("apk size:"+srcApkFile.length());
byte[] enSrcApkArray = encrpt(readFileBytes(srcApkFile));
//需要解殼的dex 以二進位制形式讀出dex
File unShellDexFile = new File("force/shelldex.dex");
byte[] unShellDexArray = readFileBytes(unShellDexFile);
//將源APK長度和需要解殼的DEX長度相加並加上存放源APK大小的四位得到總長度
int enSrcApkLen = enSrcApkArray.length;
int unShellDexLen = unShellDexArray.length;
int totalLen = enSrcApkLen + unShellDexLen +4;
//依次將解殼DEX,加密後的源APK,加密後的源APK大小,拼接出新的Dex
byte[] newdex = new byte[totalLen];
System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);
System.arraycopy(enSrcApkArray, 0, newdex, unShellDexLen, enSrcApkLen);
System.arraycopy(intToByte(enSrcApkLen), 0, newdex, totalLen-4, 4);
//修改DEX file size檔案頭
fixFileSizeHeader(newdex);
//修改DEX SHA1 檔案頭
fixSHA1Header(newdex);
//修改DEX CheckSum檔案頭
fixCheckSumHeader(newdex);
//寫出
String str = "force/classes.dex";
File file = new File(str);
if (!file.exists()) {
file.createNewFile();
}
FileOutputStream localFileOutputStream = new FileOutputStream(str);
localFileOutputStream.write(newdex);
localFileOutputStream.flush();
localFileOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
//可以修改成自己的加密方法
private static byte[] encrpt(byte[] srcdata){
for(int i = 0;i<srcdata.length;i++){
srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;
}
/**
* 修改dex頭,CheckSum 校驗碼
* @param dexBytes
*/
private static void fixCheckSumHeader(byte[] dexBytes) {
Adler32 adler = new Adler32();
adler.update(dexBytes, 12, dexBytes.length - 12);//從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(Integer.toHexString(newcs[i]));
}
System.arraycopy(recs, 0, dexBytes, 8, 4);//效驗碼賦值(8-11)
System.out.println(Long.toHexString(value));
System.out.println();
}
/**
* int 轉byte[]
* @param number
* @return
*/
public 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;
}
/**
* 修改dex頭 sha1值
* @param dexBytes
* @throws NoSuchAlgorithmException
*/
private static void fixSHA1Header(byte[] dexBytes)
throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(dexBytes, 32, dexBytes.length - 32);//從32為到結束計算sha--1
byte[] newdt = md.digest();
System.arraycopy(newdt, 0, dexBytes, 12, 20);//修改sha-1值(12-31)
//輸出sha-1值,可有可無
String hexstr = "";
for (int i = 0; i < newdt.length; i++) {
hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16)
.substring(1);
}
System.out.println(hexstr);
}
/**
* 修改dex頭 file_size值
* @param dexBytes
*/
private static void fixFileSizeHeader(byte[] dexBytes) {
//新檔案長度
byte[] newfs = intToByte(dexBytes.length);
System.out.println(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(Integer.toHexString(newfs[i]));
}
System.arraycopy(refs, 0, dexBytes, 32, 4);//修改(32-35)
}
/**
* 以二進位制讀出檔案內容
* @param file
* @return
* @throws IOException
*/
private static byte[] readFileBytes(File file) throws IOException {
byte[] arrayOfByte = new byte[1024];
ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(file);
while (true) {
int i = fis.read(arrayOfByte);
if (i != -1) {
localByteArrayOutputStream.write(arrayOfByte, 0, i);
} else {
return localByteArrayOutputStream.toByteArray();
}
}
}
}
我們可以看到程式比較簡單和我們之前描述的一樣,核心部分就在檔案拼接部分和修改三個頭資訊的部分。
//依次將解殼DEX,加密後的源APK,加密後的源APK大小,拼接出新的Dex
byte[] newdex = new byte[totalLen];
System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);
System.arraycopy(enSrcApkArray, 0, newdex, unShellDexLen, enSrcApkLen);
System.arraycopy(intToByte(enSrcApkLen), 0, newdex, totalLen-4, 4);
//修改DEX file size檔案頭
fixFileSizeHeader(newdex);
//修改DEX SHA1 檔案頭
fixSHA1Header(newdex);
//修改DEX CheckSum檔案頭
fixCheckSumHeader(newdex);
我們再來看下第三個專案:
加殼程式,這個程式主要負責將在JAVA專案中加密的源Apk獲取及解密,以及動態載入源Apk。專案結構
我們看下程式碼:
package com.jju.yuxin.reforceapk;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
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.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import dalvik.system.DexClassLoader;
/**
* =============================================================================
* Copyright (c) 2017 yuxin All rights reserved.
* Packname com.jju.yuxin.reforceapk
* Created by yuxin.
* Created time 2017/6/18 0018 下午 5:03.
* Version 1.0;
* Describe :
* History:
* ==============================================================================
*/
public class ProxyApplication extends Application{
private static final String appkey = "APPLICATION_CLASS_NAME";
private static final String TAG=ProxyApplication.class.getSimpleName();
private String srcApkFilePath;
private String odexPath;
private String libPath;
//以下是載入資源
protected AssetManager mAssetManager;
protected Resources mResources;
protected Resources.Theme mTheme;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
Log.d(TAG,"----------onCreate");
try {
File odex = this.getDir("payload_odex", MODE_PRIVATE);
File libs = this.getDir("payload_lib", MODE_PRIVATE);
//用於存放源apk釋放出來的dex
odexPath = odex.getAbsolutePath();
//用於存放源Apk用到的so檔案
libPath = libs.getAbsolutePath();
//用於存放解密後的apk
srcApkFilePath = odex.getAbsolutePath() + "/payload.apk";
File srcApkFile= new File(srcApkFilePath);
Log.i("demo", "apk size:"+srcApkFile.length());
//第一次載入
if (!srcApkFile.exists())
{
Log.i("demo", "isFirstLoading");
srcApkFile.createNewFile();
//拿到dex檔案
byte[] dexdata = this.readDexFileFromApk();
//取出源APK解密後放置在/payload.apk,及其so檔案放置在payload_lib/下
this.splitPayLoadFromDex(dexdata);
}
// 配置動態載入環境
//反射獲取主執行緒物件,並從中獲取所有已載入的package資訊,並中找到當前的LoadApk物件的弱引用
Object currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {});
String packageName = this.getPackageName();
ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mPackages");
WeakReference wr = (WeakReference) mPackages.get(packageName);
//建立一個新的DexClassLoader用於載入源Apk,
// 傳入apk路徑,dex釋放路徑,so路徑,及父節點的DexClassLoader使其遵循雙親委託模型
DexClassLoader dLoader = new DexClassLoader(srcApkFilePath, odexPath,
libPath, (ClassLoader) RefInvoke.getFieldOjbect(
"android.app.LoadedApk", wr.get(), "mClassLoader"));
//getClassLoader()等同於 (ClassLoader) RefInvoke.getFieldOjbect()
//但是為了替換掉父節點我們需要通過反射來獲取並修改其值
Log.i(TAG,"父classloader:"+(ClassLoader) RefInvoke.getFieldOjbect(
"android.app.LoadedApk", wr.get(), "mClassLoader"));
//將父節點DexClassLoader替換
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
wr.get(), dLoader);
Log.i(TAG,"子classloader:"+dLoader);
try{
//嘗試載入源Apk的MainActivity
Object actObj = dLoader.loadClass("com.jju.yuxin.sourceproject.MainActivity");
Log.i(TAG, "SrcApk_MainActivity:"+actObj);
}catch(Exception e){
Log.i(TAG, "LoadSrcActivityErr:"+Log.getStackTraceString(e));
}
} catch (Exception e) {
Log.i(TAG, "error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
}
public void onCreate() {
//載入源apk資源
//loadResources(srcApkFilePath);
Log.i(TAG, "--------onCreate");
//獲取配置在清單檔案的源Apk的Application路勁
String appClassName = null;
try {
ApplicationInfo ai = this.getPackageManager()
.getApplicationInfo(this.getPackageName(),
PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {
appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml檔案中的。
} else {
Log.i(TAG, "have no application class name");
return;
}
} catch (PackageManager.NameNotFoundException e) {
Log.i(TAG, "error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
//獲取當前殼Apk的ApplicationInfo
Object currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {});
Object mBoundApplication = RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mBoundApplication");
Object loadedApkInfo = RefInvoke.getFieldOjbect(
"android.app.ActivityThread$AppBindData",
mBoundApplication, "info");
//將LoadedApk中的ApplicationInfo設定為null
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
loadedApkInfo, null);
//獲取currentActivityThread中註冊的Application
Object oldApplication = RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mInitialApplication");
//獲取ActivityThread中所有已註冊的Application,並將當前殼Apk的Application從中移除
ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
.getFieldOjbect("android.app.ActivityThread",
currentActivityThread, "mAllApplications");
mAllApplications.remove(oldApplication);
ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke
.getFieldOjbect("android.app.LoadedApk", loadedApkInfo,
"mApplicationInfo");
ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke
.getFieldOjbect("android.app.ActivityThread$AppBindData",
mBoundApplication, "appInfo");
//替換原來的Application
appinfo_In_LoadedApk.className = appClassName;
appinfo_In_AppBindData.className = appClassName;
//註冊Application
Application app = (Application) RefInvoke.invokeMethod(
"android.app.LoadedApk", "makeApplication", loadedApkInfo,
new Class[] { boolean.class, Instrumentation.class },
new Object[] { false, null });
//替換ActivityThread中的Application
RefInvoke.setFieldOjbect("android.app.ActivityThread",
"mInitialApplication", currentActivityThread, app);
ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mProviderMap");
Iterator it = mProviderMap.values().iterator();
while (it.hasNext()) {
Object providerClientRecord = it.next();
Object localProvider = RefInvoke.getFieldOjbect(
"android.app.ActivityThread$ProviderClientRecord",
providerClientRecord, "mLocalProvider");
RefInvoke.setFieldOjbect("android.content.ContentProvider",
"mContext", localProvider, app);
}
Log.i(TAG, "Srcapp:"+app);
app.onCreate();
}
private void splitPayLoadFromDex(byte[] shelldexdata) throws IOException {
int sdlen = shelldexdata.length;
//取被加殼apk的長度
byte[] dexlen = new byte[4];
System.arraycopy(shelldexdata, sdlen - 4, dexlen, 0, 4);
ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
DataInputStream in = new DataInputStream(bais);
int readInt = in.readInt();
Log.d(TAG,"Integer.toHexString(readInt):"+Integer.toHexString(readInt));
//取出apk
byte[] ensrcapk = new byte[readInt];
System.arraycopy(shelldexdata, sdlen - 4 - readInt, ensrcapk, 0, readInt);
//對源程式Apk進行解密
byte[] srcapk = decrypt(ensrcapk);
//寫入源apk檔案
File file = new File(srcApkFilePath);
try {
FileOutputStream localFileOutputStream = new FileOutputStream(file);
localFileOutputStream.write(srcapk);
localFileOutputStream.close();
} catch (IOException localIOException) {
throw new RuntimeException(localIOException);
}
//分析源apk檔案
ZipInputStream localZipInputStream = new ZipInputStream(
new BufferedInputStream(new FileInputStream(file)));
while (true) {
ZipEntry localZipEntry = localZipInputStream.getNextEntry();
if (localZipEntry == null) {
localZipInputStream.close();
break;
}
//依次取出被加殼apk用到的so檔案,放到 libPath中(data/data/包名/payload_lib)
String name = localZipEntry.getName();
if (name.startsWith("lib/") && name.endsWith(".so")) {
File storeFile = new File(libPath + "/"
+ name.substring(name.lastIndexOf('/')));
storeFile.createNewFile();
FileOutputStream fos = new FileOutputStream(storeFile);
byte[] arrayOfByte = new byte[1024];
while (true) {
int i = localZipInputStream.read(arrayOfByte);
if (i == -1)
break;
fos.write(arrayOfByte, 0, i);
}
fos.flush();
fos.close();
}
localZipInputStream.closeEntry();
}
localZipInputStream.close();
}
/**
* 拿到自己apk檔案中的dex檔案
* @return
* @throws IOException
*/
private byte[] readDexFileFromApk() throws IOException {
ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
ZipInputStream localZipInputStream = new ZipInputStream(
new BufferedInputStream(new FileInputStream(
this.getApplicationInfo().sourceDir)));
while (true) {
ZipEntry localZipEntry = localZipInputStream.getNextEntry();
if (localZipEntry == null) {
localZipInputStream.close();
break;
}
//拿到dex檔案
if (localZipEntry.getName().equals("classes.dex")) {
byte[] arrayOfByte = new byte[1024];
while (true) {
int i = localZipInputStream.read(arrayOfByte);
if (i == -1)
break;
dexByteArrayOutputStream.write(arrayOfByte, 0, i);
}
}
localZipInputStream.closeEntry();
}
localZipInputStream.close();
return dexByteArrayOutputStream.toByteArray();
}
// //直接返回資料,讀者可以新增自己解密方法
private byte[] decrypt(byte[] srcdata) {
for(int i=0;i<srcdata.length;i++){
srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;
}
protected void loadResources(String srcApkPath) {
//建立一個AssetManager放置源apk的資源
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, srcApkPath);
mAssetManager = assetManager;
} catch (Exception e) {
Log.i(TAG, "inject:loadResource error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
Resources superRes = super.getResources();
superRes.getDisplayMetrics();
superRes.getConfiguration();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
@Override
public AssetManager getAssets() {
return mAssetManager == null ? super.getAssets() : mAssetManager;
}
@Override
public Resources getResources() {
return mResources == null ? super.getResources() : mResources;
}
@Override
public Resources.Theme getTheme() {
return mTheme == null ? super.getTheme() : mTheme;
}
}
這個檔案比較長我們來依次分析:
//第一次載入
if (!srcApkFile.exists())
{
Log.i("demo", "isFirstLoading");
srcApkFile.createNewFile();
//拿到dex檔案
byte[] dexdata = this.readDexFileFromApk();
//取出源APK解密後放置在/payload.apk,及其so檔案放置在payload_lib/下
this.splitPayLoadFromDex(dexdata);
}
通過判斷用於存放解密後的源Apk檔案是否存在來判斷是否是第一次載入。第一次載入時通過readDexFileFromApk()來獲取當前Apk的Dex檔案
/**
* 拿到自己apk檔案中的dex檔案
* @return
* @throws IOException
*/
private byte[] readDexFileFromApk() throws IOException {
ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
ZipInputStream localZipInputStream = new ZipInputStream(
new BufferedInputStream(new FileInputStream(
this.getApplicationInfo().sourceDir)));
while (true) {
ZipEntry localZipEntry = localZipInputStream.getNextEntry();
if (localZipEntry == null) {
localZipInputStream.close();
break;
}
//拿到dex檔案
if (localZipEntry.getName().equals("classes.dex")) {
byte[] arrayOfByte = new byte[1024];
while (true) {
int i = localZipInputStream.read(arrayOfByte);
if (i == -1)
break;
dexByteArrayOutputStream.write(arrayOfByte, 0, i);
}
}
localZipInputStream.closeEntry();
}
localZipInputStream.close();
return dexByteArrayOutputStream.toByteArray();
}
然後通過this.splitPayLoadFromDex();將當前Dex分解,從中獲取源Apk並將其解密,以及源Apk的so庫
private void splitPayLoadFromDex(byte[] shelldexdata) throws IOException {
int sdlen = shelldexdata.length;
//取被加殼apk的長度
byte[] dexlen = new byte[4];
System.arraycopy(shelldexdata, sdlen - 4, dexlen, 0, 4);
ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
DataInputStream in = new DataInputStream(bais);
int readInt = in.readInt();
Log.d(TAG,"Integer.toHexString(readInt):"+Integer.toHexString(readInt));
//取出apk
byte[] ensrcapk = new byte[readInt];
System.arraycopy(shelldexdata, sdlen - 4 - readInt, ensrcapk, 0, readInt);
//對源程式Apk進行解密
byte[] srcapk = decrypt(ensrcapk);
//寫入源apk檔案
File file = new File(srcApkFilePath);
try {
FileOutputStream localFileOutputStream = new FileOutputStream(file);
localFileOutputStream.write(srcapk);
localFileOutputStream.close();
} catch (IOException localIOException) {
throw new RuntimeException(localIOException);
}
//分析源apk檔案
ZipInputStream localZipInputStream = new ZipInputStream(
new BufferedInputStream(new FileInputStream(file)));
while (true) {
ZipEntry localZipEntry = localZipInputStream.getNextEntry();
if (localZipEntry == null) {
localZipInputStream.close();
break;
}
//依次取出被加殼apk用到的so檔案,放到 libPath中(data/data/包名/payload_lib)
String name = localZipEntry.getName();
if (name.startsWith("lib/") && name.endsWith(".so")) {
File storeFile = new File(libPath + "/"
+ name.substring(name.lastIndexOf('/')));
storeFile.createNewFile();
FileOutputStream fos = new FileOutputStream(storeFile);
byte[] arrayOfByte = new byte[1024];
while (true) {
int i = localZipInputStream.read(arrayOfByte);
if (i == -1)
break;
fos.write(arrayOfByte, 0, i);
}
fos.flush();
fos.close();
}
localZipInputStream.closeEntry();
}
localZipInputStream.close();
}
然後通過動態載入機制將加殼程式的ClassLoader替換成他的子ClassLoader這樣確保既能載入自己的Class又能載入源Apk的Class
核心程式碼,如果這段程式碼不是很懂,你可能需要去了解Java反射,Android中Classloader的雙親委託模型,以及動態載入機制
DexClassLoader dLoader = new DexClassLoader(srcApkFilePath, odexPath,
libPath, (ClassLoader) RefInvoke.getFieldOjbect(
"android.app.LoadedApk", wr.get(), "mClassLoader"));
然後在Application的onCreate()中替換LoadApk以及ActivityThread中的Application,希望更清楚的明白這點需要了解下Android中Activity以及Application的啟動流程,其中在加殼程式的清單檔案中我們配置了源Apk的相關資訊以便能找到他們
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:name=".ProxyApplication"
android:theme="@style/AppTheme">
<meta-data
android:name="APPLICATION_CLASS_NAME"
android:value="com.jju.yuxin.sourceproject.SourceApplication"/>
<activity android:name="com.jju.yuxin.sourceproject.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name="com.jju.yuxin.sourceproject.SubActivity">
</activity>
</application>
實現操作流程:
先將源Apk的專案生成Apk,放置到JAVA專案中
將加殼程式也生成Apk,通過直接將apk字尾名改成zip的方式獲取到classes.dex,(最好複製一份,這個後面還要用)。將classes.dex改名成shelldex.dex放置到JAVA專案中
執行JAVA專案,將生成新的Dex檔案classes.dex,將新生成的classes.dex替換加殼Apk的classes.dex(通過解壓軟體直接拖放進去替換即可)
最後cd到apktool的目錄下,使用apktool中的jarsigner對應用重新簽名即可,重新簽名指令
jarsigner -verbose -keystore 簽名檔案路徑 -storepass 密碼 -keypass 密碼 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar 簽名後生成Apk路徑 需要簽名Apk路徑 簽名檔案別名
//例如:jarsigner -verbose -keystore yuxin.jks -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk reforce.apk yuxin
這三個專案的Github地址(專案地址)
常見錯誤:
ClassNotFoundException:
這個錯誤主要注意:Class路徑拼寫有沒錯,加密Apk能否正確的轉為源Apk,還有就是ClassLoader有沒用錯
Class ref in pre-verified class resolved to unexpected
implementation:
這是類被重複載入的錯誤,需要檢查報錯的類是否被別的ClassLoder已經載入過了,我的Activity在繼承android.support.v7.app.AppCompatActivity時候報了這個錯誤,改成Activity就好了,原因還沒去找。