iOSApp+springboot后端sign簽名+aes加密流程&逆向破解分析示例(class-dump+Logos+monkeyDev+IDA)
github地址:https://github.com/SmileZXLee/iOSSignatureAnalysis
Sign加密
概述&原理
【簡(jiǎn)述】當(dāng)客戶端像服務(wù)端發(fā)起請(qǐng)求時(shí),以POST
請(qǐng)求為例,如果提交的請(qǐng)求體內(nèi)容未經(jīng)過(guò)加密,請(qǐng)求可能存在被篡改的危險(xiǎn),即使是https請(qǐng)求
也是如此(https
抓包、修改請(qǐng)求只需要信任根證書(shū)即可)??梢酝ㄟ^(guò)眾多代理抓包、網(wǎng)卡抓包的程序?qū)φ?qǐng)求進(jìn)行攔截,修改請(qǐng)求內(nèi)容或是使用程序模擬客戶端請(qǐng)求發(fā)送,達(dá)到仿造請(qǐng)求、脫機(jī)請(qǐng)求等目的。例如搶購(gòu)、秒殺類業(yè)務(wù);以及一些要求客戶端信息準(zhǔn)確的業(yè)務(wù)(如打卡等);應(yīng)該對(duì)請(qǐng)求進(jìn)行加密,例如使用aes對(duì)整個(gè)請(qǐng)求體進(jìn)行對(duì)稱加密、使用sign
對(duì)請(qǐng)求體進(jìn)行簽名等。以下介紹sign
簽名的大致流程,因?yàn)?code>aes加密流程相對(duì)比較簡(jiǎn)單,后續(xù)也會(huì)在密碼加密中簡(jiǎn)要說(shuō)明。
- ① 對(duì)請(qǐng)求體中的所有參數(shù)的key根據(jù)
ASCII碼
的順序排序(a-z),然后根據(jù)key=value&key=value
拼接成一個(gè)字符串;如請(qǐng)求體為{"password":"123456","timestamp":"1642765564000","account":"zxlee"}
,經(jīng)過(guò)處理后為account=zxlee&password=123456×tamp=1642765564000
。 - ② 對(duì)上方
account=zxlee&password=123456×tamp=1642765564000
進(jìn)行md5
加密,獲得一串唯一的不可逆的16位或32位字符串。將計(jì)算出的md5
加密后的字符串放在請(qǐng)求體或請(qǐng)求頭中傳給服務(wù)端。 - ③ 服務(wù)端在獲取到請(qǐng)求體后,重復(fù)①、②中客戶端的操作,根據(jù)請(qǐng)求體中的參數(shù)計(jì)算出
sign
值,與客戶端傳過(guò)來(lái)的sign
進(jìn)行比較,若不相等,則認(rèn)為這個(gè)請(qǐng)求不合法,不進(jìn)行任何業(yè)務(wù)操作,直接響應(yīng)錯(cuò)誤信息。 - ④ 若客戶端發(fā)送的請(qǐng)求被篡改了,例如password被修改為了45678,則服務(wù)端實(shí)際上是對(duì)
account=zxlee&password=45678×tamp=1642765564000
進(jìn)行md5
,則計(jì)算出來(lái)的sign
值必定和客戶端傳過(guò)來(lái)的sign
不相等,則此請(qǐng)求無(wú)效。
【進(jìn)階①:加鹽】此時(shí)大家可能就在想了,我們完全可以仿造客戶端的簽名,自己計(jì)算sign值一并傳給服務(wù)端,就可以繞過(guò)這個(gè)驗(yàn)證了。確實(shí)如此,因?yàn)槲覀円呀?jīng)知道了sign
校驗(yàn)的基本流程和規(guī)則,但是事實(shí)上sign
簽名中md5
之前的值并不是需要固定使用key=value&key=value
拼接,只需要客戶端與服務(wù)端私下約定好規(guī)則即可,例如可以在前后拼接約定好的字符串73281937jjdsa key=value&key=value dsjahdjsah
,或者對(duì)md5
進(jìn)行加鹽操作,這樣攻擊者希望僅僅通過(guò)抓包和自己嘗試生成sign的夢(mèng)想就完全破滅了。
【進(jìn)階②:添加時(shí)間戳,防止請(qǐng)求重放】在上述示例中,我們添加了timestamp
這個(gè)時(shí)間戳,它的作用就是防止請(qǐng)求重放
;通過(guò)上述的分析我們發(fā)現(xiàn)確實(shí)達(dá)到了可以防止請(qǐng)求體中的內(nèi)容被篡改的問(wèn)題,可以很大程度保證客戶端數(shù)據(jù)的真實(shí)性,但是遇到類似搶購(gòu)、秒殺業(yè)務(wù)的時(shí)候我們需要思考一個(gè)問(wèn)題:當(dāng)我們需要通過(guò)程序來(lái)?yè)屬?gòu)一個(gè)商品時(shí),實(shí)際上不需要仿造任何請(qǐng)求,只需要通過(guò)抓包抓到提交商品訂單
的請(qǐng)求,然后通過(guò)程序不斷重放,例如在1秒內(nèi)請(qǐng)求10次,就可以獲得遠(yuǎn)勝于手動(dòng)點(diǎn)擊的搶購(gòu)速度,這對(duì)于一般的用戶是不公平的,對(duì)服務(wù)器的負(fù)擔(dān)也會(huì)大大增加。服務(wù)端可以通過(guò)nginx
等對(duì)相同ip的請(qǐng)求次數(shù)加以限制,但是又有ip池
等反制措施,所以對(duì)請(qǐng)求重放的限制也是必須的。而在添加timestamp
參數(shù)后,客戶端獲取當(dāng)前timestamp
一并傳給服務(wù)端,服務(wù)端只需要將校驗(yàn)相等的sign
存在緩存中,并且在下一次請(qǐng)求時(shí),判斷sign
是否在之前緩存的sign
中即可。若在之前緩存的sign
中,則直接響應(yīng)錯(cuò)誤信息。因正常的客戶端每次請(qǐng)求都會(huì)生成最新的毫秒級(jí)的timestamp
,則不會(huì)受任何影響(因?yàn)槊看紊傻?code>sign必然不同),但通過(guò)請(qǐng)求重放
方式提交的請(qǐng)求將被視為無(wú)效請(qǐng)求。
AES加密
aes
為對(duì)稱加密
,即加密和解密的密鑰是相同的,客戶端可以和服務(wù)端私下約定好一個(gè)密鑰,然后客戶端通過(guò)這個(gè)密鑰對(duì)請(qǐng)求體進(jìn)行加密,服務(wù)端通過(guò)這個(gè)密鑰進(jìn)行解密,若服務(wù)端能正常解密,則認(rèn)為這是一個(gè)有效請(qǐng)求,通過(guò)aes
加密可以有效防止請(qǐng)求被篡改,因?yàn)橥ㄟ^(guò)抓包看到的是aes
加密之后的密文,抓包者不知道密鑰的情況下無(wú)法獲得明文信息,也無(wú)法修改明文內(nèi)容。aes
加密后的內(nèi)容一般需要進(jìn)行base64
處理,因?yàn)橛行?code>aes加密后的字符串是不可讀的。
MD5加密
md5
加密是一種不可逆的加密
,也就是明文通過(guò)md5
加密后獲得密文后,無(wú)法通過(guò)密文解密獲得明文,且相同明文加密后獲得的密文必定相同且唯一,所以md5
一般也用作密碼加密(這就是為什么大多數(shù)網(wǎng)站只能提供"重置密碼功能"而不能提供"查詢密碼"功能的原因,因?yàn)榧词故情_(kāi)發(fā)者也不知道用戶的明文密碼是什么,服務(wù)端驗(yàn)證密碼也只是對(duì)比md5
之后的密碼)和sign加密(因md5
是不可逆并且唯一的,所以可以避免泄露sign簽名的規(guī)則,并且可以保證前后端計(jì)算出的sign的一致性)。
但是md5
也不是完全不可逆的,一些網(wǎng)站也推出了md5
解密功能,但是實(shí)際基本都是使用暴力破解字典
的方案,例如123456
通過(guò)md5
加密后的結(jié)果為49ba59abbe56e057
,則已知密文為49ba59abbe56e057
可以推算出明文為123456
。因此密碼不宜過(guò)于簡(jiǎn)單,如果是字母+數(shù)字的情況下,破解就幾乎不可能。近年有報(bào)道指明md5
已可逆、已不再安全,但是目前而言md5
依然被廣泛應(yīng)用在各個(gè)需要加密的場(chǎng)景中,總體還是依舊可靠的。
實(shí)現(xiàn)sign簽名+密碼aes加密(示例)
- ios App+springboot登錄接口sign簽名+密碼aes加密示例
iOS App
-
在
LoginViewController
的點(diǎn)擊登錄按鈕事件中,請(qǐng)求登錄接口//點(diǎn)擊了登錄按鈕 - (IBAction)loginAction:(id)sender { NSString *account = self.accountTf.text; NSString *password = self.pwdTf.text; if(account.length && password.length){ //對(duì)密碼進(jìn)行aes加密,key是xsahdjsad890dsaf password = [EncryptionTool aesEncrypt:password key:@"xsahdjsad890dsaf"]; //發(fā)送登錄請(qǐng)求 [HttpRequest postInterface:@"/login" postData:@{@"account":account,@"password":password} callBack:^(BOOL result, id _Nonnull data) { if(result && data){ int code = [data[@"code"] intValue]; if(code == 0){ //登錄成功 } } }]; } }
-
在
HttpRequest
的postInterface
方法中獲取sign和timestamp+ (void)postInterface:(NSString *)interface postData:(id)postData callBack:(kGetDataEventHandler)_result { NSString *urlStr = [NSString stringWithFormat:@"%@%@",kMainUrl,interface]; NSURL *url = [NSURL URLWithString:urlStr]; NSMutableURLRequest *mr = [NSMutableURLRequest requestWithURL:url]; mr.HTTPMethod = @"POST"; NSMutableDictionary *muDic = [postData mutableCopy]; //獲取&設(shè)置timestamp muDic[@"timestamp"] = [self getTimeStamp]; //獲取&設(shè)置sign NSString *sign = [self getSignWithDic:muDic interface:interface]; muDic[@"sign"] = sign; NSString *postJson = [self getJsonStrWithDic:muDic]; mr.HTTPBody = [postJson dataUsingEncoding:NSUTF8StringEncoding]; [mr setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; mr.timeoutInterval = TimeOutSec; [NSURLConnection sendAsynchronousRequest:mr queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) { if (connectionError) { _result(NO,connectionError); }else{ NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSData *reData = [dataStr dataUsingEncoding:NSUTF8StringEncoding]; _result(YES,[NSJSONSerialization JSONObjectWithData:reData options:NSJSONReadingMutableLeaves error:nil]); } }]; }
-
在
HttpRequest
的getSignWithDic
方法中計(jì)算sign+ (NSString *)getSignWithDic:(NSDictionary *)dic interface:(NSString *)interface { //將請(qǐng)求體中的key按照a-z排列 NSArray *sortedKeys = [[dic allKeys] sortedArrayUsingSelector: @selector(compare:)]; NSString *sumStr = @""; //請(qǐng)求體中排除timestamp,并且按照key+value拼接成一個(gè)字符串 for (NSString *key in sortedKeys) { if(![key isEqualToString:@"timestamp"]){ NSObject *value = [dic valueForKey:key]; NSString *valueStr = [NSString stringWithFormat:@"%@",value]; sumStr = [sumStr stringByAppendingString:[NSString stringWithFormat:@"%@%@",key,valueStr]]; } } //設(shè)計(jì)自己的sign簽名規(guī)則 //mysign$#@+interface(接口路徑:/login)+sumStr(按照key+value拼接成一個(gè)字符串)+timestamp+csjnjksadh,然后md5加密 sumStr = [NSString stringWithFormat:@"mysign$#@%@%@%@csjnjksadh",interface,sumStr,dic[@"timestamp"]]; NSString *sign = [EncryptionTool md5Hex:[NSString stringWithFormat:@"%@",sumStr]]; return sign; }
JAVA后端接口
-
在
LoginController
中接收/login
請(qǐng)求@RestController @RequestMapping("/api/v1/") public class LoginController { @RequestMapping("/login") public CommonResponse login(@RequestBody LoginVO vo) { System.out.println("請(qǐng)求參數(shù)=> " + vo.toString()); //簽名校驗(yàn) //一般放在攔截器/過(guò)濾器中統(tǒng)一處理,此處為了方便直接寫(xiě)在控制器中 if(!SignUtils.checkSign(vo,"/login")){ return new CommonResponse().error("簽名校驗(yàn)失敗"); } String account = vo.getAccount(); String password = vo.getPassword(); //----------begin賬號(hào)和密碼判空操作----------- if(null == account || account.isEmpty()){ return new CommonResponse().error("賬號(hào)不能為空"); } if(null == password || password.isEmpty()){ return new CommonResponse().error("密碼不能為空"); } //----------end----------- //密碼aes解密 try { password = AESUtils.decrypt(password,"xsahdjsad890dsaf"); } catch (Exception e) { e.printStackTrace(); return new CommonResponse().error("密碼解密失敗"); } //對(duì)賬號(hào)密碼進(jìn)行簡(jiǎn)單的校驗(yàn) //賬號(hào)為:zxlee,密碼為123456時(shí),可以登錄成功 if(!"zxlee".equals(account)){ return new CommonResponse().error("用戶名不存在"); } if(!"123456".equals(password)){ return new CommonResponse().error("密碼錯(cuò)誤"); } return new CommonResponse("登錄成功").success(); } }
-
在
SignUtils
中計(jì)算和驗(yàn)證signpublic class SignUtils { //用于在內(nèi)存中緩存合法的sign,實(shí)際項(xiàng)目中建議存在redis中或用Spring Cache之類的進(jìn)行管理 static ArrayList<String> signCahceArr = new ArrayList<>(); /** * @Description: 計(jì)算簽名 * @Param: [vo, inter] * @return: java.lang.String * @Author: zxlee * @Date: 2022/1/21 */ public static String getSign(CommonVO vo,String inter){ String result = ""; Map<String,Object> map = (Map<String,Object>) JSON.toJSON(vo); Set set = map.keySet(); Object[] arr = set.toArray(); Arrays.sort(arr); for(Object key : arr){ if(!"timestamp".equals(key) && !"sign".equals(key)){ result += key + map.get(key).toString(); } } result = "mysign$#@" + inter + result + map.get("timestamp") + "csjnjksadh"; return DigestUtils.md5DigestAsHex(result.getBytes()); } /** * @Description: 驗(yàn)證簽名是否合法 * @Param: [vo, inter] * @return: java.lang.Boolean * @Author: zxlee * @Date: 2022/1/21 */ public static Boolean checkSign(CommonVO vo,String inter){ String sign = vo.getSign(); //如果入?yún)⒅衧ign不存在,直接返回false if(null == sign || sign.isEmpty()){ return false; } //如果signCahceArr中已經(jīng)存在此sign,則直接返回false,可有效避免請(qǐng)求重放 if(signCahceArr.contains(sign)){ return false; } //如果入?yún)⒅衧ign不存在,直接返回false String calcSign = getSign(vo,inter); Boolean equals = calcSign.equals(sign); if(equals){ //如果簽名驗(yàn)證通過(guò),將合法的sign存到緩存中,因?yàn)樘砑恿藅imestamp參數(shù),可以正常請(qǐng)求下保證同一客戶端每次請(qǐng)求sign必定不同 //若考慮高并發(fā)情況,建議根據(jù)ip區(qū)分一下sign signCahceArr.add(sign); } return equals; } }
驗(yàn)證
運(yùn)行iOS App,輸入賬號(hào)密碼,點(diǎn)擊登錄,登錄流程正常。
-
開(kāi)啟
Charles
進(jìn)行全局代理抓包,重復(fù)上述步驟,攔截到登錄請(qǐng)求請(qǐng)求URL:http://api.zxlee.cn:6303/api/v1/login
請(qǐng)求體:
{ "password": "cbBIs8XOZJ2L5YjfuaOLAQ==", "account": "zxlee", "timestamp": "1642929374691", "sign": "469751ce43abf684e8fbf6786d8343b0" }
響應(yīng):
{ "message": "success", "code": 0, "data": "登錄成功" }
修改請(qǐng)求體內(nèi)容,重新提交請(qǐng)求,響應(yīng):
{ "message": "簽名校驗(yàn)失敗", "code": 400, "data": null }
在
Charles
中右鍵請(qǐng)求,點(diǎn)擊Repeat
進(jìn)行請(qǐng)求重放(不修改任何參數(shù)),響應(yīng):{ "message": "簽名校驗(yàn)失敗", "code": 400, "data": null }
經(jīng)過(guò)測(cè)試,各項(xiàng)功能達(dá)到預(yù)期要求。
逆向分析
【目的&思路】
- 【目的】通過(guò)逆向分析破解
sign
簽名和aes
加密 - 【思路】破解
sign
簽名的關(guān)鍵就是分析清楚md5
之前的字符串是根據(jù)何種規(guī)則拼接的,然后根據(jù)這個(gè)規(guī)則拼接參數(shù)然后進(jìn)行md5加密即可。破解ase
加密的關(guān)鍵是獲取”密鑰“。二者的核心都是攔截加密函數(shù),破解sign
簽名通過(guò)攔截md5
加密函數(shù)獲取md5
之前的字符串、aes
加密通過(guò)攔截加密函數(shù)獲取形參中的key。 - 【說(shuō)明】?jī)H對(duì)逆向思路作簡(jiǎn)要說(shuō)明,具體代碼請(qǐng)查看demo:HookApp
【方案1】函數(shù)hook(class-dump+Logos)
-
① 通過(guò)
class-dump
導(dǎo)出Target.app
(將.ipa
后綴修改為.zip
后解壓)的頭文件,查看頭文件中的內(nèi)容,尋找加密的工具類,可以發(fā)現(xiàn)一個(gè)名為EncryptionTool.h
的頭文件,查看文件內(nèi)容:#import <objc/NSObject.h> @interface EncryptionTool : NSObject { } + (id)AES128Decrypt:(id)arg1 key:(id)arg2; + (id)AES128Encrypt:(id)arg1 key:(id)arg2; + (id)md5Hex:(id)arg1; + (id)aesDecryptWithBase64:(id)arg1 key:(id)arg2; + (id)aesEncrypt:(id)arg1 key:(id)arg2; @end
可以觀察到這個(gè)工具類中有
+ (id)md5Hex:(id)arg1
方法,根據(jù)命名可以猜測(cè)這個(gè)函數(shù)是用于md5
加密的,我們通過(guò)Logos
hook這個(gè)函數(shù):%hook EncryptionTool + (id)md5Hex:(id)arg1{ NSLog(@"md5加密之前的明文:%@",arg1); return %orig; } %end
注入后重新運(yùn)行后輸入賬號(hào)密碼點(diǎn)擊登錄并查看打?。?/p>
TargetApp[18861:4954135] md5加密之前的明文:mysign$#@/loginaccountzxleepasswordcbBIs8XOZJ2L5YjfuaOLAQ==1642953195950csjnjksadh
并通過(guò)抓包查看請(qǐng)求體內(nèi)容,與上方
md5
之前的明文進(jìn)行對(duì)照:{ "password": "cbBIs8XOZJ2L5YjfuaOLAQ==", "account": "zxlee", "timestamp": "1642953195950", "sign": "bc7c62f09da86b2ee0c28476a70be709" }
從上方可以推測(cè)出
sign
簽名的規(guī)則為:mysign$#@+interface(接口路徑:/login)+sumStr(按照key+value拼接成一個(gè)字符串)+timestamp+csjnjksadh。此時(shí)sign
簽名就已被破解。 -
②
EncryptionTool.h
中有兩個(gè)aes相關(guān)的類,不清楚實(shí)際上用的是哪個(gè),因此兩個(gè)都hook一下:%hook EncryptionTool + (id)AES128Encrypt:(id)arg1 key:(id)arg2{ NSLog(@"aes加密之前的明文:%@;aes的key:%@",arg1,arg2); return %orig; } + (id)aesEncrypt:(id)arg1 key:(id)arg2{ NSLog(@"aes加密之前的明文:%@;aes的key:%@",arg1,arg2); return %orig; } %end
注入后重新運(yùn)行后輸入賬號(hào)密碼點(diǎn)擊登錄并查看打?。?/p>
TargetApp[18861:4954135] aes加密之前的明文:123456;aes的key:xsahdjsad890dsaf
因此
aes
加密的key為xsahdjsad890dsaf
,至于具體是那種aes
加密的模式,只需要通過(guò)在線aes
加密工具逐一驗(yàn)證一下即可。
【方案2】方法追蹤(class-dump+monkeyDev)
【ps】需要導(dǎo)入ZXHookUtil
-
① 與【方案1】一致,通過(guò)
class-dump
導(dǎo)出頭文件,發(fā)現(xiàn)EncryptionTool.h
這個(gè)頭文件,通過(guò):[ZXHookUtil addClassTrace:@"EncryptionTool"];
添加方法追蹤,監(jiān)視
EncryptionTool
這個(gè)類的方法調(diào)用情況,運(yùn)行后輸入賬號(hào)密碼點(diǎn)擊登錄并查看打?。?/p>┌ +[Call][EncryptionTool aesEncrypt:123456 key:xsahdjsad890dsaf] │ ┌ +[Call][EncryptionTool AES128Encrypt:123456 key:xsahdjsad890dsaf] │ └ +[Return]cbBIs8XOZJ2L5YjfuaOLAQ== └ +[Return]cbBIs8XOZJ2L5YjfuaOLAQ== ┌ +[Call][EncryptionTool md5Hex:mysign$#@/loginaccountzxleepasswordcbBIs8XOZJ2L5YjfuaOLAQ==1642955401392csjnjksadh] └ +[Return]cf1e6b5ddb2b51764b7e44a9b1fd080e
由以上的打印可以看到方法調(diào)用關(guān)系,一組通過(guò)
[
連接起來(lái)的就是方法調(diào)用的起始和終止位置,可以看到在點(diǎn)擊登錄按鈕之后EncryptionTool aesEncrypt
方法內(nèi)又調(diào)用了EncryptionTool AES128Encrypt
,并且我們可以清晰看到參數(shù)和返回值,sign
簽名和aes
加密均已破解。
【方案3】UI分析+IDA反編譯(monkeyDev+IDA)
【ps】需要導(dǎo)入ZXHookUtil
-
① 在初始化時(shí)書(shū)寫(xiě)代碼:
//延時(shí)1秒 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //添加全局的紅色按鈕 [ZXHookUtil addBtnCallBack:^(UIButton *button) { //在這個(gè)紅色按鈕的點(diǎn)擊事件中,打印當(dāng)前顯示的控制器 NSLog(@"當(dāng)前控制器--%@",[ZXHookUtil getTopVC]); }]; });
點(diǎn)擊紅色按鈕,查看打?。?/p>
TargetApp[1359:165053] 當(dāng)前控制器--<LoginViewController: 0x147e12d60>
可知,當(dāng)前控制器為
LoginViewController
。 -
② 繼續(xù)在初始化的地方添加對(duì)
LoginViewController
的方法追蹤:[ZXHookUtil addClassTrace:@"LoginViewController"];
重新運(yùn)行項(xiàng)目,輸入賬號(hào)密碼后點(diǎn)擊登錄按鈕:
┌ -[Call][<LoginViewController: 0x10530fab0> loginAction:<UIButton: 0x105314af0>] │ ┌ -[Call][<LoginViewController: 0x10530fab0> accountTf] │ └ -[Return]<UITextField: 0x10582b000;> │ ┌ -[Call][<LoginViewController: 0x10530fab0> pwdTf] │ └ -[Return]<UITextField: 0x106031c00;> └ -[Return]void
由上方打印可以看出,在點(diǎn)擊登錄按鈕之后,調(diào)用了
LoginViewController
的loginAction
方法。我們通過(guò)IDA
對(duì)loginAction
方法中的代碼進(jìn)行反編譯。 -
③ 我們?cè)?code>IDA中導(dǎo)入
TargetApp.app
內(nèi)的可執(zhí)行文件,并找到-[LoginViewControll loginAction:]
方法,通過(guò)F5
直接查看反編譯后的偽代碼:void __cdecl -[LoginViewController loginAction:](LoginViewController *self, SEL a2, id a3) { LoginViewController *v3; // x19 void *v4; // x0 void *v5; // x20 UITextField *v6; // x0 void *v7; // x0 void *v8; // x22 void *v9; // x0 void *v10; // x20 UITextField *v11; // x0 void *v12; // x0 void *v13; // x24 void *v14; // x0 void *v15; // x22 struct objc_object *v16; // x0 __int64 v17; // x21 void *v18; // x0 __int64 v19; // x23 void *v20; // x0 void *v21; // x19 void **v22; // [xsp+0h] [xbp-80h] __int64 v23; // [xsp+8h] [xbp-78h] __int64 (__fastcall *v24)(); // [xsp+10h] [xbp-70h] void *v25; // [xsp+18h] [xbp-68h] LoginViewController *v26; // [xsp+20h] [xbp-60h] const __CFString *v27; // [xsp+28h] [xbp-58h] const __CFString *v28; // [xsp+30h] [xbp-50h] void *v29; // [xsp+38h] [xbp-48h] __int64 v30; // [xsp+40h] [xbp-40h] v3 = self; v4 = objc_msgSend(self, "view", a3); v5 = (void *)objc_retainAutoreleasedReturnValue(v4); objc_msgSend(v5, "endEditing:", 1LL); objc_release(v5); v6 = -[LoginViewController accountTf](v3, "accountTf"); v7 = (void *)objc_retainAutoreleasedReturnValue(v6); v8 = v7; v9 = objc_msgSend(v7, "text"); v10 = (void *)objc_retainAutoreleasedReturnValue(v9); objc_release(v8); v11 = -[LoginViewController pwdTf](v3, "pwdTf"); v12 = (void *)objc_retainAutoreleasedReturnValue(v11); v13 = v12; v14 = objc_msgSend(v12, "text"); v15 = (void *)objc_retainAutoreleasedReturnValue(v14); objc_release(v13); if ( objc_msgSend(v10, "length") && objc_msgSend(v15, "length") ) { v16 = +[EncryptionTool aesEncrypt:key:]( &OBJC_CLASS___EncryptionTool, "aesEncrypt:key:", v15, CFSTR("xsahdjsad890dsaf")); v17 = objc_retainAutoreleasedReturnValue(v16); objc_release(v15); v27 = CFSTR("account"); v28 = CFSTR("password"); v29 = v10; v30 = v17; v18 = objc_msgSend(&OBJC_CLASS___NSDictionary, "dictionaryWithObjects:forKeys:count:", &v29, &v27, 2LL); v19 = objc_retainAutoreleasedReturnValue(v18); v22 = _NSConcreteStackBlock; v23 = 3254779904LL; v24 = sub_10001240C; v25 = &unk_1000184C8; v26 = v3; +[HttpRequest postInterface:postData:callBack:]( &OBJC_CLASS___HttpRequest, "postInterface:postData:callBack:", CFSTR("/login"), v19, &v22, _NSConcreteStackBlock, 3254779904LL, sub_10001240C, &unk_1000184C8, v3); objc_release(v19); v15 = (void *)v17; } else { v20 = objc_msgSend(v3, "view"); v21 = (void *)objc_retainAutoreleasedReturnValue(v20); objc_msgSend(v21, "makeToast:duration:position:", CFSTR("賬號(hào)或密碼不得為空"), off_10001E658, 1.5); objc_release(v21); } objc_release(v15); objc_release(v10); }
從上方偽代碼中我們可以找到一段關(guān)鍵的代碼:
v16 = +[EncryptionTool aesEncrypt:key:]( &OBJC_CLASS___EncryptionTool, "aesEncrypt:key:", v15, CFSTR("xsahdjsad890dsaf"));
其中括號(hào)內(nèi)的第一個(gè)參數(shù)
&OBJC_CLASS___EncryptionTool
代表方法的類名,第二個(gè)參數(shù)"aesEncrypt:key:"
代表的是方法名,第三、第四個(gè)參數(shù)分別代表aesEncrypt:key:
這個(gè)方法的兩個(gè)入?yún)ⅲ?code>v15通過(guò)上文推倒可以知道是用戶輸入的密碼文本,CFSTR("xsahdjsad890dsaf")
就是這個(gè)密碼aes
加密的key。至此,我們成功獲取到aes
的key。 -
④ 猜測(cè)sign簽名代碼是在封裝的請(qǐng)求的內(nèi)部,從上方偽代碼可以看出,登錄事件中,調(diào)用了
+[HttpRequest postInterface:postData:callBack:]
進(jìn)行請(qǐng)求,我們繼續(xù)查看這個(gè)方法中的偽代碼:void __cdecl +[HttpRequest postInterface:postData:callBack:](HttpRequest_meta *self, SEL a2, id a3, id a4, id a5) { objc_msgSend(self, "baseInterface:postData:callBack:", a3, a4, a5); }
可以看出,
[HttpRequest postInterface:postData:callBack:]
方法中直接調(diào)用了[HttpRequest baseInterface:postData:callBack:]
方法,我們繼續(xù)查看[HttpRequest baseInterface:postData:callBack:]
中的偽代碼:void __cdecl +[HttpRequest baseInterface:postData:callBack:](HttpRequest_meta *self, SEL a2, id a3, id a4, id a5) { id v5; // x21 id v6; // x20 HttpRequest_meta *v7; // x25 __int64 v8; // x19 __int64 v9; // x1 void *v10; // x20 __int64 v11; // x1 __int64 v12; // x21 void *v13; // x0 void *v14; // x0 void *v15; // x23 void *v16; // x0 __int64 v17; // x0 __int64 v18; // x24 struct objc_object *v19; // x0 void *v20; // x0 __int64 v21; // x0 __int64 v22; // x22 void *v23; // x0 __int64 v24; // x0 __int64 v25; // x23 void *v26; // x0 void *v27; // x0 void *v28; // x24 void *v29; // x26 void *v30; // x0 __int64 v31; // x27 void *v32; // x0 __int64 v33; // x27 void *v34; // x0 void *v35; // x0 void *v36; // x25 void *v37; // x0 __int64 v38; // x28 void *v39; // x0 __int64 v40; // x26 __int64 v41; // x1 __int64 v42; // x21 void **v43; // [xsp+18h] [xbp-78h] __int64 v44; // [xsp+20h] [xbp-70h] __int64 (__fastcall *v45)(); // [xsp+28h] [xbp-68h] void *v46; // [xsp+30h] [xbp-60h] __int64 v47; // [xsp+38h] [xbp-58h] v5 = a5; v6 = a4; v7 = self; v8 = objc_retain(a3, a2); v10 = (void *)objc_retain(v6, v9); v12 = objc_retain(v5, v11); v13 = objc_msgSend(&OBJC_CLASS___UIApplication, "sharedApplication"); v14 = (void *)objc_retainAutoreleasedReturnValue(v13); v15 = v14; v16 = objc_msgSend(v14, "keyWindow"); v17 = objc_retainAutoreleasedReturnValue(v16); v18 = v17; v19 = +[MBProgressHUD showHUDAddedTo:animated:](&OBJC_CLASS___MBProgressHUD, "showHUDAddedTo:animated:", v17, 1LL); objc_unsafeClaimAutoreleasedReturnValue(v19); objc_release(v18); objc_release(v15); v20 = objc_msgSend( &OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@%@"), CFSTR("http://api.zxlee.cn:6303/api/v1"), v8); v21 = objc_retainAutoreleasedReturnValue(v20); v22 = v21; v23 = objc_msgSend(&OBJC_CLASS___NSURL, "URLWithString:", v21); v24 = objc_retainAutoreleasedReturnValue(v23); v25 = v24; v26 = objc_msgSend(&OBJC_CLASS___NSMutableURLRequest, "requestWithURL:", v24); v27 = (void *)objc_retainAutoreleasedReturnValue(v26); v28 = v27; if ( v10 ) { objc_msgSend(v27, "setHTTPMethod:", CFSTR("POST")); v29 = objc_msgSend(v10, "mutableCopy"); v30 = objc_msgSend(v7, "getTimeStamp"); v31 = objc_retainAutoreleasedReturnValue(v30); objc_msgSend(v29, "setObject:forKeyedSubscript:", v31, CFSTR("timestamp")); objc_release(v31); //這里給v32賦值,就是給sign賦值,我們可以發(fā)現(xiàn)這個(gè)v32是通過(guò)一個(gè)方法的返回值賦值,這個(gè)方法的類名為v7,方法名為getSignWithDic:interface:,繼續(xù)往上推導(dǎo),可以發(fā)現(xiàn)v7=self,也就是當(dāng)前類,所以sign是通過(guò)[HttpRequest getSignWithDic:interface:]計(jì)算出來(lái)的,因此我們繼續(xù)查看[HttpRequest getSignWithDic:interface:]的偽代碼 v32 = objc_msgSend(v7, "getSignWithDic:interface:", v29, v8); //v33=v32 v33 = objc_retainAutoreleasedReturnValue(v32); //設(shè)置sign,v33就是最終的sign objc_msgSend(v29, "setObject:forKeyedSubscript:", v33, CFSTR("sign")); v34 = objc_msgSend(v7, "getJsonStrWithDic:", v29); v35 = (void *)objc_retainAutoreleasedReturnValue(v34); v36 = v35; v37 = objc_msgSend(v35, "dataUsingEncoding:", 4LL); v38 = objc_retainAutoreleasedReturnValue(v37); objc_msgSend(v28, "setHTTPBody:", v38); objc_release(v38); objc_msgSend(v28, "setValue:forHTTPHeaderField:", CFSTR("application/json"), CFSTR("Content-Type")); objc_release(v36); objc_release(v33); objc_release(v29); } else { objc_msgSend(v27, "setHTTPMethod:", CFSTR("GET")); } objc_msgSend(v28, "setTimeoutInterval:", 10.0); v39 = objc_msgSend(&OBJC_CLASS___NSOperationQueue, "mainQueue"); v40 = objc_retainAutoreleasedReturnValue(v39); v43 = _NSConcreteStackBlock; v44 = 3254779904LL; v45 = sub_100008518; v46 = &unk_100018378; v47 = v12; v42 = objc_retain(v12, v41); objc_msgSend(&OBJC_CLASS___NSURLConnection, "sendAsynchronousRequest:queue:completionHandler:", v28, v40, &v43); objc_release(v40); objc_release(v47); objc_release(v42); objc_release(v28); objc_release(v25); objc_release(v22); objc_release(v10); objc_release(v8); }
從上方
objc_msgSend(v29, "setObject:forKeyedSubscript:", v33, CFSTR("sign"));
可以看出,這句代碼在給請(qǐng)求體設(shè)置sign,我在這一行上方添加了注釋,大家可以看一下。經(jīng)過(guò)分析可以發(fā)現(xiàn)sign是通過(guò)[HttpRequest getSignWithDic:interface:]
計(jì)算出來(lái)的,因此我們繼續(xù)查看[HttpRequest getSignWithDic:interface:]
的偽代碼://a3就是getSignWithDic:后方的入?yún)?,a4是interface:后方的入?yún)?id __cdecl +[HttpRequest getSignWithDic:interface:](HttpRequest_meta *self, SEL a2, id a3, id a4) { id v4; // x19 void *v5; // x20 __int64 v6; // x1 void *v7; // x0 void *v8; // x0 void *v9; // x19 void *v10; // x0 __int64 v11; // x20 __int64 v12; // x1 void *v13; // x0 void *v14; // x0 void *v15; // x23 __CFString *v16; // x26 __int64 v17; // x25 const __CFString *v18; // x21 unsigned __int64 v19; // x19 __int64 v20; // x27 void *v21; // x0 __int64 v22; // x0 __int64 v23; // x28 __CFString *v24; // x20 void *v25; // x0 __int64 v26; // x0 const __CFString *v27; // x22 __int64 v28; // x21 void *v29; // x0 __int64 v30; // x0 __int64 v31; // x27 void *v32; // x0 __int64 v33; // x0 void *v34; // x0 __int64 v35; // x0 __int64 v36; // x20 void *v37; // x0 __int64 v38; // x21 void *v39; // x0 __int64 v40; // x0 __int64 v41; // x20 struct objc_object *v42; // x0 __int64 v43; // x19 __int64 v44; // x0 __int64 v46; // [xsp+20h] [xbp-160h] void *v47; // [xsp+30h] [xbp-150h] void *v48; // [xsp+48h] [xbp-138h] void *v49; // [xsp+58h] [xbp-128h] __int128 v50; // [xsp+60h] [xbp-120h] __int128 v51; // [xsp+70h] [xbp-110h] __int128 v52; // [xsp+80h] [xbp-100h] __int128 v53; // [xsp+90h] [xbp-F0h] char v54; // [xsp+A0h] [xbp-E0h] __int64 v55; // [xsp+120h] [xbp-60h] v4 = a4; //v5=a3 v5 = (void *)objc_retain(a3, a2); //v46=v4 v46 = objc_retain(v4, v6); //v47=v5 v47 = v5; v7 = objc_msgSend(v5, "allKeys"); v8 = (void *)objc_retainAutoreleasedReturnValue(v7); v9 = v8; v10 = objc_msgSend(v8, "sortedArrayUsingSelector:", "compare:"); v11 = objc_retainAutoreleasedReturnValue(v10); objc_release(v9); v52 = 0u; v53 = 0u; v50 = 0u; v51 = 0u; v13 = (void *)objc_retain(v11, v12); v49 = v13; v14 = objc_msgSend(v13, "countByEnumeratingWithState:objects:count:", &v50, &v54, 16LL); if ( v14 ) { v15 = v14; v16 = &stru_100018798; v17 = *(_QWORD *)v51; v18 = CFSTR("timestamp"); do { v19 = 0LL; v48 = v15; do { if ( *(_QWORD *)v51 != v17 ) objc_enumerationMutation(v49); v20 = *(_QWORD *)(*((_QWORD *)&v50 + 1) + 8 * v19); if ( !((unsigned __int64)objc_msgSend(*(void **)(*((_QWORD *)&v50 + 1) + 8 * v19), "isEqualToString:", v18) & 1) ) { v21 = objc_msgSend(v47, "valueForKey:", v20); v22 = objc_retainAutoreleasedReturnValue(v21); v23 = v22; v24 = v16; v25 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@"), v22); v26 = objc_retainAutoreleasedReturnValue(v25); v27 = v18; v28 = v26; v29 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@%@"), v20, v26); v30 = objc_retainAutoreleasedReturnValue(v29); v31 = v30; v32 = objc_msgSend(v16, "stringByAppendingString:", v30); v16 = (__CFString *)objc_retainAutoreleasedReturnValue(v32); objc_release(v24); objc_release(v31); v33 = v28; v18 = v27; objc_release(v33); objc_release(v23); v15 = v48; } ++v19; } while ( v19 < (unsigned __int64)v15 ); v15 = objc_msgSend(v49, "countByEnumeratingWithState:objects:count:", &v50, &v54, 16LL); } while ( v15 ); } else { v16 = &stru_100018798; } objc_release(v49); //v34=v47[@"timestamp"],因?yàn)関47就是傳進(jìn)來(lái)的dic,所以這里取的就是當(dāng)前請(qǐng)求體中的時(shí)間戳 v34 = objc_msgSend(v47, "objectForKeyedSubscript:", CFSTR("timestamp")); //v35=v34 v35 = objc_retainAutoreleasedReturnValue(v34); v36 = v35; //v37=mysign$#+v46+v16+v35+csjnjksadh v37 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("mysign$#@%@%@%@csjnjksadh"), v46, v16, v35); //v38=v37 v38 = objc_retainAutoreleasedReturnValue(v37); objc_release(v16); objc_release(v36); //v39=v38 v39 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@"), v38); //v40=v39 v40 = objc_retainAutoreleasedReturnValue(v39); v41 = v40; //這里對(duì)sign進(jìn)行md5加密,因此加密之前的sign就是我們要的,即v40 v42 = +[EncryptionTool md5Hex:](&OBJC_CLASS___EncryptionTool, "md5Hex:", v40); v43 = objc_retainAutoreleasedReturnValue(v42); objc_release(v41); objc_release(v38); objc_release(v49); objc_release(v46); v44 = objc_release(v47); if ( __stack_chk_guard == v55 ) v44 = v43; return (id)_objc_autoreleaseReturnValue(v44); }
通過(guò)分析上方偽代碼(分析過(guò)程見(jiàn)上方代碼中的注釋)可以得出
sign
簽名規(guī)則為:mysign$#@+interface(接口路徑:/login)+sumStr(按照key+value拼接成一個(gè)字符串)+timestamp+csjnjksadh,然后md5加密。 ⑤ 【總結(jié)】:使用
IDA
分析比其他方案繁瑣得多,但是更可靠更嚴(yán)謹(jǐn),但是一般情況是仍然推薦【方案1】和【方案2】。