Breaking Anti-Cheat: A Complete Guide from Event Hijacking to Prototype Chain Pollution
⚠️ Disclaimer
This content is intended solely for technical research and security education purposes.
- The code examples analyzed in this article are randomly selected technical samples and do not target any specific website, system, or organization
- The technical methods and code snippets in this article are used to illustrate web security principles and attack-defense mechanisms
- If the examples in this article are similar to any actual system, it is purely a technical coincidence and does not indicate that this article targets that system
- Readers must comply with the laws and regulations of their country and region when applying the techniques in this article
- Unauthorized testing or attacking others' systems is illegal, and the author assumes no responsibility for any abuse of the techniques in this article
- Readers are advised to conduct technical verification only in their own test environments or with explicit authorization
Technical research should serve to improve system security, not to disrupt legitimate services. Please use the knowledge in this article responsibly.
Analysis Target
The complete anti-cheat script I want to bypass:
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('⚠️ Shortcuts disabled to ensure fair assessment');
27 return false;
28 }
29
30 // Block F12
31 if (key === 'f12') {
32 e.preventDefault();
33 e.stopPropagation();
34 e.stopImmediatePropagation();
35 alert('⚠️ Developer tools disabled to ensure fair assessment');
36 return false;
37 }
38
39 // Block right click
40 if (type === 'contextmenu') {
41 e.preventDefault();
42 e.stopPropagation();
43 e.stopImmediatePropagation();
44 alert('⚠️ Right-click menu disabled to ensure fair assessment');
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' ? 'Copy' : type === 'cut' ? 'Cut' : 'Paste'} disabled to ensure fair assessment`
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})();My Attack Strategy Overview
| Target Protection | Implementation | My Bypass Method |
|---|---|---|
| Keyboard Event Interception | addEventListener('keydown') | Hijack EventTarget.prototype.addEventListener |
| F12 Interception | Detect key === 'f12' | Event listener not registered |
| Right-Click Menu Interception | addEventListener('contextmenu') | Event listener not registered |
| Copy/Paste Interception | addEventListener('copy/paste/cut') | Event listener not registered |
| Text Selection Disabled | CSS user-select: none | MutationObserver + Periodic Cleanup |
| Alert Warnings | alert() | Hijack window.alert |
| Periodic Check | setInterval(3000) | Faster cleanup frequency (2000) |
Core Breakthrough: Event Hijacking
Target's Protection Mechanism
After analyzing the target script, I discovered it intercepts events using the following method:
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});It intercepts in the capture phase using stopImmediatePropagation() to prevent propagation. This looks solid, but I found a fatal weakness.
My Solution
My bypass strategy is simple: hijack addEventListener itself before it registers events.
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};Implementation logic: When the target script calls document.addEventListener('keydown', universalBlock), it actually executes my hijacked version. I simply return empty, and its listener is never registered.
My Attack Timeline
1Phase 1: Early Page Load
2 My script executes (Tampermonkey @run-at document-start)
3 Hijack EventTarget.prototype.addEventListener
4
5Phase 2: Target Script Loads
6 Calls document.addEventListener()
7 Actually executes my hijacked version, listener not registered
8
9Phase 3: User Interaction
10 User presses Ctrl+C
11 Browser triggers keydown event
12 No listener responds
13 Copy succeedsTiming Advantage: My script executes at the document-start timing, tens to hundreds of milliseconds earlier than the target script (depending on network and script size). This time window is sufficient to complete the prototype chain hijacking.
CSS Countermeasures
Target's CSS Protection
The target script injects styles to disable selection:
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);My Multi-Layer Defense
I adopted a four-layer defense strategy:
Layer 1: Remove Target Styles
1function removeAntiCheatStyles() {
2 const style = document.getElementById('anti-cheat-style');
3 if (style) {
4 style.remove();
5 }
6}Layer 2: Inject My Counter Styles
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}Layer 3: Real-time DOM Monitoring
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// Only monitor direct child additions to <head>, avoiding performance overhead
13observer.observe(document.head, {
14 childList: true,
15 subtree: false
16});Layer 4: Periodic Cleanup
1function periodicCleanup() {
2 removeAntiCheatStyles();
3 setTimeout(periodicCleanup, 2000);
4}Alert Interception
I hijacked the alert function to block annoying popups:
1const originalAlert = window.alert;
2
3window.alert = function (message) {
4 if (message && typeof message === 'string' && message.includes('to ensure fair assessment')) {
5 return;
6 }
7 return originalAlert.call(window, message);
8};Time Competition Advantage
I designed a faster cleanup frequency than the target:
| Time | Target Behavior | My Behavior |
|---|---|---|
| 0s | First activation, inject styles | Hijacking complete, remove styles |
| 2s | - | 1st cleanup |
| 3s | Check, attempt to re-inject | Already cleaned |
| 4s | - | 2nd cleanup |
| 6s | Check, MutationObserver detects immediately | Remove immediately |
I maintain a 1-second time advantage.
Execution Timing is Key
I exploited the browser loading flow:
11. Parse HTML
22. Tampermonkey (@run-at document-start) ← My script here
33. Parse <head>
44. Parse <body>
55. Execute <script> ← Target script here
66. DOMContentLoaded
77. Load resources
88. load eventI complete hijacking in step 2, while the target starts executing in step 5. By the time it wakes up, the game is already over.
The Power of Prototype Chain Hijacking
1// After my hijacking
2EventTarget.prototype.addEventListener = My hijacked version
3
4// Target attempts to register
5document.addEventListener('keydown', handler)
6 → My hijacked version.call(document, 'keydown', handler)
7 → return; // Not registered
8
9// Target cannot recover
10// Original function saved in my closure, inaccessible externallyTarget might attempt countermeasures:
1// Attempt 1: Detect hijacking
2if (EventTarget.prototype.addEventListener.toString().includes('blocked')) {
3 // Discovered, but cannot recover original function
4}
5
6// Attempt 2: Use original reference
7const original = EventTarget.prototype.addEventListener;
8// Already my hijacked version
9
10// Attempt 3: Obtain from iframe
11const iframe = document.createElement('iframe');
12const clean = iframe.contentWindow.EventTarget.prototype.addEventListener;
13// My Tampermonkey executes in all framesMy Defense System
11. Event Hijacking ← Core, destroys target's foundation
22. Alert Interception ← Blocks harassment
33. CSS Removal ← Restores functionality
44. MutationObserver ← Real-time response
55. Periodic Cleanup ← Continuous protection
66. Console Monitoring ← Early warningEven if one layer fails, other layers still work.
Target Weaknesses I Discovered
Single Point of Dependency
The target's fatal weakness: all events are registered through addEventListener. I only need to hijack this one API, and the entire protection system collapses.
Fixed ID
1style.id = 'anti-cheat-style';Too easy to locate. I can directly getElementById then remove().
If I were writing anti-cheat, I would use a random ID:
1style.id = `ac-${Math.random().toString(36).substr(2, 9)}`;Check Frequency Too Slow
The target checks every 3 seconds, I clean every 2 seconds. In this competition, my cleanup frequency is consistently higher, maintaining a time advantage.
If I were implementing this, I would adopt a layered checking strategy:
Approach 1: Moderate Polling (Recommended)
1// Check once per second, balancing performance and response speed
2setInterval(checkAndActivate, 1000);Approach 2: Event-Driven Checking
1// Trigger checks at critical moments, better performance
2window.addEventListener('focus', checkAndActivate);
3document.addEventListener('visibilitychange', checkAndActivate);
4
5// Check on route changes
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'] });Approach 3: Lightweight DOM Monitoring
1// Only monitor style element additions in <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// Only monitor direct child node additions in <head>, avoiding global monitoring performance overhead
14observer.observe(document.head, {
15 childList: true,
16 subtree: false
17});Precautions:
-
❌ Avoid: Monitoring
document.documentElement+subtree: true- Monitors all changes to the entire DOM tree
- May trigger hundreds of times per second on complex pages
- Performance overhead similar to requestAnimationFrame
-
✅ Recommended: Only monitor specific targets (like
document.head)- Precise targeting, triggered on demand
- Performance impact negligible
Why not use requestAnimationFrame?
- 60fps = 60 calls per second, excessive performance overhead
- Anti-cheat checks don't need animation-level precision
- Affects page smoothness and battery life
- Attackers can also hijack requestAnimationFrame
Best practice: Combine Approach 1 and Approach 2 to ensure coverage while controlling performance overhead.
Missing Integrity Check
The target never detects if APIs have been hijacked.
Improvement approach:
1const originalString = EventTarget.prototype.addEventListener.toString();
2
3function checkIntegrity() {
4 const current = EventTarget.prototype.addEventListener.toString();
5 if (current !== originalString) {
6 // Hijacking detected
7 }
8}But I can further hijack toString():
1Function.prototype.toString = function() {
2 if (this === EventTarget.prototype.addEventListener) {
3 return originalToString.call(originalAddEventListener);
4 }
5 return originalToString.call(this);
6};This leads to an infinite attack-defense loop.
Real-World Scenarios
Scenario 1: User Presses Ctrl+C
Target's expectation:
keydown event → universalBlock → preventDefault → alert → Fail
Actual (after my script takes effect):
keydown event → No listener → Browser default behavior → Success
Scenario 2: User Selects Text
Target's expectation:
user-select: none → Cannot select
Actual:
My bypass-style (user-select: text) → MutationObserver monitoring → Success
Scenario 3: Target Reactivates After 3 Seconds
Target's expectation:
setInterval → Re-register → Re-inject CSS → Recover
Actual:
1setInterval → Calls my hijacked addEventListener → Not registered
2appendChild → My MutationObserver detects → Remove immediately → FailAdvanced Countermeasures
If Target Uses Code Obfuscation
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));Obfuscation only increases reading difficulty, it doesn't change execution logic. I hijack low-level APIs, no need to understand obfuscated code.
If Target Uses Iframe Sandbox
Target might attempt to obtain clean APIs through iframe:
1const iframe = document.createElement('iframe');
2iframe.style.display = 'none';
3document.body.appendChild(iframe);
4
5// Obtain unhijacked API from iframe
6const clean = iframe.contentWindow.EventTarget.prototype.addEventListener;
7clean.call(document, 'keydown', handler);My Countermeasure 1: Tampermonkey Global Hijacking
1// @match *://*/*
2// @run-at document-start
3// @grant none
4
5// Tampermonkey executes in all frames by default, including dynamically created iframes
6// APIs inside newly created iframes are also hijackedMy Countermeasure 2: Monitor iframe Creation
1const originalCreate = document.createElement;
2document.createElement = function(tag) {
3 const el = originalCreate.call(document, tag);
4 if (tag.toLowerCase() === 'iframe') {
5 // Use original addEventListener (saved in closure)
6 originalAddEventListener.call(el, 'load', () => {
7 // After iframe loads, hijack its internal APIs
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};Limitation: If iframe uses strict same-origin policy or sandbox attributes, accessing contentWindow may be blocked. But in that case, the target script also cannot access APIs inside the iframe.
If Target Uses Service Worker
1navigator.serviceWorker.register('/anti-cheat-sw.js');My countermeasure:
1const originalRegister = navigator.serviceWorker.register;
2navigator.serviceWorker.register = function(...args) {
3 return Promise.reject(new Error('Blocked'));
4};JavaScript Features I Exploited
1// JavaScript allows me to rewrite anything
2Object.prototype.toString = function() { return 'hacked'; };
3window.alert = function() { console.log('blocked'); };
4EventTarget.prototype.addEventListener = function() {};JavaScript has no true private members. All objects can be accessed and modified by me. Even using Object.freeze(), I can hijack before freezing.
Closure-Protected Code
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// External code cannot access original
10// Target cannot recover original functionConclusion
Client-side protection can never be completely secure. Execution timing, prototype chain hijacking, and JavaScript's dynamic nature give the attacker an inherent advantage.
True security must rely on server-side validation. Client-side anti-cheat can only serve as an auxiliary measure, combined with behavior analysis and multi-factor authentication to raise the barrier.
But for pure client-side protection, there's always a way to bypass it.
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