浏览器调试与常用 Hook

2026-03-22

Hook 相关内容参考视频 https://www.bilibili.com/video/BV1gQ4mzMEA4/open in new window

定位加密参数

使用请求重放,一步步移除cookie、headers、query等参数中无关紧要内容。定位最终的加密参数

注意响应是否存在 set-cookie。一些 cookie 与加密 js 一一对应

断点

根据不同情况,可选用不同断点

dom断点

通过 ajax 请求拿到数据后,渲染到页面中时可通过 dom 断点定位页面元素被修改的代码

Elements tap ->指定标签右键 -> Break on(发生中断的条件)

事件监听断点

在调试时常用的 Event Listener Breakpoints 事件监听器断点 介绍

  • Script断点: 读取到脚本时暂停 Script 脚本
  • Timer 断点: 设置或清除计时器
  • Control 断点: 元素焦点,元素失焦,submit提交数据
  • Mouse 断点: 鼠标操作断点

xhr断点

发送指定请求时的断点,可通过请求地址暂停到指定请求之前

源码断点

源码断点常用的是以下几种

  • 普通代码断点:蓝色

    • 搜索关键词: 如 sign= , sign", sign:
    • open 函数: xhr请求初始化函数
    • etc.
  • 条件断点:橙色。右键添加条件断点。运行到条件断点处,会执行设置的内容。当设置内容返回为true时中断

    • 编写条件,返回true时断下

      • 参数数量为 N 时断下 arguments.callee.length === N
      • window 上绑定变量用于控制是否启动断点后,启动断点 window.enableBreakpoints === true
    • 可以编写普通代码(无返回值代码,如Statement),则会执行而不中断(代码不返回true时,不会触发断点机制)

      • Eg1: 修改断点行的参数值

        var id = 1;
        var a = id;  // 此行加断点,编写 id = 999,则此时id被修改为999,a会被赋值id的当前值,即999
        
      • Eg2: 计时

        for (let i = 1; i < 9999999; i++) {  // 断点内容  console.time('calc time')
            window.getComputedStyle(document.body)
        }  // 断点内容  console.timeEnd('calc time')
        
  • 日志断点:粉色。自动在控制台输出断点所设置的内容(监控当前代码位置,某个变量的值,即使当前代码为使用该变量)。通常设置为字符串拼接变量,例如 "当前i的值为" + i

调用堆栈

在调试时,常看的调用堆栈信息分为 Network 数据包的 Initiator 启动器 和代码断点的 Call Stack 调用栈

两个地方堆栈显示都遵循了上方是栈顶的策略。即最下方的是先触发的,最上方的是最后调用的

禁控制台

开源项目 disable-devtoolopen in new window 会检测控制台情况并触发以下行为

  • 禁用 F12 与右键,减少控制台打开的方式
  • 检测到控制台打开后,调用 window.close() 关闭当前标签页
  • 检测到控制台打开后,跳转到其他安全页面
  • 检测到控制台打开后,反复使用 console.clear() 清空控制台输出
  • 检测到控制台打开后,反复使用 console.table() 控制台输出大量内容
  • 检测到控制台打开后,反复使用 window.history.go()window.history.back() 进行跳转
  • etc(其他行为可详见项目说明)

处理手段

  • hook window.close() 为空函数,防止自动关闭
  • hook window.onbeforeunload = () => {debugger;return false}, 在堆栈中定位跳转代码,移除跳转代码并 overrides
  • hook console.clear() 为空函数,防止清楚控制台输出
  • hook console.table()
  • hook window.historygoback 为空方法

过 debugger

debugger 干扰调试通过与定时器配合实现反复触发 debugger 但不影响主进程执行业务逻辑

常规的 debugger 可通过断点不暂停过掉。

出现一些debugger无法通过断点不暂停过掉的情况,如: 使用 Function 构造 debugger

// 通过自定义函数构造器,调用 Function 构造 debugger 并调用
func.constructor('debugger').call() 
// 通过匿名函数
(function(){}.constructor('debugger').call())

可在发生 debugger 时查看堆栈,定位触发 debugger 的代码块(函数)。在执行该代码块前下断点。刷新后重写该代码,再放行(HOOK时机: 函数定义之后,执行之前,进行重写)

或通过 hook 方式过 debugger (修改内置可能会被检测)。以下是 hook 代码

  • 重写定时器
    _setInterval = setInterval
    setInterval = function(a, b){
        // 判断无 debugger 关键字,再正常设置定时器
        if (a.toSting().indexOf('debugger') === -1) {
            return _setInterval(a, b)
        }
    }
    
  • 重写构造器
    _constructor = Function.prototype.constructor
    Function.prototype.constructor = function() {
        if(arguments[0] === 'debugger') {
            // 什么都不做
        } else {
            return _constructor.apply(this, arguments)
        }
    }
    
  • 重写 eval: 处理使用 eval 进行 debugger 的方式
    _eval = eval
    eval = function(s) {
        // 检测到 eval 内容不存在 debugger
        if (s.indexOf('debugger') === -1) {
            return _eval(s)
        }
    }
    

内存爆破

通过格式化检测,代码执行时间差,浏览器指纹等方式,检查代码是否正常运行或处于调试状态。再通过死循环进行内存爆破

格式化检测手段: 正则或 toString ,检测目标函数是否被格式化

Hook

可用于快速定位指定函数或属性的调用点

注意点:

  • Hook 时机: Hook 指定函数,需在函数定义之后,函数使用之前。属性也是如此。如

    • 浏览器内置函数或属性: 已经在浏览器中被定义,因此可在新建标签页时进行 Hook
    • 代码中定义的函数或属性: 通过断点,使代码停在该函数或属性定义之后(防止被重写回原本内容),函数调用之前(防止被调用的依然是原本内容),再进行 Hook
  • Hook 内置方法时,注意使用 toString保护代码 对修改后的方法进行保护,防止 toString 检测

Hook 模版

  • 函数
    _<函数名> = <函数名>
    <函数名> = function(argument) {
        // 需要增加的操作,如 打印,debugger 等
    
        // 正常返回
        return _<函数名>.apply(argument)
    }
    // 重写原型链,避免原型链检测 Hook
    // <函数名>.prototype.xxx = xxx
    <函数名>.toString = function(){return 'function <函数名>() { [naive code] }'}
    
  • 对象的属性: 使用 Object.defineProperty 方法。注意: 此方法只能绑定一次。如果网站使用此函数定义对象属性,则无法再通过该函数 Hook
    <对象名>._<属性名> = <对象名>.<属性名>
    Object.defineProperty(<对象名>, '<属性名>', {
        get: function() {
            // 需要增加的操作,如 打印,debugger 等
    
            // 正常返回
            return <对象名>._<属性名>
        },
        set: function (value) {
            // 需要增加的操作,如 打印,debugger 等
    
            return ...
        }
    })
    

Tips: 如果 Hook 的内容是对象在原型链上的内容,如字符串、数组、XHR 的相关函数,为了使该函数顺利执行,可在返回时使用 this

Array.prototype._pop = Array.prototype.pop
Array.prototype.pop = function() {
    debugger;
    return this._pop()
}
// 此处使用 toString 保护
safeFunction(Array.prototype.pop)
// 测试是否正常
console.log(Array.prototype.pop.toString())
arr = [1,2,3,4,5]
arr.pop(6)

XMLHttpRequest.prototype._setRequestHeader = XMLHttpRequest.prototype.setRequestHeader
XMLHttpRequest.prototype.setRequestHeader = function(){...}

油猴脚本

可通过油猴脚本将 js 代码进行注入,从而实现 hook

需要注意的配置项 @run-at。该配置项有以下几种选项

  • @run-at document-start: 开始渲染 document 时注入(hook 内置对象或函数时使用此方法)
  • @run-at document-body: body渲染时注入
  • @run-at document-idle(默认值): OOMContentLoaded 事件后注入
  • @run-at context-menu: 手动点击脚本时注入

Hook 脚本示例

https://github.com/0xsdeo/Hook_JSopen in new window

扣代码

流程:

  • 定位到加密位置,将最终生成代码取出

  • 取出加密位置涉及的一个内容,查看内部是否存在未定义的内容,进行补充,直到加密位置涉及的这个内容完全可用(深度优先)

  • 再取出下一个内容进行补充

Tips:

  • 在控制台中活用 copy() 函数,将目标变量复制到剪切板,便于直接粘贴
  • 在补环境或扣代码时,如果在浏览器中正常运行但 node 中异常,只有两种可能性: 浏览器指纹 或 格式化检测
    • 进一步判断: 将代码在浏览器控制台运行。 如果此时顺利运行,说明检测设备指纹。 如果依然存在问题,说明存在格式化检测
      • 如何处理格式化检测
      • 此时在代码行首debugger,在浏览器控制台 使用 单步调试 与 跳过函数调用 定位内存爆破的调用位置
      • 通过debugger与堆栈定位哪个分支进入了内存爆破的代码
      • 通过阅读分支条件了解是检测了哪个函数的格式化
      • 为实现最小影响,仅修改被检测函数为非格式化状态
      • 后续再遇到问题时,接着通过浏览器控制台执行代码判断是指纹检测还是格式化检测
      • 如果依然格式化检测,从之前已排查的位置开始 debugger 向后排查

混淆框架处理手段: jsfuck、eval 等内容,通过源码得知检测内容。再手动过检测后,检查上下文是否有其他改动内容。如果有则保持原本内容在原本位置或替换成源码

  • OB: 明确数组、位移、解密函数的位置,不格式化这三部分代码。在其余的代码逻辑(主逻辑)中定位加密参数生成位置
  • jsfuck: 将代码在浏览器控制台执行,查看是否报错
    • 报错: 报错信息内会包含去编码后的源码
    • 未报错: 在代码末尾删减部分,使其报错,从而可在报错信息内查看到源码
      • 移除括号
      • 末尾括号内(eval)拼接一个任意字符
  • AAEncode(表情包): 将代码在浏览器控制台执行,查看输出
    • undefined: 删除部分结尾字符
  • eval: 可能与某个混淆手段配合,eval 主逻辑中很长一串内容。将代码中的 eval 替换成 console.log 打印拿到代码内容

蜜罐

当nodejs顺利输出结果但结果不可用,可能走到了错误的分支。此时重点排查的产生分支的地方

  • try
  • if
  • ||&& 逻辑符控制代码是否执行
  • ? + : 检查nodejs执行分支是否与浏览器内一致

插桩

面对 VMP 时,可使用 overide 手动增加 console.log 或使用 日志断点 进行插桩

如果没输出,检查分支情况,console.log 是否被重写

恢复log输出

console.log 被重写时,日志断点也无法正常输出

  • 使用 script 断点,使代码停在执行js之前
  • 使用自定义函数接收输出功能 console._log = console.log
  • 使用断点停在插桩位置前,恢复输出功能 console.log = console._log

插桩重点位置

  • callapply: eval 或 wasm 等调用时会使用到
  • 栈内弹出元素的 ^ + - * % >> 等二元计算: 吐出核心计算流程