Android逆向筆記之吾愛破解論壇上吧友分享的一個駕校路訓軟件的破解


一、 緣起

在吾愛破解上看到有人發了一個帖子:

https://www.52pojie.cn/thread-601768-1-1.html

於是自己也拿來練習一下,只是我的破解方式和樓主有些不同,僅供新手練習無思路時參考。


附件:

https://github.com/CC11001100/Android-RE-attachment-netdisk/blob/e138f6652d078f3b4bdaf5f51532d148365073cf/cnblogs/13975256/DriverSchool.apk


二、觀察

把樓主分享的apk文件下載下來拖到夜神模擬器中安裝啟動:

1

未注冊的情況下應用一啟動就會彈出一個注冊彈窗,要么輸入正確的注冊碼單擊“注冊”按鈕,要么單擊“退出”按鈕就會直接退出程序,OK,接下來就是想辦法能夠使用這個軟件。


三、 思路一:想辦法得到正確的注冊碼

觀察上面的注冊對話框界面,發現有一個叫做本機機器碼的字段,很容易想到注冊碼很可能是與本機機器碼綁定的,比如根據本機機器碼計算而來,針對這種注冊碼式的驗證一般破解套路都是先隨便輸入點東西,然后看下注冊失敗時候有啥提示,然后根據這個提示去定位到判斷是否注冊成功的那段代碼,然后再根據判斷邏輯決定如何破解。OK,看下注冊失敗信息:

1

紅框框起來的部分就是注冊失敗時的提示,這個提示還很個性,一般應用的提示都是Toast之類的,這個是有一個專門的應該是TextView來顯示的,不管它,我們把失敗信息比着打一下在這里記一下,因為等下要用到這段文本去搜索,不用那么實在全部搞出來,整一段有代表意義的就可以了,比如:

注冊碼有誤

然后把apk文件拖到改之理中打開:

1

我們先大體的看下smali下的包結構,就看到有個包名叫register的比較扎眼,展開看看這下面是什么東西:

1

...怎么可以這么輕易就找到呢,一定是迷魂彈,smali代碼太冗長了,我們選中Register.smali然后單擊工具欄上的“打開Java源碼”方便閱讀:

1

這是Register的Java源碼:

package com.raul.driverschool.register;

import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface.OnKeyListener;
import android.content.DialogInterface;
import android.net.wifi.WifiManager;
import android.view.KeyEvent;
import android.view.View.OnClickListener;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.raul.driverschool.db.RegisterTable;
import com.raul.driverschool.init.SystemData;
import com.raul.driverschool.view.WelcomeActivity;

public class Register implements View.OnClickListener {
    private Button bnt_getcode;
    private Button bnt_register;
    private WelcomeActivity context;
    private Dialog dialog;
    private EditText et_code;
    private TextView tv_local_mac;
    private TextView tv_msg;
    private TextView tv_phone;

    public Register(WelcomeActivity arg1) {
        this.context = arg1;
    }

    public String getCode(String arg8) {
        char[] chars = arg8.toCharArray();
        StringBuffer buffer = new StringBuffer("");
        int i;
        for(i = chars.length - 1; i >= 0; --i) {
            buffer.append(Integer.toHexString((Integer.parseInt(String.valueOf(chars[i]), 16) + (i + 1) * 999) % 16));
        }

        return buffer.toString();
    }

    public String getLocalMacAddress() {
        WifiManager wifi = (WifiManager)this.context.getSystemService("wifi");
        wifi.setWifiEnabled(true);
        return wifi.getConnectionInfo().getMacAddress().replace(":", "").substring(4, 12);
    }

    public Dialog getRegisterDialog(Context arg6) {
        AlertDialog.Builder builder = new AlertDialog.Builder(arg6);
        View v = LinearLayout.inflate(arg6, 0x7F030008, null);  // layout:register_dialog
        this.bnt_register = (Button)v.findViewById(0x7F09001E);  // id:bnt_register
        this.bnt_getcode = (Button)v.findViewById(0x7F09001F);  // id:bnt_get_code
        this.tv_msg = (TextView)v.findViewById(0x7F09001D);  // id:tv_msg
        this.tv_local_mac = (TextView)v.findViewById(0x7F09001B);  // id:tv_local_mac
        this.tv_phone = (TextView)v.findViewById(0x7F09001A);  // id:register_title
        this.et_code = (EditText)v.findViewById(0x7F09001C);  // id:et_code
        this.bnt_getcode.setOnClickListener(this);
        this.bnt_register.setOnClickListener(this);
        this.tv_local_mac.setText(this.getLocalMacAddress());
        this.tv_phone.setText("您還沒有注冊,請撥打" + SystemData.getPhone() + "索要注冊碼。");
        builder.setCancelable(false);
        builder.setOnKeyListener(new DialogInterface.OnKeyListener() {
            @Override  // android.content.DialogInterface$OnKeyListener
            public boolean onKey(DialogInterface arg2, int arg3, KeyEvent arg4) {
                return arg3 == 4;
            }
        });
        builder.setView(v);
        this.dialog = builder.create();
        return this.dialog;
    }

    @Override  // android.view.View$OnClickListener
    public void onClick(View arg5) {
        if(arg5.equals(this.bnt_register)) {
            String code = this.et_code.getText().toString();
            if(code.trim().length() > 0) {
                if(code.trim().equals(this.getCode(this.getLocalMacAddress()))) {
                    RegisterTable.save(this.context, code, this.getLocalMacAddress());
                    Toast.makeText(this.context, "恭喜!已經注冊成功!", 1).show();
                    this.dialog.dismiss();
                    this.context.initSystem();
                    return;
                }

                this.tv_msg.setText("注冊碼有誤,請重新輸入!");
                this.tv_msg.setVisibility(0);
                return;
            }

            this.tv_msg.setText("請先輸入注冊碼!");
            this.tv_msg.setVisibility(0);
            return;
        }

        if(arg5.equals(this.bnt_getcode)) {
            System.exit(0);
            return;
        }
    }
}

這就有點尷尬了,那我之前保存的字符串豈不是用不到了,我們先假裝沒有找到源碼,因為這個僅僅只是根據經驗得到的,並不一定是完全可靠的,接下來使用比較可靠地方式來重新找一遍,還是上面保存的注冊失敗字符串,在“搜索和替換”中嘗試搜索:

1

沒有搜索到,這是因為smali反編譯的中文有的是unicode編碼的,讓我們把這段文字轉為unicode編碼然后再搜索一次:

1

好了,這次搜索到了一個結果,雙擊這個結果可以定位到對應位置,發現這個文件就是Register.smali,所以最終還是回到了Register.java這個文件,接下來就是對這個文件反編譯為Java后的源碼進行分析,先看下“注冊碼有誤”附近的代碼,因為注冊成功的代碼也在這附近,可以看到這段邏輯主要是對我們的輸入做個判斷,如果我們輸入的注冊碼OK的話就保存注冊信息,同時彈一個Toast提示注冊成功,否則就是在TextView上顯示注冊失敗:

1

然后計算注冊碼的時候用到了一個getLocalMacAddress,看下這個方法:

1

懷疑這個東西是和顯示的“本機機器碼”是相同的:

1

找一下“本機機器碼”這個字段是怎么來的:

1

在展示對話框的時候就是調用了getLocalMacAddress()方法得到的,因為界面上已經顯示了這個方法的值,並且它是冪等的,多次計算都會得到相同的值,因此對我們有用的值在界面已經能夠得到,記下那個值就可以,后面還可能會用到,至於這個方法則可以忽略了。

然后是getCode方法,計算我們的注冊碼:

1

破解的話寫腳本一般Python用得比較多,但是因為這里反編譯到的源碼就是Java的,我們用Java直接拷貝運行會比較方便,所以新建一個Java類把代碼拷過去,然后把我們自己的機器碼傳進去跑一下就能得到注冊碼了:

package cc11001100.kotlin.study.basic.basicTypes;

/**
 * @author CC11001100
 */
public class ComputeSignCode {

    /**
     * 根據機器碼計算注冊碼
     *
     * @param arg8 本機機器碼
     * @return
     */
    public static String getCode(String arg8) {
        char[] chars = arg8.toCharArray();
        StringBuffer buffer = new StringBuffer("");
        int i;
        for (i = chars.length - 1; i >= 0; --i) {
            buffer.append(Integer.toHexString((Integer.parseInt(String.valueOf(chars[i]), 16) + (i + 1) * 999) % 16));
        }

        return buffer.toString();
    }

    public static void main(String[] args) {
        System.out.println(getCode("18D49BFD")); // 505c0268
    }

}

最后得到注冊碼:

505c0268

回到夜神模擬器,輸入注冊碼試一下看能不能注冊成功:

1

OK,彈出了一個Toast的提示注冊成功,等待一會兒之后就進去了軟件的教學界面,不過接下來的事情已經不感興趣了,至此收手。

1


四、 思路二:把注冊驗證框干掉重新打包

還有一種思路是硬剛一下,想辦法把注冊驗證框干掉,上面我們發現這個注冊框是在啟動應用的時候就顯示的,那么我們順着應用啟動的流程捋一下應該就能找到這個注冊檢查邏輯了,首先打開MainActivity.xml,看下啟動類是哪個:

1

根據配置文件可知,啟動類是com.raul.driverschool.view. WelcomeActivity,接下來就是看下這個類的代碼,先看下onCreate:

1

很容易就找到了這個位置,紅框框起來的部分就是對是否注冊做校驗的。

接下來的目標就比較明確了,修改WelcomeActivity.smali文件,將校驗的流程直接刪掉,只保留初始化系統的流程就可以,如下圖,划對號的行就被保留,打叉號的行將被刪除:

1

回到改之理,打開WelcomeActivity.smali,定位到onCreate方法,直接拖到最后,對照着Java的代碼邏輯將多余部分刪掉:

1

然后重新編譯打包:

1

編譯打包后的apk位置會顯示在控制台上,找到這個apk包,然后重新安裝,啟動應用,會發現不會再彈出注冊彈窗了,但也僅僅是不會再彈出彈窗了,如果后面的請求還會對是否注冊做校驗的話,比如每次與服務器的請求交互數據都要校驗注冊信息,那我們沒有注冊信息可能就會悲劇,當然那是后面的事情了,流程太過冗長沒興趣去一一驗證,讀者知道干掉彈窗不等於萬事大吉即可,其實在有選擇的情況下應該盡量去采用生成注冊碼的形式,即優先嘗試去按照軟件本身正常的流程去走能避免一些坑浪費時間,本次練習就此結束。


附件(去掉注冊框的軟件):

https://github.com/CC11001100/Android-RE-attachment-netdisk/blob/e138f6652d078f3b4bdaf5f51532d148365073cf/cnblogs/13975256/ApkIDE_DriverSchool.apk


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM