哈嘍小夥伴們你們好~歡迎繼續學習探討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