0%

逆向某拉货APP登录过程

注意

  • 此文仅限于技术交流,请不要做违法的事情。对于那些居心叵测的人根据此文造成违法的事情与本人无关。此文章不得转载!!!
  • 关键图片信息已打码,关键代码信息已用XXOO替代。如果APP方需要删除,请发邮件zhulongfei28@gmail.com,谢谢。
  • 这并不是基础教程,需要一定iOS逆向经验的人才能看懂。

准备

  • ida反汇编mach-o文件
  • class-dump头文件
  • Reveal查看APP层级视图
  • Theos编写tweak插件
  • Charles+SSL Kill Switch 2https抓包
  • Frida Hook框架

寻找切入点

  • App切换到使用账号密码登录界面,输入账号和密码,点击两次登录按钮,使用Charles查看抓包内容。

  • 可以发现_sign_su_t三个参数在变,所以接下来就是要探索这三个参数的生成过程。怎样切入呢?本文从点击登录按钮动作开始一层一层往下寻找。

  • 使用Reveal查看发现登录按钮属于PasswordLoginVC这个控制器,那么登录执行的方法也在这个头文件中。遗憾的是从主mach-o导出的头文件中并没有找到PasswordLoginVC.h,那么肯定是在其它模块中包含的。怎样寻找哪个模块包含PasswordLoginVC这个类呢,可以使用LLDB。

  • LLDB连接上后执行image lookup -r -n PasswordLoginVC,发现在HLLCommonModule这个模块中。根据经验可知,从.app/Frameworks中可以找到这个模块的mach-o文件,然后使用class-dump导出头文件。

  • 打开PasswordLoginVC.h经过排除,以下几个方法可能是点击登录按钮要执行的操作,所以接下来要确定是哪个方法是我们需要的。

    1
    2
    3
    4
    - (void)changePasswordLogin;	// IMP=0x000000000007e534
    - (void)fieldChangeAction:(id)arg1; // IMP=0x000000000007e27c
    - (void)securityAction:(id)arg1; // IMP=0x000000000007e1c0
    - (void)login; // IMP=0x000000000007d400
  • mach-o没有恢复符号,可以通过地址断点给上面四个方法下断点,看会命中哪个;可以通过frida-trace跟踪函数调用。本文采用简单的方法,如下,发现调用了login方法。

    1
    2
    3
    4
    $ frida-trace -U -m "*[PasswordLoginVC *]" 货拉拉
    ....
    30522 ms -[PasswordLoginVC login]
    30522 ms | -[PasswordLoginVC isAgree]

寻找可变参数

  • HLLCommonModulemach-o文件拖进IDA,分析-[PasswordLoginVC login],发现最显眼的一个方法,接着在进去这个方法分析。

    1
    +[HLLCommonNetServer loginWithTel:password:currentCityId:response:]
  • 进去之后,又发现如下核心代码,还是没有我们需要的那三个参数,进去这个方法继续分析。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    v41 = ((id (__cdecl *)(HLLNetRequest_meta *, SEL, id, id, signed __int64, id, id, id))objc_msgSend)(
    (HLLNetRequest_meta *)&OBJC_CLASS___HLLNetRequest,
    "requestDataWithClass:key:type:method:params:response:",
    (id)CFSTR("Login"),
    0LL,
    0LL,
    (id)CFSTR("login_by_pwd"),
    v38,
    &v47);
  • 进去+[HLLNetRequest requestDataWithClass:key:type:method:params:response:]发现urlJoinTogetherWithPeams:method:,有关键字joinmerge像是在生成需要的参数

    1
    2
    3
    4
    5
    v28 = ((id (__cdecl *)(HllUrlMergeManager_meta *, SEL, id, id))objc_msgSend)(
    (HllUrlMergeManager_meta *)&OBJC_CLASS___HllUrlMergeManager,
    "urlJoinTogetherWithPeams:method:",
    v17,
    v15);
  • 我们需要urlJoinTogetherWithPeams:method:方法下地址断点,在方法执行后打印一下执行结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    (lldb) image list -o -f | grep HLLCommonModule
    [ 0] 0x0000000100a04000 /private/var/containers/Bundle/Application/ 0C0095F0-160B-4BCA-8D91-F357CF72A295/XXOO.app/Frameworks/HLLCommonModule.framework/HLLCommonModule (0x0000000100a04000)
    (lldb) breakpoint set -a 0x0000000100a04000+0x000000000004F8D8

    (lldb) po $x0
    {
    "_su" = 20072417571221220000003299129480;
    "_t" = 1595584632;
    args = "{\"password\":\"e10adc3949ba59abbe56e057f20f883e\",\"ref\":\"\",\"device_id\":\"292126F625BD47CFAB6E44162DCC85CA\",\"city_id\":1002,\"phone_no\":\"18618618618\",\"device_type\":\"iPhone 6\",\"idfa\":\"3D506F30-D4D9-49C2-A61E-EC5EF222D475\"}";
    "device_id" = 292126F625BD47CFAB6E44162DCC85CA;
    "device_type" = "iPhone 6";
    os = ios;
    revision = 6446;
    version = "6.4.46";
    }
  • 可以发现_su-t参数,我们前面提到三个可变参数,现在已经找到两个了。点击+[HllUrlMergeManager urlJoinTogetherWithPeams:method:]进去查看方法实现,大致浏览一遍可以发现_su-t的生成过程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    v22 = +[HLLGlobelManager startUUIDString](&OBJC_CLASS___HLLGlobelManager, "startUUIDString");
    v23 = objc_retainAutoreleasedReturnValue(v22);
    v24 = objc_msgSend(&OBJC_CLASS___NSDate, "date");
    v25 = (void *)objc_retainAutoreleasedReturnValue(v24);
    objc_msgSend(v25, "timeIntervalSince1970");
    v27 = (signed __int64)v26;
    objc_release(v25);
    objc_msgSend(v9, "setValue:forKey:", v23, CFSTR("_su"));
    v28 = objc_msgSend(&OBJC_CLASS___NSNumber, "numberWithLong:", v27);
    v29 = objc_retainAutoreleasedReturnValue(v28);
    objc_msgSend(v9, "setValue:forKey:", v29, CFSTR("_t"));
    objc_release(v29);

解析_su参数

  • 由上可知_su来自于v23,v23来自于+[HLLGlobelManager startUUIDString],查看方法实现如下。根据经验可知+[NSDate dateStringForTimeInterval:format:]不是iOS自带的方法,猜测是NSDate的分类方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    v2 = objc_msgSend(&OBJC_CLASS___NSMutableString, "string");
    v3 = (void *)objc_retainAutoreleasedReturnValue(v2);
    v4 = objc_msgSend(&OBJC_CLASS___NSDate, "date");
    v5 = (void *)objc_retainAutoreleasedReturnValue(v4);
    objc_msgSend(v5, "timeIntervalSince1970");
    v6 = objc_msgSend(&OBJC_CLASS___NSDate, "dateStringForTimeInterval:format:", CFSTR("yyMMddHHmmssSSS"));
    v7 = objc_retainAutoreleasedReturnValue(v6);
    objc_msgSend(v3, "appendFormat:", CFSTR("%@"), v7);
    objc_release(v7);
    objc_release(v5);
    objc_msgSend(v3, "appendString:", CFSTR("2"));
    objc_msgSend(v3, "appendString:", CFSTR("000000"));
    v8 = arc4random();
    objc_msgSend(v3, "appendFormat:", CFSTR("%010lld"), v8);
  • 查看+[NSDate dateStringForTimeInterval:format:]伪代码实现

    • lldb中执行image lookup -r -s dateStringForTimeInterval:format:,结果显示方法所在的模块是HLLKit

    • HLLKit的mach-o文件拖进IDA进行分析,伪代码实现如下,经验可知都是iOS自带的方法,不需要往下分析。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      +[NSDate dateStringForTimeInterval:format:](NSDate_meta *self, SEL a2, double a3, id a4)
      v4 = a3;
      v5 = objc_retain(a4);
      v6 = objc_msgSend(&OBJC_CLASS___NSDate, "dateWithTimeIntervalSince1970:", v4);
      v7 = objc_retainAutoreleasedReturnValue(v6);
      v8 = objc_msgSend(&OBJC_CLASS___NSDateFormatter, "new");
      v9 = (void *)objc_alloc(&OBJC_CLASS___NSLocale);
      v10 = objc_msgSend(v9, "initWithLocaleIdentifier:", CFSTR("en_US_POSIX"));
      objc_msgSend(v8, "setLocale:", v10);
      objc_release(v10);
      objc_msgSend(v8, "setDateFormat:", v5);
      objc_release(v5);
      v11 = objc_msgSend(v8, "stringFromDate:", v7);
      v12 = objc_retainAutoreleasedReturnValue(v11);
      objc_release(v8);
      objc_release(v7);
      return (id)_objc_autoreleaseReturnValue(v12);
    • 参照上下文,实现如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @implementation NSDate (Extension)
      + (NSString *)dateStringForTimeInterval:(NSTimeInterval)timeInterval format:(NSString *)format {
      NSDate *date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
      NSDateFormatter *dateFormatter = [NSDateFormatter new];
      NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
      [dateFormatter setLocale:locale];
      [dateFormatter setDateFormat:format];
      NSString *dateString = [dateFormatter stringFromDate:date];
      return dateString;
      }
      @end
  • 已经手动实现了+[NSDate dateStringForTimeInterval:format:],接下来+[HLLGlobelManager startUUIDString]参照上下文实现如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    NSString *startUUIDString() {
    NSMutableString *strM = [NSMutableString string];
    NSTimeInterval timeInterval = [[NSDate date] timeIntervalSince1970];
    NSString *dateString = [NSDate dateStringForTimeInterval:timeInterval format:@"yyMMddHHmmssSSS"];
    [strM appendFormat:@"%@", dateString];
    [strM appendString:@"2"];
    [strM appendFormat:@"000000"];
    [strM appendFormat:@"%010lld",arc4random()];
    return strM;
    }
  • 根据上面两段代码,整合一下_su生成过程,到这里_su的值已经解析完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    NSString *_su() {
    // 生成dateString
    NSTimeInterval timeInterval = [[NSDate date] timeIntervalSince1970];
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
    NSDateFormatter *dateFormatter = [NSDateFormatter new];
    NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
    [dateFormatter setLocale:locale];
    [dateFormatter setDateFormat: @"yyMMddHHmmssSSS"];
    NSString *dateString = [dateFormatter stringFromDate:date];
    // 拼接字符串
    NSMutableString *strM = [NSMutableString string];
    [strM appendFormat:@"%@", dateString];
    [strM appendString:@"2"];
    [strM appendFormat:@"000000"];
    [strM appendFormat:@"%010lld",arc4random()];
    return strM;
    }

解析_t参数

  • 寻找可变参数部分可知,_t来自于v29,v29来自于v28,v28来自于v27,v27来自于v26,而v26并看不到来源。

    1
    2
    3
    4
    5
    v27 = (signed __int64)v26;
    ...
    v28 = objc_msgSend(&OBJC_CLASS___NSNumber, "numberWithLong:", v27);
    v29 = objc_retainAutoreleasedReturnValue(v28);
    objc_msgSend(v9, "setValue:forKey:", v29, CFSTR("_t"));
  • 伪代码模式下可以发现v26在汇编模式下的值是D0。这个时候鼠标点击到v27 = (signed __int64)v26;这行,按Tab键从伪代码模式切换到汇编模式,得出v26 == D0 == [[NSDate date] timeIntervalSince1970]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ADRP            X8, #classRef_NSDate@PAGE
    LDR X0, [X8,#classRef_NSDate@PAGEOFF] ; void *
    ADRP X8, #selRef_date@PAGE
    LDR X1, [X8,#selRef_date@PAGEOFF] ; char *
    BL _objc_msgSend
    MOV X29, X29
    BL _objc_retainAutoreleasedReturnValue
    MOV X23, X0
    ADRP X8, #selRef_timeIntervalSince1970@PAGE
    LDR X1, [X8,#selRef_timeIntervalSince1970@PAGEOFF] ; char *
    BL _objc_msgSend
    FCVTZS X24, D0
  • _t实现过程的完整代码为

    1
    2
    3
    4
    5
    NSNumber *_t() {
    NSTimeInterval timeInterval = [[NSDate date] timeIntervalSince1970];
    NSNumber *number = [NSNumber numberWithLong:(signed long long)timeInterval];
    return number;
    }

解析_sign参数

查找_sign第一重来源

  • _t_su都已经解析完成,剩下一个_sign。到现在并没有发现_sign的任何线索,怎么办呢?从上可知_t_su都是通过setValue:forKey:进行赋值的,猜测_sign也是这样赋值的。

  • 当key的值为_sign时,value的值就是需要的,然后往上追索value的来源。条件断点最适合,如下。

    1
    br set -n setValue:forKey: -c '(BOOL)[(NSString *)$x3 containsString:@"_sign"]'
  • 当断点命中时,查看调用堆栈,看value的值来自于哪里。

    1
    2
    3
    4
    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
    * frame #0: 0x0000000186706660 Foundation` -[NSMutableDictionary(NSKeyValueCoding) setValue:forKey:]
    frame #1: 0x00000001020c2030 HLLKit` __17-[HLLSession GET]_block_invoke + 216
  • 发现0x00000001020c2030处给value赋值了,所以需要查看这个地址在mach-o文件中的位置

    1
    2
    3
    (lldb) image lookup -a 0x00000001020c2030
    Address: HLLKit[0x0000000000026030] (HLLKit.__TEXT.__text + 135936)
    Summary: HLLKit`__17-[HLLSession GET]_block_invoke + 216
  • IDA中按G找到0x0000000000026030处代码,调用signForAllParameters方法生成了value,也就是_sign的值。

    1
    2
    3
    4
    5
    v4 = objc_msgSend(*(void **)(v1 + 32), "allparameters");
    v5 = (void *)objc_retainAutoreleasedReturnValue(v4);
    v6 = objc_msgSend(*(void **)(v1 + 32), "signForAllParameters");
    v7 = objc_retainAutoreleasedReturnValue(v6);
    objc_msgSend(v5, "setValue:forKey:", v7, CFSTR("_sign"));

查找_sign第二重来源

  • 查看signForAllParameters的伪代码,发现还挺长。精简处理后,保留如下重要的部分。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    v8 = objc_msgSend(&OBJC_CLASS___NSURL, "URLWithString:", +[HLLSession hostURL]);
    v11 = objc_msgSend(&OBJC_CLASS___NSURLComponents, "componentsWithURL:resolvingAgainstBaseURL:", v8, 0LL);
    v13 = objc_msgSend(v11, "host");
    v16 = objc_msgSend(
    v13,
    "stringByReplacingOccurrencesOfString:withString:",
    CFSTR("XXOOmove.net"),
    CFSTR("XXOO.cn"));
    v18 = objc_msgSend(
    v16,
    "stringByReplacingOccurrencesOfString:withString:",
    CFSTR("XXOOmove.com"),
    CFSTR("XXOO.cn"));
    v20 = ((NSMutableDictionary *(__cdecl *)(HLLSession *, SEL))objc_msgSend)(v2, "allparameters");
    v23 = HLLStringUtil003(v20, v18);
    return v23;
  • 从上可知核心代码是v23 = HLLStringUtil003(v20, v18);,我们在这行下地址断点,打印x0和x1的值,也就是伪代码中v20和v18的值。(后文把v20的值成为所有参数)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    (lldb) image list -o -f | grep HLLKit
    [ 0] 0x00000001008e8000 /private/var/containers/Bundle/Application/ 0C0095F0-160B-4BCA-8D91-F357CF72A295/XXOO.app/Frameworks/HLLKit.framework/HLLKit (0x00000001008e8000)
    (lldb) breakpoint set -a 0x00000001008e8000+0x000000000002817C

    (lldb) po $x0
    {
    "_m" = "login_by_pwd";
    "_su" = 20072521323852720000001535952001;
    "_t" = 1595683958;
    args = "{\"password\":\"e10adc3949ba59abbe56e057f20f883e\",\"ref\":\"\", \"device_id\":\"292126F625BD47CFAB6E44162DCC85CA\",\"city_id\":1002, \"phone_no\":\"18618618618\",\"device_type\":\"iPhone 6\", \"idfa\":\"3D506F30-D4D9-49C2-A61E-EC5EF222D475\"}";
    "device_id" = 292126F625BD47CFAB6E44162DCC85CA;
    "device_type" = "iPhone 6";
    os = ios;
    revision = 6446;
    version = "6.4.46";
    }

    (lldb) po $x1
    uapi.XXOO.cn

查找_sign第三重来源

  • 接下来就是查看HLLStringUtil003的实现。伪代码还是很长,截取重要的部分如下。根据上下文可知v5的值是uapi.XXOO.cn,经过各种判断,首先来到v19 = CFSTR("kZErbmP$XXOO$c0jtQ&ru0s3lGW87");,然后来到LABEL_25:,最后到LABEL_26:返回值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    if ( (unsigned __int64)objc_msgSend(v5, "hasSuffix:", CFSTR("XXOO.cn")) & 1
    || (unsigned __int64)objc_msgSend(v5, "hasSuffix:", CFSTR("XXOOmove.net")) & 1 )
    {
    v13 = objc_msgSend(v5, "componentsSeparatedByString:", CFSTR("."));
    v16 = objc_msgSend(v13, "firstObject");
    v17 = (void *)objc_retainAutoreleasedReturnValue(v16);
    if ( (unsigned int)objc_msgSend(v17, "hasPrefix:", CFSTR("uimg")) )
    {
    ...
    LABEL_24:
    objc_release(v40);
    LABEL_25:
    v43 = HLLStringUtil102(v3, (__int64)v19);
    v44 = objc_retainAutoreleasedReturnValue(v43);
    v41 = objc_retain(v44, v45);
    goto LABEL_26;
    }

    v23 = objc_msgSend([NSBundle mainBundle], "bundleIdentifier");
    v24 = (__CFString *)objc_retainAutoreleasedReturnValue(v23);
    if ( (unsigned __int64)objc_msgSend(v24, "isEqualToString:", CFSTR("com.XXOO.driver")) & 1
    || (unsigned __int64)objc_msgSend(v24, "isEqualToString:", CFSTR("cn.XXOO.ltldriver")) & 1
    || (unsigned int)objc_msgSend(v24, "isEqualToString:", CFSTR("com.XXOOmove.global.driver.india")) )
    {
    ...
    }
    else
    {
    if ( (unsigned int)objc_msgSend(v24, "isEqualToString:", CFSTR("com.XXOO.user"))
    || (unsigned int)objc_msgSend(v24, "isEqualToString:", CFSTR("com.XXOOmove.client.india")) )
    {
    v19 = CFSTR("kZErbmP$XXOO$c0jtQ&ru0s3lGW87");
    }
    else
    {
    ...
    }
    }

    LABEL_11:
    v40 = v24;
    goto LABEL_24;
    LABEL_26:
    return _objc_autoreleaseReturnValue(v41);
  • 上面分析可知v43 = HLLStringUtil102(v3, (__int64)v19);,这句代码就是我们下步要探索的。使用LLDB打印,发现参数v3就是所有参数,v19我们也在上面求过了。(下文把v19称为签名key)。进去HLLStringUtil102后发现伪代码超长,有400多行。先看第一个需要跳转的地方,LLDB中打印objc_msgSend(v16, "count")的返回值为0,所以跳转到LABEL_26:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    v17 = 0LL;
    do
    {
    if ( (unsigned __int64)objc_msgSend(v16, "count") <= v17 )
    goto LABEL_26;
    v18 = objc_msgSend(v16, "objectAtIndexedSubscript:", v17);
    v19 = (void *)objc_retainAutoreleasedReturnValue(v18);
    v20 = objc_msgSend(v19, "rangeOfString:", CFSTR("application-identifier"));
    objc_release(v19);
    ++v17;
    }
    while ( v20 == (void *)0x7FFFFFFFFFFFFFFFLL );
  • 根据上下文可知v3的值是所有参数的值,[allKeyes count]肯定不为空,所以LABEL_26:直接跳过不执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    LABEL_26:
    if ( !v3
    || (v47 = objc_msgSend(v3, "allKeys"),
    v48 = (void *)objc_retainAutoreleasedReturnValue(v47),
    v49 = objc_msgSend(v48, "count"),
    objc_release(v48),
    !v49) )
    {
    v72 = 0LL;
    LABEL_44:
    v66 = v113;
    v46 = v115;
    goto LABEL_45;
    }
  • 跳过LABEL_26:代码后就会来到下面代码(代码经过精简的)。看着挺长的,其实就是对allkeys首先进行排序,然后通过key取出value,对key和value进行拼接。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    v53 = objc_msgSend([allparameters allKeys], "sortedArrayUsingComparator:", &__block_literal_global_19);
    v116 = objc_msgSend(&OBJC_CLASS___NSMutableString, "string");
    v58 = objc_msgSend(v53, "countByEnumeratingWithState:objects:count:", &v120, &v128, 16LL);
    if ( v58 )
    {
    v59 = v58;
    v60 = *v122;
    do
    {
    v61 = 0LL;
    do
    {
    if ( *v122 != v60 )
    objc_enumerationMutation(v119);
    v62 = *(_QWORD *)(v121 + 8 * v61);
    if ( !((unsigned __int64)objc_msgSend(*(void **)(v121 + 8 * v61), "hasPrefix:", CFSTR("__")) & 1) )
    {
    v63 = objc_msgSend(v118, "objectForKey:", v62);
    v64 = (void *)objc_retainAutoreleasedReturnValue(v63);
    v65 = objc_msgSend(&OBJC_CLASS___NSData, "class");
    if ( !((unsigned __int64)objc_msgSend(v64, "isKindOfClass:", v65) & 1) )
    objc_msgSend(v116, "appendFormat:", CFSTR("%@%@"), v62, v64);
    objc_release(v64);
    }
    ++v61;
    }
    while ( v61 < (unsigned __int64)v59 );
    v59 = objc_msgSend(v119, "countByEnumeratingWithState:objects:count:", &v120, &v128, 16LL);
    }
    while ( v59 );
    }
  • 自己实现上面代码,后文把最终结果称为keyValueStr

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // allParameters是上文`所有参数`的值
    NSMutableString *keyValueStr = [NSMutableString string];
    NSArray *allKeys= [[allParameters allKeys] sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
    return [obj1 compare:obj2];
    }];
    for (NSString *key in allKeys) {
    id value = [allParameters objectForKey:key];
    if (![key hasPrefix:@"__"] && ![value isKindOfClass:[NSData class]]) {
    [keyValueStr appendFormat:@"%@%@", key, value];
    }
    }
    NSLog(@"最终的值为%@", keyValueStr);
  • 上面的代码执行完成后,会来到如下代码。根据上下文可知v113就是签名key,v116就是keyValueStr,这两个值上文已经求出,直接进行字符串拼接就得到v70的值。v70进行md5就得到v109的值,也就是最终我们要求出的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    if ( (unsigned __int64)objc_msgSend(v113, "isEqualToString:", CFSTR("kZErbmP$XXOO$c0jtQ&ru0s3lGW87")) & 1
    || (unsigned int)objc_msgSend(v113, "isEqualToString:", CFSTR("P8P$XXOO&fKox3uT#lq3a6YN!jBmMlBYw4")) )
    {
    v70 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@%@%@"), v113, v116, v113);
    v71 = (struct objc_object *)objc_retainAutoreleasedReturnValue(v70);
    } else
    {
    ...
    }

    v109 = ((id (__cdecl *)(HLLMD5_meta *, SEL, id, signed __int64, bool))objc_msgSend)(
    (HLLMD5_meta *)&OBJC_CLASS___HLLMD5,
    "md5ForString:bit:uppercase:",
    v71,
    32LL,
    1);
  • 根据上面代码可知对v70进行md5,生成32位的大写字符串。所以[HLLMD5 md5ForString:bit:uppercase:]不用往下深究,直接给出代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    NSString *stringToMD5String(NSString *string) {
    const char *cStr = [string UTF8String];
    unsigned char digest[CC_MD5_DIGEST_LENGTH];
    CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
    NSMutableString *result = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
    for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
    [result appendFormat:@"%02X", digest[i]];
    }
    return [result uppercaseString];
    }
  • 最后来个大总结,串一下刚才上面的分析,要不然显得有些凌乱。

    • _sign来自于-[HLLSession signForAllParameters]
    • -[HLLSession signForAllParameters]来自于HLLStringUtil003
    • HLLStringUtil003来自于HLLStringUtil102
    • HLLStringUtil102实现在上面已经说过了,这里把代码整合一下。(为了简单都写成C函数)。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      NSString *getMD5FromString(NSString *string, BOOL uppercase) {
      const char *cStr = [string UTF8String];
      unsigned char digest[CC_MD5_DIGEST_LENGTH];
      CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
      NSMutableString *result = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
      for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
      [result appendFormat:@"%02X", digest[i]];
      }
      return uppercase ? [result uppercaseString] : [result lowercaseString];
      }
      NSString *getKeyValueStrFromAllParameters(NSDictionary *allParameters) {
      NSMutableString *keyValueStr = [NSMutableString string];
      NSArray *allKeys= [[allParameters allKeys] sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
      return [obj1 compare:obj2];
      }];
      for (NSString *key in allKeys) {
      id value = [allParameters objectForKey:key];
      if (![key hasPrefix:@"__"] && ![value isKindOfClass:[NSData class]]) {
      [keyValueStr appendFormat:@"%@%@", key, value];
      }
      }
      return keyValueStr;
      }
      // key就是上文的`签名key`,allParameters就是上的`所有参数`
      NSString *_sign(NSDictionary *allParameters, NSString *key) {
      NSString *keyValueStr = getKeyValueStrFromAllParameters(allParameters);
      NSString *lastStr = [NSString stringWithFormat:@"%@%@%@",key,keyValueStr,key];
      NSString *sign = getMD5FromString(lastStr, YES);
      return sign;
      }
  • 到此三个可变参数_sign_su_t都解析完成。

解析args参数

  • 从抓包截图可知args参数还没有解析,只不过它不是可变的,所以我们放在最后进行解析。args的值是json字符串,根据经验可知,它的原始类型是NSDictionary。
  • password是32位的小写字符串,猜测是对用户输入密码就行md5加密,验证一下,证明了猜想。
  • refdevice_idcity_iddevice_typeidfa直接写死就可以,phone_no来自于用户输入的手机号
  • 终上所述,生成args的代码为
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    NSString *phoneNo = @"18618618618";
    NSString *password = getMD5FromString(@"123456", NO);
    NSString *deviceId = @"292126F625BD47CFAB6E44162DCC85CA";
    NSString *deviceType = @"iPhone 6";
    // 生成args
    NSMutableDictionary *argsDict = [NSMutableDictionary dictionary];
    argsDict[@"password"] = password;
    argsDict[@"ref"] = @"";
    argsDict[@"device_id"] = deviceId;
    argsDict[@"city_id"] = @"1002";
    argsDict[@"phone_no"] = phoneNo;
    argsDict[@"device_type"] = deviceType;
    argsDict[@"idfa"] = @"3D506F30-D4D9-49C2-A61E-EC5EF222D47";
    NSData *argsData = [NSJSONSerialization dataWithJSONObject:argsDict options:0 error:nil];
    // 最终结果
    NSString *argsStr = [[NSString alloc] initWithData:argsData encoding:NSUTF8StringEncoding];

发起登录请求

  • api接口使用的是https协议,所以有两种方式进行访问。本文简单起见,用的是tweak项目

    • 直接新建tweak项目,直接访问。
    • 新建Xcode项目,从ipa解压包中找到访问https的cer证书,然后在NSURLSession代理方法中加载cer证书,证书验证通过后,就可以访问了。
  • 封装的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    #import <CommonCrypto/CommonDigest.h>
    // 获取_su参数
    NSString *_su() {
    // 生成dateString
    NSTimeInterval timeInterval = [[NSDate date] timeIntervalSince1970];
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
    NSDateFormatter *dateFormatter = [NSDateFormatter new];
    NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
    [dateFormatter setLocale:locale];
    [dateFormatter setDateFormat: @"yyMMddHHmmssSSS"];
    NSString *dateString = [dateFormatter stringFromDate:date];
    // 拼接字符串
    NSMutableString *strM = [NSMutableString string];
    [strM appendFormat:@"%@", dateString];
    [strM appendString:@"2"];
    [strM appendFormat:@"000000"];
    [strM appendFormat:@"%010u",arc4random()];
    return strM;
    }
    // 获取_t参数
    NSNumber *_t() {
    NSTimeInterval timeInterval = [[NSDate date] timeIntervalSince1970];
    NSNumber *number = [NSNumber numberWithLong:(signed long long)timeInterval];
    return number;
    }
    // 获取字符串的md5值
    NSString *getMD5FromString(NSString *string, BOOL uppercase) {
    const char *cStr = [string UTF8String];
    unsigned char digest[CC_MD5_DIGEST_LENGTH];
    CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
    NSMutableString *result = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
    for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
    [result appendFormat:@"%02X", digest[i]];
    }
    return uppercase ? [result uppercaseString] : [result lowercaseString];
    }
    // 获取keyValueStr
    NSString *getKeyValueStrFromAllParameters(NSDictionary *allParameters) {
    NSMutableString *keyValueStr = [NSMutableString string];
    NSArray *allKeys= [[allParameters allKeys] sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
    return [obj1 compare:obj2];
    }];
    for (NSString *key in allKeys) {
    id value = [allParameters objectForKey:key];
    if (![key hasPrefix:@"__"] && ![value isKindOfClass:[NSData class]]) {
    [keyValueStr appendFormat:@"%@%@", key, value];
    }
    }
    return keyValueStr;
    }
    // 获取_sign值
    NSString *_sign(NSDictionary *allParameters, NSString *key) {
    NSString *keyValueStr = getKeyValueStrFromAllParameters(allParameters);
    NSString *lastStr = [NSString stringWithFormat:@"%@%@%@",key,keyValueStr,key];
    NSString *sign = getMD5FromString(lastStr, YES);
    return sign;
    }
    // 把NSDictionary参数转化为query字符串
    NSString *getStrFromDict(NSDictionary *dict) {
    NSMutableString *strM = [NSMutableString string];
    for (int i = 0; i < dict.allKeys.count; i++) {
    if (i != 0) [strM appendString:@"&"];
    NSString *key = dict.allKeys[i];
    NSString *value = dict[key];
    [strM appendFormat:@"%@=%@",key,value];
    }
    return strM;
    }
    // 获取args参数
    NSString *getArgsStr(NSString *phoneNo, NSString *password, NSString *deviceId, NSString *deviceType ) {
    NSMutableDictionary *argsDict = [NSMutableDictionary dictionary];
    argsDict[@"password"] = getMD5FromString(password, NO);
    argsDict[@"ref"] = @"";
    argsDict[@"device_id"] = deviceId;
    argsDict[@"city_id"] = @"1002";
    argsDict[@"phone_no"] = phoneNo;
    argsDict[@"device_type"] = deviceType;
    argsDict[@"idfa"] = @"3D506F30-D4D9-49C2-A61E-EC5EF222D47";
    NSData *argsData = [NSJSONSerialization dataWithJSONObject:argsDict options:0 error:nil];
    NSString *argsStr = [[NSString alloc] initWithData:argsData encoding:NSUTF8StringEncoding];
    return argsStr;
    }
  • tweak中进行访问。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    %hook PasswordLoginVC

    - (void)login {
    // 登录手机号
    NSString *phoneNo = @"18618618618";
    // 登录密码
    NSString *password = @"123456";
    NSString *deviceId = @"292126F625BD47CFAB6E44162DCC85CA";
    NSString *deviceType = @"iPhone 6";
    // 生成args
    NSString *args = getArgsStr(phoneNo, password, deviceId, deviceType);
    // query参数
    NSMutableDictionary *queryDict = [NSMutableDictionary dictionary];
    queryDict[@"_m"] = @"login_by_pwd";
    queryDict[@"_su"] = _su();
    queryDict[@"_t"] = _t();
    queryDict[@"args"] = args;
    queryDict[@"device_id"] = deviceId;
    queryDict[@"device_type"] = deviceType;
    queryDict[@"os"] = @"ios";
    queryDict[@"revision"] = @"6446";
    queryDict[@"version"] = @"6.4.46";
    queryDict[@"_sign"] = _sign(queryDict, @"kZErbmP$XXOO$c0jtQ&ru0s3lGW87");
    // 得到最终参数后进行转义
    NSString *queryStr = getStrFromDict(queryDict);
    queryStr = [queryStr stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet URLQueryAllowedCharacterSet]];
    // 进行请求
    NSURL *url = [NSURL URLWithString: [NSString stringWithFormat:@"https://uapi.XXOO.cn?%@",queryStr]];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"GET";
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSString *str = [[NSString alloc] initWithData:data encoding: NSUTF8StringEncoding];
    NSLog(@"打印值:%@--%@",error, str);
    }] resume];
    }

    %end