#0x00 前言 安卓應用加固技術是伴隨安卓應用破解技術一同發展起來的。 安卓應用加固技術的發展可以劃分為以下幾代技術: 第一代殼:Dex混淆、Dex加密、資源加密、反調試及自定義DexClassLoader 第二代殼:Dex抽取與so加固、Dex Method代碼抽取、Dex動態載入及so加固 第 ...
#0x00 前言
安卓應用加固技術是伴隨安卓應用破解技術一同發展起來的。加固技術分為加殼保護和反反彙編保護技術。市面上成熟的加固廠商一般會使用加殼保護技術配合反反彙編技術對安卓應用進行加固。
安卓反反彙編技術的總結在我另一篇博客中將進行詳細探討,鏈接:
http://.................(更新中,未完待續)
安卓應用加殼技術的發展可以劃分為以下幾代殼技術:
第一代殼——動態載入殼:對用戶端的Dex、so、資源進行混淆及加密,在運行時再解密,最後通過自定義ClassLoader動態載入源APK
第二代殼——代碼抽取殼:抽取Dex中方法的實現代碼加密存儲在用戶端,APK運行時調用殼的Native方法解密Dex中被加密的方法,執行完後再加密
第三代殼——代碼混淆殼:指令變換、花指令混淆、指令膨脹、代碼流程混淆等
#0x01 第一代殼
一、Dex、so混淆
1.代碼混淆
即將源程式代碼中的類名、方法名修改為a、b、c()的形式,讓破解這不能通過類名、方法名輕易地尋找/猜測到函數所實現的功能,增加逆向分析的難度
2.編譯期混淆
若APK在開發時使用的是LLVM編譯套件進行編譯,則可以在編譯期間對代碼進行混淆。分為前端混淆和IR混淆。
前端混淆:指編譯器前端在進行原生代碼分析時採取的混淆技術
IR混淆:指在編譯原生代碼時對生成的中間代碼IR進行混淆的技術
3.二進位混淆
在生成APK文件後,提取出其中的二進位文件,對二進位文件再進行混淆。典型的技術有Dex二次混淆,即對生成的Dex文件進行反編譯後,再進行混淆。使得Dex的執行流程被混淆和字元串被混淆。
1 //以字元串混淆為例
2
3 //混淆前
4 void methodA()
5 {
6 clz.method1("abc");
7 clz.method3(clz.method2("def"),"ghi");
8 }
9
10 //使用ProGuard進行代碼混淆後的反彙編代碼可能如下
11 void methodA()
12 {
13 a.a("abc");
14 a.aaa(a.aa("def"),"ghi");
15 }
16
17 //使用DexGuard進行代碼混淆後的反彙編代碼可能如下,b.a()方法
18 void methodA()
19 {
20 a.a(b.a("xxxxxxxxx"));
21 a.aaa(a.aa(b.a("yyyyyyyyy")),b.a("zzzzzzzzz"));
22 }
二、Dex加密
Dex加密指將APK的DEX文件進行加密,在運行時再解密載入到記憶體中執行,防止Dex文件被逆向的一種保護技術。
Dex加密APK的執行流程如下圖:
加固過程:
1.需要編寫三個項目:
(1)源APK(真正的APK程式)
(2)加殼程式(Java項目,用於對源APK進行加密,並將加密後的源APK與脫殼程式的dex進行合併)
(3)脫殼程式(Android項目,用於對加密後的源APK文件進行解密,並將其載入到記憶體中去)
2.加殼程式將源APK加密成所有操作系統都不能正確讀取的APK文件。
3.加殼程式再提取出解密APK的classes.dex文件,將脫殼程式的dex文件與加密後的源apk進行合併,得到新的dex文件
4.在新dex文件的末尾添加一個長度為4位的欄位,用於標識加密apk的大小
5.修改新dex文件的頭部三個欄位:checksum,signature,file_size,以保證新dex文件的正確性
6.生成新的classes.dex文件,打包,生成加固APK
源APK加密原理流程如下圖:
加殼程式主要邏輯代碼分析:
1 public static void main(String args[]) 2 { 3 try 4 { 5 //---------獲取文件------------------------------------------- 6 //獲取需要加密的源apk程式 7 File payloadSrcFile = new File("xxxx/abc.apk"); 8 System.out.println("SrcAPK size:"+payloadSrcFile.length()); 9 //獲取脫殼(解密)程式的DEX文件,需要提前把脫殼程式的DEX文件解壓出來 10 File unshellDexFile = new File("yyyy/classes.dex"); 11 12 //---------加密源apk文件---------------------------------------- 13 /* 14 以二進位形式讀取apk,並加密,存到payloadArray數組中 15 readFileBytes是以二進位形式讀取文件 16 encrpt()是自定義的對二進位流加密的函數 17 */ 18 byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile)); 19 //以二進位形式讀取脫殼dex 20 byte[] unshellDexArray = readFileBytes(unshellDexFile); 21 22 //---------合併------------------------------------------------ 23 //***1.計算出新Dex文件的總長度 24 int payloadLen = payloadArray.length(); //加密APK的長度 25 int unshellDexLen = unshellDexArray.length(); //脫殼Dex的長度 26 int totalLen = payloadLen+unshellDexLen=4 //新的Dex文件長度,最後+4是為了用於存儲payloadLen值的 27 //***2.定義新dex文件 28 //先定義新dex文件的長度 29 byte[] newDex = new byte[totalLen]; 30 //***3.開始合併 31 //將脫殼dex文件的內容寫入新dex 32 System.arraycopy(unshellDexArray,0,newDex,0,unshellDexLen); 33 //再將加密後的源APK內容連接在後面 34 /* 35 arraycopy(arr1,x,arr2,y,m)表示將數組arr1從索引為x開始的元素,複製m個元素到數組arr2中去,從arr2的y索引開始粘貼 36 */ 37 System.arraycopy(payloadArray,0,newDex,unshellDexLen,payloadLen); 38 //最後將將加密後的apk長度記錄在新dex文件末尾 39 /* 40 intToByte()將int類型數據轉換成Byte 41 total是int類型的,newDex是Byte類型 42 total是脫殼dex長度+加密apk長度+4,所以,最後是從newDex的第total-4索引開始寫入payload的長度 43 一個byte是1個位元組,即8位;一個int是4個位元組,即32位;所以最後的4表示寫入intToByte(payloadLen)的4個元素 44 */ 45 System.arraycopy(intToByte(payloadLen),0,newDex,totalLen-4,4); 46 47 //---------修改新dex文件的頭部信息---------------------------------- 48 //修改file_size欄位 49 fixFileSizeHeader(newDex); 50 //修改signature欄位 51 fixSHA1Header(newDex); 52 //修改checksum欄位 53 fixCheckSumHeader(newDex); 54 55 //---------生成新dex文件------------------------------------------ 56 //定義要生成文件的路徑及名稱並創建空文件 57 String str = "zzzz/classes.dex"; 58 File file = new File(str); 59 if (!file.exists()) 60 { 61 file.createNewFile(); 62 } 63 //將合併好的數據寫入文件 64 FileOutputStream localFileOutputStream = new FileOutputStream(str); 65 localFileOutputStream.write(newDex); 66 localFileOutputStream.flush(); 67 localFileOutputStream.close(); 68 }catch (Exception e) { 69 e.printStackTrace(); 70 } 71 } 72 73 //加密函數encrpt()的定義 74 private static byte[] encrpt(byte data[]) 75 { 76 //加密演算法實現,這裡就不寫了 77 return data; 78 }
脫殼原理流程如下圖:
用戶執行加固APP,其實進入的是脫殼APK的代碼,脫殼APK會先提取並解密出真正的APK,但此時真正的APK並沒有被載入到手機的記憶體中。因此需要在一個恰當的時期,在用戶交互界面進入脫殼APK之前將真正的APK解密提取出來並載入到記憶體中去執行起來。到底在何時進行提取、解密和載入真正APK?如下圖分析所示即最佳時機:
如上圖所示,
(1)提取和解密APK的最佳時機則是在脫殼APK的Activity被拉起之前,Activity是通過onCreate()方法拉起來的,因此在onCreate()方法之前,應將源APK提取並解密。
(2)提取並解密出源APK之後,通過改寫脫殼APK的onCreate()方法,讓其拉起源APK。
脫殼程式主要邏輯代碼分析:
1 //********重寫attachBaseContext()函數*********** 2 protected void attachBaseContext(Context base) 3 { 4 super.attachBaseContext(base); 5 try 6 { 7 //---------提取加密apk--------------------------------------------- 8 //創建兩個私有、可寫的目錄payload_odex和payload_lib 9 File odex = this.getDir("payload_odex",MODE_PRIVATE); //用於存放源APK的dex文件 10 File libs = this.getDir("payload_lib",MODE_PRIVATE); //用於存放源APK的so文件 11 odexPath = odex.getAbsolutePath(); 12 libPath = libs.getAbsolutePath(); 13 apkFileName = odex.getAbsolutePath()+"/abc.apk"; 14 File dexFile = new File(apkFileName); 15 Log.i("demo","apk size:"+dexFile.length()); 16 if (!dexFile.exists()) 17 { 18 //在payload_odex文件目錄下,創建abc.apk空文件 19 dexFile.createNewFile(); 20 //讀取出加殼後的整個dex文件 21 //readDexFileFromApk()方法在attachBaseContext()方法外面實現 22 byte[] dexdata = this.readDexFileFromApk(); 23 //脫殼(分離出加密的源apk併進行解密),分離出apk文件以用於動態載入 24 //splitPayloadFromDex()方法在attachBaseContext()方法外面實現 25 this.splitPayloadFromDex(dexdata); 26 } 27 28 //---------載入解密之後的源apk程式------------------------------------ 29 //配置動態載入環境 30 //通過反射機制獲取當前activity的線程對象 31 Object currentActivityThread = RefInvoke.invokeStaticMethod( 32 "android.app.ActivityThread", 33 "currentActivityThread", 34 new Class[] {}, 35 new Object[] {}); 36 37 //獲取加殼apk的包名 38 String packageName = this.getPackageName(); 39 //通過反射獲取和設置當前APK的各種信息 40 ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldObject( 41 "android.app.ActivityThread", 42 currentActivityThread, 43 "mPackages"); 44 WeakReference wr = (WeakReference) mPackages.get(packageName); 45 46 //自定義一個DexClassLoader對象,用於載入被載入解密後的源apk和源apk內的類和C/C++代碼 47 DexClassLoader dLoader = new DexClassLoader( 48 apkFileName, 49 odexPath, 50 libPath, 51 (ClassLoader) RefInvoke.getFieldObject( 52 "android.app.LoadedApk", 53 wr.get(), 54 "mClassLoader")); 55 //把當前進程的DexClassLoader()設置成脫殼apk在上一行自定義的DexClassLoader() 56 RefInvoke.setFiedObject( 57 "android.app.LoadedApk", 58 "mClassLoader", 59 wr.get(), 60 dLoader); 61 62 Log.i("demo","classloader:"+dLoader); 63 64 try 65 { 66 Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity"); 67 Log.i("demo","actObj:"+actObj); 68 } 69 catch (Exception e) 70 { 71 Log.i("demo","activity:"+Log.getStackTraceString(e)); 72 } 73 }catch (Exception e) 74 { 75 Log.i("demo","error:"+Log.getStackTraceString(e)); 76 e.printStackTrace(); 77 } 78 } 79 80 81 /* 82 *@name :readDexFileFromApk() 83 *@function :從脫殼程式中提取脫殼DEX文件內容 84 *@param :無 85 *@throws :IOException 86 *@return :byte[] 87 */ 88 private byte[] readDexFileFromApk() throws IOException 89 { 90 //定義一個Byte類型的數組對象,用於存儲dex文件的內容 91 ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream(); 92 //定義一個ZIP對象,用於解壓APK 93 ZipInputStream localZipInputStream = new ZipInputStream( 94 new BufferedInputStream(new FileInputStream( 95 this.getApplicationInfo().sourceDir))); 96 97 while (true) 98 { 99 ////解壓並遍歷ZIP文件,以二進位流形式讀入數組arrayOfByte[] 100 ZipEntry localZipEntry = localZipInputStream.getNextEntry(); 101 if (localZipEntry == null) 102 { 103 localZipInputStream.close(); 104 break; 105 } 106 //找到脫殼dex文件 107 if (localZipEntry.getName().equals("classes.dex")) 108 { 109 110 byte[] arrayOfByte = new byte[1024]; 111 while (true) 112 { 113 int i = localZipInputStream.read(arrayOfByte); 114 if (i == -1) 115 { 116 break; 117 } 118 dexByteArrayOutputStream.write(arrayOfByte,0,i); 119 } 120 } 121 localZipInputStream.closeEntry(); 122 } 123 localZipInputStream.close(); 124 return dexByteArrayOutputStream.toByteArray(); 125 } 126 127 /* 128 *@name :splitPayloadFromDex(byte apkdata[]) 129 *@function :從脫殼dex文件中提取出被加密的apk文件和so文件,並解密 130 *@param :apkdata -要解密的DEX文件二進位流 131 *@throws :IOException 132 *@return :無 133 */ 134 private void splitPayloadFromDex(byte apkdata[]) throws IOException 135 { 136 //---------剝離出加密源APK--------------------------------------------- 137 //獲取整個dex文件的長度 138 int ablen = apkdata.length(); 139 //獲取脫殼dex最後4位的數據,即獲取源加密APK的長度 140 //ablen-4在arraycopy()中即表示從apkdata的第倒數第四位開始 141 byte[] dexlen = new byte[4]; 142 System.arraycopy(apkdata,ablen-4,dexlen,0,4); 143 144 //對獲取到的4位元組長度值進行轉換,將二進位數組類型轉換成可以計算的int類型 145 ByteArrayInputStream bais = new ByteArrayInputStream(dexlen); 146 DataInputStream in = new DataInputStream(bais); 147 int readInt = in.readInt(); 148 System.out.println(Integer.toHexString(readInt)); 149 150 //定義一個二進位流數組用來存儲即將提取出來的加密apk 151 byte[] newdex = new byte[readInt]; 152 //把加密apk存到newdex中 153 /* 154 apkdata是整個dex文件的數據內容 155 ablen是整個dex文件的長度-4即表示整個dex文件從頭到加密apk的末尾的位置 156 readInt表示加密apk的長度 157 ablen-4再-readInt即表示從整個dex從頭到脫殼dex的末尾的位置 158 */ 159 System.arraycopy(apkdata,ablen-4-readInt,newdex,0,readInt); 160 161 //---------解密源APK------------------------------------------------------- 162 //decrypt()函數在splitPayloadFromDex()函數外實現 163 newdex = decrypt(newdex); 164 165 //---------釋放解密後的源APK到磁碟----------------------------------------- 166 File file = new File(apkFileName); 167 try 168 { 169 FileOutputStream localFileOutputStream = new FileOutputStream(file); 170 localFileOutputStream.write(newdex); 171 localFileOutputStream.close(); 172 }catch (IOException e) 173 { 174 throw new RuntimeException(localIOException); 175 } 176 177 //---------分析釋放出來的源APK---------------------------------------------- 178 //定義一個ZIP對象 179 ZipInputStream localZipInputStream = new ZipInputStream( 180 new BufferedInputStream(new FileOutputStream(file))); 181 while (true) 182 { 183 //解壓並遍歷ZIP包 184 ZipEntry localZipEntry = localZipInputStream.getNextEntry(); 185 if (localZipInputStream == null) 186 { 187 localZipInputStream.close(); 188 break; 189 } 190 //提取出APK中的so文件,並釋放到脫殼程式一開始在磁碟新建的payload_lib目錄中去中去 191 String name = localZipEntry.getName(); 192 if (name.startWith("lib/") && name.endwith(".so")) //確認遍歷到的文件是否是so文件 193 { 194 File storeFile = new File(libPath+"/"+name.substring(name.lastIndexOf('/'))); 195 storeFile.createNewFile(); 196 FileOutputStream fos = new FileOutputStream(storeFile); 197 byte[] arrayOfByte = new byte[1024]; 198 while (true) 199 { 200 int i = localZipInputStream.read(arrayOfByte); 201 if (i == -1) 202 { 203 break; 204 } 205 fos.write(arrayOfByte,0,i); 206 } 207 fos.flush(); 208 fos.close(); 209 } 210 localZipInputStream.closeEntry(); 211 } 212 localZipInputStream.close(); 213 } 214 215 /* 216 *@name :decrypt(byte srcdata[]) 217 *@function :對被加密的apk文件進行解密 218 *@param :srcdata -要解密的apk文件二進位流 219 *@throws :無byte 220 *@return :srcdata -byte[]類型 221 */ 222 private byte[] decrypt(byte[] srcdata) 223 { 224 //解密演算法,根據加密演算法編寫對應的解密過程 225 return srcdata; 226 } 227 228 229 //**********重寫onCreate()方法*********** 230 public void onCreate () 231 { 232 //未完待續 233 }
#0x02 第二代殼
更新中......
#0x03 第三代殼
更新中.......