• 如何让 JS 代码不可断点


    绕过断点

    调试 JS 代码时,单步执行(F11)可跟踪所有操作。例如这段代码,每次调用 alert 时都会被断住:

    debugger
    alert(11)
    alert(22)
    alert(33)
    alert(44)
    

    有没有什么办法能让单步执行失效,一次执行多个操作?

    事实上有一些巧妙的办法。例如通过数组回调执行这些 alert 函数:

    debugger
    [11, 22, 33, 44].forEach(alert)
    

    这样只有 forEach 之前和之后会被断住,中间所有 alert 调用都不会被断住。

    由此可见,通过 内置回调 执行 原生函数,调试器是无法断住的!

    利用这个特性,我们可将一些重要的操作隐藏起来,从而能在调试者眼皮下悄悄执行。

    应用案例

    主流浏览器的调试器允许拦截特定事件,例如触发 mousemove 时断点;

    addEventListener('mousemove', e => {
      console.log(e)
    })
    

    因此调试者很容易找到事件回调函数,从而分析相应的处理逻辑。

    如何防止事件回调被断点?这就需要前面讲解的黑科技了。我们对上述代码稍微修改,将自己的回调函数改成原生函数:

    addEventListener('mousemove', console.log)
    

    这时,每次触发 mousemove 事件都不会被断住!

    然而现实中的回调逻辑远比 console.log 复杂,又该如何应用?

    事实上我们可以做一些调整,将事件的回调逻辑变得足够简单,简单到只需一个操作 —— 保存结果:

    const Q = []
    addEventListener('mousemove', Q.push.bind(Q))
    

    现在触发 mousemove 事件不仅不会被断住,而且还能将结果追加到数组 Q 中。

    至于读取则有很多办法,例如渲染事件、空闲事件、定期轮询等。

    setInterval(() => {
      for (const v of Q) {
        console.log(v)
      }
      Q.length = 0
    }, 20)
    

    如果 JS 只是采集信息而没有交互,可用更低的读取频率。

    属性访问

    前面的案例都是函数调用,例如 alert 函数、数组 push 函数。但属性读写又该如何实现?例如:

    window.onclick = function() {
      document.title = 'hello'
    }
    

    其实也不难。属性读写本质上是 getter 和 setter 函数的调用。例如:

    const setter = Object.getOwnPropertyDescriptor(Document.prototype, 'title').set
    setter.call(document, 'hello')
    

    当然这样会立即执行,而不是在 onclick 事件时执行。

    因此我们可以给 setter 柯里化,创建一个已绑定参数的新函数,作为事件回调:

    const setter = Object.getOwnPropertyDescriptor(Document.prototype, 'title').set
    onclick = setter.bind(document, 'hello')
    

    这样只有在点击时才会执行。并且调试器的 click 事件断点不会触发。

    对象属性

    除了原型上的属性,普通对象的属性又该如何访问?例如:

    const obj = {}
    window.onclick = function() {
      obj.name = 'jack'
    }
    

    事实上 JS 基本操作都可通过 Reflect API 实现。例如:

    const obj = {}
    Reflect.set(obj, 'name', 'jack')
    

    不过需注意的是,Reflect.set 的参数必须是 3 个,多一个也不行。例如:

    const obj = {}
    Reflect.set(obj, 'age', 20, {})
    obj.age   // undefined
    

    这样将其柯里化成事件回调函数是有问题的,因为事件回调还会加上一个 event 参数。

    不过 Reflect.apply 方法倒没有这个限制,往后再加几个参数也不影响执行:

    Reflect.apply(alert, null, ['hello'], 100, 200, 300)
    

    因此我们可通过 Reflect.apply 执行 Reflect.set,从而过滤多余的参数:

    const obj = {}
    Reflect.apply(Reflect.set, null, [obj, 'age', 20])
    obj.age   // 20
    

    然后将其柯里化成事件回调函数:

    const obj = {}
    onclick = Reflect.apply.bind(null, Reflect.set, null, [obj, 'age', 20])
    

    这样即可通过原生函数执行 obj.age = 20,并且 click 事件断点依然不会触发。

    多个操作

    前面讲解的都是单个操作,是否可以一次执行多个操作?例如:

    console.log('hello')
    console.log('world')
    alert(123)
    

    最容易想到的办法,就是将每个操作放入数组,然后通过 forEach 回调 Reflect.apply 执行每个操作:

    [
      Reflect.apply.bind(null, console.log, null, ['hello']),
      Reflect.apply.bind(null, console.log, null, ['world']),
      Reflect.apply.bind(null, alert, null, [123]),
    ].forEach(Reflect.apply)
    

    幸运的是 forEach 的回调函数和 Reflect.apply 函数都是 3 个参数,并且第 3 个都是数组类型:

    forEach_callback(element, index, array)
    
    Reflect.apply(target, thisArgument, argumentsList)
    

    这样通过 forEach 回调 Reflect.apply 是完全没问题的。于是可以一次执行多个操作,并且都无法断住!

    除了上述提到的,其实还有更多玩法,大家可发挥想象~

    (2021/11/01)

  • 相关阅读:
    李航老师《统计学习方法》第四章阅读笔记
    MySQL之SQL注入及解决
    吉利远景s1
    React useRequest解读
    差分方程模型:基金运作与管理
    MySQL数据库基本操作2
    <Linux>(极简关键、省时省力)《Linux操作系统原理分析之Linux 进程管理 1》(5)
    滴滴 OrangeFS 数据湖存储关键技术揭秘!
    共模电感选择请收好谷景电感教你的小方法
    常见漏洞危害总结
  • 原文地址:https://www.cnblogs.com/index-html/p/js-anti-breakpoint.html