背景
在进行在线考试时,面对有限的时间压力,系统的前端限制(禁止 F12、禁止复制、禁止粘贴、甚至页面死锁)严重阻碍了正常的作答效率。通过对超星学习通前端安全机制长达数个版本的深度逆向分析与对抗,我构建了一条从 DOM 突破到 JS 原型链劫持,再到事件捕获层阻断的完整攻防链路,彻底瓦解了其前端防御体系。
第一步:突破 F12 开发者工具(根节点反逃逸 Hook)
点击 F12 打开开发者工具时,页面瞬间卡死,触发了无限 debugger 断点拦截。
限制机制与对抗升级
最初,我们通过重写全局的 window.Function 成功拦截了 new Function('debugger')。但随后发现,超星的前端混淆代码使用了“原型链构造器逃逸”技术:
// 超星绕过 window.Function 的经典手法
(function anonymous() {}).constructor("debugger")();
这种写法直接调用了 JavaScript 底层的 Function.prototype.constructor,完美避开了我们在全局对象上设下的陷阱。
逆向解除(Root Prototype Hooking)
要彻底粉碎这种逃逸,我们必须将防线下沉,直接劫持所有函数的“祖宗”节点:
const originalFunction = window.Function;
const blockDebugger = function(...args) {
const fnStr = args[args.length - 1];
if (typeof fnStr === 'string' && fnStr.includes('debugger')) {
return function() {}; // 替换为空函数,静默失效
}
return originalFunction.apply(this, args);
};
// 1. 拦截全局调用
window.Function = blockDebugger;
// 2. 核心杀招:拦截原型链逃逸调用
Function.prototype.constructor = blockDebugger;
第二步:解决题干无法复制的问题(事件捕获层阻断)
起初,我们认为题干无法复制仅仅是因为 notAllowCopy.css 中的 user-select: none。但在强行覆盖 CSS 后,发现依然无法选中。这说明超星启用了 JavaScript 事件级拦截(如 onselectstart = return false 和 oncopy 拦截)。
攻防推导:降维打击 JS 拦截器
如果我们在冒泡阶段去解除限制,往往会被超星底层的框架死死卡住。真正的打击是实施“事件捕获层阻断(Event Capture Interception)”。
我们在浏览器事件流的最顶层(捕获阶段),直接把 copy、selectstart 事件的传播给掐断,让超星的拦截函数变成“瞎子”。
// 顶层事件拦截(掐断超星的 JS 拦截器)
const allowEvents = ['contextmenu', 'copy', 'cut', 'paste', 'selectstart'];
allowEvents.forEach(ev => {
document.documentElement.addEventListener(ev, function(e) {
e.stopPropagation(); // 阻止事件向下传播到超星的拦截器!
}, true); // true 代表在捕获阶段执行,拥有最高优先级
});
配合定时器高频清除行内属性(应对 Ajax 动态加载的题目),复制与右键菜单被完美解锁。
第三步:突破答案输入框的粘贴限制(原型链劫持)
这是整个攻防链路中最核心的一环。超星通过在 UEditor 实例上绑定 beforepaste 事件来清空剪贴板。
从“事后清理”到“事前拦截”
依赖 DOM 加载完毕后去执行 removeListener 会因为执行时机滞后而彻底失效。我们必须在页面任何脚本执行前(@run-at document-start),直接修改 UEditor 的底层图纸,让它先天丧失绑定粘贴拦截器的能力。
let _UE;
Object.defineProperty(window, 'UE', {
get: function() { return _UE; },
set: function(val) {
_UE = val;
if (_UE && _UE.Editor && _UE.Editor.prototype) {
const originalAddListener = _UE.Editor.prototype.addListener;
_UE.Editor.prototype.addListener = function(types, listener) {
// 核心:无论前端传入什么拦截函数,只要是粘贴事件,直接丢弃!
if (typeof types === 'string' && types.indexOf('paste') !== -1) {
return this;
}
return originalAddListener.apply(this, arguments);
};
}
},
configurable: true
});
注:在早期版本中,我们曾尝试冻结全局的 editorPaste 变量,但这会导致超星后续的代码抛出 SyntaxError,从而引发“下一题”和“提交”按钮失效的级联崩溃。V6.3 版本果断废弃了该做法,仅保留底层劫持,实现了完美的无痕突破。
第四步:应对极端情况(紧急救援防死锁模块)
在实战中,由于网络波动或脚本冲突,超星页面有时会出现“点击提交没反应”、“一直显示正在提交”或“被透明遮罩层卡死”的死锁状态。
为此,我开发了一个悬浮的“🆘 解除死锁”模块。它精准映射了超星底层的业务锁变量,并能强行恢复 DOM 交互权限:
// 1. 暴力释放超星原生业务锁
if (typeof window.submitLock !== 'undefined') window.submitLock = 0;
if (typeof window.saveLock !== 'undefined') window.saveLock = false;
// 2. 隐藏超星专属遮罩层与弹窗(不破坏DOM结构,防止后续报错)
document.querySelectorAll('.maskDiv, .mask-no-bg, #worktoast').forEach(mask => mask.style.display = 'none');
// 3. 恢复页面整体交互权限
document.body.style.pointerEvents = "auto";
第五步:终极突破——iframe 内核级粘贴劫持(Final Layer Exploit)
在完成了原型链劫持之后,理论上所有 beforepaste 拦截已经失效。但在实际测试中,我发现仍然存在“无法粘贴”的情况。经过进一步逆向分析,问题的根源逐渐浮出水面:
UEditor 的真正输入环境,并不在主页面,而是在一个独立的 iframe 中。
攻防盲区:跨上下文事件隔离
此前所有的防御绕过,都是基于主文档(document)层完成的:
- 捕获阶段拦截
paste - 阻断
addListener - 清除 DOM 行内事件
但浏览器的事件系统存在一个关键特性:
iframe 内部拥有独立的事件流系统
也就是说:
主页面 document → ❌ 无法影响 → iframe.contentDocument
这就导致:
- 我们在外层阻断的
paste - 在 iframe 内仍然可以被重新捕获并处理
- 从而触发超星内部的“二次清洗机制”
这也是为什么在 V6.3 阶段,“理论成功但实际失败”的核心原因。
降维打击:直接接管 iframe 事件流
既然问题出在 iframe 内部,那么解决方案也很直接:
进入 iframe 内部,接管它的 paste 事件
核心思路是:
- 获取所有编辑器 iframe
- 注入事件监听(捕获阶段)
- 阻断原始事件传播
- 手动写入剪贴板内容
实现代码如下:
function hookIframePaste() {
document.querySelectorAll('iframe').forEach(frame => {
try {
const doc = frame.contentDocument;
if (!doc || doc._mist_hooked) return; doc._mist_hooked = true; // 捕获 paste(优先级最高)
doc.addEventListener('paste', function(e) {
e.stopImmediatePropagation(); const text = (e.clipboardData || window.clipboardData).getData('text'); try {
// 标准路径
doc.execCommand('insertText', false, text);
} catch(err) {
// fallback(兼容低版本)
const sel = doc.getSelection();
if (sel && sel.rangeCount) {
sel.deleteFromDocument();
sel.getRangeAt(0).insertNode(doc.createTextNode(text));
}
}
}, true); } catch(err) {
// 跨域 iframe 自动跳过
}
});
}
动态环境对抗:持续扫描机制
由于题目是通过 Ajax 动态加载的,iframe 也会不断生成,因此必须采用“持续接管策略”:
setInterval(hookIframePaste, 1000);
这一机制确保:
- 新生成的编辑器 ✔
- 翻页后的输入框 ✔
- 延迟加载的 iframe ✔
全部自动纳入控制。
🚀 终极自动化实现(V6.4)
结合上述所有原理,构建最终的自动化用户脚本。将以下代码放入 Tampermonkey 中即可实现全自动降维打击。
// ==UserScript==
// @name 超星学习通考试限制解除器(V6.4 )
// @namespace http://tampermonkey.net/
// @version 6.4
// @description 底层协议拦截粘贴 + 根节点反逃逸 + 强制解除选中复制 + 悬浮死锁解除
// @author Mist Vulnerability Assistant
// @match *://*.chaoxing.com/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function() {
'use strict';
console.log("[Mist] V6.4");
// ========================================================================
// 维度一:UEditor 原型链劫持 (保证输入框能粘贴)
// ========================================================================
let _UE;
Object.defineProperty(window, 'UE', {
get: function() { return _UE; },
set: function(val) {
_UE = val;
if (_UE && _UE.Editor && _UE.Editor.prototype) {
const originalAddListener = _UE.Editor.prototype.addListener;
_UE.Editor.prototype.addListener = function(types, listener) {
if (typeof types === 'string' && types.indexOf('paste') !== -1) {
return this; // 丢弃粘贴拦截
}
return originalAddListener.apply(this, arguments);
};
const originalGetEditor = _UE.getEditor;
_UE.getEditor = function(id, opt) {
if (opt) {
opt.pasteplain = false;
opt.disablePasteImage = false;
}
return originalGetEditor.call(this, id, opt);
};
}
},
configurable: true
});
// ========================================================================
// 维度二:无限 Debugger 绕过 (根节点反逃逸 Hook)
// ========================================================================
const originalFunction = window.Function;
const blockDebugger = function(...args) {
const fnStr = args[args.length - 1];
if (typeof fnStr === 'string' && fnStr.includes('debugger')) {
return function() {};
}
return originalFunction.apply(this, args);
};
window.Function = blockDebugger;
window.Function.prototype = originalFunction.prototype;
Function.prototype.constructor = blockDebugger; // 封杀构造器逃逸
const originalEval = window.eval;
window.eval = function(string) {
if (typeof string === 'string' && string.includes('debugger')) return;
return originalEval.apply(this, arguments);
};
// ========================================================================
// 维度三:终极选中与复制解锁 (事件捕获层阻断 + CSS 暴力覆盖)
// ========================================================================
function injectUnlockCSS() {
if (document.getElementById('mist-unlock-css')) return;
const style = document.createElement('style');
style.id = 'mist-unlock-css';
style.textContent = `
html:not(input):not(textarea):not(select):not(option):not(button),
html, body, *, [class*="notAllowCopy"] {
-webkit-touch-callout: text !important;
-webkit-user-select: text !important;
-khtml-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
pointer-events: auto !important;
}
::selection { background: #3390FF !important; color: #fff !important; }
`;
(document.head || document.documentElement).appendChild(style);
}
// 顶层事件拦截(掐断超星的 JS 拦截器)
const allowEvents = ['contextmenu', 'copy', 'cut', 'paste', 'selectstart', 'dragstart', 'mousedown', 'mouseup'];
allowEvents.forEach(ev => {
document.documentElement.addEventListener(ev, function(e) {
e.stopPropagation();
}, true);
});
// 高频动态清场(应对 Ajax 动态加载的题目)
function clearInlineHandlers() {
injectUnlockCSS();
const elements = [document, window, document.body];
elements.forEach(el => {
if (el) {
el.oncontextmenu = null;
el.onselectstart = null;
el.ondragstart = null;
el.oncopy = null;
el.oncut = null;
}
});
document.querySelectorAll('[aria-hidden="true"][tabindex]').forEach(el => {
el.removeAttribute('aria-hidden');
});
}
clearInlineHandlers();
window.addEventListener('DOMContentLoaded', clearInlineHandlers);
setInterval(clearInlineHandlers, 2000);
// ========================================================================
// 维度四:紧急救援模块(防卡死/强行解锁)
// ========================================================================
window.addEventListener('DOMContentLoaded', () => {
const rescueBtn = document.createElement('div');
rescueBtn.innerHTML = ' 解除死锁';
rescueBtn.title = '当点击提交没反应、或者页面被遮罩层卡死时点击此按钮';
rescueBtn.style.cssText = `
position: fixed; top: 20px; right: 20px; z-index: 9999999;
background: #ff4d4f; color: white; padding: 8px 12px;
border-radius: 4px; cursor: pointer; font-size: 14px;
font-weight: bold; box-shadow: 0 4px 6px rgba(0,0,0,0.3);
user-select: none; transition: all 0.3s;
`;
rescueBtn.onmouseover = () => rescueBtn.style.transform = 'scale(1.05)';
rescueBtn.onmouseout = () => rescueBtn.style.transform = 'scale(1)';
rescueBtn.onclick = function() {
if (typeof window.submitLock !== 'undefined') window.submitLock = 0;
if (typeof window.saveLock !== 'undefined') window.saveLock = false;
document.querySelectorAll('.maskDiv, .mask-no-bg, .popSetupShowHide, #worktoast, #workpop, .maskBox').forEach(mask => {
if (mask) mask.style.display = 'none';
});
document.body.style.pointerEvents = "auto";
document.body.style.overflow = "auto";
document.querySelectorAll('.completeBtn, .jb_btn, a[onclick*="submit"]').forEach(btn => {
btn.style.pointerEvents = "auto";
btn.style.opacity = "1";
btn.removeAttribute('disabled');
});
if(confirm("UI 限制已解除!是否需要进一步强行终止所有后台定时器?\n\n警告:这会同时停止考试倒计时!\n仅在页面一直显示'正在提交...'且无法恢复时点击【确定】。")) {
let highestId = window.setTimeout(function() {}, 0);
for (let i = 0; i < highestId; i++) {
window.clearTimeout(i);
window.clearInterval(i);
}
}
alert("页面死锁已解除!您可以重新点击保存或提交。");
};
document.body.appendChild(rescueBtn);
});
// ========================================================================
// 维度五:UEditor iframe 内核级粘贴劫持(真正破解点)
// ========================================================================
function hookIframePaste() {
document.querySelectorAll('iframe').forEach(frame => {
try {
const doc = frame.contentDocument;
if (!doc || doc._mist_hooked) return;
doc._mist_hooked = true;
console.log("[Mist] 已接管 iframe:", frame);
// 捕获粘贴事件(最关键)
doc.addEventListener('paste', function(e) {
e.stopImmediatePropagation();
const text = (e.clipboardData || window.clipboardData).getData('text');
// 强制插入内容
try {
doc.execCommand('insertText', false, text);
} catch(err) {
// fallback
const sel = doc.getSelection();
if (sel && sel.rangeCount) {
sel.deleteFromDocument();
sel.getRangeAt(0).insertNode(doc.createTextNode(text));
}
}
console.log("[Mist] iframe 粘贴已注入");
}, true);
} catch(err) {
// 跨域 iframe 忽略
}
});
}
// 持续扫描 iframe(应对动态加载)
setInterval(hookIframePaste, 1000);
})();
总结反思
通过这次实战,揭示了前端安全对抗的几个核心法则:
- 执行时机即是最高权限(Timing is Everything):利用
@run-at document-start配合Object.defineProperty,在目标代码执行前“篡改规则”,使防御机制在初始化阶段直接瘫痪。 - 事件流的降维打击:面对复杂的 JS 冒泡拦截,直接在捕获阶段调用
stopPropagation()掐断事件传播,是破解前端防复制的最优解。 - 防线崩溃的蝴蝶效应:在对抗中要注意避免引发目标业务逻辑的
SyntaxError,否则会导致页面正常功能(如提交试卷)瘫痪。防守的最高境界是“无痕替换”。
所有纯客户端的安全限制在掌握了底层执行流的攻击者面前都是透明的,真正的业务安全必须建立在服务端严格的数据校验之上。