**面試官: 如何徹底防止反編譯,dex加密怎么做 **
心理分析:面試官想知道你是否有過對dex加固相關的經驗,該題想考的是dex加固流程,dex編碼有沒有了解
**求職者:**應該從dex加固流程 ,從項目中開始,dex加固--打包--驗證 說起。接下來給大家講解dex原理分析
原理解析
下面看一下Android中加殼的原理: [
在加固過程中需要三個對象:
- 需要加密的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文件的格式如下:
知道了原理,下面就是代碼實現了。這里有三個工程:
- 源程序項目(需要加密的APK)
- 殼項目(解密源程序APK和加載APK)
- 對源APK進行加密和殼項目的DEX的合并
項目案例
下面先來看一下源程序 1.需要加密的源程序項目:SourceApk [
需要一個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
加殼程序其實就是一個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
先來了解一下殼程序項目的工作:
- 通過反射置換android.app.ActivityThread中的mClassLoader為加載解密出APK的DexClassLoader,該DexClassLoader一方面加載了源程序,另一方面以原mClassLoader為父節點,這就保證即加載了源程序,又沒有放棄原先加載的資源與系統代碼。 關于這部分內容不了解的可以看一下Android動態加載之免安裝運行程序這篇文章。
- 找到源程序的Application,通過反射建立并運行。 這里需要注意的是,我們現在是加載一個完整的APK,讓他運行起來。一個APK運行的時候都是有一個Application對象的,這個也是一個程序運行之后的全局類,所以我們必須找到解密之后的源程序APK的Application類,運行它的onCreate方法,這樣源程序APK才開始它的運行生命周期。后面會說如何得到源程序APK的Application類:使用meta標簽進行設置。
下面看一下整體流程:
下面看一下代碼: ProxyApplication.java
- 得到殼程序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<srcdata.length;i++){ srcdata[i] = (byte)(0xFF ^ srcdata[i]); } return srcdata; }
- 找到源程序的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<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對象的。 下面來看一下AndroidManifest.xml文件中的內容:
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:name="com.example.packapk.ProxyApplication"> <meta-data android:name="APPLICATION_CLASS_NAME" android:value="com.example.sourceapk.MyApplication"/>
這里我們定義了源程序APK的Application類名。 項目下載
運行程序
下面就看看程序的運行步驟:
- 第一步:得到源程序APK文件和殼程序的DEX文件 運行源程序和殼程序項目,之后得到這兩個文件(將殼程序的classes.dex文件改名為SourceApk.dex),然后使用加密工具進行加殼。
- 第二步:替換殼程序中的classes.dex文件 我們在第一步中得到加殼之后的classes.dex文件之后,將其與PackApk.apk中的原classes.dex文件替換。
- 第三步:在第二步的時候得到替換之后的PackApk.apk文件,這個文件因為被修改了,所以我們需要重新對它簽名,不然運行也是報錯的。 簽名之后的文件就可以運行了,效果如下:
轉發+私信,可以免費獲取Android面試資料