zoukankan      html  css  js  c++  java
  • 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可以直接删除文件啥的)

     

     

  • 相关阅读:
    杂文: 日剧《轮到你了》第7集中的组合数学问题
    CF 板刷总结
    【题解】P2324 [SCOI2005]骑士精神
    【模板】 $ ext{K}$ 短路
    P4554 小明的游戏
    [题解]P1856 [USACO5.5]矩形周长Picture
    洛谷P2243 电路维修
    【题解】洛谷P3660 [USACO17FEB]Why Did the Cow Cross the Road III
    【题解】P1119 灾后重建
    集福卡活动
  • 原文地址:https://www.cnblogs.com/re-is-good/p/api_study_ast_node_js_babel.html
Copyright © 2011-2022 走看看