作為Android開發(fā)者,工作中少不了要反編譯別人的apk,當(dāng)然主要目的還是為了學(xué)習(xí)到更多,取彼之長,補(bǔ)己之短。今天就來總結(jié)一下Android反編譯和二次打包的一些知識。首先聲明本文的目的是為了通過例子講解反編譯和二次打包的原理和方法,繼而作為后續(xù)講解防止二次打包和App安全的依據(jù),并不是鼓勵大家去重新打包別人的App,盜取他人勞動成果。
       本文首先介紹幾種Android反編譯工具的使用,然后實現(xiàn)在不需要知道源代碼的情況下,僅通過修改反編譯得到的smali文件實現(xiàn)修改apk邏輯功能的目的。
Android中常用的反編譯工具有三個:dex2jar、jd-gui和apktool,這三個工具的作用如下:
dex2jar:將apk中的classes.dex文件轉(zhuǎn)換成jar文件。
jd-gui:查看由dex2jar轉(zhuǎn)換成的jar文件,以界面的形式展示反編譯出來的Java源代碼。
apktool:反編譯生成smali字節(jié)碼文件,提取apk中的資源文件。
為了盡可能的把問題講清楚,我們來實現(xiàn)一個很簡單的例子。首先創(chuàng)建一個工程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工具的目錄下,然后執(zhí)行命令

會在當(dāng)前目錄下生成class-dex2jar.jar文件

然后打開jd-gui,將class-dex2jar.jar文件拖進(jìn)去,就可以看到反編譯出來的源代碼。

可以看到反編譯的代碼和原本的代碼差別不大,主要差別是原來的資源引用全都變成了數(shù)字。
下面我們來修改這個apk的內(nèi)容。
首先我們將apk拷貝到apktool工具目錄下,執(zhí)行命令apktool d app-release.apk。

生成的目錄中包含smali文件夾

然后找到我們的主要的類MainActivity.smali,文件內(nèi)容如下:
.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行是打印日志的位置,文件內(nèi)容很清晰,每個區(qū)域的意義如下:
.class 類名
.super 父類名
.source 文件名
.implements 這個類實現(xiàn)的接口
.field 成員變量
.method 方法
然后新建一個工程,在這個工程中實現(xiàn)想要替換的代碼,我們這里是希望將原始工程中打印日志的地方替換為彈出一個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,"我是反編譯后進(jìn)行的修改。",Toast.LENGTH_LONG).show();  }}然后像前面一樣執(zhí)行apktool命令,生成的smali文件內(nèi)容如下:
.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文件的行號來修改。我認(rèn)為只要保證方法內(nèi)的行號不亂序,并且方法之間的行號不沖突就可以。然后,需要將原始工程中打印日志的代碼替換為顯示Toast的代碼,也就是將原始smali文件中36-40行修改為新建工程中33、39-56行的內(nèi)容。修改后的內(nèi)容如下,主要關(guān)注下面內(nèi)容中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然后我們需要將修改后的文件目錄重新打包,執(zhí)行命令 apktool b app-release,就會在app-releae目錄下生成兩個文件夾:build 文件夾里面是一些中間文件(classes.dex等內(nèi)容),dist 文件夾里面存放著重新打包出來的apk文件。
最后還要記得對生成的apk進(jìn)行簽名,否則安裝時會報錯。執(zhí)行下面的命令行:
jarsigner -verbose -keystore viclee.keystore -signedjar app-release-signed.apk app-release.apk viclee.keystore 
-verbose 輸出簽名詳細(xì)信息 
-keystore 指定密鑰對的存儲路徑 
-signedjar 后面三個參數(shù)分別是簽名后的apk、未簽名的apk和密鑰對的別名
安裝簽名后的apk,點擊按鈕,確實彈出了Toast,內(nèi)容和我們所設(shè)置的一致,說明我們的修改成功了。
我們注意到,修改smali文件的時候并不是直接在文件上進(jìn)行修改,畢竟smali文件的可讀性差,直接修改是十分困難的。我們的解決辦法是新建一個工程將需要增加的代碼實現(xiàn),最好抽成一個單獨的方法(方便替換),然后將新工程打包產(chǎn)生的apk反編譯,得到對應(yīng)的smali文件,再用其中的內(nèi)容對原始smali文件進(jìn)行替換。這樣的修改方式降低了修改的難度也減小了犯錯誤的風(fēng)險。

另外,apk反編譯后也可以修改資源,將反編譯出來的資源文件修改一通,然后按照之前的方法,重新打包、簽名、安裝。下面兩個頁面是修改之前和修改之后的對比圖。


到這里,本文的全部內(nèi)容就講解完了,歡迎大家評論交流~
新聞熱點
疑難解答
圖片精選