【小程式逆向專欄】某潤選房小程式逆向分析

K哥爬虫發表於2024-04-30

1

宣告

本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整程式碼,抓包內容、敏感網址、資料介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!

本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯絡作者立即刪除!

前言

近期在交流群中發現,有群友提到了小程式逆向相關的問題,之前也有不少粉絲私聊提問過相關問題,也對他們的疑問進行了解答, K哥一向會盡力滿足粉絲們的需求 ,為了避免群友手動採集之痛,本文將對某潤小程式進行逆向分析:

2

逆向目標

  • 目標:某潤選房小程式逆向
  • 地址:I+Wwj+eoi+W6jzovL+Wwj+a2pumAieaIvy9yQ1FKZGlJVzZmQ002WnM=

小程式除錯

關於微信小程式強啟開發者人員工具,網上已經有很多辦法,這裡主要講一種個人認為比較食用的方法,首先進入導航站(https://www.kgtools.cn/),找到 <小程式相關> 欄目:

3

然後選擇右面已經編譯好的 exe 進行下載,下載以後解壓到本地資料夾:

4

我們需要將本地的微信版本安裝到最新版(3.9.10.19_x64),根據測試,小程式版本 8555 比較穩定,別的版本可能會一直提示未找到匹配版本資訊的微信程序,透過以下連結下載微信客戶端,小程式版本為 8555:

https://weixin.qq.com/cgi-bin/readtemplate?lang=zh_CN&t=weixin_faq_list&head=true

解壓檔案,進入 cmd,輸入 WechatOpenDevTools.exe -all 進行注入,發現提示:

5

我們按照相關教程,進入快取頁面:

6

退出微信,將如下的倆個資料夾刪掉:

7

再次執行終端命令,注入成功:

8

最新版本的 WechatOpenDevTools.exe 會自動彈出微信,更為便利了。

抓包分析

首先,開啟開發者人員工具(DevTools):

9

選擇底部樓盤,發現有一個 /ssdp 介面,發現 url 中的 ssed 與協議頭中的 saleSignature 資料存在加密情況,如下:

10

同時響應資料中返回了相關查詢資訊,本文將對查詢介面做進一步分析:

11

逆向分析

ssdp 引數

ssdp 引數是請求每個介面都會攜帶的引數,我們猜測他包含了請求的型別,全域性搜尋 ssdp ,進入第二個 js 檔案d5c5.js

12

發現大概是有 12 處相關的地方:

13

我們依次在這幾個地方下斷,再次進入樓盤,上下滑動,成功斷了下來。透過觀察發現,它在一個 request 的地方斷了下來:

14

分析可知,他每次傳送 request 請求的時候會被攔截下來,然後自動新增 ssdp 引數,生成步驟如下:

var u = this
, _ = Date.now()
, S = this
, D = this.getTimeDate()
, f = e + "&Api_Version=1.0&App_ID=".concat(this.globalData.appid, "&App_Sub_ID=").concat(this.globalData.getSsdpApp_Sub_ID, "&App_Token=").concat(this.globalData.App_Token_code, "&App_Version=1.0&Divice_ID=").concat(wx.getStorageSync("user_flag"), "&Divice_Version=wxapp&OS_Version=8.0.6&Partner_ID=").concat(this.globalData.getSsdpPartner_ID);
"post" == n && (f += "&REQUEST_DATA=".concat(JSON.stringify(s), "&Time_Stamp=").concat(D, "&User_Token=&").concat(this.globalData.getSsdpApp_key)),
"post" != n && (f += "&Time_Stamp=".concat(D, "&User_Token=&").concat(this.globalData.getSsdpApp_key));
var A = c(f).toUpperCase()
, b = this.base64_encode(e + "&Api_Version=1.0&App_ID=".concat(this.globalData.appid, "&App_Sub_ID=").concat(this.globalData.getSsdpApp_Sub_ID, "&App_Token=").concat(this.globalData.App_Token_code, "&App_Version=1.0&Divice_ID=").concat(wx.getStorageSync("user_flag"), "&Divice_Version=wxapp&OS_Version=8.0.6&Partner_ID=").concat(this.globalData.getSsdpPartner_ID, "&Time_Stamp=").concat(D, "&User_Token=&Sign=").concat(A))

好了,一眼望去密密麻麻:

15

經過分析,傳入介面的請求引數,與時間、App_Sub_ID、App_Token、Divice_ID 等引數進行拼接。測試發現 Api_VersionApp_IDApp_Sub_ID 等引數可以固定,復現如下:

function md5Encrypt(data) {
    const hash = crypto.createHash('md5');
    hash.update(data);
    return hash.digest('hex');
}
function get_url() {
    Divice_ID = "這裡寫自己抓到的裝置號"
    //以下引數僅供參考,請替換各自實際引數
    e = "Api_ID=crland.isale.nsc.searchProjectList";
    D = getTimeDate();
    f = e + "&Api_Version=1.0&App_ID=".concat("wx948ef9858f04f6e9", "&App_Sub_ID=").concat("0005000502QF", "&App_Token=").concat("2af3061a-fa3d-4ac2-8456-56546a8daaa9", "&App_Version=1.0&Divice_ID=").concat(Divice_ID, "&Divice_Version=wxapp&OS_Version=8.0.6&Partner_ID=").concat("00050000");
    var A = md5Encrypt(f).toUpperCase()
    b = btoa(e + "&Api_Version=1.0&App_ID=".concat("wx948ef9858f04f6e9", "&App_Sub_ID=").concat("0005000502QF", "&App_Token=").concat("2af3061a-fa3d-4ac2-8456-56546a8daaa9", "&App_Version=1.0&Divice_ID=").concat(Divice_ID, "&Divice_Version=wxapp&OS_Version=8.0.6&Partner_ID=").concat("00050000", "&Time_Stamp=").concat(D, "&User_Token=&Sign=").concat(A))
    return b
}

輸出如下:

16

saleSignature 引數

同 ssdp 一樣,全域性搜尋 saleSignature 發現同樣是在 d5c5.js 中存在這個引數,定位到相關位置如下:

17

發現 k 是透過 var k = r.doEncrypt(T, p, 1) 得來,T 透過 T = i(I.toUpperCase()) 得來。進入的 i 函式發現是一個 md5 加密,所以 saleSignature 引數是由引數拼接然後轉為大寫,透過 md5 進行加密,然後傳入 doEncrypt 進行處理後得到。

跟進到 doEncrypt 中,發現他屬於一個匯出函式,如下:

18

那麼,這種我們應該怎麼辦呢?第一種辦法就是搜尋相關加密引數特徵值,看看它是否屬於什麼加密演算法,看看能不能引庫復現,第二種就是整個 js 拿下,補環境呼叫:

19

第三種就是扣演算法了,這裡我們選擇扣演算法,本文重點討論這型別的演算法,應該從何下手去扣。

將 js 全部複製,放到 nodepad++ 中,全部收起,好傢伙,8w 多行,你就扣吧,一扣一個不吱聲:

20

這裡我們搜尋 doEncrypt: function(t, e),定位該函式作用域,發現它屬於 webpack 打包的裡面:

21

不僅如此,我們還發現它處於最底層宣告瞭一個 utils/sm-crypto.js 模組,然後給其他地方呼叫這個加密模組,我們將整個宣告模組下的內容全部複製到一個檔案裡面,如下:

22

將定義部分刪除,將剩下的函式改為自執行函式:

23

然後將分發器匯出到全域性,如圖:

24

執行一下我們匯出的部分,輸出如下:(部分使用者可能會提示缺少模組 jsbn,手動 npm 安裝一下即可):

25

搜尋 doEncrypt 發現他處於第三個模組:

26

所以我們透過呼叫這個加密模組這個看看能不能呼叫成功,程式碼如下:

console.log(window.kk(3).doEncrypt("f5ef7eb5653944f8eef04891b195171b", "04a337dc634bddbbfbcae9d30470663fb5e221feab40239f1675a0b2d9d42e46413a0adfa4868c963aebb39d7ec89073885eccd011e0f96d5fe434be98734d9993", 1))

發現輸出以下結果:

27

害,看似成功了,其實並沒成功:

28

我們到瀏覽器看一下最終結果是怎麼樣的:

29

好了,我們遇到這種情況只能採用與瀏覽器聯調了,看看到底是哪部分與瀏覽器不一樣:

30

一步一步與瀏覽器進行聯調,看看哪一部分不同:

31

經過除錯發現,我們與瀏覽器不同的地方就在於,我們的 o 與 i 值是不同的,在瀏覽器中這倆個屬於 32 位陣列,我們這為 undefined。

那麼我們就去找一下這個模組中 o 與 i 是哪裡被賦值的,經過排錯,發現在 doPublicKey 中生成了 o 與 i ,如下:

32

看來它是透過傳入 t 值 用來生成一個金鑰對 i 與 o,全域性搜尋 doPublicKey,發現他在前面被呼叫,傳入了一個 p:

33

再往前發現 p 是一個公鑰 key:

34

發現此係列屬於非對稱加密,類似於 RSA,所以必須要初始化生成金鑰對才能進行下一步。

所以,我們復現如下:

// 公鑰
p = "04a337dc634bddbbfbcae9d30470663fb5e221feab40239f1675a0b2d9d42e46413a0adfa4868c963aebb39d7ec89073885eccd011e0f96d5fe434be98734d9993"
// 使用公鑰生成的臨時金鑰對
window.kk(3).doPublicKey(p)

35

當然最後的結果也與我們期待的結果一致!

至此 doEncrypt 演算法逆向完成,T 引數的生成與上面引數拼接生成的方法一致,這裡就不復述了。

結果驗證

36

相關文章