Object 静态方法与 Hook 使用场景

Object 的静态方法是 JS 逆向中 Hook 技术的底层基础。理解每个方法的能力边界,直接决定你能不能绕过检测、能不能稳定注入、能不能还原被篡改的环境。 descriptor 两种形式(不可混用): 部分网站检测 fn.toString() 是否包含 native code: 一次性定义多个属性,等同于多次调用 defineProperty。 获取对象自身某个属性的完整描述符对象,是 Hook 前的必要准备动作。 见上方 defineProperty 场景 2,核心就是先用这个方法取到原生 getter/setter,再包一层。 返回对象所有自身属

分享

官方文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
适用版本:ES5+(2026-05-08 核实)

Object 的静态方法是 JS 逆向中 Hook 技术的底层基础。理解每个方法的能力边界,直接决定你能不能绕过检测、能不能稳定注入、能不能还原被篡改的环境。


目录


Object.defineProperty

基础

Object.defineProperty(obj, prop, descriptor)

descriptor 两种形式(不可混用):

字段 数据描述符 访问器描述符
value 属性值
writable 是否可写
get getter 函数
set setter 函数
enumerable 是否可枚举(for...in / Object.keys) 同左
configurable 是否可再次 defineProperty / delete 同左

Hook 场景 1:拦截属性写入(定位赋值来源)

// 监控 window.sign 被谁写入
(function () {
    let _val;
    Object.defineProperty(window, 'sign', {
        configurable: true,
        enumerable: true,
        get() { return _val; },
        set(v) {
            console.log('[sign SET]', v, new Error().stack);
            _val = v;
        }
    });
})();

Hook 场景 2:Cookie 写入监控(带条件断点)

(function () {
    // 先从 Document.prototype 获取原生访问器,避免循环调用
    const desc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
    const nativeGet = desc.get;
    const nativeSet = desc.set;

    Object.defineProperty(document, 'cookie', {
        configurable: true,
        get() { return nativeGet.call(document); },
        set(v) {
            if (v.includes('_token')) debugger; // 条件断点
            console.log('[cookie SET]', v);
            nativeSet.call(document, v);
        }
    });
})();

直接用 document.cookie 取原生访问器会触发自己的 getter,必须从 Document.prototype 上取。

Hook 场景 3:将 configurable 设为 false 来阻止被二次 Hook

// 锁死某个属性,让别人无法再次 defineProperty
Object.defineProperty(window, 'navigator', {
    configurable: false, // 锁死
    get() { return myFakeNavigator; }
});

// 此后再 defineProperty 会抛 TypeError: Cannot redefine property: navigator

Hook 场景 4:伪造 toString 防检测

部分网站检测 fn.toString() 是否包含 [native code]

const _fetch = window.fetch;
window.fetch = function (...args) {
    console.log('[fetch]', args[0]);
    return _fetch.apply(this, args);
};

// 让 toString 返回原生字符串
Object.defineProperty(window.fetch, 'toString', {
    value: () => 'function fetch() { [native code] }',
    configurable: false
});
// 或者更简单:
window.fetch.toString = _fetch.toString.bind(_fetch);

踩坑

  • configurable: false 之后无法再修改描述符,也无法 delete,Hook 注入必须在此之前
  • 数据描述符和访问器描述符不能混合,已经是数据描述符的属性若要改成访问器,必须先 configurable: true
  • writable: false 的属性严格模式下赋值抛错,非严格模式下静默失败

Object.defineProperties

一次性定义多个属性,等同于多次调用 defineProperty

Object.defineProperties(obj, {
    prop1: { value: 1, writable: true, enumerable: true, configurable: true },
    prop2: { get() { return this._p2; }, set(v) { this._p2 = v; } }
})

Hook 场景:批量伪造 navigator 属性

// 补环境时批量设置 navigator 的各个字段
Object.defineProperties(navigator, {
    webdriver: {
        configurable: true,
        get: () => false    // 关键:隐藏自动化标志
    },
    platform: {
        configurable: true,
        get: () => 'Win32'
    },
    languages: {
        configurable: true,
        get: () => ['zh-CN', 'zh', 'en']
    },
    plugins: {
        configurable: true,
        get: () => fakePluginArray   // 伪造插件列表
    }
});

Object.getOwnPropertyDescriptor

获取对象自身某个属性的完整描述符对象,是 Hook 前的必要准备动作。

const desc = Object.getOwnPropertyDescriptor(obj, prop);
// 返回: { value, writable, enumerable, configurable }
// 或:   { get, set, enumerable, configurable }
// 属性不存在时返回 undefined

Hook 场景 1:安全 Hook cookie(取原生访问器)

见上方 defineProperty 场景 2,核心就是先用这个方法取到原生 getter/setter,再包一层。

const cookieDesc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
// cookieDesc.get  → 原生 cookie getter
// cookieDesc.set  → 原生 cookie setter

Hook 场景 2:检测属性是否可被 Hook

function canHook(obj, prop) {
    const desc = Object.getOwnPropertyDescriptor(obj, prop);
    if (!desc) return true; // 不存在,可定义
    return desc.configurable === true; // configurable 为 true 才能重定义
}

if (canHook(window, 'XMLHttpRequest')) {
    // 安全 Hook
} else {
    console.warn('XMLHttpRequest 已被锁死,无法 Hook');
}

Hook 场景 3:完整保存原始描述符用于恢复

// 保存原始描述符
const originalDesc = Object.getOwnPropertyDescriptor(window, 'localStorage');

// 执行 Hook
Object.defineProperty(window, 'localStorage', { /* ... */ });

// 需要恢复时
Object.defineProperty(window, 'localStorage', originalDesc);

Object.getOwnPropertyDescriptors

返回对象所有自身属性的描述符集合(以对象形式)。

const descs = Object.getOwnPropertyDescriptors(obj);
// { prop1: descriptor1, prop2: descriptor2, ... }

Hook 场景 1:深拷贝原型(保留 getter/setter)

普通的 Object.assign 会丢失 getter/setter,只拷贝值:

// 错误做法:getter/setter 会被求值后复制为普通值
const bad = Object.assign({}, sourceProto);

// 正确做法:完整保留描述符
const copy = Object.create(
    Object.getPrototypeOf(sourceProto),
    Object.getOwnPropertyDescriptors(sourceProto)
);

Hook 场景 2:扫描对象,找出所有访问器属性(可能是 Hook 埋点)

function findAccessors(obj) {
    const descs = Object.getOwnPropertyDescriptors(obj);
    return Object.entries(descs)
        .filter(([, d]) => typeof d.get === 'function' || typeof d.set === 'function')
        .map(([key, d]) => ({ key, get: !!d.get, set: !!d.set }));
}

// 检测 window 上有哪些属性被定义了 getter/setter(可能是反调试注入)
console.table(findAccessors(window));

Hook 场景 3:一次性复制整个对象的 Hook 状态到另一个对象

// 将 document 的所有属性描述符整体复制到 fakeDoc(补环境常用)
const fakeDoc = Object.create(null);
Object.defineProperties(fakeDoc, Object.getOwnPropertyDescriptors(document));

Object.getPrototypeOf / Object.setPrototypeOf

Object.getPrototypeOf(obj)      // 等同于 obj.__proto__,但更标准
Object.setPrototypeOf(obj, proto) // 修改原型链

Hook 场景 1:检测对象是否是某个类的实例(绕过 instanceof 检测)

某些网站用 obj instanceof XMLHttpRequest 检测请求是否被代理:

const fakeXHR = {};
Object.setPrototypeOf(fakeXHR, XMLHttpRequest.prototype);
// 现在 fakeXHR instanceof XMLHttpRequest === true

Hook 场景 2:通过原型链 Hook 批量拦截所有实例

Hook 原型上的方法,比 Hook 每个实例更高效:

const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
    console.log('[XHR open]', method, url);
    return _open.apply(this, arguments);
};
// 所有已存在的和未来创建的 XHR 实例都会被拦截

Hook 场景 3:检测 Proxy 对象(Proxy 无法伪装 getPrototypeOf 陷阱之外的行为)

// 某些反爬代码通过原型链检测 Proxy
function isProxy(obj) {
    try {
        // Proxy 在 getPrototypeOf 上可以返回任意值,但 Object.getPrototypeOf 绕过陷阱
        return Object.getPrototypeOf(obj) !== Object.getPrototypeOf(Object.getPrototypeOf(obj));
    } catch {
        return true;
    }
}

Object.create

Object.create(proto, propertiesObject?)
// 创建一个以 proto 为原型的新对象,可选第二参数同 defineProperties 的格式

Hook 场景 1:创建干净的无原型对象作为 Map 使用

// 避免原型上的 hasOwnProperty/toString 等方法干扰 key 查找
const store = Object.create(null);
store['constructor'] = 'someValue'; // 不会有冲突

Hook 场景 2:补环境——创建带正确原型的假对象

// 创建一个假的 navigator,原型链指向真实 Navigator.prototype
const fakeNavigator = Object.create(Navigator.prototype, {
    userAgent: { get: () => 'Mozilla/5.0 ...', configurable: true },
    webdriver: { get: () => false, configurable: true },
    platform:  { get: () => 'Win32', configurable: true },
});

// navigator instanceof Navigator === true,通过原型检测
Object.defineProperty(window, 'navigator', {
    get: () => fakeNavigator,
    configurable: true
});

Hook 场景 3:创建继承了原始对象所有方法的"代理壳"

// 创建一个和 localStorage 同原型的假对象,但可以拦截所有方法
const fakeStorage = Object.create(Storage.prototype);
['getItem', 'setItem', 'removeItem', 'clear'].forEach(method => {
    fakeStorage[method] = function (...args) {
        console.log(`[localStorage.${method}]`, ...args);
        return localStorage[method].apply(localStorage, args);
    };
});

Object.freeze / Object.isFrozen

freeze 让对象同时满足:configurable: falsewritable: false、不可新增属性。冻结是浅冻结,嵌套对象不受影响。

Object.freeze(obj)
Object.isFrozen(obj) // 返回 boolean

Hook 场景 1:保护 console 不被网站覆盖

// 在页面脚本执行前冻结 console,防止网站用 console.log = ()=>{} 屏蔽调试输出
Object.freeze(console);
// 此后 console.log = xxx 静默失败(严格模式抛错)

Hook 场景 2:检测对象是否被冻结(判断是否可以 Hook)

if (Object.isFrozen(window.crypto)) {
    console.warn('crypto 已被冻结,无法直接 Hook,需要用 Proxy 替换整个对象');
} else {
    // 可以直接 defineProperty
}

Hook 场景 3:冻结后的对象只能用 Proxy 整体替换

// 假设 window.performance 被冻结了
const _performance = window.performance;
const proxyPerformance = new Proxy(_performance, {
    get(target, prop) {
        if (prop === 'now') {
            return function () {
                console.log('[performance.now]');
                return target.now();
            };
        }
        return Reflect.get(target, prop);
    }
});
// 注意:window.performance 本身如果 configurable:false 则这一步也做不到
Object.defineProperty(window, 'performance', {
    get: () => proxyPerformance
});

踩坑

  • freeze 只是浅冻结,obj.nested.key = 1 仍然可以修改
  • 冻结后既不能 Hook 属性,也不能 delete,逆向时遇到冻结对象只能 Proxy 整体替换

Object.seal / Object.isSealed

sealfreeze 弱一档:不可新增/删除属性,但已有属性的值仍然可以修改writable 不变,configurable 全设为 false)。

Object.seal(obj)
Object.isSealed(obj)

Hook 场景:seal 后的对象可以修改属性值但不能 defineProperty

const config = { mode: 'normal' };
Object.seal(config);

config.mode = 'hacked'; // 允许,seal 不改变 writable
// Object.defineProperty(config, 'mode', { get: ()=>'hacked' }); // 抛错,configurable:false

逆向时如果遇到 sealed 对象要 Hook,同样只能用 Proxy 整体替换。


Object.preventExtensions / Object.isExtensible

最弱的限制:只阻止新增属性,已有属性的 configurable/writable 不变。

Object.preventExtensions(obj)
Object.isExtensible(obj) // 默认 true,preventExtensions 后变 false

Hook 场景:检测对象是否可以新增属性(决定是否可直接挂载 Hook)

if (!Object.isExtensible(window)) {
    // window 被锁定,不能新增属性(极少见)
    console.warn('window 不可扩展');
}

三种限制的对比:

方法 新增属性 删除属性 修改值 修改描述符
preventExtensions 禁止 允许 允许 允许
seal 禁止 禁止 允许 禁止
freeze 禁止 禁止 禁止 禁止

Object.getOwnPropertyNames

返回对象自身所有属性名(包含不可枚举属性),不包含 Symbol 属性。

Object.getOwnPropertyNames(obj)
// 对比:Object.keys(obj) 只返回可枚举属性

Hook 场景 1:找出对象上所有隐藏的不可枚举属性(侦察目标对象结构)

// Object.keys 看不到不可枚举属性,getOwnPropertyNames 全部暴露
const hidden = Object.getOwnPropertyNames(window).filter(
    key => !Object.prototype.propertyIsEnumerable.call(window, key)
);
console.log('不可枚举属性:', hidden);

Hook 场景 2:遍历原型链上的所有方法,批量 Hook

function getAllMethodNames(obj) {
    const names = new Set();
    let proto = obj;
    while (proto && proto !== Object.prototype) {
        Object.getOwnPropertyNames(proto)
            .filter(n => typeof proto[n] === 'function')
            .forEach(n => names.add(n));
        proto = Object.getPrototypeOf(proto);
    }
    return [...names];
}

// 获取 XMLHttpRequest 原型链上所有方法名
getAllMethodNames(XMLHttpRequest.prototype);
// ['open', 'send', 'abort', 'setRequestHeader', ...]

Hook 场景 3:检测原型上是否存在可疑方法(反调试侦察)

// 检测 Array.prototype 上是否有被注入的方法
const nativeMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'slice', 'map', 'filter', 'reduce'];
Object.getOwnPropertyNames(Array.prototype).forEach(name => {
    if (!nativeMethods.includes(name)) {
        console.warn('[可疑] Array.prototype 上发现非原生方法:', name);
    }
});

Object.getOwnPropertySymbols

返回对象自身所有 Symbol 属性键,普通枚举完全无法发现这些属性。

Object.getOwnPropertySymbols(obj)

Hook 场景 1:找出用 Symbol 隐藏的私有状态

// 某些库用 Symbol 存储内部状态,外部无法直接访问
const syms = Object.getOwnPropertySymbols(targetObj);
syms.forEach(sym => {
    console.log(sym.toString(), targetObj[sym]);
});
// 可能发现:Symbol(kState), Symbol(fetch state) 等内部属性

Hook 场景 2:Hook Symbol.toPrimitive(影响类型转换)

// Symbol.toPrimitive 控制对象被转换为原始值时的行为
const originalFn = targetObj[Symbol.toPrimitive];
targetObj[Symbol.toPrimitive] = function (hint) {
    console.log('[toPrimitive] hint =', hint);
    return originalFn ? originalFn.call(this, hint) : String(this);
};

Hook 场景 3:Hook Symbol.iterator(拦截 for...of / 解构)

const _iter = Array.prototype[Symbol.iterator];
Array.prototype[Symbol.iterator] = function () {
    console.log('[Array iterator] 被调用,length =', this.length);
    return _iter.call(this);
};

Object.assign

将一个或多个源对象的可枚举自身属性浅拷贝到目标对象。

Object.assign(target, ...sources)

Hook 场景 1:快速拷贝原始方法备份

// 保存一批原生方法的引用,防止被污染后丢失
const nativeMethods = Object.assign(Object.create(null), {
    defineProperty: Object.defineProperty.bind(Object),
    getOwnPropertyDescriptor: Object.getOwnPropertyDescriptor.bind(Object),
    freeze: Object.freeze.bind(Object)
});

// 即使后续 Object.defineProperty 被 Hook,依然可以用 nativeMethods.defineProperty

Hook 场景 2:合并 Hook 配置

const defaultConfig = { logGet: true, logSet: true, logCall: false };
const userConfig = { logSet: false, logCall: true };
const config = Object.assign({}, defaultConfig, userConfig);
// { logGet: true, logSet: false, logCall: true }

踩坑

Object.assign 无法复制 getter/setter,只会把 getter 的返回值当成普通值复制过去:

const src = { get foo() { return 'bar'; } };
const dst = Object.assign({}, src);
// dst.foo === 'bar'(普通值),getter 丢失了
// 需要用 Object.getOwnPropertyDescriptors 才能保留 getter/setter

Object.keys / Object.values / Object.entries

只返回对象自身的可枚举属性(不包括原型链,不包括 Symbol)。

Object.keys(obj)    // 返回属性名数组
Object.values(obj)  // 返回属性值数组
Object.entries(obj) // 返回 [key, value] 对数组

Hook 场景 1:遍历请求参数对象,找出签名字段

// 拦截 fetch 后解析请求体,找出 sign / token 字段
window.fetch = async function (url, options) {
    if (options?.body) {
        try {
            const body = JSON.parse(options.body);
            Object.entries(body).forEach(([k, v]) => {
                if (['sign', 'token', '_signature', 'nonce'].includes(k)) {
                    console.log(`[参数签名] ${k} = ${v}`);
                    debugger;
                }
            });
        } catch {}
    }
    return originalFetch(url, options);
};

Hook 场景 2:批量 Hook 某个命名空间下所有方法

// 将 CryptoJS 命名空间下所有加密方法全部 Hook
Object.entries(CryptoJS).forEach(([name, algo]) => {
    if (algo && typeof algo.encrypt === 'function') {
        const _enc = algo.encrypt.bind(algo);
        algo.encrypt = function (...args) {
            console.log(`[CryptoJS.${name}.encrypt]`, args);
            return _enc(...args);
        };
    }
});

Hook 场景 3:检测 window 上被注入的可疑全局变量

// 记录页面加载前 window 的 key 集合
const beforeKeys = new Set(Object.keys(window));

// 等待页面脚本执行后
setTimeout(() => {
    const afterKeys = new Set(Object.keys(window));
    const newKeys = [...afterKeys].filter(k => !beforeKeys.has(k));
    console.log('[新增全局变量]', newKeys);
}, 3000);

Object.hasOwn

Object.hasOwn(obj, prop)  // ES2022,替代 obj.hasOwnProperty(prop)

hasOwnProperty 存在被原型污染的风险(如果对象是 Object.create(null) 的,它根本没有这个方法)。Object.hasOwn 是静态方法,更安全。

Hook 场景:安全检测某个属性是否是对象自有属性

// 不推荐
if (obj.hasOwnProperty('sign')) { ... } // 若 obj 原型被污染则出错

// 推荐
if (Object.hasOwn(obj, 'sign')) { ... }

// 在 Hook 代码中判断请求参数是否包含签名字段
function hasSignField(params) {
    return ['sign', 'token', '_ts', 'nonce'].some(k => Object.hasOwn(params, k));
}

Object.fromEntries

[key, value] 对的可迭代对象(如 Mapentries() 结果)转回对象。

Object.fromEntries(entries)

Hook 场景 1:修改请求参数后重新组装

window.fetch = async function (url, options) {
    if (typeof url === 'string' && url.includes('/api/')) {
        // 解析并修改 URL 查询参数
        const urlObj = new URL(url);
        const params = Object.fromEntries(urlObj.searchParams.entries());

        console.log('[fetch params]', params);

        // 修改某个参数(如替换 timestamp 为固定值方便重放)
        params.ts = '1700000000';
        const newSearch = new URLSearchParams(params).toString();
        url = `${urlObj.origin}${urlObj.pathname}?${newSearch}`;
    }
    return originalFetch(url, options);
};

Hook 场景 2:将 Map 形式的 Header 转为普通对象打印

XMLHttpRequest.prototype.send = function (body) {
    // 将 Headers 对象转为普通对象,方便打印
    if (this._headers) {
        const headersObj = Object.fromEntries(
            Object.entries(this._headers)
        );
        console.log('[XHR headers]', headersObj);
    }
    return _send.call(this, body);
};

综合场景:检测与反检测对抗

场景 A:检测 window 属性是否被 Hook(反向侦察)

网站用来检测逆向者是否注入了 Hook:

// 网站侧:检测 XMLHttpRequest.prototype.open 是否被替换
function detectHook(obj, method) {
    const desc = Object.getOwnPropertyDescriptor(obj, method);
    if (!desc) return false;
    const fn = desc.value || desc.get?.();
    if (!fn) return false;
    // 检测函数 toString 是否包含 native code
    return !/\[native code\]/.test(fn.toString());
}

if (detectHook(XMLHttpRequest.prototype, 'open')) {
    console.warn('open 被 Hook 了!');
}

逆向者的反制——让 toString 返回原生字符串:

const _open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (...args) {
    console.log('[XHR open]', args[1]);
    return _open.apply(this, args);
};

// 关键:伪造 toString
Object.defineProperty(XMLHttpRequest.prototype.open, 'toString', {
    value: () => 'function open() { [native code] }',
    configurable: false  // 锁死,让别人也无法再改
});

场景 B:保存一份干净的 Object 方法引用(防止被污染)

// 在所有脚本执行之前,保存原生方法引用
// 即使后续 Object.defineProperty 被 Hook,这些引用依然指向原生实现
const safeDefineProperty   = Object.defineProperty.bind(Object);
const safeGetDescriptor    = Object.getOwnPropertyDescriptor.bind(Object);
const safeGetProto         = Object.getPrototypeOf.bind(Object);
const safeFreeze           = Object.freeze.bind(Object);
const safeGetOwnPropNames  = Object.getOwnPropertyNames.bind(Object);

// 后续 Hook 操作全部使用 safe 前缀的版本
safeDefineProperty(window, 'myHook', { value: true, configurable: false });

场景 C:检测 defineProperty 本身是否被 Hook(深度防御)

// 如果连 Object.defineProperty 都被 Hook 了,上面所有操作都不可信
function isDefinePropertyHooked() {
    return !/\[native code\]/.test(Object.defineProperty.toString());
}

if (isDefinePropertyHooked()) {
    // 尝试从 iframe 中取干净的原生方法
    const iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    const cleanDefineProperty = iframe.contentWindow.Object.defineProperty;
    document.body.removeChild(iframe);
    // 用 cleanDefineProperty 注入 Hook
}

综合运用上述方法,写出最稳定的 Cookie Hook:

(function () {
    // 1. 保存原生方法引用(防止递归/被污染)
    const _defineProperty        = Object.defineProperty.bind(Object);
    const _getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor.bind(Object);

    // 2. 从 Document.prototype 取原生访问器(不能从 document 实例取,否则触发自己)
    const cookieDesc = _getOwnPropertyDescriptor(Document.prototype, 'cookie');
    if (!cookieDesc || !cookieDesc.configurable) {
        console.warn('[Cookie Hook] 无法注入,cookie 描述符不可配置');
        return;
    }
    const nativeGet = cookieDesc.get;
    const nativeSet = cookieDesc.set;

    // 3. 注入 Hook
    _defineProperty(document, 'cookie', {
        configurable: true,
        enumerable: true,
        get() {
            return nativeGet.call(document);
        },
        set(v) {
            // 4. 条件断点:只在写入目标 cookie 时断下
            if (v.startsWith('_acl') || v.includes('sign=')) {
                console.log('[Cookie SET 命中]', v, new Error().stack);
                debugger;
            }
            nativeSet.call(document, v);
        }
    });

    console.log('[Cookie Hook] 注入成功');
})();


最佳实践

优先使用 Object.defineProperty 做精确属性 Hook:Hook 单个属性时,Object.defineProperty 可以设置 get/set 拦截器,并控制 configurable/enumerable/writable,比直接赋值更细粒度,也更难被检测到。

Object.getOwnPropertyDescriptor 在 Hook 前保存原始描述符

const orig = Object.getOwnPropertyDescriptor(navigator, 'userAgent')
Object.defineProperty(navigator, 'userAgent', {
  get: () => { console.log('[UA accessed]'); return orig.get.call(this) }
})

先保存再 Hook,既能监控访问又保留原始行为,避免 Hook 后功能异常。

Object.freeze 保护 Hook 代码自身:注入 Hook 后,用 Object.freeze(hookedObject) 防止目标代码用 delete/赋值覆盖 Hook;Object.isFrozen 检测是否已被保护。

Object.getPrototypeOf 追踪原型链:混淆代码有时将方法定义在原型上而非实例上,用 Object.getPrototypeOf(obj) 找到实际原型,然后 Hook 原型上的方法,比 Hook 实例属性影响范围更广。

Object.entries + Object.fromEntries 做深拷贝前的检测:检测目标对象有哪些可枚举属性时,Object.entries(obj)for...in 更安全(不包含原型链属性);Object.fromEntries 可以快速重建过滤后的对象。


常见陷阱

陷阱:Object.freeze 后 Hook 失效

现象: 尝试 Object.defineProperty Hook 某属性,但静默失败(严格模式下抛 TypeError)。
原因: 目标对象或其原型已被 Object.freeze() 保护,被冻结后所有属性修改都被阻止。
解决: 检查 Object.isFrozen(obj);若已冻结,需要替换整个对象引用(window.targetObj = new Proxy(originalObj, {...}))而非修改已冻结对象的属性。

陷阱:configurable: false 的属性无法用 defineProperty 重定义

现象: 对某属性 defineProperty 抛出 TypeError: Cannot redefine property
原因: 属性描述符中 configurable: false 禁止重新定义;原生浏览器 API(如 navigator.userAgent)通常都是 configurable: false
解决: 先用 Object.defineProperty 替换父对象(如整个 navigator);或使用 Proxy 代理父对象而非直接修改属性。

陷阱:Object.assign 只做浅拷贝,深层属性 Hook 失效

现象:Object.assign 创建目标对象副本后再 Hook,但原始对象的深层属性访问没被拦截。
原因: Object.assign 只复制第一层可枚举属性,嵌套对象还是同一引用;Hook 了副本的属性,不影响原始对象的访问路径。
解决: Hook 需要在原始对象引用上操作;或用深拷贝(JSON.parse(JSON.stringify(obj)))但注意会丢失函数属性;或直接 Hook 原始对象。


参见

hook
代码片段大全
补环境
js逆向调试技巧
Proxy对象与Hook

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog