破解反作弊:从事件劫持到原型链污染的完整攻略
⚠️ 免责声明
本文内容仅用于技术研究与安全教育目的。
- 本文所分析的代码示例均为随机选取的技术样本,不针对任何特定网站、系统或组织
- 文中涉及的技术方法和代码片段仅用于说明 Web 安全原理和攻防机制
- 如文中示例与任何实际系统存在相似之处,纯属技术巧合,不代表本文针对该系统
- 读者在实际应用本文技术时,必须遵守所在国家和地区的法律法规
- 未经授权对他人系统进行测试或攻击属于违法行为,作者对任何滥用本文技术的行为不承担任何责任
- 建议读者仅在自己拥有的测试环境或获得明确授权的情况下进行技术验证
技术研究应当服务于提升系统安全性,而非破坏合法服务。请负责任地使用本文知识。
分析目标
我要绕过的反作弊脚本完整代码:
1// ULTIMATE Anti-cheat script - CONDITIONAL LOADING
2
3(function () {
4 'use strict';
5
6 // Status indicator (disabled for cleaner UI)
7 function showStatus() {
8 // Red banner removed for better user experience
9 // Protection is still active, just invisible
10 }
11
12 // Universal blocker
13 function universalBlock(e) {
14 // Only block if we should be active
15 if (!shouldActivate()) return;
16
17 const type = e.type;
18 const key = e.key ? e.key.toLowerCase() : '';
19 const ctrl = e.ctrlKey || e.metaKey;
20
21 // Block ALL keyboard shortcuts when Ctrl is pressed
22 if (ctrl) {
23 e.preventDefault();
24 e.stopPropagation();
25 e.stopImmediatePropagation();
26 alert('⚠️ 为保证测评公平,快捷键已被禁用');
27 return false;
28 }
29
30 // Block F12
31 if (key === 'f12') {
32 e.preventDefault();
33 e.stopPropagation();
34 e.stopImmediatePropagation();
35 alert('⚠️ 为保证测评公平,开发者工具已被禁用');
36 return false;
37 }
38
39 // Block right click
40 if (type === 'contextmenu') {
41 e.preventDefault();
42 e.stopPropagation();
43 e.stopImmediatePropagation();
44 alert('⚠️ 为保证测评公平,右键菜单已被禁用');
45 return false;
46 }
47
48 // Block clipboard events
49 if (['copy', 'cut', 'paste'].includes(type)) {
50 e.preventDefault();
51 e.stopPropagation();
52 e.stopImmediatePropagation();
53 alert(
54 `⚠️ 为保证测评公平,${type === 'copy' ? '复制' : type === 'cut' ? '剪切' : '粘贴'}功能已被禁用`
55 );
56 return false;
57 }
58
59 // Block selection
60 if (['selectstart', 'dragstart'].includes(type)) {
61 e.preventDefault();
62 return false;
63 }
64 }
65
66 let isActive = false;
67
68 // Check if we should activate protection based on URL
69 function shouldActivate() {
70 const path = window.location.pathname;
71 // Only activate on assessment pages, NOT survey or admin pages
72 return path.match(/^\/assessment\/[^/]+$/) || path.match(/^\/[^/]+\/assessment\/[^/]+$/);
73 }
74
75 // Immediate activation
76 function activate() {
77 if (!shouldActivate()) {
78 if (isActive) {
79 console.log('🛡️ Anti-cheat deactivated (not on assessment page)');
80 deactivate();
81 }
82 return;
83 }
84
85 if (isActive) return; // Already active
86
87 isActive = true;
88 console.log('🚀 ULTIMATE Anti-cheat loading...');
89 console.log('🛡️ Anti-cheat activated for assessment page');
90
91 // Add universal event listener to EVERYTHING
92 const events = [
93 'keydown',
94 'keyup',
95 'keypress',
96 'contextmenu',
97 'copy',
98 'cut',
99 'paste',
100 'selectstart',
101 'dragstart',
102 'mousedown',
103 'mouseup',
104 ];
105
106 events.forEach(eventType => {
107 // Add to document with highest priority
108 document.addEventListener(eventType, universalBlock, {
109 capture: true,
110 passive: false,
111 once: false,
112 });
113
114 // Add to window as backup
115 window.addEventListener(eventType, universalBlock, {
116 capture: true,
117 passive: false,
118 once: false,
119 });
120 });
121
122 // Disable selection everywhere
123 const style = document.createElement('style');
124 style.id = 'anti-cheat-style';
125 style.innerHTML = `
126 *, *::before, *::after {
127 -webkit-user-select: none !important;
128 -moz-user-select: none !important;
129 -ms-user-select: none !important;
130 user-select: none !important;
131 -webkit-touch-callout: none !important;
132 -webkit-user-drag: none !important;
133 }
134 input, textarea, [contenteditable] {
135 -webkit-user-select: text !important;
136 -moz-user-select: text !important;
137 -ms-user-select: text !important;
138 user-select: text !important;
139 }
140 `;
141
142 if (document.head) {
143 document.head.appendChild(style);
144 } else {
145 setTimeout(() => document.head.appendChild(style), 100);
146 }
147
148 showStatus();
149 console.log('✅ ULTIMATE Anti-cheat loaded and ACTIVE');
150 }
151
152 // Deactivate protection
153 function deactivate() {
154 if (!isActive) return;
155 isActive = false;
156
157 // Remove style
158 const style = document.getElementById('anti-cheat-style');
159 if (style) style.remove();
160
161 // Note: Event listeners are harder to remove cleanly, but they'll check shouldActivate()
162 }
163
164 // Check and activate based on current page
165 function checkAndActivate() {
166 activate();
167 }
168
169 // Activate immediately
170 checkAndActivate();
171
172 // Also activate when DOM is ready
173 if (document.readyState === 'loading') {
174 document.addEventListener('DOMContentLoaded', checkAndActivate);
175 }
176
177 // Check every few seconds for navigation changes
178 setInterval(checkAndActivate, 3000);
179})();我的攻击策略总览
| 目标防护 | 实现方式 | 我的突破方法 |
|---|---|---|
| 键盘事件拦截 | addEventListener('keydown') | 劫持 EventTarget.prototype.addEventListener |
| F12 拦截 | 检测 key === 'f12' | 事件监听器未注册 |
| 右键菜单拦截 | addEventListener('contextmenu') | 事件监听器未注册 |
| 复制粘贴拦截 | addEventListener('copy/paste/cut') | 事件监听器未注册 |
| 文本选择禁用 | CSS user-select: none | MutationObserver + 定期清理 |
| 弹窗警告 | alert() | 劫持 window.alert |
| 定期检查 | setInterval(3000) | 更快的清理频率(2000) |
核心突破:事件劫持
目标的防护机制
我分析了目标脚本,发现它使用以下方式拦截事件:
1function universalBlock(e) {
2 if (!shouldActivate()) return;
3 const ctrl = e.ctrlKey || e.metaKey;
4 if (ctrl) {
5 e.preventDefault();
6 e.stopPropagation();
7 e.stopImmediatePropagation();
8 return false;
9 }
10}
11
12events.forEach(eventType => {
13 document.addEventListener(eventType, universalBlock, {
14 capture: true,
15 passive: false,
16 });
17});它在捕获阶段拦截,使用 stopImmediatePropagation() 阻止传播。这看起来很严密,但我发现了致命弱点。
我的解决方案
我的突破策略很简单:在它注册事件之前劫持 addEventListener 本身。
1const originalAddEventListener = EventTarget.prototype.addEventListener;
2
3EventTarget.prototype.addEventListener = function (type, listener, options) {
4 if (blockedEvents.includes(type)) {
5 return;
6 }
7 return originalAddEventListener.call(this, type, listener, options);
8};实现思路:当目标脚本调用 document.addEventListener('keydown', universalBlock) 时,实际执行的是我劫持后的版本。我直接返回空,它的监听器永远不会被注册。
我的攻击时序
1阶段1: 页面加载初期
2 我的脚本执行 (Tampermonkey @run-at document-start)
3 劫持 EventTarget.prototype.addEventListener
4
5阶段2: 目标脚本加载
6 调用 document.addEventListener()
7 实际执行我劫持的版本,监听器未注册
8
9阶段3: 用户交互
10 用户按下 Ctrl+C
11 浏览器触发 keydown 事件
12 无监听器响应
13 复制成功时序优势:我的脚本在 document-start 时机执行,比目标脚本早数十到数百毫秒(具体取决于网络和脚本大小),这个时间窗口足以完成原型链劫持。
CSS 对抗
目标的CSS防护
目标脚本注入了禁用选择的样式:
1const style = document.createElement('style');
2style.id = 'anti-cheat-style';
3style.innerHTML = `
4 * {
5 user-select: none !important;
6 }
7 input, textarea {
8 user-select: text !important;
9 }
10`;
11document.head.appendChild(style);我的多层对策
我采用了四层防御策略:
第一层:移除目标样式
1function removeAntiCheatStyles() {
2 const style = document.getElementById('anti-cheat-style');
3 if (style) {
4 style.remove();
5 }
6}第二层:注入我的对抗样式
1function enableTextSelection() {
2 const style = document.createElement('style');
3 style.id = 'bypass-style';
4 style.innerHTML = `
5 * {
6 user-select: text !important;
7 }
8 `;
9 document.head.appendChild(style);
10}第三层:DOM 实时监控
1const observer = new MutationObserver(mutations => {
2 mutations.forEach(mutation => {
3 mutation.addedNodes.forEach(node => {
4 if (node.id === 'anti-cheat-style') {
5 removeAntiCheatStyles();
6 enableTextSelection();
7 }
8 });
9 });
10});
11
12// 只监控 <head> 的直接子元素添加,避免性能开销
13observer.observe(document.head, {
14 childList: true,
15 subtree: false
16});第四层:定期清理
1function periodicCleanup() {
2 removeAntiCheatStyles();
3 setTimeout(periodicCleanup, 2000);
4}Alert 拦截
我劫持了 alert 函数来阻止烦人的弹窗:
1const originalAlert = window.alert;
2
3window.alert = function (message) {
4 if (message && typeof message === 'string' && message.includes('为保证测评公平')) {
5 return;
6 }
7 return originalAlert.call(window, message);
8};时间竞争优势
我设计了比目标更快的清理频率:
| 时刻 | 目标行为 | 我的行为 |
|---|---|---|
| 0s | 首次激活,注入样式 | 劫持完成,移除样式 |
| 2s | - | 第1次清理 |
| 3s | 检查,尝试重新注入 | 已清理 |
| 4s | - | 第2次清理 |
| 6s | 检查,MutationObserver立即检测 | 立即移除 |
我始终保持1秒的时间优势。
执行时机是关键
我利用了浏览器加载流程:
11. 解析 HTML
22. Tampermonkey (@run-at document-start) ← 我的脚本在这里
33. 解析 <head>
44. 解析 <body>
55. 执行 <script> ← 目标脚本在这里
66. DOMContentLoaded
77. 加载资源
88. load 事件我在步骤2完成劫持,目标在步骤5才开始执行。等它醒来时,游戏已经结束了。
原型链劫持的威力
1// 我劫持后的状态
2EventTarget.prototype.addEventListener = 我的劫持版本
3
4// 目标尝试注册
5document.addEventListener('keydown', handler)
6 → 我的劫持版本.call(document, 'keydown', handler)
7 → return; // 未注册
8
9// 目标无法恢复
10// 原始函数保存在我的闭包中,外部无法访问目标可能尝试反制:
1// 尝试 1: 检测劫持
2if (EventTarget.prototype.addEventListener.toString().includes('blocked')) {
3 // 发现了,但无法恢复原始函数
4}
5
6// 尝试 2: 使用原始引用
7const original = EventTarget.prototype.addEventListener;
8// 已经是我劫持后的版本
9
10// 尝试 3: iframe 获取
11const iframe = document.createElement('iframe');
12const clean = iframe.contentWindow.EventTarget.prototype.addEventListener;
13// 我的 Tampermonkey 在所有 frame 执行我的防御体系
11. 事件劫持 ← 核心,摧毁目标的根基
22. Alert 拦截 ← 阻止骚扰
33. CSS 移除 ← 恢复功能
44. MutationObserver ← 实时响应
55. 定期清理 ← 持续保护
66. Console 监控 ← 早期预警即使某一层失效,其他层仍能工作。
我发现的目标弱点
单点依赖
目标的致命弱点:所有事件都通过 addEventListener 注册。我只需劫持这一个 API,整个防护体系就崩溃了。
固定 ID
1style.id = 'anti-cheat-style';太容易定位。我可以直接 getElementById 然后 remove()。
如果是我写反作弊,会用随机 ID:
1style.id = `ac-${Math.random().toString(36).substr(2, 9)}`;检查频率太慢
目标每3秒检查一次,我每2秒清理一次。在这个竞争中,我的清理频率始终更高,保持时间优势。
如果是我,会采用分层检查策略:
方案1:适度轮询(推荐)
1// 每秒检查一次,平衡性能和响应速度
2setInterval(checkAndActivate, 1000);方案2:事件驱动检查
1// 在关键时机触发检查,性能更优
2window.addEventListener('focus', checkAndActivate);
3document.addEventListener('visibilitychange', checkAndActivate);
4
5// 路由变化时检查
6const observer = new PerformanceObserver((list) => {
7 for (const entry of list.getEntries()) {
8 if (entry.entryType === 'navigation') {
9 checkAndActivate();
10 }
11 }
12});
13observer.observe({ entryTypes: ['navigation'] });方案3:轻量级 DOM 监控
1// 只监控 <head> 中样式元素的添加
2const observer = new MutationObserver((mutations) => {
3 for (const mutation of mutations) {
4 for (const node of mutation.addedNodes) {
5 if (node.tagName === 'STYLE' && node.id === 'anti-cheat-style') {
6 checkAndActivate();
7 break;
8 }
9 }
10 }
11});
12
13// 仅监控 <head> 的直接子节点添加,避免全局监控的性能开销
14observer.observe(document.head, {
15 childList: true,
16 subtree: false
17});注意事项:
-
❌ 避免:监控
document.documentElement+subtree: true- 会监控整个DOM树的所有变化
- 复杂页面可能每秒触发数百次
- 性能开销类似于 requestAnimationFrame
-
✅ 推荐:仅监控特定目标(如
document.head)- 精准定位,按需触发
- 性能影响可忽略不计
为什么不用 requestAnimationFrame?
- 60fps = 每秒60次调用,性能开销过大
- 反作弊检查不需要动画级别的精度
- 会影响页面流畅度和电池寿命
- 攻击者同样可以劫持 requestAnimationFrame
最佳实践:组合使用方案1和方案2,既保证覆盖面,又控制性能开销。
缺少完整性检查
目标从未检测 API 是否被劫持。
改进方案:
1const originalString = EventTarget.prototype.addEventListener.toString();
2
3function checkIntegrity() {
4 const current = EventTarget.prototype.addEventListener.toString();
5 if (current !== originalString) {
6 // 检测到劫持
7 }
8}但我可以进一步劫持 toString():
1Function.prototype.toString = function() {
2 if (this === EventTarget.prototype.addEventListener) {
3 return originalToString.call(originalAddEventListener);
4 }
5 return originalToString.call(this);
6};这会陷入无限攻防循环。
实战场景
场景1:用户按 Ctrl+C
目标预期:
keydown 事件 → universalBlock → preventDefault → alert → 失败
实际(我的脚本生效后):
keydown 事件 → 无监听器 → 浏览器默认行为 → 成功
场景2:用户选择文本
目标预期:
user-select: none → 无法选择
实际:
我的 bypass-style (user-select: text) → MutationObserver 监控 → 成功
场景3:3秒后目标重新激活
目标预期:
setInterval → 重新注册 → 重新注入 CSS → 恢复
实际:
1setInterval → 调用我劫持的 addEventListener → 未注册
2appendChild → 我的 MutationObserver 检测 → 立即移除 → 失败高级对抗
如果目标使用代码混淆
1var _0x1a2b=['addEventListener','keydown'];
2(function(_0x4a5b,_0x6c7d){
3 var _0x8e9f=function(_0x10g11){
4 while(--_0x10g11){
5 _0x4a5b['push'](_0x4a5b['shift']());
6 }
7 };
8})(_0x1a2b,0x123));混淆只增加阅读难度,不改变执行逻辑。我劫持底层 API,无需理解混淆代码。
如果目标使用 Iframe 沙箱
目标可能尝试通过iframe获取干净的API:
1const iframe = document.createElement('iframe');
2iframe.style.display = 'none';
3document.body.appendChild(iframe);
4
5// 从iframe获取未被劫持的API
6const clean = iframe.contentWindow.EventTarget.prototype.addEventListener;
7clean.call(document, 'keydown', handler);我的对策1:Tampermonkey 全局劫持
1// @match *://*/*
2// @run-at document-start
3// @grant none
4
5// Tampermonkey 默认在所有 frame 执行,包括动态创建的 iframe
6// 新创建的 iframe 内部的 API 也会被劫持我的对策2:监控iframe创建
1const originalCreate = document.createElement;
2document.createElement = function(tag) {
3 const el = originalCreate.call(document, tag);
4 if (tag.toLowerCase() === 'iframe') {
5 // 使用原始的 addEventListener(保存在闭包中)
6 originalAddEventListener.call(el, 'load', () => {
7 // iframe加载后,劫持其内部的API
8 if (el.contentWindow) {
9 const iframeProto = el.contentWindow.EventTarget.prototype;
10 const iframeOriginal = iframeProto.addEventListener;
11 iframeProto.addEventListener = function(type, listener, options) {
12 if (blockedEvents.includes(type)) return;
13 return iframeOriginal.call(this, type, listener, options);
14 };
15 }
16 });
17 }
18 return el;
19};局限性:如果iframe使用了严格的同源策略或sandbox属性,访问contentWindow可能被阻止。但此时目标脚本同样无法访问iframe内部的API。
如果目标使用 Service Worker
1navigator.serviceWorker.register('/anti-cheat-sw.js');我的对策:
1const originalRegister = navigator.serviceWorker.register;
2navigator.serviceWorker.register = function(...args) {
3 return Promise.reject(new Error('Blocked'));
4};我利用的 JavaScript 特性
1// JavaScript 允许我重写任何东西
2Object.prototype.toString = function() { return 'hacked'; };
3window.alert = function() { console.log('blocked'); };
4EventTarget.prototype.addEventListener = function() {};JavaScript 没有真正的私有成员。所有对象都可以被我访问和修改。即使使用 Object.freeze(),我也可以在冻结前劫持。
闭包保护代码
1(function () {
2 const original = EventTarget.prototype.addEventListener;
3
4 EventTarget.prototype.addEventListener = function(...args) {
5 return original.call(this, ...args);
6 };
7})();
8
9// 外部无法访问 original
10// 目标无法恢复原始函数结论
客户端防护永远无法做到完全安全。执行时机、原型链劫持、JavaScript 动态特性让攻击方天生占优势。
真正的安全必须依赖服务器端验证。客户端反作弊只能作为辅助手段,结合行为分析、多因素验证才能提高门槛。
但对于纯客户端防护,永远有办法绕过。
References
[1] Google Developers. (n.d.). Chrome extensions API reference. https://developer.chrome.com/docs/extensions/reference/
[2] Google Developers. (n.d.). How browsers work. Web.dev. https://web.dev/articles/howbrowserswork
[3] Greasespot Wiki. (n.d.). Greasemonkey manual: API. https://wiki.greasespot.net/Greasemonkey_Manual:API
[4] Mozilla. (n.d.). Closures. MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
[5] Mozilla. (n.d.). Content security policy (CSP). MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
[6] Mozilla. (n.d.). Critical rendering path. MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/Performance/Critical_rendering_path
[7] Mozilla. (n.d.). Event.stopImmediatePropagation(). MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/API/Event/stopImmediatePropagation
[8] Mozilla. (n.d.). EventTarget.addEventListener(). MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
[9] Mozilla. (n.d.). MutationObserver. MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
[10] Mozilla. (n.d.). Same-origin policy. MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
[11] OWASP Foundation. (n.d.). Web security testing guide. https://owasp.org/www-project-web-security-testing-guide/
[12] PortSwigger. (n.d.). DOM-based cross-site scripting. Web Security Academy. https://portswigger.net/web-security/cross-site-scripting/dom-based
[13] PortSwigger. (n.d.). Prototype pollution. Web Security Academy. https://portswigger.net/web-security/prototype-pollution
[14] Tampermonkey. (n.d.). Documentation. https://www.tampermonkey.net/documentation.php