文章结构: 按照分析时的可疑类为单位划分

DeviceToken

调用链

  • PushAgentManager.initialize()​: 创建了一个线程, 运行了PushAgentManager.initialize.1(this)
  • PushAgentManager.initialize.1(this)​运行run()​, 其中PushAgentManager本身被传入到线程中, 又获取了manager​的mPushAgent​数据域

com.tanma.unirun.network.settings.MyInterceptor

包拦截器

大致逻辑:

  • APPKEY​: 是固定的
  • sign​: 根据APPKEY(常量), APPSECRET(常量), item(未分析), paramValue(未分析), 经过MD5加密后生成sign​值.

sign获取

抓包

  • 同一设备的sign相同
  • 不同的设备Pixel小米的sign不同

分析

主要是sign的生成流程, 但是中间流程反编译出来的结果并不理想, 需要慢慢分析

第一直觉

感觉可以Hook上StringBuilder​的append()​方法, 然后搜索APPKEY("389885588s0648fa"​)在那附近的数据就是我们要找的数据

com.tanma.unirun.ui.activity.login.LoginPresenterImpl (重点类)

思路:

  • 一开始的思路是抓包或者是找导入了net包的文件, 但是这种方式效率实在太低
  • 所以直接找了activity.login包
  • 找到了LoginActivity
  • 在里面很多都看不懂...于是又是找导入的文件, 发现了导入Intent, 因为LoginActivity里面的内容实在太少, 所以可以猜到应该使用了Service, Intent等等方式将逻辑放到其他地方了.
  • 最后找到了LoginPresenterImpl类(这个presenter提供者最好记住, 后面可以根据名字一眼就看出来了)

LoginActivity调用

在LoginActivity中直接调用了login()静态方法, 所以优先查看login()方法

((LoginPresenter)this.getMPresenter()).login();

方法

login()

分析:

  • 看了前面的东西基本都是初始化
  • LoginBody body = new LoginBody();​开始, 就是构造登录报文实体体的部分了
  • 其中需要注意的是MD5Digest​这个类是程序自定义的, 怀疑在里面修改了算法, 不一定是标准的md5 (下一阶有分析复现)
  • 跟进MD5Digest
    public void login() {
        CheckBox checkBox0 = (CheckBox)((FragmentActivity)this.getContext()).findViewById(id.checkbox);
        Intrinsics.checkExpressionValueIsNotNull(checkBox0, "context.checkbox");
        if(!checkBox0.isChecked()) {
            Toast.makeText(((Context)this.getContext()), "请同意“用户协议”和“隐私政策”", 0).show();
            return;
        }

        EditText AAAccount = (EditText)((FragmentActivity)this.getContext()).findViewById(id.et_loginAccount);
        Intrinsics.checkExpressionValueIsNotNull(AAAccount, "context.et_loginAccount");
        if(TextUtils.isEmpty(((CharSequence)AAAccount.getText()))) {
            TextView textView0 = (TextView)((FragmentActivity)this.getContext()).findViewById(id.tv_alert);
            Intrinsics.checkExpressionValueIsNotNull(textView0, "context.tv_alert");
            String s = this.accountEmpty;
            if(s == null) {
                Intrinsics.throwUninitializedPropertyAccessException("accountEmpty");
            }

            textView0.setText(((CharSequence)s));
            ((EditText)((FragmentActivity)this.getContext()).findViewById(id.et_loginAccount)).requestFocus();
            return;
        }

        EditText editText1 = (EditText)((FragmentActivity)this.getContext()).findViewById(id.et_password);
        Intrinsics.checkExpressionValueIsNotNull(editText1, "context.et_password");
        if(TextUtils.isEmpty(((CharSequence)editText1.getText()))) {
            TextView textView1 = (TextView)((FragmentActivity)this.getContext()).findViewById(id.tv_alert);
            Intrinsics.checkExpressionValueIsNotNull(textView1, "context.tv_alert");
            String s1 = this.pwdEmpty;
            if(s1 == null) {
                Intrinsics.throwUninitializedPropertyAccessException("pwdEmpty");
            }

            textView1.setText(((CharSequence)s1));
            ((EditText)((FragmentActivity)this.getContext()).findViewById(id.et_password)).requestFocus();
            return;
        }

        LoginBody body = new LoginBody();
        EditText account = (EditText)((FragmentActivity)this.getContext()).findViewById(id.et_loginAccount);
        Intrinsics.checkExpressionValueIsNotNull(account, "context.et_loginAccount");
        String account_ = account.getText().toString();
        if(account_ != null) {
            body.setUserPhone(StringsKt.trim(((CharSequence)account_)).toString());
            Companion mD5Digest$Companion0 = MD5Digest.Companion;
            EditText password = (EditText)((FragmentActivity)this.getContext()).findViewById(id.et_password);
            Intrinsics.checkExpressionValueIsNotNull(password, "context.et_password");
            String password_ = password.getText().toString();
            if(password_ != null) {
                body.setPassword(mD5Digest$Companion0.encodeByMD5(StringsKt.trim(((CharSequence)password_)).toString()));
                body.setDeviceToken(((String)new PreUtil("sp_name_app").getValue("sp_device_token", Reflection.getOrCreateKotlinClass(String.class), "")));
                body.setDeviceType("1");
                Context context0 = (Context)this.getContext();
                body.setAppVersions(APKVersionUtils.INSTANCE.getVersionName(context0));
                body.setMobileType(APKVersionUtils.INSTANCE.getSystemModel());
                body.setBrand(APKVersionUtils.INSTANCE.getDeviceBrand());
                body.setSysVersions(APKVersionUtils.INSTANCE.getSystemVersion());
                this.getModelImpl().login(body);
                return;
            }

            throw new TypeCastException("null cannot be cast to non-null type kotlin.CharSequence");
        }

        throw new TypeCastException("null cannot be cast to non-null type kotlin.CharSequence");
    }

其他资料

CheckBox

复选框控件, 用来选择"请同意“用户协议”和“隐私政策”"

Intrinsics类(内在的)

主要讲的是checkExpressionValueIsNotNull()​静态方法, 在用Kotlin编写的程序, 反编译出来的Java程序会出现该类.

Intrinsices是Kotlin内部的一个类, 包括了如下方法:

  • checkParameterIsNotNull()​ 检查参数是否为null
  • checkExpressionValueIsNotNull()​ 检查表达式结果是否是null
  • ...剩下的还没碰到, 就不展开了

checkExpressionValueIsNotNull()

检查表达式结果是否是null

Intrinsics.checkExpressionValueIsNotNull(checkBox0, "context.checkbox");

com.tanma.unirun.utils.MD5Digest

上一节中构造实体体用到的加密类

实现的是标准的md5加密, 没有魔改

方法

encodeByMD5()

        public final String encodeByMD5(String inputKey) {
            Intrinsics.checkParameterIsNotNull(inputKey, "inputKey");
            try {
                byte[] input = inputKey.getBytes(Charsets.UTF_8);
                Intrinsics.checkExpressionValueIsNotNull(input, "(this as java.lang.String).getBytes(charset)");
                MessageDigest messageDigest0 = MessageDigest.getInstance("MD5");
                Intrinsics.checkExpressionValueIsNotNull(messageDigest0, "MessageDigest.getInstance(\"MD5\")");
                messageDigest0.update(input);
                byte[] arr_b1 = messageDigest0.digest();
                char[] str = new char[arr_b1.length * 2];
                int k = 0;  // 经过了另外的加密
                for(int v1 = 0; v1 < arr_b1.length; ++v1) {
                    byte byte0 = arr_b1[v1];
                    int k = k + 1;
                    str[k] = MD5Digest.hexDigits[byte0 >>> 4 & 15];
                    k = k + 1;
                    str[k] = MD5Digest.hexDigits[((byte)(byte0 & 15))];
                }

                return new String(str);
            }
            catch(Exception ex) {
                return "";
            }
        }

算法复现

package com.lamecrow.APK.Unirun.Login;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;

public class Login {
    public static void main(String[] args) {
        String password = "1749571140";
        byte[] input = password.getBytes(StandardCharsets.UTF_8);
        char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};


        try {
            MessageDigest messageDigest0 = MessageDigest.getInstance("MD5");
            messageDigest0.update(input);

            byte[] midput = messageDigest0.digest();

            char[] str = new char[midput.length * 2];

            int k0 = 0;
            for (int v1 = 0; v1 < midput.length; ++v1) {
                byte element = midput[v1];
                int k1 = k0 + 1;
                str[k0] = hexDigits[element >>> 4 & 15];
                k0 = k1 + 1;
                str[k1] = hexDigits[element & 15];
            }

            System.out.println(new String(str));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}