iOSApp+springboot后端sign簽名+aes加密流程&逆向破解分析示例(class-dump+Logos+monkeyDev+IDA)

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&timestamp=1642765564000
  • ② 對(duì)上方account=zxlee&password=123456&timestamp=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&timestamp=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){
                        //登錄成功
                    }
                }
            }];
        }
    }
    
  • HttpRequestpostInterface方法中獲取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]);
            }
        }];
    }
    
  • HttpRequestgetSignWithDic方法中計(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)證sign

    public 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ò)Logoshook這個(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)用了LoginViewControllerloginAction方法。我們通過(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】。

End

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,553評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,620評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,416評(píng)論 6 405
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,940評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,170評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,597評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評(píng)論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,029評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,407評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,663評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,403評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,746評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容