JS 逆向百例】猿人學系列 web 比賽第五題:js 混淆 - 亂碼增強,詳細剖析

語言: CN / TW / HK

逆向目標

逆向過程

抓包分析

進入網頁,點選右鍵檢視頁面原始碼,搜尋不到直播間相關資料資訊,證明是通過 ajax 載入的資料,ajax 載入有特殊的請求型別 XHR,開啟開發者人員工具,重新整理網頁進行抓包,在 Network 的篩選欄中選擇 XHR,資料介面為 5?m=XXX&f=XXX,在響應預覽中可以看到各直播間熱度資料:

介面 url 有兩個請求引數 m 和 f,現在還不知道具體怎麼來的:

本題提示 cookie 有效期僅為 50 秒鐘,即 cookie 值是在動態變化的,經過對比分析,cookie 中有兩個動態變化的引數 m 和 RM4hZBv0dDon443M,接下來需要定位到其生成的位置:

逆向分析

Cookie 加密引數分析

可以通過 Hook Cookie 的方式定位引數位置,這裡通過 Fiddler 程式設計貓外掛進行 Hook,相關外掛在 K哥爬蟲公眾號傳送【Fiddler外掛】即可獲取,Hook 程式碼如下:

(function () {
  'use strict';
  var cookieTemp = '';
  Object.defineProperty(document, 'cookie', {
    set: function (val) {
      if (val.indexOf('RM4hZBv0dDon443M') != -1) {
        debugger;
      }
      console.log('Hook捕獲到cookie的值->', val);
      cookieTemp = val;
      return val;
    },
    get: function () {
      return cookieTemp;
    },
  });
})();

將以上程式碼寫入外掛中,注入 Hook:

清除網頁快取,勾選開啟框,開啟 Fiddler 進行 Hook 注入,可以發現成功斷住:

從右側堆疊中向上跟棧,會發現跟到了虛擬機器 VMXXX 中,點選右下角 { } 格式化,跳轉到了第 978 行,程式碼部分如下:

_0x3d0f3f[_$Fe] = 'R' + 'M' + '4' + 'h' + 'Z' + 'B' + 'v' + '0' + 'd' + 'D' + 'o' + 'n' + '4' + '4' + '3' + 'M=' + _0x4e96b4['_$ss'] + ';\x20path=/';

在該行打下斷點進行除錯,控制檯列印相關引數:

  • _$Fe:cookie
  • _ 0x4e96b4['_$ss']:RM4hZBv0dDon443M 引數加密後的值

前面各字母組成起來就是 RM4hZBv0dDon443M=,此處就是 RM4hZBv0dDon443M 引數加密後賦值給 cookie 的位置,所以關鍵的加密部分為 _0x4e96b4['_$ss'],列印相關內容會發現 _0x4e96b4 是 window 物件,window. _$ss 即加密後的值:

直接搜尋 _$ss 沒有結果,同樣嘗試 Hook,Hook 程式碼:

(function () {
  'use strict'
  Object.defineProperty(window, '_$ss', {
    set: function (val) {
      console.log('Hook捕獲到_$ss的值->', val);
      debugger;
    },
    });
})();

成功斷住:

同樣向上跟棧,找到其定義位置,跟到了虛擬機器中,格式化後跳到第 1229 行:

_0x4e96b4['_$' + _$UH[0x348][0x1] + _$UH[0x353][0x1]] = _0x29dd83[_$UH[0x1f]]();

在該行打下斷點除錯分析各自含義:

  • '_$s'_$UH[0x348][0x1] _$UH[0x353][0x1] 組合起來:'_$ss'
  • _$UH[0x1f]():toString()
  • _0x29dd83[ _$UH[0x1f]]():將 _0x29dd83 生成的值轉換為字串

因此關鍵的加密位置肯定在 _0x29dd83 中,往上看, _0x29dd83 定義在第 1225 行,這時候眼前一亮,看到了 mode 和 padding 兩個關鍵字,這裡大概率為 AES 或者 DES 加密,將程式碼解混淆替換後的結果如下:

_$Ww = _$Tk['enc']['utf-8']['parse'](_0x4e96b4['_$pr']['toString']()),
_0x29dd83 = _$Tk['AES'](_$Ww, _0x4e96b4['_$qF'], {
    'mode': _$Tk['mode']['ECB'],
    'padding': _$Tk['pad']['pkcs7']
}),
_0x4e96b4['_$ss'] = _0x29dd83['toString']();

現在就很明顯了,這裡為 AES 加密,加密內容為 _$Ww,key 值為 _0x4e96b4['_$qF'],加密模組為 ECB,填充方式為 pkcs7:

  • CBC:Cipher Block Chaining(密碼塊連結模式),是一種迴圈模式,前一個分組的密文和當前分組的明文異或操作後再加密,這樣做的目的是增強破解難度
  • PKCS7:在填充時首先獲取需要填充的位元組長度 = 塊長度 - (資料長度 % 塊長度), 在填充位元組序列中所有位元組填充為需要填充的位元組長度值

_$Ww 的值由 _0x4e96b4['_$pr'] 轉換為字串後經過 utf-8 編碼得到,其與 key 值 _0x4e96b4['_$qF'] 都是陣列,需要知道這兩個陣列是怎麼生成的,先 ctrl + f 搜尋 _0x4e96b4['_$qF'],定義在第 1444 行,內容如下:

_0x4e96b4['_$qF'] = CryptoJS['enc']['Utf8'][_$UH[0xff]](_0x4e96b4['btoa'](_0x4e96b4['_$is'])['slice'](0x0, 0x10));

在該行打下斷點,控制檯列印分析一下:

由此可見,_0x4e96b4['_$qF'] 是通過 CryptoJS 庫將字串經過 base64 加密後取前 16 位的結果,搜尋 _0x4e96b4['_$is'],找到字串生成的位置,在第 674 行,由 _$yw 賦值,在上一行可以看到熟悉的 _$Fe,即 cookie,發現 cookie 中的 m 引數是在這裡定義的:

_0x3d0f3f[_$Fe] = 'm=' + _0x474032(_$yw) + ';\x20path=/';

引數 m 的值也與 _$yw 有關,m 引數是將 _$yw 經過 _0x474032 函式處理後得到,後面再專門進行分析,_$yw 定義在第 672 行:

_$yw = _0x2d5f5b()[_$UH[0x1f]]();

_$UH[0x1f] 為 “toString”,_$yw 的值是將 _0x2d5f5b() 函式的返回值轉換成了字串得到的,跟進到該函式定義的位置,搜尋後發現在第 279 行,控制檯列印後發現這裡就是時間戳,所以 _$yw 即時間戳:

因此 _0x4e96b4['_$qF']的值是將時間戳經過 base64 加密後取了前 16 位的結果,接下來只需要知道 _0x4e96b4['_$pr']是如何生成的,就能復現出 RM4hZBv0dDon443M 引數的加密過程,在第 1224 行打斷點除錯發現此時的 _0x4e96b4['_$pr'] 陣列包含五個值:

現在就需要知道這五個值是在哪傳進去的,搜尋 _0x4e96b4['_$pr'] 看看哪裡對其進行了賦值,每個都打下斷下,該陣列定義在第 270 行:

_0x4e96b4['_$pr'] = new _0x4d2d2c();

_0x4d2d2c 在第 224 行定義為 Array,所以這裡是建立了一個數組 _0x4e96b4['_$pr'],接著往後找傳值的地方,繼續執行斷點除錯,第 1717 行的斷點運行了四次傳入了四個值:

_0x4e96b4['_$pr']['push'](_0x474032(_$Wa));

跟進 _$Wa 定義的位置,在第 1715 行,由 _0x12eaf3 函式生成,跟進到這個函式的位置,在第 275 行,返回值解混淆後如下:

Date['parse'](new Date());

再次下一步除錯斷點會跳轉到第 868 行,這時候陣列被傳入了第五個值,_$yw 為時間戳,由於 m = _0x474032(_$yw),所以第五個值也就是引數 m 的值,記住這裡出現的 _0x4e96b4['_$is']

_0x3d0f3f[_$Fe] = 'm=' + _0x474032(_$yw) + ';\x20path=/';
_0x4e96b4['_$is'] = _$yw;
_0x4e96b4['_$pr']['push'](_0x474032(_$yw));

陣列值的生成位置都找到了,跟 m 引數一樣,傳入的值都經過了 _0x474032 函式的處理,因此需要跟進 _0x474032 函式,滑鼠選中,點選即可跳轉到該函式定義的位置:

在第 455 行,返回值為三目表示式:

function _0x474032(_0x233f82, _0xe2ed33, _0x3229f9) {
        return _0xe2ed33 ? _0x3229f9 ? v(_0xe2ed33, _0x233f82) : y(_0xe2ed33, _0x233f82) : _0x3229f9 ? _0x41873d(_0x233f82) : _0x37614a(_0x233f82);
}

在 return 處打下斷點除錯,_0x233f82 為傳入的 _$yw 的值,即時間戳,後面兩個引數均為 undefined,所以不妨將函式簡化下:

function _0x474032(_0x233f82, _0xe2ed33, _0x3229f9) {
    return _0x37614a(_0x233f82);
}

接下來需要跟進到 _0x37614a 函式的位置:

function _0x37614a(_0x32e7c1) {
        return _0x499969(_0x41873d(_0x32e7c1));
}

這裡就需要跟出 _0x499969 函式和 _0x41873d 函式的內容,接下來就是扣,缺啥補啥,缺函式補函式,缺環境補環境,若報錯提示 _$UH is not defined_$UH 是個大陣列,直接將其整體解混淆替換掉就行了,例如:

_$UH[0x6c] ---> "length" 

或者寫成鍵值對形式:

_$UH = {
    8: 'prototype',
    15: 'charCodeAt',
    31: 'toString',
    108: 'length'
}

值得注意的是 _0x11a7a2 函式,執行時會報錯 op is not defined,op 定義在第 308 行:

op 的值為 26,這裡直接將其定義成固定值即可,即 var op = 26;

同樣將 _0x42fb36 和 b64pad 也寫成固定值,即 _0x42fb36 = 16;b64pad = 1;

除錯過程中還發現 window['_$6_']window['_$tT']window['_$Jy'] 這幾個引數的值是在動態變化的,不進行改寫甚至將相關部分註釋掉,在本地 node 環境中都是可以執行出結果的,但是用 python 呼叫的話會報錯,證明在前端會對這幾個引數進行校驗,這幾個引數在 _0x11a7a2 函式中定義,該函式溯源後最終被 _0x474032 函式呼叫,_0x474032 函式對 _$yw 的值進行處理,生成了 _0x4e96b4['_$pr'] 陣列的最後一個值及 m 引數的值,所以如果這幾個引數的值匹配錯誤的話會導致校驗失敗,我們只需要打斷點看 m 引數的值生成的時候,這三個引數的值是多少,然後寫成固定值就行了:

window['_$6_'] = -389564586;
window['_$tT'] = -660478335;
window['_$Jy'] = -405537848;

至此 Cookie 中 RM4hZBv0dDon443M 引數和 m 引數的生成邏輯就疏通了,以下通過 JavaScript 對其復現:

// 以下函式部分內容過長,此處省略
// 完整程式碼關注 GitHub:https://github.com/kgepachong/crawler

var CryptoJS = require('crypto-js');

function rm4Encrypt(_$yw, pr){
    var value = Buffer.from(_$yw).toString('base64').slice(0, 16);
    var srcs = CryptoJS.enc.Utf8.parse(pr);
    var key = CryptoJS.enc.Utf8.parse(value);
    var encrypted = CryptoJS.AES.encrypt(srcs, key, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    });
    return encrypted.toString();
}

var _$yw = new Date().valueOf().toString();
var _$Wa = Date.parse(new Date())
function pr(){
    pr = [];
    for (i = 1; i < 5; i++) {
        // _$Wa 傳入四個值
        pr.push(_0x474032(_$Wa))
    }
    // _$yw 傳入一個值
    pr.push(_0x474032(_$yw));
    return pr.toString();
}

var RM4hZBv0dDon443M = rm4Encrypt(_$yw, pr());
// m 為陣列傳入的最後一個值
var m = pr[4];
console.log('RM4hZBv0dDon443M 引數加密後的值為: ' + RM4hZBv0dDon443M)
console.log('m 引數的值為: ' + m)

執行結果:

請求頭引數分析

Cookie 中的引數分析完了,還有兩個請求引數 m 和 f 沒有解決,直接從介面處跟棧,從 Initiator 中跟到 request 裡:

點選右下角 { } 格式化後會跳轉到 5:formatted 檔案的第 856 行,在第 883 行的 list 中可以找到引數 m 和 f 的定義位置:

"m": window._$is,
"f": window.$_zw[23]

m 的值是 window._$is,有沒感覺似曾相識,就是上文所說的 _0x4e96b4['_$is']_0x4e96b4 就是 window,所以這裡 m 的值其實就是 _$yw ;f 的值是 window.$_zw[23] ,現在需要知道 $_zw[23] 的值怎麼生成的,區域性搜尋 $_zw 會發現該陣列定義在第 611 行,接著往後找,看看陣列中的第 23 個是什麼,先控制檯列印一下內容:

第 633 行內容是第六個,順下去找會發現第 23 個的內容如下:

$_aiding.$_zw.push($_t1);

在此處打下斷點除錯驗證一下,可以發現結果是一樣的:

接下來只需要找到 $_t1 的定義位置即可,ctrl + f 區域性搜尋 $_t1 ,其定義在第 613 行,是個時間戳:

let $_t1 = Date.parse(new Date());
  • Date.parse(new Date()):獲取的時間戳是把毫秒改成 000 顯示,如 1662691102000
  • new Date().valueOf():獲取了當前包括毫秒的時間戳,如 1662691114310

可以發現與 _$Wa 的定義方式一致,對比一下 m 和 f 兩個引數的值會發現差值接近於 50 秒,與題目中提示的 Cookie 有效期僅 50 秒鐘對應上了:

在虛擬機器檔案的第 1975 行也有個 50 秒的定時器:

至此所有引數生成的邏輯都調理清晰了,本題並不難,但是扣程式碼的過程中有許多需要注意的細節,猿人學給大家提供了一個優質的練習平臺,做題也是一個很好的自我提升的方式。

完整程式碼

bilibili 關注 K 哥爬蟲,小助理手把手視訊教學:https://space.bilibili.com/16...

GitHub 關注 K 哥爬蟲,持續分享爬蟲相關程式碼!歡迎 star !https://github.com/kgepachong/

以下只演示部分關鍵程式碼,不能直接執行!

JavaScript 程式碼

var _0x4e96b4 = window = {};
var _0x1171c8 = 0x67452301;
var _0x4dae05 = -0x10325477;
var _0x183a1d = -0x67452302;
var _0xcfa373 = 0x10325476;
var _0x30bc70 = String;

// 以下函式部分內容過長,此處省略
// 完整程式碼關注 GitHub:https://github.com/kgepachong/crawler

var CryptoJS = require('crypto-js');

function rm4Encrypt(_$yw, pr){
    var value = Buffer.from(_$yw).toString('base64').slice(0, 16);
    var _$Ww = CryptoJS.enc.Utf8.parse(pr);
    var key = CryptoJS.enc.Utf8.parse(value);
    var encrypted = CryptoJS.AES.encrypt(_$Ww, key, {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    });
    return encrypted.toString();
}

function getParamers() {
    pr = []; 
    for (i = 1; i < 5; i++) {
        var _$Wa = Date.parse(new Date());
        pr.push(_0x474032(_$Wa))
    }
    var _$yw = new Date().valueOf().toString(); 
    pr.push(_0x474032(_$yw));
    cookie_m = pr[4];
    cookie_rm4 = rm4Encrypt(_$yw, pr.toString());
    return{
        "cookie_m": cookie_m,
        "cookie_rm4": cookie_rm4,
        "m": _$yw,
        "f": Date.parse(new Date()).toString()
    }
}

console.log(getParamers());

Python 程式碼

# =======================
# --*-- coding: utf-8 --*--
# @Time    : 2022/9/8
# @Author  : 微信公眾號:K哥爬蟲
# @FileName: yrx5.py
# @Software: PyCharm
# =======================

import execjs
import requests
import re


def encrypt_yrx5():
    room_heat_all = []
    for page_num in range(1, 6):
        with open('yrx5.js', 'r', encoding='utf-8') as f:
            encrypt = f.read()
            encrypt_params = execjs.compile(encrypt).call('getParamers')
        headers = {
            "user-agent": "yuanrenxue,project",
        }
        cookies = {
            # 填入自己的 sessionid
            "sessionid": " your sessionid ",
            "m": encrypt_params['cookie_m'],
            "RM4hZBv0dDon443M": encrypt_params['cookie_rm4']
        }
        params = {
            "m": encrypt_params['m'],
            "f": encrypt_params['f']
        }
        url = "https://match.yuanrenxue.com/api/match/5?page=%s" % page_num
        response = requests.get(url, headers=headers, cookies=cookies, params=params)
        for i in range(10):
            value = response.json()['data'][i]
            room_heat = re.findall(r"'value': (.*?)}", str(value))[0]
            room_heat_all.append(room_heat)
    room_heat_all.sort(reverse=True)
    top_five_total = 0
    for i in range(5):
        top_five_total += int(room_heat_all[i])
    print(top_five_total)


if __name__ == '__main__':
    encrypt_yrx5()

本文來源:JS 逆向百例】猿人學系列 web 比賽第五題:js 混淆 - 亂碼增強,詳細剖析