日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

【JS 逆向百例】某空气质量监测平台无限 debugger 以及数据动态加密分析

發(fā)布時間:2023/12/10 javascript 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【JS 逆向百例】某空气质量监测平台无限 debugger 以及数据动态加密分析 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

關(guān)注微信公眾號:K哥爬蟲,持續(xù)分享爬蟲進(jìn)階、JS/安卓逆向等技術(shù)干貨!


文章目錄

    • 聲明
    • 逆向目標(biāo)
    • 寫在前面
    • 繞過無限 debugger
      • 方法一
      • 方法二
      • 方法三
    • 抓包分析
    • 加密入口
    • 動態(tài) JS
    • 本地改寫


聲明

本文章中所有內(nèi)容僅供學(xué)習(xí)交流,抓包內(nèi)容、敏感網(wǎng)址、數(shù)據(jù)接口均已做脫敏處理,嚴(yán)禁用于商業(yè)用途和非法用途,否則由此產(chǎn)生的一切后果均與作者無關(guān),若有侵權(quán),請聯(lián)系我立即刪除!

逆向目標(biāo)

  • 目標(biāo):某空氣質(zhì)量監(jiān)測平臺無限 debugger 以及請求數(shù)據(jù)、返回數(shù)據(jù)動態(tài)加密、解密
  • 主頁:aHR0cHM6Ly93d3cuYXFpc3R1ZHkuY24v
  • 接口:aHR0cHM6Ly93d3cuYXFpc3R1ZHkuY24vYXBpbmV3L2FxaXN0dWR5YXBpLnBocA==

寫在前面

這個站點更新頻率很高,在K哥之前也已經(jīng)有很多博主寫了該站點的分析文章,近期有讀者問請求數(shù)據(jù)的加密和返回數(shù)據(jù)的解密,發(fā)現(xiàn)其加解密 JS 變成了動態(tài)的,以前的那些文章提到的解決思路不太行了,但整體上來說也不是很難,只不過處理起來比較麻煩一點,還有一些小細(xì)節(jié)需要注意。

在網(wǎng)站的“關(guān)于系統(tǒng)”里可以看到,這個站貌似是個人開發(fā)者在維護(hù),最早在2013年就有了,在友情贊助列表里,可以看到大多數(shù)都是一些環(huán)境、測繪、公共衛(wèi)生相關(guān)的大學(xué)專業(yè)、研究院人員,可以猜測到這些數(shù)據(jù)對于他們的研究是非常有幫助的,再加上反爬更新頻繁,可以看出站長飽受爬蟲之苦,K哥也不想給站長添加負(fù)擔(dān),畢竟這種站點咱們應(yīng)該支持,讓他長久維護(hù)下去,所以本期K哥只分析邏輯和少部分代碼,就不放完整代碼了,如果有相關(guān)專業(yè)人士確實需要抓取數(shù)據(jù)做研究的,可以在公眾號后臺聯(lián)系我。

繞過無限 debugger

右鍵 F12,會提示右鍵被禁用,不要緊,使用快捷鍵 Ctrl+Shift+i 或者瀏覽器右上角,更多工具,開發(fā)者工具,照樣能打開。

方法一

打開控制臺后會進(jìn)入第一個無限 debugger,往上跟一個棧,可以看到一個 try-catch 語句,你下斷點會發(fā)現(xiàn)他會一直走 catch,調(diào)用 setTimeout() 方法,該方法用于在指定的毫秒數(shù)后調(diào)用函數(shù)或計算表達(dá)式,注意上面,是將 debugger 傳遞給了構(gòu)造方法 constructor,所以這里我們有兩種方法過掉 debugger,Hook 掉 constructor 或 setTimeout 都可以。

// 兩種 Hook 任選一中 // Hook 構(gòu)造方法 Function.prototype.constructor_ = Function.prototype.constructor; Function.prototype.constructor = function (a) {if(a == "debugger") {return function (){};}return Function.prototype.constructor_(a); };// Hook setTimeout var setTimeout_ = setTimeout var setTimeout = function (func, time){if (func == txsdefwsw){return function () {};}return setTimeout_(func, time) }

然后就來到了第二個無限 debugger,同樣跟棧,發(fā)現(xiàn)有個 setInterval 定時器和構(gòu)造方法 constructor,類似的,我們 Hook 掉 constructor 或 setInterval 都可以。注意:定時器這里還檢測了窗口高寬,即便是你過了 constructor 或 setInterval,如果不把開發(fā)者工具單獨拿出來也是不行的,會不斷輸出“檢測到非法調(diào)試”。

// Hook setInterval var setInterval_ = setInterval setInterval = function (func, time){if (time == 2000) {return function () {};}return setInterval_(func, time) }

我們觀察到,其實這兩個無限 debugger 都可以 Hook 構(gòu)造方法來過掉,所以直接 Fiddler 注入該 Hook 構(gòu)造方法的代碼即可:

方法二

在我們遇到第二個無限 debugger 的時候,還可以直接跟棧到一個 city_realtime.php 的頁面,里面有兩個 eval 語句,執(zhí)行第一個 eval 里面的語句你就會發(fā)現(xiàn)正是前面我們在 VM 虛擬機里面看到的 debugger 代碼,所以這里理論上可以直接替換掉這個頁面,去掉 eval 語句,就不會有無限 debugger 了,但是K哥先告訴你,現(xiàn)在不行了,因為里面有加載了某個 JS,這個 JS 在后面加密解密中會用到,但是這個 JS 是動態(tài)的,每10分鐘就會改變,我們后面還要通過此頁面來獲取動態(tài)的 JS,所以是不能替換的!這里只是提一下這個思路!

方法三

當(dāng)然,這里還有一種最簡單的方法,直接右鍵選擇 Never pause here,永不在此處斷下即可,同樣還需要把開發(fā)者工具窗口單獨拿出來,不然會一直輸出“檢測到非法調(diào)試”。

抓包分析

我們在實時監(jiān)控頁面,順便點擊查詢一個城市,可以看到請求的 Form Data 和返回的數(shù)據(jù)都是加密的,如下圖所示:

加密入口

由于是 XHR,所以我們直接跟棧,很容易找到加密的位置:

可以看到傳遞的 data 鍵值對:{hXM8NDFHN: p7crXYR},鍵在這個 JS 里是寫死的,值是通過一個方法 pU14VhqrofroULds() 得到的,這個方法需要傳遞兩個參數(shù),第一個是定值 GETDATA,第二個就是城市名稱,我們再跟進(jìn)看看這個方法是啥:

一些 appId、時間戳、城市等參數(shù),做了一些 MD5、base64 的操作,返回的 param 就是我們要的值了。看起來不難,我們再找找返回的加密數(shù)據(jù)是如何解密的,我們注意到 ajax 請求有個 success 關(guān)鍵字,我們即便是不懂 JS 邏輯,也可以猜到應(yīng)該是請求成功后的處理操作吧,如下圖所示:傳進(jìn)來的 dzJMI 就是返回的加密的數(shù)據(jù),經(jīng)過 db0HpCYIy97HkHS7RkhUn() 方法后,就解密成功了:

跟進(jìn) db0HpCYIy97HkHS7RkhUn() 方法,可以看到是 AES+DES+BASE64 解密,傳入的密鑰 key 和偏移量 iv 都在頭部有定義:

動態(tài) JS

經(jīng)過以上分析后,我們加密解密的邏輯都搞定了,但是你多調(diào)試一下就會發(fā)現(xiàn),這一個加密解密的 JS 是動態(tài)變化的,定義的密鑰 key 和偏移量 iv 都是隔段時間就會改變的,如果你在這段代碼里下斷點,停留時間過長,突然發(fā)現(xiàn)斷點失效無法斷下了,那就是 JS 變了,當(dāng)前代碼已經(jīng)失效了。

我們隨便薅兩個不同的 JS 下來(提示:JS 每隔10分鐘會變化,后文有詳細(xì)分析),利用 PyCharm 的文件對比功能(依次選擇 View - Compare With)可以總結(jié)出以下幾個變化的地方(變量名的變化不算):

  • 開頭的8個參數(shù)的值:兩個 aes key 和 iv,兩個 des key 和 iv;
  • 生成加密的 param 時,appId 是變化的,最后的加密分為 AES、DES 和沒有加密,三種情況(這里是最容易忽略的地方,這里沒有注意到,請求可能會提示 appId 無效的情況):
  • 最后發(fā)送請求時,data 鍵值對,其中的鍵也是變化的:
  • 變化的地方我們找到了,那我們怎么獲取這個 JS 呢?因為這個 JS 的在 VM 虛擬機里,所以我們還要找到它的源頭,是從哪里來的,我們抓包可以看到一個比較特殊的 JS,類似于 encrypt_xxxxxx.js,看這取名就知道不簡單,返回的是一段 eval 包裹的代碼:

    對于 eval 我們已經(jīng)很熟悉了,直接去掉 eval,讓他執(zhí)行一下,就可以看到正是我們需要的那段 JS:

    這里有個小細(xì)節(jié),如果你使用控制臺,會發(fā)現(xiàn)它一直在打印 img 標(biāo)簽,影響我們的輸入,這里可以直接跟進(jìn)去下斷點暫時阻止他運行就行了,不需要做其他操作浪費時間:

    你以為到這里就差不多搞定了?錯了,同樣的這個 encrypt_xxxxxx.js 也藏有玄機:

  • encrypt_xxxxxx.js 的名稱是動態(tài)的,后面的 v 值是秒級時間戳,隔600秒,也就是十分鐘就會改變,這個 JS 可以在 city_realtime.php 頁面找到,還記得我們前面說過的繞過無限 debugger 不能替換此頁面嗎?我們要通過此頁面來獲取動態(tài)的 JS,所以是不能替換的!
  • encrypt_xxxxxx.js 返回的 JS,并不是所有的執(zhí)行一遍 eval 就能得到明文代碼了,它是 eval 和 base64 相結(jié)合的,第一遍都是 eval,但是后面就說不定了,有可能直接出結(jié)果,有可能需要 base64,有可能 base64 兩遍,有可能兩遍 base64 之后還要再 eval,總之,除了第一遍是 eval 以外,后面是否需要 base64 和 eval,以及需要的次數(shù)和先后順序,都是不確定的!舉幾個例子:
  • 這里可能有人會問,你怎么看出來那是 base64 呢?很簡單,直接在網(wǎng)站頁面的控制臺里輸入 dswejwehxt,點擊去看這個函數(shù),就是 base64:

    那么針對 encrypt_xxxxxx.js 內(nèi)容不確定的情況,我們可以寫一個方法,獲取到 encrypt_xxxxxx.js 后,需要執(zhí)行 eval 就執(zhí)行 eval,需要執(zhí)行 base64 就執(zhí)行 base64,直到?jīng)]有 eval 和 base64 即可,可以分別用字符串 eval(function 和 dswejwehxt( 來判斷是否需要 eval 和 base64(當(dāng)然也有其他方式,比如 () 的個數(shù)等),示例代碼如下所示:

    def get_decrypted_js(encrypted_js_url):""":param encrypted_js_url: encrypt_xxxxxx.js 的地址:return: 解密后的 JS"""decrypted_js = requests.get(url=encrypted_js_url, headers=headers).textflag = Truewhile flag:if "eval(function" in decrypted_js:# 需要執(zhí)行 evalprint("需要執(zhí)行 eval!")replace_js = decrypted_js.replace("eval(function", "(function")decrypted_js = execjs.eval(replace_js)elif "dswejwehxt(" in decrypted_js:# 需要 base64 解碼base64_num = decrypted_js.count("dswejwehxt(")print("需要 %s 次 base64 解碼!" % base64_num)decrypted_js = re.findall(r"\('(.*?)'\)", decrypted_js)[0]num = 0while base64_num > num:decrypted_js = base64.b64decode(decrypted_js).decode()num += 1else:# 得到明文flag = False# print(decrypted_js)return decrypted_js

    本地改寫

    通過以上函數(shù)我們就拿到了動態(tài)的 JS 了,那么我們可以直接執(zhí)行拿回來的 JS 嗎?當(dāng)然是不可以的,你可以自己本地執(zhí)行一下,可以發(fā)現(xiàn)里面的 CryptoJS、Base64、hex_md5 都需要補齊才行,所以到這里我們就有兩種做法:

  • 拿到解密后的動態(tài) JS 后,動態(tài) JS 和我們自己寫的 Base64、hex_md5 等方法組成新的 JS 代碼,執(zhí)行新的 JS 代碼拿到參數(shù),這里還需要注意因為里面的其他方法名都是動態(tài)的,所以你還得想辦法匹配到正確的方法名來調(diào)用才行,所以這種方法個人感覺還是稍微有點兒麻煩的;
  • 我們本地自己寫一個 JS,拿到解密后的動態(tài) JS 后,把里面的 key、iv、appId、data 鍵名、param 是否需要 AES 或 DES 加密,這些信息都匹配出來,然后傳給我們自己寫的 JS,調(diào)用我們自己的方法拿到加密結(jié)果。
  • 雖然兩種方法都很麻煩,但K哥暫時也想不到更好的解決方法了,有比較好的想法的朋友可以留言說一說。

    以第二種方法為例,我們本地的 JS 示例(main.js):

    var CryptoJS = require("crypto-js");var BASE64 = {encrypt: function (text) {return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(text))},decrypt: function (text) {return CryptoJS.enc.Base64.parse(text).toString(CryptoJS.enc.Utf8)} };var DES = {encrypt: function (text, key, iv) {var secretkey = (CryptoJS.MD5(key).toString()).substr(0, 16);var secretiv = (CryptoJS.MD5(iv).toString()).substr(24, 8);secretkey = CryptoJS.enc.Utf8.parse(secretkey);secretiv = CryptoJS.enc.Utf8.parse(secretiv);var result = CryptoJS.DES.encrypt(text, secretkey, {iv: secretiv,mode: CryptoJS.mode.CBC,padding: CryptoJS.pad.Pkcs7});return result.toString();},decrypt: function (text, key, iv) {var secretkey = (CryptoJS.MD5(key).toString()).substr(0, 16);var secretiv = (CryptoJS.MD5(iv).toString()).substr(24, 8);secretkey = CryptoJS.enc.Utf8.parse(secretkey);secretiv = CryptoJS.enc.Utf8.parse(secretiv);var result = CryptoJS.DES.decrypt(text, secretkey, {iv: secretiv,mode: CryptoJS.mode.CBC,padding: CryptoJS.pad.Pkcs7});return result.toString(CryptoJS.enc.Utf8);} };var AES = {encrypt: function (text, key, iv) {var secretkey = (CryptoJS.MD5(key).toString()).substr(16, 16);var secretiv = (CryptoJS.MD5(iv).toString()).substr(0, 16);secretkey = CryptoJS.enc.Utf8.parse(secretkey);secretiv = CryptoJS.enc.Utf8.parse(secretiv);var result = CryptoJS.AES.encrypt(text, secretkey, {iv: secretiv,mode: CryptoJS.mode.CBC,padding: CryptoJS.pad.Pkcs7});return result.toString();},decrypt: function (text, key, iv) {var secretkey = (CryptoJS.MD5(key).toString()).substr(16, 16);var secretiv = (CryptoJS.MD5(iv).toString()).substr(0, 16);secretkey = CryptoJS.enc.Utf8.parse(secretkey);secretiv = CryptoJS.enc.Utf8.parse(secretiv);var result = CryptoJS.AES.decrypt(text, secretkey, {iv: secretiv,mode: CryptoJS.mode.CBC,padding: CryptoJS.pad.Pkcs7});return result.toString(CryptoJS.enc.Utf8);} };function getDecryptedData(data, AES_KEY_1, AES_IV_1, DES_KEY_1, DES_IV_1) {data = AES.decrypt(data, AES_KEY_1, AES_IV_1);data = DES.decrypt(data, DES_KEY_1, DES_IV_1);data = BASE64.decrypt(data);return data; }function ObjectSort(obj) {var newObject = {};Object.keys(obj).sort().map(function (key) {newObject[key] = obj[key];});return newObject; }function getRequestParam(method, obj, appId) {var clienttype = 'WEB';var timestamp = new Date().getTime()var param = {appId: appId,method: method,timestamp: timestamp,clienttype: clienttype,object: obj,secret: CryptoJS.MD5(appId + method + timestamp + clienttype + JSON.stringify(ObjectSort(obj))).toString()};param = BASE64.encrypt(JSON.stringify(param));return param; }function getRequestAESParam(requestMethod, requestCity, appId, AES_KEY_2, AES_IV_2){var param = getRequestParam(requestMethod, requestCity, appId);return AES.encrypt(param, AES_KEY_2, AES_IV_2); }function getRequestDESParam(requestMethod, requestCity, appId, DES_KEY_2, DES_IV_2){var param = getRequestParam(requestMethod, requestCity, appId);return DES.encrypt(param, DES_KEY_2, DES_IV_2); }

    我們匹配 JS 里面的各項參數(shù)的 Python 代碼示例(匹配8個 key、iv 值、appId 和 param 的加密方式):

    def get_key_iv_appid(decrypted_js):""":param decrypted_js: 解密后的 encrypt_xxxxxx.js:return: 請求必須的一些參數(shù)"""key_iv = re.findall(r'const.*?"(.*?)";', decrypted_js)app_id = re.findall(r"var appId.*?'(.*?)';", decrypted_js)request_data_name = re.findall(r"aqistudyapi.php.*?data.*?{(.*?):", decrypted_js, re.DOTALL)# 判斷 param 是 AES 加密還是 DES 加密還是沒有加密if "AES.encrypt(param" in decrypted_js:request_param_encrypt = "AES"elif "DES.encrypt(param" in decrypted_js:request_param_encrypt = "DES"else:request_param_encrypt = "NO"key_iv_appid = {# key 和 iv 的位置和原來 js 里的是一樣的"aes_key_1": key_iv[0],"aes_iv_1": key_iv[1],"aes_key_2": key_iv[2],"aes_iv_2": key_iv[3],"des_key_1": key_iv[4],"des_iv_1": key_iv[5],"des_key_2": key_iv[6],"des_iv_2": key_iv[7],"app_id": app_id[0],# 發(fā)送請求的 data 的鍵名"request_data_name": request_data_name[0].strip(),# 發(fā)送請求的 data 值需要哪種加密"request_param_encrypt": request_param_encrypt}# print(key_iv_appid)return key_iv_appid

    我們發(fā)送請求以及解密返回值的 Python 代碼示例(以北京為例):

    def get_data(key_iv_appid):""":param key_iv_appid: get_key_iv_appid() 方法返回的值"""request_method = "GETDATA"request_city = {"city": "北京"}with open('main.js', 'r', encoding='utf-8') as f:execjs_ = execjs.compile(f.read())# 根據(jù)不同加密方式調(diào)用不同方法獲取請求加密的 param 參數(shù)request_param_encrypt = key_iv_appid["request_param_encrypt"]if request_param_encrypt == "AES":param = execjs_.call('getRequestAESParam', request_method, request_city,key_iv_appid["app_id"], key_iv_appid["aes_key_2"], key_iv_appid["aes_iv_2"])elif request_param_encrypt == "DES":param = execjs_.call('getRequestDESParam', request_method, request_city,key_iv_appid["app_id"], key_iv_appid["des_key_2"], key_iv_appid["des_iv_2"])else:param = execjs_.call('getRequestParam', request_method, request_city, key_iv_appid["app_id"])data = {key_iv_appid["request_data_name"]: param}response = requests.post(url=aqistudy_api, headers=headers, data=data).text# print(response)# 對獲取的加密數(shù)據(jù)解密decrypted_data = execjs_.call('getDecryptedData', response,key_iv_appid["aes_key_1"], key_iv_appid["aes_iv_1"],key_iv_appid["des_key_1"], key_iv_appid["des_iv_1"])print(json.loads(decrypted_data))

    運行結(jié)果,成功請求并解密返回值:

    總結(jié)

    以上是生活随笔為你收集整理的【JS 逆向百例】某空气质量监测平台无限 debugger 以及数据动态加密分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。