小伙伴们大家好!今天已经是1.23号了,春节正在逐渐临近,不知道你是不是现在已经回到了家里帮爸妈置办年货,还是像博主这样继续奋战在工作一线呢?不管如何,在这里博主首先献上新春的节日问候,祝大家春节快乐,阖家幸福!然后接下来就要继续开始我们今天的学习了。在上一篇博客中,博主已经提到了最近一个月都在逆向分析小米的MIUI系统,MIUI系统想必大家很熟悉了,国内也是比较知名的基于Android定制的手机系统,它在安全防护,风险控制上也是十分的出色,那么下面就跟随博主一起来领略一下它的神秘面纱吧~
本次学习目的正如标题所示,MIUI系统的账号安全防范机制!这里的账号指的是小米账号。小米对小米账号的安全是格外的重视,想必那些狂热的手机刷机友们都很清楚,在对小米手机进行刷机的时候,会发现怎么样都无法清除掉这部手机之前已经登录上的小米账号,除非你知道该账号密码进行解锁操作,否则的话这部手机就相当于废掉了!
那么我们就不禁生出疑问,小米账号一旦登陆,账号信息到底存放到什么地方了呢?账号信息究竟是通过什么方式进行获取的呢?为什么我已经刷机了更换了系统但还是能检测到原先登陆的小米账号?那么这里,就带着这些疑问,来开始今天的正式学习!
本次教案对象为小米手机的系统应用:我的小米。在下面的学习中,我们主要围绕“我的小米”进行分析和探讨。在此声明,本次讲解内容不可用于不正当破坏行为,学习技术为主,搞破坏是不可以的!
首先要想知道账号信息存放在了什么地方,我们还是要先知道账后在登陆的时候做什么?老样子,首先对登录页面进行界面分析:
我们可以很容易的看到登录按钮的id值为:btn_login ,接下在我们打开Jadx,在源码中对该id进行全局搜素:
我们发现在源码中出现了四处引用,上面那一大堆的drawable不在考虑范围之内。庆幸的是,我们可以通过 这些类的名字大致分析出这四个类的功能:LoginBaseFragment,基本的登录页面,估计这个应该是别的登录页面的父类,具体的登录事务应该由它里面的方法进行实现;AccountLoginFragment,账号登录页面,估计是当使用小米ID进行登陆的时候采用这个页面,同时我们通过源码可以发现,它的父类正是LoginBaseFragment:
下面PhoneAccountLoginFragment,手机号登录页面,顾名思义该页面是用手机号登录,看一下它的父类,照样是 LoginBaseFragment:
这个时候我们就可以猜出来了, LoginBaseFragment是所有不同类型的登录页面的父类,按照惯例的开发模式和系统架构,实现登录方法应该就在这个LoginBaseFragment内!那么我们就去看看这个LoginBaseFragment的源码:
我们首先找到在LoginBaseFragment中登录按钮的实例:mLoginButton。重点还是在点击事件里面,看下它的Onclick方法:
我们可以看到这里采用的是匹配触发点击事件的view,这里我们已经知道登录按钮的view变量是mLoginButton,很容易就能确定当点击登录的时候,将会执行下面两行代码:
首先第一个方法从字面意义我们就可以大致分析出该方法的功能:统计点击登录按钮的次数;第二个方法的功能:检查登录参数并且开始登陆。在这里,MIUI系统的安全防范就开始正式的体现出来了,首先这个统计点击登录按钮的次数,我们可以去看一下它的代码:
调用了PassportStaHelper类中的静态方法statLoginCountEvent:
就只能看到这里了。为什么?我们可以看一下AccountStatInterface类所处路径:
位于com.xiaomi.accountsdk包内,看下”我的小米“系统应用的包架构:
很清楚的看到在”我的小米“项目中根本就不存在accountsdk包!不存在的包,但是编译和运行还没问题,只能说这确实是系统应用的任性之处,博主这里猜测,accountsdk包是属于系统提供的一个架包用于对这些敏感类型的操作,其实深入的分析下去,我们还可以发现更多这种项目中不存在的架包!这里博主对MIUI系统的账号风险防范做出点评:敏感的关键代码,比如这里的记录登录次数,全部都采用了封装在系统内,对外提供接口进行调用的的方式来进行,这样保证了关键代码不会面临被窥视别修改的风险;账号登录模块采用了记录账号登录次数的形式来防止恶意暴力批量登陆,进一步保护了登录接口的稳定性。这里我们其实还可以看到,在安全防范上,系统应用有着得天独厚的巨大优势(系统),相比之下,普通应用只能采取强大的加密,风险监测,完美的架构,来尽可能的降低应用被破解的风险!
接下来我们就来看看小米账号是如何登录的?登录的时候都做了哪些事情?checkLoginParamsAndStartLogin()方法代码如下:
这里很清楚的看到是检测输入框是否为空,以及输入内容是否符合规则,如果输入的账号和密码没有问题,那么就将会执行startLogin ()方法进行登录,下面看一下startLogin()方法都做了什么:
简单的一行代码调用,调用了loginByPassword()方法:看下他的实现方法:
protected void loginByPassword(String userId, String password, String captchaCode, String ick, final String serviceId) {
if (isFutureTaskRunning(UIControllerType.PASSWORD)) {
AccountLog.d(TAG, "password login has not finished");
return;
}
showLoginLoadingDialog();
this.mUIControllerFutures.put(UIControllerType.PASSWORD, MiPassportUIController.get(getActivity()).loginByPassword(new PasswordLoginParams.Builder().setUserId(userId).setCaptCode(captchaCode).setCaptIck(ick).setPassword(password).setServiceId(serviceId).build(), new PasswordLoginUICallback() {
protected void call(PasswordLoginFuture future) {
LoginBaseFragment.this.dismissLoginLoadingDialog();
int errorReasonId = -1;
String statKey = null;
try {
LoginBaseFragment.this.addOrUpdateAccountManager((AccountInfo) future.get());
} catch (InterruptedException e) {
AccountLog.e(LoginBaseFragment.TAG, "interrupted", e);
errorReasonId = R.string.passport_error_unknown;
} catch (ExecutionException e1) {
try {
future.interpretExecutionException(e1);
} catch (IOException e2) {
AccountLog.e(LoginBaseFragment.TAG, "network error", e2);
statKey = StatConstants.ERROR_NETWORK;
errorReasonId = R.string.passport_error_network;
} catch (NeedCaptchaException e3) {
AccountLog.e(LoginBaseFragment.TAG, "need captcha");
if (LoginBaseFragment.this.mIsTheFirstTimeOfInputCaptcha) {
LoginBaseFragment.this.mIsTheFirstTimeOfInputCaptcha = false;
LoginBaseFragment.this.applyCaptchaUrl(e3.getCaptchaUrl());
return;
}
statKey = StatConstants.ERROR_CAPTCHA;
LoginBaseFragment.this.applyCaptchaUrl(e3.getCaptchaUrl());
errorReasonId = R.string.passport_wrong_captcha;
} catch (InvalidUserNameException e4) {
AccountLog.e(LoginBaseFragment.TAG, "nonExist user name", e4);
errorReasonId = R.string.passport_error_user_name;
} catch (InvalidResponseException e5) {
AccountLog.e(LoginBaseFragment.TAG, "invalid response", e5);
statKey = StatConstants.ERROR_SERVER;
errorReasonId = R.string.passport_error_server;
} catch (AccessDeniedException e6) {
AccountLog.e(LoginBaseFragment.TAG, "access denied", e6);
statKey = StatConstants.ERROR_ACCESS_DENIED;
errorReasonId = R.string.passport_access_denied;
} catch (InvalidCredentialException e7) {
AccountLog.e(LoginBaseFragment.TAG, "wrong password", e7);
statKey = StatConstants.ERROR_PASSWORD;
if (!TextUtils.isEmpty(e7.getCaptchaUrl())) {
LoginBaseFragment.this.mIsTheFirstTimeOfInputCaptcha = false;
LoginBaseFragment.this.applyCaptchaUrl(e7.getCaptchaUrl());
}
errorReasonId = R.string.passport_bad_authentication;
} catch (NeedVerificationException e8) {
AccountLog.e(LoginBaseFragment.TAG, "need step2 login", e8);
LoginBaseFragment.this.gotoLoginStep2(e8.getUserId(), serviceId, e8.getMetaLoginData(), e8.getStep1Token());
return;
} catch (NeedNotificationException e9) {
AccountLog.e(LoginBaseFragment.TAG, "need notification", e9);
LoginBaseFragment.this.startNotificationActivityForResult(e9.getNotificationUrl(), serviceId);
return;
} catch (IllegalDeviceException e10) {
AccountLog.e(LoginBaseFragment.TAG, "illegal device id ", e10);
statKey = StatConstants.ERROR_ILLEGAL_DEVICE_ID;
errorReasonId = R.string.passport_error_device_id;
} catch (RemoteException e11) {
AccountLog.e(LoginBaseFragment.TAG, "remote exception", e11);
errorReasonId = R.string.passport_error_unknown;
}
}
if (errorReasonId != -1) {
LoginBaseFragment.this.showLoginFailedReason(LoginBaseFragment.this.getString(errorReasonId));
}
if (statKey != null) {
PassportStatHelper.statLoginFailureReasonCountEvent(statKey);
}
}
}));
}
代码量看着比较多,其实关键的代码并没有几行,最多的是对登录失败的情况判断展示,我们从头看:首先执行的是showLoginLoadingDialog()方法,显然易见,这里是弹出正在登陆转圈圈的提示框,注意下面的那一长行的代码:
this.mUIControllerFutures.put(UIControllerType.PASSWORD, MiPassportUIController.get(getActivity()).loginByPassword(new PasswordLoginParams.Builder().setUserId(userId).setCaptCode(captchaCode).setCaptIck(ick).setPassword(password).setServiceId(serviceId).build(), new PasswordLoginUICallback() {
关注最后面那个new PasswordLoginUICallback(),这个PasswordLoginUICallback就是用来进行账号登陆的线程类,当然这是已经封装好的线程类,下面的代码就是登录动作执行完毕回调到这里拿取登录结果。我们这里照样是没有办法窥探PasswordLoginUICallback类的源码,因为它还上面提到的accountsdk包一样,不存在这个项目中,也是属于一个系统封装的方法~你可能会泄气了,这关键方法都看不到还怎么进行破解啊!别急,虽然登陆的过程我们看不到,这里我们还可以看下它登录后的处理,还是loginByPassword()方法,注意下面那行代码:
这里调用了一个方法addOrUpdateAccountManager(),从字面意思来看,增加或者更新账号管理者,很明显这是一个重要的东西啊 ,我们很有必要去看一下,对获取账号信息途径是很有帮助的!addOrUpdateAccountManager()方法:
private void addOrUpdateAccountManager(final AccountInfo accountInfo) {
if (isFutureTaskRunning(UIControllerType.ADD_OR_UPDATE_ACCOUNT_MANAGER)) {
AccountLog.d(TAG, "add or update AccountManager has not finished");
return;
}
this.mUIControllerFutures.put(UIControllerType.ADD_OR_UPDATE_ACCOUNT_MANAGER, MiPassportUIController.get(getActivity()).addOrUpdateAccountManager(accountInfo, new AddOrUpdateUICallback() {
protected void call(AddOrUpdateAccountFuture future) {
LoginBaseFragment.this.statLoginSuccessCount();
LoginBaseFragment.this.onAddOrUpdateAccountManagerSuccess(accountInfo);
LoginBaseFragment.this.saveLastLoginAccountName();
}
}));
}
一大圈看下来我们可以看到还是一样的套路有木有!重点关注new AddOrUpdateUICallback() ,这个照样是关键代码所在,依然是看不到的,回调成功后执行了三个方法:统计登录成功的次数,登录成功的状态下增加或者更新账号管理者,保存最新登录的账号名字。很明显的是,我们要去看第二个方法:onAddOrUpdateAccountManagerSuccess(),我们去看看在这里面会不会发现什么有价值的东西:
方法内共有两处if判断语句,我们首先看第一个的if:
if (PassportExternal.getPassportFindDeviceInterface() != null) {
PassportExternal.getPassportFindDeviceInterface().onLoginSuccess(getActivity(), this.mIsFindDeviceStatusOpen);
}
这里我们终于发现了一个有意思的东西,getPassportFindDeviceInterface,Passport是护照,通行证的意思,在这里的意思较为模糊,可以看作是凭据的意思,后面出现了关键字:FindDevice,发现设备。发现设备?设备自然是这部手机,再加上Passport,作为逆向工程师的职业敏感,这里面很有可能有问题!那就过去看看这个是什么鬼东西~
一个接口,不慌不慌,使用jadx全局查找它的继承者是谁:
很快就找到了继承者是PassportFindDeviceImpl类,这个类系统没有封装,我们可以看到他的源码:
public class PassportFindDeviceImpl implements PassportFindDeviceInterface {
private static final String TAG = "PassportFindDeviceImpl";
public PassportCheckFindDeviceResult checkFindDeviceStatus(Context context) {
PassportCheckFindDeviceResult result = new PassportCheckFindDeviceResult();
result.checkOperationResult = CheckOperationResult.FAILED;
FindDeviceStatusManager findDeviceStatusManager = FindDeviceStatusManager.obtain(context);
try {
FindDeviceInfo info = findDeviceStatusManager.getFindDeviceInfoFromServer();
result.checkOperationResult = CheckOperationResult.SUCCESS;
result.isOpen = info.isOpen;
result.isLocked = info.isLocked;
result.sessionUserId = info.sessionUserId;
result.displayId = info.displayId;
} catch (RemoteException e) {
AccountLog.e(TAG, "Checking find device status failed", e);
result.errorMessage = context.getString(R.string.find_device_remote_exception);
} catch (InterruptedException e2) {
AccountLog.e(TAG, "Checking find device status failed", e2);
result.errorMessage = context.getString(R.string.find_device_interrupted);
} catch (FindDeviceStatusManagerException e3) {
AccountLog.e(TAG, "Checking find device status failed", e3);
result.errorMessage = new PassportFindDeviceStatusManager().getErrorMessageGivenFindDeviceStatusManagerException(context, e3);
} finally {
findDeviceStatusManager.release();
}
return result;
}
public void onLoginSuccess(Context context, boolean isFindDeviceStatusOpen) {
Bundle bundle = new Bundle();
bundle.putBoolean(LoginSuccessTaskService.KEY_IS_FIND_DEVICE_STATUS_OPEN, isFindDeviceStatusOpen);
Intent intent = new Intent(context, LoginSuccessTaskService.class);
intent.setPackage(context.getPackageName());
intent.putExtras(bundle);
context.startService(intent);
}
}
这个类的源码其实比较短,但是透露出的关键信息却很大!我们看到它的第一个方法:checkFindDeviceStatus,检查查找设备状态,名字听起来就很不简单,返回是一个实体类PassportCheckFindDeviceResult,检查查找设备的结果,更不一般啊!在方法中我们可以看到这句代码: FindDeviceInfo info = findDeviceStatusManager.getFindDeviceInfoFromServer();,下面又通过实体类FindDeviceInfo的实例为PassportCheckFindDeviceResult的实例进行了赋值和返回,看下这个实体类FindDeviceInfo,也是无法查看的,包路径如下:
对此我们已经习惯了,这里我们先记住这个 PassportFindDeviceImpl类,别忘了我们还有一个if判断:
if (this.mOnLoginInterface != null) {
this.mOnLoginInterface.onLoginSuccess(accountInfo.getUserId(), ExtendedAuthToken.build(accountInfo.getServiceToken(), accountInfo.getSecurity()).toPlain());
}
我们看下onLoginSuccess方法:
也是一个接口,如法炮制,我们查看它的继承者:
继承者是LoginActivity,进去看看它的onLoginSuccess方法:
简单的两句代码,去看看processLoginResult()方法:
关键代码出现了:XiaomiAccountTaskService.startQueryUserData(this)。startQueryUserData()方法,开始查询使用者数据!难道终于要知道怎么获取账号信息的么,快去看看:
有点失望,很明显这是一个开启服务操作!别着急,既然是开启了服务,那么我们就要看看这个服务做了什么! 在Jadx中全局搜索这条Action:ACTION_QUERY_USER_INFO:
顺利找到,点进去看看:
还是在XiaomiAccountTaskService类中,我们可以看到,执行了方法handleQueryUserInfo(),看看这个方法的实现是什么:
很好,我们终于找到了一个非常关键代码: Account account = ExtraAccountManager.getXiaomiAccount(this);!从字面意思就可以看到:获取小米账号!能够进行到这里真的很不错啊!功夫不负有心人,我们发现了一处获取账号信息的关键代码!看下这个getXiaomiAccount()方法,不用多想,照样是无法查看的,被封装在系统内的一个获取账号信息的接口,包括返回值,Account类的实例,也是无法查看的。
既然如此,我们就要想办法来查看这个ExtraAccountManager.getXiaomiAccount(this)返回的Account实例数据到底是什么鬼!怎么查看?当然看源码是不可能的了,别忘了我们还有一个秘密武器,Xposed!下面我们就使用Xposed来查看Account实例数据。编写拦截代码,目标类是ExtraAccountManager,目标方法是getXiaomiAccount,拦截代码如下:
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()));
}
}
});
拦截到getXiaomiAccount方法后就通过反射机制获取到Account类的变量信息和值,然后进行打印,这样就可以查看到Account的实例数据啦!下面就运行一下Xposed开始拦截,看看拦截结果如何:
可以很明显地看到,Account类中存在两个变量,name值就是小米ID,type值为:com.xiaomi,可见确实是通过该方法获取到了小米账号信息。 我们回到源码看下这句代码:
如果ExtraAccountManager.getXiaomiAccount(this)返回的Account实例是空的话,那么就代表没有小米账号,将跳转到查询sns信息,sns是什么鬼?接着分析查找:在XiaomiAccountTaskService类中发现方法handleQuerySnsInfo():
通过SnsAccountInfo.newSnsAccountInfo(snsType)获取到一个SnsAccountInfo的实例,那就看一下这个newSnsAccountInfo()方法:
好了,明白了,看到了新浪,QQ,还有FaceBook,,,第三方账号!不存在小米账号就会看看有没有登录的第三方账号。
通过上面的源码分析,我们发现了MIUI系统内获取小米账号的方式,明白了充分利用自身系统的优势隐藏关键逻辑代码正是MIUI系统安全防护措施之一!MIUI系统还有哪些值得称道的安全防护措施?PassportFindDeviceImpl这个类真正作用是什么?在下面的博客中我们将会一一揭晓!
本次博客到此结束,有问题请评论留言,我看到后会及时回复!有需要引用本文的地方请标明出处,谢谢合作!