轉載:APK自我保護方法

標 題: APK自我保護方法
做 者: MindMac
時 間: 2013-12-28,21:41:15
鏈 接: http://bbs.pediy.com/showthread.php?t=183116
php

APK 的自我保護

 

MindMac
2013/12/28 


因爲 Android 應用程序中的大部分代碼使用 Java 語言編寫,而 Java 語言又比較容易進
行逆向,因此 Android 應用程序的自我保護具備必定的意義。本文總結了 Android 中可使
用的一些 APK 自我保護的技術,大部分都通過實際的代碼測試。

Dex 文件結構

classes.dex 文件是 Android 系統運行於 Dalvik Virtual Machine 上的可執行文件,也是
Android 應用程序的核心所在,因此咱們首先來看下 DEX 文件的結構,這樣可以更好的理解
後續的分析,須要更加詳細的信息,能夠參考 Google 關於 Dex 的技術文檔
從 Java 源文件(固然 Android 也支持 JNI 的調用方式)到生成 Dex 文件的基本映射關係
如圖 1 所示,Java 源文件經過 Java 編譯器生成 class 文件,再經過 dx 工具轉換爲 classes.dex
文件。Dex 文件從總體上來看是個索引的結構,類名、方法名、字段名等信息都存儲在常量
池中,這樣可以充分減小存儲空間,一個 Dex 文件的基本結構如圖 2 所示,相關結構聲明定
義在 DexFile.h 中,在 AOSP 中的路徑爲/dalvik/libdex/DexFile.h。
名稱:  1.png查看次數: 10文件大小:  43.0 KB
圖 1 Java 源文件生成 Dex 文件的映射關係

header: Dex 文件頭,包含 magic 字段、adler32 校驗值、SHA-1 哈希值、string_ids 的個數
名稱:  2.png
查看次數: 6
文件大小:  20.5 KB
圖 2 Dex 文件基本結構

以及偏移地址等。Dex 文件頭結構固定,佔用 0x70 個字節,定義以下所示。

java

代碼:
1. struct DexHeader {

2. u1 magic[8]; /* includes version number */

3. u4 checksum; /* adler32 checksum */

4. u1 signature[kSHA1DigestLen]; /* SHA-1 hash */

5. u4 fileSize; /* length of entire file */

6. u4 headerSize; /* offset to start of next section */

7. u4 endianTag;

8. u4 linkSize;

9. u4 linkOff;

10. u4 mapOff;

11. u4 stringIdsSize;

12. u4 stringIdsOff;

13. u4 typeIdsSize;

14. u4 typeIdsOff;

15. u4 protoIdsSize;

16. u4 protoIdsOff;

17. u4 fieldIdsSize;

18. u4 fieldIdsOff;

19. u4 methodIdsSize;

20. u4 methodIdsOff;

21. u4 classDefsSize;

22. u4 classDefsOff;

23. u4 dataSize;

24. u4 dataOff;

25. };

DexStringId: 定義了字符串數據的偏移, stringDataOff 指向字符串數據;android

代碼:
1. struct DexStringId {
 
2. u4 stringDataOff; /* file offset to string_data_item */

3. };

DexTypeId: 表示應用程序代碼中使用到的具體類型,如整型、字符串等,在 Dalvik 字節 
碼中表示爲 I、Ljava/lang/String;,descriptorIdx 指向 DexStringId 列表的索引;數組

代碼:
1. struct DexTypeId {

2. u4 descriptorIdx; /* index into stringIds list for type descriptor */

3. };

DexProtoId:表示方法聲明的結構體,shortyIdx 是方法聲明字符串,格式爲返回值類型
後緊跟參數列表類型,如方法聲明爲 VI,表示返回值爲 V(空,無返回值),參數爲 I
(整型),全部的引用類型用 L 表示;returnTypeIdx 指向 DexTypeId 列表的索引,表示返
回值類型;parametersOff 指向 DexTypeList 的偏移,表示參數列表類型;服務器

代碼:
1. struct DexProtoId {
 
2. u2 classIdx; /* index into typeIds list for defining class */

3. u2 typeIdx; /* index into typeIds for field type */

4. u4 nameIdx; /* index into stringIds for field name */

5. };

DexFieldId: 表示代碼中的字段,classIdx 指向 DexTypeId 列表索引,表示字段所屬的類;
typeIdx 表示字段類型,nameIdx 指向 DexStringId 列表索引,表示字段名;cookie

代碼:
1. struct DexFieldId {
 
2. u2 classIdx; /* index into typeIds list for defining class */

3. u2 typeIdx; /* index into typeIds for field type */

4. u4 nameIdx; /* index into stringIds for field name */

5. };

DexMethodId: 表示代碼中使用的方法, classIdx 表示方法所屬的類, protoIdx 指向
DexProtoId 列表索引,表示方法原型,nameIdx 表示方法名;網絡

代碼:
1. struct DexMethodId {

2. u2 classIdx; /* index into typeIds list for defining class */

3. u2 protoIdx; /* index into protoIds for method prototype */

4. u4 nameIdx; /* index into stringIds for method name */

5. };

DexClassDef: 該結構相對要複雜一些,定義了代碼中的使用的類,以及相關的代碼指令。app

代碼:
1. struct DexClassDef {

2. u4 classIdx; /* index into typeIds for this class */

3. u4 accessFlags;

4. u4 superclassIdx; /* index into typeIds for superclass */

5. u4 interfacesOff; /* file offset to DexTypeList */

6. u4 sourceFileIdx; /* index into stringIds for source file name */

7. u4 annotationsOff; /* file offset to annotations_directory_item */

8. u4 classDataOff; /* file offset to class_data_item */

9. u4 staticValuesOff; /* file offset to DexEncodedArray */

10. };

classIdx 指向 DexTypeId 列表索引,表示該類的類型;accessFlags 是類的訪問標誌,如
public,private,static 等;superclassIdx 表示父類的類型;interfacesOff 指向一個 DexTypeList
的偏移值,由於 Java 中能夠實現多個接口,這裏使用列表也就不難理解了;sourceFileIdx
指向 DexStringIdx 列表的索引,表示類所在的源文件名稱;annotationsOff 指向註解目錄
結構;classDataOff 指向 DexClassData 結構,表示類的數據部分;staticValuesOff 表示類
中的靜態數據。

DexClassData 結構體定義在 DexClass.h 文件中,路徑爲/dalvik/libdex/DexClass.h,聲明如
下,header 中包含靜態字段個數,實例字段個數,直接方法(經過類直接訪問的方法)
個數,虛方法(經過類實例訪問的方法)個數;框架

代碼:
1. struct DexClassData {

2. DexClassDataHeader header;

3. DexField* staticFields;

4. DexField* instanceFields;

5. DexMethod* directMethods;

6. DexMethod* virtualMethods;

7. };

DexField 表示字段的類型和訪問標誌, fieldIdx 指向 DexFieldId;koa

代碼:
1. struct DexField {

2. u4 fieldIdx; /* index to a field_id_item */

3. u4 accessFlags;

4. };

DexMethod 結構描述了方法的原型、名稱、訪問標誌以及代碼指令的偏移地址,methodIdx
指向 DexMethodId 索引,須要注意的是在 Google 的 Dex 文件文檔中對此的定義:
index into the method_ids list for the identity of this method (includes the name and descriptor),
represented as a difference from the index of previous element in the list. The index of the first
element in a list is represented directly.


注意紅色字體部分,表示的是在 Dex 文件中,methodIdx 是相對於前一個 DexMethod 中
的 methodIdx 的增量,例如若是一個類中有兩個 directMethods,第一個 directMethod 的
methodIdx 值爲 0x13,表示指向索引爲 0x13 的 methodIdx,那麼第二個 directMethod 的
methodIdx 的值是相對於前一個值的增量,例如 0x01,表示指向索引爲 0x14 的 methodIdx;
accessFlags 爲方法的訪問標誌,codeOff 表示指令代碼的偏移地址;

代碼:
1. struct DexMethod {

2. u4 methodIdx; /* index to a method_id_item */

3. u4 accessFlags;

4. u4 codeOff; /* file offset to a code_item */

5. };

DexCode 的結構體聲明以下。

代碼:
1. struct DexCode {

2. u2 registersSize;

3. u2 insSize;

4. u2 outsSize;

5. u2 triesSize;

6. u4 debugInfoOff; /* file offset to debug info stream */

7. u4 insnsSize; /* size of the insns array, in u2 units */

8. u2 insns[1];

9. /* followed by optional u2 padding */

10. /* followed by try_item[triesSize] */

11. /* followed by uleb128 handlersSize */

12. /* followed by catch_handler_item[handlersSize] */

13. };

須要注意的是,在 DexClass.h 中,全部的 u4 類型,其實是 uleb128 類型。每一個 uleb128
類型是 leb128 的無符號類型,每一個 leb128 類型的數據包含 1-5 個字節,表示一個 32bit
的數值。每一個字節只有 7 位有效,最高一位用來表示是否須要使用到下一個字節,好比
若是第一個字節最高位爲 1,表示還須要使用到第 2 個字節,若是第二個字節的最高位
爲 1,表示會使用到第 3 個字節,以此類推,最多 5 個字節。對於一個 2 個字節的 leb128
類型數據,其結構如圖 3 所示。
名稱:  3.png
查看次數: 7
文件大小:  19.2 KB
圖 3 兩字節的 leb128 類型數據格式

Dex 中方法的隱藏

此部份內容可參考 Playing Hide and Seek with Dalvik Executables

前文分析了 Dex 文件的結構,根據 Dex 的文件結構,能夠實現對 Dex 中特定方法的隱藏,
這樣在使用 baksamli 或者 apktool 工具對 classes.dex 文件進行反彙編時,沒法發現隱藏的方
法,不過會有特定的現象發生,其實也是比較容易檢測出來的。

在 Dex 文件格式分析中關於 method 的結構體是 DexMethod,若是將 methodIdx 的值指向
另外一個 method,同時修改相應的代碼偏移量 codeOff(accessFlags 通常不須要修改),修改
後續相應的 methodIdx,則能夠實現特定方法的隱藏。對 Dex 文件修改後須要從新計算 Dex
文件的 SHA1 值以及校驗值,用來更新 Dex 文件。

隱藏方法的步驟以下:
修改 Dex 文件中須要隱藏方法的 DexMethod 結構體,如圖 4 所示,圖中隱藏了方法
B。具體包括: 

  • 將 DexMethod 的 methodIdx 值設爲 0x0,至關於將原先的方法指向了前一個方
    法;
  • 訪問標誌符 accessFlags 通常不須要修改,在 Dex 文件格式裏,directMethods
    和 virtualMethods 是分開的;
  • 將 codeOffset 設置爲前一個方法的代碼偏移地址。
  • 更新需隱藏方法的下一個方法的 methodIdx,可使用公式:
    next_method_idx=original_next_method_idx + original_method_idx

名稱:  4.png
查看次數: 2
文件大小:  56.7 KB

  • 從新計算 Dex 的 SHA1 哈希值和 Adler 校驗值,並用以更新 DexHeader,可使用
    DexFixer 修復 classes.dex 文件;
  • 從新打包生成 APK 文件:
    • 將 APK 解壓縮,提取其中出 META-INF 文件夾以外的全部文件;
    • 壓縮成 Zip 格式文件;
    • 使用 jarsigner 或者其餘工具對生成的 Zip 文件簽名,後綴名修改爲.apk。

    隱藏的方法仍然須要在程序中進行調用,調用隱藏方法的步驟以下:
  • 使用反射調用 android.content.res.AssetManager.openNonAsset 方法打開當前應用程
     序 的 classes.dex 文 件 , 將 數 據 保 存 到 內 存 中 ; 還 可 以 通 過 調 用
    Context.getPackageCodePath()來得到當前應用程序對應的 apk 文件的路徑,利用此
    路 徑 構 造 ZipFile 對 象 , 進 而 獲 取 classes.dex 的 ZipEntry , 利 用 ZipFile 的
    getInputStream(ZipEntry)方法獲取 classes.dex 的數據流,核心代碼以下所示;
代碼:
1. String apkPath = this.getPackageCodePath();

2. ZipFile apkfile = new ZipFile(apkPath);

3. ZipEntry dexentry = zipfile.getEntry("classes.dex");

4. InputStream dexstream = zipfile.getInputStream(dexentry);
  • 修復 Dex 文件,將以前隱藏方法的 DexMethod 結構體恢復;
  • 將修復後的 Dex 數據使用類加載器從新加載;
  • 搜索被隱藏的方法;
  • 調用被隱藏的方法。


須要注意的是,方法在 Dex 文件中是按方法名的字典序排序的,因此須要隱藏的方法如
果是該類中全部方法排序第一個的話,那麼 methodIdx 值是個絕對值,若是要隱藏的話就不
是很方便,因此建議能夠寫個無用的方法,其方法名排序爲第一個,讓須要隱藏的方法從新
指向該方法。

使用修改 methodIdx 的方法,讓其指向另外一個 DexMethodId 的結構體,若是使用 baksmali
進行反彙編,則會發如今一個類中有兩個徹底相同的函數。

那有沒有更加隱蔽的手段來隱藏一個方法了?考慮到在 DexClassData 結構體中的
DexClassDataHeader 頭部,其中 directMethodsSize 和 virtualMethodsSize 分別表示直接方法個
數和虛方法個數,所以若是但願隱藏某個方法,能夠經過將相應的 directMethodsSize 或
virtualMethodsSize 減 1,同時將表示該須要隱藏方法的 DexMethod 結構體中的數據所有修改
爲 0,這樣就能夠將該方法隱藏起來,使用 baksmali 反彙編時,不會顯示出該方法的反彙編
代碼,具體能夠參考 Hashdays 2012 Android Chanllenge

固然,上述這兩種隱藏方法,都沒能隱藏掉 DexMethodId 結構體,這個結構體中包含了
方法所屬的類名、原型聲明以及方法名,因此能夠經過對比 DexMethodId 的個數和 DexMethod
結構體的個數來判斷是否存在方法隱藏的問題。

Dex 完整性校驗

classes.dex 在 Android 系統上基本負責完成全部的邏輯業務,所以不少針對 Android 應用
程序的篡改都是針對 classes.dex 文件的。在 APK 的自我保護上,也能夠考慮對 classes.dex
文件進行完整性校驗,簡單的能夠經過 CRC 校驗完成,也能夠檢查 Hash 值。因爲只是檢查
classes.dex,因此能夠將 CRC 值存儲在 string 資源文件中,固然也能夠放在本身的服務器上,
經過運行時從服務器獲取校驗值。基本步驟以下:
 

  • 首先在代碼中完成校驗值比對的邏輯,此部分代碼後續不能再改變,不然 CRC 值
    會發生變化;
  • 從生成的 APK 文件中提取出 classes.dex 文件,計算其 CRC 值,其餘 hash 值相似;
  • 將計算出的值放入 strings.xml 文件中。

核心代碼以下:

代碼:
1. String apkPath = this.getPackageCodePath();

2. Long dexCrc = Long.parseLong(this.getString(R.string.dex_crc));

3. try {

4. ZipFile zipfile = new ZipFile(apkPath);

5. ZipEntry dexentry = zipfile.getEntry("classes.dex");

6. if(dexentry.getCrc() != dexCrc){

7. System.out.println("Dex has been *modified!");

8. }else{

9. System.out.println("Dex hasn't been modified!");

10. }

11. } catch (IOException e) {

12. // TODO Auto-generated catch block

13. e.printStackTrace();

14. }

可是上述的保護方式容易被暴力破解, 完整性檢查最終仍是經過返回 true/false 來控制
後續代碼邏輯的走向,若是攻擊者直接修改代碼邏輯,完整性檢查始終返回 true,那這種方
法就無效了,因此相似文件完整性校驗須要配合一些其餘方法,或者有其餘更爲巧妙的方式
實現?

APK 完整性校驗

雖然 Android 程序的主要邏輯經過 classes.dex 文件執行,可是其餘文件也會影響到整個
程序的邏輯走向,以上述 Dex 文件校驗爲例,若是程序依賴 strings.xml 文件中的某些值,則
修改這些值就會影響程序的運行,因此進一步能夠整個 APK 文件進行完整性校驗。可是如
果對整個 APK 文件進行完整性校驗,因爲在開發 Android 應用程序時,沒法知道完整 APK 文
件的 Hash 值,因此這個 Hash 值的存儲沒法像 Dex 完整性校驗那樣放在 strings.xml 文件中,
因此能夠考慮將值放在服務器端。核心代碼以下:

代碼:
1. MessageDigest msgDigest = null;

2. try {

3. msgDigest = MessageDigest.getInstance("MD5")

4. byte[] bytes = new byte[8192];

5. int byteCount;

6. FileInputStream fis = null;

7. fis = new FileInputStream(new File(apkPath));

8. while ((byteCount = fis.read(bytes)) > 0)

9. msgDigest.update(bytes, 0, byteCount);

10. BigInteger bi = new BigInteger(1, msgDigest.digest());

11. String md5 = bi.toString(16);

12. fis.close();

13. /*

14. 從服務器獲取存儲的 Hash 值,並進行比較

15. */
  
16. } catch (Exception e) {

17. e.printStackTrace();

18. }

Java 反射

Android 應用程序開發主要使用 Java 語言,Java 中可使用反射技術來更加靈活地控制
程序的運行,爲 Java 運行時的行爲提供了強大的支持。Java 反射機制容許運行中的 Java 程
序對自身進行檢查,並能直接操做程序的內部屬性或方法,可動態生成類實例、變動屬性內
容以及調用方法。關於 Java 反射更詳細內容能夠參考 Java programming dynamics, Part 2:
Introducing reflection


在 Android 中使用反射技術來動態調用方法,能夠增長對應用程序進行靜態分析的難度。
如下代碼是使用 Java 反射的一個簡單例子,須要使用反射調用的方法存在於 Reflection 類中。

代碼:
1. public class Reflection {

2. public void methodA(){

3. System.out.println("Invoke methodA");

4. }

5. public void methodB(){

6. System.out.println("Invoke methodB");

7. }

8. }

如下代碼完成對 Reflection 類中方法的直接調用和反射調用。

代碼:
1. protected void onCreate(Bundle savedInstanceState) {

2. ......

3. Reflection reflection = new Reflection();

4. reflection.methodA();

5. reflection.methodB();

6. 

7. Class[] consTypes = new Class[]{};

8. Class reflectionCls = null;

9. String className = "com.example.reflection.Reflection";

10. String methodName = "methodA";

11. try {

12. reflectionCls = Class.forName(className);

13. Constructor cons = reflectionCls.getConstructor(consTypes);

14. Reflection reflectionIns = (Reflection) cons.newInstance(new Object[]{});

15. Method method = reflectionCls.getDeclaredMethod(methodName, new Class[]{});

16. method.invoke(reflectionIns, new Object[]{});

17. } catch (Exception e) {

18. // TODO Auto-generated catch block

19. e.printStackTrace();

20.    }

21. }

固然以上 Java 反射的例子過於簡單,使用 dex2jar 反編譯後,用 jd-gui 打開,仍是可以很容
易的識別出須要調用的方法,如圖 5 所示。
名稱:  5.png
查看次數: 5
文件大小:  30.2 KB
圖 5 使用 dex2jar+jd-gui 反編譯結果

因此須要進一步採起措施增長靜態分析的難度。反射調用須要獲取調用的類名和方法名,而
上述代碼將須要調用的類名或方法硬編碼在代碼中,一方面違背了 Java 反射使用的場景,
Java 反射主要是爲了提供程序的運行時動態行爲的控制,另外一方面並無增長了靜態分析
的難度。

能夠根據程序運行過程當中的實時狀態來調用相應的方法,從而進一步提升靜態分析的難
度。一個可能的應用場景是:根據當前應用程序的狀態,從網絡服務器獲取須要進行反射調
用的方法以及參數信息。例如對於上述例子,類名和方法名均可以從網絡獲取。這樣作的好
處是使得僅僅經過靜態分析沒法獲知程序運行過程當中實際調用的方法,也會增長自動化分析
的難度。也可使用反射加密的方式,將類名、方法名作加密處理,在實際調用時再進行解
密。固然以上兩種處理方式可能對性能有較大影響(自己 Java 反射對性能就有必定影響),
不該該頻繁使用,並且必須申請網絡鏈接的權限(不過如今凡是個 Android 應用程序,不申
請個網絡鏈接權限都很差意思說本身是個 Android 應用)同時還得須要接入網絡。


動態加載

Android 系統提供了 DexClassLoader 來支持在程序運行過程當中動態加載包含 classes.dex
的.jar 或者.apk 文件,若是再結合 Java 反射技術,能夠實現執行非應用程序部分的代碼。利
用動態加載技術,能夠提供逆向分析的難度,在必定程度上能夠保護 APK 自身的業務邏輯
防止被破解。

DexClassLoader 的構造函數原型以下:

代碼:
1. public DexClassLoader (String dexPath, String optimizedDirectory, String
libraryPath, ClassLoader parent)

其中,dexPath 爲包含 dex 文件的.apk 或者.jar 路徑,optimizedDirectory 是優化後的 dex 文件
的路徑,libraryPath 表示 Native 庫的路徑,parent 是父類加載器。經過 DexClassLoader 實例
化對象,調用 loadClass 加載須要調用的類,得到 Class 對象後,就能夠進一步使用 Java 反
射技術來調用相應的方法。以下:

代碼:
1. DexClassLoader classLoader = new DexClassLoader(apkPath, dexPath, null,

getClassLoader());

2. try {

3. Class<?> mLoadClass =

classLoader.loadClass("com.example.dexclassloaderslave.DexSlave");

4. Constructor<?> constructor = mLoadClass.getConstructor(new Class[] {});

5. Object dexSlave = constructor.newInstance(new Object[] {});

6. Method sayHello = mLoadClass.getDeclaredMethod("sayHello", new Class[]{} );

7. sayHello.setAccessible(true);

8. sayHello.invoke(dexSlave, new Object[]{});

9. } catch (Exception e)

10. {
 
11. e.printStackTrace();
 
12. }

上述代碼實現調用 com.example.dexclassloaderslave.DexSlave 類中的 sayHello 方法。

對於須要經過 DexClassLoader 被調用的.apk 或者.jar 文件的分發,能夠將其放入 Android
項目的 assets 或者 res 目錄下,也能夠將其放在服務器端,在實際須要調用時經過網絡獲取
文件。爲了提升逆向的難度,能夠對被調用的.apk 或者.jar 文件採起如下措施進行進一步的
保護:
 

  • 進行完整性校驗,防止文件被篡改;
  • 進行加密處理,在調用加載前進行解密;
  • 對須要調用的函數相關信息使用經過網絡獲取的方式,而不是硬編碼在代碼中,可
    以真正實現動態調用,提升靜態分析的難度;
  • 對於使用網絡服務器分發的方式,注意對網絡服務器地址的保護,不要以字符串硬
    編碼的方式寫在代碼中,對下載請求也須要使用 cookie 等輔助識別的技術。


除了使用 DexClassLoader 類實現動態加載外,還可使用 dalvik.system.DexFile 類實現
Dex 文件的加載,可是 DexFile 類提供的構造方法在實例化過程當中須要在/data/davik-cache 目
錄下生成相應的 Dex 文件,而/data/davik-cache 目錄對於通常應用程序是沒有寫權限的,所
以在程序中沒法實例化 DexFile 對象,也就沒法調用 DexFile.loadClass 方法。因此須要經過反
射調用 DexFile 類的 openDex 方法,具體能夠參考該代碼中 invokeHidden 函數。

字符串處理

Android 應用程序開發中不免會使用到字符串,如服務器的地址等一些敏感信息,對於
這些字符串若是使用硬編碼的方式,容易經過靜態分析獲取,甚至可使用自動化分析工具
批量提取。例如若在 Java 源代碼中定義一個字符串以下:

代碼:
1. String str = "I am a string!";

則在反編譯的.smali 代碼中對應的代碼以下(寄存器可能會有區別):

代碼:
1. const-string v0, "I am a string!"

對於自動化分析工具,只須要掃描到 const-string 關鍵字就能夠提取到字符串值。所以應該
儘可能避免在源代碼中定義字符串常量,比較簡單的作法可使用 StringBuilder 類經過 append
方法來構造須要的字符串,或者使用數組的方式來存儲字符串。使用 StringBuilder 構造字符
串反編譯後的代碼以下,使用這種方式能夠增長自動化分析的難度,若是想要完整提取一個
字符串,若是僅僅採用靜態分析方法就必需要進行相應的詞法語法解析了。

代碼:
1. .line 26

2. .local v10, strBuilder:Ljava/lang/StringBuilder;

3. const-string v11, "I"

4. invoke-virtual {v10, v11},

 Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

5. .line 27

6. const-string v11, "am"

7. invoke-virtual {v10, v11},

Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

8. .line 28

9. const-string v11, "a"

10. invoke-virtual {v10, v11},

Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

11. .line 29

12. const-string v11, "String"

13. invoke-virtual {v10, v11},

Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

14. .line 30

15. invoke-virtual {v10}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

另外也能夠對字符串進行加密處理,不少惡意代碼就採用了此種方法,例如一些具備
bot 功能的惡意代碼會將 C&C 服務器地址以及命令進行加密處理,運行時再進行解密。

代碼亂序

爲了增長逆向分析的難度,能夠將原有代碼在 smali 格式上進行亂序處理同時又不會影
響程序的正常運行。亂序的基本原理如圖 6 所示,將指令從新佈局,並給每塊指令賦予一個
label,在函數開頭處使用 goto 跳到原先的第一條指令處,而後第一條指令處理完,再跳到
第二條指令,以此類推。
名稱:  6.png
查看次數: 3
文件大小:  16.4 KB
圖 6 代碼亂序的基本原理

以兩個整數相加爲例,Java 代碼以下所示:

代碼:
1. public void test(){

2. int a = 1;

3. int b = 2;

4. int c = a + b;

5. System.out.println(c);

6. }

反編譯後的 smali 代碼以下所示

代碼:
1. .method public test()V

2. .locals 4

3.

4. .prologue

5. .line 24

6. const/4 v0, 0x1

7.

8. .line 25

9. .local v0, a:I

10. const/4 v1, 0x2

11.

12. .line 26

13. .local v1, b:I

14. add-int v2, v0, v1

15.

16. .line 27

17. .local v2, c:I

18. sget-object v3, Ljava/lang/System;->out:Ljava/io/PrintStream;

19.

20. invoke-virtual {v3, v2}, Ljava/io/PrintStream;->println(I)V

21.

22. .line 28

23. return-void

咱們能夠根據上述提到的代碼亂序原理,將 test 這個函數亂序成以下代碼所示(刪除了.line):

代碼:
1. .method public test()V

2. .locals 4

3.

4. .local v2, c:I

5. goto :lab1

6. :lab3

7. sget-object v3, Ljava/lang/System;->out:Ljava/io/PrintStream;

8. invoke-virtual {v3, v2}, Ljava/io/PrintStream;->println(I)V

9. goto :end

10. 

11. .local v1, b:I

12. :lab2

13. add-int v2, v0, v1

14. goto :lab3

15. 

16. .local v0, a:I

17. :lab1

18. const/4 v0, 0x1

19. const/4 v1, 0x2

20. goto :lab2

21. 

22. :end

23. return-void
 
24. .end method

最後使用 apktool 從新打包發佈。進行代碼亂序能夠在必定程度上增長逆向分析的難度,例
如可使用 dex2jar+jd-GUI 工具來分析上述亂序先後的代碼。亂序前代碼如圖 7 所示:
名稱:  7.png
查看次數: 2
文件大小:  12.5 KB
圖 7 使用 jd-GUI 分析亂序前代碼

亂序後的代碼如圖 8 所示。從亂序先後的代碼能夠看出,使用代碼亂序技術可以在必定程
度上增長逆向分析的難度,固然這是由於 dex2jar 工具在進行代碼解析時的問題,若是可以
針對性的處理這種代碼亂序的狀況,那麼這種反編譯的狀況應該會有所好轉。
關於代碼亂序的技術,能夠參考 ADAM:An automatic and extensible platform to stress test android anti-virus systems 、 DroidChameleon:Evaluating  Android Anti-malware against Transformation Attacks
名稱:  8.png
查看次數: 3
文件大小:  20.3 KB
圖 8 使用 jd-GUI 分析亂序後代碼

模擬器檢測

般在分析 APK 的過程當中會藉助於 Android 模擬器,好比分析網絡行爲,動態調試等。
所以從 APK 自我保護的角度出發,能夠增長對 APK 當前運行環境的檢測,判斷是否運行在
模擬器中,若是運行在模擬器中能夠選擇退出整個應用程序的執行或者跳到其餘分支。模擬
器檢測的手段有不少,下面逐一分析。

1.屬性檢測

Android 屬性系統相似於 Windows 的註冊表機制,全部的進程能夠共享系統設置值。關
於 Android 屬 性 系 統 的 詳 細 原 理 , 可 以 參 考 我 對 於 Android 屬 性 系 統 的 分 析 文 章
http://www.kanxue.com/bbs/showthread.php?t=182901。一些屬性值在 Android 模擬器和真機上
是不一樣的,例如對於 Nexus4 和 SDK 爲 4.1.2 的模擬器來講,Build.BRAND 和 Build.DEVICE 屬
性值分別如圖 9 和圖 10 所示。根據這些屬性值在真實機器和模擬器上的差異能夠比較容易

名稱:  9.png
查看次數: 8
文件大小:  12.3 KB
圖 9 Nexus4 的 BRAND 和 DEVICE 值
名稱:  10.png
查看次數: 5
文件大小:  8.4 KB
圖 10 Android 模擬器的 BRAND 和 DEVICE 值

檢測 Android 應用程序是否運行在模擬器中。不過對於這種檢測方式,繞過也是比較容易的,
我如今想到的有 3 種方式繞過:

  • 在源碼中修改相應的屬性值,從新編譯生成內核等鏡像文件,再使用這些從新生成
    的鏡像文件加載模擬器(對於 BRAND 屬性值能夠修改/build/target/product/generic.mk
    文件中的 PRODUCT_BRAND 值,從新編譯過程沒測試,是否是隻須要修改這個值就
    能搞定不必定正確,能夠參考下 build.prop 生成過程分析);
  • 修改 boot.img 文件;
  • 使用 Xposed 框架,能夠 hook SystemProperties.get 函數,在 before 函數中檢查須要 
    獲取的屬性,根據狀況修改對應的值,而後返回;
    另外還能夠經過檢測 IMEI,IMSI 等值來判斷是不是模擬器,在模擬器中,這兩個值默認
    分別是 000000000000000 和 310260000000000。經過如下代碼能夠獲取 IMSI 值:
代碼:
1. TelephonyManager manager = (TelephonyManager)getSystemService(TELEPHONY_SERVICE);

2. String imsi = manager.getSubscriberId();

TELEPHONY_SERVICE 須要申請 android.permission.READ_PHONE_STATE 權限。一樣咱們能夠有
相應的繞過方式,一個相對簡單的方法是直接修改 Android SDK 下/tools/emulator-arm.exe 文
件(Windows 版本)。使用 010Editor 打開 emulator-arm.exe 文件,搜索 CIMI,如圖 11,」CIMI.」
後面的 15 位數字值是 IMSI,」CGSN.」後面的 15 位數字爲 IMEI,修改這兩個值(確保沒有運行
模擬器),而後保存。
名稱:  11.png
查看次數: 4
文件大小:  106.1 KB
圖 11 修改 IMSI 和 IMEI

修改後再運行模擬器,此時查看 IMSI 值如所示,IMEI 值如所示,可見能夠成功修改這兩個
值。
名稱:  12.png
查看次數: 2
文件大小:  6.5 KB
圖 12 修改後的 IMSI 值

名稱:  13.png
查看次數: 4
文件大小:  12.8 KB
圖 13 修改後的 IMEI 值

還存在其餘的修改方式,能夠參考 Hide the Emulator以及 Android emulator patch for configurable
IMEI, IMSI and SIM card serial number

固然還能夠檢查一些其餘的值,如電池的電池狀態、電池電量,Secure.ANDROID_ID,
DeviceId,手機號碼等。

2.虛擬機文件檢測

相 對 於 真 實 設 備 , Android 模 擬 器 中 存 在 一 些 特 殊 的 文 件 或 者 目 錄 , 如
/system/bin/qemu-props,該可執行文件能夠用來在模擬器中設置系統屬性。另外還有
/system/lib/libc_malloc_debug_qemu.so 文件以及/sys/qemu_trace 目錄。咱們能夠經過檢測這些
特殊文件或者目錄是否存在來判斷 Android 應用程序是否運行在模擬器中,關鍵代碼以下:

代碼:
1. private static String[] known_files = {

2. "/system/lib/libc_malloc_debug_qemu.so",

3. "/sys/qemu_trace",

4. "/system/bin/qemu-props"
 
5. };

6.

7. public static boolean hasQEmuFiles() {

8. for(String pipe : known_files) {

9. File qemu_file = new File(pipe);

10. if (qemu_file.exists())

11. return true;

12. }

13. return false;

14. }

更完整的代碼能夠參考 Tim Strazzere 的 Github 中 anti-emulator,該項目中還列舉了其餘一些
模擬器檢測的方法,如檢測 socket 文件/dec/socket/qemud。

3.基於 Cache 行爲的模擬器檢測方法

BlueBox 關於 Android 模擬器檢測的方法
http://bluebox.com/corporate-blog/android-emulator-detection/

4.基於代碼指令執行的模擬器檢測方法

DexLabs 關於 Android 模擬器檢測的方法
http://dexlabs.org/blog/btdetect

5.其餘方法

其餘一些檢測方法,能夠參考以下文獻:

  • DISSECTING THE ANDROID BOUNCER
  • 逃離安卓動態檢測
  • Guns and Smoke to Defeat Mobile Malware
  • DEX EDUCATION 201 ANTI-EMULATION
  • INSECURE MAGZINE 34 – Introduction to Android malware analysis


APK 僞加密

APK 其實是 Zip 壓縮文件,可是 Android 系統在解析 APK 文件時,和傳統的解壓縮軟
件在解析 Zip 文件時仍是有所差別的,利用這種差別能夠實現給 APK 文件加密的功能。Zip
文件格式能夠參考 MasterKey 漏洞分析的一篇文章。在 Central Directory 部分的 File Header 頭
文件中,有一個 2 字節長的名爲 General purpose bit flags 的字段,這個字段中每一位的做用
能夠參考 Zip 文件格式規範的 4.4.4 部分,其中若是第 0 位置 1,則表示 Zip 文件的該 Central
Directory 是加密的,若是使用傳統的解壓縮軟件打開這個 Zip 文件,在解壓該部分 Central
Directory 文件時,是須要輸入密碼的,如圖 14 所示。可是 Android 系統在解析 Zip 文件時並
沒有使用這一位,也就是說這一位是否置位對 APK 文件在 Android 系統的運行沒有任何影響。
通常在逆向 APK 文件時,會首先使用 apktool 來完成資源文件的解析,dex 文件的反彙編工
做,但若是將 Zip 文件中 Central Directory 的 General purpose bit flags 第 0 位置 1 的話,
apktool(version:1.5.2)將沒法完成正常的解析工做,如圖 15 所示,可是又不會影響到 APK 在
Android 系統上的正常運行,如圖 16 所示。
名稱:  14.png
查看次數: 3
文件大小:  17.5 KB
圖 14 傳統解壓縮軟件須要輸入密碼進行解壓縮

名稱:  15.png
查看次數: 4
文件大小:  37.4 KB
圖 15 apktool 解析僞加密的 APK 文件失敗

對 APK 文件進行僞加密可使用這個腳本,在 Python 的 zipfile 模塊中,ZipInfo 類中記
錄了 Zip 文件中相應的 Central Directory 的相關信息,包括 General purpose bit flags,在 ZipInfo
類中屬性爲 flag_bits,所以上述腳本中將需加密的 APK 文件的每一個 ZipInfo 的 flag_bits 和 1 作
或操做,實如今 General purpose bit flags 的第 0 位置 1.
而須要去除這些僞加密的標誌的話,可使用這個腳本。相關內容能夠參考 BlueBox 之
前提出的一個 Android Security Analysis Chanllenge.
名稱:  16.png
查看次數: 3
文件大小:  53.2 KB
圖 16 僞加密的 APK 能夠正常運行

代碼:
1. unsigned int gpbf = get2LE(lfhBuf + kLFHGPBFlags);

2. if ((gpbf & kGPFUnsupportedMask) != 0) {

3. ALOGW("Invalid General Purpose Bit Flag: %d", gpbf);

4. return false;

5. }

Manifest Cheating

AndroidManifest.xml 是 Android 應用程序的配置文件,包含了包名、應用程序名稱、申請
的權限信息以及組件信息等。在 Android 應用程序開發,生成 APK 時,aapt 會負責完成資源
的打包,打包會將文本格式的 XML 資源文件編譯成二進制格式的 XML 資源文件。將文本格
式的 XML 文件轉換成二進制格式,一方面經過字符串資源池的統一管理,減小文件體積;
另外一方面二進制格式的 XML 文件解析速度也會更快。在 Android 開發過程當中,生成的 R.java
文件中包含了相應的資源類型、名稱以及對應的 id 值。資源 id 是 32bit 的整型值,格式
爲:0xPPTTNNNN。其中 PP 表示使用該資源的包,TT 表明該資源的類型,而 NNNN 是該類型
中資源的名稱。對於應用程序資源,PP 值固定爲 7f,而對於被引用的系統資源包,其 PP
值爲 01。TT 和 NNNN 通常是 aapt 按照資源出現的順序生成的。更多分析能夠參考羅昇陽的
Android 應用程序資源的編譯和打包過程分析

Manifest Cheating 的基本原理是,在 AndroidManifest 的<application>節點中插入一個未知
id(如 0x0),名稱爲 name 的屬性,其值能夠是一個從未定義實現的 Java 類文件名。而對
AndroidManifest 的修改須要在二進制格式下進行,這樣才能不會破壞以前 aapt 對資源文件的
處理。因爲是未知的資源 id,在應用程序運行過程當中,Android 會忽略此屬性。可是在使用
apktool 進行重打包時,首先會將 AndroidManifest.xml 轉換爲明文,進而會包含名稱爲 name
的屬性,而相應的 id 信息會丟失,apktool 重打包會從新進行資源打包處理,因爲該 name
屬性值是一個未實現的 Java 類,重打包後的應用程序在運行過程當中,因爲 application 節點
中定義的類是先於全部其餘組件運行的,若系統找不到對應的類,會出現運行時錯誤,Dalvik
虛擬機會直接關閉。另外,也能夠實現 name 屬性值對應的 Java 類,若此類被調用,則代表
被重打包了,能夠採起進一步的措施。這樣就能夠起到保護自身 APK 的做用,防止被重打
包。可是這種方法也很容易被繞過,只須要在通過 apktool 解碼的 AndroidManifest 文件中,
去掉在 application 節點中添加的 name 屬性便可。整個過程以下:

  • 將 APK 解壓縮,提取其中的 AndroidManifest.xml 文件;
  • 使用 axml 工具,修改二進制的 AndroidManifest.xml 文件,在 application 節點下插入
    id 未知(如 0x0),名爲 name 的屬性(值能夠任意,只要不對應到項目中的類文件名
    便可,如 some.class);
  • 將除 META-INF 文件夾以外的文件壓縮成 zip 文件,簽名後生成.apk 文件。
    如果攻擊者使用 apktool 重打包,運行重打包後的文件會出現以下運行時錯誤:

名稱:  17.png
查看次數: 3
文件大小:  83.3 KB
圖 17 使用 Manifest Cheating 重打包後 APK 文件運行時錯誤

調試器檢測

在對 APK 逆向分析時,每每會採起動態調試技術,可使用 netbeans+apktool 對反彙編
生成的 smali 代碼進行動態調試。爲了防止 APK 被動態調試,能夠檢測是否有調試器鏈接。
Android 系統在 android.os.Debug 類中提供了 isDebuggerConnected()方法,用於檢測是否有調
試器鏈接。能夠在 Application 類中調用 isDebuggerConnected()方法,判斷是否有調試器鏈接,
若是有,直接退出程序。

除了 isDebuggerConnected 方法,還能夠經過在 AndroidManifest 文件的 application 節點中
加入 android:debuggable=」false」使得程序不可被調試,這樣若是但願調試代碼,則須要修改
該值爲 true,所以能夠在代碼中檢查這個屬性的值,判斷程序是否被修改過,代碼以下:

代碼:
1. if(getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE != 0){
 
2. System.out.println("Debug");

3. android.os.Process.killProcess(android.os.Process.myPid());

4. }

代碼混淆

使用 Java 編寫的代碼很容易被反編譯,所以可使用代碼混淆的方法增長反編譯代碼
閱讀的難度。ProGuard 是一款免費的 Java 代碼混淆工具,提供了文件壓縮、優化、混淆和
審覈功能。在 Eclipse+ADT 開發環境下,每一個 Android 應用程序項目目錄下會默認生成
project.properties 和 proguard-project.txt 文件。若是須要使用 ProGuard 進行壓縮以及混淆,首
先須要在 project.properties 文件中去掉對以下語句的註釋:

代碼:
1. proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt

ProGuard 的相關配置信息須要在 proguard-project.txt 文件中聲明,在其中能夠設置須要混淆
和保留的類或方法。因爲在某些狀況下,ProGuard 會錯誤地認爲某些代碼沒有被使用,如在
只在 AndroidManifest 文件中引用的類,從 JNI 中調用的方法等。對於這些狀況,須要在
proguard-project.txt 文件中添加-keep 命令,用來保留類或方法。關於 ProGuard 更加詳細的配
置項能夠參考 ProGuard Manual
除了使用 ProGuard 對 Android 代碼進行混淆外,還可使用 DexGuard。DexGuard 是特別
針對 Android 的一款代碼優化混淆的收費軟件,提供代碼優化混淆、字符串加密、類加密、
Assets 資源加密、隱藏對敏感 API 的調用、篡改檢測以及移除 Log 代碼。DexGuard 的進一步
分析能夠參考 JEB 上的相關 blog,能夠在這裏總結一下:

1.字符串加密
通過 DexGuard 加固過的 APK,對字符串的訪問會經過調用一個解密函數來完成加密字
符串的解密。如圖 18 所示,紅框中的字節數組是加密後的字符串,在 onCreate 函數中,調
用瞭解密函數進行解密。字符解密函數如圖 19 所示,對其進行處理後如圖 20 所示,加密算
法也很簡單,基本思路是:當前字符由前一個字符加上加密字符數組中的字符,再減去常量
8 造成,當字符長度達到給定的長度時,會最終構成字符串並返回。
名稱:  18.png
查看次數: 5
文件大小:  72.4 KB
圖 18 DexGuard 字符串加密

名稱:  19.png
查看次數: 4
文件大小:  75.2 KB
圖 19 字符串解密函數

名稱:  20.png
查看次數: 2
文件大小:  71.1 KB
圖 20 處理後的解密函數

2.assets 加密
APK 文件的 assets 目錄下包含了應用程序須要使用到的資源文件,DexGuard 提供了對
assets 資源文件的加密功能。對於一個通過保護的 asset 資源文件,例如 1.png 文件,使用
十六進制查看器查看該文件,如圖 21 所示。從圖中可見,加密後的 png 文件,缺失了相應
的文件頭。解密則是首先經過反射調用 AssetManager.open 函數,同時對該函數的反射調用
名稱:  21.png
查看次數: 3
文件大小:  105.6 KB
圖 21 通過加密的 png 文件

又使用了加密處理,最後經過 Cipher 類完成 png 文件的解密。相關解密處理如所示。
名稱:  22.png
查看次數: 5
文件大小:  92.8 KB
圖 22 asset 解密處理

關於 ProGuard 和 DexGuard 還能夠參考 ProGuard and DexGuard,其中除了介紹了 ProGuard
和 DexGuard,還提供了一些 APK 加固處理的方法。
關於代碼混淆,還能夠參考 Android:Game of Obfuscation

NDK

Android 軟件的開發主要使用 Java 語言,可是 Android 也提供了對本地語言 C、C++的支
持。藉助 JNI,能夠在 Java 類中使用 C 語言庫中的特定函數,或在 C 語言程序中使用 Java
類庫。通常來講,若是代碼中對處理速度有較高要求或者爲了更好地控制硬件,抑或者爲了
複用既有的 C/C++代碼,均可以考慮經過 JNI 來實現對 Native 代碼的調用。
因爲逆向 Native 程序的彙編代碼要比逆向 Java 彙編代碼困難,所以能夠考慮在關鍵代
碼部位使用 Native 代碼,如註冊驗證,加解密操做等。一個可能的藉助 Native 代碼保護 APK
的方法是:將核心業務邏輯代碼放入加密的.jar 或者.apk 文件中,在須要調用時使用 Native
代碼進行解密,同時完成對解密後文件的完整性校驗,不過無論是.jar 仍是.apk 文件,解密
後都會留在物理存儲上,爲了不這種狀況,可使用反射技術直接調用
dalvik.system.DexFile.openDex()方法,該方法接受 classes.dex 文件字節流返回 DexFile 對象。
關於 Native 代碼的編寫,能夠參考 Google 官方文檔的 Android NDK

逆向工具對抗

在逆向分析 Android 應用程序時,通常會使用 apktool,baksmali/smali,dex2jar,androguard,
jdGUI 以及 IDA Pro 等。所以能夠考慮使得這些工具在反編譯 APK 時出錯來保護 APK,這些工
具大部分都是開源的,能夠經過閱讀其源代碼,分析其在解析 APK、dex 等文件存在的缺陷,
在開發 Android 應用程序時加以利用。能夠參考 Tim Strazzere 的 Dex Education:Practicing Safe
Dex
,相應的 Demo,看雪上的中文翻譯,不過其中的不少技巧已經失效了。DexLabs 的 Dalvik
Bytecode Obfuscation on Android
 介紹了垃圾字節碼插入的技術。

使用 apktool 進行重打包時,對於後綴爲 png 的文件,會按照 png 格式的文件進行打包
處理,所以若是在項目開發時,有意將一個非 png 格式文件的文件名改成後綴爲 png 的文件,
則使用 apktool 進行重打包時會出錯。能夠利用這種方法來對抗重打包。能夠試試對這個文
使用 apktool 進行重打包,會報不少錯誤,可是這種 appt 致使的錯誤,不少都是因爲第一
個錯誤一塊兒的,如圖 23 所示。從第一個錯誤描述中可知,res/drawable-hdpi/station.png 不是
名稱:  23.png
查看次數: 4
文件大小:  123.5 KB
圖 23 apktool 重打包錯誤

一個 PNG 格式的文件,使用 file 命令,能夠發現其實是一個 Windows icon 文件,如圖 24
所示。將這個文件後綴修改爲.icon 就能夠從新打包了。
名稱:  24.png
查看次數: 4
文件大小:  26.3 KB
圖 24 station.png 的真實文件類型

總結

以上 APK 自我保護的技術並不能作到徹底的保護做用,只是提升了逆向分析的難度,
在實際運用中應該根據狀況多種技術結合使用。這些技術其實不少來源於 Android 惡意代碼,
因此能夠關注 Android 惡意代碼中使用的一些技術來應用到本身開發的 Android 應用程序中。

注:本帖由看雪論壇志願者PEstone 從新將pdf整理排版,若和原文有出入,以原做者附件爲準 



總結的一些關於APK自我保護的方法,固然還有不少其餘的技巧,沒法一一列舉,如今可使用的一些服務包括:
1.梆梆 :http://www.bangcle.com/
2.愛加密 :https://wwws.ijiami.cn/
3.APKProtect :http://www.apkprotect.com/
4.Shield4J :http://shield4j.com/
5.DexGuard :http://www.saikoa.com/dexguard

相關文章
相關標籤/搜索