記一次抓包和破解App介面

後廠村海盜發表於2020-07-25

第一章 · 起源

某日,想做個爬蟲工具,爬某個網站上的資料已做實驗之用。大家都知道爬pc網頁上的資料有幾個常見的問題:首先是資料不規範需要自己解析html,第二現在很多網站是前端動態渲染的,直接爬取的html可能就是個靜態頁面什麼也沒有,還需要執行js才能生成最終的頁面。因此就考慮,能否用它App的介面去爬資料,因為一般App呼叫的介面返回的都是json格式,解析起來比較方便。

第二章 · 嘗試

既然要抓取App的介面,肯定需要抓包,現在介面一般都用的https通訊,直接抓二進位制的tcp包是無法解密的。想到用Fiddler,它的原理很簡單,就是啟動一個https代理伺服器,手機上設定Fidder啟動的代理服務ip和埠。這是第一步,第二步就是證照,Fiddler會自己生成一個證照,把這個證照作為根證照匯入的手機中,這時手機對Fiddler返回的證照就是可信任的。通訊的時候,Fiddler作為客戶端它會和服務端建立https連線,得到解密後的資料,然後自己啟動一個https的服務,把資料返回給手機端。手機端這個時候實際訪問的是Fillder啟動的https服務,驗證的證照也是Fillder自己頒發的。至於https如何驗證簽名如何加密通訊的這裡就不做展開了,有興趣的可以自己查閱。

但是,必須要說的是這樣配置代理用Fiddler抓包的確可以抓到用手機瀏覽器訪問的內容,現在App為了防止代理模式抓https的包,一般都會把證照放到自己的App中,驗證https server返回的證照和App中的證照是否一致,這個時候server返回的證照是Fiddler自己頒發的,肯定和App本地的不一樣所以App這時是無法訪問介面的。

為什麼Fiddler要自己頒發一個證照呢?這是由於Fiddler需要把和服務端通訊的內容解密,所以它作為一個正常的client端先和服務端建立連線,驗證服務端的證照,用服務端的證照上的公鑰加密自己生成的對稱祕鑰,服務端拿到對稱祕鑰後用私鑰解密,雙方都知道對稱祕鑰,之後的通訊用對稱祕鑰加解密。同理Fiddler作為App的代理伺服器,也是要和app建立https連線的,這時如果直接返回服務端上的證照,App用服務端上證照的公鑰加密App生成的對稱祕鑰。但是Fiddler沒有證照的私鑰,所以Fiddler需要自己頒發一個證照,有公私鑰,才能和App正常的用https通訊。

嘗試用Fiddler代理抓App的包失敗了

第三章 · 脫獄

遇到了問題,肯定要解決問題。現在問題的關鍵就是App驗證本地證照和Fiddler返回的證照不一致,要做的就是繞開這驗證,或者讓App驗證這個證照永遠通過。不得不說,道高一尺魔高一丈,還真有這樣的東西,根據維基百科的解釋Xposed框架(Xposed framework)是一套開放原始碼的、在Android高許可權模式下執行的框架服務,可以在不修改APK檔案的情況下修改程式的執行(修改系統),基於它可以製作出許多功能強大的模組,且在功能不衝突的情況下同時運作。
安裝Xposed是有風險的,弄不好手機會變成磚,這裡我就用模擬器替代手機安裝了,下載MuMu模擬器,安裝Xposed框架

同時還需要安裝一個Xposed的一個外掛Just Trust Me

安裝好之後,我們再開啟這個App,同時配置模擬器網路的代理,再用Fiddler抓包可以看到內容

第四章 · 柳暗花明

看到上面的截圖,的確是找打了請求的地址,但是發現發出的請求內容是

{
  "pagesize": 10,
  "page": 1,
  "platid": "0",
  "langid": "0",
  "typeid": "0",
  "saletime": "全部",
  "time": 1595672934792,
  "sign": "c2522179dd87522f0d2997e28d48289e"
}

沒錯,有一個簽名,這個簽名一看就知道是md5做的,但是我不知道規則,而且生成的簽名肯定是這些請求的引數加上一個字串,最後md5一下,服務端還要驗證簽名,才能正常返回結果。這時彷彿又回到了原點,不解決這個簽名,我們是無法正常請求服務的。要解決這個簽名唯一的辦法就是反編譯包,從程式碼中發現蛛絲馬跡。
apktool是一個開源的用於反編譯apk包的工具,用它試試看能不能找到對應的md5加密的原始碼。用它反編譯apk後,程式碼非常龐大,在裡面搜尋介面的名稱djscreen竟然找到程式碼了

    invoke-static {}, Ljava/lang/System;->currentTimeMillis()J
    move-result-wide v8
    .line 493
    .local v8, "time":J
    new-instance v10, Ljava/lang/StringBuilder;
    invoke-direct {v10}, Ljava/lang/StringBuilder;-><init>()V
    const-string v11, "10"
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v10
    iget v11, p0, Lcom/ws3dm/app/activity/GameCategoryActivity;->mPage:I
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
    move-result-object v10
    iget-object v11, p0, Lcom/ws3dm/app/activity/GameCategoryActivity;->platid:Ljava/lang/String;
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v10
    iget-object v11, p0, Lcom/ws3dm/app/activity/GameCategoryActivity;->langid:Ljava/lang/String;
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v10
    iget-object v11, p0, Lcom/ws3dm/app/activity/GameCategoryActivity;->typeid:Ljava/lang/String;
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v10
    iget-object v11, p0, Lcom/ws3dm/app/activity/GameCategoryActivity;->saletime:Ljava/lang/String;
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v10
    invoke-virtual {v10, v8, v9}, Ljava/lang/StringBuilder;->append(J)Ljava/lang/StringBuilder;
    move-result-object v10
    invoke-virtual {v10}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
    move-result-object v7
    .line 494
    .local v7, "validate":Ljava/lang/String;
    invoke-static {v7}, Lcom/ws3dm/app/util/StringUtil;->MD5(Ljava/lang/String;)Ljava/lang/String;

根據程式碼可以看到MD5的規則就是10+mPage+platid+langid+typeid+saletime+time然後送達工具com.ws3dm.app.util.StringUtil執行MD5方法,進入這個類看看怎麼MD5的

    .method public static MD5(Ljava/lang/String;)Ljava/lang/String;
    .locals 9
    .param p0, "string"    # Ljava/lang/String;
    .prologue
    .line 77
    const-string v4, "e8S8Ho0N25z78u6qn4kHyN"
    .line 78
    .local v4, "key":Ljava/lang/String;
    new-instance v5, Ljava/lang/StringBuilder;
    invoke-direct {v5}, Ljava/lang/StringBuilder;-><init>()V
    invoke-virtual {v5, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v5
    invoke-virtual {v5, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v5
    invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
    move-result-object p0

驚喜的發現傳入的字串+e8S8Ho0N25z78u6qn4kHyN然後再MD5就是結果
試試101000全部1595672934792e8S8Ho0N25z78u6qn4kHyN正好是c2522179dd87522f0d2997e28d48289e和抓包中的引數一致,搞定,到此基本上是破解結束了。後續還發現基本上所有的介面都是採取這種模式進行md5然後請求的。

第五章 · 終結

至此整個的抓包加破解的流程就結束了,整個過程其實是做二件事情,解決https驗證證照,找到md5簽名的規則。整個App相對而且是比較好破解的,整個MD5的過程都是Java寫的,比較好反編譯。這也是和後端通訊,常見的一種處理方式。但是這種方式,的確不是很安全,沒有很好的保護Api不被外部破解,比較安全的做法是把簽名方法放到so中,或者對資料進行加密解密,同時也不要忘記混淆程式碼。

相關文章