對某實時公交App的接口簽名Hack過程

背景

筆者常常從家門口乘坐某公交到某地去玩耍,然而那一趟公交發車週期長且時間不定,每次出行前都須要打開實時公交的軟件搜索班次和信息不只浪費流量,並且很是不便。所以筆者便打算基於實時公交接口開發一個-1屏的Today Widget來顯示這趟公交的實時信息。html

分析

網絡數據包分析

首先筆者抓取了App的網絡數據包,並對請求數據進行了簡要分析,請求數據的內容以下,處於信息安全考慮,敏感信息均以打碼。前端

網絡數據接口

請求接口中包含了簽名和時間戳字段,時間戳字段有效的防止了請求的連續重放,簽名字段則保護了接口數據完整性,以黑盒角度分析,在簽名算法未知的狀況下僅能經過重放當前請求的形式來獲取數據,一旦時間戳與服務器時間相差超過閾值,服務端就會拋出系統時間不許確的錯誤,以防止這種形式的攻擊。因爲簽名的存在,接下來僅剩下兩條路能夠走,一個是在原來App的基礎上進行二次開發,經過動態庫注入的形式來添加本身想要的能力,另外一個是經過反編譯獲取簽名算法。算法

二次開發方式分析

對iOS應用進行二次開發的原理是將應用的可執行文件進行修改,在Mach-O文件的Load Commands字段中加入動態庫加載指令,使其可以加載新添加的動態庫,而後破解者在動態庫中以切面的形式對原來的App邏輯進行修改,最後對應用和動態庫進行重簽名,這種方式有以下幾個缺點:json

  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等關鍵詞來查找相關符號,在這個過程當中可能須要排除大量信息,經過搜索,筆者找到了兩處可疑的符號。app

經過名稱判斷,第二個方法傳遞的是一個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和參數列表,而簽名的對象通常就是參數列表的內容,簽名函數主要是MD五、Hmac和SHA等算法,所以在分析時就會有目的的對號入座,因此積累工程經驗對於逆向工程十分必要。

聲明

這一次Hacking過程以及本文的撰寫均是處於學習和研究目的,絕無惡意,且關鍵信息均以隱去,不會對廠商形成威脅,且筆者未提供具體的二進制內容,但願廣大讀者僅僅抱着學習和研究的心態去閱讀本文,但願此次過程記錄可以幫助到走在逆向工程路上的大家。

相關文章
相關標籤/搜索