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 Java 源文件生成 Dex 文件的映射關係

header: Dex 文件頭,包含 magic 字段、adler32 校驗值、SHA-1 哈希值、string_ids 的個數

圖 2 Dex 文件基本結構

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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 指向字符串數據;
1
2
3
4
5
1. struct DexStringId {
  
2. u4 stringDataOff; /*  file  offset to string_data_item */
 
3. };


DexTypeId: 表示應用程序代碼中使用到的具體類型,如整型、字符串等,在 Dalvik 字節 
碼中表示爲 I、Ljava/lang/String;,descriptorIdx 指向 DexStringId 列表的索引;
1
2
3
4
5
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
2
3
4
5
6
7
8
9
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 列表索引,表示字段名;
1
2
3
4
5
6
7
8
9
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
2
3
4
5
6
7
8
9
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: 該結構相對要複雜一些,定義了代碼中的使用的類,以及相關的代碼指令。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
2
3
4
5
6
7
8
9
10
11
12
13
1. struct DexClassData {
 
2. DexClassDataHeader header;
 
3. DexField* staticFields;
 
4. DexField* instanceFields;
 
5. DexMethod* directMethods;
 
6. DexMethod* virtualMethods;
 
7. };


DexField 表示字段的類型和訪問標誌, fieldIdx 指向 DexFieldId;
1
2
3
4
5
6
7
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
2
3
4
5
6
7
8
9
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 兩字節的 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


● 
[*]重新計算 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
2
3
4
5
6
7
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 使用 dex2jar+jd-gui 反編譯結果

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

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

動態加載

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

DexClassLoader 的構造函數原型如下:
1
2
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
1. String str =  "I am a string!" ;


則在反編譯的.smali 代碼中對應的代碼如下(寄存器可能會有區別):
1
1. const-string v0,  "I am a string!"


對於自動化分析工具,只需要掃描到 const-string 關鍵字就可以提取到字符串值。因此應該
儘量避免在源代碼中定義字符串常量,比較簡單的做法可以使用 StringBuilder 類通過 append
方法來構造需要的字符串,或者使用數組的方式來存儲字符串。使用 StringBuilder 構造字符
串反編譯後的代碼如下,使用這種方式可以增加自動化分析的難度,如果想要完整提取一個
字符串,如果僅僅採用靜態分析方法就必須要進行相應的詞法語法解析了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 代碼亂序的基本原理

以兩個整數相加爲例,Java 代碼如下所示:
1
2
3
4
5
6
7
8
9
10
11
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
相關文章
相關標籤/搜索