從零開始分析第一個簡單的Android程序

<<Android軟件安全權威指南>>筆記

第二章 如何分析Android程序

編寫一個Android程序

新建一個Android工程,名稱爲Crackme0201

主界面添加用戶名和註冊碼輸入框

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity"
    android:id="@+id/linearLayout">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/top_string"
        android:textAlignment="center"
        android:gravity="center_horizontal" />


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/username"/>

        <EditText
            android:id="@+id/edit_username"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:ems="10"
            android:layout_marginRight="10dp"
            android:layout_marginLeft="10dp"
            android:layout_weight="1"
            android:hint="@string/hint_username"/>

    </LinearLayout>


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/regCode"/>

        <EditText
            android:id="@+id/edit_regCode"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:ems="10"
            android:hint="@string/hint_regCode"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:layout_weight="1" />

    </LinearLayout>

    <Button
        android:id="@+id/button_register"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/btn_register"
        android:layout_marginRight="10dp"
        android:layout_gravity="right"/>

</LinearLayout>

界面

然後,編寫MainActivity類的代碼,添加一個checkSN()方法

    private boolean checkSN(String userName, String sn) {
        try {
            if ((userName == null) || (userName.length() == 0)) {
                return false;
            }

            if ((sn == null) || (sn.length() != 16)) {
                return false;
            }

            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.reset();
            digest.update(userName.getBytes());
            byte[] bytes = digest.digest();
            String hexstr = toHexString(bytes, "");
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < hexstr.length(); i+=2) {
                stringBuilder.append(hexstr.charAt(i));
            }
            String userSN = stringBuilder.toString();
            //Log.d("crackme", hexstr);
            //Log.d("crackme", userSN);

            if (!userSN.equalsIgnoreCase(sn)) {
                return false;
            }

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

該方法計算用戶名與註冊碼是否匹配,用MD5計算用戶名的散列值,轉換爲hex串,128bit轉換爲32位十六進制串,再取32位中的奇數位,拼成16位,即爲註冊碼。

最後判斷傳入的註冊碼與計算得出的註冊碼是否匹配。

MainActivity中的onCreate()方法中添加註冊按鈕點擊事件

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        setTitle(R.string.unregister);
        edit_userName = (EditText) findViewById(R.id.edit_username);
        edit_sn = (EditText) findViewById(R.id.edit_regCode);
        btn_register = (Button) findViewById(R.id.button_register);
        btn_register.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!checkSN(edit_userName.getText().toString().trim(),
                        edit_sn.getText().toString().trim())) {
                    Toast.makeText(MainActivity.this, R.string.unsuccessed,
                            Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(MainActivity.this, R.string.successed,
                            Toast.LENGTH_SHORT).show();
                    btn_register.setEnabled(false);
                    setTitle(R.string.register);
                }
            }
        });

    }

如果匹配,則彈出註冊成功,如果不匹配,則彈出註冊無效的提示

編譯APK文件

有兩種編譯方式,使用命令行和AS的Build菜單

命令行

gradlew是基於task來編譯項目的,在命令行下執行gradlew tasks會列出所有支持的task,我們主要關注編譯使用的Build tasks和安裝APK時用的Install tasks

生成Debug調試版本的APK:./gradlew assembleDebug,會在Crackme0201/app/build/outputs/apk目錄下生成app-debug.apk文件

安裝Debug版本APK:./gradlew installDebug,會安裝到與系統相連的設備/模擬器

adb devices,列出當前連接的設備,包括模擬器

AS編譯

直接點擊run app會自動編譯安裝

只編譯不安裝點make project

破解第一個Android程序

以Crackme0201爲例

通常方法是,使用ApkTool反編譯APK文件,生成smali格式的反彙編代碼,通過閱讀smali代碼理解程序運行機制,找到突破口進行修改,再使用ApkTool工具重新編譯爲APK並簽名,進行測試

上述過程循環,直到完全破解

實際中還可以用IDA Pro直接分析APK文件,使用dex2jarjd-gui配合進行Java源碼級分析

反編譯APK文件

下載ApkTool工具,添加路徑到PATH環境變量,命令行直接使用

apktool d ./apk-debug.apk -o outdir

會在outdir文件夾下生成反編譯文件,smali目錄下是程序的所有反彙編代碼,res目錄下是程序所有資源文件,子目錄的文件結構與開發源碼一致。

分析APK文件

找到突破口,一般是錯誤提示信息,屬於字符串資源

APK打包時,string.xml文件被加密存儲爲resources.arsc文件,反編譯會解密

string.xml中的所有字符串資源都在gen/R.java文件中的String類中標識,每個字符串都有int類型的索引值,對應關係在同目錄下的public.xml文件中

找到unsuccessed字符串和對應索引值

索引

但是搜索文件夾下沒有與索引值相關的信息

接下來生成Release版本APK文件

用AS進行簽名生成app-release.apk,再用apktool進行反編譯

找到unsuccessed對應的索引值,全局搜索,在MainActivity$1.smali中找到了索引值

.line 33處調用checkSN()(但是這裏是access$000,沒有#備註,不知道爲啥)

    .line 33
    invoke-static {p1, v0, v1}, Lorg/nuaa/crackme0201/MainActivity;->access$000(Lorg/nuaa/crackme0201/MainActivity;Ljava/lang/String;Ljava/lang/String;)Z

    move-result p1

    const/4 v0, 0x0

    if-nez p1, :cond_0

意思是,返回Boolean類型值(哪裏看出來的?)

返回結果保存在p1

move-result p1

忽略掉第二行,對寄存器p1進行判斷,其值不爲0,即條件爲真則跳轉cond_0處

const/4 v0, 0x0

if-nez p1, :cond_0

繼續往下看,不跳轉會執行

    .line 35
    iget-object p1, p0, Lorg/nuaa/crackme0201/MainActivity$1;->this$0:Lorg/nuaa/crackme0201/MainActivity;

    const v1, 0x7f0b0032

    invoke-static {p1, v1, v0}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;

    move-result-object p1

    .line 36
    invoke-virtual {p1}, Landroid/widget/Toast;->show()V

    goto :goto_0

.line 35處事要iget-object指令獲取MainActivity的實例的引用,-this$0是內部類MainActivity$1中的一個synthetic字段,存儲父類MainActivity的引用

const v1, 0x7f0b0032

該指令是將v1寄存器傳入0x7f0b0032,也就是unsuccessed的id值,接下來調用Toast;->makeToast()

如果跳轉cond_0,則執行如下指令

    .line 38
    :cond_0
    iget-object p1, p0, Lorg/nuaa/crackme0201/MainActivity$1;->this$0:Lorg/nuaa/crackme0201/MainActivity;

    const v1, 0x7f0b002f

    invoke-static {p1, v1, v0}, Landroid/widget/Toast;->makeText(Landroid/content/Context;II)Landroid/widget/Toast;

    move-result-object p1

    .line 39
    invoke-virtual {p1}, Landroid/widget/Toast;->show()V

    .line 40
    iget-object p1, p0, Lorg/nuaa/crackme0201/MainActivity$1;->this$0:Lorg/nuaa/crackme0201/MainActivity;

    iget-object p1, p1, Lorg/nuaa/crackme0201/MainActivity;->btn_register:Landroid/widget/Button;

    invoke-virtual {p1, v0}, Landroid/widget/Button;->setEnabled(Z)V

    .line 41
    iget-object p1, p0, Lorg/nuaa/crackme0201/MainActivity$1;->this$0:Lorg/nuaa/crackme0201/MainActivity;

    const v0, 0x7f0b002c

    invoke-virtual {p1, v0}, Lorg/nuaa/crackme0201/MainActivity;->setTitle(I)V

    :goto_0
    return-void

這裏的代碼是註冊成功時候的邏輯

修改smali文件代碼

通過上述分析,,line 32處的代碼,if-nez p1, :cond_0是跳轉關鍵點

這是Dalvik指令集的一個條件跳轉指令(和彙編差不多),應該是not equal zero,不等於0的意思,相反的指令是if-eqz,表示等於0時跳轉

就用if-eqz代替if-nez,保存修改

if-eqz p1, :cond_0
重新編譯APK並簽名

修改好以後,使用apktool重新編譯打包

apktool b outdir_rel

outdir_rel/dist目錄下生成apk文件

要安裝還需要簽名,之前簽名的時候生成了證書,我們可以用命令行來對apk進行簽名

apksigner sign --ks [xxx.jks] --out [xxx.apk] [signed.apk]

apksigner是Android的build-tool中自帶的,--ks後接數字證書存儲路徑,--out後接簽名後apk輸出路徑,最後是原apk路徑

在Terminal下,模擬器開啓的情況下,推出原應用,輸入命令卸載

adb uninstall org.nuaa.crackme0201

再安裝簽名後的apk

adb install signed.apk

安裝成功,測試隨意的用戶名+註冊碼都可以驗證成功。

破解成功

總結

一般流程

反編譯 -> 分析 -> 修改 -> 回編譯 -> 簽名

集成工具:MAC下Android-Crack-Tool,Windows下Android Killer

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