Android開發(fā)apk反編譯和二次打包教程
作為Android開發(fā)者,工作中少不了要反編譯別人的apk,當(dāng)然主要目的還是為了學(xué)習(xí)到更多,取彼之長,補(bǔ)己之短。今天就來總結(jié)一下Android反編譯和二次打包的一些知識。首先聲明本文的目的是為了通過例子講解反編譯和二次打包的原理和方法,繼而作為后續(xù)講解防止二次打包和App安全的依據(jù),并不是鼓勵大家去重新打包別人的App,盜取他人勞動成果。
本文首先介紹幾種Android反編譯工具的使用,然后實(shí)現(xiàn)在不需要知道源代碼的情況下,僅通過修改反編譯得到的smali文件實(shí)現(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中的資源文件。
為了盡可能的把問題講清楚,我們來實(shí)現(xiàn)一個很簡單的例子。首先創(chuàng)建一個工程DecompileDemo,在MainActivity中定義一個布局,其中包含一個Button,點(diǎn)擊會打印一段日志。
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 這個類實(shí)現(xiàn)的接口
.field 成員變量
.method 方法
然后新建一個工程,在這個工程中實(shí)現(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,點(diǎn)擊按鈕,確實(shí)彈出了Toast,內(nèi)容和我們所設(shè)置的一致,說明我們的修改成功了。
我們注意到,修改smali文件的時候并不是直接在文件上進(jìn)行修改,畢竟smali文件的可讀性差,直接修改是十分困難的。我們的解決辦法是新建一個工程將需要增加的代碼實(shí)現(xiàn),最好抽成一個單獨(dú)的方法(方便替換),然后將新工程打包產(chǎn)生的apk反編譯,得到對應(yīng)的smali文件,再用其中的內(nèi)容對原始smali文件進(jìn)行替換。這樣的修改方式降低了修改的難度也減小了犯錯誤的風(fēng)險。

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


到這里,本文的全部內(nèi)容就講解完了,歡迎大家評論交流~
相關(guān)文章
Android ormlite更改數(shù)據(jù)庫默認(rèn)位置
本文主要介紹Android ormlite,這里提供實(shí)例代碼并詳細(xì)說明了 ormlite更改數(shù)據(jù)庫默認(rèn)位置,有需要的朋友可以參考下2016-07-07
Android開發(fā)實(shí)現(xiàn)加載網(wǎng)絡(luò)圖片并下載至本地SdCard的方法
這篇文章主要介紹了Android開發(fā)實(shí)現(xiàn)加載網(wǎng)絡(luò)圖片并下載至本地SdCard的方法,涉及Android圖片文件的讀取、保存及權(quán)限相關(guān)操作技巧,需要的朋友可以參考下2018-01-01
Android編程設(shè)計(jì)模式之狀態(tài)模式詳解
這篇文章主要介紹了Android編程設(shè)計(jì)模式之狀態(tài)模式,結(jié)合實(shí)例形式詳細(xì)分析了Android狀態(tài)模式的概念、功能、使用方法及相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-12-12
Android編程實(shí)現(xiàn)打勾顯示輸入密碼功能
這篇文章主要介紹了Android編程實(shí)現(xiàn)打勾顯示輸入密碼功能,涉及Android控件布局及屬性相關(guān)操作技巧,需要的朋友可以參考下2017-02-02
Android學(xué)習(xí)項(xiàng)目之簡易版微信為例(一)
這篇文章主要以簡易版微信為例,為大家介紹了Android簡易版微信項(xiàng)目的基礎(chǔ)知識,感興趣的小伙伴們可以參考一下2016-06-06
Android仿微信實(shí)現(xiàn)首字母導(dǎo)航條
這篇文章主要為大家詳細(xì)介紹了Android仿微信實(shí)現(xiàn)首字母導(dǎo)航條的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-08-08
Android利用Intent實(shí)現(xiàn)記事本功能(NotePad)
這篇文章主要為大家詳細(xì)介紹了Android利用Intent實(shí)現(xiàn)簡單記事本功能(NotePad)的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-06-06
android開發(fā)教程之實(shí)現(xiàn)listview下拉刷新和上拉刷新效果
這篇文章主要介紹了android實(shí)現(xiàn)listview下拉刷新和上拉刷新效果,Android的ListView上拉下拉刷新,原理都一樣,在Touch事件中操作header/footer的paddingTop屬性,需要的朋友可以參考下2014-02-02
Android?RecyclerBarChart繪制使用教程
這篇文章主要為大家介紹了Android?RecyclerBarChart繪制使用教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

