猿人学第二题 js混淆-动态cookie1
# h1 猿人学第二题 js混淆-动态cookie1刚进入网页遇见无限debugger,
使用油猴脚本
```
// ==UserScript==
// @name Leave-debugger
// @namespace https://github.com/SherryBX/Leave-debugger
// @version v2.2.0
// @description用于破解网页无限debugger,支持多种调试方式拦截
// @AuThor Sherry
// @match *://*/*
// @include *://*/*
// @run-at document-start
// @license MIT
// @Icon https://mms0.baidu.com/it/u=2886239489,318124131&fm=253&app=138&f=JPEG?w=800&h=800
// @downloadURLhttps://update.greasyfork.org/scripts/524858/Leave-debugger.user.js
// @updateURL https://update.greasyfork.org/scripts/524858/Leave-debugger.meta.js
// ==/UserScript==
(function () {
'use strict';
// 配置项
const CONFIG = {
version: 'v2.2.0',
debugMode: false, // 调试模式开关
checkPatterns: ['debugger', 'debug', 'DevTools'], // 检查的关键字模式
};
// 统一的日志输出
const Logger = {
styles: {
main: 'color: #43bb88; font-size: 14px; font-weight: bold;',
info: 'color: #666; font-size: 12px;',
hook: 'color: #43bb88;'
},
print(message, style = 'main') {
console.log(`%c ${message}`, this.styles);
},
debug(...args) {
if (CONFIG.debugMode) {
console.log('', ...args);
}
}
};
// Hook 状态管理
const HookManager = {
notified: new Set(),
markNotified(type) {
if (!this.notified.has(type)) {
this.notified.add(type);
Logger.print(`🎯 Hook ${type} debugger!`, 'hook');
}
}
};
// 工具函数
const Utils = {
// 安全地检查函数字符串
safeToString(func) {
try {
const str = Function.prototype.toString.call(func);
return typeof str === 'string' ? str.replace(/\s+/g, '') : '';
} catch (e) {
Logger.debug('toString error:', e);
return '';
}
},
// 检查是否包含调试相关代码
containsDebugger(content) {
if (!content) return false;
return CONFIG.checkPatterns.some(pattern => content.includes(pattern));
},
// 创建空函数
createEmptyFunction() {
return function () { return -1; };
}
};
// Hook 实现
const Hooks = {
// Hook Function constructor
hookConstructor() {
const original = Function.prototype.constructor;
Function.prototype.constructor = function (string) {
if (Utils.containsDebugger(string)) {
HookManager.markNotified('constructor');
return Utils.createEmptyFunction();
}
return original.apply(this, arguments);
};
},
// Hook setInterval
hookSetInterval() {
const original = window.setInterval;
window.setInterval = function (func, delay) {
if (typeof func === 'function' && Utils.containsDebugger(Utils.safeToString(func))) {
HookManager.markNotified('setInterval');
return Utils.createEmptyFunction();
}
return original.apply(this, arguments);
};
},
// Hook setTimeout
hookSetTimeout() {
const original = window.setTimeout;
window.setTimeout = function (func, delay) {
if (typeof func === 'function' && Utils.containsDebugger(Utils.safeToString(func))) {
HookManager.markNotified('setTimeout');
return Utils.createEmptyFunction();
}
return original.apply(this, arguments);
};
},
// Hook eval
hookEval() {
const original = window.eval;
window.eval = function (string) {
if (Utils.containsDebugger(string)) {
HookManager.markNotified('eval');
string = string.replace(/debugger\s*;?/g, '');
}
return original.call(this, string);
};
// 保持 toString 的原始行为
Object.defineProperty(window.eval, 'toString', {
value: function() { return original.toString(); },
configurable: false,
writable: false
});
}
};
// 错误处理
const ErrorHandler = {
setup() {
window.addEventListener('error', function (event) {
if (event.error?.message?.includes('Cannot read properties') ||
event.error?.message?.includes('Cannot set property')) {
event.preventDefault();
Logger.debug('Prevented error:', event.error.message);
return false;
}
}, true);
}
};
// 初始化
function initialize() {
Logger.print('Leave-debugger 已启动 🚀');
Logger.print(`Version: ${CONFIG.version} 📦`, 'info');
Logger.print('Author: Sherry 🛡️', 'info');
// 应用所有 hooks
Object.values(Hooks).forEach(hook => {
try {
hook();
} catch (e) {
Logger.debug('Hook error:', e);
}
});
// 设置错误处理
ErrorHandler.setup();
}
// 启动脚本
initialize();
})();
```
成功过掉debugger,
打开F12,观察请求数据包,发现要逆向的是cookie中的m参数,使用Hook代码进行hook
```
(function get_cookie(){
var cookie_temp =''
Object.defineProperty(document,"cookie",{
set : function(val){
if (val.indexOf('m') !=-1){
debugger;
}
console.log('Hook捕获到m设置->',val);
cookie_temp = val;
return val;
},
get : function(){
return cookie_temp;
}
});
})();
```
hook成功如下
[!(https://s41.ax1x.com/2026/09/15/pnnu6pV.png)](https://imgchr.com/i/pnnu6pV)
向上跟栈
[!(https://s41.ax1x.com/2026/09/15/pnnusf0.png)](https://imgchr.com/i/pnnusf0)
```
document[$dbsm_0x17c7('\x30\x78\x39\x36', '\x72\x26\x50\x55') + $dbsm_0x17c7('\x30\x78\x34\x65\x33', '\x50\x21\x5a\x40')] = _0x2528b8['\x43\x78\x6a' + '\x4b\x44'](_0x2528b8['\x6b\x78\x50' + '\x5a\x4c'](_0x2528b8['\x65\x42\x74' + '\x51\x49'](_0x2528b8['\x64\x53\x4c' + '\x51\x51'](_0x2528b8[$dbsm_0x17c7('\x30\x78\x32\x64\x37', '\x58\x73\x6f\x38') + '\x51\x51'](_0x2528b8[$dbsm_0x17c7('\x30\x78\x64\x37', '\x74\x6a\x55\x58') + '\x47\x75']('\x6d', _0x2528b8[$dbsm_0x17c7('\x30\x78\x33\x63\x33', '\x38\x29\x36\x5b') + '\x4c\x5a'](_0x37bc5d)), '\x3d'), _0x2528b8['\x54\x63\x58' + '\x42\x47'](_0xd59548, _0x2fa5cc)), '\x7c'), _0x2fa5cc), _0x2528b8[$dbsm_0x17c7('\x30\x78\x31\x38', '\x68\x7a\x32\x76') + '\x41\x5a']);
```
发现hook跟栈断住的是这里,document[]里面的肯定是"cookie"不用想,经过验证确实是这样
[!(https://s41.ax1x.com/2026/09/15/pnnurYq.png)](https://imgchr.com/i/pnnurYq)
这么一看结构就很清晰了=左边的可以不用要,=号右边的保留,这样的话 ,
```
m = _0x2528b8['\x54\x63\x58' + '\x42\x47'](_0xd59548, _0x2fa5cc) + '|' + _0x2fa5cc;
```
其中_0x2528b8是个对象,_0x2528b8['\x54\x63\x58' + '\x42\x47']是个函数跟过去查看是
```
_0x16ad56[$dbsm_0x17c7('\x30\x78\x34\x61\x61', '\x57\x24\x42\x61') + '\x42\x47'] = function(_0xe39150, _0x27d0fc) {
return _0xe39150(_0x27d0fc);
}
```
因此可以继续对m进行改写,其中_0x2fa5cc是13位时间戳,可以自己生成,那么剩下来的就是扣代码了,重点是 _0xd59548这个函数
```
m = _0xd59548(_0x2fa5cc) + '|' + _0x2fa5cc;
```
扣代码时会遇见反调试,扣代码时遇见的问题,可以丢给AI进行分析
# h3 1.`$dbsm_0x17c7` 里的反调试
```
var _0x89d3a3 = function(_0x3e259d) {
this['lYIPmV'] = _0x3e259d;
this['TWAexT'] = ;
this['VvPjIw'] = function() { return 'newState'; };
this['ecEyCl'] = '\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*';
this['ypByWx'] = '[\x27|\x22].+[\x27|\x22];?\x20*}';
};
_0x89d3a3['prototype']['IoDeMC'] = function() {
var _0x383f0b = new RegExp(this['ecEyCl'] + this['ypByWx']);
var _0x197f1f = _0x383f0b['test'](this['VvPjIw']['toString']()) ? --this['TWAexT'] : --this['TWAexT'];
return this['zFlzrQ'](_0x197f1f);
};
_0x89d3a3['prototype']['zFlzrQ'] = function(_0x358e5b) {
if (!Boolean(~_0x358e5b)) return _0x358e5b;
return this['xEYmTx'](this['lYIPmV']);
};
_0x89d3a3['prototype']['xEYmTx'] = function(_0x28831c) {
for (var _0x4e3e26 = 0x0, _0x165d64 = this['TWAexT']['length']; _0x4e3e26 < _0x165d64; _0x4e3e26++) {
this['TWAexT']['push'](Math['round'](Math['random']()));
_0x165d64 = this['TWAexT']['length']; // 每次循环都更新长度,导致永远循环
}
return _0x28831c(this['TWAexT']);
};
new _0x89d3a3($dbsm_0x17c7)['IoDeMC']();
```
原理:
- +正则 `\w+ *\(\) *{\w+ *['|"].+['|"];? *}` 用来检测 `VvPjIw.toString()` 是否保持原始格式(即 `function() { return 'newState'; }` 这种单行形式)。
- 如果代码被格式化(比如有换行、缩进),正则匹配失败,则 `_0x197f1f = --this['TWAexT']`,`TWAexT` 初始为 ``,`--` 后 `TWAexT` 变成 `0`。
- 然后 `zFlzrQ` 中 `~0 = -1` 为真,进入 `xEYmTx`。
- `xEYmTx` 里的 `for` 循环条件 `_0x4e3e26 < _0x165d64`,而 `_0x165d64` 在循环体内被更新为 `this['TWAexT'].length`,每次 `push` 后长度增加,条件永远成立,数组无限膨胀,最终抛出 `RangeError: Invalid array length`,浏览器卡死。
去除方法:
直接注释掉最后一行
```
// new _0x89d3a3($dbsm_0x17c7)['IoDeMC']();
```
# h3 2. `_0xf0a64d` 检测 `$dbsm_0x3db563` 与正则篡改
```
if (_0x2528b8['\x71\x4d\x4c' + '\x53\x41'] === _0x2528b8['\x31\x39\x35' + '\x66\x62']) {
var _0x284d57 = new RegExp(YSjRgp['\x30\x33' + '\x54\x6c']);
var _0x11be10 = new RegExp(YSjRgp['\x64\x66' + '\x4c\x61'], 'i');
var _0x267fad = YSjRgp['\x63\x74\x74' + '\x59\x50']($dbsm_0x3db563, YSjRgp['\x33\x30' + '\x69\x59']);
if (!_0x284d57['\x74\x65\x73\x74'](YSjRgp['\x68\x71\x7a' + '\x6d\x49'](_0x267fad, YSjRgp['\x31\x62\x37' + '\x64\x63'])) ||
!_0x11be10['\x74\x65\x73\x74'](YSjRgp['\x32\x39\x61' + '\x6d\x49'](_0x267fad, YSjRgp['\x33\x30\x66' + '\x57\x55']))) {
YSjRgp['\x33\x37\x35' + '\x6e\x5a'](_0x267fad, '0'); // 篡改 toString
} else {
YSjRgp['\x34\x31\x34' + '\x46\x78']($dbsm_0x3db563); // 调用死循环函数
}
} else {
var _0x350b74 = !![];
return function(_0x2d6199, _0x88c083) { ... };
}
```
原理:
- `$dbsm_0x3db563` 是源代码末尾定义的一个全局函数,内部包含死循环 + `debugger`。
- 这里用两个正则检查 `$dbsm_0x3db563.toString()` 是否被 Hook 或篡改。如果检测到异常,会执行 `_0x267fad('0')` 修改 `Function.prototype.toString`;如果正常,则直接调用 `$dbsm_0x3db563()`,从而触发死循环。
- 条件 `_0x2528b8['\x71\x4d\x4c' + '\x53\x41'] === _0x2528b8['\x31\x39\x35' + '\x66\x62']` 在正常环境下可能为假,从而走 `else` 分支返回一个无害函数。但为了保险,扣代码时最好直接修改。
去除方法:
- 将整个 `if` 条件改为 `if (false)`,或者直接删除 `_0xf0a64d` 的立即执行。
- 或者将 `$dbsm_0x3db563` 函数体清空,让它什么都不做。
# h3 3.`_0x2fc380` 中的 `console.log` 检测与死循环
原理:
- `_0x2fc380` 返回一个函数,该函数内部会检测 `console.log` 是否被改写。如果发现 `console.log` 被 Hook,会进入 `while(1){ console.log(...); debugger; }` 死循环。
- 同时还会检测一些字符串比较,决定是否执行 `_0x112ece` 等加密函数。
去除方法:
- 在扣代码时,直接注释掉 `_0x2fc380` 的调用,或者修改其内部条件,使其永远返回无害函数。
# h3 4. `_0x13168f` —— 纯粹的死循环 + `debugger`
```
function _0x13168f(_0x109608, _0x338448) {
if (_0x2528b8['\x34\x65' + '\x48\x42'](_0x2528b8['\x34\x61\x64' + '\x56\x53'], _0x2528b8['\x37\x62' + '\x56\x53'])) {
while (0x1) {
console['\x6c\x6f\x67'](_0x2528b8['\x6a\x55\x6e' + '\x4d\x57']);
debugger ;
}
} else {
...
}
}
```
原理:
- 当两个字符串比较相等时,进入 `while(1)`,不断输出日志并触发 `debugger`,让调试者无法继续。
去除方法:
- 修改条件为 `if (false)`,或者直接让函数返回。
# h3 5. `qz` + `eval` 注入:重写 `console.log` 进行爆破
```
qz = ; // 一段编码的 JS
_0x2528b8['\x79\x4f\x75' + '\x61\x51'](eval, _0x2528b8['\x31\x31\x34' + '\x61\x51'](_0x1c1e7a, qz));
```
原理:
- `qz` 是一个字节数组,通过 `_0x1c1e7a` 解码成字符串,然后 `eval` 执行。
- 解码后的代码会重写 `console.log`,让它变成:
```
console.log = function(s) {
while (1) {
for (i = 0; i < 11000000; i++) {
history.pushState(0, 0, i);
}
}
}
```
这样一旦调用 `console.log`,就会疯狂 `pushState`,导致浏览器卡死。
去除方法:
- 直接不执行这段 `eval`。在扣代码时,将 `_0x37bc5d` 整体替换为一个空函数或直接 `return`。
# h3 6. `setInterval` 定时自检
```
setInterval(function() {
var _0x16d82d = {};
_0x16d82d[...] = function(_0x451cb5) { return _0x451cb5(); };
var _0x4a97db = _0x16d82d;
_0x4a97db[...]($dbsm_0x3db563);
}, 0xfa0);
```
`$dbsm_0x102537` 内部:
```
_0x2528b8[...](setInterval, _0x2528b8[...](_0x37bc5d), 0x1f4);
```
原理:
- 每 500ms 或 4000ms 执行一次 `_0x37bc5d` 或 `$dbsm_0x3db563`,进行环境检测。如果发现 `console.log` 被改写、`debugger` 被禁用等,就触发死循环。
去除方法:
注释掉这些 `setInterval` 调用。
# h3 7. `$dbsm_0x3db563` 自身的死循环
```
function $dbsm_0x3db563(_0x44d65f) {
var _0xb52b3b = {};
...
try {
if (_0x44d65f) {
return _0x2e4a16;
} else {
_0xe0f8ba[...](_0x2e4a16, 0x0);
}
} catch (_0x360549) {}
}
```
原理:
该函数内部包含大量反调试检测,例如检查 `console.log`、`debugger` 等。当参数为假时,会执行 `_0x2e4a16(0)`,最终可能进入死循环。
去除方法:
- 不调用它,或者将其函数体清空。
# h3 8.`$dbsm_0x17c7` 开头的 `_0x220a58` 中的 `removeCookie.toString()` 检测
```
var _0x1bdccb = function() {
var _0x464f3f = new RegExp('\\w+ *\\(\\) *{\\w+ *[\'|"].+[\'|"];? *}');
return _0x464f3f['test'](_0x5e1b08['removeCookie']['toString']());
};
_0x5e1b08['updateCookie'] = _0x1bdccb;
var _0x81818a = _0x5e1b08['updateCookie']();
if (!_0x81818a) {
_0x5e1b08['setCookie'](['*'], 'counter', 0x1);
} else if (_0x81818a) {
_0x12a868 = _0x5e1b08['getCookie'](null, 'counter');
} else {
_0x5e1b08['removeCookie']();
}
```
原理:
- 用正则检测 `removeCookie.toString()` 是否被格式化。如果被格式化,正则匹配失败,`_0x81818a` 为 `false`,然后执行 `setCookie(['*'], 'counter', 0x1)`。
- 这看起来是正常的 cookie 初始化,但它的存在是为了检测 `removeCookie` 是否被 Hook 或修改。如果被修改,可能会影响后续 `$dbsm_0x17c7` 的解密(因为 `_0x220a58` 内部调用了 `_0x3aaf1f` 来旋转数组)。
去除方法:
- 通常不需要特别处理,因为即使走了 `setCookie` 分支,也不影响 `$dbsm_0x17c7` 的解密。但如果发现数组旋转次数不对,可以检查这里。
- 在我扣代码中,这个 IIFE 被保留,但数组旋转逻辑被用 `(function(arr, n) { while (--n) { arr.push(arr.shift()); } })($dbsm_0xde03, 0x1d1);` 手动实现了,所以不受影响。
# h3 9. `_0x5e9e66` 和 `_0x5aa3d8` 的调用
```
_0x2528b8[...](_0x5e9e66, _0x2528b8[...](_0x5aa3d8));
```
原理:
这两个函数内部包含 `debuggerProtection` 等反调试逻辑,一旦执行会触发死循环或 `debugger`。
想要让扣的代码,真正的跑起来,就需要干掉反检测点,可以把扣的代码,丢给ai,ai是知道在哪些地方做了检测,应该如何处
理去过掉反检测。
好家伙,当我用Python去调用m的生成逻辑代码去请求时发现失败了,这题居然升级了,ai分析说升级重要的点在
m 的有效性取决于"是否用当次下发的挑战体计算",与算法实现是否正确无关。
ex.js (我本地生成m的js文件)作为静态文件,无法获取"当次挑战",因此***无论用什么语言去调用它,都拿不到有效 m***。
下面放一下过这题的js脚本,记得替换为你自己的sessionid
```
/**
* 猿人学第2题 - js混淆 动态cookie 1(纯 Node.js 协议采集,无浏览器参与)
*
* 接口链路:
* 1. GET https://match.yuanrenxue.cn/api/question/2?page=N&pageSize=10&kw=
* 未携带有效动态 Cookie 时返回 202,响应体 JSON.data 为该次下发的挑战 JS
* (OB 混淆,每次下发内容不同:变量名/数组轮转/嵌入的大常数均会变化)
* 2. 挑战 JS 执行后写入 document.cookie:
* m = _0xd59548(timestamp) + '|' + timestamp // MD5 链 + 13位毫秒时间戳
* 哈希与挑战体内容绑定,因此必须用"当次下发的挑战"计算 m
* 3. 携带 m=<hash>|<ts> Cookie 重试同一接口 → 200,返回页面数据
*
* 实现方式:vm 沙箱最小补环境执行挑战 JS,捕获 document.cookie 写入取 m。
* 补环境项:console / navigator / location / document.cookie(仅捕获)/
* 定时器置空(防反调试死循环)。均为最小必需,无真实浏览器。
*/
const https = require('https');
const vm = require('vm');
/* ---------------- 配置 ---------------- */
const CONFIG = {
host: 'match.yuanrenxue.cn',
apiPath: '/api/question/2',
pages: 5,
pageSize: 10,
sessionid: '替换为你自己的sessionid',
// 服务端校验 UA:必须为 yuanrenxue,否则返回占位字符串而非真实数据
ua: 'yuanrenxue',
};
/* ---------------- HTTP(原生 https + keep-alive) ---------------- */
const agent = new https.Agent({ keepAlive: true, maxSockets: 1 });
function request(page, cookie) {
return new Promise((resolve, reject) => {
const req = https.request({
host: CONFIG.host,
path: `${CONFIG.apiPath}?page=${page}&pageSize=${CONFIG.pageSize}&kw=`,
method: 'GET',
agent,
headers: {
'User-Agent': CONFIG.ua,
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest',
'Referer': 'https://match.yuanrenxue.cn/match/2',
'Cookie': cookie,
},
}, (res) => {
let body = '';
res.on('data', (c) => (body += c));
res.on('end', () => resolve({ status: res.statusCode, body }));
});
req.on('error', reject);
req.end();
});
}
/* ---------------- 挑战 JS 求解:vm 最小补环境,捕获 m ---------------- */
function solveChallenge(challengeJs) {
const captured = [];
const sandbox = {
console: { log: () => {}, warn: () => {}, info: () => {}, error: () => {}, debug: () => {} },
navigator: { userAgent: CONFIG.ua, appName: 'Netscape', platform: 'Win32' },
location: {
href: 'https://match.yuanrenxue.cn/match/2',
protocol: 'https:',
host: CONFIG.host,
hostname: CONFIG.host,
},
setInterval: () => 0, clearInterval: () => {},
setTimeout: () => 0, clearTimeout: () => {},
Date,
};
sandbox.window = sandbox;
sandbox.self = sandbox;
Object.defineProperty(sandbox.document || (sandbox.document = {}), 'cookie', {
get: () => captured.join('; '),
set: (v) => captured.push(v),
configurable: true,
});
vm.createContext(sandbox);
try {
vm.runInContext(challengeJs, sandbox, { timeout: 8000, displayErrors: true });
} catch (e) {
throw new Error('挑战 JS 执行失败: ' + e.message.slice(0, 120));
}
// 捕获形如 "m<suffix>=<32hex>|<13位ts>; path=/",取等号后的值
const joined = captured.join(';');
const m = joined.match(/=({32}\|\d{13})/);
if (!m) throw new Error('未捕获到 m 值, captured=' + joined.slice(0, 100));
return m;
}
/* ---------------- 主流程 ---------------- */
(async () => {
console.log('[*] 题目:第2题 - js混淆 动态cookie 1');
console.log('[*] 目标:采集 5 页数据并计算总和');
const all = [];
for (let page = 1; page <= CONFIG.pages; page++) {
let cookie = `sessionid=${CONFIG.sessionid}`;
let res = await request(page, cookie);
// 202 = 下发动态挑战,沙箱执行取 m 后重试
if (res.status === 202) {
const challengeJs = JSON.parse(res.body).data;
const m = solveChallenge(challengeJs);
res = await request(page, `${cookie}; m=${m}`);
}
if (res.status !== 200) {
throw new Error(`第 ${page} 页请求失败:HTTP ${res.status}`);
}
const rows = JSON.parse(res.body).data || [];
all.push(...rows);
console.log(`[+] 正在采集第 ${page}/${CONFIG.pages} 页... ✓ 获取 ${rows.length} 条数据`);
}
console.log(`[+] 采集完成,共 ${all.length} 条数据`);
const answer = all.reduce((s, x) => s + x, 0);
console.log('');
console.log('========== 计算结果 ==========');
console.log(`答案:${answer}`);
console.log('==============================');
})().catch((e) => {
console.error(' 失败:', e.message);
process.exit(1);
});
```
页:
[1]