某招聘网站的反调试分析与绕过总结
1. 网站反调试技术
分析文件:app.80fffb17.js
1.1 入口:abFn -> noDebug
// app.80fffb17.js:49815
(0, g.abFn)();
// app.80fffb17.js:52425-52431
e.abFn = function() {
var t, e = null === (t = window.iGeekRoot) || void 0 === t || null === (t = t.aBService) || void 0 === t || null === (t = t.abData) || void 0 === t || null === (t = t.nd_result_13912_number_1) || void 0 === t ? void 0 : t.result;
try {
e !== "Q,M,&L,b".split(",").join("") && (0, r.noDebug)({
env: "prod",
appName: "zhipin_geek_spa_web"
}, o.ndObj.cb)
} catch (t) { console.log(t) }
}
含义:业务入口触发 abFn,再根据条件进入 noDebug。
1.2 快捷键拦截(F12 / Ctrl+Shift+I/J / Ctrl+U/S)
// app.80fffb17.js:52696-52705
function rt(t) {
const e = $[g(P)] ? (t, e) => t.metaKey && t.altKey && (73 === e || 74 === e) : (t, e) => t.ctrlKey && t.shiftKey && (73 === e || 74 === e),
n = $[g(P)] ? (t, e) => t.metaKey && t.altKey && 85 === e || t.metaKey && 83 === e : (t, e) => t.ctrlKey && (83 === e || 85 === e);
t.addEventListener(g("`;^<[S:5c45I"), r => {
const o = (r = r || t.event).keyCode || r.which;
if (123 === o || e(r, o) || n(r, o)) return function(t, e) {
return (e = e || t.event).returnValue = !1, e.preventDefault(), !1
}(t, r)
}, !0)
}
含义:阻止常见打开开发者工具和查看源码的快捷键。
1.3 隐藏 iframe 取“干净原生 API”
// app.80fffb17.js:52670-52687
function J(t) {
var e;
try {
W || (W = document.createElement("iframe"), W.style.display = "none", document.body.appendChild(W));
if (Array.isArray(t)) return t.map(t => {
var e;
return null === (e = W.contentWindow) || void 0 === e ? void 0 : e[t]
});
return null === (e = W.contentWindow) || void 0 === e ? void 0 : e[t]
} catch (e) {
return Array.isArray(t) ? t.map(t => window[t]) : window[t]
}
}
含义:通过 iframe.contentWindow 获取未被主窗口 hook 的原生方法,绕过简单补丁。
// app.80fffb17.js:53067-53070
(() => {
if (!t) {
for (const t of Bt) t.type, t.XCID();
et()
}
}, 500)
// app.80fffb17.js:52973-52980
XCID() {
var t, e;
const n = g(a), r = g("[d^;oS:pcSVI"), o = g("`QW]`S:5"), i = g(s), u = g("wvWc0Mpcd^][Q{jX:rbu>;");
(!0 === (null === (e = null === (t = window[n]) || void 0 === t ? void 0 : t[r]) || void 0 === e ? void 0 : e[o]) || window[i] && window.document.querySelector(u)) && this[g(c)]()
}
// app.80fffb17.js:52944-52946
}[g(c)]() {
this.seact(), _t(xt), ht(), this.type
}
含义:每 500ms 执行检测器;命中 DevTools 条件后调用 onDevToolOpen,最终执行 ht()。
1.5 触发后的破坏动作(核心)
// app.80fffb17.js:52873-52905
try {
window[g("c:Jwc1l")] = null, window[g("c:Jwcu>;")](g(""), g("q:WwcSr;"))
} catch (t) { window.console.log(t) }
try {
window[g("X;pbIn;")]()
} catch (t) { window.console.log(t) }
try {
window[g("`dvswS:vX5I")][g("Xvdc`9>;")]()
} catch (t) { window.console.log(t) }
dt(() => {
try { document[g(d)][g("`^R|[^ZeoaND")] = g("") } catch (t) { window.console.log(t) }
try { window.location[g("`SBw[u>;")] = g("") } catch (t) { window.console.log(t) }
!function() {
const t = document.createElement("style"),
e = [g("`Sr}cG>;"), g(d), g("XQJ~")],
n = ["filter: blur(20px) !important", "display: none !important", "visibility: hidden !important", "opacity: 0 !important"],
r = Math.floor(Math.random() * n.length);
e.forEach(e => { t.textContent = `${e} { ${n[r]} }` }), document[g(ut)].appendChild(t)
}()
}, 100)
含义:打开空页、关闭/后退、清空 body、改 location.href、注入隐藏样式,组合拳让页面不可用。
1.6 反篡改失败后的惩罚(内存炸弹)
// app.80fffb17.js:52817-52830
function lt(t) {
let e = { success: !0, methods: {} };
try {
t.forEach(t => {
let n = J(t);
if (t.includes(".")) {
const [e, r] = t.split(".");
n = window[e][r]
}
n && /\[native code\]/.test(n.toString()) ? e.methods[t] = !0 : (e.success = !1, e.methods[t] = !1)
})
} catch (t) {}
return e
}
// app.80fffb17.js:52707-52735
function ot() {
try {
const t = [], e = () => {
for (let e = 0; e < 1e3; e++) {
const n = {};
for (let t = 0; t < 1e3; t++) n[`key_${e}_${t}`] = "x".repeat(1e3);
t.push(n)
}
const e = [];
for (let t = 0; t < 100; t++) e.push(new Array(1e4).fill("JBwd{b5S=[d^pg@M9`^rw0{3vd:cd{+bdWpX^g/[QBk1"));
t.push(...e)
};
e();
const r = window.setInterval(() => {
try {
const e = [];
for (let t = 0; t < 1e3; t++) e.push(new Array(1e4).fill("x"));
t.push(...e)
} catch (t) { window.clearInterval(r) }
}, 10)
} catch (t) {}
}
含义:检测到“方法被改”后,触发内存/CPU 压制。
1.7 调用链
abFn 执行(49815)
- 条件触发
noDebug(52428)
- 检测轮询
for (const t of Bt) t.XCID()(53069)
- 检测命中 ->
onDevToolOpen(52944-52946)
onDevToolOpen 调 ht()(52945)
ht() 执行跳转/清空/样式破坏(52873-52905)
2. 通过 Hook 定位到关键代码
这一段是定位思路。
2.1 第一步:先抓“谁在跳转”
先在 document-start 注入最小 trap,只拦截并打印这些高价值 sink:
window.open
location.assign/replace/reload
location.href setter
history.back/go/forward
核心思路是:不先猜业务代码,而是先抓最终破坏动作调用者。
2.2 第二步:从运行时栈反推到 bundle 位置
最早抓到的关键日志形态是:
[REDIRECT-TRAP] open()
from: https://www.zhipin.com/web/geek/jobs
to:
stack:
at ht (app.80fffb17.js:54:43498)
at Et.onDevToolOpen (app.80fffb17.js:54:44611)
at Et.XCID (app.80fffb17.js:54:48576)
这个栈直接给出了关键链路:
XCID(检测器)
onDevToolOpen(触发点)
ht(执行破坏动作)
于是再回到本地快照 app.80fffb17.js 按这些符号/模式定位,最终落到:
ht():app.80fffb17.js:52838 附近
onDevToolOpen([g(c)]()):app.80fffb17.js:52944-52946
XCID():app.80fffb17.js:52973、53069 附近轮询调用
2.3 第三步:确认“它会绕过主窗口 hook”
在定位阶段发现 J(t) 会创建隐藏 iframe,并从 contentWindow 取原生方法:
// app.80fffb17.js:52670-52687
W || (W = document.createElement("iframe"), W.style.display = "none", document.body.appendChild(W));
return W.contentWindow[t];
这解释了为什么“只 hook 主窗口 API”一开始拦不全。
2.4 第四步:定位时使用的关键 Hook 代码(精简版)
// 关键:统一记录 from/to/stack
function trap(action, target) {
const stack = new Error('[REDIRECT-TRAP] stack').stack;
console.groupCollapsed(`[REDIRECT-TRAP] ${action}`);
console.log('from:', location.href);
console.log('to:', target);
console.log(stack);
console.groupEnd();
}
// 关键:先抓 window.open
const rawOpen = window.open;
window.open = function (...args) {
trap('open()', args[0]);
return rawOpen.apply(this, args);
};
// 关键:抓 location.href setter
const proto = Object.getPrototypeOf(window.location);
const hrefDesc = Object.getOwnPropertyDescriptor(proto, 'href');
Object.defineProperty(proto, 'href', {
configurable: true,
get: hrefDesc.get,
set(url) {
trap('location.href =', url);
return hrefDesc.set.call(this, url);
}
});
这套“先抓 sink -> 看 stack -> 回 bundle 对位”的方法,能在混淆代码里快速找到真正生效的反调试逻辑,而不是在大包里盲猜。
3. 绕过策略
实现文件:hook.js(Tampermonkey,@run-at document-start)
3.1 总体策略
- 不改站点 bundle,走运行时 hook。
- 主目标是阻断“破坏动作”(跳转、清空 DOM、样式遮蔽、惩罚定时器)。
- 避免误伤正常业务定时器,最后收敛为“高置信规则”。
3.2 跳转/刷新/窗口动作统一拦截
// hook.js:205-229(节选)
patchMethod(win.Location.prototype, 'assign', (args) => args[0], { lock: true, tag: prefix });
patchMethod(win.Location.prototype, 'replace', (args) => args[0], { lock: true, tag: prefix });
patchMethod(win.Location.prototype, 'reload', () => '[reload]', { lock: true, tag: prefix });
patchWindowLocationSetter(win, prefix); // window.location= / document.location=
patchMethod(win, 'open', (args) => args[0], { lock: true, tag: prefix });
patchMethod(win, 'stop', () => '[stop]', { lock: true, tag: prefix });
patchMethod(win, 'close', () => '[close]', { lock: true, tag: prefix });
含义:直接封堵 ht() 里的主要跳转/关闭路径。
3.3 DOM 破坏防护
// hook.js:295-311(节选)
if (this === document.body && stackMatched(stack)) {
console.warn('[REDIRECT-TRAP] blocked body.innerHTML overwrite');
return;
}
return innerHTMLDesc.set.call(this, value);
// hook.js:327-335(节选)
const suspiciousStyle = /blur\(20px\)|display:\s*none|visibility:\s*hidden|opacity:\s*0/i.test(css);
if (suspiciousStyle && stackMatched(stack)) {
console.warn('[REDIRECT-TRAP] blocked suspicious STYLE append');
return node;
}
含义:阻断“清空 body + 注入隐藏样式”。
3.4 解决“隐藏 iframe 绕过 hook”
// hook.js:255-260(节选)
function patchIframeNode(node, reason) {
if (!node || node.tagName !== 'IFRAME') return;
const install = () => {
if (node.contentWindow) installWindowGuards(node.contentWindow, `iframe(${reason})`);
};
install();
}
// hook.js:336-354(节选)
const result = rawAppendChild.call(this, node);
patchIframeNode(node, 'appendChild');
...
const result = rawInsertBefore.call(this, newNode, referenceNode);
patchIframeNode(newNode, 'insertBefore');
含义:页面新增 iframe 时,把同样防护注入到 iframe.contentWindow,补上绕过口。
3.5 定时器策略:从“全拦截”收敛到“高置信拦截”
// hook.js:25-26
const STACK_FLAG = /onDevToolOpen|XCID|noDebug|(?:^|\W)abFn(?:\W|$)|(?:^|\W)ot(?:\W|$)/i;
const CALLBACK_FLAG = /new Array\(1e4\)\.fill\(['"]x['"]\)|['"]x['"]\.repeat\(1e4\)|nested_/i;
// hook.js:173-178
function shouldBlockTimer(callback) {
if (!CONFIG.blockSuspiciousTimers) return false;
const timerStack = getStack('[REDIRECT-TRAP] timer stack');
if (stackMatched(timerStack)) return true;
return callbackMatched(callback);
}
含义:只拦截反调试/内存炸弹定时器,不再误伤 Vue/路由/埋点的正常定时器。
3.6 当前状态
- 打开 DevTools 后不再被强制刷新或重定向。
- 页面能正常加载和操作。
- 如后续站点升级 hash 或策略,按同样方法重新抓栈和收敛规则即可。