如何徹底防止反編譯,dex加密怎麼做

**面試官: 如何徹底防止反編譯,dex加密怎麼做 **

心理分析:面試官想知道你是否有過對dex加固相關的經驗,該題想考的是dex加固流程,dex編碼有沒有了解

**求職者:**應該從dex加固流程 ,從項目中開始,dex加固--打包--驗證 說起。接下來給大家講解dex原理分析

原理解析

下面看一下Android中加殼的原理: [

如何徹底防止反編譯,dex加密怎麼做

在加固過程中需要三個對象:

  • 需要加密的APK(源程序APK)
  • 殼程序APK(負責解密APK工作)
  • 加密工具(將源APK進行加密和殼程序的DEX合併)

主要步驟 用加密算法對源程序APK進行加密,再將其與殼程序APK的DEX文件合併生成新的DEX文件,最後替換殼程序中的原DEX文件即可。得到新的APK也叫做脫殼程序APK,它已經不是一個完整意義上的APK程序了,它的主要工作是:負責解密源程序APK,然後加載APK,讓其正常運行起來。 在這個過程中需要了解的知識是:如何將源程序APK和殼程序APK進行合併 這需要了解DEX文件的格式,下面簡單介紹一下:

addressnamesize/bytevalue0magic[8]80x6465 780a 3033 35008checksum40xc136 5e17csignature[20]2020file_size40x02e424header_size40x7028endian_tag40x123456782Clink_size40x0030link_off40x0034map_off40x024438string_ids_size40x0e3cstring_ids_off40x7040type_ids_size40x0744type_ids_off40xa848proto_ids_size40x034Cproto_ids_off40xc450field_ids_size40x0154field_ids_off40xe858method_ids_size40x045Cmethod_ids_off40xf060class_defs_size40x0164class_defs_off40x011068data_size40x01b46Cdata_off40x0130

現在只要關注其中三個部分:

  • checksum(文件校驗碼)使用alder32算法校驗文件,除去magic、checksum外餘下的所有文件區域,用於檢查文件錯誤。
  • signature 使用SHA-1算法hash出去magic、checksum和signature外餘下的所有文件區域,用於唯一識別本文件。
  • file_size DEX文件大小。

我們需要將加密之後的源程序APK文件寫入到DEX中,那麼就需要修改checksum,因為它的值和文件內容有關。signature也是一樣,也是唯一識別文件的算法,還有DEX文件的大小。 還需要一個操作,就是標註加密之後的源程序APK文件的大小,因為運行解密的時候,需要知道APK的大小,才能正確得到源程序APK。這個值直接放到文件的末尾就可以了。 修改之後的DEX文件的格式如下:

如何徹底防止反編譯,dex加密怎麼做

知道了原理,下面就是代碼實現了。這裡有三個工程:

  • 源程序項目(需要加密的APK)
  • 殼項目(解密源程序APK和加載APK)
  • 對源APK進行加密和殼項目的DEX的合併

項目案例

下面先來看一下源程序 1.需要加密的源程序項目:SourceApk [

如何徹底防止反編譯,dex加密怎麼做

需要一個Application類,這個到後面說為什麼需要: MyApplication.java

package com.example.sourceapk;
public class MyApplication extends Application {
 @Override
 public void onCreate() {
 super.onCreate();
 Log.i("demo", "source apk onCreate:" + this);
 }
}

就是打印一下onCreate方法。 MainActivity.java

package com.example.sourceapk;
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.加殼程序項目:DexPackTool

如何徹底防止反編譯,dex加密怎麼做

加殼程序其實就是一個Java工程,它的工作就是加密源程序APK,然後將其寫入到殼程序的DEX文件裡,修改文件頭,得到一個新的DEX文件。 看一下代碼:

package com.example.packdex;
public class mymain {
 public static void main(String[] args) {
 try {
 File payloadSrcFile = new File("files/SourceApk.apk"); // 需要加殼的源程序
 System.out.println("apk size:"+payloadSrcFile.length());
 File packDexFile = new File("files/SourceApk.dex"); // 殼程序dex
 byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile)); // 以二進制形式讀出源apk,並進行加密處理
 byte[] packDexArray = readFileBytes(packDexFile); // 以二進制形式讀出dex
 /* 合併文件 */
 int payloadLen = payloadArray.length;
 int packDexLen = packDexArray.length;
 int totalLen = payloadLen + packDexLen + 4; // 多出4字節是存放長度的
 byte[] newdex = new byte[totalLen]; // 申請了新的長度
 // 添加解殼代碼
 System.arraycopy(packDexArray, 0, newdex, 0, packDexLen); // 先拷貝dex內容
 // 添加加密後的解殼數據
 System.arraycopy(payloadArray, 0, newdex, packDexLen, 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 = "files/classes.dex"; // 創建一個新文件
 File file = new File(str);
 if (!file.exists()) {
 file.createNewFile();
 }
 
 FileOutputStream localFileOutputStream = new FileOutputStream(str);
 localFileOutputStream.write(newdex); // 將新計算出的二進制dex數據寫入文件
 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;
 }
 ...
}

加密算法很簡單,只是對每個字節進行異或一下。

這裡是為了簡單,所以就用了很簡單的加密算法,其實為了增加破解難度,我們應該使用更高效的加密算法,同時最好將加密操作放到native層去做。

這裡需要兩個輸入文件:

  • 源程序APK文件:SourceApk.apk
  • 殼程序的DEX文件:SourceApk.dex

第一個文件就是源程序項目編譯之後的APK文件,第二個文件是下面要講的第三個項目:殼程序項目中的classes.dex文件,修改名稱之後得到。 3.殼程序項目:PackApk

如何徹底防止反編譯,dex加密怎麼做

先來了解一下殼程序項目的工作:

  • 通過反射置換android.app.ActivityThread中的mClassLoader為加載解密出APK的DexClassLoader,該DexClassLoader一方面加載了源程序,另一方面以原mClassLoader為父節點,這就保證即加載了源程序,又沒有放棄原先加載的資源與系統代碼。 關於這部分內容不瞭解的可以看一下Android動態加載之免安裝運行程序這篇文章。
  • 找到源程序的Application,通過反射建立並運行。 這裡需要注意的是,我們現在是加載一個完整的APK,讓他運行起來。一個APK運行的時候都是有一個Application對象的,這個也是一個程序運行之後的全局類,所以我們必須找到解密之後的源程序APK的Application類,運行它的onCreate方法,這樣源程序APK才開始它的運行生命週期。後面會說如何得到源程序APK的Application類:使用meta標籤進行設置。

下面看一下整體流程:

如何徹底防止反編譯,dex加密怎麼做

下面看一下代碼: ProxyApplication.java

  1. 得到殼程序APK中的DEX文件,然後從這個文件中得到源程序APK進行解密、加載
// 這是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[] {});//獲取主線程對象 
 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"));
 //把當前進程的mClassLoader設置成了被加殼apk的DexClassLoader
 RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
 wr.get(), dLoader);
 
 Log.i("demo","classloader:"+dLoader);
 try{
 Object actObj = dLoader.loadClass("com.example.sourceapk.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方法執行前就會執行了,所以我們的工作就需要在這裡進行。 A) 從APK中獲取到DEX文件

/**
 * 從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();
}

B) 從殼程序DEX中得到源程序APK文件

/**
 * 釋放被加殼的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

//直接返回數據,讀者可以添加自己解密方法
private byte[] decrypt(byte[] srcdata) {
 for(int i=0;i
  1. 找到源程序的Application程序,讓其運行
@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 mAllApplications = (ArrayList) 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對象的。 下面來看一下AndroidManifest.xml文件中的內容:

 
 

這裡我們定義了源程序APK的Application類名。 項目下載

運行程序

下面就看看程序的運行步驟:

  • 第一步:得到源程序APK文件和殼程序的DEX文件 運行源程序和殼程序項目,之後得到這兩個文件(將殼程序的classes.dex文件改名為SourceApk.dex),然後使用加密工具進行加殼。
  • 第二步:替換殼程序中的classes.dex文件 我們在第一步中得到加殼之後的classes.dex文件之後,將其與PackApk.apk中的原classes.dex文件替換。
  • 第三步:在第二步的時候得到替換之後的PackApk.apk文件,這個文件因為被修改了,所以我們需要重新對它簽名,不然運行也是報錯的。 簽名之後的文件就可以運行了,效果如下:
如何徹底防止反編譯,dex加密怎麼做

轉發+私信,可以免費獲取Android面試資料


分享到:


相關文章: