JavaScript 函数 Hook:拦截 fetch、XHR、Function.prototype.call 与 setInterval

Hook 的核心模式

JavaScript 函数 Hook 的通用模式:

const original = window.targetFunction;

window.targetFunction = function (...args) {
    // 前置处理
    console.log('before call:', args);

    const result = original.apply(this, args);

    // 后置处理
    console.log('after call:', result);

    return result;
};

apply(this, args) 保持原始的 this 上下文,...args 透传所有参数。

Hook fetch

const rawFetch = window.fetch;

window.fetch = async function (...args) {
    console.log('[fetch]', args[0]);

    const response = await rawFetch.apply(this, args);

    // clone() 避免 body 被消费后无法再读
    const clone = response.clone();
    clone.text().then(body => {
        console.log('[fetch response]', body.slice(0, 200));
    });

    return response;
};

Hook XMLHttpRequest

const RawXHR = window.XMLHttpRequest;

window.XMLHttpRequest = function () {
    const xhr = new RawXHR();

    const rawOpen = xhr.open.bind(xhr);
    xhr.open = function (method, url, ...args) {
        console.log('[XHR]', method, url);
        return rawOpen(method, url, ...args);
    };

    const rawSend = xhr.send.bind(xhr);
    xhr.send = function (body) {
        xhr.addEventListener('load', function () {
            console.log('[XHR response]', xhr.responseText.slice(0, 200));
        });
        return rawSend(body);
    };

    return xhr;
};

Hook Function.prototype.call(监听所有函数调用)

(function () {
    const rawCall = Function.prototype.call;

    Function.prototype.call = function (...args) {
        // this 就是被调用的函数
        if (this.name === 'targetFunction') {
            console.log('[call hook]', this.name, args);
        }
        return rawCall.apply(this, args);
    };
})();

影响范围极大,生产环境谨慎使用,建议只在调试时临时注入。

Hook 某个对象的方法

const original = window.api.submit;

window.api.submit = function (...args) {
    console.log('[hook api.submit]', args);
    const result = original.apply(this, args);
    console.log('[result]', result);
    return result;
};

Hook setInterval(拦截 debugger)

部分网页在 setInterval 里运行 debugger 语句来阻止调试:

(() => {
    const rawSetInterval = window.setInterval;
    const rawClearInterval = window.clearInterval;
    const fakeTimers = new Set();
    let fakeId = 1_000_000;

    function shouldBlock(fn) {
        try {
            let code = typeof fn === 'function'
                ? Function.prototype.toString.call(fn)
                : String(fn);

            if (/\bdebugger\b/.test(code)) {
                console.warn('[blocked debugger interval]');
                return true;
            }
        } catch (e) {}
        return false;
    }

    window.setInterval = function (fn, delay, ...args) {
        if (shouldBlock(fn)) {
            fakeTimers.add(++fakeId);
            return fakeId;
        }
        return rawSetInterval(fn, delay, ...args);
    };

    window.clearInterval = function (id) {
        if (fakeTimers.has(id)) {
            fakeTimers.delete(id);
            return;
        }
        return rawClearInterval(id);
    };
})();

fakeId 返回一个虚假的 timer ID,避免调用 clearInterval 时报错。

Hook WebAssembly.instantiate

const rawInstantiate = WebAssembly.instantiate;

WebAssembly.instantiate = async function (...args) {
    const result = await rawInstantiate.apply(this, args);

    // 拦截导出函数
    const exports = result.instance.exports;
    for (const key of Object.keys(exports)) {
        if (typeof exports[key] === 'function') {
            const original = exports[key];
            exports[key] = function (...callArgs) {
                console.log(`[wasm] ${key}(`, callArgs, ')');
                return original.apply(this, callArgs);
            };
        }
    }

    return result;
};

油猴中的注入时机

// ==UserScript==
// @run-at document-start
// ==/UserScript==

document-start 是最早的时机,在页面任何脚本执行之前运行,适合 hook 全局 API。如果用 document-enddocument-idle,页面原始代码可能已经运行了。