admin revisou este gist . Ir para a revisão
1 file changed, 281 insertions
6.md(arquivo criado)
| @@ -0,0 +1,281 @@ | |||
| 1 | + | ### 防切屏检测 | |
| 2 | + | ||
| 3 | + | chrome浏览器自行安装油猴脚本 | |
| 4 | + | ||
| 5 | + | ``` | |
| 6 | + | // ==UserScript== | |
| 7 | + | // @name 通用阻止切屏检测 | |
| 8 | + | // @namespace http://tampermonkey.net/ | |
| 9 | + | // @version 0.1.0 | |
| 10 | + | // @description 尝试阻止各类网站的切屏、焦点丢失等检测 | |
| 11 | + | // @author xxxxxx | |
| 12 | + | // @match http://*/* | |
| 13 | + | // @match https://*/* | |
| 14 | + | // @run-at document-start | |
| 15 | + | // @grant unsafeWindow | |
| 16 | + | // @license GPL-3.0 | |
| 17 | + | // ==/UserScript== | |
| 18 | + | ||
| 19 | + | (function () { | |
| 20 | + | 'use strict'; | |
| 21 | + | const window = unsafeWindow; // 使用原始 window 对象 | |
| 22 | + | ||
| 23 | + | // 黑名单事件,这些事件的监听器将被阻止 | |
| 24 | + | const blackListedEvents = new Set([ | |
| 25 | + | "visibilitychange", // 页面可见性改变 | |
| 26 | + | "blur", // 元素或窗口失去焦点 | |
| 27 | + | "focus", // 元素或窗口获得焦点 (某些检测可能反向利用focus) | |
| 28 | + | "pagehide", // 页面隐藏(例如导航到其他页面) | |
| 29 | + | "freeze", // 页面被冻结 (较新的事件) | |
| 30 | + | "resume", // 页面从冻结状态恢复 (较新的事件) | |
| 31 | + | "mouseleave", // 鼠标移出元素(通常是 document 或 body) | |
| 32 | + | "mouseout", // 鼠标移出元素(更通用的移出,但要小心副作用) | |
| 33 | + | // "focusout", // 元素将要失去焦点(与blur类似,但更通用,看情况添加) | |
| 34 | + | // "focusin", // 元素将要获得焦点(与focus类似,看情况添加) | |
| 35 | + | ]); | |
| 36 | + | ||
| 37 | + | // 白名单属性,这些属性在 document 对象上将被伪造 | |
| 38 | + | const spoofedDocumentProperties = { | |
| 39 | + | hidden: { value: false, configurable: true }, | |
| 40 | + | mozHidden: { value: false, configurable: true }, // Firefox (旧版) | |
| 41 | + | msHidden: { value: false, configurable: true }, // Internet Explorer | |
| 42 | + | webkitHidden: { value: false, configurable: true }, // Chrome, Safari, Opera (旧版 Blink/WebKit) | |
| 43 | + | visibilityState: { value: "visible", configurable: true }, | |
| 44 | + | hasFocus: { value: () => true, configurable: true } | |
| 45 | + | }; | |
| 46 | + | ||
| 47 | + | // 需要清空/置空的事件处理器属性 (on-event handlers) | |
| 48 | + | const eventHandlersToNullifyDocument = [ | |
| 49 | + | "onvisibilitychange", | |
| 50 | + | "onblur", | |
| 51 | + | "onfocus", | |
| 52 | + | "onmouseleave", | |
| 53 | + | "onmouseout", | |
| 54 | + | // "onfocusout", | |
| 55 | + | // "onfocusin", | |
| 56 | + | "onpagehide", | |
| 57 | + | "onfreeze", | |
| 58 | + | "onresume" | |
| 59 | + | ]; | |
| 60 | + | ||
| 61 | + | const eventHandlersToNullifyWindow = [ | |
| 62 | + | "onblur", | |
| 63 | + | "onfocus", | |
| 64 | + | "onpagehide", | |
| 65 | + | "onpageshow", // 有些检测可能通过 pageshow 结合 persisted 属性判断 | |
| 66 | + | "onfreeze", | |
| 67 | + | "onresume", | |
| 68 | + | "onmouseleave", // window 也有 onmouseleave | |
| 69 | + | "onmouseout" | |
| 70 | + | ]; | |
| 71 | + | ||
| 72 | + | const isDebug = false; // 设置为 true 以启用调试日志 | |
| 73 | + | const scriptPrefix = "[通用阻止切屏检测]"; | |
| 74 | + | const log = console.log.bind(console, `%c${scriptPrefix}`, 'color: #4CAF50; font-weight: bold;'); | |
| 75 | + | const warn = console.warn.bind(console, `%c${scriptPrefix}`, 'color: #FFC107; font-weight: bold;'); | |
| 76 | + | const error = console.error.bind(console, `%c${scriptPrefix}`, 'color: #F44336; font-weight: bold;'); | |
| 77 | + | const debug = isDebug ? log : () => { }; | |
| 78 | + | ||
| 79 | + | /** | |
| 80 | + | * 伪装函数的 toString 方法,使其看起来像原始函数。 | |
| 81 | + | * @param {Function} modifiedFunction 被修改的函数 | |
| 82 | + | * @param {Function} originalFunction 原始函数 | |
| 83 | + | */ | |
| 84 | + | function patchToString(modifiedFunction, originalFunction) { | |
| 85 | + | if (typeof modifiedFunction !== 'function' || typeof originalFunction !== 'function') { | |
| 86 | + | warn("patchToString: 传入的参数不是函数。", modifiedFunction, originalFunction); | |
| 87 | + | return; | |
| 88 | + | } | |
| 89 | + | try { | |
| 90 | + | const originalToStringSource = Function.prototype.toString.call(originalFunction); | |
| 91 | + | modifiedFunction.toString = () => originalToStringSource; | |
| 92 | + | ||
| 93 | + | // 进一步伪装 toString.toString | |
| 94 | + | const originalToStringToStringSource = Function.prototype.toString.call(originalFunction.toString); | |
| 95 | + | Object.defineProperty(modifiedFunction.toString, 'toString', { | |
| 96 | + | value: () => originalToStringToStringSource, | |
| 97 | + | enumerable: false, | |
| 98 | + | configurable: true, // 保持可配置,以防万一 | |
| 99 | + | writable: false | |
| 100 | + | }); | |
| 101 | + | debug(`patchToString applied for: ${originalFunction.name || 'anonymous function'}`); | |
| 102 | + | } catch (e) { | |
| 103 | + | error("patchToString failed:", e, "for function:", originalFunction.name); | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | ||
| 108 | + | /** | |
| 109 | + | * 劫持并修改对象的 addEventListener 方法。 | |
| 110 | + | * @param {EventTarget} targetObject 要劫持的对象 (window, document, Element) | |
| 111 | + | * @param {string} objectName 用于日志记录的对象名称 | |
| 112 | + | */ | |
| 113 | + | function patchAddEventListener(targetObject, objectName) { | |
| 114 | + | if (!targetObject || typeof targetObject.addEventListener !== 'function') { | |
| 115 | + | warn(`Cannot patch addEventListener for invalid target: ${objectName}`); | |
| 116 | + | return; | |
| 117 | + | } | |
| 118 | + | const originalAddEventListener = targetObject.addEventListener; | |
| 119 | + | ||
| 120 | + | targetObject.addEventListener = function (type, listener, optionsOrCapture) { | |
| 121 | + | if (blackListedEvents.has(type.toLowerCase())) { | |
| 122 | + | log(`BLOCKED ${objectName}.addEventListener: ${type}`); | |
| 123 | + | return undefined; // 阻止添加黑名单中的事件监听器 | |
| 124 | + | } | |
| 125 | + | debug(`ALLOWED ${objectName}.addEventListener: ${type}`, listener, optionsOrCapture); | |
| 126 | + | return originalAddEventListener.call(this, type, listener, optionsOrCapture); | |
| 127 | + | }; | |
| 128 | + | ||
| 129 | + | patchToString(targetObject.addEventListener, originalAddEventListener); | |
| 130 | + | log(`${objectName}.addEventListener patched.`); | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | /** | |
| 134 | + | * 劫持并修改对象的 removeEventListener 方法 (可选,但建议一起修改)。 | |
| 135 | + | * @param {EventTarget} targetObject 要劫持的对象 | |
| 136 | + | * @param {string} objectName 用于日志记录的对象名称 | |
| 137 | + | */ | |
| 138 | + | function patchRemoveEventListener(targetObject, objectName) { | |
| 139 | + | if (!targetObject || typeof targetObject.removeEventListener !== 'function') { | |
| 140 | + | warn(`Cannot patch removeEventListener for invalid target: ${objectName}`); | |
| 141 | + | return; | |
| 142 | + | } | |
| 143 | + | const originalRemoveEventListener = targetObject.removeEventListener; | |
| 144 | + | ||
| 145 | + | targetObject.removeEventListener = function (type, listener, optionsOrCapture) { | |
| 146 | + | if (blackListedEvents.has(type.toLowerCase())) { | |
| 147 | + | log(`Original call to ${objectName}.removeEventListener for blacklisted event '${type}' would have been ignored by our addEventListener patch anyway. Allowing native call if needed.`); | |
| 148 | + | // 即使我们阻止了 addEventListener,原始的 removeEventListener 仍然应该能安全调用 | |
| 149 | + | // 因为如果监听器从未被添加,调用 remove 也无害。 | |
| 150 | + | } | |
| 151 | + | debug(`PASSTHROUGH ${objectName}.removeEventListener: ${type}`, listener, optionsOrCapture); | |
| 152 | + | return originalRemoveEventListener.call(this, type, listener, optionsOrCapture); | |
| 153 | + | }; | |
| 154 | + | patchToString(targetObject.removeEventListener, originalRemoveEventListener); | |
| 155 | + | log(`${objectName}.removeEventListener patched.`); | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | ||
| 159 | + | /** | |
| 160 | + | * 修改对象上的属性,使其返回伪造的值。 | |
| 161 | + | * @param {object} targetObject 目标对象 (e.g., document) | |
| 162 | + | * @param {object} propertiesToSpoof 属性描述对象 | |
| 163 | + | * @param {string} objectName 对象名称 | |
| 164 | + | */ | |
| 165 | + | function spoofProperties(targetObject, propertiesToSpoof, objectName) { | |
| 166 | + | if (!targetObject) { | |
| 167 | + | warn(`Cannot spoof properties for invalid target: ${objectName}`); | |
| 168 | + | return; | |
| 169 | + | } | |
| 170 | + | for (const prop in propertiesToSpoof) { | |
| 171 | + | if (Object.prototype.hasOwnProperty.call(propertiesToSpoof, prop)) { | |
| 172 | + | try { | |
| 173 | + | Object.defineProperty(targetObject, prop, propertiesToSpoof[prop]); | |
| 174 | + | debug(`Spoofed ${objectName}.${prop}`); | |
| 175 | + | } catch (e) { | |
| 176 | + | error(`Failed to spoof ${objectName}.${prop}:`, e); | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | log(`${objectName} properties spoofed.`); | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | /** | |
| 184 | + | * 清空或置空对象上的事件处理器属性。 | |
| 185 | + | * @param {object} targetObject 目标对象 | |
| 186 | + | * @param {string[]} eventHandlerNames 事件处理器名称数组 | |
| 187 | + | * @param {string} objectName 对象名称 | |
| 188 | + | */ | |
| 189 | + | function nullifyEventHandlers(targetObject, eventHandlerNames, objectName) { | |
| 190 | + | if (!targetObject) { | |
| 191 | + | warn(`Cannot nullify event handlers for invalid target: ${objectName}`); | |
| 192 | + | return; | |
| 193 | + | } | |
| 194 | + | eventHandlerNames.forEach(handlerName => { | |
| 195 | + | try { | |
| 196 | + | Object.defineProperty(targetObject, handlerName, { | |
| 197 | + | get: () => { | |
| 198 | + | debug(`Access to ${objectName}.${handlerName} (get), returning undefined.`); | |
| 199 | + | return undefined; | |
| 200 | + | }, | |
| 201 | + | set: (newHandler) => { | |
| 202 | + | log(`Attempt to set ${objectName}.${handlerName} blocked.`); | |
| 203 | + | if (typeof newHandler === 'function') { | |
| 204 | + | // 可以选择性地调用 newHandler,或者完全阻止 | |
| 205 | + | // debug(`(Blocked) Handler function was:`, newHandler); | |
| 206 | + | } | |
| 207 | + | }, | |
| 208 | + | configurable: true // 保持可配置,以便脚本可以多次运行或被其他脚本修改 | |
| 209 | + | }); | |
| 210 | + | debug(`Nullified ${objectName}.${handlerName}`); | |
| 211 | + | } catch (e) { | |
| 212 | + | error(`Failed to nullify ${objectName}.${handlerName}:`, e); | |
| 213 | + | } | |
| 214 | + | }); | |
| 215 | + | log(`${objectName} on-event handlers nullified.`); | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | // --- 开始执行 --- | |
| 219 | + | ||
| 220 | + | log("Script starting..."); | |
| 221 | + | ||
| 222 | + | // 1. 劫持 window 和 document 的 addEventListener/removeEventListener | |
| 223 | + | patchAddEventListener(window, "window"); | |
| 224 | + | patchRemoveEventListener(window, "window"); // 也 patch removeEventListener 以保持一致性 | |
| 225 | + | patchAddEventListener(document, "document"); | |
| 226 | + | patchRemoveEventListener(document, "document"); | |
| 227 | + | ||
| 228 | + | // 2. 修改 document 的属性 | |
| 229 | + | spoofProperties(document, spoofedDocumentProperties, "document"); | |
| 230 | + | ||
| 231 | + | // 3. 置空 document 和 window 上的事件处理器 | |
| 232 | + | nullifyEventHandlers(document, eventHandlersToNullifyDocument, "document"); | |
| 233 | + | nullifyEventHandlers(window, eventHandlersToNullifyWindow, "window"); | |
| 234 | + | ||
| 235 | + | // 4. 对于 document.body,需要等待 DOMContentLoaded | |
| 236 | + | // 使用 MutationObserver 确保 body 存在时立即 patch,比 DOMContentLoaded 更早且更可靠 | |
| 237 | + | const observer = new MutationObserver((mutations, obs) => { | |
| 238 | + | if (document.body) { | |
| 239 | + | patchAddEventListener(document.body, "document.body"); | |
| 240 | + | patchRemoveEventListener(document.body, "document.body"); | |
| 241 | + | // 对于 document.body,也可以考虑 nullify onmouseleave, onmouseout 等 | |
| 242 | + | nullifyEventHandlers(document.body, ["onmouseleave", "onmouseout", "onblur", "onfocus"], "document.body"); | |
| 243 | + | log("document.body patched via MutationObserver."); | |
| 244 | + | obs.disconnect(); // 完成任务后断开观察者 | |
| 245 | + | } | |
| 246 | + | }); | |
| 247 | + | ||
| 248 | + | if (document.body) { // 如果 body 已经存在 (不太可能在 document-start,但以防万一) | |
| 249 | + | patchAddEventListener(document.body, "document.body"); | |
| 250 | + | patchRemoveEventListener(document.body, "document.body"); | |
| 251 | + | nullifyEventHandlers(document.body, ["onmouseleave", "onmouseout", "onblur", "onfocus"], "document.body"); | |
| 252 | + | log("document.body patched directly."); | |
| 253 | + | } else { | |
| 254 | + | observer.observe(document.documentElement || document, { childList: true, subtree: true }); | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | ||
| 258 | + | // 5. 调试:劫持计时器 (如果 isDebug 为 true) | |
| 259 | + | if (isDebug) { | |
| 260 | + | const originalSetInterval = window.setInterval; | |
| 261 | + | window.setInterval = function(...args) { | |
| 262 | + | const id = originalSetInterval.apply(this, args); | |
| 263 | + | debug("calling window.setInterval", id, args); | |
| 264 | + | return id; | |
| 265 | + | }; | |
| 266 | + | patchToString(window.setInterval, originalSetInterval); | |
| 267 | + | ||
| 268 | + | const originalSetTimeout = window.setTimeout; | |
| 269 | + | window.setTimeout = function(...args) { | |
| 270 | + | const id = originalSetTimeout.apply(this, args); | |
| 271 | + | debug("calling window.setTimeout", id, args); | |
| 272 | + | return id; | |
| 273 | + | }; | |
| 274 | + | patchToString(window.setTimeout, originalSetTimeout); | |
| 275 | + | log("Timer functions (setInterval, setTimeout) wrapped for debugging."); | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | log("Script execution finished. Monitoring active."); | |
| 279 | + | ||
| 280 | + | })(); | |
| 281 | + | ``` | |
Próximo
Anterior