首页 » 技术分享 » Android逆向工程:MIUI系统大揭秘:去不掉的小米账号!

Android逆向工程:MIUI系统大揭秘:去不掉的小米账号!

 

哈喽小伙伴们大家好~欢迎继续学习探讨MIUI系统的安全防范知识!在上篇博客中:Android逆向工程:带你领略MIUI系统的账号安全防范机制:账号是从哪里获取的?我们了解到了MIUI系统通过对关键代码进行封装进系统内,对外采用统一调用接口的方式来防止关键代码被破解窥视,保护了系统应用的安全,同时我们发现了获取账号信息的准确接口,那么MIUI系统除此之外,还有什么值得称道的安全防护措施呢?接着上篇博客我们尚未解决的问题:为什么刷机都无法刷掉之前已经登陆的小米账号?我们发现的那个PassportFindDeviceImpl类它真正的作用是什么?下面就带着这些疑问,来开启我们今天的学习吧!

首先还是有请我们今天的教案对象:我的小米。在下面的学习中,我们主要围绕“我的小米”进行分析和探讨。在此声明,本次讲解内容不可用于不正当破坏行为,学习技术为主,搞破坏是不可以的!

在上篇博客中,我们发现了获取小米账号的系统方法: ExtraAccountManager.getXiaomiAccount(this);,同时发现如果此方法返回为null的话,那么就代表着不存在小米账号,既然如此,那么我们就来尝试一下,如果我们拦截到之后把它的返回值修改为null会出现什么样的情况呢?

下面就开始修改我们的拦截代码:

XposedHelpers.findAndHookMethod("miui.accounts.ExtraAccountManager", loadPackageParam.classLoader, "getXiaomiAccount", Context.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("小米账号获取:抓到方法ExtraAccountManager->getXiaomiAccount()");

                Class classAccount=XposedHelpers.findClass("android.accounts.Account",loadPackageParam.classLoader);

                Field []fs=classAccount.getDeclaredFields();
                for (Field field:fs){
                    field.setAccessible(true);
                    XposedBridge.log("小米账号获取:Account类 参数"+field.getName()+"值为:"+field.get(param.getResult()));
                }

                param.setResult(null);
            }
        });

在拦截方法内我们设置返回结果为null,下面就看一下出现了什么状况:

很直接,手机直接被锁了!为什么会这样?看到这个界面,已经关联的小米账号,包括解锁编号,喜欢刷机的小伙伴们估计会很熟悉,因为对之前已经登陆小米账号的手机进行刷机的时候,刷机完成就会出现这个界面,提示你此手机有关联的账号,让你输入账号密码进行解锁! 这就是那个刷机也刷不掉的小米账号!

很不错,看来MIUI系统确实是有两把刷子。按照常理,刷机就是更换了一整个系统,其中也包括了那些存放关键信息的系统文件和系统内部数据库,在文件和数据库已经被更换,数据也被清空的情况下,这个关联的账号信息又是从哪里获取到的?MIUI系统又是怎么知道这个设备(手机)之前有登录的小米账号呢?带着这些疑问,我们接着往下逆向分析!

首先这里我们对该锁定界面进行界面元素分析:目标是上面那条String资源:此手机已经关联到小米账号(xxxx):

很不错,是一条TextView,id值为:find_device_status。看到这个id值我们心里差不多明白了七八分,为了验证我们的猜测,那就去看看这条id被引用的代码:使用jadx对此id值进行全局字符串搜索:

找到了,在类:LockedAccountLoginByFindDeviceFragment中。这个类的名字挺长,但是可以大致的看出它的功能: 通过查找设备然后锁定账号进行登陆的页面!很不错,名字起的倒是很直白啊~找了这个页面,下面我们就需要知道,这个页面是在什么地方被调用的?接下来该怎么查找?直接全局搜索LockedAccountLoginByFindDeviceFragment,看看它在别的类中是否有引用,有引用的地方估计就是他被调用的地方!使用Jadax全局搜索 LockedAccountLoginByFindDeviceFragment:

搜索发现,在整个“我的小米”项目中, LockedAccountLoginByFindDeviceFragment类只在LoginBaseFragment类的checkFindDeviceStatusIfNecessary()方法下被引用了,那估计被调用就是这里没跑了,我们过去看一下checkFindDeviceStatusIfNecessary()方法的代码:

protected void checkFindDeviceStatusIfNecessary() {
        if (PassportExternal.getPassportFindDeviceInterface() != null) {
            this.mCheckFindDeviceStatusTask = new CheckFindDeviceStatusTask.Builder(getActivity()).setCheckOperationFailedRunnable(new CheckOperationFailedRunnable() {
                public void run(String errorMessage) {
                    LoginBaseFragment.this.showCheckFindDeviceStatusFailedDialog(errorMessage);
                }
            }).setCheckOperationSuccessRunnable(new CheckOperationSuccessRunnable() {
                public void run(boolean isOpenFindDevice, String lockedUserId, String displayId) {
                    PassportStatHelper.statLoginCountEvent(StatConstants.CHECK_FIND_DEVICE_STATUS_SUCCESS, LoginBaseFragment.this.mOnSetupGuide);
                    if (isOpenFindDevice) {
                        LockedAccountLoginByFindDeviceFragment fragment = LockedAccountLoginByFindDeviceFragment.getLockedAccountLoginByFindDeviceFragment(lockedUserId, LoginBaseFragment.this.mServiceId, LoginBaseFragment.this.mOnSetupGuide, LoginBaseFragment.this.mFindPasswordOnPc, displayId);
                        fragment.setOnLoginInterface(LoginBaseFragment.this.mOnLoginInterface);
                        SysHelper.replaceToFragment(LoginBaseFragment.this.getActivity(), fragment, true);
                    }
                }
            }).build();
            this.mCheckFindDeviceStatusTask.executeOnExecutor(XiaomiPassportExecutor.getSingleton(), new Void[0]);
        }
    }

checkFindDeviceStatusIfNecessary()方法从名字上就可以大致猜出它的功能:如果必要的话就检查查找设备状态!我们关键看下面那个if判断中的代码:

if (isOpenFindDevice) {
                        LockedAccountLoginByFindDeviceFragment fragment = LockedAccountLoginByFindDeviceFragment.getLockedAccountLoginByFindDeviceFragment(lockedUserId, LoginBaseFragment.this.mServiceId, LoginBaseFragment.this.mOnSetupGuide, LoginBaseFragment.this.mFindPasswordOnPc, displayId);
                        fragment.setOnLoginInterface(LoginBaseFragment.this.mOnLoginInterface);
                        SysHelper.replaceToFragment(LoginBaseFragment.this.getActivity(), fragment, true);
                    }

这里判断一个isOpenFindDevice的布尔变量值,如果为true的话,那么就会创建一个LockedAccountLoginByFindDeviceFragment,接着就会开启那个锁屏页面!关键值在这个isOpenFindDevice变量,那这个isOpenFindDevice变量又是从哪来的呢?看这些代码:

.setCheckOperationSuccessRunnable(new CheckOperationSuccessRunnable() {
                public void run(boolean isOpenFindDevice, String lockedUserId, String displayId) {
                    PassportStatHelper.statLoginCountEvent(StatConstants.CHECK_FIND_DEVICE_STATUS_SUCCESS, LoginBaseFragment.this.mOnSetupGuide);
                    if (isOpenFindDevice) {
                        LockedAccountLoginByFindDeviceFragment fragment = LockedAccountLoginByFindDeviceFragment.getLockedAccountLoginByFindDeviceFragment(lockedUserId, LoginBaseFragment.this.mServiceId, LoginBaseFragment.this.mOnSetupGuide, LoginBaseFragment.this.mFindPasswordOnPc, displayId);
                        fragment.setOnLoginInterface(LoginBaseFragment.this.mOnLoginInterface);
                        SysHelper.replaceToFragment(LoginBaseFragment.this.getActivity(), fragment, true);
                    }
                }
            }).build();

我们发现这是开启了一个线程,线程名字为:CheckOperationSuccessRunnable,在这个线程里run方法内,isOpenFindDevice变量被传入!看下该线程的名字大致可以猜到:检查操作成功后进行的操作,原来这个是检查执行结束之后调用的,这个isOpenFindDevice变量实质上是一个检查结果的标志,true代表发现了关联的账号信息,然后开启锁屏页面,false代表没有发现关联账号信息,就会正常登录!

既然不是检查实现的线程,那么我们接着把目光转移到上面:

 this.mCheckFindDeviceStatusTask = new CheckFindDeviceStatusTask.Builder(getActivity()).setCheckOperationFailedRunnable(new CheckOperationFailedRunnable() {
                public void run(String errorMessage) {
                    LoginBaseFragment.this.showCheckFindDeviceStatusFailedDialog(errorMessage);
                }
            }).setCheckOperationSuccessRunnable(new CheckOperationSuccessRunnable() {
                public void run(boolean isOpenFindDevice, String lockedUserId, String displayId) {
                    PassportStatHelper.statLoginCountEvent(StatConstants.CHECK_FIND_DEVICE_STATUS_SUCCESS, LoginBaseFragment.this.mOnSetupGuide);
                    if (isOpenFindDevice) {
                        LockedAccountLoginByFindDeviceFragment fragment = LockedAccountLoginByFindDeviceFragment.getLockedAccountLoginByFindDeviceFragment(lockedUserId, LoginBaseFragment.this.mServiceId, LoginBaseFragment.this.mOnSetupGuide, LoginBaseFragment.this.mFindPasswordOnPc, displayId);
                        fragment.setOnLoginInterface(LoginBaseFragment.this.mOnLoginInterface);
                        SysHelper.replaceToFragment(LoginBaseFragment.this.getActivity(), fragment, true);
                    }
                }
            }).build();

看第一行代码,我们发现一个名字为:CheckFindDeviceStatusTask的线程被调起,后面setCheckOperationFailedRunnable方法看名字就会知道,这是线程执行失败后执行的方法,会开启一个叫做CheckOperationFailedRunnable线程,正好与刚才我们分析那个setCheckOperationSuccessRunnable方法和CheckOperationSuccessRunnable线程是对应的!这里就差不多明白了,执行检查操作的正是CheckFindDeviceStatusTask线程,它分别设置了检查失败和检查成功两个回调方法,那下面我们就去看看这个CheckFindDeviceStatusTask实现代码:

public class CheckFindDeviceStatusTask extends AsyncTask<Void, Void, PassportCheckFindDeviceResult> {
    private static final String PROGRESS_DIALOG_TAG = "CheckFindDeviceStatusTaskProgressDialog";
    private final Activity mActivity;
    private final CheckOperationFailedRunnable mCheckOperationFailedRunnable;
    private final CheckOperationSuccessRunnable mCheckOperationSuccessRunnable;
    private SimpleDialogFragment mProgressDialogFragment;

    public static class Builder {
        private Activity mActivity;
        private CheckOperationFailedRunnable mCheckOperationFailedRunnable;
        private CheckOperationSuccessRunnable mCheckOperationSuccessRunnable;

        public Builder(Activity activity) {
            this.mActivity = activity;
        }

        public Builder setCheckOperationFailedRunnable(CheckOperationFailedRunnable runnable) {
            this.mCheckOperationFailedRunnable = runnable;
            return this;
        }

        public Builder setCheckOperationSuccessRunnable(CheckOperationSuccessRunnable runnable) {
            this.mCheckOperationSuccessRunnable = runnable;
            return this;
        }

        public CheckFindDeviceStatusTask build() {
            return new CheckFindDeviceStatusTask(this.mActivity, this.mCheckOperationFailedRunnable, this.mCheckOperationSuccessRunnable);
        }
    }

    public interface CheckOperationFailedRunnable {
        void run(String str);
    }

    public interface CheckOperationSuccessRunnable {
        void run(boolean z, String str, String str2);
    }

    private CheckFindDeviceStatusTask(Activity activity, CheckOperationFailedRunnable checkOperationFailedRunnable, CheckOperationSuccessRunnable checkOperationSuccessRunnable) {
        this.mActivity = activity;
        this.mCheckOperationFailedRunnable = checkOperationFailedRunnable;
        this.mCheckOperationSuccessRunnable = checkOperationSuccessRunnable;
    }

    protected void onPreExecute() {
        this.mProgressDialogFragment = (SimpleDialogFragment) this.mActivity.getFragmentManager().findFragmentByTag(PROGRESS_DIALOG_TAG);
        if (this.mProgressDialogFragment == null) {
            this.mProgressDialogFragment = new AlertDialogFragmentBuilder(2).setMessage(this.mActivity.getString(R.string.passport_login_check_find_device)).create();
            this.mProgressDialogFragment.setCancelable(false);
            this.mProgressDialogFragment.show(this.mActivity.getFragmentManager(), PROGRESS_DIALOG_TAG);
        }
    }

    protected PassportCheckFindDeviceResult doInBackground(Void... params) {
        return PassportExternal.getPassportFindDeviceInterface().checkFindDeviceStatus(this.mActivity.getApplicationContext());
    }

    protected void onPostExecute(PassportCheckFindDeviceResult result) {
        if (!(this.mProgressDialogFragment == null || this.mProgressDialogFragment.getActivity() == null || this.mProgressDialogFragment.getActivity().isFinishing())) {
            this.mProgressDialogFragment.dismissAllowingStateLoss();
        }
        if (result != null && this.mActivity != null && !this.mActivity.isFinishing()) {
            if (result.checkOperationResult == CheckOperationResult.FAILED) {
                if (this.mCheckOperationFailedRunnable != null) {
                    this.mCheckOperationFailedRunnable.run(result.errorMessage);
                }
            } else if (result.checkOperationResult != CheckOperationResult.SUCCESS) {
                throw new IllegalStateException("Normally not reachable. ");
            } else if (this.mCheckOperationSuccessRunnable != null) {
                this.mCheckOperationSuccessRunnable.run(result.isOpen, result.sessionUserId, result.displayId);
            }
        }
    }
}

代码量有点多,不过没关系,我们首先看到CheckFindDeviceStatusTask继承的是AsyncTask,那就好办了,执行具体操作逻辑的方法是doInBackground(),我们直接看它的doInBackground()实现方法:

   protected PassportCheckFindDeviceResult doInBackground(Void... params) {
        return PassportExternal.getPassportFindDeviceInterface().checkFindDeviceStatus(this.mActivity.getApplicationContext());
    }

只有一句代码,调用了这个checkFindDeviceStatus()方法!看到这里,小伙伴们有没有发现这个checkFindDeviceStatus()方法我们很眼熟啊,不就是那个PassportFindDeviceImpl类中重写实现的PassportFindDeviceInterface接口中的方法吗?!我们点击这个方法去查看它的来源:

果然是它!那这里就好办了,我们直接打开 PassportFindDeviceImpl类再次分析一下这个checkFindDeviceStatus()方法:

我们在昨天分析查找获取账号信息的时候,查到过这个方法,只不过那个时候调用的是它的onLoginSuccess()方法。我们可以看到在方法checkFindDeviceStatus()内,通过代码FindDeviceInfo info = findDeviceStatusManager.getFindDeviceInfoFromServer(); 获取到了一个FindDeviceInfo的实例,这个实例则用来给下面的PassportCheckFindDeviceResult实例赋值,最后返回这个PassportCheckFindDeviceResult实例。getFindDeviceInfoFromServer()方法和FindDeviceInfo类都是无法查看的,他们同样是封装在系统内的方法,不存在该项目内。包路径:

我们还可以看到赋值总共有四个值,分别为:isOpen,isLocked,sessionUserId,displayId。注意这个isOpen值,这个布尔值就是上面说的那个关键判断值:isOpenFindDevice!我们回去看下CheckFindDeviceStatusTask的收尾方法:onPostExecute:

protected void onPostExecute(PassportCheckFindDeviceResult result) {
        if (!(this.mProgressDialogFragment == null || this.mProgressDialogFragment.getActivity() == null || this.mProgressDialogFragment.getActivity().isFinishing())) {
            this.mProgressDialogFragment.dismissAllowingStateLoss();
        }
        if (result != null && this.mActivity != null && !this.mActivity.isFinishing()) {
            if (result.checkOperationResult == CheckOperationResult.FAILED) {
                if (this.mCheckOperationFailedRunnable != null) {
                    this.mCheckOperationFailedRunnable.run(result.errorMessage);
                }
            } else if (result.checkOperationResult != CheckOperationResult.SUCCESS) {
                throw new IllegalStateException("Normally not reachable. ");
            } else if (this.mCheckOperationSuccessRunnable != null) {
                this.mCheckOperationSuccessRunnable.run(result.isOpen, result.sessionUserId, result.displayId);
            }
        }
    }

在这里对返回的PassportCheckFindDeviceResult实例进行了处理,注意这句代码:

 this.mCheckOperationSuccessRunnable.run(result.isOpen, result.sessionUserId, result.displayId);

调用了检查成功方法,传入的关键判断值正是isOpen变量!

分析到这里,我们差不多就明白了大致过程:在无法通过正常途径获得到账号信息的情况下,即方法ExtraAccountManager.getXiaomiAccount(this);返回的Account实例为空的时候(表示没有账号登录的时候),系统就会去启动CheckFindDeviceStatusTask这个线程去进行检查,这里的检查是检查是否存在关联账号,如果存在关联账号信息,那么就会展示锁定页面,提示你输入密码进行解锁,如果没有发现关联账号,那么就不会展示锁定页面!

关键还是这个读取到的FindDeviceInfo实例,我们虽然无法窥探它的代码,但是我们照样可以看到他的变量值,编写拦截代码,目标类是FindDeviceStatusManager,目标方法是:getFindDeviceInfoFromServer():

XposedHelpers.findAndHookMethod("miui.cloud.finddevice.FindDeviceStatusManager", loadPackageParam.classLoader, "getFindDeviceInfoFromServer", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("小米账号获取:抓到方法FindDeviceStatusManager->getFindDeviceInfoFromServer()");
                Class classFindDeviceInfo=XposedHelpers.findClass("miui.cloud.finddevice.FindDeviceInfo",loadPackageParam.classLoader);
             
                Field []fs=classFindDeviceInfo.getDeclaredFields();
                for (Field field:fs){
                    field.setAccessible(true);
                    XposedBridge.log("小米账号获取:FindDeviceInfo类:参数"+field.getName()+"值为:"+field.get(param.getResult()));

                
                }

              
            }
        });

这里我们拦截到方法后,然后通过反射机制访问FindDeviceInfo实例的变量值进行打印,下面就运行一下看看这个FindDeviceInfo实例的值都是什么:

这下终于明白了!displayId值原来为是那个解锁编号,sessionUserId值就是小米ID!原来检查查找设备竟然是这个鬼东西,就算刷机也能发现关联账号信息就是从这里面获取的!

下面我们对拦截代码进行修改,把displayId和sessionUserId值修改为null,isOpen值修改为false!这样看看它还会不会把我的界面给锁定了,修改拦截代码如下:

XposedHelpers.findAndHookMethod("miui.cloud.finddevice.FindDeviceStatusManager", loadPackageParam.classLoader, "getFindDeviceInfoFromServer", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("小米账号获取:抓到方法FindDeviceStatusManager->getFindDeviceInfoFromServer()");
                Class classFindDeviceInfo=XposedHelpers.findClass("miui.cloud.finddevice.FindDeviceInfo",loadPackageParam.classLoader);
                Object objectDeviceInfo=classFindDeviceInfo.newInstance();
                Field []fs=classFindDeviceInfo.getDeclaredFields();
                for (Field field:fs){
                    field.setAccessible(true);
                    XposedBridge.log("小米账号获取:FindDeviceInfo类:参数"+field.getName()+"值为:"+field.get(param.getResult()));

                    switch (field.getName()){
                        case "displayId":field.set(objectDeviceInfo,null);break;
                        case "sessionUserId":field.set(objectDeviceInfo,null);break;
                        case "isLocked":field.setBoolean(objectDeviceInfo,false);break;
                        case "isOpen":field.setBoolean(objectDeviceInfo,false);break;
                        default:field.set(objectDeviceInfo,field.get(param.getResult()));
                    }
                }

                param.setResult(objectDeviceInfo);
            }
        });

我们通过反射创建一个FindDeviceInfo类对象,设置新对象的变量值,然后修改返回结果为新的对象。运行一下看看效果:

哈哈,成功了,没有把我的页面给锁定了!不过还是有细心的小伙伴会问,为什么输入框内还是出现了之前登录的小米账号呢?不急,我们这就看看,对页面进行分析:查看这个输入框的id为:et_account_name,然后老样子用jadx进行全局搜索,找到它的引用在类LoginBaseFragment中:

类中查找这个输入框设置文本的方法:

 在onViewCreated()方法内发现了该输入框设置的文本,为字符串lastLoginUserId,lastLoginUserId又是方法getLastLoginAccountName()的返回值,我们去看一下这个getLastLoginAccountName()方法:

原来是从xml文件中拿到的,那这个xml字段又是在什么时候被放进去的呢?还记得我们上篇博客分析的那个addOrUpdateAccountManager()方法了吗?:

 

我们当时只重点看了那个onAddOrUpdateAccountManagerSuccess()方法,在这个方法的下面,是saveLastLoginAccountName()方法,我们去看看这个方法做了什么:

 

答案非常的显而易见,把最新登陆的账号保存在了xml文件中!这样做的目的并不涉及到安全策略,只是为了方便用户,直接输入密码就可以登录,不用输入账号了!

好了,至此我们成功的修改掉了锁定页面,我们再次把目光放到 FindDeviceInfo info = findDeviceStatusManager.getFindDeviceInfoFromServer();上。为什么刷机会刷不掉?这里很显然的是,MIUI系统把账号信息存放到了一个就硬件设备里,对外提供了一个获取方法 getFindDeviceInfoFromServer()来获取保存在硬件内的账号数据。这个硬件是什么?很大的可能是CPU,CPU内经常会被硬件厂商放入一些对用户来说极为重要的关键数据,放在CPU内比放在系统文件内可要安全可靠的多。比如华为,他把用户的指纹信息放在了CPU内,对外只提供一个匹配接口,拒不同意腾讯要求把指纹信息上传至腾讯云端,也因此到现在华为手机上使用微信支付还是不能使用指纹支付!

这里博主点评MIUI系统的安全策略就是:把用户的关键信息放置在了CPU内(这里姑且认为是CPU),对外提供了一个统一负责写入和读取的类:FindDeviceStatusManager。当我们在刷机的时候,把保存在系统文件和数据库中的账号信息删除掉了,通过那个统一获取账号信息接口ExtraAccountManager.getXiaomiAccount(Context);无法获取账号信息,这时候系统就会去调用FindDeviceStatusManager的获取接口,来查看CPU内是否保存的有用户信息,如果存在的话,那就说明该手机处于不安全的状态(比如手机丢失被人为刷机),就会把页面锁定,输入密码才能进行解锁,这样就会极大的保证了手机设备和账号的安全性!

 好了,本篇博客到此结束,有不明白的地方请评论留言,我看到后会及时进行回复!有需要引用本文的地方请标明出处,谢谢合作!最后祝大家猪年大吉,红红火火!

转载自原文链接, 如需删除请联系管理员。

原文链接:Android逆向工程:MIUI系统大揭秘:去不掉的小米账号!,转载请注明来源!

0