apistudy js逆向(ast)


前言

目標網站 

https://www.aqistudy.cn/historydata/daydata.php?city=%E6%9D%AD%E5%B7%9E&month=2013-12

 

爬取內容

頁面上的表格中的所有數據

 

 

爬取過程

先F12檢查吧。沒想到一打開控制台,頁面就變成這樣了。看樣子做了反爬。

 

 

F12檢測-解決方法

我所采取的方法是使用 油猴下一個斷點。因為油猴的執行時機較早。因此那個時候debugger住,就可以看到這個頁面的邏輯

下面便是油猴的代碼,其實只有debugger這一行語句。

(function() {
    'use strict';
    debugger;
    // Your code here...
})();

 

打開F12,刷新頁面,就可以看到頁面在斷點處停下了

 

有可能打開頁面就並不是上面的樣子(2020.8月18日)

 

 也是這個樣子,並且我們的右鍵啥的都被禁用了。

 

找到禁用F12的代碼處

調整tampermonkey的運行時機,將其調到document-start。這樣油猴便是第一個運行的js文件。

 

 

 

在油猴的腳本處停止后,我們直接在resource面板上找到加載的html文件。

 

 

如何重新定義方法呢?

控制台上輸入 function txsdefwsw(){} 便可以了。

 

 

 

這時候翻一下頁面,發現數據已經被加載出來了。但可喜的是,頁面沒有出現那個提示了。

這時候去network面板找找所有的請求,會很驚喜的發現,貌似沒有找到相關的請求啊。

 

答案是 並沒有發送請求,而是使用了localhost本地存儲。

 

 

有人可能在想了,這是啥玩意啊。你怎么這么確實就是我們想要的數據呢?

這點等一下我們可以在源碼中見到。現在我們需要將localstorage中的緩存全部清掉,這樣頁面就會發起真正的請求了。

 

 

 

然后還需要下一個XHR斷點,因為油猴下的斷點其實有些晚了(油猴可以設置腳本的執行時機,可以設置到最早,默認的不是最早的)

然后刷新下頁面。

這次的斷點就不是那個油猴斷點了。熟悉前端的朋友一眼就可以看出這個便是ajax請求。

 

切換調用棧,我們很快就會找到發送ajax請求的源頭

 

 

 

其中這個s76開頭的函數的第二個參數便是要請求的參數

 

那下一步是不是直接在這個s76函數的第一行代碼處打一個斷點,然后觀察請求就可以了?

並不是的哦。如果你仔細觀察的話,就會發現這部分代碼並沒有啥具體的地址,取而代之的是VM5735之類的東西

這說明了啥?說明了這部分代碼是通過eval執行的,動態執行的js代碼。因此在此處下斷點沒用。

 

我們還得順着調用棧向上找,找到一處不是在vm中執行的。

 

 

 

在1056行處下一個斷點,再次刷新,然后斷點就會在此處停住了。 

 

這個sU開頭的函數便是整個的加載邏輯了,第三個是異步回調函數,用於設置數據用的。 

 

 

 我們進入是sU開頭函數了,解釋下函數的作用

 

有些人看到這個就會覺得好簡單了,我直接把171行的函數和179行的數據解密函數扣下來不就搞定了?

 但事實上沒那么簡單,171行的函數其實是動態生成的(見后面的"關於eval的200行代碼")。里面並不是固定死的算法。有些參數的值是變化的 

下面兩張圖中畫框的內容便是變化的東西。

 

 

 

很不幸,不但值在變,變量名也在變。因此如果想要用正則來匹配的話,實際操作難度很大。

 

我的想法是這個代碼肯定是可以運行的,我們大可不必考慮變量名變來變去的。我們只需要修改下sU函數里的東西就可以了。

經過測試,只需要保存下面的幾處代碼即可在node環境中運行了

 

 

 

還有個小小的問題,這些函數名都不是固定的,如果想要調用,根本沒有辦法調用。其實可以做一層映射即可。

function sUBtOIE2skajXLT(muHwwFDaJ, oBRgVeYIlQ, cs16YvBHk, pN62eFL) { const k9dI = hex_md5(muHwwFDaJ + JSON.stringify(oBRgVeYIlQ)); var psyg2ok = pZ5JcsDR5kiPoq(muHwwFDaJ, oBRgVeYIlQ); return ["hr9jdXsuU", psyg2ok]; }
function
getParam(obj){ // 以后要調用的話 getParam({city: "杭州", "month": "201312"}) 便可以得到需要post的data了。

  return sUBtOIE2skajXLT("GETDAYDATA", obj); // 這里要對應上 上面具體的函數(有工具可以自動解析到)
}

function parseData(input){ // 也是一樣的,如果想要解密下響應內容,調用此函數即可。
  return d0UUVoZ0h8GEzpjSCLcq(input);
}

 
        

 

你可能會問了,這樣寫有啥用。下次函數名變了,難道還要手動去改函數名嗎?

No,No,No。並不需要。

 

這里就要引入一個工具了,它叫babel。

干啥用了?

這個工具可以將我們的js代碼先變成一個ast語法樹,然后我們通過修改或者訪問這個樹的節點。最終生成的代碼就會被我們這樣修改掉了。

 

我拿這個工具舉個簡單的例子吧。

我們需要在所有的console.log中輸出我們所調用的函數名字(經典例子)

function foo(){
  console.log(111);
}

// 變成這個樣子

function foo(){
   console.log("function foo", 111);        
}

 

// 下面的四個依賴包需要npm安裝下
const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");

function compile(code) {
    const ast = parser.parse(code); // 將代碼解析成ast語法樹
    const visitor = {
        CallExpression(path) { // 下面的節點名稱啥的可以通過 
            https://astexplorer.net/ 找到

            const node = path.node;
            if (
                node.callee.type === "MemberExpression"
                && node.callee.object.name === 'console'
                && node.callee.property.name === 'log'
            ) {
                // 找到函數的名字
                const parentNode = path.findParent(p => types.isFunctionDeclaration(p))
                const parentName = parentNode.node.id.name;
                console.log(parentName);
                // ast增加一個結構
                node.arguments.unshift(types.stringLiteral(`function ${parentName}`))

            }
            // 找到函數中的console.log語句

        }
    }
    traverse.default(ast, visitor);
    return generator.default(ast, {}, code);
}

const code = `
function foo(){
  console.log(111);
}
`;
const output = compile(code);
console.log(output.code);
查看具體實現

 

 

那對於這個網站,我也寫了個對應的ast來應對。

const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");
const fs = require("fs");
function compile(code) {
    // 1.parse 將代碼解析為抽象語法樹(AST)
    const ast = parser.parse(code);
    const visitor = {
        FunctionDeclaration(path) {
            const node = path.node;
            // console.log(node.params)
            // 目標函數長這個樣子
            // function sBAAH3A7LFcJgXe(method, object, callback, period) { }
            if (
                node.params.length === 4
                // && node.params[0].name === "method"
                // && node.params[1].name === "object"
                // && node.params[2].name === "callback"
                // && node.params[3].name === "period"
            ) {
                // 獲取此函數名,暴露出接口
                const funcName = node.id.name
                console.log(funcName);
                // 去除第二行的 const data = getDataFromLocalStorage(key, period);
                const ifStatement = node.body.body[2];
                const addParamExpression = ifStatement.consequent.body[0]
                node.body.body.splice(0, 2, addParamExpression);

                // 獲取post請求中data的key
                postDataKey = ifStatement.consequent.body[1].expression.arguments[0].properties[1].value.properties[0].key.name
                console.log(postDataKey)

                // 解密函數映射
                const decFuncName = ifStatement.consequent.body[1].expression.arguments[0].properties[3].value.body.body[0].expression.right.callee.name;
                console.log(decFuncName)

                // 刪除if語句
                node.body.body.pop()

                // 增加 return [param, postDataKey]
                const paramName = node.body.body[0].declarations[0].id.name;
                const returnStatement = types.returnStatement(types.ArrayExpression([types.identifier(paramName), types.stringLiteral(postDataKey)]))
                node.body.body.push(returnStatement)

                // 增加映射
                const globalBody = path.findParent(p => {
                    return true;
                })

                const funcMappingForGetParam = types.functionDeclaration(
                    types.identifier("getParam"), [
                    types.identifier("obj")
                ], types.blockStatement([
                    types.returnStatement(
                        types.callExpression(
                            types.identifier(funcName),
                            [
                                types.stringLiteral("GETDAYDATA"),
                                types.identifier("obj")
                            ]
                        )
                    )
                ])
                )
                globalBody.container.program.body.push(funcMappingForGetParam)

                // 關於數據解密函數
                // function parseData(input) {
                //     return dA3Gc6OUqeCBgWSh53T(input);
                // }
                const funcMappingForParseData = types.functionDeclaration(
                    types.identifier("parseData"), [
                    types.identifier("input")
                ], types.blockStatement([
                    types.returnStatement(
                        types.callExpression(
                            types.identifier(decFuncName),
                            [
                                types.identifier("input")
                            ]
                        )
                    )
                ])
                )
                globalBody.container.program.body.push(funcMappingForParseData)
            }
        }
    }
    // 2,traverse 轉換代碼
    traverse.default(ast, visitor);

    // 3. generator 將 AST 轉回成代碼
    return generator.default(ast, {}, code);
}
// const code = fs.readFileSync("out.js", "utf-8");
// const newCode = compile(code)
// fs.writeFileSync("out2.js", newCode.code, "utf-8")

ast轉換

這樣便可以實現上面所說的效果了。刪除不需要的結構,添加我們所需的結構。

這個需要進行ast轉化的js代碼其實只有200來行,它依賴多個加密庫。因為行數過多,就不在此處展示了。

 

關於eval的那200行代碼

 

 

 

 

代碼具體實現

百度網盤

鏈接: https://pan.baidu.com/s/1DfMIDDc-SLjd0tVIeyyKOQ  密碼: e2ou

 

 

具體效果

 

 

 

 

總結

這個網站的反爬做的很簡單。算是普通的反爬吧。

花了3個來小時就搞定了。

 

這網站反爬倒是更新很勤快。

 

關於execjs執行速度

可能有人覺得獲取數據好慢,其實有部分時間花在了babel編譯過程中與js代碼的運行上(600毫秒到1.2秒)

只是因為execjs是通過純字符串與解釋器通信的,損耗很大。

並且每次都要新生成一個代碼,繼續編譯執行。(算法部分的代碼其實是不變的)

 

如果真的想提升速度的話,不妨將所有的代碼都放到node環境里(有一定的風險,因為node可以直接刪除文件啥的)

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM