爬虫——夜幕团队JS逆向系列课

// chrome dev-tools console
console.count() console.table()
Copy(var)复制到粘贴板
自带jQuery中操作符: $、$$、$x(xpath)

js语法-函数

变量定义:var在函数内部起作用,const、let 块级作用域
变量提升:扫描整个函数体语句,将所有声明提升到函数顶部
全局作用域:如不使用 var、const、let 关键字声明,变量会绑定到全局 window 上
块级作用域:for、while 等语句内

js进阶

  1. 事件循环
    1. 宏任务(Macrotasks)与微任务(Microtasks)概述
      • 宏任务
        • 在 JavaScript 中,宏任务是指那些被安排到执行队列中按照顺序执行的任务。常见的宏任务包括setTimeoutsetIntervalI/O操作、script(全局任务)等。
        • 例如,当使用setTimeout函数时,它会将回调函数作为一个宏任务添加到事件循环的任务队列中。
      • 微任务
        • 微任务是在当前任务执行结束后立即执行的任务。微任务队列中的任务优先级高于宏任务队列中的任务。常见的微任务包括Promisethen/catch/finally方法、process.nextTick(在 Node.js 中)等。
        • 当一个Promise被解决(resolved)或者被拒绝(rejected)时,其对应的thencatch或者finally中的回调函数会被作为微任务添加到微任务队列中。
    2. Node.js 事件循环
      • 事件循环的阶段
        • Timers 阶段:这个阶段执行setTimeoutsetInterval的回调函数,这些回调函数在定时器到期时被添加到这个阶段的任务队列中。
        • I/O callbacks 阶段:处理上一轮循环中除了close事件之外的异步 I/O 操作的回调函数。
        • Idle, Prepare 阶段:内部使用,主要用于准备工作。
        • Poll 阶段:这个阶段是事件循环的核心部分。它有两个主要功能:
          • 如果没有定时器到期,并且没有即将被执行的setImmediate,它会阻塞在这里等待 I/O 事件的返回,并处理这些 I/O 事件对应的回调函数。
          • 如果有定时器到期,它会处理这些定时器对应的回调函数。
        • Check 阶段:执行setImmediate的回调函数。
        • Close callbacks 阶段:执行close事件的回调函数,比如server.close后的回调函数。
      • 宏任务与微任务在事件循环中的执行顺序
        • 在每个阶段内,首先执行该阶段对应的宏任务。
        • 在宏任务执行完之后,会立即执行微任务队列中的所有微任务。例如,在Timers阶段,如果有setTimeout的回调函数(宏任务)执行,在这个宏任务执行完后,如果有微任务(如Promisethen回调),会先执行微任务队列中的微任务,然后再进入下一个阶段(如I/O callbacks阶段)。
  2. 原型链
    ES6引入class关键字后用的就不多,但现有项目中很多都没用es6,或用bable转为es5

    访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 会自动在其原型对象上查找。如果原型对象也没有,就会继续在原型对象的原型上查找,这样一直向上查找直到找到该属性或方法或者到达原型链的顶端(Object.prototype
  3. 异步编程
    回调函数:简单、回调地狱
    异步操作对象:resolve reject then catch,可读性不高
    async await,多个并行操作要用 Promise.all 执行
  4. 浏览器存储
    //cookie
    通过document.cookie属性来设置,格式为name=value; expires=expiration_time; path=domain_path; domain=domain_name; secure。
    例如:document.cookie = "username=John Doe; expires=Thu, 18 Dec 2024 12:00:00 UTC; path=/";
    同样使用document.cookie,它会返回一个包含所有当前页面可用 cookies 的字符串,需要手动解析。
    将expires设置为过去的时间即可删除。
    
    //Local Storage
    localStorage.setItem('key', 'value')
    const theme = localStorage.getItem('theme') //如果不存在则返回null
    localStorage.removeItem('key')
    
    //Session Storage  数据在页面会话期间(浏览器打开直到关闭)有效,关闭浏览器后数据丢失,大小限制与localStorage类似
    sessionStorage.setItem('key', 'value')
    sessionStorage.getItem('key')
    sessionStorage.removeItem('key')
    
    // IndexedDB
    const request = indexedDB.open('myDatabase', 1);
    request.onupgradeneeded = function(event) {
        const db = event.target.result;
        const objectStore = db.createObjectStore('customers', { keyPath: 'id' });
    };

  5. 跨域
  6. Webpack 打包

python 调用 JS

库:PyV8 Js2Py PyExecJS(特殊编码可能导致报错,可通过删除或Base64编码处理) PyminiRacer、 Selenium Pyppeteer、使用nodejs开放一个执行js文件的微服务

无限 debugger

  1. 禁用所有断点,或禁用条件断点
  2. 使用devtools的override功能
  3. 使用代理修改页面内容,如 Fiddler Script, mitmProxy
  4. 函数调用前,在控制台重写函数 block = function () {}
  5. reres 插件

快速定位

明文搜索、sources查看、xhr请求追溯、hook追溯、调用栈跟踪、全局断点、dom断点、主动debugger、warning error 追溯、内存优化分析、事件监听等

搜索

dev-tools:CTRL-F、CTRL-SHIFT-F、Network 界面 Filter
Fiddler:Ctrl-F

断点

xhr、DOM、EVENT、自定义

事件监听器

hook

  1. json
    var my_stringify = JSON.stringify;
    JSON.stringify = function (params) {
        console.log("yemu", params);
        return my_stringify(params);
    };
    var my_parse = JSON.parse;
    JSON.parse = function (params) {
        console.log("yemu", params);
        return my_parse(params);
    };
  2. cookie
    var cookie_cache = document.cookie;
    Object.defineProperty(document, 'cookie', {
        get: function () { },
        set: function (val) {
            console.log('setting cookie', val);
            var cookie = val.split(";")[0];
            var ncookie = cookie.split("=");
            var flag = false;
            var cache = cookie_cache.split(";");
            cache = cache.map(function (a) {
                if (a.split("=")[0] === ncookie[0]) {
                    this.value = val;
                    return cookie_cache;
                }
                return a;
                flag = true;
                return cookie;
            });
            cookie_cache = cache.join(";");
            if (!flag) {
                cookie_cache += cookie + ";";
            }
        }
    });
  3. window attr
  4. eval / Function
    window._cr_eval = window.eval;
    var myeval = function(src) {
        console.log(src);
        console.log("======================= eval end =======================");
        return window._cr_eval(src);
    };
    var myeval = myeval.bind(null);
    myeval.toString = window._cr_eval.toString;
    Object.defineProperty(window, 'eval', { value: myeval });
    // 尝试执行 eval('1+2')
    
    window._cr_fun = window.Function;
    var myfun = function() {
        var args = Array.prototype.slice.call(arguments, 0, -1).join(",");
        var src = arguments[arguments.length - 1];
        console.log(src);
        console.log("======================= Func end =======================");
        return window._cr_fun.apply(this, arguments);
    };
    myfun.toString = function() { return window._cr_fun + ""; };
    Object.defineProperty(window, 'Function', { value: myfun });
    // 尝试执行 let a = new Function("return 1 + 2");
  5. websocket
    Websocket.prototype.sendA = Websocket.prototype.send;
    Websocket.prototype.send = function(data) {
        console.info('Hook websocket', data);
        return this.sendA(data);
    };
  6. tampermonkey hook https://blog.csdn.net/Yy_Rose/article/details/124216720 
    hook
     // ==UserScript==
    // @name         HookBase64
    // @namespace    https://login1.scrape.center/
    // @version      0.1
    // @description  Hook Base64 encode function
    // @match        https://login1.scrape.center/
    // @grant        none
    // ==/UserScript==
     
    (function() {
        'use strict';
        function hook(object, attr){
            var func = object[attr]
            object[attr] = function(){
                console.log('hooked', object, attr)
                var ret = func.apply(object, arguments)
                debugger
                return ret
            }
        }
        hook(window, 'btoa')
    })();
    hook
     // ==UserScript==
    // @name         Hook global
    // @namespace    http://tampermonkey.net/
    // @include      *
    // @grant        none
    // @run-at       document-start
    // ==/UserScript==
    
    (function() {
        'use strict';
        //全局变量 监控
        var t = window._t;
        var window_flag = '_t'; // 要监控的值
        var window_value = window[window_flag];
        Object.defineProperty(window, window_flag, { // window 对象上的值
            get: function() {
                console.log('Getting window._t',window_value);
                return t;
            },
            set: function(val) {
                console.log('Setting window._t', val);
                debugger;
                t = val;
                return t;
            }
        });
    })();
    检测是否存在可疑的加密函数
     // ==UserScript==
    // @name         HOOK 遍历
    // @namespace    http://tampermonkey.net/
    // @version      0.1
    // @description  day day up
    // @author       FY
    // @include      *
    // @grant        none
    // @run-at       document-end
    // ==/UserScript==
    
    (function() {
        'use strict';
        !function () {
            'use strict';
            var source = ['DeCode','EnCode','decodeData','base64decode','md5','decode','btoa','MD5','RSA','AES','CryptoJS','encrypt','strdecode',"encode",'decodeURIComponent','_t','JSON.stringify','String.fromCharCode','fromCharCode'];
            console.log("开始测试是否有解密函数");
            let realCtx, realName;
            function getRealCtx(ctx, funcName) {
                let parts = funcName.split(".");
                let realCtx = ctx;
                for(let i = 0; i < parts.length - 1; i++) {
                    realCtx = realCtx[parts[i]];
                }
                return realCtx;
            }
            function getRealName(funcName) {
                let parts = funcName.split(".");
                return parts[parts.length - 1];
            }
            function test(ctx) {
                for(let i = 0; i < source.length; i++) {
                    let f = source[i];
                    let realCtx = getRealCtx(ctx, f);
                    let realName = getRealName(f);
                    let chars = realCtx[realName];
                    if (chars != undefined){
                        console.log("发现可疑函数:", f);
                        console.log(chars);
                        console.log("---------------------");
                    }else{
                        console.log("未发现:", f);
                    }
                }
            }
            test(window);
        }();
    })();
    获取返回值、内部调用函数名
     // ==UserScript==
    // @name         HOOK ALL end
    // @namespace    http://tampermonkey.net/
    // @include      *
    // @grant        none
    // @run-at       document-end
    // ==/UserScript==
    
    (function() {
        'use strict';
        var source = ['DeCode','EnCode','decodeData','base64decode','md5','decode','btoa','MD5','RSA','AES','CryptoJS','encrypt','strdecode',"encode",'decodeURIComponent','_t','JSON.stringify','String.fromCharCode','fromCharCode'];
        console.log("开始测试是否有解密函数");
        let realCtx, realName;
        function getRealCtx(ctx, funcName) {
            let parts = funcName.split(".");
            let realCtx = ctx;
            for(let i = 0; i < parts.length - 1; i++) {
                realCtx = realCtx[parts[i]];
            }
            return realCtx;
        }
        function getRealName(funcName) {
            let parts = funcName.split(".");
            return parts[parts.length - 1];
        }
        function hook(ctx, funcName, level, originFunc) {
            ctx[funcName] = function(a){
                console.log("level:" + level + " function:" + funcName,a);
                console.log(originFunc.toString());
                console.log(originFunc.toString);
                debugger;
                return originFunc(a);
            };
        }
        function test(ctx, level) {
            for(let i = 0; i < source.length; i++) {
                let f = source[i];
                let realCtx = getRealCtx(ctx, f);
                let realName = getRealName(f);
                let chars = realCtx[realName];
                hook(realCtx, realName, level, chars);
            }
        }
        test(window, 1);
    })();
    ///////////////////////////
    // ==UserScript==
    // @name         HOOK 二层函数名 end
    // @namespace    http://tampermonkey.net/
    // @include      *
    // @grant        none
    // @run-at       document-end
    // ==/UserScript==
    
    (function() {
        'use strict';
        var source = ['decodeData','base64decode','md5','decode','btoa','MD5','RSA','AES','CryptoJS','encrypt','strdecode',"encode",'decodeURIComponent','_t','JSON.stringify','String.fromCharCode','fromCharCode'];
        console.log("开始测试是否有解密函数");
        let realCtx, realName;
        function getRealCtx(ctx, funcName) {
            let parts = funcName.split(".");
            let realCtx = ctx;
            for(let i = 0; i < parts.length - 1; i++) {
                realCtx = realCtx[parts[i]];
            }
            return realCtx;
        }
        function getRealName(funcName) {
            let parts = funcName.split(".");
            return parts[parts.length - 1];
        }
        function hook(ctx, funcName, level, originFunc) {
            ctx[funcName] = function(a){
                console.log("level:" + level + " function:" + funcName,a);
                let regexp = / [\S]*\(.*\)\;/g;
                let match = originFunc.toString().match(regexp)
                console.log(match);
                debugger;
                return originFunc(a);
            };
        }
        function test(ctx, level) {
            for(let i = 0; i < source.length; i++) {
                let f = source[i];
                let realCtx = getRealCtx(ctx, f);
                let realName = getRealName(f);
                let chars = realCtx[realName];
                hook(realCtx, realName, level, chars);
            }
        }
        test(window, 1);
    })();

可通过tampermonkey、执行js前打断点的方式进行注入

分析

Event、网络、XMLHttpRequest 调用栈

nodejs模拟chrome环境

// npm install jsdom
const jsdom = require('jsdom');
const { JSDOM } = jsdom;

const dom = new JSDOM(`<!DOCTYPE html>`);
global.window = dom.window;
global.document = dom.window.document;

// 定义 window.atob
global.window.atob = function (encoded) {
    // 根据 atob 的实际功能实现逻辑,如果只是模拟一个固定值返回,可以这样:
    return 'decoded_value';
};

// 定义 screen 对象
global.screen = { width: 1920, height: 1080 };

// 定义 navigator 对象
global.navigator = {
    userAgent: 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/[your_version] Safari/537.36',
};

处理js代码混淆

查找加密参数:

  1. 通过抓包找到加密参数
  2. 全局搜索参数
  3. 查看网络面板 Initiator
  4. xhr 断点调试
  5. hook
  6. 分析加密逻辑

颜文字、符号加密 JSFuck 等原理:

解决方案:

  1. 直接粘贴到console执行即可获得原js,通过VM查看
  2. 或删除最后一个表情('_');/括号(),添加 toString() 方法在控制台执行
  3. 删除最后的 (); 删除,执行得到代码
  4. 如果不是以()结尾,那么将最后一对括号内部的符号复制并执行,得到原生代码。剩余代码大概率为 eval 函数的编
  5. eval开头的代码,将 eval 改为 alert console.log

平坦化混淆

  1. 全局观察
    是否有 dom操作、纯计算循环体、try-catch异常捕获
  2. 整体分析与载入
    断点定于 while开头、try代码第一行、while整体取出构造原始函数
  3. 构造函数
    通过报错补充函数或数据

通过 AST 增强代码可读性 https://astexplorer.net/ ,npm的 recast 包将js转为AST语法树。平坦流将代码转换为 while-switch-case 格式,通过 consolelog 打印加AST语法分析。

CSS反爬

  1. 字体
    通过font-family指定特殊字体,在页面中不可见的unicode在指定字体中映射到可读字符。爬虫只能爬取到unicode而不是可读字体
    应对:下载woff字体转为tff字体,用字体编辑器确定其字符与unicode间映射关系,替代映射得到正确数据
    有些网站动态生成woff,很难自动化绕开
  2. 背景
    数据(通常是数字)通过雪碧图(Sprite)通过背景偏移展示,抓取时看不到实际值而是图片背景
    下载图片,手动检查获取 background-position 偏移量与实际值间映射关系,爬虫获取偏移值并转化为实际值
  3. 伪类
    不直接展示内容,而是通过伪类content属性展示值。难在获取指定元素的伪类属性
    利用puppeteer或Selenium获取伪类,解析css
    /*
    .valuable-content::before {
        content: "hiding content"
    }
    */
    const el = document.querySelector('.valuable-content')
    const styles = getComputedStyle(el, 'before')
    console.log(styles.content)
  4. 元素定位
    利用绝对定位(position: absolute)将某个数字或字符将原字符通过一定偏移量替换。替换的字符是随机的,直接抓取获得错误信息。
    计算出替换的元素的偏移量,与被替换元素对比,还原实际值

    const elPr = document.querySelector('.mb-10.b-airfly:nth-child(1).fixprice.prc_wp');
    let strArr = Array.from(elPr.querySelectorAll('b:first-child > i')).map(el => el.innerText);
    // 替换元素
    elPr.querySelectorAll('b:not(:first-child)').forEach(el => {
        // 偏移
        const left = Number(getComputedStyle(el).left.replace('px', ''));
        // 替换
        strArr[strArr.length + left / 16] = el.innerText;
    });
    console.log(strArr.join(''));
  5. 字符切割
    将字符串用标签分割,内联块级(inline-block)可一行展示,还可混有不显示标签(display:none)
    拼接 innerText 并忽略 display:none 标签

    const elIp = document.querySelector('.ip');
    let str = '';
    const elList = elIp.querySelectorAll('*:not([style="display: none;"])');
    elList.forEach((el, i) => {
        if (i === elList.length - 1) return;
        str += el.innerText;
    });
    console.log(str);

应对策略:

  1. 通过调试工具人工查看CSS样式
  2. 判断CSS反爬类型
  3. 思考应对措施,css反爬非常多变,没有固定套路

 

posted @ 2025-09-02 17:51  某某人8265  阅读(25)  评论(0)    收藏  举报