哈喽小伙伴们你们好~欢迎继续学习探讨MIUI系统的安全防范知识!在上篇博客中:Android逆向工程:带你领略MIUI系统的帐号安全防范机制:帐号是从哪里获取的?咱们了解到了MIUI系统经过对关键代码进行封装进系统内,对外采用统一调用接口的方式来防止关键代码被破解窥视,保护了系统应用的安全,同时咱们发现了获取帐号信息的准确接口,那么MIUI系统除此以外,还有什么值得称道的安全防御措施呢?接着上篇博客咱们还没有解决的问题:为何刷机都没法刷掉以前已经登录的小米帐号?咱们发现的那个PassportFindDeviceImpl类它真正的做用是什么?下面就带着这些疑问,来开启咱们今天的学习吧!android
首先仍是有请咱们今天的教案对象:个人小米。在下面的学习中,咱们主要围绕“个人小米”进行分析和探讨。在此声明,本次讲解内容不可用于不正当破坏行为,学习技术为主,搞破坏是不能够的!数据库
在上篇博客中,咱们发现了获取小米帐号的系统方法: 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,下面就看一下出现了什么情况:session
很直接,手机直接被锁了!为何会这样?看到这个界面,已经关联的小米帐号,包括解锁编号,喜欢刷机的小伙伴们估计会很熟悉,由于对以前已经登录小米帐号的手机进行刷机的时候,刷机完成就会出现这个界面,提示你此手机有关联的帐号,让你输入帐号密码进行解锁! 这就是那个刷机也刷不掉的小米帐号!ide
很不错,看来MIUI系统确实是有两把刷子。按照常理,刷机就是更换了一整个系统,其中也包括了那些存放关键信息的系统文件和系统内部数据库,在文件和数据库已经被更换,数据也被清空的状况下,这个关联的帐号信息又是从哪里获取到的?MIUI系统又是怎么知道这个设备(手机)以前有登陆的小米帐号呢?带着这些疑问,咱们接着往下逆向分析!学习
首先这里咱们对该锁定界面进行界面元素分析:目标是上面那条String资源:此手机已经关联到小米帐号(xxxx):微信支付
很不错,是一条TextView,id值为:find_device_status。看到这个id值咱们内心差很少明白了七八分,为了验证咱们的猜想,那就去看看这条id被引用的代码:使用jadx对此id值进行全局字符串搜索:ui
找到了,在类:LockedAccountLoginByFindDeviceFragment中。这个类的名字挺长,可是能够大体的看出它的功能: 经过查找设备而后锁定帐号进行登录的页面!很不错,名字起的却是很直白啊~找了这个页面,下面咱们就须要知道,这个页面是在什么地方被调用的?接下来该怎么查找?直接全局搜索LockedAccountLoginByFindDeviceFragment,看看它在别的类中是否有引用,有引用的地方估计就是他被调用的地方!使用Jadax全局搜索 LockedAccountLoginByFindDeviceFragment:this
搜索发现,在整个“个人小米”项目中, 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内是否保存的有用户信息,若是存在的话,那就说明该手机处于不安全的状态(好比手机丢失被人为刷机),就会把页面锁定,输入密码才能进行解锁,这样就会极大的保证了手机设备和帐号的安全性!
好了,本篇博客到此结束,有不明白的地方请评论留言,我看到后会及时进行回复!有须要引用本文的地方请标明出处,谢谢合做!最后祝你们猪年大吉,红红火火! ———————————————— 版权声明:本文为CSDN博主「奋进的代码」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处连接及本声明。 原文连接:https://blog.csdn.net/qq_34149335/article/details/86621470