浏览器反调试
常见debugger实现
- 直接使用debugger
- 使用
eval()函数
eval("debugger;")
eval('(function() {var a = new Date(); debugger; return new Date() - a > 100;}())')- 定时器触发
setInterval(function (){debugger;},1000);- 其他方式
Function("debugger").call()/apply() 或赋值 bind()
xxx.constructor("debugger").call("action")
Fuction.constructor("debugger").call("action")
(function(){return !![];}["constructor"]("debugger")["call"]("action"))
eval('(function() {}["constructor"]("debugger")["call"]("action"));')跳过方式
Never pause here
直接在出现debugger的位置的最左边,鼠标右键,弹出如下菜单,选择【Never pause here】,意思是永远不在这里停下来。
添加条件断点
在JS代码 debugger 行数位置的最左侧点击右键,添加条件断点(满足条件才会进入断点),将条件设置为false,就是条件永远不成立,永远不会断下来。
Hook 无限 debugger 函数
通过定时器实现的debugger,可以在进入定时器代码之前进行hook,修改定时器的逻辑。也可以使用FD或者油猴插件进行hook。
定时器实现的debugger形式如下:
setInterval(function (){
debugger;
},1000)
//setInterval这个是JS语言中的定时器,它有两个参数,第一个是要执行的代码,第二个执行间隔时间,
// 此处就是每过1000毫秒就执行一次参数一的方法,就会一直debugger下去1、设置 script 断点 在Sources面板设置 script 断点,这一步的目的,是为了让浏览器在刚运行时就被断下来,以方便进行 Hook
重新刷新页面,就会断下来,然后就可以进行hook了
2、Hook 无限 debugger 函数 2.1 Hook Function 构造器函数
Function.prototype.__constructor_back = Function.prototype.constructor;
Function.prototype.constructor = function() {
if(arguments && typeof arguments[0]==='string'){
//alert("new function: "+ arguments[0]);
if("debugger" === arguments[0]){
//arguments[0]="console.log(\"anti debugger\");";
//arguments[0]=";";
return
}
}
return Function.prototype.__constructor_back.apply(this,arguments);
}2.2 Hook eval 函数
eval_ = eval;
//下面这样写,是为了过瑞数的 eval.toString 检测
eval = function(a){if(a=='debugger'){return ''}else{return eval_(a)}}
// 或者如下:
var my_eval = eval;
eval = function (arg) {
if (arg.indexOf('debugger') != -1) {
return function () {
};
}
return my_eval(arg);
}
var _old = Function.prototype.toString.call
Function.prototype.toString.call = function (arg) {
if (arg === eval) {
return "function eval() { [native code] }"
}
return _old(arg);
}2.3 Hook setInterval 函数 (1)业务代码和 setInterval 无关时
setInterval = function(){}(2)业务代码和 setInterval 有关时
setInterval_back = setInterval
setInterval = function(a,b){
if(a.toString().indexOf('debugger') != -1){
return null;
}
return setInterval_back(a, b);
}本地替换js
直接使用浏览器开发者工具替换修改js(Sources面板 --> Overrides),或者通过FD工具替换。
这种方式的核心思路,是替换 JS 文件中的 debugger 关键字,并保存为本地文件,在请求返回的时候、通过正则匹配等方式、拦截并替换返回的 JS 代码,以达到绕过 debugger 的目的。也可以直接删掉相关的debugger代码。
使用FD、油猴等插件Hook
使用这种方法,就不需要再打 script 断点。
注意:如果 script 断点无法在 debugger 函数之前断下来,那就只能用这种方式进行拦截 Hook,才能过掉 debugger。
定时器触发打开调试无限debugger
因为debugger 关键字在 JavaScript 中的行为设计为仅在处于调试模式时生效。所以我们可以写出如下代码,使打开调试模式后进入无限debugger。
setInterval(() => {
// eslint-disable-next-line no-debugger
debugger
}, 1000)注意需要保证上面代码一定能执行,直接将其放入main.ts的末尾比较合适。
该方案的问题
在DevTools模式下,有一些跳过debugger的技巧,比如开启deactivate breakpoints。这时上面的代码就失效了。
检查是否启用deactivate breakpoints
为了应对开启deactivate breakpoints后,断点失效的问题。我们可以检查是否开启了deactivate breakpoints。 下面我们使用间隔时间来判断是否启用了deactivate breakpoints。如果启用,我们刷新页面为一个无用地址。
setInterval(() => {
const startTime = Date.now()
// eslint-disable-next-line no-debugger
debugger
const endTime = Date.now()
if (endTime - startTime > 100) {
return
}
console.log('检查到已:deactivate breakpoints')
window.location.replace('about:blank')
}, 1000)该方案的问题
上面的代码有一个问题,在未开启调试模式的情况下,将误判为启用deactivate breakpoints,而刷新为无用地址。
检测是否打开了DevTools
为了解决上个方案的误判问题,我们需要能判断是否开启了调试模式。这里我们使用一个比较小众的三方库devtools-detect来完成,该库专门用来检测开发者工具有没有被打开,支持Chrome、Firefox、Safari、Firebug。
- 安装
$ pnpm add -D devtools-detect- 使用
import devtools from 'devtools-detect'
// Check if it's open
console.log('Is DevTools open:', devtools.isOpen)
// Check it's orientation, `undefined` if not open
console.log('DevTools orientation:', devtools.orientation)
// Get notified when it's opened/closed or orientation changes
window.addEventListener('devtoolschange', (event) => {
console.log('Is DevTools open:', event.detail.isOpen)
console.log('DevTools orientation:', event.detail.orientation)
})- 调整逻辑
这里我们只需要使用isOpen来判断是否开启了调试模式即可。
import devtools from 'devtools-detect'
setInterval(() => {
if (!devtools.isOpen) {
return
}
const startTime = Date.now()
// eslint-disable-next-line no-debugger
debugger
const endTime = Date.now()
if (endTime - startTime > 100) {
return
}
console.log('检查到已:deactivate breakpoints')
window.location.replace('about:blank')
}, 1000)通过devtools-detect的自述文档,发现可以监听状态的改变。于是我们可以将逻辑放入监听中。
import devtools from 'devtools-detect'
let intervalId: number | undefined = undefined
const devtoolsOpen = () => {
const startTime = Date.now()
// eslint-disable-next-line no-debugger
debugger
const endTime = Date.now()
if (endTime - startTime > 100) {
return
}
console.log('检查到已:deactivate breakpoints')
window.location.replace('about:blank')
}
window.addEventListener('devtoolschange', (event) => {
const isOpen = event.detail.isOpen
if (!isOpen) {
// FIXME: - 这里忽略了intervalId=0的情况
intervalId && window.clearInterval(intervalId)
intervalId = undefined
return
}
intervalId && window.clearInterval(intervalId)
intervalId = window.setInterval(devtoolsOpen, 1000)
})
/*
必须要先读取一下isOpen,监听事件devtoolschange才会生效
当然这正和我们的意图,需要先检查一下,如不如果初始化时,已经处于调试状态就无法判断了
*/
const isOpen = devtools.isOpen
if (isOpen) {
intervalId && window.clearInterval(intervalId)
intervalId = window.setInterval(devtoolsOpen, 1000)
}注意这里需要注意的是,devtoolschange事件并不是标准的监听事件,应该是devtools-detect实现的。经测试,需要读取isOpen属性后才会生效。 提示如果是需要兼容其他浏览器,最好自己多测试一下。如果devtools-detect检测不了,可以换其他检测方案。除了使用devtools-detect检查是否打开了调试模式外,还有其他方式。如检测浏览器内宽和外宽差(可以缩放不是很准确)。
setInterval(() => {
;(function () {
//当浏览器内宽和外宽的差大于20,或者内高和外高的差大于100的情况下
if (
window.outerWidth - window.innerWidth > 20 ||
window.outerHeight - window.innerHeight > 100
) {
console.log('控制台被打开')
}
})()
}, 1000)问题
至此基本逻辑已经完成了,但是还有个小问题,当处于debugger断点状态时,在断点位置能够看到其他代码,其他人就能发现我们的实现逻辑,从而针对性的找解决方案。
提取文件
我们把debugger相关的代码提取到一个独立的文件中。
- 编写一个
breakpoint.ts文件。
/*
* @Author: matiastang
* @Date: 2024-04-28 18:04:49
* @LastEditors: matiastang
* @LastEditTime: 2024-04-28 18:05:43
* @FilePath: /MT/src/breakpoint.ts
* @Description: 断点文件
*/
const breakpoint = () => {
// eslint-disable-next-line no-debugger
debugger
}
export default breakpoint- 修改
debugger的使用
import devtools from 'devtools-detect'
import breakpoint from './breakpoint'
let intervalId: number | undefined = undefined
const devtoolsOpen = () => {
const startTime = Date.now()
breakpoint()
const endTime = Date.now()
if (endTime - startTime > 100) {
return
}
console.log('检查到已:deactivate breakpoints')
window.location.replace('about:blank')
}
window.addEventListener('devtoolschange', (event) => {
const isOpen = event.detail.isOpen
if (!isOpen) {
// FIXME: - 这里忽略了intervalId=0的情况
intervalId && window.clearInterval(intervalId)
intervalId = undefined
return
}
intervalId && window.clearInterval(intervalId)
intervalId = window.setInterval(devtoolsOpen, 1000)
})
/*
必须要先读取一下isOpen,监听事件devtoolschange才会生效
当然这正和我们的意图,需要先检查一下,如不如果初始化时,已经处于调试状态就无法判断了
*/
const isOpen = devtools.isOpen
if (isOpen) {
intervalId && window.clearInterval(intervalId)
intervalId = window.setInterval(devtoolsOpen, 1000)
}经过以上调整,处于断点状态时,只能看到已经函数中有个debugger,其他代码再其他文件中,再加上代码混稀的处理,其他开发者就无法发现我们的实现逻辑了。
提示这个功能都可以提取为一个功能文件。
- 发现个问题需要优化一下,每次至少需要1秒才会检测,当处于调试模式的时候主动触发一次。
在setInterval之前主动调用一次devtoolsOpen函数。
devtoolsOpen()
intervalId = window.setInterval(devtoolsOpen, 1000)封装一下,并丰富配置功能
创建一个文件夹存放相关代码。
- antiDebugging
- - breakpoint.ts
- - customConsole.ts
- - index.ts
- main.tsantiDebugging>breakpoint.ts存放断点函数。 antiDebugging>customConsole.ts存放日志封装。 antiDebugging>index.ts存放主要逻辑。 main.ts为使用。
breakpoint.ts
/*
* @Author: matiastang
* @Date: 2024-04-28 18:04:49
* @LastEditors: matiastang
* @LastEditTime: 2024-04-29 10:59:58
* @FilePath: /MT/src/breakpoint.ts
* @Description: 断点文件
*/
const breakpoint = () => {
// eslint-disable-next-line no-debugger
debugger
}
export default breakpointcustomConsole.ts
/*
* @Author: matiastang
* @Date: 2024-04-29 15:19:02
* @LastEditors: matiastang
* @LastEditTime: 2024-04-29 15:30:06
* @FilePath: /MT/src/antiDebugging/devLog.ts
* @Description: 自定义日志
*/
export enum ConsoleType {
LOG = 'log',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
}
/**
* log
* @param message
*/
export const consoleLog = (message?: /* eslint-disable */ any /* eslint-enable */) => {
customConsole(message, ConsoleType.LOG)
}
/**
* info
* @param message
*/
export const consoleInfo = (message?: /* eslint-disable */ any /* eslint-enable */) => {
customConsole(message, ConsoleType.INFO)
}
/**
* warn
* @param message
*/
export const consoleWarn = (message?: /* eslint-disable */ any /* eslint-enable */) => {
customConsole(message, ConsoleType.WARN)
}
/**
* error
* @param message
*/
export const consoleError = (message?: /* eslint-disable */ any /* eslint-enable */) => {
customConsole(message, ConsoleType.ERROR)
}
/**
* 自定义console
* @param message
* @param type
*/
const customConsole = (
message?: /* eslint-disable */ any /* eslint-enable */,
type: ConsoleType = ConsoleType.LOG
) => {
if (type === ConsoleType.LOG) {
console.log(message)
} else if (type === ConsoleType.INFO) {
console.info(message)
} else if (type === ConsoleType.WARN) {
console.warn(message)
} else {
console.error(message)
}
}
export default customConsoleindex.ts
/*
* @Author: matiastang
* @Date: 2024-04-29 09:43:40
* @LastEditors: matiastang
* @LastEditTime: 2024-04-29 15:48:17
* @FilePath: /MT/src/antiDebugging/index.ts
* @Description: Anti-debugging
*/
import devtools from 'devtools-detect'
import breakpoint from './breakpoint'
import customConsole, { ConsoleType } from './customConsole'
/**
* 配置
*/
export interface AntiDebuggingConfig {
/**
* 轮询时间, 默认1000
*/
timeout?: number
/**
* 是否立即执行一次, 默认true
*/
immediate?: boolean
/**
* 是否开启deactivate breakpoints的判定条件, 默认100
* 仅正数生效
*/
dbDiff?: number
/**
* 是否输出开发日志
*/
devLog?: boolean
/**
* 是否关闭debugger
*/
deactivateDebugger?: boolean
/**
* localStorage中保存的是否关闭debugger key
* 如果传递了该值,则在localStorage中读取该值
* 如果该值为true,则关闭debugger
* 如果该值为false,则检查deactivateDebugger,决定是否关闭debugger
*/
debuggerLocalStorageKey?: string
/**
* 调试工具状态变化
* @param open 是否打开
* @returns
*/
devtoolsChange?: (open: boolean) => void
/**
* deactivate breakpoints 状态变化
* @param open 是否打开
* @returns
*/
breakpointsChange?: (open: boolean) => void
/**
* 开启 deactivate breakpoints 的处理函数
* 默认:window.location.replace('about:blank')
* @returns
*/
deactivateBreakpoints?: () => void
}
export interface AntiDebuggingOptions extends AntiDebuggingConfig {
/**
* 调试工具状态
* true: 开启调试工具
* false: 关闭调试工具
* undefined: 未知
*/
devtoolsStatus?: boolean
/**
* 断点状态
* true: 关闭断点
* false: 开启断点
* undefined: 未知
*/
breakpointStatus?: boolean
}
// 默认timeout
const NORMAL_TIMEOUT = 1000
// 默认diff
const NORMAL_DIFF = 100
/**
* 配置
*/
export let options: AntiDebuggingOptions = {
timeout: NORMAL_TIMEOUT,
immediate: true,
dbDiff: NORMAL_DIFF,
devLog: false,
deactivateDebugger: false,
}
// interval id
let intervalId: number | null = null
/**
* log
* @param message
*/
const devConsole = (
message?: /* eslint-disable */ any /* eslint-enable */,
type: ConsoleType = ConsoleType.LOG
) => {
const { devLog } = options
devLog && customConsole(message, type)
}
/**
* 开启interval
* @param timeout 间隔时间
* @returns
*/
const setIntervalTime = (timeout?: number | undefined) => {
clearIntervalTime()
options.immediate && devtoolsOpen()
intervalId = window.setInterval(devtoolsOpen, timeout)
devConsole(`setInterval:${intervalId}`)
}
/**
* 关闭interval
* @returns
*/
const clearIntervalTime = () => {
if (typeof intervalId !== 'number') {
return
}
devConsole(`clearInterval:${intervalId}`)
intervalId && window.clearInterval(intervalId)
intervalId = null
}
/**
* 调试工具打开时处理
* @returns
*/
const devtoolsOpen = () => {
const {
dbDiff,
deactivateDebugger,
breakpointStatus,
breakpointsChange,
deactivateBreakpoints,
} = options
if (deactivateDebugger) {
return
}
const startTime = Date.now()
breakpoint()
const endTime = Date.now()
let maxDiff = NORMAL_DIFF
if (typeof dbDiff === 'number' && dbDiff > 0) {
maxDiff = dbDiff
}
if (endTime - startTime > maxDiff) {
if (breakpointStatus) {
devConsole('breakpointStatus:true->false')
options.breakpointStatus = false
breakpointsChange && breakpointsChange(false)
}
return
}
if (!breakpointStatus) {
devConsole('breakpointStatus:false->true')
options.breakpointStatus = true
breakpointsChange && breakpointsChange(true)
}
if (deactivateBreakpoints) {
deactivateBreakpoints()
return
}
window.location.replace('about:blank')
}
/**
* 监听开发者工具是否打开状态的变化
* `devtoolschange`事件并不是标准的监听事件。必须要先读取一下isOpen,监听事件devtoolschange才会生效
* @param {*} event
* @returns
*/
window.addEventListener('devtoolschange', (event) => {
const isOpen = event.detail.isOpen
options.devtoolsStatus = isOpen
devConsole(`devtools status:${isOpen}`)
const { devtoolsChange } = options
if (isOpen) {
setIntervalTime()
devtoolsChange && devtoolsChange(true)
return
}
clearIntervalTime()
devtoolsChange && devtoolsChange(false)
})
/**
* 检查localStorage中保存的是否关闭debugger状态
* @param key
*/
const checkLocalStorage = (key: string) => {
const value = getLocalStorageDebugger(key)
devConsole(`localStorage deactivate debugger status:${value}`)
if (value) {
options.deactivateDebugger = true
}
}
/**
* 获取LocalStorage中的调试设置
* @param key
* @returns
*/
const getLocalStorageDebugger = (key: string) => {
const localValue = localStorage.getItem(key)
if (typeof localValue !== 'string') {
return false
}
try {
const value = JSON.parse(localValue).value
return Boolean(value)
} catch (error) {
devConsole(error, ConsoleType.ERROR)
return false
}
}
/**
* anti-debugging
* @param config 配置参数
*/
const antiDebugging = (config?: AntiDebuggingConfig) => {
options = { ...options, ...config }
const isOpen = devtools.isOpen
options.devtoolsStatus = isOpen
const localKey = options.debuggerLocalStorageKey
if (localKey) {
checkLocalStorage(localKey)
}
devConsole('====== config ======')
devConsole(options)
devConsole(`init devtools status:${isOpen}`)
const { devtoolsChange } = options
if (isOpen) {
setIntervalTime()
devtoolsChange && devtoolsChange(true)
return
}
devtoolsChange && devtoolsChange(false)
}
export default antiDebuggingmain.ts
基础使用
import antiDebugging from './antiDebugging'
antiDebugging()更多配置
import antiDebugging from './antiDebugging'
antiDebugging({
/**
* 轮询时间, 默认1000
*/
timeout: 1000,
/**
* 是否立即执行一次, 默认true
*/
immediate: true,
/**
* 是否开启deactivate breakpoints的判定条件, 默认100
* 仅正数生效
*/
dbDiff: 100,
/**
* 是否输出开发日志
*/
devLog: false,
/**
* 是否关闭debugger
*/
deactivateDebugger: false,
/**
* localStorage中保存的是否关闭debugger key
* 如果传递了该值,则在localStorage中读取该值
* 如果该值为true,则关闭debugger
* 如果该值为false,则检查deactivateDebugger,决定是否关闭debugger
*/
debuggerLocalStorageKey: 'ANTI-DEBUGGER',
/**
* 调试工具状态变化
* @param open 是否打开
* @returns
*/
devtoolsChange: (open: boolean) => {
console.log(`devtools ${open ? '开启' : '关闭'}`)
},
/**
* deactivate breakpoints 状态变化
* @param open 是否打开
* @returns
*/
breakpointsChange: (open: boolean) => {
console.log(`deactivate breakpoints ${open ? '开启' : '关闭'}`)
},
/**
* 开启 deactivate breakpoints 的处理函数
* 默认:window.location.replace('about:blank')
* @returns
*/
deactivateBreakpoints: () => {
console.log('检查到开启:deactivate breakpoints')
window.location.replace('about:blank')
},
})如上基本的封装就完成了,接下来就是在项目使用,如果挺好用得,可以考虑发布成npm.js包。
测试的时候发现,默认diff差设置成100ms时,在连续点击时可能会被判定为跳过了断点,所以缩短时间50ms。
// 默认diff
const NORMAL_DIFF = 50开源项目
前端开发中如何在JS文件中检测用户浏览器是否打开了调试面板(F12打开开发者工具)?
devtools-detect
处于不停靠状态(undocked)状态无法检测。
Support Chrome DevTools Safari DevTools Firefox DevTools Opera DevTools Caveats Doesn't work if DevTools is undocked and will show false positive if you toggle any kind of sidebar.
devtools-detector
各种检测控制台是否打开的方法
版权所有
版权归属:matiastang
许可证:Copyright © 2022-present tangdaoyong
