2015移動安全挑戰賽(阿里&看雪主辦)全程回顧

GoSSIP_SJTU · 2015/04/01 9:26java

Author: 上海交通大學密碼與計算機安全實驗室軟件安全小組GoSSIPpython

第一題


0x1 分析

APK界面

題目下載android

本次比賽的第一個題目是一個APK文件,安裝後,須要用戶輸入特定的密碼,輸入正確會顯示破解成功。該題目的APK文件沒有太多的保護,能夠直接使用各類分析工具(如jeb等)反編譯獲得Java代碼。git

得到正確註冊碼的代碼邏輯爲: 1. 從logo.png這張圖片的偏移89473處,讀取一個映射表,768字節編碼成UTF-8,即256箇中文表 2. 從偏移91265處讀取18個字節編碼的UTF-8(即6箇中文字符)爲最終比較的密碼。而後經過輸入的字符的轉換,轉換規則就是ASCII字符編碼,去比較是否和最終密碼相等。github

0x2 巧妙的解法

咱們在這裏提供一種很是愉快的解法,不須要複雜的工具和分析,你們能夠參見視頻算法

打開app後,咱們使用adb logcat並加上這個app獨有的 lil 標籤過濾日誌輸出,發現app輸出日誌中有table,pw以及enPassword。隨意輸入字符串如123456789,發現enPassword中有對應的中文輸出,根據輸出反饋,能夠知道有以下對應關係安全

  • 1 - 麼
  • 2 - 廣
  • 3 - 亡
  • 4 - 門
  • 5 - 義
  • 6 - 之
  • 7 - 屍
  • 8 - 弓
  • 9 - 己

經過觀察Logcat輸出可知,最終目標pw應爲義弓麼丸廣之,根據上述table中的對應關係,咱們能夠獲得最終密碼爲:app

581026
複製代碼

第二題


0x1 分析

APK界面

題目下載框架

本次比賽的第二個題目仍然是一個獨立的APK文件,安裝後,須要用戶輸入特定的密碼,輸入正確會顯示成功。第二題APK在Java層代碼中並無關鍵邏輯,將用戶輸入直接傳給native so層中securityCheck這個native method(securityCheck方法在libcrackme.so中),由native code來決定返回正確與否。函數

用IDA工具打開libcrackme.so,首先看下程序的大體流程,能夠看到在securityCheck這個方法調用前,在init_array段和JNI_Onload函數里程序都作了些處理,而在securityCheck方法的最後有一個判斷,將用戶輸入和wojiushidaan作比較。嘗試直接輸入wojiushidaan,發現密碼錯誤,所以能夠猜想前面一大段邏輯的做用就是會把這個最終的字符串改掉。此時的思路是隻需知道最終判斷時候這個wojiushidaan地址上的變換後的值就好了。嘗試使用IDA調試發現一旦attach上去,整個程序就退出,想必必定是在以前的代碼中有反調試的代碼。

0x2 巧妙的解法

同上一題同樣,咱們提供一種很是巧妙的解法:

注意到在最終比較以前,程序使用了android_log_print函數,當咱們直接運行程序時,發現這裏固定輸出了

I/yaotong ( XXX): SecurityCheck Started...
複製代碼

這時候咱們想,是否能夠直接patch這個libcrackme.so,修改打印的內容,直接利用這個函數幫咱們輸出此時真正須要比較的值。

咱們選擇patch的方法是直接把這個log函數往下移,由於在0x12A4地址處正好有咱們須要的打印的數據地址賦值給了R2寄存器(原本是爲了給後面作比較用的),所以將代碼段從0x1284到0x129C的地方都用NOP改寫,在0x12AC的地方調用log函數,同時爲了避免影響R1的值,把0x12A0處的R1改爲R3。

下面是對比patch前和patch後的圖:

Patch前代碼

Patch後代碼

參考視頻給出了完整的解決過程:

經過觀察Logcat輸出可知,最終密碼爲:

aiyou,bucuoo
複製代碼

第三題


題目下載

在介紹本次比賽第三道題目以前,首先要介紹一個咱們GoSSIP小組開發的基於Dalvik VM的插樁分析框架InDroid,其設計思想是直接修改AOSP上的Dalvik VM解釋器,在解釋器解釋執行Dalvik字節碼時,插入監控的代碼,這樣就能夠獲取到全部程序運行於Dalvik上的動態信息,如執行的指令、調用的方法信息、參數返回值、各類Java對象的數據等等。InDroid只須要修改AOSP的dalvik vm部分代碼,編譯以後,可直接將編譯生成的新libdvm.so刷入任何AOSP支持的真機設備上(目前咱們主要使用Nexus系列機型特別是Nexus4和Galaxy Nexus)。在本次比賽的第三題和第四題分析過程當中,咱們使用該工具進行分析,大大提升了分析效率。具體細節能夠參考咱們發表的CIT 2014論文DIAS: Automated Online Analysis for Android Applications

回到題目上,將第三題的APK進行反編譯後發現代碼使用了加殼保護,對付這類加殼的APK,最方便的方法就是使用InDroid來進行動態監控,由於靜態加密的DEX必定會在執行時在Dalvik上時解密執行,這樣咱們能夠直接在InDroid框架裏對解釋執行過程當中釋放出來的指令進行監控。在咱們本身使用的工具裏,咱們開發了一個動態讀取整個dex信息的接口,執行時去讀DexFile這個結構,而後對其進行解析(解析時直接複用了Android自帶的dexdump的代碼)。這樣,咱們的插樁工具在運行程序後,可以直接獲得程序的dex信息,同未經保護時使用dexdump後獲得的結果基本一致。雖然咱們獲得的信息是dalvik字節碼,沒有直接反編譯成Java代碼那麼友好,但因爲程序不大,關鍵邏輯很少,所以對咱們的分析效率影響並不大。

使用InDroid進行脫殼的演示視頻:

在獲得脫殼以後的dexdump結果後,咱們能夠對代碼進行靜態分析。咱們發現用戶的輸入會傳遞給繼承自Class timertask的Class b,被Class b的run方法處理。在run方法中,若是sendEmptyMessage方法被調用時的參數爲0,就會致使Class c的handleMessage這個方法中獲得的messagewhat值爲0,進而致使103除0跳入異常處理中,觸發成功的提示。

繼續分析這個run方法的邏輯,能夠知道用戶的輸入會被傳遞到Class e的a方法中,作個相似摩爾斯譯碼的過程(其譯碼與標準的摩爾斯電碼不太同樣),而後通過下面一系列大量的混淆用的無用處理和不可能相等的比較後,將譯碼後獲得的字符串送入到關鍵的判斷中去。這個判斷成功的條件比較複雜:對於譯碼後獲得的字符串的前兩個字節,要求使用hashcode方法的結果等於3618,而且這兩個字節相加等於168,纔會進入後面的比較。咱們窮搜索一下符合這類輸入的字符串:

#!java
for ( size_t i = 33; i < 127; ++i )
{
    for ( size_t j = 33; j < 127; ++j )
    {
        String x =String.valueOf((char)j)+String.valueOf((char)i);
        if (x.hashCode()==3618 && (i+j) == 168)
        {
            System.out.println(x);
            System.out.println(j+i);
        }
    }
}
複製代碼

輸出爲:

s5
168
複製代碼

也就是說只有s5知足hashcode爲3618,而相加等於168這個條件。

肯定前兩個字符後,後面還有四個字符須要同Class e和Class a的Annotation值比較。由於咱們作脫殼的時候直接使用了dexdump的代碼,而dexdump即便到最新版裏也沒法很好地處理Annotations:

// TODO: Annotations.
複製代碼

不過不要緊,咱們還有動態分析工具這一利器,由於最終目的是獲得getAnnotation方法的返回值,依然能夠用InDroid在Dalvik執行到getAnnotation方法時監控返回值,就能獲得Annotation的具體值。使用InDroid獲取具體信息的視頻以下:

最後可知,符合程序需求的字符串是

s57e1p
複製代碼

使用程序內部的對應表,對其進行逆變換,可以讓程序輸入成功提示的輸入應該是:

… _____ ____. . ..___ .__.
複製代碼

第四題


APK界面

題目下載

本次移動安全挑戰賽的第四題和第三題同樣,是一個包含了加殼dex的APK文件,咱們使用同解決上一題同樣的方法,用InDroid獲得原始dex文件的dexdump結果:

使用InDroid進行脫殼的演示視頻:

Dex總體處理過程和上一題也相似,使用handleMessage處理最後的判斷輸入成功與否,只有sendEmptyMessage(0)後,觸發除以0的異常才能成功。不過這一題將用戶輸入轉成byte後,傳給一個native的方法:ali$aM$j方法,另外參數還包括一個常數48和Handler。看樣子逆向native庫勢在必行了。這一題的lib文件夾下文件和上一題是同樣的,有三個文件,其中libmobisecy.so實際上是個zip文件,解壓後是個classes.dex,直接反彙編後,類和方法的名字都在,只是裏面的代碼都是

throw new RuntimeException();
複製代碼

libmobisecz.so直接就是一堆binary數據,猜想應該是運行時會被解密,經過某種方式映射到爲真正的代碼執行。所以咱們的目標就是libmobisec.so這個ELF文件。

直接用IDA打開libmobisec.so,發現IDA會崩潰。用readelf發現正常的節區頭數據都被破壞了,所以應該這個so自己也被加過殼了,不少數據只有在動態運行時纔會解開,因此直接使用動態的方法,先運行這個程序後,直接在內存中把這個so dump出來。

首先須要在輸入框中隨便輸入些數據後,點擊肯定,保證用戶輸入數據執行到native方法裏後再作dump。咱們使用的方法是查看maps後,使用dd命令把整個so都dump出來。

輸入命令:

[email protected]:/ # ps | grep crackme.a4
u0_a73    1935  126   512204 48276 ffffffff 400dc408 S crackme.a4
[email protected]:/ # cat /proc/1935/maps
5e0f2000-5e283000 r-xp 00000000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e283000-5e466000 r-xp 00000000 00:00 0 
5e466000-5e467000 rwxp 00000000 00:00 0 
5e467000-5e479000 rw-p 00000000 00:00 0
5e479000-5e490000 r-xp 00191000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e490000-5e491000 rwxp 001a8000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e491000-5e492000 rw-p 001a9000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e492000-5e493000 rwxp 001aa000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e493000-5e4c1000 rw-p 001ab000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
複製代碼

使用dd命令將libmobisec.so的內存dump出來

[email protected]:/ # dd if=/proc/1935/mem of=/sdcard/alimsc4 skip=1578049536 ibs=1 count=3993600
複製代碼

dd命令使用的數字都是十進制的,skip就是libmobisec.so的起始地址,count是總長度。 爲了讓IDA還可以識別libmobisec裏的libc函數,咱們還須要把libc也載入到IDA中,libc就直接從system/lib裏拖出來就好了。

adb pull /system/lib/libc.so ./
複製代碼

用IDA先打開libc,調整好是在內存中的偏移即rebase program,再在load additional binary裏載入dd出來的libmobisec.so,經過maps裏的偏移後載入。接下來的任務就是在其中找到M\$j這個函數的地址。

一開始嘗試直接在dd出來的ELF文件中找這個M\$j這個函數名,相似的名字會被處理成

Java_ali_00024a_M_00024j
複製代碼

相似下圖:

圖1

不過我沒找到這個M\$j這個名字,逆過JNI庫的都知道,若是符號表裏找不到這個函數名,說明在JNI_Onload的時候,使用RegisterNatives函數從新將一個JNI函數映射爲Native函數了。

正當束手無策的時候,我再次想起了InDroid系統。在Dalvik中,每一個方法都是一個Method的結構體,其中當這個方法是native的時候,Method的insns這個指針會指向native方法的起始地址。所以咱們修改了下InDroid,讓Dalvik在執行M\$j這個方法前,去打印了M\$j方法的insns指針。這時咱們獲得了一個指向另外一片內存區域的值,既不在libdvm中,也不在libmobisec中,而且這片內存頁被映射成了rwx,由此推斷裏面也極有多是代碼,咱們繼而又dd出了這塊內存,用IDA打開,使用ARM平臺反彙編,發現該處就一條指令,是LOAD PC到另外一個地址,而這個地址剛好在libmobisec中。因而咱們直接到IDA中跳到這個地址,發現正好是個壓棧指令,印證了咱們的想法,此處就是M$j函數,因而在在IDA裏該地址指令處,右擊選擇create function,讓IDA識別這一段彙編指令爲函數指令後,就能夠經過F5查看看反編譯的C代碼了。

這個函數自己作了一些控制流混淆,同時還有不少字符串加解密的功能函數,一些簡單的如異或操做,也被展開成與和或的組合等更長更復雜的表達式形式。另外還看到一些變形過的RC4,等等。不過由於咱們已是dump出來執行過的數據,因此必要的數據都已經解密了。以下圖:

圖2

經過查看反編譯的C代碼,我發現程序中是直接經過JNI方法調用了Java中的bh類的方法a(在圖2常量中也能夠看到)。 再次回到dex層查看a方法,該方法是不斷的將輸入傳遞給不一樣的函數進行處理,先是cda方法,cCa方法,pa方法,xa方法,ali$aM$d方法(native),aSa方法,xa方法,ali$aM&z方法(native),cda方法,cCa方法,每個方法都是些簡單的數學運算,編碼,以及密碼學處理等可逆的操做,結合逆向和Indroid對輸入輸出的監控,均可以輕鬆肯定每一個Java函數的做用,具體過程以下代碼顯示:

invoke-static {}, LbKn;.a:()Z // [email protected]
move-result v3
invoke-static {v3}, LbKn;.b:(I)V // [email protected]
add-int/lit8 v0, v5, #int 1 // #01
invoke-static {v4, v5}, Lcd;.a:([BI)[B // [email protected]
move-result-object v1
add-int/lit8 v2, v0, #int 1 // #01
invoke-static {v1, v0}, LcC;.a:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v1, v2, #int -1 // #ff
invoke-static {v0, v2}, Lp;.a:([BI)[B // [email protected]
move-result-object v0
invoke-static {v0, v1}, Lx;.a:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v2, v1, #int -1 // #ff
invoke-static {v0, v1}, Lali$a;.M$d:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v1, v2, #int 1 // #01
invoke-static {v0, v2}, LaS;.a:([BI)[B // [email protected]
move-result-object v0
invoke-static {v0, v1}, Lx;.a:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v2, v1, #int 1 // #01
invoke-static {v0, v1}, Lali$a;.M$z:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v1, v2, #int 1 // #01
invoke-static {v0, v2}, Lcd;.a:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v2, v1, #int 1 // #01
invoke-static {v0, v1}, LcC;.a:([BI)[B // [email protected]
move-result-object v0
return-object v0
複製代碼

值得注意的是,其中有兩個native的方法,由於InDroid還能夠監控調用native方法的參數以及返回值,咱們發現這幾個native都沒有對輸入作複雜的處理,只有M\$d對輸入的第四個字節作了減8的處理。

作了這些逆變換之後咱們其實並無找到最終比較的處理,不過在解密過的數據中(圖2),不只有以前須要調用的各類方法和類,還能夠發現有個十分可疑的Base64的字符串。

aJTCZnf6NyBPYJfbrBuLu0wOhRFbPtvqpYjiby5J81M=
複製代碼

而且在native的M\$z方法的反彙編代碼中,能夠看到有對這個Base64字符串的長度比較,因爲咱們並無找到真正的比較函數,所以獲得這個字符串後,咱們直接從M\$z開始向上逆推以前的變換就獲得了的答案。

具體解密代碼以下:

#!python
#!/usr/bin/env python
# encoding: utf-8

from Crypto.Cipher import AES

def Lcda(s):
    return ''.join(map(lambda x: chr((ord(x) + 3) & 0xff), s))
def de_Lcda(s):
    return ''.join(map(lambda x: chr((ord(x) - 3) & 0xff), s))

def LcCa(s, a):
    return ''.join([chr(((ord(s[i]) ^ a) + i) & 0xff) for i in xrange(len(s))])
def de_LcCa(s, a):
    return ''.join([chr(((ord(s[i]) - i) & 0xff) ^ a) for i in xrange(len(s))])

def Lpa(s):
    return s[1:] + s[0]
def de_Lpa(s):
    return s[-1] + s[:-1]

def Lxa(s):
    return s.encode("base64")[:-1]
def de_Lxa(s):
    return s.decode("base64")

def LaliaMd(s):
    return s[:3] + chr((ord(s[3]) - 8) & 0xff) + s[4:]
def de_LaliaMd(s):
    return s[:3] + chr((ord(s[3]) + 8) & 0xff) + s[4:]

def LaSa(s):
    BS = 16
    pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
    cc = AES.new("qqVJwt11yyLm7hVK1iI2aw==".decode("base64"), AES.MODE_ECB)
    cipher = cc.encrypt(pad(s))
    return cipher
def de_LaSa(s):
    cc = AES.new("qqVJwt11yyLm7hVK1iI2aw==".decode("base64"), AES.MODE_ECB)
    cipher = cc.decrypt(s)
    return cipher

res = "aJTCZnf6NyBPYJfbrBuLu0wOhRFbPtvqpYjiby5J81M="

flag = de_Lcda(de_LcCa(de_Lpa(de_Lxa(de_LaliaMd(de_LaSa(de_Lxa(res))))), 49))
print flag
複製代碼

結果爲:

alilaba2345ba
複製代碼

這裏還須要提一下如何尋找M\$dM\$z兩個函數在so庫中的地址的方法,不過這個方法是一些經驗的總結,緣由是整個native ELF文件的節區結構是被修改過的。這兩個方法和M\$j不太同樣,由於在dump出的libmobisec裏能夠找到M\$z的函數名,證實這個方法沒有使用RegiterNatives來作變換,所以咱們能夠經過符號表來找這個函數與文件頭部的偏移。方法是找M\$z和字符串表的偏移,如0x03FE,而後窮搜整個文件:

圖3

由於符號表應該會把字符串表偏移做爲一項,這塊區域的結構體,咱們對照ELF結構發現並非標準的符號表,但仍是能夠大概看出結構體的內容,包括索引,字符串表偏移,以及ELF特殊的標誌數,所以推測0x57BE4偏移是M\$z函數。該地址也正好是個壓棧的指令,證實了咱們的猜測。

第五題


enter image description here

題目下載

2015年移動安全挑戰賽的最後一道題目,在規定的比賽時間內,僅有來自咱們GoSSIP的wbyang一名選手解決了這道問題,今天咱們就來揭開這一道最高難度題目的神祕面紗。

先把名爲AliCrackme_5.apk的文件丟到JEB裏看一看:

enter image description here

dex文件並無進行加殼和混淆,看上去是一個很是簡單的程序,Java代碼部分使用函數Register("Bomb_Atlantis", input)對輸入進行判斷。因此須要分析的邏輯應該都在libcrackme.so裏的Register函數中。

接下來咱們用IDA打開這個libcrackme.so,不出所料的發現IDA徹底無法處理,應該是進行了強烈的混淆和加殼處理:

enter image description here

使用和解決前面題目相同的技巧,咱們繼續使用dd的方法來去處一部分的混淆和加殼。運行一次程序後,從/proc/self/maps裏找到libcrackme.so在內存中的位置,使用dd命令從/proc/self/mem中提取出內存中的libcrackme.so,接着使用在解決第四題時使用過的技巧,將libcrackme.solibc.so一塊兒加載到IDA裏。

用IDA打開dump出的代碼後,咱們發現仍然有大部分的代碼沒法被IDA識別,須要手動定位到須要分析的代碼而後手工定義(IDA快捷鍵C)代碼,同時因爲代碼會在THUMB指令集和ARM指令集之間切換,有時候須要用快捷鍵ALT+G來將T寄存器設置爲不一樣的值,設置正確後才能正確翻譯出代碼。這裏咱們首先遇到的問題是沒法定位Register函數,一樣使用第四題中的技巧,用InDroid監控到Register函數的真實地址,就能夠在該地址上開始分析。

libcrackme.so這個動態庫裏使用的一些混淆方法,對於處理了前面一些相似混淆後如今的咱們來講已經不是問題(^_^)。經過分析代碼,咱們定位了幾個函數,這些函數的偏移在不一樣的設備上應該是不一樣的。總體的邏輯其實並不複雜,首先會有一個固定的字符串「Bomb_Atlantis」和一個固定的salt去進行一次md5運算,salt是動態生成的,不過因爲dump內存的時候這些動態的值已經生成好了,因此可以直接發現這個salt(出於一些版權緣由咱們不便公佈本題目的一些內部細節,所以該salt值請你們本身分析)

以後程序會將這個md5值和咱們的輸入進行一些異或和計算的操做,通過幾步比較簡單、可逆的變換以後,進入一個比較複雜的函數,通過這個函數處理後直接和一個內存中的值進行比較,返回比較結果。

enter image description here

這裏說一個咱們在作第五題時用到的分析方法——動態hook。因爲libcrackme.so中並無對調用自身的上層應用進行驗證,這就致使了咱們能夠本身寫一個程序去加載這個so,調用其中的方法。這也致使了咱們在加載libcrackme.so後,能夠加載另外一個用於hook的so,這樣咱們能夠hook libcrackme.so中的任意函數,從而知道任意函數的參數和返回值,這對於咱們理解程序有着很是大的幫助。這裏咱們使用的hook框架是著名Android安全研究人員Collin Mulliner開發的Android平臺上的一個二進制注入框架adbi。固然這道題目並不可以經過注入的方法將咱們的so注入進去,由於源程序禁掉了ptrace這樣的系統調用。咱們對adbi稍做修改,使之成爲一個能夠手動加載的動態hook框架。同時因爲咱們無法經過符號表來定位函數的地址,全部的hook地址都須要硬編碼,而且要和運行這道題目程序的Android設備內存映射嚴格對應。

須要指出的是adbi中存在一個小bug,hook.c這個文件的118行應該是

h->jumpt[0]=0x60
複製代碼

而不是0x30,對應的thumb彙編應該是

push{r5,r6}
複製代碼

而不是

push{r4,r5},
複製代碼

這個小bug在解題過程當中會形成一些影響。使用adbi來hook這道題目的函數還須要注意一點,這題的代碼中有一些函數使用的THUMB指令集,hook這些函數時,不要忘記人工的對hook地址+1。

經過hook的方法,咱們已經可以動態的分析libcrackme.so,首先咱們驗證了咱們對以前幾步變換的分析結果。以後就是分析最後一個複雜的處理函數,經過靜態分析+動態調試,咱們發現這是一個相似於白盒密碼學的加密函數。咱們的輸入進入函數後,首先通過幾步相似DES的預處理,以後會進行若干輪的查表,經過查詢一個巨大的表將咱們的輸入進行某種加密,生成一段密文,再通過幾回簡單的處理後和最後內存中的一段常量(出於一些版權緣由咱們不便公佈本題目的一些內部細節,所以該常量請你們本身分析)進行比較。

經過動態調試,咱們可以計算出加密算法最後應該輸出的值,可是因爲這個加密算法的密鑰融入了整個置換表中,要找出一個逆置換表顯然不太可能。咱們簡單過濾了libcrackme.so的其餘函數,也沒有發現用於解密的函數,想要正常解密密文是不太現實了。不過根據對加密算法的分析,咱們發現這若干輪的置換是相互獨立的,而且每一輪的複雜度並不高,這就意味這咱們能夠在能夠接受的時間內對算法進行爆破。咱們一開始的想法是code reuse,直接在Android設備上爆破,可是發現速度太慢,最後只能用笨辦法,經過hook從內存中dump出來置換表,用C代碼重寫了這個算法,有驚無險地在比賽結束前半小時搜索出結果。根據逆推算法推出正確輸入是:

3EFoAdTxepVcVtGgdVDB6AA=   
複製代碼

好了,咱們的2015移動安全挑戰賽全系列回顧就到此爲止了!但願你們能和咱們多多交流討論,歡迎你們關注咱們的微博GoSSIP_SJTU,基本上天天都會有精彩的內容發佈哦。

相關文章
相關標籤/搜索