一、前言
今天又到週末了,憋了好久又要出博客了,今天來介紹一下Android中的如何對Apk進行加固的原理。現階段。我們知道Android中的反編譯工作越來越讓人操作熟練,我們辛苦的開發出一個apk,結果被人反編譯了,那心情真心不舒服。雖然我們混淆,做到native層,但是這都是治標不治本。反編譯的技術在更新,那麼保護Apk的技術就不能停止。現在網上有很多Apk加固的第三方平臺,最有名的應當屬於:愛加密和梆梆加固了。其實加固有些人認爲很高深的技術,其實不然,說的簡單點就是對源Apk進行加密,然後在套上一層殼即可,當然這裏還有一些細節需要處理,這就是本文需要介紹的內容了。
二、原理解析
下面就來看一下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文件格式的詳細介紹可以查看這個文件:http://download.csdn.net/detail/jiangwei0910410003/9102599
主要來看一下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
[java] view plaincopy
- 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
[java] view plaincopy
- 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文件即可。
看一下代碼:
[java] view plaincopy
- 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文件
[java] view plaincopy
- byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二進制形式讀出apk,並進行加密處理//對源Apk進行加密操作
加密算法很簡單:
[java] view plaincopy
- //直接返回數據,讀者可以添加自己加密方法
- 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進行合併
[java] view plaincopy
- 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的長度
[java] view plaincopy
- //添加解殼數據長度
- System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最後4爲長度
4>、修改新Dex文件的文件頭信息:file_size; sha1; check_sum
[java] view plaincopy
- //修改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的源碼:
或者直接看一下這篇文章:
http://blog.csdn.net/jiangwei0910410003/article/details/48104455
如何得到系統加載Apk的類加載器,然後我們怎麼將加載進來的Apk運行起來等問題都在這篇文章中說到了。
2>、找到源程序的Application,通過反射建立並運行。
這裏需要注意的是,我們現在是加載一個完整的Apk,讓他運行起來,那麼我們知道一個Apk運行的時候都是有一個Application對象的,這個也是一個程序運行之後的全局類。所以我們必須找到解密之後的源Apk的Application類,運行的他的onCreate方法,這樣源Apk纔開始他的運行生命週期。這裏我們如何得到源Apk的Application的類呢?這個我們後面會說道。使用meta標籤進行設置。
下面來看一下整體的流程圖:
所以我們看到這裏還需要一個核心的技術就是動態加載。關於動態加載技術,不瞭解的同學可以看這篇文章:
http://blog.csdn.net/jiangwei0910410003/article/details/48104581
下面來看一下代碼:
[java] view plaincopy
- 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 + "/"
- + 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文件內容(byte)
- * @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;
- }
- 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 AssetManager mAssetManager;//資源管理器
- protected Resources mResources;//資源
- protected Theme mTheme;//主題
- protected void loadResources(String dexPath) {
- try {
- AssetManager assetManager = AssetManager.class.newInstance();
- Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
- addAssetPath.invoke(assetManager, dexPath);
- mAssetManager = assetManager;
- } catch (Exception e) {
- Log.i("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 Theme getTheme() {
- return mTheme == null ? super.getTheme() : mTheme;
- }
- }
首先我們來看一下具體步驟的代碼實現:
1>、得到脫殼Apk中的dex文件,然後從這個文件中得到源程序Apk.進行解密,然後加載
[java] view plaincopy
- //這是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();
- }
- }
這裏需要注意的一個問題,就是我們需要找到一個時機,就是在脫殼程序還沒有運行起來的時候,來加載源程序的Apk,執行他的onCreate方法,那麼這個時機不能太晚,不然的話,就是運行脫殼程序,而不是源程序了。查看源碼我們知道。Application中有一個方法:attachBaseContext這個方法,他在Application的onCreate方法執行前就會執行了,那麼我們的工作就需要在這裏進行
1)、從脫殼程序Apk中找到源程序Apk,並且進行解密操作
[java] view plaincopy
- //創建兩個文件夾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);
- }
這個脫殼解密操作一定要和我們之前的加殼以及加密操作對應,不然就會出現Dex加載錯誤問題
A) 從Apk中獲取到Dex文件
[java] view plaincopy
- /**
- * 從apk包裏面獲取dex文件內容(byte)
- * @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;
- }
- 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();
- }
其實就是解壓Apk文件,直接得到dex文件即可
B) 從脫殼Dex中得到源Apk文件
[java] view plaincopy
- /**
- * 釋放被加殼的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 + "/"
- + 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();
- }
C) 解密源程序Apk
[java] view plaincopy
- ////直接返回數據,讀者可以添加自己解密方法
- private byte[] decrypt(byte[] srcdata) {
- for(int i=0;i<srcdata.length;i++){
- srcdata[i] = (byte)(0xFF ^ srcdata[i]);
- }
- return srcdata;
- }
這個解密算法和加密算法是一致的
2>、加載解密之後的源程序Apk
[java] view plaincopy
- //配置動態加載環境
- 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));
- }
2)、找到源程序的Application程序,讓其運行
[java] view plaincopy
- @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();
- }
- }
直接在脫殼的Application中的onCreate方法中進行就可以了。這裏我們還可以看到是通過AndroidManifest.xml中的meta標籤獲取源程序Apk中的Application對象的。
下面來看一下AndoridManifest.xml文件中的內容:
在這裏我們定義了源程序Apk的Application類名。
項目下載:http://download.csdn.net/detail/jiangwei0910410003/9102741
四、運行程序
那麼到這裏我們就介紹完了,這三個項目的內容,下面就來看看如何運行吧:
運行步驟:
第一步:得到源程序Apk文件和脫殼程序的Dex文件
運行源程序和脫殼程序項目,之後得到這兩個文件(記得將classes.dex文件改名ForceApkObj.dex),然後使用加殼程序進行加殼:
這裏的ForceApkObj.apk文件和ForceApkObj.dex文件是輸入文件,輸出的是classes.dex文件。
第二步:替換脫殼程序中的classes.dex文件
我們在第一步中得到加殼之後的classes.dex文件之後,並且我們在第一步運行脫殼項目的時候得到一個ReforceApk.apk文件,這時候我們使用解壓縮軟件進行替換:
第三步:我們在第二步的時候得到替換之後的ReforceApk.apk文件,這個文件因爲被修改了,所以我們需要從新對他簽名,不然運行也是報錯的。
工具下載:http://download.csdn.net/detail/jiangwei0910410003/9102767
下載之後的工具需要用ReforeceApk.apk文件替換ReforceApk_des.apk文件,然後運行run.bat就可以得到簽名之後的文件了。
run.bat文件的命令如下:
cd C:\Users\i\Desktop\forceapks
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk.apk jiangwei
del ReforceApk.apk
這裏最主要的命令就是中間的一條簽名的命令,關於命令的參數說明如下:
jarsigner -verbose -keystore 簽名文件 -storepass 密碼 -keypass alias的密碼 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA 簽名後的文件 簽名前的apk alias名稱
eg:
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk_src.apk jiangwei
簽名文件的密碼:123456
alais的密碼:123456
所以這裏我們在得到ReforceApk.apk文件的時候,需要簽名,關於Eclipse中如何簽名一個Apk的話,這裏就不多說了,自己google一下吧:
那麼通過上面的三個步驟之後我們得到一個簽名之後的最終文件:ReforceApk_des.apk
我們安裝這個Apk,然後運行,效果如下:
看到運行結果的那一瞬間,我們是多麼的開心,多麼的有成就感,但是這個過程中遇到的問題,是可想而知的。
我們這個時候再去反編譯一下源程序Apk(這個文件是我們脫殼出來的payload.apk,看ReforeceApk中的代碼,就知道他的位置了)
發現dex文件格式是不正確的。說明我們的加固是成功的。
五、遇到的問題
1、研究的過程中遇到簽名不正確的地方,開始的時候,直接替換dex文件之後,就直接運行了Apk,但是總是提示簽名不正確。
2、運行的過程中說找不到源程序中的Activity,這個問題其實我在動態加載的那篇文章中說道了,我們需要在脫殼程序中的AndroidManifest.xml中什麼一下源程序中的Activiity:
六、技術要點
1、對Dex文件格式的瞭解
2、動態加載技術的深入掌握
3、Application的執行流程的瞭解
4、如何從Apk中得到Dex文件
5、如何從新簽名一個Apk程序
七、綜合概述
我們通過上面的過程可以看到,關於Apk加固的工作還是挺複雜的,涉及到的東西也挺多的,下面就在來總結一下吧:
1、加殼程序
任務:對源程序Apk進行加密,合併脫殼程序的Dex文件 ,然後輸入一個加殼之後的Dex文件
語言:任何語言都可以,不限於Java語言
技術點:對Dex文件格式的解析
2、脫殼程序
任務:獲取源程序Apk,進行解密,然後動態加載進來,運行程序
語言:Android項目(Java)
技術點:如何從Apk中獲取Dex文件,動態加載Apk,使用反射運行Application
八、總結
Android中的Apk反編譯可能是每個開發都會經歷的事,但是在反編譯的過程中,對於源程序的開發者來說那是不公平的,那麼Apk加固也是應運而生,但是即使是這樣,我們也還是做不到那麼的安全,現在網上也是有很多文章在解析梆梆加固的原理了。而且有人破解成功了,那麼加固還不是怎麼安全。最後一句話:逆向和加固是一個永不停息的戰爭。