標籤:
作為Android開發人員,工作中少不了要反編譯別人的apk,當然主要目的還是為了學習到更多,取彼之長,補己之短。今天就來總結一下Android反編譯和二次打包的一些知識。首先聲明本文的目的是為了通過例子講解反編譯和二次打包的原理和方法,繼而作為後續講解防止二次打包和App安全的依據,並不是鼓勵大家去重新打包別人的App,盜取他人勞動成果。
本文首先介紹幾種Android反編譯工具的使用,然後實現在不需要知道原始碼的情況下,僅通過修改反編譯得到的smali檔案實現修改apk邏輯功能的目的。
Android中常用的反編譯工具有三個:dex2jar、jd-gui和apktool,這三個工具的作用如下:
dex2jar:將apk中的classes.dex檔案轉換成jar檔案。
jd-gui:查看由dex2jar轉換成的jar檔案,以介面的形式展示反編譯出來的Java原始碼。
apktool:反編譯產生smali位元組碼檔案,提取apk中的資源檔。
為了儘可能的把問題講清楚,我們來實現一個很簡單的例子。首先建立一個工程DecompileDemo,在MainActivity中定義一個布局,其中包含一個Button,點擊會列印一段日誌。
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private static final String TAG = "MainActivity"; private Button btn; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn = (Button) findViewById(R.id.btn); btn.setOnClickListener(this); } @Override public void onClick(View v) { Log.d(TAG,"Button is clicked"); }} 將這個工程編譯產生的apk解壓,取出其中的classes.dex放在dex2jar工具的目錄下,然後執行命令
會在目前的目錄下產生class-dex2jar.jar檔案
然後開啟jd-gui,將class-dex2jar.jar檔案拖進去,就可以看到反編譯出來的原始碼。
可以看到反編譯的代碼和原本的代碼差別不大,主要差別是原來的資源引用全都變成了數字。
下面我們來修改這個apk的內容。
首先我們將apk拷貝到apktool工具目錄下,執行命令apktool d app-release.apk。
產生的目錄中包含smali檔案夾
然後找到我們的主要的類MainActivity.smali,檔案內容如下:
.class public Lcom/viclee/decompiledemo/MainActivity;.super Landroid/support/v7/app/AppCompatActivity;.source "MainActivity.java"# interfaces.implements Landroid/view/View$OnClickListener;?# static fields.field private static final TAG:Ljava/lang/String; = "MainActivity"# instance fields.field private btn:Landroid/widget/Button;# direct methods.method public constructor <init>()V .locals 0 .prologue .line 9 invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V return-void.end method# virtual methods.method public onClick(Landroid/view/View;)V .locals 2 .param p1, "v" # Landroid/view/View; .prologue .line 23 const-string v0, "MainActivity" const-string v1, "Button is clicked" invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I .line 24 return-void.end method.method protected onCreate(Landroid/os/Bundle;)V .locals 1 .param p1, "savedInstanceState" # Landroid/os/Bundle; .prologue .line 14 invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V .line 15 const v0, 0x7f040019 invoke-virtual {p0, v0}, Lcom/viclee/decompiledemo/MainActivity;->setContentView(I)V .line 17 const v0, 0x7f0c0050 invoke-virtual {p0, v0}, Lcom/viclee/decompiledemo/MainActivity;->findViewById(I)Landroid/view/View; move-result-object v0 check-cast v0, Landroid/widget/Button; iput-object v0, p0, Lcom/viclee/decompiledemo/MainActivity;->btn:Landroid/widget/Button; .line 18 iget-object v0, p0, Lcom/viclee/decompiledemo/MainActivity;->btn:Landroid/widget/Button; invoke-virtual {v0, p0}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V .line 19 return-void.end method
其中36-40行是列印日誌的位置,檔案內容很清晰,每個地區的意義如下:
.class 類名
.super 父類名
.source 檔案名稱
.implements 這個類實現的介面
.field 成員變數
.method 方法
然後建立一個工程,在這個工程中實現想要替換的代碼,我們這裡是希望將原始工程中列印日誌的地方替換為彈出一個Toast。
public class MainActivity extends AppCompatActivity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); showToast(); } public void showToast() { Toast.makeText(this,"我是反編譯後進行的修改。",Toast.LENGTH_LONG).show(); }}
然後像前面一樣執行apktool命令,產生的smali檔案內容如下:
.class public Lcom/viclee/decompiledemo/MainActivity;.super Landroid/support/v7/app/AppCompatActivity;.source "MainActivity.java"# direct methods.method public constructor <init>()V .locals 0 .prologue .line 7 invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V return-void.end method# virtual methods.method protected onCreate(Landroid/os/Bundle;)V .locals 1 .param p1, "savedInstanceState" # Landroid/os/Bundle; .prologue .line 10 invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V .line 11 const v0, 0x7f040019 invoke-virtual {p0, v0}, Lcom/viclee/decompiledemo/MainActivity;->setContentView(I)V .line 13 invoke-virtual {p0}, Lcom/viclee/decompiledemo/MainActivity;->showToast()V .line 14 return-void.end method.method public showToast()V .locals 2 .prologue .line 17 const-string v0, "\u6211\u662f\u53cd\u7f16\u8bd1\u540e\u8fdb\u884c\u7684\u4fee\u6539\u3002" 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 18 return-void.end method
上面代碼中,33、39-56行就是彈出Toast的代碼部分。將上面整個showToast方法拷貝到原始工程的smali檔案中,這裡要特別注意修改行號,這個行號表示的是代碼在原始Java檔案中的行號,需要參考兩個smali檔案的行號來修改。我認為只要保證方法內的行號不亂序,並且方法之間的行號不衝突就可以。然後,需要將原始工程中列印日誌的代碼替換為顯示Toast的代碼,也就是將原始smali檔案中36-40行修改為建立工程中33、39-56行的內容。修改後的內容如下,主要關注下面內容中36行、75-91行與原始smali檔案的差異。
.class public Lcom/viclee/decompiledemo/MainActivity;.super Landroid/support/v7/app/AppCompatActivity;.source "MainActivity.java"# interfaces.implements Landroid/view/View$OnClickListener;# static fields.field private static final TAG:Ljava/lang/String; = "MainActivity"# instance fields.field private btn:Landroid/widget/Button;# direct methods.method public constructor <init>()V .locals 0 .prologue .line 9 invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V return-void.end method# virtual methods.method public onClick(Landroid/view/View;)V .locals 2 .param p1, "v" # Landroid/view/View; .prologue .line 23 invoke-virtual {p0}, Lcom/viclee/decompiledemo/MainActivity;->showToast()V .line 24 return-void.end method.method protected onCreate(Landroid/os/Bundle;)V .locals 1 .param p1, "savedInstanceState" # Landroid/os/Bundle; .prologue .line 14 invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V .line 15 const v0, 0x7f040019 invoke-virtual {p0, v0}, Lcom/viclee/decompiledemo/MainActivity;->setContentView(I)V .line 17 const v0, 0x7f0c0050 invoke-virtual {p0, v0}, Lcom/viclee/decompiledemo/MainActivity;->findViewById(I)Landroid/view/View; move-result-object v0 check-cast v0, Landroid/widget/Button; iput-object v0, p0, Lcom/viclee/decompiledemo/MainActivity;->btn:Landroid/widget/Button; .line 18 iget-object v0, p0, Lcom/viclee/decompiledemo/MainActivity;->btn:Landroid/widget/Button; invoke-virtual {v0, p0}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V .line 19 return-void.end method.method public showToast()V .locals 2 .prologue .line 27 const-string v0, "\u6211\u662f\u53cd\u7f16\u8bd1\u540e\u8fdb\u884c\u7684\u4fee\u6539\u3002" 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 28 return-void
然後我們需要將修改後的檔案目錄重新打包,執行命令 apktool b app-release,就會在app-releae目錄下產生兩個檔案夾:build 檔案夾裡面是一些中間檔案(classes.dex等內容),dist 檔案夾裡面存放著重新打包出來的apk檔案。
最後還要記得對產生的apk進行簽名,否則安裝時會報錯。執行下面的命令列:
jarsigner -verbose -keystore viclee.keystore -signedjar app-release-signed.apk app-release.apk viclee.keystore
-verbose 輸出簽名詳細資料
-keystore 指定金鑰組的儲存路徑
-signedjar 後面三個參數分別是簽名後的apk、未簽名的apk和金鑰組的別名
安裝簽名後的apk,點擊按鈕,確實彈出了Toast,內容和我們所設定的一致,說明我們的修改成功了。
另外,apk反編譯後也可以修改資源,將反編譯出來的資源檔修改一通,然後按照之前的方法,重新打包、簽名、安裝。下面兩個頁面是修改之前和修改之後的對比圖。
到這裡,本文的全部內容就講解完了,歡迎大家評論交流~
Android反編譯和二次打包實戰