017 Android加固之APK混淆和保护方式
文章目錄
- APK加固介紹
- 使用proguard對(duì)apk中的源碼進(jìn)行混淆
- proguard原理
- 對(duì)APK反編譯之后的smali進(jìn)行混淆
- 代碼亂序原理
- 亂序?qū)嵗?Hello
- 對(duì)APK中的字符串進(jìn)行加密
- 解決方案1-StringBuilder拼接
- 解決方案2-編碼混淆
- 解決方案3-加密處理
- 小結(jié)
- 對(duì)APK中的文件進(jìn)行校驗(yàn)
- 對(duì)APK中的Dex文件進(jìn)行校驗(yàn)
- 對(duì)APK中的APK進(jìn)行校驗(yàn)
- 對(duì)APK中的簽名進(jìn)行校驗(yàn)
- 小結(jié)
APK加固介紹
Android應(yīng)用程序使用的開發(fā)語言是JAVA,由于采用的是這種解釋型語言,所以代碼可以被反編譯。如果沒有經(jīng)過混淆或是加密,會(huì)非常容易讓人分析。
為了使我們的APK更好的保護(hù)起來,Android的開發(fā)以及安全人員對(duì)APK進(jìn)行了一系列的加固措施。從開發(fā)者而言,以下是常用的手段和方式:
使用proguard對(duì)apk中的源碼進(jìn)行混淆
Android Studio默認(rèn)已經(jīng)有proguard的支持,在項(xiàng)目目錄app下的build.gradle中有配置信息,只需要將false改成true即可
默認(rèn)只有release的配置,而工程默認(rèn)是debug的,所以想要debug添加代碼混淆,需要在release后面添加debug模式
編譯以后可以看到混淆的效果
上面都是系統(tǒng)的一些類庫(kù),被混淆成了無意義的字符
反編譯后的源碼可以看到對(duì)類名和系統(tǒng)庫(kù)進(jìn)行了一定程度的混淆。
Proguard的混淆結(jié)果會(huì)輸出到<module-name>/build/outputs/mapping/release/目錄或是``/build/outputs/mapping/debug/`
一共有四個(gè)輸出文件
proguard原理
proguard包括四個(gè)功能,shrinker(壓縮),optimizer(優(yōu)化),obfuscator(混淆),preverifier(預(yù)校驗(yàn))
- shrinker(壓縮):檢測(cè)并移除沒有用到的類,變量,方法和屬性
- optimizer(優(yōu)化):優(yōu)化代碼,非入口節(jié)點(diǎn)類會(huì)加上private/static/final,沒有用到的參數(shù)會(huì)被刪除,一些方法可能會(huì)變成內(nèi)聯(lián)代碼
- obfuscator(混淆):使用短又沒有語義的名字重命名非入口類的類名,變量名和方法名。入口類的類名保持不變
- preverify(預(yù)校驗(yàn)):預(yù)校驗(yàn)代碼是否符合Javal.6或者更高的規(guī)范
一般用戶可以選擇默認(rèn)的設(shè)置即可,如果想要自定義可以在proguard-rules.pro文件中進(jìn)行配置 。
對(duì)APK反編譯之后的smali進(jìn)行混淆
源碼經(jīng)過混淆之后,在一個(gè)產(chǎn)品級(jí)的APK中對(duì)APK的保護(hù)是有一定幫助的,而對(duì)于一些有經(jīng)驗(yàn)的破解者,他們還是會(huì)找到比較關(guān)鍵的地方,一般這種方式都是反編譯之后進(jìn)行分析的,那么如果我們效仿Windows下的保護(hù)軟件,對(duì)smali代碼進(jìn)行混淆,某種程度上就加大了破解者的分析難度。比較方便的混淆方式就是代碼亂序
代碼亂序原理
我們?cè)诜治鰏mali代碼時(shí),一般會(huì)借助于反編譯工具反編譯成java代碼。代碼亂序的目的就是想要將smali代碼亂序后,使反編譯的效果大打折扣,這樣就會(huì)擋住很多菜鳥,至少可以減慢分析速度,增加破解難度。
亂序的基本原理如上圖所示,將指令重新布局,并給每塊指令賦予一個(gè) label,在函數(shù)開頭處使用 goto 跳到原先的第一條指令處,然后第一條指令處理完,再跳到 第二條指令,以此類推。
亂序?qū)嵗?Hello
我們先從最簡(jiǎn)單的Hello開始
public class Hello{public static void main(String[] argc){String a="1";String b="2";String c=a+b;System.out.println(c);} }首先用Android Studio編譯成APK,然后放到Android Killer中反編譯
接著來修改smali代碼
我們可以大致把整個(gè)代碼分成三部分:
根據(jù)這三部分,我們可以根據(jù)上面的原理對(duì)代碼中的三部分進(jìn)行亂序,加上標(biāo)簽跟跳轉(zhuǎn)
亂序后的代碼如下
復(fù)制一份smali副本
將沒有用的smali代碼全部刪除,只保留Hello.smali
先用smali.jar將整個(gè)文件夾轉(zhuǎn)成dex文件,接著用dex2jar將dex文件轉(zhuǎn)成jar文件
此時(shí)再來查看jd-gui,可以看到這個(gè)工具的反編譯效果大打折扣
但是對(duì)jadx實(shí)際上是無效的,畢竟這個(gè)工具的實(shí)在是太強(qiáng)大了 。
同時(shí)proguard一個(gè)東家的dexguard,最初就采取類似的方法對(duì)代碼進(jìn)行保護(hù),當(dāng)然除了這種保護(hù)方式外,還有其他的,比如最常見的就是字符串加密。
對(duì)APK中的字符串進(jìn)行加密
在開發(fā)過程中字符串的使用是不可避免的,但是這些字符串極可能是破解的關(guān)鍵點(diǎn),比如服務(wù)器的地址和錯(cuò)誤提示這些敏感的字符串信息。如果這些字符串采用硬編碼的方式很容易通過靜態(tài)分析獲取
在java中定義一個(gè)字符串
public String normalString(){String str="Hello wordl";return str; }反編譯的smali代碼
.method public normalString()Ljava/lang/String;const-string v0, "Hello wordl".local v0, "str":Ljava/lang/String;return-object v0 .end method可以看出const-string關(guān)鍵字后面就是定義的字符串值,甚至可以使用自動(dòng)化分析工具批量提取出來
解決方案1-StringBuilder拼接
StringBuilder類通過append方法來構(gòu)造需要的字符串,這種方式可以增加自動(dòng)化分析的難度,如果要獲取完整的字符串就必須進(jìn)行相應(yīng)的語法解析了
Java中拼接字符串代碼
public String buildString(){StringBuilder builder=new StringBuilder();builder.append("Hello");builder.append(" ");builder.append("wordl");return builder.toString(); }反編譯的smali代碼
.method public buildString()Ljava/lang/String;.locals 2.line 18new-instance v0, Ljava/lang/StringBuilder;invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V.line 19.local v0, "builder":Ljava/lang/StringBuilder;const-string v1, "Hello"invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;.line 20const-string v1, " "invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;.line 21const-string v1, "wordl"invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;.line 22invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;move-result-object v1return-object v1 .end method可以看出反編譯后的smali代碼對(duì)破解增加了一定的難度,并不能一眼就識(shí)別出來
解決方案2-編碼混淆
編碼混淆是在硬編碼的時(shí)候?qū)⒆址D(zhuǎn)換成16進(jìn)制的數(shù)組或者Unicode編碼,在使用的時(shí)候轉(zhuǎn)回字符串。這種方式在反編譯smali代碼比StringBuilder方式更難直接識(shí)別
Java代碼
public String encodingString() {byte[] strBytes = {0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6f, 0x72, 0x6C, 0x64};String str = new String(strBytes);return str; }反編譯的smali代碼
.method public encodingString()Ljava/lang/String;.locals 2.line 26const/16 v0, 0xbnew-array v0, v0, [Bfill-array-data v0, :array_0.line 27.local v0, "strBytes":[Bnew-instance v1, Ljava/lang/String;invoke-direct {v1, v0}, Ljava/lang/String;-><init>([B)V.line 28.local v1, "str":Ljava/lang/String;return-object v1nop:array_0.array-data 10x48t0x65t0x6ct0x6ct0x6ft0x20t0x77t0x6ft0x72t0x6ct0x64t.end array-data .end method解決方案3-加密處理
加密處理是先將字符串在本地進(jìn)行加密處理,后將密文硬編碼進(jìn)去,運(yùn)行時(shí)再進(jìn)行解密。加密步驟:
當(dāng)然因?yàn)镴ava代碼相對(duì)來說比較容易反編譯,并且該方式需要將解密方法放在APK本地,所以我們可以將解密方法通過JNI實(shí)現(xiàn),加大反編譯難度
小結(jié)
任何一種加固方式都只是加大了破解的難度,并不能完全避免Android程序被破解。
對(duì)APK中的文件進(jìn)行校驗(yàn)
在APK中包括代碼和資源以及簽名文件,我們一般會(huì)對(duì)可執(zhí)行文件進(jìn)行校驗(yàn),還有證書的簽名進(jìn)行校驗(yàn),以及apk本身的校驗(yàn)。
對(duì)APK中的Dex文件進(jìn)行校驗(yàn)
classes.dex是Android虛擬機(jī)的可執(zhí)行文件,我們所寫的JAVA代碼其實(shí)都在這里,很多應(yīng)用程序的篡改都是針對(duì)classes.dex的。
代碼比較簡(jiǎn)單,這里是通過計(jì)算好的CRC保存在string.xml里。本例可以直接運(yùn)行代碼校驗(yàn),在監(jiān)視器中查看到CRC的值,然后修改string.xml中對(duì)應(yīng)的值即可。
校驗(yàn)代碼:
private void VarifyDex(){//獲取string.xml的valueLong dexCrc=Long.parseLong(this.getString(R.string.crc_value));String apkPath=this.getPackageCodePath();try {ZipFile zipFile=new ZipFile(apkPath);ZipEntry dexEntry=zipFile.getEntry("classes.dex");//計(jì)算class.dex的CRC值long dexEntryCrc=dexEntry.getCrc();//對(duì)比if (dexCrc==dexEntryCrc){Log.d("Dex","dex not been modify");}else{Log.d("Dex","dex has been modify");}}catch (IOException e){e.printStackTrace();} }對(duì)APK中的APK進(jìn)行校驗(yàn)
與Dex校驗(yàn)不同,APK校驗(yàn)必須把計(jì)算好的哈希值放在網(wǎng)絡(luò)服務(wù)端,因?yàn)閷?duì)APK的任何改動(dòng)都會(huì)影響到最后的哈希值。
校驗(yàn)代碼:
private void VarifyApk() {//獲取data/app/***/base.apkString apkPath=getPackageResourcePath();Log.d("GuiShou","apkPath:"+apkPath);MessageDigest msgDigest;try {//獲取apk并計(jì)算MD5值msgDigest=MessageDigest.getInstance("MD5");byte[] bytes=new byte[4096];int count;FileInputStream fis;fis=new FileInputStream(new File(apkPath));while ((count=fis.read(bytes))>0){msgDigest.update(bytes,0,count);}//計(jì)算出md5值BigInteger bigInteger=new BigInteger(1,msgDigest.digest());String md5=bigInteger.toString(16);fis.close();Log.d("GuiShou","md5:"+md5);}catch (IOException e){e.printStackTrace();} catch (NoSuchAlgorithmException e) {e.printStackTrace();}//獲取服務(wù)端的md5進(jìn)行對(duì)比.....}對(duì)APK中的簽名進(jìn)行校驗(yàn)
每個(gè)APK都會(huì)經(jīng)過開發(fā)者獨(dú)有的證書進(jìn)行簽名,如果破解者對(duì)APK進(jìn)行二次打包一般會(huì)用自己的簽名證書進(jìn)行打包。這時(shí)我們就可以通過校驗(yàn)簽名證書的MD5值進(jìn)行校驗(yàn)
校驗(yàn)代碼
public void VerifySignatrue(){String packageName=this.getPackageName();PackageManager pm=this.getPackageManager();PackageInfo pi;String md5="";try {pi=pm.getPackageInfo(packageName,PackageManager.GET_SIGNATURES);Signature[] s=pi.signatures;//計(jì)算出MD5值MessageDigest messageDigest=MessageDigest.getInstance("MD5");messageDigest.reset();messageDigest.update(s[0].toByteArray());BigInteger bigInteger=new BigInteger(1,messageDigest.digest());md5=bigInteger.toString();} catch (PackageManager.NameNotFoundException e) {e.printStackTrace();} catch (NoSuchAlgorithmException e) {e.printStackTrace();}//---獲取服務(wù)端的簽名證書進(jìn)行對(duì)比---}獲取簽名證書md5可以解壓APK,找到META-INF文件夾中的CERT.RSA,通過keytool工具就可以看到其中的md5等屬性
小結(jié)
當(dāng)然上述的保護(hù)方式容易被暴力破解,完整性檢查最終還是通過返回true/false來控制后續(xù)代碼邏輯的走向。如果攻擊者直接修改代碼邏輯,使完整性檢查始終返回true,那這種方法就無效了,所以類似文件完整性的校驗(yàn)需要配合一些其他的方法,或者更為巧妙的方法實(shí)現(xiàn)。
總結(jié)
以上是生活随笔為你收集整理的017 Android加固之APK混淆和保护方式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 016 Android之NDK开发
- 下一篇: 018 Android加固之实现dex加