Android的代碼安全那些事

Android的代碼安全那些事

作爲一個正經八百的IT碼農,我們有很多途徑可以提高我們的“碼農”能力,比如說,看書,比如說,瀏覽技術網站,比如說多寫代碼。其實根據我的經驗呢,還有一種方式–讀別人的代碼。

大家都知道啊,我們中國人,有個能力挺厲害的,你們猜是啥?

讀代碼也是有很多途徑,比如說下載下載Demo,讀讀github的代碼,讀讀同事的代碼。其實還有一種方式的,反編譯商業軟件。(不要學壞噢。)

所以說,這是一個不正經的技術分享。

這裏寫圖片描述

爲啥呢?總結來說,其實就是借鑑。

爲啥要反編譯,其實真的不是要獲取啥機密資料,幹些見光死的勾當。我們其實看看別人都幹了些啥。

衆所周知,我們人的進化,都是從模仿的過程進化而來的(廢話)。還有做好我們自身應用的安全工作,防止比如API被破解啊,核心的一些內容被人知道,爲了更好的爲了自身的安全,所以去學習。

說到反編譯呢,咱們就得先說說編譯過程。

Java編譯過程

Android的虛擬機是Dalvik,那麼我們在非Android平臺上默認使用的是Hotspot虛擬機,不僅有Hotsport這個虛擬機實現,其他的還包括JRocket(BEA),j9(IBM),Microsoft JVM等等等等。

各種虛擬機在編譯方式,指令集的方式,編譯優化,內存管理等上的差別是很大。但是都是根據《Java虛擬機規範》約定開發的,他們的基本的數據管理結構是一樣的。

java代碼在在正常的編譯中,需要經過以下一些步驟,形成java字節碼
這裏寫圖片描述

而,Dalvik虛擬機和其他Java虛擬機除了以上的編譯步驟是相同的,還是有很多差別的,比如:Dalvik 基於寄存器,而 JVM 基於棧,而由於Dalvik採用的是基於寄存器,所以整個優化和加載,就跟其他的JVM的實現有很多不同。Dalvik虛擬機在編譯的時候會將Java字節碼轉爲Dalvik虛擬機可運行的Dalvik字節碼。

這裏寫圖片描述

詳細可參考 http://blog.csdn.net/dd864140130/article/details/52076515

Android的打包編譯過程

這裏寫圖片描述

1.Java編譯器對工程本身的java代碼進行編譯,這些java代碼有三個來源:app的源代碼,由資源文件生成的R文件(aapt工具),以及有aidl文件生成的java接口文件(aidl工具)。產出爲.class文件。

①.用AAPT編譯R.java文件
②編譯AIDL的java文件
③把java文件編譯成class文件

2..class文件和依賴的三方庫文件通過dex工具生成Dalvik虛擬機可執行的.dex文件,包含了所有的class信息,包括項目自身的class和依賴的class。產出爲.dex文件。

3.apkbuilder工具將.dex文件和編譯後的資源文件生成未經簽名對齊的apk文件。這裏編譯後的資源文件包括兩部分,一是由aapt編譯產生的編譯後的資源文件,二是依賴的三方庫裏的資源文件。產出爲未經簽名的.apk文件。

4.分別由Jarsigner和zipalign對apk文件進行簽名和對齊,生成最終的apk文件。

總結爲:編譯–>DEX–>打包–>簽名和對齊

反編譯的過程就是前面提到編譯的過程的逆向

說了那麼多的編譯過程,那麼如何反編譯呢。

首先需要逆向aapt的過程。這時候要用到apktools的工具。

單獨的dex要解析成smali文件需要用到 baksmali工具,如果你要需要smali工具。

如果你還要將dex轉爲jar,需要用到dex2jar工具。

如果你還要查看jar的代碼,還需要用到jd-gui。

需要單獨解析編譯後的AXML(Android Binary XML),需要用到AXMLPrinter去解碼

啊。。工具還是有點多的,其實網上有人已經整理了以上的工具,長這樣子的

這裏寫圖片描述

這裏寫圖片描述
回到我們文章的開頭,我們如果想說去學習裏面的一些原理,使用的框架,結構。把dex轉爲jar,再查看jar裏面的代碼,jd-gui的破解效率是比較低的。經常看到一些邏輯混亂的情況,其實我們的Android Studio 也是一個非常好用,反編譯源代碼的利器。

這裏寫圖片描述

相對而言,Android studio 帶的反編譯工具,邏輯能夠原模原樣的還原出來,但是仍會存在一些文件,反編譯不出來。

這時候,記得從dex入手,前面說過,dalvik虛擬機是基於寄存器(register)的,那麼他的運行指令是跟寄存器掛鉤的。Dex不僅可以反編譯成字節碼文件,還可以反編譯成爲一種跟寄存器相關的文件—-Smali。通俗的說,smali語言是Dalvik的反彙編語言。

在學習smali之前,對寄存器有點了解是能夠更好的學習,分析邏輯。

以下是一個我寫的Demo的原先代碼。

package test.lemon.decompile;
import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
    Button button;
    String string = "顯示一段話";
    public static int count = 0 ;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.bt_check);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                test(MainActivity.this,string);
            }
        });
    }
    public void test(Context context,String msg){
        msg +=string;
        button.setText(msg);
    }
}

界面是這樣子的,點擊按鈕之後,方法中的一句話跟MainActivity當中的String拼接。這裏寫圖片描述

這是一個反編譯之後的代碼

.class public Ltest/lemon/decompile/MainActivity; #說明類的位置
.super Landroid/support/v7/app/AppCompatActivity;#父類
.source "MainActivity.java"#源文件名稱


# static fields
.field public static count:I #定義一個 公共 靜態 int 名稱爲count的字段


# instance fields
.field button:Landroid/widget/Button;#定義一個android/widget/Button類型的 名稱爲button

.field string:Ljava/lang/String;


# direct methods
.method static constructor <clinit>()V #接口初始化方法(名爲<clinit>)。在存在靜態變量,類變量(本來中)的時候,會執行這個構造函數
    .locals 1

    .prologue
    .line 16
    const/4 v0, 0x0 #在寄存器聲明一個 “引用” v0 將0x0賦予 給v0

    sput v0, Ltest/lemon/decompile/MainActivity;->count:I #用sput操作,將寄存器 v0賦予給count

    return-void
.end method

.method public constructor <init>()V #實例初始化方法(名爲<init>)
    .locals 1

    .prologue
    .line 11
    invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V #調用父類的構造函數

    .line 15
    const-string v0, "\u663e\u793a\u4e00\u6bb5\u8bdd"  # unicode碼 翻譯過來就是 顯示一句話

    iput-object v0, p0, Ltest/lemon/decompile/MainActivity;->string:Ljava/lang/String; # 將常量值 賦予 給p0的寄存器地址,代指類中的string字段

    return-void
.end method


# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V #oncreate函數
    .locals 2
    .param p1, "savedInstanceState"    # Landroid/os/Bundle; 參數 p1

    .prologue
    .line 19
    invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V # 調用父類

    .line 21
    const v0, 0x7f04001b   # = 十進制 2130968603 java代碼中 ,R。layout.activity_main:

    invoke-virtual {p0, v0}, Ltest/lemon/decompile/MainActivity;->setContentView(I)V

    .line 25
    const v0, 0x7f0b005e    # findViewById 用到 是 R.id.bt_check

    # invoke-virtual 調用一個虛方法 
    invoke-virtual {p0, v0}, Ltest/lemon/decompile/MainActivity;->findViewById(I)Landroid/view/View;

    move-result-object v0

    #強制轉換結果
    check-cast v0, Landroid/widget/Button;

    iput-object v0, p0, Ltest/lemon/decompile/MainActivity;->button:Landroid/widget/Button;


    # .line 指令代碼原來的代碼行的位置,瞎改都沒事,改變了之後,在報錯,調試的時候,代碼行數位置不對。
    .line 26
    iget-object v0, p0, Ltest/lemon/decompile/MainActivity;->button:Landroid/widget/Button;

    new-instance v1, Ltest/lemon/decompile/MainActivity$1; # MainActivity 是指OnClickListener,實例化,賦給寄存器 v1

    invoke-direct {v1, p0}, Ltest/lemon/decompile/MainActivity$1;-><init>(Ltest/lemon/decompile/MainActivity;)V

    invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V

    .line 32
    return-void
.end method

.method public test(Landroid/content/Context;Ljava/lang/String;)V
    .locals 2
    .param p1, "context"    # Landroid/content/Context;
    .param p2, "msg"    # Ljava/lang/String;

    .prologue
    .line 35

    #注意,注意,原來的 string +=msg;在這裏進行了優化,變成了StringBuilder的實現方式,提高了效率
    new-instance v0, Ljava/lang/StringBuilder;

    invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V

    invoke-virtual {v0, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    iget-object v1, p0, Ltest/lemon/decompile/MainActivity;->string:Ljava/lang/String;

    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object p2

    .line 36
    iget-object v0, p0, Ltest/lemon/decompile/MainActivity;->button:Landroid/widget/Button;

    invoke-virtual {v0, p2}, Landroid/widget/Button;->setText(Ljava/lang/CharSequence;)V #將值 賦給了setText()

    .line 38
    return-void
.end method

三十行的代碼反編譯成smali之後一百二十行,去掉換行,源代碼跟smali代碼兩倍多的代碼的差距。

smali還是一種比較麻煩而且複雜的語法,但是細細的分析下去,我們能看到一些編譯過程之中的優化和語法的含義,還有虛擬機初始化的過程。

比如:

所做的優化

源代碼
public void test(Context context,String msg){
    msg +=string;
    button.setText(msg);

}

smali

.method public test(Landroid/content/Context;Ljava/lang/String;)V
    .locals 2
    .param p1, "context"    # Landroid/content/Context;
    .param p2, "msg"    # Ljava/lang/String;

    .prologue
    .line 35

    #注意,注意,原來的 string +=msg;在這裏進行了優化,變成了StringBuilder的實現方式,提高了效率

    new-instance v0, Ljava/lang/StringBuilder;

    invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V

    invoke-virtual {v0, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    iget-object v1, p0, Ltest/lemon/decompile/MainActivity;->string:Ljava/lang/String;

    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    move-result-object v0

    invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object p2
    .line 36
    iget-object v0, p0, Ltest/lemon/decompile/MainActivity;->button:Landroid/widget/Button;

    invoke-virtual {v0, p2}, Landroid/widget/Button;->setText(Ljava/lang/CharSequence;)V #將值 賦給了setText()

    .line 38
    return-void
.end method

該方法當中原來簡單的一句話 msg +=string; 已經被優化成爲StringBuilder的實現方式,從原來的一句話,變成了句操作。

再比如,我們細細的閱讀Smali代碼,嘗試去理解的時候 ,會發現兩個構造函數

# direct methods
.method static constructor <clinit>()V

.method public constructor <init>()V

他們之間所代表的意義是這樣子的。

構造函數是靜態的 ,裏面包含的一些代碼,也是對靜態的count字段進行初始化,所代表的意義是“ 裝載一個類初始化的時候調用”。就是虛擬機在load一個類的時候,就調用的。。。哦~,原來靜態變量,是有單獨的構造函數初始化的。

而 也是一個構造函數,這個就是我們常見的構造函數。裏面也是對我們類塊中的初始化,放到了構造函數當中進行初始化。

這裏寫圖片描述

看了Smali的語法之後,其實幹點“調試”的事。

我們原來的test方法中,拼接前面一串字符串,然後丟給button進行顯示。現在如果說不丟給button顯示,而是要用toast進行顯示,我們在test函數下,修改相應的smali文件,重新打包。即可實現。

.line 28
const/4 v0, 0x0
# 調用Toast的靜態方法 將參數 p1,p2,v0餵給makeText
 invoke-static {p1, p2, v0}, 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

這裏寫圖片描述

即重新生成了新的程序,包含了我們hook之後的代碼。

可見,一個軟件要被篡改是多麼的容易!!!

不僅如此,常見的so庫,也可以用IDEA PRO去偵探邏輯,修改邏輯。

所以,可見一個應用的安全是很重要的。

文章如果錯誤或者描述錯誤,請指出。

文章部分參考文獻

《深入理解Java虛擬機 JVM高級特性與最佳實踐》2版 周志明

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章