Skip to content

Hook 机制

调试器的核心能力来源于对 JavaScript 原型链的 Hook 技术。本文档解释其工作原理及如何利用这一机制。

为什么需要 Hook?

Tanki Online 的代码经过高度混淆,变量名被重命名为无意义的字符串(如 uids 变成 Z_1a2b3c)。调试器需要:

  1. 找到混淆名 - 解析游戏代码,建立映射表
  2. 拦截属性访问 - 在属性被读/写时触发回调
  3. 修改返回值 - 在属性被读取时返回自定义值

工作原理

第一步:解析混淆映射

调试器扫描游戏的所有 JavaScript 文件,通过正则表达式识别特定模式:

模式 1:toString 重写

javascript
// 游戏代码中的典型模式
.someProperty.toString = function() {
    return "BattleUsers" + this._obfuscatedName
}

调试器捕获此模式,提取出:

  • 类名:BattleUsers
  • 混淆属性名:_obfuscatedName

模式 2:Getter 函数

javascript
.getSomething = function() {
    return this._hiddenVar
}

调试器追踪这种间接引用,找到真正的变量名。

模式 3:构造函数参数

javascript
return "BattleUsers" + (function() {
    // 参数定义
    uids = "..." + this.obf1,
    ranks = "..." + this.obf2
})

第二步:建立双向映射

解析完成后,调试器维护两个映射表:

映射表方向用途
obfToRealMap混淆名 → 真实名翻译显示
realToObfMap真实名 → 混淆名注入修改
javascript
// 示例映射
obfToRealMap = {
  "Z_1a2b3c": "uids",
  "Y_4d5e6f": "ranks"
}
realToObfMap = {
  "uids": "Z_1a2b3c",
  "ranks": "Y_4d5e6f"
}

第三步:Hook 原型属性

使用 Object.definePropertyObject.prototype 上定义混淆属性:

javascript
Object.defineProperty(Object.prototype, obfuscatedName, {
  get: function() {
    // 返回修改后的值(如果启用了注入)
    // 否则返回原始值
  },
  set: function(value) {
    // 存储原始值
    // 触发历史记录
  }
})

关键点:

  • Hook 在 Object.prototype 上,所以任何对象的这个属性被访问时都会触发
  • 使用 WeakMap 存储每个对象的独立值,避免全局污染

第四步:拦截与修改

读取时(get):

javascript
// 用户添加了 BattleUsers.ranks 并设置了注入值 [99,99,99]
// 当游戏读取 this.ranks 时:
get: function() {
  const realValue = store.get(this);  // 真实值
  if (注入已启用 && 注入值有效) {
    return 注入值;  // 返回 [99,99,99]
  }
  return realValue;  // 返回真实值
}

写入时(set):

javascript
set: function(value) {
  store.set(this, value);  // 保存真实值
  // 记录到历史(origValues.push)
  // 如果有自动补全配置,更新输入框
}

注入值的类型

字面量

类型示例
数字100
字符串"hello"
布尔值true
数组[1, 2, 3]
对象{"x": 100, "y": 200}
nullnull

JS 脚本

js: 为前缀,$ 代表原始值:

javascript
// 翻倍
js: return $ * 2

// 修改对象属性
js: return { ...$, custom: "value" }

// 条件逻辑
js: if($ > 100) { return 0 } else { return $ }

// 函数包装
js: return (function(x) { return x + 1 })($)

脚本在 getter 中执行,返回值将替代原始值。


历史记录机制

每次属性被赋值时,调试器会:

  1. 将新值存入 origValues 数组
  2. 更新 origVal(最新值)
  3. 触发 UI 刷新
javascript
// runtimeDebugs 结构
runtimeDebugs = {
  "Z_1a2b3c": {
    key: "BattleUsers.uids",
    origVal: [...],           // 最新值
    origValues: [[...], [...]] // 所有历史值
  }
}

历史记录存储在内存中,刷新页面会丢失。重要数据请使用「保存全部」功能导出。


性能考虑

影响性能的因素

因素影响建议
Hook 数量每个 Hook 增加原型链查找开销只 Hook 需要的参数
历史记录数量大量记录占用内存及时清空不用的记录
高频变量如鼠标位置频繁变化避免长期监听
注入脚本复杂度复杂脚本增加 getter 执行时间保持脚本简洁

最佳实践

javascript
// ❌ 不好:Hook 过多
Object.keys(allParams).forEach(addTarget);

// ✅ 好:只 Hook 需要的
['BattleUsers.uids', 'BattleUsers.ranks'].forEach(addTarget);

// ❌ 不好:高频变量监听
addTarget('Input.mousePosition');

// ✅ 好:使用内核探针代替
TankiDebug.kernel.openInspector();

// ❌ 不好:复杂注入脚本
js: return (function() { /* 100行代码 */ })($)

// ✅ 好:简洁脚本
js: return $ * 2

安全边界

原型链污染风险

Hook 在 Object.prototype 上,如果属性名与内置方法冲突可能产生问题。调试器会:

  1. 过滤以 __ 开头的属性(部分)
  2. 避免 Hook 已存在的原生属性
  3. 使用 WeakMap 隔离实例数据

游戏更新适配

游戏更新可能导致:

  • 混淆名变化 → 重新扫描即可
  • 代码结构变化 → 需要更新解析正则

内核探针的动态发现机制对结构变化的适应性更强。


调试 Hook

查看已 Hook 的属性

javascript
// 控制台输入
console.log(TankiDebug.hookedVars);
// Set { "_obf1", "_obf2", ... }

查看映射表

javascript
console.log(TankiDebug.obfToRealMap);
console.log(TankiDebug.realToObfMap);

手动触发重新扫描

javascript
TankiDebug.runAutoScan();

限制与注意事项

限制说明
仅限客户端只能修改客户端内存,服务端校验的数据无效
依赖解析如果解析失败(代码结构变化),Hook 不会生效
性能开销大量 Hook 可能影响游戏流畅度
无法 Hook 原生属性toStringvalueOf 等不会被 Hook

相关文档

内部技术交流 · 禁止外传