January 9, 2026
18 浏览量
Welcome

破解反作弊:从事件劫持到原型链污染的完整攻略

破解反作弊:从事件劫持到原型链污染的完整攻略

⚠️ 免责声明

本文内容仅用于技术研究与安全教育目的。

  1. 本文所分析的代码示例均为随机选取的技术样本,不针对任何特定网站、系统或组织
  2. 文中涉及的技术方法和代码片段仅用于说明 Web 安全原理和攻防机制
  3. 如文中示例与任何实际系统存在相似之处,纯属技术巧合,不代表本文针对该系统
  4. 读者在实际应用本文技术时,必须遵守所在国家和地区的法律法规
  5. 未经授权对他人系统进行测试或攻击属于违法行为,作者对任何滥用本文技术的行为不承担任何责任
  6. 建议读者仅在自己拥有的测试环境或获得明确授权的情况下进行技术验证

技术研究应当服务于提升系统安全性,而非破坏合法服务。请负责任地使用本文知识。


分析目标

我要绕过的反作弊脚本完整代码:

javascript
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: noneMutationObserver + 定期清理
弹窗警告alert()劫持 window.alert
定期检查setInterval(3000)更快的清理频率(2000)

核心突破:事件劫持

目标的防护机制

我分析了目标脚本,发现它使用以下方式拦截事件:

javascript
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 本身。

javascript
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) 时,实际执行的是我劫持后的版本。我直接返回空,它的监听器永远不会被注册。

我的攻击时序

text
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防护

目标脚本注入了禁用选择的样式:

javascript
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);

我的多层对策

我采用了四层防御策略:

第一层:移除目标样式

javascript
1function removeAntiCheatStyles() {
2    const style = document.getElementById('anti-cheat-style');
3    if (style) {
4        style.remove();
5    }
6}

第二层:注入我的对抗样式

javascript
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 实时监控

javascript
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});

第四层:定期清理

javascript
1function periodicCleanup() {
2    removeAntiCheatStyles();
3    setTimeout(periodicCleanup, 2000);
4}

Alert 拦截

我劫持了 alert 函数来阻止烦人的弹窗:

javascript
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秒的时间优势。

执行时机是关键

我利用了浏览器加载流程:

text
11. 解析 HTML
22. Tampermonkey (@run-at document-start) ← 我的脚本在这里
33. 解析 <head>
44. 解析 <body>
55. 执行 <script> ← 目标脚本在这里
66. DOMContentLoaded
77. 加载资源
88. load 事件

我在步骤2完成劫持,目标在步骤5才开始执行。等它醒来时,游戏已经结束了。

原型链劫持的威力

javascript
1// 我劫持后的状态
2EventTarget.prototype.addEventListener = 我的劫持版本
3
4// 目标尝试注册
5document.addEventListener('keydown', handler)
6  → 我的劫持版本.call(document, 'keydown', handler)
7return; // 未注册
8
9// 目标无法恢复
10// 原始函数保存在我的闭包中,外部无法访问

目标可能尝试反制:

javascript
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 执行

我的防御体系

text
11. 事件劫持      ← 核心,摧毁目标的根基
22. Alert 拦截    ← 阻止骚扰
33. CSS 移除      ← 恢复功能
44. MutationObserver ← 实时响应
55. 定期清理      ← 持续保护
66. Console 监控  ← 早期预警

即使某一层失效,其他层仍能工作。

我发现的目标弱点

单点依赖

目标的致命弱点:所有事件都通过 addEventListener 注册。我只需劫持这一个 API,整个防护体系就崩溃了。

固定 ID

javascript
1style.id = 'anti-cheat-style';

太容易定位。我可以直接 getElementById 然后 remove()

如果是我写反作弊,会用随机 ID:

javascript
1style.id = `ac-${Math.random().toString(36).substr(2, 9)}`;

检查频率太慢

目标每3秒检查一次,我每2秒清理一次。在这个竞争中,我的清理频率始终更高,保持时间优势。

如果是我,会采用分层检查策略:

方案1:适度轮询(推荐)

javascript
1// 每秒检查一次,平衡性能和响应速度
2setInterval(checkAndActivate, 1000);

方案2:事件驱动检查

javascript
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 监控

javascript
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 是否被劫持。

改进方案:

javascript
1const originalString = EventTarget.prototype.addEventListener.toString();
2
3function checkIntegrity() {
4    const current = EventTarget.prototype.addEventListener.toString();
5    if (current !== originalString) {
6        // 检测到劫持
7    }
8}

但我可以进一步劫持 toString():

javascript
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 → 恢复

实际:

text
1setInterval → 调用我劫持的 addEventListener → 未注册
2appendChild → 我的 MutationObserver 检测 → 立即移除 → 失败

高级对抗

如果目标使用代码混淆

javascript
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:

javascript
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 全局劫持

javascript
1// @match *://*/*
2// @run-at document-start
3// @grant none
4
5// Tampermonkey 默认在所有 frame 执行,包括动态创建的 iframe
6// 新创建的 iframe 内部的 API 也会被劫持

我的对策2:监控iframe创建

javascript
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

javascript
1navigator.serviceWorker.register('/anti-cheat-sw.js');

我的对策:

javascript
1const originalRegister = navigator.serviceWorker.register;
2navigator.serviceWorker.register = function(...args) {
3    return Promise.reject(new Error('Blocked'));
4};

我利用的 JavaScript 特性

javascript
1// JavaScript 允许我重写任何东西
2Object.prototype.toString = function() { return 'hacked'; };
3window.alert = function() { console.log('blocked'); };
4EventTarget.prototype.addEventListener = function() {};

JavaScript 没有真正的私有成员。所有对象都可以被我访问和修改。即使使用 Object.freeze(),我也可以在冻结前劫持。

闭包保护代码

javascript
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

喜欢这篇文章吗?

分享给你的朋友和同事吧!

Welcome
Last updated: January 9, 2026
相关文章
正在检查服务状态...
破解反作弊:从事件劫持到原型链污染的完整攻略 - ICTRUN