对某实时公交App的接口签名Hack过程

3,676 阅读23分钟

背景

笔者经常从家门口乘坐某公交到某地去玩耍,然而那一趟公交发车周期长且时间不定,每次出行前都需要打开实时公交的软件搜索班次和信息不仅浪费流量,而且非常不便。因此笔者便打算基于实时公交接口开发一个-1屏的Today Widget来显示这趟公交的实时信息。

分析

网络数据包分析

首先笔者抓取了App的网络数据包,并对请求数据进行了简要分析,请求数据的内容如下,处于信息安全考虑,敏感信息均以打码。

网络数据接口

请求接口中包含了签名和时间戳字段,时间戳字段有效的防止了请求的连续重放,签名字段则保护了接口数据完整性,以黑盒角度分析,在签名算法未知的情况下仅能通过重放当前请求的形式来获取数据,一旦时间戳与服务器时间相差超过阈值,服务端就会抛出系统时间不准确的错误,以防止这种形式的攻击。由于签名的存在,接下来仅剩下两条路可以走,一个是在原来App的基础上进行二次开发,通过动态库注入的形式来添加自己想要的能力,另一个是通过反编译获取签名算法。

二次开发方式分析

对iOS应用进行二次开发的原理是将应用的可执行文件进行修改,在Mach-O文件的Load Commands字段中加入动态库加载指令,使其能够加载新添加的动态库,然后破解者在动态库中以切面的形式对原来的App逻辑进行修改,最后对应用和动态库进行重签名,这种方式有如下几个缺点:

  1. iOS应用的二进制文件在发布时被加密,在对应用进行开发时需要获取砸壳的二进制文件,而获取砸壳应用的方式只能是通过越狱机或是越狱市场;
  2. 由于使用了动态库,应用不能发布到App Store;
  3. 由于二次开发是基于动态库的,没有办法为应用添加Target来实现一些扩展功能。

考虑到这次的应用主要是为-1屏添加Today Widget,因此二次开发方式似乎行不通。

反编译方式分析

反编译方式似乎是万能的,它的缺点主要是需要人工去定位相关逻辑,并对反汇编得到的汇编代码和反编译得出的C伪代码进行分析,成本较高,但是在iOS平台上实施反编译的成本是相对较低的,原因有如下三点:

  1. iOS所使用的ARM指令集是定长指令,因此通过插入垃圾数据导致反汇编器解析异常的花指令没法应用;
  2. iOS应用在审核时需要对二进制静态扫描,因此对代码混淆有所限制,因此很难做到前端那样完整的混淆;
  3. Objective-C是在C基础上的动态语言,因此在生成的中间代码中会包含大量的Runtime调用,这些Runtime调用对于定位和分析逻辑十分有帮助。

综合上述分析,笔者最后采用了反编译方式,来梳理接口签名的逻辑。

实战

笔者首先通过越狱市场获取了砸壳后的二进制文件,随后将其拖入IDA进行了反汇编。在反汇编完成之后,需要做的第一步是找到接口签名的相关逻辑。

逻辑定位

通过网络数据包可知这是一个GET请求,并且包含了签名信息,因此我们可以在IDA获得的符号中搜索getrequestsignature等关键词来查找相关符号,在这个过程中可能需要排除大量信息,通过搜索,笔者找到了两处可疑的符号。

通过名称判断,第二个方法传递的是一个URL数组,应该是由于批处理的请求,第一个方法则是普通的GET请求,因此我们打开第一个方法的汇编代码,通过IDA插件将其反编译成C伪代码进行分析,伪代码内容如下。

id __cdecl -[DTAFNRequestGet GetDictionaryByURL:Op:KeyAndValue:successBlocker:failureBlockers:](DTAFNRequestGet *self, SEL a2, id a3, id a4, id a5, id a6, id a7)
{
  DTAFNRequestGet *v7; // r5
  id v8; // r6
  int v9; // r8
  int v10; // r1
  int v11; // r6
  int v12; // r1
  int v13; // r4
  int v14; // r1
  int v15; // ST0C_4
  int v16; // r1
  int v17; // ST10_4
  struct objc_object *v18; // r10
  int v19; // r0
  int v20; // r4
  struct objc_object *v21; // r0
  int v22; // r11
  struct objc_object *v23; // r4
  int v24; // r8
  struct objc_object *v25; // r0
  void *v26; // r0
  void *v27; // r5
  void *v28; // r0
  void *v29; // r6
  struct objc_object *v30; // r0
  int v31; // r4
  struct objc_object *v32; // r0
  int v33; // r4
  void *v34; // r0
  void *v35; // r4
  void *v36; // r0
  void *v37; // r6
  void *v38; // r0
  int v39; // r4
  int v40; // r10
  int v41; // r6
  void *v42; // r0
  DTAFNRequestGet *v43; // r4
  SEL v44; // r1
  id v45; // r2
  id v46; // r3
  void *v48; // [sp+14h] [bp-48h]
  int v49; // [sp+18h] [bp-44h]
  int v50; // [sp+1Ch] [bp-40h]
  int (*v51)(); // [sp+20h] [bp-3Ch]
  void *v52; // [sp+24h] [bp-38h]
  int v53; // [sp+28h] [bp-34h]
  void *v54; // [sp+2Ch] [bp-30h]
  int v55; // [sp+30h] [bp-2Ch]
  int v56; // [sp+34h] [bp-28h]
  int (*v57)(); // [sp+38h] [bp-24h]
  void *v58; // [sp+3Ch] [bp-20h]
  int v59; // [sp+40h] [bp-1Ch]
  struct objc_object *v60; // [sp+70h] [bp+14h]
  struct objc_object *v61; // [sp+74h] [bp+18h]

  v7 = self;
  v8 = a4;
  v9 = objc_retain(a3, a2);
  v11 = objc_retain(v8, v10);
  v13 = objc_retain(a5, v12);
  v15 = objc_retain(a6, v14);
  v17 = objc_retain(a7, v16);
  v18 = -[DTAFNetRequest AddNecessaryParamDic:](v7, "AddNecessaryParamDic:", v13);
  objc_release(v13);
  v19 = objc_retainAutoreleasedReturnValue(v18);
  v20 = v19;
  v21 = +[SingalMethod signByHMac:AndHttpMethod:](
          &OBJC_CLASS___SingalMethod,
          "signByHMac:AndHttpMethod:",
          v19,
          CFSTR("GET"));
  v22 = objc_retainAutoreleasedReturnValue(v21);
  objc_release(v20);
  v23 = -[DTAFNetRequest UrlJointByUrl:Op:KeyAndValue:PostKeyAndValue:](
          v7,
          "UrlJointByUrl:Op:KeyAndValue:PostKeyAndValue:",
          v9,
          v11,
          v22,
          0);
  objc_release(v11);
  objc_release(v9);
  v24 = objc_retainAutoreleasedReturnValue(v23);
  v25 = +[AFHTTPRequestOperationManager manager](&OBJC_CLASS___AFHTTPRequestOperationManager, "manager");
  v26 = (void *)objc_retainAutoreleasedReturnValue(v25);
  v27 = v26;
  v28 = objc_msgSend(v26, "securityPolicy");
  v29 = (void *)objc_retainAutoreleasedReturnValue(v28);
  objc_msgSend(v29, "setAllowInvalidCertificates:", 1);
  objc_release(v29);
  v30 = +[AFHTTPResponseSerializer serializer](&OBJC_CLASS___AFHTTPResponseSerializer, "serializer");
  v31 = objc_retainAutoreleasedReturnValue(v30);
  objc_msgSend(v27, "setResponseSerializer:", v31);
  objc_release(v31);
  v32 = +[AFHTTPRequestSerializer serializer](&OBJC_CLASS___AFHTTPRequestSerializer, "serializer");
  v33 = objc_retainAutoreleasedReturnValue(v32);
  objc_msgSend(v27, "setRequestSerializer:", v33);
  objc_release(v33);
  v34 = objc_msgSend(v27, "requestSerializer");
  v35 = (void *)objc_retainAutoreleasedReturnValue(v34);
  objc_msgSend(v35, "setTimeoutInterval:", 20.0);
  objc_release(v35);
  v36 = objc_msgSend(v27, "responseSerializer");
  v37 = (void *)objc_retainAutoreleasedReturnValue(v36);
  v38 = objc_msgSend(
          &OBJC_CLASS___NSSet,
          "setWithObjects:",
          CFSTR("text/html"),
          CFSTR("text/plain"),
          CFSTR("application/json"),
          CFSTR("charset=UTF-8"),
          0);
  v39 = objc_retainAutoreleasedReturnValue(v38);
  objc_msgSend(v37, "setAcceptableContentTypes:", v39);
  objc_release(v39);
  objc_release(v37);
  v54 = &_NSConcreteStackBlock;
  v55 = -1040187392;
  v56 = 0;
  v57 = sub_CA608;
  v58 = &unk_6A014C;
  v59 = v15;
  v48 = &_NSConcreteStackBlock;
  v49 = -1040187392;
  v50 = 0;
  v51 = sub_CA7C0;
  v52 = &unk_6A0164;
  v40 = objc_retain(v15, sub_CA7C0);
  v53 = v17;
  v41 = objc_retain(v17, &selRef_GET_parameters_success_failure_);
  v42 = objc_msgSend(v27, "GET:parameters:success:failure:", v24, 0, &v54, &v48);
  v43 = (DTAFNRequestGet *)objc_retainAutoreleasedReturnValue(v42);
  objc_release(v53);
  objc_release(v59);
  objc_release(v41);
  objc_release(v40);
  objc_release(v27);
  objc_release(v24);
  objc_release(v22);
  return j__objc_autoreleaseReturnValue(v43, v44, v45, v46, a5, a6, a7, v60, v61);
}

其中包含了变量定义和ARC代码,将其去掉后,余下的主要逻辑如下。

id __cdecl -[DTAFNRequestGet GetDictionaryByURL:Op:KeyAndValue:successBlocker:failureBlockers:](DTAFNRequestGet *self, SEL a2, id a3, id a4, id a5, id a6, id a7)
{
  v18 = -[DTAFNetRequest AddNecessaryParamDic:](v7, "AddNecessaryParamDic:", v13);
  v21 = +[SingalMethod signByHMac:AndHttpMethod:](
          &OBJC_CLASS___SingalMethod,
          "signByHMac:AndHttpMethod:",
          v19,
          CFSTR("GET"));
  v23 = -[DTAFNetRequest UrlJointByUrl:Op:KeyAndValue:PostKeyAndValue:](
          v7,
          "UrlJointByUrl:Op:KeyAndValue:PostKeyAndValue:",
          v9,
          v11,
          v22,
          0);
  v25 = +[AFHTTPRequestOperationManager manager](&OBJC_CLASS___AFHTTPRequestOperationManager, "manager");
  v26 = (void *)objc_retainAutoreleasedReturnValue(v25);
  v27 = v26;
  v28 = objc_msgSend(v26, "securityPolicy");
  v29 = (void *)objc_retainAutoreleasedReturnValue(v28);
  objc_msgSend(v29, "setAllowInvalidCertificates:", 1);
  objc_release(v29);
  v30 = +[AFHTTPResponseSerializer serializer](&OBJC_CLASS___AFHTTPResponseSerializer, "serializer");
  v31 = objc_retainAutoreleasedReturnValue(v30);
  objc_msgSend(v27, "setResponseSerializer:", v31);
  objc_release(v31);
  v32 = +[AFHTTPRequestSerializer serializer](&OBJC_CLASS___AFHTTPRequestSerializer, "serializer");
  v33 = objc_retainAutoreleasedReturnValue(v32);
  objc_msgSend(v27, "setRequestSerializer:", v33);
  objc_release(v33);
  v34 = objc_msgSend(v27, "requestSerializer");
  v35 = (void *)objc_retainAutoreleasedReturnValue(v34);
  objc_msgSend(v35, "setTimeoutInterval:", 20.0);
  objc_release(v35);
  v36 = objc_msgSend(v27, "responseSerializer");
  v37 = (void *)objc_retainAutoreleasedReturnValue(v36);
  v38 = objc_msgSend(
          &OBJC_CLASS___NSSet,
          "setWithObjects:",
          CFSTR("text/html"),
          CFSTR("text/plain"),
          CFSTR("application/json"),
          CFSTR("charset=UTF-8"),
          0);
  v39 = objc_retainAutoreleasedReturnValue(v38);
  objc_msgSend(v37, "setAcceptableContentTypes:", v39);
  objc_release(v39);
  objc_release(v37);
  v54 = &_NSConcreteStackBlock;
  v55 = -1040187392;
  v56 = 0;
  v57 = sub_CA608;
  v58 = &unk_6A014C;
  v59 = v15;
  v48 = &_NSConcreteStackBlock;
  v49 = -1040187392;
  v50 = 0;
  v51 = sub_CA7C0;
  v52 = &unk_6A0164;
  v40 = objc_retain(v15, sub_CA7C0);
  v53 = v17;
  v41 = objc_retain(v17, &selRef_GET_parameters_success_failure_);
  v42 = objc_msgSend(v27, "GET:parameters:success:failure:", v24, 0, &v54, &v48);
  v43 = (DTAFNRequestGet *)objc_retainAutoreleasedReturnValue(v42);
  return j__objc_autoreleaseReturnValue(v43, v44, v45, v46, a5, a6, a7, v60, v61);
}

可以看到,在上面的代码中,后半部分是对AFNetworking的调用和回调处理,这一部分对于分析接口签名没有帮助,也可以略去,最后剩下的逻辑如下。

id __cdecl -[DTAFNRequestGet GetDictionaryByURL:Op:KeyAndValue:successBlocker:failureBlockers:](DTAFNRequestGet *self, SEL a2, id a3, id a4, id a5, id a6, id a7)
{
  v18 = -[DTAFNetRequest AddNecessaryParamDic:](v7, "AddNecessaryParamDic:", v13);
  v21 = +[SingalMethod signByHMac:AndHttpMethod:](
          &OBJC_CLASS___SingalMethod,
          "signByHMac:AndHttpMethod:",
          v19,
          CFSTR("GET"));
  v23 = -[DTAFNetRequest UrlJointByUrl:Op:KeyAndValue:PostKeyAndValue:](
          v7,
          "UrlJointByUrl:Op:KeyAndValue:PostKeyAndValue:",
          v9,
          v11,
          v22,
          0);

先不考虑变量传递,可以看到在接口调用前,进行了三个操作,分别是AddNecessaryParamDic:signByHMac::UrlJointByUrl::::,从字面意思分析可知分别是添加必要参数、使用HMac算法签名和构造url,在这里我们已经找到了接口签名的方法,它就是SingalMethod的类方法signByHMac:AndHttpMethod:,下面我们着手分析这个方法的实现。

签名方法分析

签名方法signByHMac:AndHttpMethod:的反编译结果如下。

id __cdecl +[SingalMethod signByHMac:AndHttpMethod:](SingalMethod_meta *self, SEL a2, id a3, id a4)
{
  id v4; // r6
  void *v5; // r8
  void *v6; // r0
  int v7; // r0
  int v8; // r5
  void *v9; // r0
  void *v10; // r4
  const __CFString *v11; // r6
  int v12; // r1
  int v13; // r1
  unsigned int v14; // r10
  int v15; // r4
  void *v16; // r0
  void *v17; // r0
  int v18; // r1
  int v19; // r6
  void *v20; // r0
  const __CFString *v21; // r11
  void *v22; // r0
  int v23; // r8
  void *v24; // r0
  int v25; // r4
  void *v26; // r0
  int v27; // r10
  void *v28; // r0
  int v29; // r0
  int v30; // r11
  void *v31; // r0
  int v32; // r5
  void *v33; // r0
  void *v34; // r0
  int v35; // r4
  void *v36; // r0
  char *v37; // r6
  void *v38; // r4
  int v39; // r1
  void *v40; // r0
  void *v41; // r0
  unsigned int v42; // r5
  int v43; // r6
  void *v44; // r0
  int v45; // r10
  void *v46; // r0
  int v47; // r4
  void *v48; // r0
  const __CFString *v49; // r6
  void *v50; // r0
  int v51; // r4
  id v52; // r2
  id v53; // r3
  int v55; // [sp+Ch] [bp-124h]
  id v56; // [sp+10h] [bp-120h]
  unsigned __int64 v57; // [sp+10h] [bp-120h]
  int v58; // [sp+14h] [bp-11Ch]
  void *v59; // [sp+1Ch] [bp-114h]
  int v60; // [sp+20h] [bp-110h]
  int v61; // [sp+20h] [bp-110h]
  void *v62; // [sp+24h] [bp-10Ch]
  void *v63; // [sp+28h] [bp-108h]
  char *v64; // [sp+2Ch] [bp-104h]
  char *v65; // [sp+30h] [bp-100h]
  void *v66; // [sp+34h] [bp-FCh]
  DTAFNRequestGet *v67; // [sp+34h] [bp-FCh]
  int v68; // [sp+38h] [bp-F8h]
  char *v69; // [sp+3Ch] [bp-F4h]
  void *v70; // [sp+40h] [bp-F0h]
  SingalMethod_meta *v71; // [sp+44h] [bp-ECh]
  __int64 v72; // [sp+48h] [bp-E8h]
  __int64 v73; // [sp+50h] [bp-E0h]
  __int64 v74; // [sp+58h] [bp-D8h]
  __int64 v75; // [sp+60h] [bp-D0h]
  const __CFString *v76; // [sp+68h] [bp-C8h]
  int v77; // [sp+6Ch] [bp-C4h]
  __int64 v78; // [sp+70h] [bp-C0h]
  __int64 v79; // [sp+78h] [bp-B8h]
  __int64 v80; // [sp+80h] [bp-B0h]
  __int64 v81; // [sp+88h] [bp-A8h]
  char v82; // [sp+94h] [bp-9Ch]
  char v83; // [sp+D4h] [bp-5Ch]
  struct objc_object *v84; // [sp+138h] [bp+8h]
  struct objc_object *v85; // [sp+13Ch] [bp+Ch]
  struct objc_object *v86; // [sp+140h] [bp+10h]
  struct objc_object *v87; // [sp+144h] [bp+14h]
  struct objc_object *v88; // [sp+148h] [bp+18h]

  v71 = self;
  v4 = a4;
  v5 = (void *)objc_retain(a3, a2);
  v56 = v4;
  v58 = objc_retain(v4, &selRef_allKeys);
  v6 = objc_msgSend(v5, "allKeys");
  v7 = objc_retainAutoreleasedReturnValue(v6);
  v8 = v7;
  v9 = objc_msgSend(&OBJC_CLASS___NSMutableArray, "arrayWithArray:", v7);
  v10 = (void *)objc_retainAutoreleasedReturnValue(v9);
  objc_release(v8);
  objc_msgSend(v10, "sortUsingComparator:", &off_69F730);
  v11 = &stru_6B8088;
  objc_retain(&stru_6B8088, v12);
  v78 = 0LL;
  v79 = 0LL;
  v80 = 0LL;
  v81 = 0LL;
  v59 = (void *)objc_retain(v10, v13);
  v66 = objc_msgSend(v59, "countByEnumeratingWithState:objects:count:", &v78, &v83, 16);
  v62 = v5;
  if ( v66 )
  {
    v11 = &stru_6B8088;
    v60 = *(_DWORD *)v79;
    do
    {
      v14 = 0;
      do
      {
        v69 = (char *)v11;
        if ( *(_DWORD *)v79 != v60 )
          objc_enumerationMutation(v59);
        v15 = *(_DWORD *)(HIDWORD(v78) + 4 * v14);
        v16 = objc_msgSend(v71, "enCodeURL:", *(_DWORD *)(HIDWORD(v78) + 4 * v14));
        v68 = objc_retainAutoreleasedReturnValue(v16);
        v17 = objc_msgSend(v5, "objectForKeyedSubscript:", v15);
        v19 = objc_retainAutoreleasedReturnValue(v17);
        if ( v19 )
        {
          v20 = objc_msgSend(v5, "objectForKeyedSubscript:", v15);
          v21 = (const __CFString *)objc_retainAutoreleasedReturnValue(v20);
        }
        else
        {
          objc_retain(&stru_6B8088, v18);
          v21 = &stru_6B8088;
        }
        objc_release(v19);
        v22 = objc_msgSend(v71, "enCodeURL:", v21);
        v23 = objc_retainAutoreleasedReturnValue(v22);
        if ( objc_msgSend(v69, "isEqualToString:", &stru_6B8088) )
          v24 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@%@=%@"), v69, v68, v23);
        else
          v24 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@&%@=%@"), v69, v68, v23);
        v25 = objc_retainAutoreleasedReturnValue(v24);
        objc_release(v69);
        v11 = (const __CFString *)v25;
        objc_release(v23);
        objc_release(v21);
        objc_release(v68);
        ++v14;
        v5 = v62;
      }
      while ( v14 < (unsigned int)v66 );
      v66 = objc_msgSend(v59, "countByEnumeratingWithState:objects:count:", &v78, &v83, 16);
    }
    while ( v66 );
  }
  objc_release(v59);
  v26 = objc_msgSend(v71, "enCodeURL:", v11);
  v27 = objc_retainAutoreleasedReturnValue(v26);
  objc_release(v11);
  v28 = objc_msgSend(v71, "enCodeURL:", CFSTR("/"));
  v29 = objc_retainAutoreleasedReturnValue(v28);
  v30 = v29;
  v31 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@&%@&%@"), v58, v29, v27);
  v32 = objc_retainAutoreleasedReturnValue(v31);
  objc_release(v27);
  v33 = objc_msgSend(v71, "hmac:withKey:", v32, CFSTR("23c2f22fadf46f3b28b6adddd242959e&"));
  v61 = objc_retainAutoreleasedReturnValue(v33);
  v76 = CFSTR("signature");
  v77 = v61;
  v34 = objc_msgSend(&OBJC_CLASS___NSDictionary, "dictionaryWithObjects:forKeys:count:", &v77, &v76, 1);
  v35 = objc_retainAutoreleasedReturnValue(v34);
  v36 = objc_msgSend(&OBJC_CLASS___NSMutableDictionary, "dictionaryWithDictionary:", v5);
  v37 = (char *)objc_retainAutoreleasedReturnValue(v36);
  v55 = v35;
  objc_msgSend(v37, "addEntriesFromDictionary:", v35);
  v38 = objc_msgSend(v56, "isEqualToString:", CFSTR("GET"));
  objc_release(v58);
  if ( v38 )
  {
    v57 = __PAIR__(v30, v32);
    v40 = objc_msgSend(&OBJC_CLASS___NSMutableDictionary, "dictionary");
    v67 = (DTAFNRequestGet *)objc_retainAutoreleasedReturnValue(v40);
    v72 = 0LL;
    v73 = 0LL;
    v74 = 0LL;
    v75 = 0LL;
    v65 = v37;
    v41 = objc_msgSend(v37, "allKeys");
    v63 = (void *)objc_retainAutoreleasedReturnValue(v41);
    v70 = objc_msgSend(v63, "countByEnumeratingWithState:objects:count:", &v72, &v82, 16);
    if ( v70 )
    {
      v64 = *(char **)v73;
      do
      {
        v42 = 0;
        do
        {
          if ( *(char **)v73 != v64 )
            objc_enumerationMutation(v63);
          v43 = *(_DWORD *)(HIDWORD(v72) + 4 * v42);
          v44 = objc_msgSend(v71, "encodeParameter:", *(_DWORD *)(HIDWORD(v72) + 4 * v42));
          v45 = objc_retainAutoreleasedReturnValue(v44);
          v46 = objc_msgSend(v65, "objectForKey:", v43);
          v47 = objc_retainAutoreleasedReturnValue(v46);
          if ( v47 )
          {
            v48 = objc_msgSend(v65, "objectForKey:", v43);
            v49 = (const __CFString *)objc_retainAutoreleasedReturnValue(v48);
          }
          else
          {
            v49 = &stru_6B8088;
            objc_retain(&stru_6B8088, "objectForKey:");
          }
          objc_release(v47);
          v50 = objc_msgSend(v71, "encodeParameter:", v49);
          v51 = objc_retainAutoreleasedReturnValue(v50);
          objc_msgSend(v67, "setValue:forKey:", v51, v45);
          objc_release(v51);
          objc_release(v49);
          objc_release(v45);
          ++v42;
        }
        while ( v42 < (unsigned int)v70 );
        v70 = objc_msgSend(v63, "countByEnumeratingWithState:objects:count:", &v72, &v82, 16);
      }
      while ( v70 );
    }
    objc_release(v63);
    v5 = v62;
    v30 = HIDWORD(v57);
    v32 = v57;
    v37 = v65;
  }
  else
  {
    v67 = (DTAFNRequestGet *)objc_retain(v37, v39);
  }
  objc_release(v37);
  objc_release(v55);
  objc_release(v61);
  objc_release(v30);
  objc_release(v32);
  objc_release(v59);
  objc_release(v5);
  return j__objc_autoreleaseReturnValue(v67, __stack_chk_guard, v52, v53, v84, v85, v86, v87, v88);
}

这部分代码很长,笔者在分析时,先把目光放在了涉及的方法调用上,通过搜寻代码中的objc_msgSend,在第一个if语句前包含了如下的方法调用。

v6 = objc_msgSend(v5, "allKeys");
v9 = objc_msgSend(&OBJC_CLASS___NSMutableArray, "arrayWithArray:", v7);
objc_msgSend(v10, "sortUsingComparator:", &off_69F730);
v66 = objc_msgSend(v59, "countByEnumeratingWithState:objects:count:", &v78, &v83, 16);

可见这里的allKeys是对字典的操作,我们向上搜索继续分析v5的传递路径,由于ARC代码的存在,变量在传递时涉及多次赋值操作,分析出的相关代码如下。

id __cdecl +[SingalMethod signByHMac:AndHttpMethod:](SingalMethod_meta *self, SEL a2, id a3, id a4)
{
    // 略去无关代码
    v5 = (void *)objc_retain(a3, a2);
    v6 = objc_msgSend(v5, "allKeys");
}

可见v5是通过对a3进行强引用得到的,a3是方法signByHMac:AndHttpMethod:的第一个参数,因此a3是一个字典,我们可以初步推测它应该是请求的参数字典,为了验证这个说法,我们回到上一级调用者,分析传递到签名方法的参数,关键代码如下。

id __cdecl -[DTAFNRequestGet GetDictionaryByURL:Op:KeyAndValue:successBlocker:failureBlockers:](DTAFNRequestGet *self, SEL a2, id a3, id a4, id a5, id a6, id a7)
{
    // 略去无关代码
    v7 = self;
    v8 = a4;
    v9 = objc_retain(a3, a2);
    v11 = objc_retain(v8, v10);
    v13 = objc_retain(a5, v12);
    v15 = objc_retain(a6, v14);
    v17 = objc_retain(a7, v16);
    v18 = -[DTAFNetRequest AddNecessaryParamDic:](v7, "AddNecessaryParamDic:", v13);
    objc_release(v13);
    v19 = objc_retainAutoreleasedReturnValue(v18);
    v20 = v19;
    v21 = +[SingalMethod signByHMac:AndHttpMethod:](
          &OBJC_CLASS___SingalMethod,
          "signByHMac:AndHttpMethod:",
          v19,
          CFSTR("GET"));
}

可见传递到签名方法的Hmac为v19,而v19最初是来自a5参数,通过分析方法头可知a5对应的是KeyAndValue,正是参数字典,a5通过添加必要参数后传递到签名方法,从这里也可以分析出签名方法的输入只有参数列表和请求方式,对于GET请求请求方式为字符串GET,因此我们只要实现出签名方法的逻辑,即可构造参数列表来构建合法请求。

接下来我们继续分析签名方法,继续上面的内容,我们看第一个if语句内的一小段内容。

  v66 = objc_msgSend(v59, "countByEnumeratingWithState:objects:count:", &v78, &v83, 16);
  v62 = v5;
  if ( v66 )
  {
    v11 = &stru_6B8088;
    v60 = *(_DWORD *)v79;
    do
    {
      v14 = 0;
      do
      {
        v69 = (char *)v11;
        if ( *(_DWORD *)v79 != v60 )
          objc_enumerationMutation(v59);
        v15 = *(_DWORD *)(HIDWORD(v78) + 4 * v14);
        v16 = objc_msgSend(v71, "enCodeURL:", *(_DWORD *)(HIDWORD(v78) + 4 * v14));
        v68 = objc_retainAutoreleasedReturnValue(v16);
        v17 = objc_msgSend(v5, "objectForKeyedSubscript:", v15);
        v19 = objc_retainAutoreleasedReturnValue(v17);
        if ( v19 )
        {
          v20 = objc_msgSend(v5, "objectForKeyedSubscript:", v15);
          v21 = (const __CFString *)objc_retainAutoreleasedReturnValue(v20);
        }
        else
        {
          objc_retain(&stru_6B8088, v18);
          v21 = &stru_6B8088;
        }
        objc_release(v19);
        v22 = objc_msgSend(v71, "enCodeURL:", v21);
        v23 = objc_retainAutoreleasedReturnValue(v22);
        if ( objc_msgSend(v69, "isEqualToString:", &stru_6B8088) )
          v24 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@%@=%@"), v69, v68, v23);
        else
          v24 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@&%@=%@"), v69, v68, v23);
        v25 = objc_retainAutoreleasedReturnValue(v24);
        objc_release(v69);
        v11 = (const __CFString *)v25;
        objc_release(v23);
        objc_release(v21);
        objc_release(v68);
        ++v14;
        v5 = v62;
      }
      while ( v14 < (unsigned int)v66 );
      v66 = objc_msgSend(v59, "countByEnumeratingWithState:objects:count:", &v78, &v83, 16);
    }
    while ( v66 );
  }

注意这里出现了如下的组合。

  v66 = objc_msgSend(v59, "countByEnumeratingWithState:objects:count:", &v78, &v83, 16);
  if ( v66 )
  {
    // ...
    do
    {
      // ...
      do
      {
        if ( *(_DWORD *)v79 != v60 )
          objc_enumerationMutation(v59);
        else
        {
          objc_retain(&stru_6B8088, v18);
          v21 = &stru_6B8088;
        }
        ++v14;
        v5 = v62;
      }
      while ( v14 < (unsigned int)v66 );
      v66 = objc_msgSend(v59, "countByEnumeratingWithState:objects:count:", &v78, &v83, 16);
    }
    while ( v66 );
  }

这里包含了countByEnumeratingWithState:::方法以及两层while语句,和while内部的objc_enumerationMutation调用,需要注意的是这一段是Objective-C的for-in语句实现的源码,我们在分析时可以忽略这部分框架内容,关心内层while的逻辑,下面我们着手分析第一个for-in语句的内容,由于内容较多,这次将讲解放在了代码的注释上,请读者阅读内层while循环体中的代码注释。

  v66 = objc_msgSend(v59, "countByEnumeratingWithState:objects:count:", &v78, &v83, 16);
  // v5为传入的参数字典
  v62 = v5;
  if ( v66 )
  {
    v11 = &stru_6B8088;
    v60 = *(_DWORD *)v79;
    do
    {
      v14 = 0;
      do
      {
        v69 = (char *)v11;
        if ( *(_DWORD *)v79 != v60 )
          objc_enumerationMutation(v59);
        // 通过计算偏移量得到数组中的一个元素,v14为迭代器,可以理解为for中的i,v15为当前取出的元素
        v15 = *(_DWORD *)(HIDWORD(v78) + 4 * v14);
        // 这里enCodeURL:的参数和v15是等效的,因此可以理解为将v15进行URL编码
        v16 = objc_msgSend(v71, "enCodeURL:", *(_DWORD *)(HIDWORD(v78) + 4 * v14));
        v68 = objc_retainAutoreleasedReturnValue(v16);
        // 以v15为key从字典v5中取出值,下面的if是对值的判空操作
        v17 = objc_msgSend(v5, "objectForKeyedSubscript:", v15);
        v19 = objc_retainAutoreleasedReturnValue(v17);
        if ( v19 )
        {
          v20 = objc_msgSend(v5, "objectForKeyedSubscript:", v15);
          v21 = (const __CFString *)objc_retainAutoreleasedReturnValue(v20);
        }
        else
        {
          objc_retain(&stru_6B8088, v18);
          v21 = &stru_6B8088;
        }
        objc_release(v19);
        // 分析可知v21就是key v15对应的参数值,这里对参数值也进行了编码
        v22 = objc_msgSend(v71, "enCodeURL:", v21);
        v23 = objc_retainAutoreleasedReturnValue(v22);
        // 这里将参数转化为字符串,规则为"key1=value1&key2=value2&...",可见是url编码的形式
        if ( objc_msgSend(v69, "isEqualToString:", &stru_6B8088) )
          v24 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@%@=%@"), v69, v68, v23);
        else
          v24 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@&%@=%@"), v69, v68, v23);
        v25 = objc_retainAutoreleasedReturnValue(v24);
        objc_release(v69);
        v11 = (const __CFString *)v25;
        objc_release(v23);
        objc_release(v21);
        objc_release(v68);
        ++v14;
        v5 = v62;
      }
      while ( v14 < (unsigned int)v66 );
      v66 = objc_msgSend(v59, "countByEnumeratingWithState:objects:count:", &v78, &v83, 16);
    }
    while ( v66 );
  }

根据上面的分析可知,第一个for-in通过遍历字典,生成了请求参数字典对应的URL参数字符串,并对其进行了编码,最终得到的参数字符串存储在v11变量中,下面的逻辑就十分简单了。

  v26 = objc_msgSend(v71, "enCodeURL:", v11);
  v27 = objc_retainAutoreleasedReturnValue(v26);
  objc_release(v11);
  v28 = objc_msgSend(v71, "enCodeURL:", CFSTR("/"));
  v29 = objc_retainAutoreleasedReturnValue(v28);
  v30 = v29;
  v31 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%@&%@&%@"), v58, v29, v27);
  v32 = objc_retainAutoreleasedReturnValue(v31);
  objc_release(v27);
  v33 = objc_msgSend(v71, "hmac:withKey:", v32, CFSTR("******2fadf46f3b28b6adddd24******"));
  v61 = objc_retainAutoreleasedReturnValue(v33);
  v76 = CFSTR("signature");
  v77 = v61;
  v34 = objc_msgSend(&OBJC_CLASS___NSDictionary, "dictionaryWithObjects:forKeys:count:", &v77, &v76, 1);
  v35 = objc_retainAutoreleasedReturnValue(v34);
  v36 = objc_msgSend(&OBJC_CLASS___NSMutableDictionary, "dictionaryWithDictionary:", v5);
  v37 = (char *)objc_retainAutoreleasedReturnValue(v36);

可见这里对参数字符串又进行了一次URL编码,然后又构建了一个三部分用&连接的字符串:第一部分v58通过向上分析可以得到是签名方法的第二个参数,即请求方式,这里传入的是GET;第二部分v29为/,第三部分v27即编码后的参数字符串,因此这里构建了一个形如GET&/&k1=v1&ke=v2...的字符串,随后调用了hmac:withKey:方法进行签名,这里的第一个参数hmac即为我们构建的字符串,第二个参数为密钥,为了信息安全这里对密钥进行了部分隐藏。

通过hmac加密后,得到的结果v33即为最终的签名结果被添加到请求参数中,分析到这里我们已经知道,只需要根据网络请求数据构建参数列表,再计算出签名添加到列表中即可构建出合法请求。目前我们离成功只有一步之遥,那就是分析hmac:withKey:方法,幸运的是,这个方法非常简单,调用了系统的HMAC函数加密后又进行了base64编码然后得到签名,这个方法的内部实现如下,这部分逻辑非常简单,这里不再赘述,读者可以自行阅读。

id __cdecl +[SingalMethod hmac:withKey:](SingalMethod_meta *self, SEL a2, id a3, id a4)
{
  id v4; // r4
  void *v5; // r10
  int v6; // r1
  void *v7; // r5
  void *v8; // r0
  int v9; // r1
  void *v10; // r0
  void *v11; // ST14_4
  int v12; // r5
  void *v13; // r0
  int v14; // r4
  void *v15; // r0
  int v16; // ST0C_4
  void *v17; // r11
  void *v18; // ST10_4
  void *v19; // ST08_4
  void *v20; // r4
  void *v21; // r8
  void *v22; // r6
  void *v23; // r0
  void *v24; // r5
  void *v25; // r0
  void *v26; // r0
  DTAFNRequestGet *v27; // r6
  void *v28; // r0
  SEL v29; // r1
  id v30; // r2
  id v31; // r3
  struct objc_object *v33; // [sp+38h] [bp+8h]
  struct objc_object *v34; // [sp+3Ch] [bp+Ch]
  struct objc_object *v35; // [sp+40h] [bp+10h]
  struct objc_object *v36; // [sp+44h] [bp+14h]
  struct objc_object *v37; // [sp+48h] [bp+18h]

  v4 = a4;
  v5 = (void *)objc_retain(a3, a2);
  v7 = (void *)objc_retain(v4, v6);
  v8 = objc_msgSend(&OBJC_CLASS___NSString, "class");
  if ( objc_msgSend(v7, "isKindOfClass:", v8) )
  {
    v10 = objc_msgSend(v7, "dataUsingEncoding:", 4);
    v11 = v7;
    v12 = objc_retainAutoreleasedReturnValue(v10);
    v13 = objc_msgSend(v5, "dataUsingEncoding:", 4);
    v14 = objc_retainAutoreleasedReturnValue(v13);
    v15 = objc_msgSend(&OBJC_CLASS___NSMutableData, "dataWithLength:", 32);
    v16 = objc_retainAutoreleasedReturnValue(v15);
    v17 = (void *)objc_retainAutorelease(v12);
    v18 = objc_msgSend(v17, "bytes");
    v19 = objc_msgSend(v17, "length");
    v20 = (void *)objc_retainAutorelease(v14);
    v21 = objc_msgSend(v20, "bytes");
    v22 = objc_msgSend(v20, "length");
    v23 = (void *)objc_retainAutorelease(v16);
    v24 = v23;
    v25 = objc_msgSend(v23, "mutableBytes");
    CCHmac(2, v18, v19, v21, v22, v25);
    v26 = objc_msgSend(v24, "base64EncodedStringWithOptions:", 0);
    v27 = (DTAFNRequestGet *)objc_retainAutoreleasedReturnValue(v26);
    v28 = v24;
    v7 = v11;
    objc_release(v28);
    objc_release(v20);
    objc_release(v17);
  }
  else
  {
    v27 = (DTAFNRequestGet *)objc_retain(v7, v9);
  }
  objc_release(v7);
  objc_release(v5);
  return j__objc_autoreleaseReturnValue(v27, v29, v30, v31, v33, v34, v35, v36, v37);
}

现在我们已经完整的分析出了签名过程,其完整流程如下图所示。

接口签名过程

到目前为止,我们只剩下URL编码的方法enCodeURL:没有分析,这部分逻辑也十分简单,只是简单地字符替换和添加,具体代码如下。

id __cdecl +[SingalMethod enCodeURL:](SingalMethod_meta *self, SEL a2, id a3)
{
  int v3; // r8
  SingalMethod_meta *v4; // r5
  void *v5; // r8
  void *v6; // r0
  int v7; // r1
  void *v8; // r0
  void *v9; // r0
  void *v10; // r6
  void *v11; // r0
  void *v12; // r4
  void *v13; // r0
  void *v14; // r6
  void *v15; // r0
  DTAFNRequestGet *v16; // r5
  SEL v17; // r1
  id v18; // r2
  id v19; // r3
  int v21; // [sp+0h] [bp-10h]
  struct objc_object *v22; // [sp+18h] [bp+8h]
  struct objc_object *v23; // [sp+1Ch] [bp+Ch]
  struct objc_object *v24; // [sp+20h] [bp+10h]
  struct objc_object *v25; // [sp+24h] [bp+14h]
  struct objc_object *v26; // [sp+28h] [bp+18h]

  v21 = v3;
  v4 = self;
  v5 = (void *)objc_retain(a3, a2);
  v6 = objc_msgSend(&OBJC_CLASS___NSString, "class");
  if ( objc_msgSend(v5, "isKindOfClass:", v6) )
  {
    v8 = objc_msgSend(v4, "encodeParameter:", v5);
    v9 = (void *)objc_retainAutoreleasedReturnValue(v8);
    v10 = v9;
    v11 = objc_msgSend(v9, "stringByReplacingOccurrencesOfString:withString:", CFSTR("+"), CFSTR("%20"), v21);
    v12 = (void *)objc_retainAutoreleasedReturnValue(v11);
    objc_release(v10);
    v13 = objc_msgSend(v12, "stringByReplacingOccurrencesOfString:withString:", CFSTR("*"), CFSTR("%2A"));
    v14 = (void *)objc_retainAutoreleasedReturnValue(v13);
    objc_release(v12);
    v15 = objc_msgSend(v14, "stringByReplacingOccurrencesOfString:withString:", CFSTR("%7E"), CFSTR("~"));
    v16 = (DTAFNRequestGet *)objc_retainAutoreleasedReturnValue(v15);
    objc_release(v14);
  }
  else
  {
    v16 = (DTAFNRequestGet *)objc_retain(v5, v7);
  }
  objc_release(v5);
  return j__objc_autoreleaseReturnValue(v16, v17, v18, v19, v22, v23, v24, v25, v26);
}

其中调用了encodeParameter:方法,该方法调用了系统方法来实现URL编码,而encodeURL:则是额外做了一些替换,encodeParameter:的实现如下。

id __cdecl +[SingalMethod encodeParameter:](SingalMethod_meta *self, SEL a2, id a3)
{
  void *v3; // r4
  void *v4; // r0
  int v5; // r1
  int v6; // r0
  int v7; // r1
  DTAFNRequestGet *v8; // r5
  SEL v9; // r1
  id v10; // r2
  id v11; // r3
  struct objc_object *v13; // [sp+14h] [bp+8h]
  struct objc_object *v14; // [sp+18h] [bp+Ch]
  struct objc_object *v15; // [sp+1Ch] [bp+10h]
  struct objc_object *v16; // [sp+20h] [bp+14h]
  struct objc_object *v17; // [sp+24h] [bp+18h]

  v3 = (void *)objc_retain(a3, a2);
  v4 = objc_msgSend(&OBJC_CLASS___NSString, "class");
  if ( objc_msgSend(v3, "isKindOfClass:", v4) )
  {
    v6 = CFURLCreateStringByAddingPercentEscapes(0, v3, 0, CFSTR("!*'();:@&=+$,/?%#[]"), 134217984);
    v8 = (DTAFNRequestGet *)objc_retain(v6, v7);
    CFRelease();
  }
  else
  {
    v8 = (DTAFNRequestGet *)objc_retain(v3, v5);
  }
  objc_release(v3);
  return j__objc_autoreleaseReturnValue(v8, v9, v10, v11, v13, v14, v15, v16, v17);
}

这两个方法都比较简单,这里不过多的分析,而是给出其转化出的Objective-C函数的代码,供读者参考。

NSString * encodeParameter(NSString *input) {
    CFStringRef output = CFURLCreateStringByAddingPercentEscapes(0, (__bridge_retained CFStringRef)input, 0, CFSTR("!*'();:@&=+$,/?%#[]"), 134217984);
    return CFBridgingRelease(output);
}

NSString * encodeURL(NSString *input) {
    input = encodeParameter(input);
    input = [input stringByReplacingOccurrencesOfString:@"+" withString:@"%20"];
    input = [input stringByReplacingOccurrencesOfString:@"*" withString:@"%2A"];
    input = [input stringByReplacingOccurrencesOfString:@"%7E" withString:@"~"];
    return input;
}

下面我们就可以动手写代码来实现这一套逻辑了,首先我们可以根据抓包分析的结果构建参数列表,其中敏感信息进行了隐去。

NSDictionary * addParams(NSDictionary *dict) {
    NSMutableDictionary *mut = dict.mutableCopy;
    mut[@"uuid"] = @"0617444F-1F6A-48D2-92B1-*******";
    mut[@"stopId"] = @"*******";
    mut[@"access_id"] = @"****";
    NSDate *date = [NSDate date];
    long long ts = [date timeIntervalSince1970];
    // 根据数据包可知需要的为毫秒时间戳
    mut[@"timestamp"] = [NSString stringWithFormat:@"%lld", ts * 1000];
    mut[@"platform"] = @"iOS";
    mut[@"deviceId"] = @"7033b6eee4efd3c3b949952dc49cb52f7798c37b014b61a10bd******";
    mut[@"appSource"] = @"******";
    mut[@"token"] = @"";
    return mut.copy;
}

接下来我们就可以实现一个完整的GET请求方法了。

+ (void)GET:(NSString *)baseURL params:(NSDictionary *)params success:(RequestGetSucceedBlock)success failure:(RequestGetFailureBlock)failure {
    // 从入参基础上添加必要参数,对应于逆向中的AddNecessaryParamDic:方法
    params = addParams(params);
    NSMutableArray *allKeys = params.allKeys.mutableCopy;
    // key升序排列
    [allKeys sortUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        NSString *key1 = obj1;
        NSString *key2 = obj2;
        return [key1 compare:key2];
    }];
    // 构造url的参数字符串
    NSString *output = @"";
    NSString *url = @"";
    for (NSString *key in allKeys) {
        NSString *value = params[key];
        if ([output isEqualToString:@""]) {
            output = [NSString stringWithFormat:@"%@", value];
            url = [NSString stringWithFormat:@"%@=%@", key, value];
        } else {
            output = [NSString stringWithFormat:@"%@,%@", output, value];
            url = [NSString stringWithFormat:@"%@&%@=%@", url, key, value];
        }
    }
    // 构造hmac算法的输入字符串
    NSString *v58 = @"GET";
    NSString *v29 = encodeURL(@"/");
    NSString *v27 = encodeURL(url);
    NSString *hmac = [NSString stringWithFormat:@"%@&%@&%@", v58, v29, v27];
    NSString *key = @"******2fadf46f3b28b6adddd24******"; // r5
    // 将字符串转为data,调用系统的Hmac函数CCHmac
    // r4-r0-r8
    NSData *hmacData = [hmac dataUsingEncoding:NSUTF8StringEncoding];
    // r5=r11
    NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding];
    unsigned char macOutBytes[CC_SHA256_DIGEST_LENGTH];
    CCHmac(kCCHmacAlgSHA256, [keyData bytes], [keyData length], [hmacData bytes], [hmacData length], macOutBytes);
    NSData *macOutData = [NSData dataWithBytes:macOutBytes length:CC_SHA256_DIGEST_LENGTH];
    // 对加密结果进行base64处理,使其变为可打印字符
    NSString *res = [macOutData base64EncodedStringWithOptions:0];
    res = encodeURL(res);
    // 构造最终的URL
    url = [url stringByAppendingFormat:@"&signature=%@", res];
    url = [NSString stringWithFormat:@"%@?%@", baseURL, url];
    // 发起请求
    [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:url] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 响应处理
    }] resume];
}

总结

反编译出的代码常常是十分复杂的,如果单纯从代码角度去分析将会异常困难,在分析时,可以结合情景和工程经验,例如在本文的分析中,利用先前的经验我们知道,一般的请求方法都会包含URL和参数列表,而签名的对象一般就是参数列表的内容,签名函数主要是MD5、Hmac和SHA等算法,因此在分析时就会有目的的对号入座,所以积累工程经验对于逆向工程十分必要。

声明

这一次Hacking过程以及本文的撰写均是处于学习和研究目的,绝无恶意,且关键信息均以隐去,不会对厂商造成威胁,且笔者未提供具体的二进制内容,希望广大读者仅仅抱着学习和研究的心态去阅读本文,希望这次过程记录能够帮助到走在逆向工程路上的你们。