天天看點

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

作者:K哥爬蟲
「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

聲明

本文章中所有内容僅供學習交流,抓包内容、敏感網址、資料接口均已做脫敏處理,嚴禁用于商業用途和非法用途,否則由此産生的一切後果均與作者無關,若有侵權,請聯系我立即删除!

逆向目标

  • 猿人學 - 反混淆刷題平台 Web 第二題:js 混淆,動态 cookie
  • 目标:提取全部 5 頁釋出日熱度的值,計算所有值的加和
  • 首頁:https://match.yuanrenxue.com/match/2
  • 接口:https://match.yuanrenxue.com/api/match/2
  • 逆向參數:
  1. Cookie 參數:m

逆向過程

抓包分析

進入網頁,點選右鍵檢視頁面源代碼,搜尋不到直播間相關資料資訊,證明是通過 ajax 加載的資料,ajax 加載有特殊的請求類型 XHR,打開開發者人員工具,重新整理網頁進行抓包,會跳轉到虛拟機中,進入無限 debugger,過無限 debugger 的方式在往期文章中有詳細介紹,感興趣的可以去閱讀學習一下,這裡直接在 debugger 行右鍵選擇 never pause here,然後下一步斷點即可過掉:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

在 Network 的篩選欄中選擇 XHR,資料接口為 2,在響應預覽中可以看到目前頁各手機釋出日的熱度:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

這時候點選第二頁,會彈出提示框:cookie 失效,正在重置頁面,證明 cookie 是有時效性的,并且會進行校驗:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

cookie 中有個關鍵加密參數 m,其内容如下:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

逆向分析

通過 hook cookie 中 m 參數的方式對其進行定位,hook 的方式有很多種,可以閱讀 K 哥往期文章,對其有詳細介紹,這裡使用程式設計貓 Fiddler 插件進行 hook,相關插件在 K哥爬蟲公衆号發送【Fiddler插件】即可擷取,Hook 代碼如下:

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

勾選開啟框,啟動 Fiddler 進行 hook 注入:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

重新整理網頁,如果進入無限 debugger,則按上述方式解決,不過直接通過 m 參數定位并不是最好的方案,因為該 cookie 中還有其他參數包含 m 字母,位置不對則重新整理網頁,這裡成功斷在 m 參數的值生成的位置:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

向上跟棧到 _0xdad69f (2:18) 處,然後點選左下角 { } 格式化代碼,會跳轉到 2:formatted 檔案的第 4943 行,該行内容如下:

document[$dbsm_0x42c3(qqLQOq, iOiqII) + $dbsm_0x42c3(q1IoqQ, QQlLlq)] = _0x5500bb['\x4e\x74\x44' + '\x72\x43'](_0x5500bb[$dbsm_0x42c3(qqqQoq, oqQiiO) + '\x6d\x65'](_0x5500bb[$dbsm_0x42c3(Ioo0ql, olq0Oq) + '\x6d\x65'](_0x5500bb[$dbsm_0x42c3(qOIqQi, OOqIQi) + '\x72\x44'](_0x5500bb[$dbsm_0x42c3(Q1qoqQ, lILOOq) + '\x72\x44'](_0x5500bb[$dbsm_0x42c3(qOO1Q0, oiqlQQ) + '\x72\x44'](Ql1OO0, _0x5500bb['\x7a\x76\x67' + '\x6c\x77'](_0x3c9ca8)), Qoqq0I), _0x5500bb[$dbsm_0x42c3(iqOiQ0, QOiq0Q) + '\x47\x6b'](_0x313b78, _0x160e3a)), lOo0QQ), _0x160e3a), _0x5500bb[$dbsm_0x42c3(qiOOiO, liQIoQ) + '\x4e\x5a']),           

控制台列印後可知這裡就是 cookie 中 m 參數值生成的位置:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

在控制台中進一步列印分析下其他部分含義:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

m 參數值的格式如下:

0ef478cf61e0749d7444c7997c917679|1663213224000           

可以依此将代碼進行簡化:

_0x5500bb[$dbsm_0x42c3(iqOiQ0, QOiq0Q) + '\x47\x6b'](_0x313b78,_0x160e3a) + lOo0QQ + _0x160e3a           

控制台列印驗證,結果比對:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

接下來先跟進到 _0x5500bb[$dbsm_0x42c3(iqOiQ0, QOiq0Q) + '\x47\x6b'] 中,滑鼠選中後點選進入:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

在該檔案的第 3911 行,内容如下:

_0x434ddb[$dbsm_0x42c3(Iooo0l, Qq1oqI) + '\x47\x6b'] = function(_0x105ffe, _0x733be0) {
            return _0x105ffe(_0x733be0);
        }           

傳回值為 _0x105ffe(_0x733be0),該函數傳入的參數為 _0x313b78 和 _0x160e3a,是以可以進一步改寫:

_0x313b78(_0x160e3a) + lOo0QQ + _0x160e3a           

_0x160e3a 為時間戳,是以 m 參數的值是将時間戳作為參數傳入 _0x313b78 函數後加密得到的,是以需要進一步跟進到 _0x313b78 函數定義的位置,同樣滑鼠選中,點選即可跳轉到第 4933 行,到 node 環境中調試,初步代碼為:

function _0x313b78(_0x575158, _0x1fa91a, _0x1cf5de) {
    // 以下部分内容過長,此處省略
    // 完整代碼關注 GitHub:https://github.com/kgepachong/crawler
}

var _0x160e3a = Date.parse(new Date());
var m = _0x313b78(_0x160e3a) + lOo0QQ + _0x160e3a;

console.log(m);           

運作後會提示 _0x5500bb 未定義,到原檔案中 ctrl + f 局部搜尋這個函數,在第 3940 行:

_0x5500bb = _0x434ddb           

補上運作後會提示 _0x434ddb 未定義,搜尋後發現 _0x434ddb 在第 2817 行定義為一個空對象,後面向其中傳入了很多值,類似于一個大數組,不能隻補 _0x434ddb = {};,需要把傳值部分補進去,不然後面運作時會出現些報錯,經測試有的部分不要也可以,但是細扣就很麻煩了,直接全補即可,這就很多了,從第 2817 行一直扣到第 3939 行,補完後接着運作程式,這次又提示 $dbsm_0x42c3 未定義,接着搜找其定義位置,在第 94 行,補了後提示 OooIi1 未定義,在第 209 行,需要從第 209 行到第 2816 行全部補上,不然會提示其中某一個未定義,同樣的,雖然經調試有的不需要也行,但是一個個調麻煩且沒有必要,補完後接着運作又會提示 $dbsm_0x123c 未定義:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

其在第 22 行,是個大數組,補了之後運作程式後發現卡住了,一段時間後程度報錯:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

這個報錯可能是記憶體資源耗盡導緻程式崩潰了,将這部分代碼複制到浏覽器中進行調試,開啟一個新頁面,打開開發者人員工具,在 Sources 中選擇 Snippets,建立一個腳本,将已經扣下來的代碼粘貼進去,在第一行寫入 debugger;手動打斷點調試,ctrl + s 儲存檔案後點選右下角按鈕運作腳本即會在第一行斷住:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

點選單步調試,一步步檢視是哪裡出了問題:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

點了幾步後,卡了一下,跳到第 2711 行,是個 for 循環,右側出現紅框報錯,意思是潛在的記憶體崩潰,即單步調試斷到到此處時程式臨近記憶體崩潰:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

接着往後單步調試,會發現一直在第 2712 行和第 2713 行間來回執行,到後來甚至浏覽器崩潰了,是以問題出在 WxzuQr 對象中出現了無限循環,直至耗盡了記憶體資源:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

這部分内容在 $dbsm_0x42c3 函數中,接下來需要研究一下崩潰原因,右側堆棧中向上跟棧,上兩步分别通過構造函數建立了兩個執行個體對象 WjJIeN 和 vnuqco,WjJIeN 部分如下:

_0x11a714['prototype']['WjJIeN'] = function(_0x4859ef) {
                if (!Boolean(~_0x4859ef)) {
                    return _0x4859ef;
                }
                return this['WxzuQr'](this['yewpLt']);
            }           

這裡進行了一個 if 判斷,~ 為按位取反,意思是如果 !Boolean(~_0x4859ef) 的值為 false,則執行 WxzuQr 的無限循環行為,直至程式崩潰,接着跟進到 vnuqco 部分,檢視 _0x4859ef 是啥,對什麼進行了判斷:

_0x11a714['prototype']['vnuqco'] = function() {
                _0x2940ac = new RegExp(this['PuKGlh'] + this['CTXIfT']),
                _0x3fba94 = _0x2940ac['test'](this['XxpyjG']['toString']()) ? --this['yHmSUE'][0x1] : --this['yHmSUE'][0x0];
                return this['WjJIeN'](_0x3fba94);
            }           

傳回值中給 WjJIeN 傳入的參數為 _0x3fba94,其定義在第 2699 行,是個三目表達式:

_0x2940ac['test'](this['XxpyjG']['toString']()) ? --this['yHmSUE'][0x1] : --this['yHmSUE'][0x0];           

到控制台列印輸出一下,看看該行各部分什麼含義:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

--this['yHmSUE'][0x1] 的值固定為 -1,而每運作一次 this['yHmSUE'][0x0] 的值即減一:

console.log(!Boolean(~-1)) // true
console.log(!Boolean(~-2)) // false           

是以隻有當 _0x2940ac['test'](this['XxpyjG']['toString']()) 的值為 true 時才不會進入無限循環,在控制台列印下 this['XxpyjG']['toString']() 部分内容:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

這個函數在第 2689 行,再來看看對其進行了怎樣的判斷,跟進到 _0x2940ac 定義位置,在第 2698 行,是個正規表達式對象,控制台中列印後可知道表達式為:

/\w+ *\(\) *{\w+ *['|"].+['|"];? *}/           
  • / ... /:正規表達式限制符,字面量寫法,防轉義,中間正規表達式部分若存在 \d 不會将 \d 的 \ 當作轉義字元
  • *\(\):比對零個或多個括号,\ 為轉義字元
  • *{ 、*}:比對前後大括号
  • \w+:比對一個或多個字母數字字元
  • .+:貪婪比對任意字元
  • ['|"]:比對單引号或者雙引号
  • ;?:比對零個或一個分号

是以比對樣式大緻如下:XXX( ){ XXX ' XXX ' ;},并不比對換行符、制表符、空格等,沒格式化的代碼會被壓縮成一行,是以這裡相當于格式化檢測,由于一開始進行了格式化操作,是以判斷結果為 false,進而進入了無限循環,導緻程式崩潰,是以隻需要将這部分内容壓縮為一行即可,檢驗一下:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

沒有格式化後列印出的結果為 true,即不會調用到 WxzuQr 對象,進而進入無限循環,修改後再次運作程式,結束了嗎,當然沒有,上個問題倒是解決了,又出現了以下報錯:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

報錯在第 3854 行,内容如下:

_0x5500bb[$dbsm_0x42c3(QoLq0i, q0Oqqo) + '\x5a\x49']           

接着在浏覽器中進行調試,在這一行上面打上 debugger;然後運作腳本,斷住後列印分析一下:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

'\x5a\x49' 即 ‘ZI’,QoLq0i、q0Oqqo 為定值,是以問題出在 $dbsm_0x42c3 函數中,其實如果對 OB 混淆了解的話會知道這種混淆方式有一些特征,其一般由三部分組成:大數組、移位自執行函數、解密字元串函數,大數組我們之前已經找到了,就是 $dbsm_0x123c,而 $dbsm_0x42c3 是解密字元串函數,這裡差了個移位自執行函數,缺東西自然結果會不對,需要找到将其補上,在第 23 行到第 93 行,夾在 $dbsm_0x123c 和 $dbsm_0x42c3 之間,補完後運作程式,又到了熟悉的卡住,過了一會後報錯:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

報錯在第 27 行,放到浏覽器中進行調試,還是在開頭打上 debugger;運作後單步向下執行,點了幾下熟悉的卡住,然後跳到第 24 行 for 循環處:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

右側出現熟悉的警告提示,證明又進入到無限循環了,果不其然,過了一會浏覽器頁面就崩潰了:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

根據之前的經驗,看看是不是哪又有個格式化檢測導緻進入到這個循環裡,果不其然,在第 55 行:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

這裡是對 removeCookie 處的代碼進行了格式化檢測,同樣将函數體部分寫成一行即可:

'removeCookie': function() {return 'dev';},           

接着運作,又提示 _0x3c9ca8 未定義,ctrl + f 局部搜尋找到函數定義位置扣下來即可,運作後又提示 _0x1316f4 未定義,這個扣下來之後記得将後面的自執行的括号删掉,接着會提示 _0x12a78e 未定義,扣下來的時候同樣記得删掉末尾的括号,再接着就沒什麼特别需要注意的了,差哪個函數補哪個就行了,到後面提示 navigator 未定義,簡單地補浏覽器環境即可,node 環境下 window 設定為 global:

var window = global; 
window.navigator = {};           

自然不會這麼輕易的結束了,運作後又會提示 _0x184fb0 未定義,跟之前一樣,搜到扣下來即可,後面就是漫長的補函數的過程,沒别的技巧,就是需要耐心,手都 cv 酸了,直到出現如下報錯:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

報錯提示 history 未定義,這是個浏覽器對象,顯示在 console.log 處報錯,在 console.log 行打斷點調試,運作到這裡時會跳轉到虛拟機中,其中代碼如下:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

history.pushState 是向浏覽器的會話曆史中添加記錄,當使用 console.log 輸出結果的時候,就會執行 history.pushState,但是我們并沒有 history 環境,是以會報錯,補了 history 環境後運作程式發現一直卡着,仔細看代碼才發現有個 while 循環,最離譜的是裡面的 for 循環設定了 1100000 次,幾乎可以說是在不間斷檢測,等不得等到猴年馬月去了,這裡直接将 console.log 指派給一個變量替換掉即可,記得放到前面:

var result = console.log;           

至此,終于結束了!成功列印出 m 參數的值:

「JS 逆向百例」猿人學 web 賽第二題:js 混淆 - 動态 cookie,詳細剖析

這個題倒是不難,逆向下來思路也很清晰,但是扣代碼的過程繁雜且坑不少,還是很值得大家上手去練習的。

完整代碼

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

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

以下隻示範部分關鍵代碼,不能直接運作!

JavaScript 代碼

var window = global; 
window.navigator = {};
var result = console.log;

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

function _0x313b78(_0x575158, _0x1fa91a, _0x1cf5de) {
    if (_0x5500bb[$dbsm_0x42c3(QoLq0i, q0Oqqo) + '\x5a\x49'](_0x5500bb[$dbsm_0x42c3(LQOI0Q, QqOI00) + '\x73\x42'], _0x5500bb[$dbsm_0x42c3(Q00oiq, QIioOo) + '\x5a\x76'])) {
        VWQQuv['\x6f\x4f\x61' + '\x68\x47'](debuggerProtection, Q0LiqQ);
    } else {
        _0x5500bb[$dbsm_0x42c3(i1lQqq, q110Lq) + '\x62\x45'](_0x3c9ca8);
        return _0x1fa91a ? _0x1cf5de ? _0x5500bb[$dbsm_0x42c3(iqqLQO, LoOOOq) + '\x4b\x6b'](_0x21cf21, _0x1fa91a, _0x575158) : _0x5500bb['\x72\x71\x75' + '\x4b\x51'](y, _0x1fa91a, _0x575158) : _0x1cf5de ? _0x5500bb[$dbsm_0x42c3(qLQQ1q, I1oOQ1) + '\x4d\x6e'](_0x443ca7, _0x575158) : _0x5500bb[$dbsm_0x42c3(qLLoQi, iO0OQo) + '\x4d\x6e'](_0x184fb0, _0x575158);
    }
}

function getCookieM(){
    var _0x160e3a = Date.parse(new Date());
    var m = _0x313b78(_0x160e3a) + lOo0QQ + _0x160e3a;
    return m;
}

// var _0x160e3a = Date.parse(new Date());
// var m = _0x313b78(_0x160e3a) + lOo0QQ + _0x160e3a;

// result(m);           

Python 代碼

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

import execjs
import requests
import re


def get_cookie_m():
    heat_total = 0
    for page_num in range(1, 6):
        with open('yrx2.js', 'r', encoding='utf-8') as f:
            encrypt = f.read()
            cookie_m = execjs.compile(encrypt).call('getCookie')
        headers = {
            "user-agent": "yuanrenxue,project",
        }
        cookies = {
            "sessionid": " 填入自己的 sessionid ",
            "m": cookie_m
        }
        url = "https://match.yuanrenxue.com/api/match/2?page=%s" % page_num
        response = requests.get(url, headers=headers, cookies=cookies)
        for i in range(10):
            value = response.json()['data'][i]
            heat = re.findall(r"'value': (.*?)}", str(value))[0]
            heat_total += int(heat)
    print(heat_total)


if __name__ == '__main__':
    get_cookie_m()