所謂APK指的是Android操作系統(tǒng)的應(yīng)用程序安裝文件。所謂Crack,簡單地理解為“破解”。我具體指的是反編譯APK文件進(jìn)行匯編級(jí)的代碼分析,并修改或插入自己的代碼,重新簽名打包為APK文件,以達(dá)到改變程序原有行為的目的。
由以上的說明可知,我們要Crack一個(gè)APK文件,主要流程有三步:反編譯、代碼分析、重新打包簽名。
基本準(zhǔn)備
我們需要一些基本的工具進(jìn)行一些主要的工作。如果你是一個(gè)會(huì)做Android APK漢化的朋友,那么你應(yīng)該對(duì)這些工具非常熟悉:
第一個(gè)工具是android-apktool,A tool for reengineering Android apk files 。這個(gè)工具是我們完成APK Crack的核心,利用它實(shí)現(xiàn)APK文件的反編譯和重新打包。它是Google Code上一個(gè)非常著名的開源項(xiàng)目,大家可以在Google Code的網(wǎng)頁上獲取它和它的Wiki、源碼及其他相關(guān)信息。網(wǎng)址是:
http://code.google.com/p/android-apktool/ 。
第二個(gè)工具是Auto-sign。這個(gè)工具實(shí)現(xiàn)的是APK打包后的簽名工作,屬于一個(gè)小工具。
除了這些基本工具外,為了更好的分析代碼,你可能還需要用到一些其他工具,例如:dex2jar和jd-gui等,這里不做詳述。
反編譯
如果你是一個(gè)經(jīng)常漢化APK程序的朋友,那么反編譯這一步你肯定不會(huì)陌生。不過,既然這篇文章側(cè)重于基本流程講解,那么這一步想來是不能省掉的。所以,覺得羅嗦的朋友,請(qǐng)?zhí)^。首先我們需要有一個(gè)待反編譯的APK。這里我自己寫了一個(gè)HelloWorld的APK,代碼如下:
- package com.zh_weir.helloworld;import android.app.Activity;
- import android.os.Bundle;
- public class MainActivity extends Activity {
- /** Called when the activity is first created. */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- }
- }
復(fù)制代碼我們通過android-apktool對(duì)這個(gè)APK進(jìn)行反編譯。對(duì)于android-apktool的使用,我就不做太多翻譯的工作,直接給出說明文檔吧。簡單一句話,就是命令行執(zhí)行。
- Apktool v1.3.2 - a tool for reengineering Android apk files
- Copyright 2010 Ryszard Wi?niewski <brut.alll@gmail.com>
- Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
- Usage: apktool [-v|--verbose] COMMAND [...]
- COMMANDs are:
- d[ecode] [OPTS] <file.apk> [<dir>]
- Decode <file.apk> to <dir>.
- OPTS:
- -s, --no-src
- Do not decode sources.
- -r, --no-res
- Do not decode resources.
- -d, --debug
- Decode in debug mode. Check project page for more info.
- -f, --force
- Force delete destination directory.
- -t <tag>, --frame-tag <tag>
- Try to use framework files tagged by <tag>.
- --keep-broken-res
- Use if there was an error and some resources were dropped, e.g.:
- "Invalid config flags detected. Dropping resources", but you
- want to decode them anyway, even with errors. You will have to
- fix them manually before building.
- b[uild] [OPTS] [<app_path>] [<out_file>]
- Build an apk from already decoded application located in <app_path>.
- It will automatically detect, whether files was changed and perform
- needed steps only.
- If you omit <app_path> then current directory will be used.
- If you omit <out_file> then <app_path>/dist/<name_of_original.apk>
- will be used.
- OPTS:
- -f, --force-all
- Skip changes detection and build all files.
- -d, --debug
- Build in debug mode. Check project page for more info.
- if|install-framework <framework.apk>
- Install framework file to your system.
- For additional info, see: http://code.google.com/p/android-apktool/
復(fù)制代碼通過apktool d HelloWorld.apk的命令,我們就完成了一個(gè)簡單的APK的反編譯工作。得到了一個(gè)叫做“HelloWorld”的文件夾。你可以看見文件夾下有Manifest文件,有反編譯出的res資源文件。這些東西都是平時(shí)漢化特別關(guān)心的,而不是我們要注意的重點(diǎn)。我們需要注意的是一個(gè)叫做“smali”的文件夾。
仔細(xì)觀察,你會(huì)發(fā)現(xiàn)這個(gè)文件夾下的文件組織結(jié)構(gòu)和我們的Android工程中java源碼的組織結(jié)構(gòu)幾乎一致。只不過Java文件被.smali的文件取而代之了。我們用文本編輯器打開這些.smali文件,你會(huì)發(fā)現(xiàn)它們都是可識(shí)別的、并且非常“整齊”的文本文件,大致如下:
- .class public Lcom/zh_weir/helloworld/MainActivity;
- .super Landroid/app/Activity;
- .source "MainActivity.java"
- # direct methods
- .method public constructor <init>()V
- .locals 0
- .prologue
- .line 6
- invoke-direct {p0}, Landroid/app/Activity;-><init>()V
- return-void
- .end method
- # virtual methods
- .method public onCreate(Landroid/os/Bundle;)V
- .locals 1
- .parameter "savedInstanceState"
- .prologue
- .line 10
- invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
- .line 11
- const/high16 v0, 0x7f03
- invoke-virtual {p0, v0}, Lcom/zh_weir/helloworld/MainActivity;->setContentView(I)V
- .line 12
- return-void
- .end method
復(fù)制代碼Smali文件其實(shí)就是dalvik虛擬機(jī)運(yùn)行的dex字節(jié)碼文件對(duì)應(yīng)的匯編文件了。如果你了解Java虛擬機(jī)的匯編語言Jasmin的話,你會(huì)發(fā)現(xiàn)兩者的語法非常相似。關(guān)于smali的語法等問題就不深入下去了,如果你想了解更多,可以訪問Google Code上Smali項(xiàng)目主頁:
http://code.google.com/p/smali/ 。
代碼分析與修改
即使你不會(huì)Jasmin語法,你也能很容易看明白上面的匯編代碼。需要指出的是,apktool反編譯出來的匯編代碼同樣也是面向?qū)ο蟮?,而不是面向過程的。這點(diǎn)和C++的反匯編可能有所不同。
根據(jù)上面的代碼,我們可以看出,這個(gè)MainActivity的類有兩個(gè)成員方法。一個(gè)是默認(rèn)的構(gòu)造函數(shù);另一個(gè)就是我們重載的OnCreate方法了。
在java匯編中,每個(gè)成員方法需要首先申明自己所使用的局部變量的個(gè)數(shù),以便實(shí)現(xiàn)分配存儲(chǔ)空間。例如OnCreate使用了一個(gè)局部變量,就聲明:.locals 1 。后面則使用v0表示。
在一個(gè)非靜態(tài)的成員方法中,p0代表的是這個(gè)類本身的引用,相當(dāng)于this,p1開始才是函數(shù)的參數(shù);而對(duì)于靜態(tài)方法,由于沒有this指針,所以p0就是函數(shù)的第一個(gè)參數(shù)。(其實(shí)本身this指針就是作為一個(gè)隱含的參數(shù)傳遞給非靜態(tài)成員函數(shù)的)。
通過分析上面Oncreate的匯編代碼,我們可以知道,它首先是調(diào)用super類的onCreate方法,然后再setContentView設(shè)置顯示。其中I、V等表示的是函數(shù)的參數(shù)和返回變量的類型,這是通用做法,這里就不多做說明了。
分析到這一步,你是否發(fā)現(xiàn)一個(gè)問題?那就是如果我們按照同樣的語法修改或者增刪一個(gè)語句,是否就可以實(shí)現(xiàn)對(duì)程序的修改了呢?答案是肯定的。
例如,我們希望這個(gè)APK程序在運(yùn)行時(shí)會(huì)彈出一個(gè)Toast,提示它被破解了。用Java的話,應(yīng)該這樣表述:
Toast.makeText(this, "I'm Cracked!", Toast.LENGTH_LONG).show();
而用Java匯編的話,則應(yīng)該表述為這樣:const-string v0, "I\'m Cracked!"
- const/4 v1, 0x1
- invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
- move-result-object v0
- invoke-virtual {v0}, Landroid/widget/Toast;->show()V
復(fù)制代碼OK,只要我們將這段代碼插入到原來程序的OnCreate中,再重新打包程序,我們就能實(shí)現(xiàn)在這個(gè)程序運(yùn)行時(shí)彈出Toast了。
改之后的代碼,大致如下:
- # virtual methods
- .method public onCreate(Landroid/os/Bundle;)V
- .locals 2
- .parameter "savedInstanceState"
- .prologue
- .line 11
- invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
- .line 12
- const/high16 v0, 0x7f03
- invoke-virtual {p0, v0}, Lcom/zh_weir/helloworld/MainActivity;->setContentView(I)V
- .line 14
- const-string v0, "I\'m Cracked!"
- const/4 v1, 0x1
- invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
- move-result-object v0
- invoke-virtual {v0}, Landroid/widget/Toast;->show()V
- .line 15
- return-void
- .end method
復(fù)制代碼重新編譯打包簽名
修改完成后,我們就可以對(duì)這個(gè)文件夾進(jìn)行編譯打包了。同樣,我們使用的工具是apktool。通過命令apktool b HelloWorld,就可以實(shí)現(xiàn)程序編譯打包了。這時(shí)會(huì)在這個(gè)文件夾下生成兩個(gè)文件夾:存放中間文件的文件夾build和存放最后的apk文件的文件夾dist。
如果一切順利的話,你就可以在dist文件夾中看到我們修改后的HelloWorld.apk了。不過需要注意的是,這個(gè)APK文件是還沒有簽名的,所以無法安裝運(yùn)行。我們還需要進(jìn)行最后一步,那就是對(duì)這個(gè)APK進(jìn)行簽名。
簽名我們需要用到的工具是Auto-sign。它主要是利用批處理命令,使用signapk.jar對(duì)APK文件進(jìn)行簽名的。你可以用記事本打開Sign.bat,看看它的具體調(diào)用關(guān)系。關(guān)鍵如下:
java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk
OK!到此,我們對(duì)這個(gè)APK的Crack就結(jié)束了?,F(xiàn)在在模擬器或者Android機(jī)器上安裝上簽好名的APK程序,運(yùn)行試試。一切與我們的預(yù)期相同~
如何反破解
這篇文章本來到此就結(jié)束了。不過,對(duì)于軟件安全來說,有攻就要有防才對(duì)。不然,Android整個(gè)產(chǎn)業(yè)鏈就會(huì)被這樣的Crack給毀掉。由于篇幅有限,反破解方面就不做詳細(xì)描述了,僅僅做一個(gè)思路上的說明,希望大家諒解。
第一種辦法:將核心代碼用JNI寫進(jìn)so庫中。由于so庫的反編譯和破解的難度加大,所以這種方式防止反編譯效果不錯(cuò)。缺點(diǎn)是,對(duì)于Java層的代碼沒有保護(hù)作用,同樣可以被篡改。
第二種辦法:在線簽名比較。在程序初始化時(shí),聯(lián)網(wǎng)將運(yùn)行的程序的簽名與服務(wù)器上的官方標(biāo)準(zhǔn)簽名進(jìn)行比較,從而達(dá)到讓反編譯后的程序無法正常運(yùn)行的效果。缺點(diǎn)是,如果此部分聯(lián)網(wǎng)檢驗(yàn)的代碼被篡改跳過,則整套機(jī)制失效。
第三種辦法:代碼混淆。為了加大反編譯后代碼分析的難度,對(duì)代碼進(jìn)行混淆。缺點(diǎn)是,治標(biāo)不治本,同樣可以修改(甚至據(jù)說還有反混淆工具,沒用過,不多做評(píng)論)。
這三種辦法都有各自的缺點(diǎn),所以單單靠某一項(xiàng)要實(shí)現(xiàn)完美的軟件保護(hù)都是不可能的。不過,我們可以采用聯(lián)合幾種辦法的方式,來增強(qiáng)軟件保護(hù)的力度。曾經(jīng)反編譯過Android版的卡巴斯基,它的保護(hù)思路似乎是這樣的:
代碼混淆,那是必須的,不過不指望它能有很好的效果。在程序初始化時(shí),就直接通過JNI的so庫初始化程序。程序激活部分也是通過JNI聯(lián)網(wǎng)下載文件,然后在JNI層讀文件并做相應(yīng)激活與否的判斷的。
卡巴斯基是將大部分功能模塊都放在JNI層來實(shí)現(xiàn)的,如果我們的程序都這樣處理,耗費(fèi)的精力必然很大。所以,我們只是借鑒它的思路而已。具體操作思路如下:
代碼混淆。初始化時(shí)JNI層聯(lián)網(wǎng)驗(yàn)證簽名。驗(yàn)證失敗則直接在JNI層退出程序。值得注意的是需要保證如果繞過JNI層的初始化,則程序無法正常啟動(dòng)。這點(diǎn)不保證的話,破解還是很容易……