jsHook原理(二)

Object.defineProperty()JavaScript 中精准定义 / 修改对象属性 的核心方法,它能控制属性的读写、枚举、修改、删除等行为,是 Vue2 响应式原理的核心 API。

一、基础语法

Object.defineProperty(obj, prop, descriptor)
 

参数说明

  1. obj:要定义属性的目标对象(必填)
  2. prop:要定义 / 修改的属性名(字符串或 Symbol,必填)
  3. descriptor属性描述符(对象,控制属性行为,必填)

二、属性描述符(核心)

描述符分两类:数据描述符存取描述符,不能混用。

1. 数据描述符(直接定义属性值)

包含 4 个配置项:
配置项 类型 默认值 作用
value 任意 undefined 属性的值
writable 布尔 false 是否允许修改值
enumerable 布尔 false 是否允许被枚举(for...in/Object.keys)
configurable 布尔 false 是否允许删除属性 / 重新配置描述符

2. 存取描述符(通过 getter/setter 控制值)

包含 2 个配置项:
配置项 类型 默认值 作用
get 函数 undefined 读取属性时自动执行,返回值作为属性值
set 函数 undefined 修改属性时自动执行,接收新值作为参数

三、完整使用示例

 

示例 1:基础数据描述符

const person = {};

// 定义属性 name
Object.defineProperty(person, 'name', {
  value: '张三',    // 属性值
  writable: true,   // 允许修改
  enumerable: true, // 允许枚举
  configurable: true// 允许删除/重新配置
});

// 使用属性
console.log(person.name); // 张三
person.name = '李四';     // 可修改
console.log(person.name); // 李四
 

示例 2:只读属性(writable: false)

const person = {};
Object.defineProperty(person, 'age', {
  value: 18,
  writable: false, // 禁止修改
  enumerable: true
});

person.age = 20; // 静默失败(严格模式下报错)
console.log(person.age); // 18(值不变)

示例 3:不可枚举属性(enumerable: false)

// const person = {};
// Object.defineProperty(person, 'age', {
//   value: 18,
//   writable: false, // 禁止修改
//   enumerable: true
// });
//
// person.age = 20; // 静默失败(严格模式下报错)
// console.log(person.age); // 18(值不变)

const person = { name: '张三' };
Object.defineProperty(person, 'id', {
  value: 1001,
  enumerable: false // 不可枚举
});

// 遍历不到 id
for (let key in person) {
  console.log(key); // 只输出 name
}
// 遍历不到id,但是单独输出还是OK的。
console.log(person.id)
console.log(Object.keys(person)); // ['name']

示例 4:不可删除属性(configurable: false) 

const person = {};
Object.defineProperty(person, 'gender', {
  value: '男',
  configurable: true // 可以删除
});

delete person.gender; // 静默失败
console.log(person.gender); // 结果是:undefined

image

const person = {};
Object.defineProperty(person, 'gender', {
  value: '男',
  configurable: false // 禁止删除
});

delete person.gender; // 静默失败
console.log(person.gender); // 男

image

当定义为false的时候,无法删除,可以打印出“男” 

示例 5:存取描述符(get/set,Vue2 核心)

 
最常用场景:监听属性的读写,实现响应式
const person = {};
let _name = '张三'; // 内部变量,存储真实值

// 定义存取器属性
Object.defineProperty(person, 'name', {
  // 读取 person.name 时触发get()函数
  get() {
    console.log('读取了 name 属性');
    return "返回值是:"+_name;
  },
  // 修改 person.name 时触发set()函数
  set(newVal) {
    console.log('修改了 name 属性,新值:', newVal);
    _name = newVal;
  },
  enumerable: true,
  configurable: true
});

// 测试读写
console.log(person.name);
// 输出:读取了 name 属性 → 张三

person.name = '李四';
// 输出:修改了 name 属性,新值:李四

console.log(person.name);
// 输出:读取了 name 属性 → 李四

image

示例6:批量定义属性

单个属性用 Object.defineProperty批量定义Object.defineProperties: 

const obj = {};
Object.defineProperties(obj, {
  name: { value: '张三', enumerable: true },
  age: { value: 18, writable: true }
});

 

四、使用 Object.defineProperty 实现数据劫持

在 JavaScript 逆向与爬虫开发中,Object.defineProperty 是实现 Hook(劫持 / 挂钩) 最核心、最常用的 API。它主要用于两大场景:
 
  1. Hook 属性访问:劫持对象属性的 get(读取)和 set(赋值)。
  2. Hook 函数本身:重写 Object.defineProperty 方法,监控所有属性定义行为。

场景 1:Hook 对象属性(Getter/Setter)

 
用途:监控 / 篡改敏感数据(如 tokencookie__webpack_require__、加密变量)。

示例 1:基础属性劫持(监控读写)

 

// 目标对象
const user = {
  _token: 'original_123' // 下划线通常表示私有
};

// 1. 保存原始值(备份)
const originalToken = user._token;

// 2. Hook:重定义 token 属性
Object.defineProperty(user, 'token', {
  get() {
    console.log('[HOOK] 读取 token:', this._token);
    // 可篡改返回值
    return this._token + '_hacked';
  },
  set(newVal) {
    console.log('[HOOK] 设置 token:', newVal);
    // 可拦截赋值,不写入原始值
    // this._token = newVal; // 注释则阻止赋值
  },
  enumerable: true,
  configurable: true
});

// 测试
console.log(user.token); // [HOOK] 读取 -> original_123_hacked
user.token = 'fake_456'; // [HOOK] 设置 -> fake_456
console.log(user._token); // 仍为 original_123(被拦截)

 image

示例2. 进阶:劫持嵌套对象

 
上面只能劫持一层,对象里包对象就监听不到,所以要递归劫持
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return;
  }

  for (let key in obj) {
    let val = obj[key];

    // 递归劫持子对象
    observe(val);

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        console.log('get: ' + key);
        return val;
      },
      set(newVal) {
        console.log('set: ' + key + ': ' + newVal);
        if (newVal === val) return;
        val = newVal;
        // 新值可能是对象,也要劫持
        observe(newVal);
      }
    });
  }
}

// 测试嵌套对象
const data = {
  user: {
    name: '小红'
  }
};
// 首先要运行observe函数
observe(data);
// 其次,触发对象的嵌套属性
data.user.name = '小兰';

image 

示例 3:Hook 全局变量(如 window 上的关键变量)

 
常用于逆向时定位加密函数入口:
// 监控全局 encryptKey 变量
let _encryptKey;

Object.defineProperty(window, 'encryptKey', {
  get() {
    console.log('[HOOK] 获取 encryptKey', new Error().stack);
    return _encryptKey;
  },
  set(val) {
    console.log('[HOOK] 设置 encryptKey =', val);
    // 断点:赋值时自动断住,查看调用栈
    debugger;
    _encryptKey = val;
  },
  enumerable: true,
  configurable: true
});

// 测试1:设置值
console.log('--- 测试1: 设置值 ---');
window.encryptKey = 'my-secret-key-123';

// 测试2:获取值
console.log('--- 测试2: 获取值 ---');
console.log('当前 encryptKey:', window.encryptKey);

// 测试3:修改值
console.log('--- 测试3: 修改值 ---');
window.encryptKey = 'new-key-456';
console.log('修改后:', window.encryptKey);
image

场景 2:Hook Object.defineProperty 自身

 
用途:监控所有通过此 API 定义属性的行为(反爬常用、定位隐藏属性)。
 

实现:重写原生方法

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Object.defineProperty Hook 测试</title>
</head>
<body>
    <h1>请打开浏览器控制台 (F12) 查看结果</h1>
    <script>
        // ==========================================
        // 1. 你的 Hook 代码 (已做微小调整以适应非严格模式下的 this 指向)
        // ==========================================
        
        // 保存原始方法
        const _defineProperty = Object.defineProperty;

        // 重写 Hook
        Object.defineProperty = function(obj, prop, descriptor) {
            console.log('====================================');
            console.log('🚨 [HOOK] 捕获到 Object.defineProperty 调用!');
            console.log('目标对象:', obj);
            console.log('属性名:', prop);
            console.log('原始描述符:', JSON.parse(JSON.stringify(descriptor))); // 序列化以便查看
            
            // 打印调用栈,方便定位是哪行代码触发了定义
            console.trace('调用堆栈追踪:');
            
            // 篡改逻辑:强制 enumerable 为 true
            if (descriptor && descriptor.enumerable === false) {
                console.warn(`⚠️ [篡改] 检测到属性 "${prop}" 试图被隐藏 (enumerable: false),已强制改为 true!`);
                descriptor.enumerable = true;
            }

            // 调用原始方法,保证功能正常
            // 注意:这里使用 _defineProperty 直接调用,避免 this 指向问题
            return _defineProperty(obj, prop, descriptor);
        };

        // ==========================================
        // 2. 模拟业务代码 (被 Hook 的目标)
        // ==========================================
        
        console.log('--- 开始模拟业务逻辑 ---');

        const user = { name: 'Alice', age: 25 };

        // 场景 A: 正常定义属性 (Hook 会记录,但不篡改)
        Object.defineProperty(user, 'city', {
            value: 'Beijing',
            writable: true,
            enumerable: true
        });

        // 场景 B: 定义隐藏属性 (Hook 会记录,并触发篡改逻辑)
        // 这通常用于某些库试图隐藏内部属性,你的 Hook 将其暴露出来
        Object.defineProperty(user, '_secretToken', {
            value: '12345-ABCDE',
            writable: false,
            enumerable: false // 这里设置为 false,会被你的 Hook 拦截
        });

        // 场景 C: 使用 value 定义普通数据属性
        Object.defineProperty(user, 'role', {
            value: 'Admin',
            writable: true,
            enumerable: true
        });

        console.log('--- 业务逻辑结束 ---');
        console.log('最终对象状态:', user);
        console.log('Object.keys(user):', Object.keys(user)); // 验证 _secretToken 是否被暴露
    </script>
</body>
</html>
image
效果
 
网站任何代码执行 Object.defineProperty 时,都会被你的日志捕获,并可看到完整调用栈,快速定位隐藏属性的定义位置。

场景 3:Hook 原型链(通用 Hook 所有实例)

 
用途:劫持类的所有对象(如 Array、自定义类)。
// Hook 所有 Person 实例的 name 属性
function Person() {}

// 先定义原始属性
Object.defineProperty(Person.prototype, 'name', {
  value: 'default',
  writable: true,
  enumerable: true,
  configurable: true
});

// 再 Hook 原型
let _name = Person.prototype.name;
Object.defineProperty(Person.prototype, 'name', {
  get() {
    console.log('[HOOK] 获取 name');
    return _name;
  },
  set(val) {
    console.log('[HOOK] 设置 name =', val);
    _name = val;
  }
});

// 测试
const p1 = new Person();
p1.name = 'Alice'; // 触发 Hook
console.log(p1.name); // 触发 Hook
image
 
 
 
 
 
 
 
posted @ 2026-04-20 21:52  chenlight  阅读(7)  评论(0)    收藏  举报