Android逆向-实战sign分析-某某合伙人_v4.0.9
1.基礎(chǔ)分析
1.1.基礎(chǔ)分析
工作中經(jīng)常會(huì)對(duì)一些app進(jìn)行安全審計(jì),一般在拿到應(yīng)用后可以使用APK Helper工具對(duì)應(yīng)用做個(gè)簡(jiǎn)單分析,了解目標(biāo)的包名,版本號(hào),名稱等基礎(chǔ)信息,之后再安裝到手機(jī)上熟悉下應(yīng)用的業(yè)務(wù),該應(yīng)用屬于哪類app,之后就是思考業(yè)務(wù)上可能存在的攻擊點(diǎn)。
本次目標(biāo)是一個(gè)電商應(yīng)用,簡(jiǎn)單使用后可知應(yīng)用存在登錄,購買,支付等眾多攻擊點(diǎn),這里我們就從登錄入手。
登錄邏輯的常見審計(jì)思路有:
有了審計(jì)思路后就可以進(jìn)行更深入的分析,在分析之前還需要確定該app是否有保護(hù)機(jī)制,比如應(yīng)用是否加固,是否存在反抓包機(jī)制。如果存在的保護(hù)機(jī)制的話還需要把這些保護(hù)機(jī)制給過掉。
1.2.抓包
第一步,抓包,在實(shí)際的測(cè)試過程中發(fā)現(xiàn)該應(yīng)用做了反抓包策略,只要我們一掛上代理或開啟VPN就會(huì)在請(qǐng)求服務(wù)器時(shí)提示以下的錯(cuò)誤。
這時(shí)候有兩種選擇
- 1.分析apk代碼,找到證書檢查的位置,hook繞過
- 2.采取通用方案:Xposed+JustTrustMe插件+SSLUnpinning2.0插件
這里為了方便就選擇第二種了,首先需要在手機(jī)上安裝Xposed環(huán)境,Xposed是一個(gè)hook框架,它支持安裝別的模塊,每個(gè)模塊都是基于hook實(shí)現(xiàn)的小工具。比如這里用于繞過SSL證書校驗(yàn)的模塊JustTrustMe和SSLUnpinning2.0,將Xposed和模塊安裝完成后需要重啟一下系統(tǒng),這是因?yàn)閄posed 的原理是對(duì)zygote進(jìn)程進(jìn)行劫持,每個(gè)apk啟動(dòng)都是由zygote進(jìn)程fork出來的,所以每次新加一個(gè)模塊都需要重啟下系統(tǒng)使得zygote進(jìn)程重啟進(jìn)而加載我們新添加的模塊。
這里再補(bǔ)充說下這兩個(gè)插件的原理,都是對(duì)APK中可能用到的校驗(yàn)SSL證書的API進(jìn)行hook,從而繞過證書檢查這么一步的。
等待系統(tǒng)啟動(dòng)完成后再次進(jìn)行抓包,可以發(fā)現(xiàn)再次請(qǐng)求時(shí)就沒有了之前的錯(cuò)誤提示,并且fiddler也順利抓到了數(shù)據(jù)包。
能夠順利捕獲數(shù)據(jù)包后,我們可以在抓包工具中對(duì)請(qǐng)求的數(shù)據(jù)進(jìn)行修改,之后點(diǎn)擊發(fā)送來檢查應(yīng)用是否做了數(shù)據(jù)包完整性的校驗(yàn),如果不存在完整性校驗(yàn)?zāi)蔷涂梢岳脭?shù)據(jù)包重放攻擊來對(duì)服務(wù)端進(jìn)行測(cè)試,這里是重放測(cè)試是失敗了的,所以確定服務(wù)端是檢查了數(shù)據(jù)包的完整性。
其實(shí)除了修改數(shù)據(jù)包來確定,還可以分析發(fā)送的數(shù)據(jù)包來確定,數(shù)據(jù)包中有個(gè)apisign字段,每次請(qǐng)求這個(gè)apisign值都會(huì)變化,一般sign字段表示簽名值,通過計(jì)算除sign之外的其他字段算出來的,這叫做參數(shù)簽名。
所以如果要實(shí)現(xiàn)模擬請(qǐng)求那就需要分析這個(gè)apisign值的生成,下面開始分析apisign值的生成。
1.3.脫殼
將應(yīng)用丟到反編譯工具中進(jìn)行反編譯時(shí)發(fā)現(xiàn)應(yīng)用做了加固,有經(jīng)驗(yàn)的小伙伴一眼應(yīng)該就能看出這個(gè)哪家的殼子
不清楚的小伙伴也沒關(guān)系,可以使用查殼工具(PKID)來檢測(cè)。
工具檢測(cè)出使用了360加固,知道使用的哪家加固后脫殼就可以有針對(duì)性的進(jìn)行,不過這里找了幾款通用的脫殼工具進(jìn)行脫殼。
1.3.1.內(nèi)存搜索法脫殼
第一款Frida-dexdump,是基于Frida的一款脫殼工具,它的原理是在內(nèi)存中暴力搜索dex開頭的標(biāo)志性字符(dex035/dex036/dex037),如果找到了就會(huì)將這個(gè)dex文件給dump下來,對(duì)于將頭部抹掉的dex還支持dex文件的模糊查找。使用時(shí)是默認(rèn)對(duì)頂層activity所在進(jìn)程的內(nèi)存進(jìn)行搜索。
E:\FRIDA-DEXDump\FRIDA-DEXDump\frida_dexdump λ python main.py -n com.ljhhr.mobile 05-31/10:32:32 INFO [DEXDump]: found target [6717] com.ljhhr.mobile [DEXDump]: DexSize=0x7324, DexMd5=d4f38621321e74cf8b44a79549af028e, SavePath=E:\FRIDA-DEXDump\FRIDA-DEXDump\frida_dexdump/com.ljhhr.mobile/0xca69c000.dex [DEXDump]: DexSize=0x647424, DexMd5=a56baf750eecd3d744feb795fe3f71b2, SavePath=E:\FRIDA-DEXDump\FRIDA-DEXDump\frida_dexdump/com.ljhhr.mobile/0xcb490000.dex [DEXDump]: DexSize=0x2399f0, DexMd5=6b784fda24a96e6e415a503c4cbbb418, SavePath=E:\FRIDA-DEXDump\FRIDA-DEXDump\frida_dexdump/com.ljhhr.mobile/0xcc0bf000.dex [DEXDump]: Skip duplicate dex 0xcc880000<d4f38621321e74cf8b44a79549af028e> [DEXDump]: DexSize=0x11c, DexMd5=f1771b68f5f9b168b79ff59ae2daabe4, SavePath=E:\FRIDA-DEXDump\FRIDA-DEXDump\frida_dexdump/com.ljhhr.mobile/0xcd0498b0.dex [DEXDump]: Skip duplicate dex 0xcd38701c<d4f38621321e74cf8b44a79549af028e> /...skip.../ E:\FRIDA-DEXDump\FRIDA-DEXDump\frida_dexdump使用后一共dump出了4個(gè)dex,簡(jiǎn)單看了下0xcb490000.dex文件比較大,使用反編譯工具可以看到確實(shí)存在ljhhr相關(guān)代碼,其中還有一些sdk的代碼。
那么該如何確認(rèn)代碼是否完整呢?
打開登錄頁面拿到頂層activity,看這個(gè)dex中是否存在這個(gè)activity的處理代碼。
當(dāng)前頂層activity"com.ljhhr.mobile/.ui.login.register.RegisterActivity",那我們?nèi)フ蚁驴茨苷业竭@個(gè)activity的java邏輯不
尷尬 ̄□ ̄||并沒有,說明dump出來的不完整,換個(gè)脫殼工具。
1.3.2.關(guān)鍵函數(shù)hook法脫殼
第二款Frida-Apk-Unpack,該工具也是基于Frida實(shí)現(xiàn)的,原理則是對(duì)libart.so中的一些關(guān)鍵函數(shù)進(jìn)行hook,比如OpenMemory和OpenCommon函數(shù),通過這些函數(shù)可以拿到內(nèi)存中的dex地址,之后就可以計(jì)算出dex文件大小并從內(nèi)存中對(duì)dex進(jìn)行導(dǎo)出。話不多說,奧利給,直接淦
E:\Frida-Apk-Unpack\Frida-Apk-Unpack λ frida -U -f com.ljhhr.mobile -l dexDump.js Spawning `com.ljhhr.mobile`... [04:06:36:441] Android Version: 9 [04:06:36:455] index 3406 function name: _ZN3art16ArtDexFileLoader10OpenCommonEPKhjS2_jRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEEPNS_13DexFileLoader12VerifyResultE [04:06:36:460] processName com.ljhhr.mobile [04:06:36:460] Android Version: 9 [04:06:36:460] hookFunction: 0xeec28765 Spawned `com.ljhhr.mobile`. Use %resume to let the main thread start executing! [AOSP on walleye::com.ljhhr.mobile]-> %resume [04:06:42:263] hookFunction onEnter [04:06:42:263] hookFunction onLeave [04:06:42:266] hookFunction onEnter [04:06:42:266] magic : dex /...skip.../ 035 [04:06:42:580] dex_size :6160852 [04:06:42:596] dump dex success, saved path: /data/data/com.ljhhr.mobile/6160852.dex [04:06:42:596] hookFunction onLeave [04:06:42:598] hookFunction onEnter [04:06:42:598] magic : dex /...skip.../ [04:06:42:614] dump dex success, saved path: /data/data/com.ljhhr.mobile/284.dex [04:06:42:614] hookFunction onLeave它的速度很快,共生成了5個(gè)dex文件。我們將這5個(gè)dex文件dump出來分別使用jeb工具進(jìn)行反編譯,看是否有我們需要的代碼。
OK,這次能夠成功找到我們想要的代碼,說明脫殼成功。
2.關(guān)鍵代碼定位
現(xiàn)在能看到apk的源碼了,接下來就是對(duì)sign生成代碼的定位,我們對(duì)核心邏輯定位用的比較多的方法有這幾個(gè),從簡(jiǎn)單到復(fù)雜依次是關(guān)鍵字定位法、相關(guān)函數(shù)定位法、業(yè)務(wù)邏輯定位法。
首先是關(guān)鍵字定位,就是看數(shù)據(jù)包中的字段名稱,然后去apk代碼中去搜索相應(yīng)的字符串,搜到有相關(guān)字符后再結(jié)合上下文進(jìn)行確定。這種方法的問題就是可能搜索出來的關(guān)鍵字比較多,所以在搜索時(shí)可以使用一些技巧比如字符串加分號(hào)等,之后遇到再講。還可能關(guān)鍵字搜不到那么就需要換方法。
其次是相關(guān)函數(shù)定位,首先猜測(cè)需要定位的邏輯可能會(huì)使用到哪些函數(shù),比如常用的加解密函數(shù),獲取用戶輸入的函數(shù)等,之后對(duì)這類函數(shù)進(jìn)行hook,當(dāng)觸發(fā)hook后輸出當(dāng)時(shí)的調(diào)用堆棧,這樣通過調(diào)用堆棧基本就能定位到核心邏輯,這樣的好處是在hook對(duì)了函數(shù)的情況下可以精準(zhǔn)快速的定位到關(guān)鍵的邏輯。
最后是業(yè)務(wù)邏輯定位法,這種相對(duì)前面兩個(gè)就比較麻煩,通過UI信息定位到大概的位置,之后去分析具體的業(yè)務(wù)邏輯進(jìn)行定位,比如可以先定位登錄Activity的邏輯,再深入分析具體的處理邏輯。
這里只是舉幾個(gè)比較常用的,定位的方法還有很多。
回到主題,這里先使用關(guān)鍵字定位看能否定位到sign生成代碼。
再抓個(gè)數(shù)據(jù)包開始分析,直接對(duì)數(shù)據(jù)包中的apisign字段進(jìn)行搜索,掏出另一個(gè)反編譯工具jadx,載入前面dump出來的dex文件,使用jadx的無敵搜索功能對(duì)關(guān)鍵字進(jìn)行搜索。
我們的運(yùn)氣不錯(cuò)搜索出的關(guān)鍵字只有一處,雙擊進(jìn)入代碼具體位置看看。
結(jié)合上下文基本可以確定就是我們要找的生成sign數(shù)據(jù)的邏輯,至此sign生成代碼的定位完成,下面開始分析生成邏輯。
3.sign生成邏輯分析
既然已經(jīng)找到了sign生成邏輯的所在類"com.ljhhr.resourcelib.network.ParameterInterceptor",那么就可以跟jadx說拜拜了,打開我們的jeb工具,三下五除二就在jeb中定位到了"com.ljhhr.resourcelib.network.ParameterInterceptor"類。這里主要是個(gè)人習(xí)慣,我比較喜歡使用jeb,感覺jeb的代碼看起來要舒服些,jadx一般用來做字符串搜索。
對(duì)這個(gè)類簡(jiǎn)單分析后,知道應(yīng)用使用的是目前比較流行的網(wǎng)絡(luò)通信框架okhttp+Retrofit進(jìn)行的網(wǎng)絡(luò)通信,而"com.ljhhr.resourcelib.network.ParameterInterceptor"這個(gè)類是一個(gè)自定義攔截器,攔截器是okhttp框架中很重要的一個(gè)特性,應(yīng)用通過將自定義的攔截器添加到okhttp客戶端中,這樣就可以實(shí)現(xiàn)對(duì)即將發(fā)出的請(qǐng)求及響應(yīng)結(jié)果做統(tǒng)一處理,比如添加一些應(yīng)用需要和服務(wù)端交互的信息,這里的apisign就是這時(shí)候添加的。
下面來分析這個(gè)自定義攔截器中的主要方法makeRequestBody:
對(duì)函數(shù)簡(jiǎn)單分析后得到"apisign"的值等于v13_8,而v13_8來源于MD5Util.ToMD5NOKey(v6_4.toString()),通過分析可知ToMD5NOKey方法是計(jì)算MD5的方法,所以v13_8等于v6_4的MD5,v6_4又是由v7_6的MD5與v7_5的MD5拼接而成。而v7_6和v7_5都是v4、v13_5、v3_1的拼接,值是一樣的,不一樣的是拼接的順序。
最終得:v13_8 = MD5(MD5(v4+v13_5+v3_1)+MD5(v3_1+v4+v13_5))
其中:
v4等于當(dāng)前秒級(jí)時(shí)間戳
v13_5等于一個(gè)字符串,如果請(qǐng)求的url中包含"App/login/getTttId",那就是固定的"@wf2w&.:kjf2[getTttId]l{flJ8l@sJisff@",否則就是動(dòng)態(tài)計(jì)算出的另一個(gè)字符串
v3_1等于請(qǐng)求參數(shù)被轉(zhuǎn)成大寫后的字符串,根據(jù)參數(shù)個(gè)數(shù)不一樣,這個(gè)值也不一樣
3.2.Frida確定數(shù)據(jù)格式
靜態(tài)對(duì)makeRequestBody函數(shù)進(jìn)行分析后總感覺差點(diǎn)什么,對(duì)還沒動(dòng)態(tài)分析,因?yàn)樗械撵o態(tài)分析都是猜想最終還是需要結(jié)合動(dòng)態(tài)分析來對(duì)我們的分析進(jìn)行一個(gè)確認(rèn)。
這里使用Frida工具來對(duì)ToMD5NOKey方法進(jìn)行hook,來看下運(yùn)行過程中真實(shí)的數(shù)據(jù)。
這里需要注意的一點(diǎn)是因?yàn)閼?yīng)用帶殼,所以需要在目標(biāo)應(yīng)用運(yùn)行起來后通過frida的attach方式進(jìn)行hook,如果使用spawn方式則會(huì)找不到類。關(guān)于這兩種啟動(dòng)有什么不同可以看看之前的文章"Android逆向-Frida入門學(xué)習(xí)筆記"里面的2.3.frida的兩種注入模式有介紹。
hook代碼如下:
運(yùn)行結(jié)果如下:
com.softgarden.baselibrary.utils.MD5Util.ToMD5NOKey(java.lang.String): [Nexus 5X::com.ljhhr.mobile]-> ================ ToMD5NOKey ============== 1622531544COUNTRIES_CODE=+86&P=ANDROID&PHONE=13711111111&TIME=1622531544&TYPE=20&V=71 ================ ToMD5NOKey ============================== ToMD5NOKey ============== COUNTRIES_CODE=+86&P=ANDROID&PHONE=13711111111&TIME=1622531544&TYPE=20&V=711622531544 ================ ToMD5NOKey ============================== ToMD5NOKey ============== 9160810470eb8965238e5c1760b2d7e0b1b0f86f13c6bbea81c0ac69ec4e15c7 ================ ToMD5NOKey ==============在實(shí)際測(cè)試中發(fā)現(xiàn)應(yīng)該是只有用戶登錄的情況下v13_5這個(gè)變量才會(huì)被賦值,否則就是默認(rèn)的空字符串,其余的和我們前面分析的都一樣。因?yàn)橹饕菍?duì)登錄邏輯進(jìn)行分析,所以v13_5不影響我們的結(jié)果,下面開始寫模擬登錄請(qǐng)求的代碼。
4.模擬請(qǐng)求
這里對(duì)登錄相關(guān)的兩個(gè)接口進(jìn)行了模擬請(qǐng)求,POC如下:
# -*- coding: UTF-8 -*- import hashlib import time import requests from requests.api import head from urllib.parse import quote, urlencodedef ToMD5NOKey(arg1, arg2):str = arg1+arg2str_md5 = hashlib.md5()str_md5.update(str.encode("utf-8"))# print(str_md5.hexdigest())return str_md5.hexdigest()def generate_sign(v1, v2, v3):ToMD5NOKey("", v1+v2+v3)ToMD5NOKey("", v1+v2+v3)# v13_8 = MD5(MD5(v4+v13_5+v3_1)+MD5(v3_1+v4+v13_5))res1 = ToMD5NOKey("", v1+v2+v3)# print(res1)res2 = ToMD5NOKey("", v3+v1+v2)# print(res2)result = ToMD5NOKey("", res1+res2)# print(result)return resultdef request_data(type):if "getcode" == type:type = "20"phone = "13711111111"countries_code = "+86"p = "android"app_version = "71"time_stamp = str(int(time.time())) # 秒級(jí)時(shí)間戳user_info = "{\"type\":\""+type+"\",\"phone\":\""+phone+"\",\"countries_code\":\"" + \countries_code+"\",\"p\":\""+p+"\",\"v\":\"" + \app_version+"\",\"time\":"+time_stamp+"}"print(user_info)sign_info = "COUNTRIES_CODE="+countries_code+"&P="+p+"&PHONE=" + \phone+"&TIME="+time_stamp+"&TYPE="+type+"&V="+app_versionapisign = generate_sign(time_stamp, "", sign_info)data = "data="+quote(user_info)+"&apisign="+apisignprint(data)return dataelif "register" == type:uuid = ""code = "222222"phone = "13711111111"countries_code = "+86"p = "android"app_version = "71"time_stamp = str(int(time.time())) # 秒級(jí)時(shí)間戳user_info = "{\"uuid\":\""+uuid+"\",\"code\":\""+code+"\",\"phone\":\""+phone+"\",\"countries_code\":\"" + \countries_code+"\",\"p\":\""+p+"\",\"v\":\"" + \app_version+"\",\"time\":"+time_stamp+"}"print(user_info)sign_info = "CODE="+code+"&COUNTRIES_CODE="+countries_code+"&P="+p+"&PHONE=" + \phone+"&TIME="+time_stamp+"&UUID="+uuid+"&V="+app_versionapisign = generate_sign(time_stamp, "", sign_info)data = "data="+quote(user_info)+"&apisign="+apisignprint(data)return dataelse:print("[-] type value error!")headers = {"Content-Type": "application/x-www-form-urlencoded","Host": "www.100hhr.com", "Connection": "Keep-Alive", "Accept-Encoding": "gzip", "User-Agent": "okhttp/3.9.1"}# 獲取驗(yàn)證碼接口 def ljhhr_get_verify_code():url = "https://www.100hhr.com/App/Login/phoneCode"data = request_data("getcode")response = requests.post(url=url, headers=headers, data=data)response.closeprint(response.text.encode('utf-8').decode('unicode_escape'))# 用戶注冊(cè)接口 def ljhhr_register():url = "https://www.100hhr.com/App/Login/register"data = request_data("register")response = requests.post(url=url, headers=headers, data=data)response.closeprint(response.text.encode('utf-8').decode('unicode_escape'))ljhhr_get_verify_code() ljhhr_register()完整的登錄請(qǐng)求
λ python ljhhr.py {"type":"20","phone":"13700000000","countries_code":"+86","p":"android","v":"71","time":1622548879} data=%7B%22type%22%3A%2220%22%2C%22phone%22%3A%2213700000000%22%2C%22countries_code%22%3A%22%2B86%22%2C%22p%22%3A%22android%22%2C%22v%22%3A%2271%22%2C%22time%22%3A1622548879%7D&apisign=fad1c8bc56eeebc5e22a30fb3efb831c {"_t":"0.6386","status":1,"info":"發(fā)送成功","data":{"code":""},"time":1622548881,"errorCode":"0"}λ python ljhhr.py {"uuid":"","code":"833196","phone":"13700000000","countries_code":"+86","p":"android","v":"71","time":1622548911} data=%7B%22uuid%22%3A%22%22%2C%22code%22%3A%22833196%22%2C%22phone%22%3A%2213700000000%22%2C%22countries_code%22%3A%22%2B86%22%2C%22p%22%3A%22android%22%2C%22v%22%3A%2271%22%2C%22time%22%3A1622548911%7D&apisign=fff370bef976602a9610abe0d9c20c99 {"_t":"0.3459","status":1,"info":"","data":{"token":"f3a44c85ae05622073b6e0e8e1a4904X","user_id":"226402","user":{"id":"226402","phone":"13700000000","head":"\/Public\/Mobile\/images\/img\/im95.png","nickname":"13700000000","sex":"0","money":"0.00","score":"0.00","auth":"0","user_id_code":"0210226402","email":"","exam_level":"0","birthday":"","add_time":"1622548912","sign":"","job":"","info":"","province":"25","city":"321","district":"0","now_city":"0","real_name":"","id_card":"","cc":"86","is_set_password":"0","province_name":"上海","city_name":"上海","district_name":"","now_city_name":"","pay_password_exist":0,"is_bind_wechat":0,"is_bind_qq":0,"is_bi nd_weibo":0,"invite":"0210226402","exam_url":"http:\/\/app.askform.cn\/501d9d2a-44bd-4061-a513-ecf4151af30a.aspx?uid=226402&sign=2e5075287215002592df88abb0260d99&callback=https%3A%2F%2Fwww.100hhr.com%2FApp%2FCallBack%2FaskCallback","exam_disable_string":"","sh_auth":0,"sh_id":0,"sh_is_check":0,"level":0,"is_card":0,"bag_id":0,"shop_end_time":0,"shop_apply_step":0,"exam_score":null,"supplier_id":"","supplier_is_check":"","lecturer_id":"","lecturer_is_check":"","coupons":"0"},"price":"1.17","invite_price":0,"invite_phone":null,"weixin_url":"https:\/\/mp.weixin.qq.com\/mp\/profile_ext?action=home&__biz=MzIzMzkzOTk5MQ==&scene=110#wechat_redirect"},"time":1622548912,"errorCode":"0"}以上便是一次完整的登錄請(qǐng)求,可以看到模擬的驗(yàn)證碼獲取和注冊(cè)請(qǐng)求都得到了對(duì)方服務(wù)器的正確響應(yīng)。接下來就可以更進(jìn)一步的做安全測(cè)試了,比如賬號(hào)密碼爆破測(cè)試,驗(yàn)證碼爆破測(cè)試等。
5.總結(jié)
在整個(gè)分析過程中可以發(fā)現(xiàn)該應(yīng)用雖然在客戶端、傳輸過程中、服務(wù)端都做了相應(yīng)的防護(hù),但防護(hù)的強(qiáng)度并不夠。傳輸過程中使用的sign簽名機(jī)制,雖然一定程度上防止了數(shù)據(jù)被篡改和偽造的風(fēng)險(xiǎn),但卻使用的是MD5進(jìn)行sign的計(jì)算,需要注意的是MD5并不是加密算法,而是哈希算法,對(duì)于sign生成還是使用自定義的簽名算法比較安全。
總結(jié)
以上是生活随笔為你收集整理的Android逆向-实战sign分析-某某合伙人_v4.0.9的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 驾照考试科目三灯光测试
- 下一篇: Android之断点续传下载