android 加固防止反編譯-重新打包
1、需要加密的Apk(源Apk)
2、殼程式Apk(負責解密Apk工作)
3、加密工具(將源Apk進行加密和殼Dex合併成新的Dex)
主要步驟:
我們拿到需要加密的Apk和自己的殼程式Apk,然後用加密演算法對源Apk進行加密在將殼Apk進行合併得到新的Dex檔案,最後替換殼程式中的dex檔案即可,得到新的Apk,那麼這個新的Apk我們也叫作脫殼程式Apk.他已經不是一個完整意義上的Apk程式了,他的主要工作是:負責解密源Apk.然後載入Apk,讓其正常執行起來。
在這個過程中我們可能需要了解的一個知識是: 如何將源Apk和殼Apk進行合併成新的Dex
這裡就需要了解Dex檔案的格式了。下面就來簡單介紹一下Dex檔案的格式
主要來看一下Dex檔案的頭部資訊,其實Dex檔案和Class檔案的格式分析原理都是一樣的,他們都是有固定的格式,我們知道現在反編譯的一些工具:
1、jd-gui:可以檢視jar中的類,其實他就是解析class檔案,只要瞭解class檔案的格式就可以
2、dex2jar:將dex檔案轉化成jar,原理也是一樣的,只要知道Dex檔案的格式,能夠解析出dex檔案中的類資訊就可以了
當然我們在分析這個檔案的時候,最重要的還是頭部資訊,應該他是一個檔案的開始部分,也是索引部分,內部資訊很重要。
我們今天只要關注上面紅色標記的三個部分:
1) checksum
檔案校驗碼 ,使用alder32 演算法校驗檔案除去 maigc ,checksum 外餘下的所有檔案區域 ,用於檢查檔案錯誤 。
2) signature
使用 SHA-1 演算法 hash 除去 magic ,checksum 和 signature 外餘下的所有檔案區域 ,用於唯一識別本檔案 。
3) file_size
Dex 檔案的大小 。
為什麼說我們只需要關注這三個欄位呢?
因為我們需要將一個檔案(加密之後的源Apk)寫入到Dex中,那麼我們肯定需要修改檔案校驗碼(checksum).因為他是檢查檔案是否有錯誤。那麼signature也是一樣,也是唯一識別檔案的演算法。還有就是需要修改dex檔案的大小。
不過這裡還需要一個操作,就是標註一下我們加密的Apk的大小,因為我們在脫殼的時候,需要知道Apk的大小,才能正確的得到Apk。那麼這個值放到哪呢?這個值直接放到檔案的末尾就可以了。
所以總結一下我們需要做:修改Dex的三個檔案頭,將源Apk的大小追加到殼dex的末尾就可以了。
我們修改之後得到新的Dex檔案樣式如下:
那麼我們知道原理了,下面就是程式碼實現了。所以這裡有三個工程:
1、源程式專案(需要加密的Apk)
2、脫殼專案(解密源Apk和載入Apk)
3、對源Apk進行加密和脫殼專案的Dex的合併
三、專案案例
下面先來看一下源程式
1、需要加密的源程式Apk專案:ForceApkObj
需要一個Application類,這個到後面說為什麼需要:
MyApplication.java
package com.example.forceapkobj;
import android.app.Application;
import android.util.Log;
public class MyApplication extends Application{
@Override
public void onCreate() {
super.onCreate();
Log.i("demo", "source apk onCreate:"+this);
}
}
就是列印一下onCreate方法。
MainActivity.java
package com.example.forceapkobj;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView content = new TextView(this);
content.setText("I am Source Apk");
content.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0) {
Intent intent = new Intent(MainActivity.this, SubActivity.class);
startActivity(intent);
}});
setContentView(content);
Log.i("demo", "app:"+getApplicationContext());
}
}
也是列印一下內容。
2、加殼程式專案:DexShellTools
加殼程式其實就是一個Java工程,因為我們從上面的分析可以看到,他的工作就是加密源Apk,然後將其寫入到脫殼Dex檔案中,修改檔案頭,得到一個新的Dex檔案即可。
看一下程式碼:
package com.example.reforceapk;
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 {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
File payloadSrcFile = new File("force/ForceApkObj.apk"); //需要加殼的程式
System.out.println("apk size:"+payloadSrcFile.length());
File unShellDexFile = new File("force/ForceApkObj.dex"); //解客dex
byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二進位制形式讀出apk,並進行加密處理//對源Apk進行加密操作
byte[] unShellDexArray = readFileBytes(unShellDexFile);//以二進位制形式讀出dex
int payloadLen = payloadArray.length;
int unShellDexLen = unShellDexArray.length;
int totalLen = payloadLen + unShellDexLen +4;//多出4位元組是存放長度的。
byte[] newdex = new byte[totalLen]; // 申請了新的長度
//新增解殼程式碼
System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷貝dex內容
//新增加密後的解殼資料
System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex內容後面拷貝apk的內容
//新增解殼資料長度
System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 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();
}
}
}
}
下面來分析一下:
紅色部分其實就是最核心的工作:
1>、加密源程式Apk檔案
byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二進位制形式讀出apk,並進行加密處理//對源Apk進行加密操作
加密演算法很簡單:
//直接返回資料,讀者可以新增自己加密方法 private static byte[] encrpt(byte[] srcdata){ for(int i = 0;i<srcdata.length;i++){ srcdata[i] = (byte)(0xFF ^ srcdata[i]); } return srcdata; }
對每個位元組進行異或一下即可。
(說明:這裡是為了簡單,所以就用了很簡單的加密演算法了,其實為了增加破解難度,我們應該使用更高效的加密演算法,同事最好將加密操作放到native層去做)
2>、合併檔案:將加密之後的Apk和原脫殼Dex進行合併
int payloadLen = payloadArray.length; int unShellDexLen = unShellDexArray.length; int totalLen = payloadLen + unShellDexLen +4;//多出4位元組是存放長度的。 byte[] newdex = new byte[totalLen]; // 申請了新的長度 //新增解殼程式碼 System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷貝dex內容 //新增加密後的解殼資料 System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex內容後面拷貝apk的內容
3>、在檔案的末尾追加源程式Apk的長度
//新增解殼資料長度 System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最後4為長度4>、修改新Dex檔案的檔案頭資訊:file_size; sha1; check_sum
//修改DEX file size檔案頭 fixFileSizeHeader(newdex); //修改DEX SHA1 檔案頭 fixSHA1Header(newdex); //修改DEX CheckSum檔案頭 fixCheckSumHeader(newdex);
具體修改可以參照之前說的檔案頭格式,修改指定位置的位元組值即可。
這裡我們還需要兩個輸入檔案:
1>、源Apk檔案:ForceApkObj.apk
2>、脫殼程式的Dex檔案:ForceApkObj.dex
那麼第一個檔案我們都知道,就是上面的源程式編譯之後的Apk檔案,那麼第二個檔案我們怎麼得到呢?這個就是我們要講到的第三個專案:脫殼程式專案,他是一個Android專案,我們在編譯之後,能夠得到他的classes.dex檔案,然後修改一下名稱就可。
3、脫殼專案:ReforceApk
在講解這個專案之前,我們先來了解一下這個脫殼專案的工作:
1>、通過反射置換android.app.ActivityThread 中的mClassLoader為載入解密出APK的DexClassLoader,該DexClassLoader一方面載入了源程式、另一方面以原mClassLoader為父節點,這就保證了即載入了源程式又沒有放棄原先載入的資源與系統程式碼。
關於這部分內容,不瞭解的同學可以看一下ActivityThread.java的原始碼:
或者直接看一下這篇文章:
如何得到系統載入Apk的類載入器,然後我們怎麼將載入進來的Apk執行起來等問題都在這篇文章中說到了。
2>、找到源程式的Application,通過反射建立並執行。
這裡需要注意的是,我們現在是載入一個完整的Apk,讓他執行起來,那麼我們知道一個Apk執行的時候都是有一個Application物件的,這個也是一個程式執行之後的全域性類。所以我們必須找到解密之後的源Apk的Application類,執行的他的onCreate方法,這樣源Apk才開始他的執行生命週期。這裡我們如何得到源Apk的Application的類呢?這個我們後面會說道。使用meta標籤進行設定。
下面來看一下整體的流程圖:
所以我們看到這裡還需要一個核心的技術就是動態載入。關於動態載入技術,不瞭解的同學可以看這篇文章:
下面來看一下程式碼:
package com.example.reforceapk;
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.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
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.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import dalvik.system.DexClassLoader;
public class ProxyApplication extends Application{
private static final String appkey = "APPLICATION_CLASS_NAME";
private String apkFileName;
private String odexPath;
private String libPath;
//這是context 賦值
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
//建立兩個資料夾payload_odex,payload_lib 私有的,可寫的檔案目錄
File odex = this.getDir("payload_odex", MODE_PRIVATE);
File libs = this.getDir("payload_lib", MODE_PRIVATE);
odexPath = odex.getAbsolutePath();
libPath = libs.getAbsolutePath();
apkFileName = odex.getAbsolutePath() + "/payload.apk";
File dexFile = new File(apkFileName);
Log.i("demo", "apk size:"+dexFile.length());
if (!dexFile.exists())
{
dexFile.createNewFile(); //在payload_odex資料夾內,建立payload.apk
// 讀取程式classes.dex檔案
byte[] dexdata = this.readDexFileFromApk();
// 分離出解殼後的apk檔案已用於動態載入
this.splitPayLoadFromDex(dexdata);
}
// 配置動態載入環境
Object currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {});//獲取主執行緒物件 http://blog.csdn.net/myarrow/article/details/14223493
String packageName = this.getPackageName();//當前apk的包名
//下面兩句不是太理解
ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mPackages");
WeakReference wr = (WeakReference) mPackages.get(packageName);
//建立被加殼apk的DexClassLoader物件 載入apk內的類和原生代碼(c/c++程式碼)
DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
libPath, (ClassLoader) RefInvoke.getFieldOjbect(
"android.app.LoadedApk", wr.get(), "mClassLoader"));
//base.getClassLoader(); 是不是就等同於 (ClassLoader) RefInvoke.getFieldOjbect()? 有空驗證下//?
//把當前程序的DexClassLoader 設定成了被加殼apk的DexClassLoader ----有點c++中程序環境的意思~~
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
wr.get(), dLoader);
Log.i("demo","classloader:"+dLoader);
try{
Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");
Log.i("demo", "actObj:"+actObj);
}catch(Exception e){
Log.i("demo", "activity:"+Log.getStackTraceString(e));
}
} catch (Exception e) {
Log.i("demo", "error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
}
@Override
public void onCreate() {
{
//loadResources(apkFileName);
Log.i("demo", "onCreate");
// 如果源應用配置有Appliction物件,則替換為源應用Applicaiton,以便不影響源程式邏輯。
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("demo", "have no application class name");
return;
}
} catch (NameNotFoundException e) {
Log.i("demo", "error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
//有值的話呼叫該Applicaiton
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");
//把當前程序的mApplication 設定成了null
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",
loadedApkInfo, null);
Object oldApplication = RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mInitialApplication");
//http://www.codeceo.com/article/android-context.html
ArrayList<Application> mAllApplications = (ArrayList<Application>) RefInvoke
.getFieldOjbect("android.app.ActivityThread",
currentActivityThread, "mAllApplications");
mAllApplications.remove(oldApplication);//刪除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");
appinfo_In_LoadedApk.className = appClassName;
appinfo_In_AppBindData.className = appClassName;
Application app = (Application) RefInvoke.invokeMethod(
"android.app.LoadedApk", "makeApplication", loadedApkInfo,
new Class[] { boolean.class, Instrumentation.class },
new Object[] { false, null });//執行 makeApplication(false,null)
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("demo", "app:"+app);
app.onCreate();
}
}
/**
* 釋放被加殼的apk檔案,so檔案
* @param data
* @throws IOException
*/
private void splitPayLoadFromDex(byte[] apkdata) throws IOException {
int ablen = apkdata.length;
//取被加殼apk的長度 這裡的長度取值,對應加殼時長度的賦值都可以做些簡化
byte[] dexlen = new byte[4];
System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);
ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
DataInputStream in = new DataInputStream(bais);
int readInt = in.readInt();
System.out.println(Integer.toHexString(readInt));
byte[] newdex = new byte[readInt];
//把被加殼apk內容拷貝到newdex中
System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);
//這裡應該加上對於apk的解密操作,若加殼是加密處理的話
//?
//對源程式Apk進行解密
newdex = decrypt(newdex);
//寫入apk檔案
File file = new File(apkFileName);
try {
FileOutputStream localFileOutputStream = new FileOutputStream(file);
localFileOutputStream.write(newdex);
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 +