【JS 逆向百例】W店UA,OB反混淆,抓包替換CORS跨域錯誤分析

K哥爬蟲發表於2021-12-07
關注微信公眾號:K哥爬蟲,持續分享爬蟲進階、JS/安卓逆向等技術乾貨!

宣告

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

逆向目標

  • 目標:W 店登入介面 UA 引數加密,JS 程式碼經過了 OB 混淆
  • 主頁:aHR0cHM6Ly9kLndlaWRpYW4uY29tLw==
  • 介面:aHR0cHM6Ly9zc28xLndlaWRpYW4uY29tL3VzZXIvbG9naW4=
  • 逆向引數:Form Data:ua: H4sIAAAAAAAAA91ViZUbMQhtiVOIcnRRxRafr%2FGuN5ukgoyfLUZC...

OB 混淆簡介

OB 混淆全稱 Obfuscator,Obfuscator 其實就是混淆的意思,官網:https://obfuscator.io/ ,其作者是一位叫 Timofey Kachalov 的俄羅斯 JavaScript 開發工程師,早在 2016 年就釋出了第一個版本。

一段正常的程式碼如下:

function hi() {
  console.log("Hello World!");
}
hi();

經過 OB 混淆後的結果:

function _0x3f26() {
    var _0x2dad75 = ['5881925kTCKCP', 'Hello\x20World!', '600mDvfGa', '699564jYNxbu', '1083271cEvuvT', 'log', '18sKjcFY', '214857eMgFSU', '77856FUKcuE', '736425OzpdFI', '737172JqcGMg'];
    _0x3f26 = function () {
        return _0x2dad75;
    };
    return _0x3f26();
}

(function (_0x307c88, _0x4f8223) {
    var _0x32807d = _0x1fe9, _0x330c58 = _0x307c88();
    while (!![]) {
        try {
            var _0x5d6354 = parseInt(_0x32807d(0x6f)) / 0x1 + parseInt(_0x32807d(0x6e)) / 0x2 + parseInt(_0x32807d(0x70)) / 0x3 + -parseInt(_0x32807d(0x69)) / 0x4 + parseInt(_0x32807d(0x71)) / 0x5 + parseInt(_0x32807d(0x6c)) / 0x6 * (parseInt(_0x32807d(0x6a)) / 0x7) + -parseInt(_0x32807d(0x73)) / 0x8 * (parseInt(_0x32807d(0x6d)) / 0x9);
            if (_0x5d6354 === _0x4f8223) break; else _0x330c58['push'](_0x330c58['shift']());
        } catch (_0x3f18e4) {
            _0x330c58['push'](_0x330c58['shift']());
        }
    }
}(_0x3f26, 0xaa023));

function _0x1fe9(_0xa907e7, _0x410a46) {
    var _0x3f261f = _0x3f26();
    return _0x1fe9 = function (_0x1fe950, _0x5a08da) {
        _0x1fe950 = _0x1fe950 - 0x69;
        var _0x82a06 = _0x3f261f[_0x1fe950];
        return _0x82a06;
    }, _0x1fe9(_0xa907e7, _0x410a46);
}

function hi() {
    var _0x12a222 = _0x1fe9;
    console[_0x12a222(0x6b)](_0x12a222(0x72));
}

hi();

OB 混淆具有以下特徵:

1、一般由一個大陣列或者含有大陣列的函式、一個自執行函式、解密函式和加密後的函式四部分組成;

2、函式名和變數名通常以 _0x 或者 0x 開頭,後接 1~6 位數字或字母組合;

3、自執行函式,進行移位操作,有明顯的 push、shift 關鍵字;

例如在上面的例子中,_0x3f26() 方法就定義了一個大陣列,自執行函式裡有 push、shift 關鍵字,主要是對大陣列進行移位操作,_0x1fe9() 就是解密函式,hi() 就是加密後的函式。

抓包分析

點選登陸抓包,可以看到有個 ua 引數,經過了加密,每次登陸是會改變的,如下圖所示:

01.png

如果直接搜尋 ua 的話,結果太多,不方便篩選,通過 XHR 斷點比較容易找到加密的位置,如下圖所示,最後提交的 r 引數包含 ua 值,往上找可以看到是 i 的值經過了 URL 編碼,再往上看,i 的值通過 window.getUa() 獲取,這個實際上是 uad.js 裡面的一個匿名函式。

02.png

跟進到 uad.js,可以看到呼叫了 window[_0x4651('0x710')] 這個方法,最後返回的 _0x261229 就是加密後的 ua 值,用滑鼠把類似 _0x4651('0x710')_0x4651('0x440') 的值選中,可以看到實際上是一些字串,這些字串通過直接搜尋,可以發現是在頭部的一個大陣列裡,如下圖所示:

03.png

04.png

混淆還原與替換

一個大陣列,一個有明顯的 push、shift 關鍵字的進行移位操作的自執行函式,是 OB 混淆無疑了,那麼我們應該怎樣去處理,讓其看起來更順眼一些呢?

你可以手動在瀏覽器選中檢視值,在本地去替換,當然不用全部去替換,跟棧走,用到的地方替換就行了,不要傻傻的全部去挨個手動替換,這種方法適用於不太複雜的程式碼。

如果遇到程式碼很多的情況,建議使用反混淆工具去處理,這裡推薦國內的猿人學OB混淆專解工具和國外的 de4js,猿人學的工具還原程度很高,但是部分 OB 混淆還原後執行會報錯,實測本案例的 OB 混淆經過猿人學的工具處理後就不能正常執行,可能需要自己預先處理一下才行,de4js 這個工具是越南的一個作者開發的,開源的,你可以部署到自己的機器上,它支援多種混淆還原,包括 Eval、OB、JSFuck、AA、JJ 等,可以直接貼上程式碼,自動識別混淆方式,本案例推薦使用 de4js,如下圖所示:

05.png

我們將還原後的結果複製到本地檔案,使用 Fiddler 的 Autoresponder 功能對響應進行替換,如下圖所示:

06.png

如果此時開啟抓包,重新整理頁面,你會發現請求狀態 status 顯示的是 CORS error,JS 替換不成功,在控制檯裡還可以看到報錯 No 'Access-Control-Allow-Origin' header is present on the requested resource. 如下圖所示:

14.png

CORS 跨域錯誤

CORS (Cross-Origin Resource Sharing,跨域資源共享)是一個 W3C 標準,該標準使用附加的 HTTP 頭來告訴瀏覽器,允許執行在一個源上的 Web 應用訪問位於另一不同源的資源。一個請求 URL 的協議、域名、埠三者之間任意與當前頁面地址不同即為跨域。常見的跨域問題就是瀏覽器提示在 A 域名下不可以訪問 B 域名的 API,有關 CORS 的進一步理解,可以參考 W3C CORS Enabled

簡要流程如下:

1、消費者傳送一個 Origin 報頭到提供者端:Origin: http://www.site.com
2、提供者傳送一個 Access-Control-Allow-Origin 響應報頭給消費者,如果值為 * 或 Origin 對應的站點,則表示允許共享資源給消費者,如果值為 null 或者不存在,則表示不允許共享資源給消費者;
3、除了 Access-Control-Allow-Origin 以外,部分站點還有可能檢測 Access-Control-Allow-Credentials,為 true 表示允許;
4、瀏覽器根據提供者的響應報文判斷是否允許消費者跨域訪問到提供者源;

我們根據前面在控制檯的報錯資訊,可以知道是響應頭缺少 Access-Control-Allow-Origin 導致的,在 Fiddler 裡面有兩種方法為響應頭新增此引數,下面分別介紹一下:

第一種是利用 Fiddler 的 Filter 功能,在 Response Headers 裡設定即可,分別填入 Access-Control-Allow-Origin 和允許的域名,如下圖所示:

15.png

第二種是修改 CustomRules.js 檔案,依次選擇 Rules —> Customize Rules,在 static function OnBeforeResponse(oSession: Session) 模組下增加以下程式碼:

if(oSession.uriContains("要處理的 URL")){
    oSession.oResponse["Access-Control-Allow-Origin"] =  "允許的域名";
}

16.png

兩種方法二選一,設定完畢後,就可以成功替換了,重新整理再次除錯就可以看到是還原後的 JS 了,如下圖所示:

07.png

逆向分析

很明顯 window.getUa 是主要的加密函式,所以我們先來分析一下這個函式:

window.getUa = function() {
    var _0x7dfc34 = new Date().getTime();
    if (_0x4a9622) {
        _0x2644f4();
    }
    _0x55b608();
    var _0x261229 = _0x1722c3(_0x2e98dd) + '|' + _0x1722c3(_0x420004) + '|' + _0x7dfc34.toString(0x10);
    _0x261229 = btoa(_0x570bef.gzip(_0x261229, {
        'to': 'string'
    }));
    return _0x261229;
};

_0x7dfc34 是時間戳,接著一個 if 判斷,我們可以滑鼠放到判斷裡去看看,發現判斷的 _0x4a9622 是 false,那麼 _0x2644f4() 就不會被執行,然後執行了 _0x55b608() 方法,_0x261229 的值,主要呼叫了 _0x1722c3() 方法得到的,前後依次傳入了 _0x2e98dd_0x420004,很明顯這兩個值比較關鍵,分別搜尋一下,可以發現:

_0x2e98dd 定義了一些 header、瀏覽器的資訊、螢幕資訊、系統字型資訊等,這些資訊可以作為定值直接傳入,如下圖所示:

09.png

10.png

_0x420004 搜尋有用的結果就是僅定義了一個空物件,在控制檯輸出一下可以看到實際上包含了一些鍵盤、滑鼠點選移動的資料,實際上經過測試發現, _0x420004 的值並不是強校驗的,可以使用隨機數模擬生成,也可以直接複製一個定值。

11.png

_0x2e98dd_0x420004 這兩個引數都沒有進行強校驗,完全可以以定值的方式傳入,這兩個值都是 JSON 格式,我們可以直接在控制檯使用 copy 語句複製其值,或者使用 JSON.stringify() 語句輸出結果再手動複製。

12.png

本地聯調

裡面各個函式相互呼叫,比較多,可以直接把整個 JS copy 下來,我們注意到整個函式是一個自執行函式,在本地呼叫時,我們可以定義一個全域性變數,然後在 window.getUa 函式裡,將 _0x261229 的值賦值給全域性變數,也就相當於匯出值,最後取這個全域性變數即可,還有一種方法就是不讓它自執行,改寫成正常一般的函式,然後呼叫 window.getUa 方法得到 ua 值。

首先我們把 _0x2e98dd_0x420004 的值在本地定義一下,這裡有個小細節,需要把原 JS 程式碼裡這兩個值定義的地方註釋掉,防止起衝突。

在本地除錯時,會提示 windowlocationdocument 未定義,定義一下為空物件即可,然後又提示 attachEvent 未定義,搜尋一下,是 _0x13cd5a 的一個原型物件,除了 attachEvent 以外,還有個 addEventListeneraddEventListener() 方法用於向指定元素新增事件控制程式碼,在 IE 中使用 attachEvent() 方法來實現,我們在 Google Chrome 裡面埋下斷點除錯一下,重新整理頁面會直接進入 addEventListener() 方法,其中的事件是 keydown,即鍵盤按下,就呼叫後面的 _0x5cec90 方法,輸出一下後面返回的 this,實際上並沒有產生什麼有用的值,所以 _0x13cd5a.prototype.bind 方法我們可以直接將其註釋掉,實際測試也沒有影響。

08.png

接著本地除錯,又會提示 btoa 未定義,btoaatob 是 window 物件的兩個函式,其中 btoa 是 binary to ascii,用於將 binary 的資料用 ascii 碼錶示,即 Base64 的編碼過程,而 atob 則是 ascii to binary,用於將 ascii 碼解析成 binary 資料,即 Base64 的解碼過程。

在 NodeJS 裡,提供了一個稱為 Buffer 的本地模組,可用於執行 Base64 編碼和解碼,這裡不做詳細介紹,可自行百度,window.getUa 方法裡的原 btoa 語句是這樣的:

_0x261229 = btoa(_0x570bef.gzip(_0x261229, {'to': 'string'}));

在 NodeJS 裡,我們可以這樣寫:

_0x261229 = Buffer.from(_0x570bef.gzip(_0x261229, {'to': 'string'}), "latin1").toString('base64');

注意:Buffer.from() 傳入了一個 latin1 引數,這是由於 _0x570bef.gzip(_0x261229, {'to': 'string'}) 的結果是 Latin1(ISO-8859-1 的別名)編碼,如果不傳,或者傳入其他引數,則最終結果可能和 btoa 方法得出的結果不一樣!

自此,本地聯調完畢,就可以得到正確的 ua 值了!

完整程式碼

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

以下只演示部分關鍵程式碼,不能直接執行! 完整程式碼倉庫地址:https://github.com/kgepachong...

JavaScript 加密關鍵程式碼架構

var window = {};
var location = {};
var document = {};
var _0x5a577d = function () {}();
var _0xe26ae = function () {}();
var _0x3204b9 = function () {}();
var _0x3c7e70 = function () {}();
var _0x4a649b = function () {}();
var _0x21524f = function () {}();
var _0x2b0d61 = function () {}();
var _0x53634a = function () {}();
var _0x570bef = function () {}();
var _0xd05c32 = function (_0x5c6c0c) {};
window.CHLOROFP_STATUS = 'start';

// 此處省略 N 個函式

var _0x2e98dd = {
    // 物件具體的值已省略
    "basic": {},
    "header": {},
    "navigator": {},
    "screenData": {},
    "sysfonts": [],
    "geoAndISP": {},
    "browserType": {},
    "performanceTiming": {},
    "canvasFp": {},
    "visTime": [],
    "other": {}
}
var _0x420004 = {
    // 物件具體的值已省略
    "keypress": true,
    "scroll": true,
    "click": true,
    "mousemove": true,
    "mousemoveData": [],
    "keypressData": [],
    "mouseclickData": [],
    "wheelDeltaData": []
}

window.getUa = function () {
    var _0x7dfc34 = new Date().getTime();
    if (_0x4a9622) {
        _0x2644f4();
    }
    _0x55b608();
    var _0x261229 = _0x1722c3(_0x2e98dd) + '|' + _0x1722c3(_0x420004) + '|' + _0x7dfc34.toString(0x10);
    // _0x261229 = btoa(_0x570bef.gzip(_0x261229, {'to': 'string'}));
    _0x261229 = Buffer.from(_0x570bef.gzip(_0x261229, {'to': 'string'}), "latin1").toString('base64');
    return _0x261229;
};

// 測試輸出
// console.log(window.getUa())

Python 登入關鍵程式碼

# ==================================
# --*-- coding: utf-8 --*--
# @Time    : 2021-11-15
# @Author  : 微信公眾號:K哥爬蟲
# @FileName: weidian_login.py
# @Software: PyCharm
# ==================================


import execjs
import requests
from urllib import parse


index_url = "脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler"
login_url = "脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler"
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"
session = requests.session()


def get_encrypted_ua():
    with open('get_encrypted_ua.js', 'r', encoding='utf-8') as f:
        uad_js = f.read()
    ua = execjs.compile(uad_js).call('window.getUa')
    ua = parse.quote(ua)
    return ua


def get_wd_token():
    headers = {"User-Agent": UserAgent}
    response = session.get(url=index_url, headers=headers)
    wd_token = response.cookies.get_dict()["wdtoken"]
    return wd_token


def login(phone, password, ua, wd_token):
    headers = {
        "user-agent": UserAgent,
        "origin": "脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler",
        "referer": "脫敏處理,完整程式碼關注 GitHub:https://github.com/kgepachong/crawler",
    }
    data = {
        "phone": phone,
        "countryCode": "86",
        "password": password,
        "version": "1",
        "subaccountId": "",
        "clientInfo": '{"clientType": 1}',
        "captcha_session": "",
        "captcha_answer": "",
        "vcode": "",
        "mediaVcode": "",
        "ua": ua,
        "scene": "PCLogin",
        "wdtoken": wd_token
    }
    response = session.post(url=login_url, headers=headers, data=data)
    print(response.json())


def main():
    phone = input("請輸入登入手機號: ")
    password = input("請輸入登入密碼: ")
    ua = get_encrypted_ua()
    wd_token = get_wd_token()
    login(phone, password, ua, wd_token)


if __name__ == '__main__':
    main()

相關文章