Fork me on GitHub

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

posted @ 2020-11-14 23:33  CC11001100  阅读(930)  评论(0编辑  收藏  举报