当前位置: 首页 > news >正文

JS反调试破解:数据流驱动的加密定位与复现方法

1. 这道题不是考“能不能解”而是考“怎么稳准狠地定位到关键点”你打开浏览器开发者工具切到 Sources 面板刷新页面满屏的混淆代码像一堵密不透风的墙_0x4a3f[\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72]、function _0x2b8e(_0x1a9c, _0x4d2e)、还有嵌套七八层的立即执行函数。你下意识点开一个看起来像加密入口的函数进去一看——又是一个eval(atob(...))再进去又是一层Function(...)构造器……这不是 JS 逆向这是俄罗斯套娃式的精神污染。这就是第23题的真实开局。它不属于那种靠“全局搜索sign或token”就能蒙混过关的初级题它也不属于那种纯靠“扣代码手写 Python”硬刚的体力活。它的核心挑战在于调试链路被主动切断、关键逻辑被动态隔离、加密上下文被刻意污染——换句话说出题人不是在防你“解密”而是在防你“看清”。我试过三种典型失败路径第一种是直接在控制台里console.log所有疑似加密函数结果发现所有输出都是undefined或function() {}因为函数体被toString()重写过第二种是设断点后单步跳进但每次走到关键计算前代码就通过debugger检测或window.location.href跳转强制中断调试第三种是用 AST 工具反混淆结果生成的代码里塞满了if (Math.random() 0.99) throw new Error()这类干扰分支静态分析直接失效。这道题真正要解决的不是“RSA 怎么算”或者“AES 的 IV 怎么生成”而是如何在对抗性极强的 JS 环境中重建一条可信、可控、可复现的调试通路。它考验的是你对 Chrome DevTools 底层机制的理解深度对 JS 执行上下文生命周期的掌控能力以及对现代前端反调试手段的识别与绕过经验。如果你还在用“F8 单步走完所有函数”的方式做逆向那这道题大概率会卡你两小时以上最后靠运气猜出参数。而真正高效的解法是从一开始就放弃“跟着代码流走”转而用“数据流驱动调试”的思路——盯住你要的sign值从哪来、在哪变、被谁改然后逆向定位到那个唯一的数据交汇点。这才是第23题留给实战者的真正考题。2. 为什么常规断点全部失效——三重反调试机制的底层拆解这道题的调试障碍不是偶然堆砌的而是由三个相互嵌套、彼此验证的反调试层构成的闭环防御体系。它们不是简单地“加个 debugger”而是利用了 V8 引擎执行模型、DevTools 协议通信机制和 JS 运行时环境的固有特性。只有逐层击穿才能让断点真正“落得下去、停得住、看得清”。2.1 第一层动态 debugger 注入检测运行时心跳最表层的防御是高频、随机、不可预测的debugger语句。但它不是写死在源码里的而是通过eval动态注入的// 实际代码中类似这样的结构已脱敏 const _0x1c2d [debugger, location, href]; const _0x3e4f window[_0x1c2d[1]][_0x1c2d[2]]; if (_0x3e4f.includes(debug)) { eval(_0x1c2d[0]); // 只有在检测到调试痕迹时才触发 }关键点在于这个eval不是静态字符串而是拼接自数组索引且判断条件依赖于当前 URL 中是否含debug字样——这意味着你只要在地址栏手动加?debug1就会立刻触发。更麻烦的是它还配合了一个“心跳计时器”setInterval(() { if (performance.now() - lastDebugTime 1200) { debugger; // 如果上一次 debugger 触发间隔太短说明你在疯狂 F8 } }, 800);提示这种检测的本质是把“人工调试行为”转化为可量化的运行时指标时间间隔、URL 参数、调用栈深度。它不关心你有没有开 DevTools只关心你的操作是否符合“真实用户行为模式”。所以单纯禁用debugger是没用的必须阻断其检测逻辑。2.2 第二层断点位置指纹识别Sources 面板级对抗Chrome DevTools 在设置断点时会向 V8 发送一个包含文件路径、行号、列号的精确坐标请求。而这道题的混淆器在生成代码时会为每个函数注入一段“断点坐标校验逻辑”function encrypt(data) { // 此处插入校验如果当前执行位置的 source map 行号是 127列号是 45 // 则认为你在此处打了断点立即跳转到错误页面 const stack new Error().stack; if (stack.includes(at encrypt) stack.includes(:127:45)) { // 精确匹配你设断点的位置 window.location.replace(/error.html); } // ...真实加密逻辑 }这个技巧非常隐蔽它不阻止你设断点但一旦你单步执行到该函数内部它就通过解析Error.stack获取当前执行位置并与预设的“敏感断点坐标”比对。由于 Source Map 是公开的出题人完全可以提前算出你最可能打断点的几处位置比如encrypt函数开头、sign赋值前一行并针对性埋点。注意这种检测无法通过“禁用 Source Map”规避因为Error.stack返回的是实际执行的混淆后代码位置如bundle.min.js:127:45而非原始源码位置。你关掉 Source Map它照样能精准识别。2.3 第三层执行上下文污染与作用域隔离V8 引擎级干扰最致命的一层是彻底破坏你对变量作用域的信任。它不靠debugger而是用with语句 Proxyeval构建了一个“伪作用域沙箱”const sandbox new Proxy({}, { get(target, prop) { if (prop sign) return fake_sign_123; // 永远返回假值 if (prop data) return JSON.stringify({ fake: true }); return target[prop]; } }); with (sandbox) { // 所有在此块内访问的变量都会先经过 Proxy 拦截 const sign generateSign(data); // 这里拿到的 sign 是假的 console.log(sign); // 输出 fake_sign_123但真实 sign 在别处生成 }更绝的是它还会在关键函数内部动态创建eval上下文function doEncrypt() { const realData getData(); // 真实数据在这里 const fakeCtx { data: ${JSON.stringify(realData)}, sign: }; eval(with(${fakeCtx}) { sign calc(realData); // 注意这里 realData 是闭包变量但被 with 覆盖了 return sign; }); }此时你在doEncrypt内部打的断点看到的data是伪造的sign是空字符串而真正的加密结果藏在eval外部的闭包里——你根本看不到它被赋值的过程。这三层机制共同构成了一个“检测-响应-混淆”的闭环第一层让你不敢轻易 F8第二层让你不敢随便打断点第三层让你即使断下来也看不懂。破解它的唯一路径不是硬刚而是绕开整个“代码流”直击“数据流”。3. 数据流驱动调试法从 sign 输出反推加密入口的完整链路面对上述三重防御我放弃了“从网络请求找加密函数”的传统路径转而采用“输出倒推法”既然最终要提交的sign值一定会出现在某个 XHR 请求的 body 或 query 参数里那就从这个确定的输出点出发逆向追踪它在 JS 中的诞生过程。这种方法不依赖代码可读性只依赖数据在内存中的真实流转路径天然免疫大部分反调试干扰。3.1 第一步精准捕获 sign 的最终输出位置首先我需要确认sign是以什么形式发出的。打开 Network 面板筛选 XHR找到目标请求通常是/api/submit或/v1/login点击进入切换到 Payload 标签页。这里显示的是请求体内容例如{ username: test, password: 123456, sign: a1b2c3d4e5f67890 }关键动作右键点击这个请求 → “Break on request” → “Request sent”。这会在请求即将发出的瞬间暂停 JS 执行此时调用栈清晰可见且所有相关变量都处于活跃状态。经验不要选 “Response received”因为响应阶段 sign 已经发出去了你只能看到结果看不到生成过程。必须卡在“发送前”这一刻这是整个调试链路的黄金锚点。3.2 第二步从调用栈向上追溯定位 sign 的直接来源暂停后查看右侧 Call Stack 面板。通常你会看到类似这样的栈send xhr.js:45 request api.js:128 submitForm login.js:89 onclick login.html:1双击最顶层的submitForm或类似名称的函数跳转到其源码。此时注意不要急着看函数体先按CtrlShiftOWindows或CmdShiftOMac打开“快速文件搜索”输入sign看看当前文件里是否有sign 或sign:的赋值语句。果然在submitForm函数末尾附近找到了这一行const payload { username: user, password: pwd, sign: window.__encrypt__(user, pwd) // 关键sign 来自这个函数调用 };注意window.__encrypt__是一个全局函数但它在 Sources 面板里搜不到定义——说明它是在运行时动态挂载的。此时不要去 Sources 里找它而是回到 Console 面板直接输入window.__encrypt__.toString()输出结果是function __encrypt__(a, b) { [native code] }哦它被Object.defineProperty隐藏了源码但没关系我们继续用数据流法在 Console 里执行debug(window.__encrypt__)这会在__encrypt__函数入口处自动设置一个断点比手动点 Sources 面板更可靠因为它不依赖 Source Map 行号。然后按 F8 继续执行程序会停在__encrypt__的第一行。3.3 第三步在encrypt内部用“变量监控”替代“单步执行”现在你停在了__encrypt__的入口。传统做法是 F10/F11 单步但这里会立刻触发第二层的“断点坐标检测”。换一种思路按CtrlShiftYWindows或CmdShiftYMac打开“Watch”面板添加两个监控表达式a即 username 参数b即 password 参数arguments查看所有传入参数然后按 F8 让它跑完。你会发现a和b的值是正确的但函数返回值却是个乱码字符串。说明加密逻辑不在函数体第一层而在某个深层调用里。此时不要单步而是右键点击__encrypt__函数名 → “Set breakpoint on function call”。这会在每次调用该函数时暂停但更重要的是它会记录下所有调用上下文。接着再次触发登录程序会在__encrypt__入口暂停。这次按F9Resume script execution让它跑起来同时紧盯 Console 面板——因为很多混淆代码会在加密过程中console.log中间值用于调试而这些日志往往没被删干净。果然在__encrypt__执行中途Console 输出了一行[DEBUG] raw input: {u:test,p:123456}这个raw input就是加密的原始数据它提示我们真正的加密函数可能叫rawInputToSign或类似名字。立刻在 Console 里搜索for (let key in window) { if (typeof window[key] function key.includes(raw)) { console.log(key, window[key].toString().slice(0, 50)); } }输出中有一项_rawEncrypt: function _rawEncrypt(e){return e.split().map...Bingo这就是我们要找的函数。现在对_rawEncrypt执行debug(_rawEncrypt)再触发登录程序就会停在这个函数内部——而这里没有debugger没有with污染没有坐标检测因为它是被__encrypt__动态调用的不在初始防御名单里。3.4 第四步在 _rawEncrypt 中锁定非对称加密的核心参数进入_rawEncrypt后代码依然混淆但结构清晰多了function _rawEncrypt(e) { const t e.split(); // e 是 raw input 字符串 const n t.map((a, i) a.charCodeAt(0) ^ i); // 异或混淆 const r new Uint8Array(n); const o window.crypto.subtle.importKey( jwk, { /* 这里是一段超长的 JWK 密钥对象 */ }, { name: RSA-OAEP, hash: SHA-256 }, false, [encrypt] ); return o.then(k window.crypto.subtle.encrypt(RSA-OAEP, k, r)); }关键发现密钥是以 JWKJSON Web Key格式硬编码在代码里的虽然被 Base64 编码过但它是静态的、不变的。我复制那段 JWK 字符串从{开始到}结束粘贴到 VS Code 里用 Prettier 格式化然后提取出n模数和e公钥指数字段{ kty: RSA, n: tXJzZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFk......## 1. 这道题不是考“能不能解”而是考“怎么稳准狠地定位到关键点” 你打开浏览器开发者工具切到 Sources 面板刷新页面满屏的混淆代码像一堵密不透风的墙_0x4a3f[\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72]、function _0x2b8e(_0x1a9c, _0x4d2e)、还有嵌套七八层的立即执行函数。你下意识点开一个看起来像加密入口的函数进去一看——又是一个 eval(atob(...))再进去又是一层 Function(...) 构造器……这不是 JS 逆向这是俄罗斯套娃式的精神污染。 这就是第23题的真实开局。它不属于那种靠“全局搜索 sign 或 token”就能蒙混过关的初级题它也不属于那种纯靠“扣代码手写 Python”硬刚的体力活。它的核心挑战在于**调试链路被主动切断、关键逻辑被动态隔离、加密上下文被刻意污染**——换句话说出题人不是在防你“解密”而是在防你“看清”。 我试过三种典型失败路径第一种是直接在控制台里 console.log 所有疑似加密函数结果发现所有输出都是 undefined 或 function() {}因为函数体被 toString() 重写过第二种是设断点后单步跳进但每次走到关键计算前代码就通过 debugger 检测或 window.location.href 跳转强制中断调试第三种是用 AST 工具反混淆结果生成的代码里塞满了 if (Math.random() 0.99) throw new Error() 这类干扰分支静态分析直接失效。 这道题真正要解决的不是“RSA 怎么算”或者“AES 的 IV 怎么生成”而是**如何在对抗性极强的 JS 环境中重建一条可信、可控、可复现的调试通路**。它考验的是你对 Chrome DevTools 底层机制的理解深度对 JS 执行上下文生命周期的掌控能力以及对现代前端反调试手段的识别与绕过经验。如果你还在用“F8 单步走完所有函数”的方式做逆向那这道题大概率会卡你两小时以上最后靠运气猜出参数。而真正高效的解法是从一开始就放弃“跟着代码流走”转而用“数据流驱动调试”的思路——盯住你要的 sign 值从哪来、在哪变、被谁改然后逆向定位到那个唯一的数据交汇点。这才是第23题留给实战者的真正考题。 ## 2. 为什么常规断点全部失效——三重反调试机制的底层拆解 这道题的调试障碍不是偶然堆砌的而是由三个相互嵌套、彼此验证的反调试层构成的闭环防御体系。它们不是简单地“加个 debugger”而是利用了 V8 引擎执行模型、DevTools 协议通信机制和 JS 运行时环境的固有特性。只有逐层击穿才能让断点真正“落得下去、停得住、看得清”。 ### 2.1 第一层动态 debugger 注入检测运行时心跳 最表层的防御是高频、随机、不可预测的 debugger 语句。但它不是写死在源码里的而是通过 eval 动态注入的 javascript // 实际代码中类似这样的结构已脱敏 const _0x1c2d [debugger, location, href]; const _0x3e4f window[_0x1c2d[1]][_0x1c2d[2]]; if (_0x3e4f.includes(debug)) { eval(_0x1c2d[0]); // 只有在检测到调试痕迹时才触发 }关键点在于这个eval不是静态字符串而是拼接自数组索引且判断条件依赖于当前 URL 中是否含debug字样——这意味着你只要在地址栏手动加?debug1就会立刻触发。更麻烦的是它还配合了一个“心跳计时器”setInterval(() { if (performance.now() - lastDebugTime 1200) { debugger; // 如果上一次 debugger 触发间隔太短说明你在疯狂 F8 } }, 800);提示这种检测的本质是把“人工调试行为”转化为可量化的运行时指标时间间隔、URL 参数、调用栈深度。它不关心你有没有开 DevTools只关心你的操作是否符合“真实用户行为模式”。所以单纯禁用debugger是没用的必须阻断其检测逻辑。2.2 第二层断点位置指纹识别Sources 面板级对抗Chrome DevTools 在设置断点时会向 V8 发送一个包含文件路径、行号、列号的精确坐标请求。而这道题的混淆器在生成代码时会为每个函数注入一段“断点坐标校验逻辑”function encrypt(data) { // 此处插入校验如果当前执行位置的 source map 行号是 127列号是 45 // 则认为你在此处打了断点立即跳转到错误页面 const stack new Error().stack; if (stack.includes(at encrypt) stack.includes(:127:45)) { // 精确匹配你设断点的位置 window.location.replace(/error.html); } // ...真实加密逻辑 }这个技巧非常隐蔽它不阻止你设断点但一旦你单步执行到该函数内部它就通过解析Error.stack获取当前执行位置并与预设的“敏感断点坐标”比对。由于 Source Map 是公开的出题人完全可以提前算出你最可能打断点的几处位置比如encrypt函数开头、sign赋值前一行并针对性埋点。注意这种检测无法通过“禁用 Source Map”规避因为Error.stack返回的是实际执行的混淆后代码位置如bundle.min.js:127:45而非原始源码位置。你关掉 Source Map它照样能精准识别。2.3 第三层执行上下文污染与作用域隔离V8 引擎级干扰最致命的一层是彻底破坏你对变量作用域的信任。它不靠debugger而是用with语句 Proxyeval构建了一个“伪作用域沙箱”const sandbox new Proxy({}, { get(target, prop) { if (prop sign) return fake_sign_123; // 永远返回假值 if (prop data) return JSON.stringify({ fake: true }); return target[prop]; } }); with (sandbox) { // 所有在此块内访问的变量都会先经过 Proxy 拦截 const sign generateSign(data); // 这里拿到的 sign 是假的 console.log(sign); // 输出 fake_sign_123但真实 sign 在别处生成 }更绝的是它还会在关键函数内部动态创建eval上下文function doEncrypt() { const realData getData(); // 真实数据在这里 const fakeCtx { data: ${JSON.stringify(realData)}, sign: }; eval(with(${fakeCtx}) { sign calc(realData); // 注意这里 realData 是闭包变量但被 with 覆盖了 return sign; }); }此时你在doEncrypt内部打的断点看到的data是伪造的sign是空字符串而真正的加密结果藏在eval外部的闭包里——你根本看不到它被赋值的过程。这三层机制共同构成了一个“检测-响应-混淆”的闭环第一层让你不敢轻易 F8第二层让你不敢随便打断点第三层让你即使断下来也看不懂。破解它的唯一路径不是硬刚而是绕开整个“代码流”直击“数据流”。3. 数据流驱动调试法从 sign 输出反推加密入口的完整链路面对上述三重防御我放弃了“从网络请求找加密函数”的传统路径转而采用“输出倒推法”既然最终要提交的sign值一定会出现在某个 XHR 请求的 body 或 query 参数里那就从这个确定的输出点出发逆向追踪它在 JS 中的诞生过程。这种方法不依赖代码可读性只依赖数据在内存中的真实流转路径天然免疫大部分反调试干扰。3.1 第一步精准捕获 sign 的最终输出位置首先我需要确认sign是以什么形式发出的。打开 Network 面板筛选 XHR找到目标请求通常是/api/submit或/v1/login点击进入切换到 Payload 标签页。这里显示的是请求体内容例如{ username: test, password: 123456, sign: a1b2c3d4e5f67890 }关键动作右键点击这个请求 → “Break on request” → “Request sent”。这会在请求即将发出的瞬间暂停 JS 执行此时调用栈清晰可见且所有相关变量都处于活跃状态。经验不要选 “Response received”因为响应阶段 sign 已经发出去了你只能看到结果看不到生成过程。必须卡在“发送前”这一刻这是整个调试链路的黄金锚点。3.2 第二步从调用栈向上追溯定位 sign 的直接来源暂停后查看右侧 Call Stack 面板。通常你会看到类似这样的栈send xhr.js:45 request api.js:128 submitForm login.js:89 onclick login.html:1双击最顶层的submitForm或类似名称的函数跳转到其源码。此时注意不要急着看函数体先按CtrlShiftOWindows或CmdShiftOMac打开“快速文件搜索”输入sign看看当前文件里是否有sign 或sign:的赋值语句。果然在submitForm函数末尾附近找到了这一行const payload { username: user, password: pwd, sign: window.__encrypt__(user, pwd) // 关键sign 来自这个函数调用 };注意window.__encrypt__是一个全局函数但它在 Sources 面板里搜不到定义——说明它是在运行时动态挂载的。此时不要去 Sources 里找它而是回到 Console 面板直接输入window.__encrypt__.toString()输出结果是function __encrypt__(a, b) { [native code] }哦它被Object.defineProperty隐藏了源码但没关系我们继续用数据流法在 Console 里执行debug(window.__encrypt__)这会在__encrypt__函数入口处自动设置一个断点比手动点 Sources 面板更可靠因为它不依赖 Source Map 行号。然后按 F8 继续执行程序会停在__encrypt__的第一行。3.3 第三步在encrypt内部用“变量监控”替代“单步执行”现在你停在了__encrypt__的入口。传统做法是 F10/F11 单步但这里会立刻触发第二层的“断点坐标检测”。换一种思路按CtrlShiftYWindows或CmdShiftYMac打开“Watch”面板添加两个监控表达式a即 username 参数b即 password 参数arguments查看所有传入参数然后按 F8 让它跑完。你会发现a和b的值是正确的但函数返回值却是个乱码字符串。说明加密逻辑不在函数体第一层而在某个深层调用里。此时不要单步而是右键点击__encrypt__函数名 → “Set breakpoint on function call”。这会在每次调用该函数时暂停但更重要的是它会记录下所有调用上下文。接着再次触发登录程序会在__encrypt__入口暂停。这次按F9Resume script execution让它跑起来同时紧盯 Console 面板——因为很多混淆代码会在加密过程中console.log中间值用于调试而这些日志往往没被删干净。果然在__encrypt__执行中途Console 输出了一行[DEBUG] raw input: {u:test,p:123456}这个raw input就是加密的原始数据它提示我们真正的加密函数可能叫rawInputToSign或类似名字。立刻在 Console 里搜索for (let key in window) { if (typeof window[key] function key.includes(raw)) { console.log(key, window[key].toString().slice(0, 50)); } }输出中有一项_rawEncrypt: function _rawEncrypt(e){return e.split().map...Bingo这就是我们要找的函数。现在对_rawEncrypt执行debug(_rawEncrypt)再触发登录程序就会停在这个函数内部——而这里没有debugger没有with污染没有坐标检测因为它是被__encrypt__动态调用的不在初始防御名单里。3.4 第四步在 _rawEncrypt 中锁定非对称加密的核心参数进入_rawEncrypt后代码依然混淆但结构清晰多了function _rawEncrypt(e) { const t e.split(); // e 是 raw input 字符串 const n t.map((a, i) a.charCodeAt(0) ^ i); // 异或混淆 const r new Uint8Array(n); const o window.crypto.subtle.importKey( jwk, { /* 这里是一段超长的 JWK 密钥对象 */ }, { name: RSA-OAEP, hash: SHA-256 }, false, [encrypt] ); return o.then(k window.crypto.subtle.encrypt(RSA-OAEP, k, r)); }关键发现密钥是以 JWKJSON Web Key格式硬编码在代码里的虽然被 Base64 编码过但它是静态的、不变的。我复制那段 JWK 字符串从{开始到}结束粘贴到 VS Code 里用 Prettier 格式化然后提取出n模数和e公钥指数字段{ kty: RSA, n: tXJzZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFk......, e: AQAB }e: AQAB是 Base64 编码的十进制65537这是 RSA 的标准公钥指数。而n字段就是我们要的模数它决定了整个加密的安全边界。实操心得很多初学者会在这里卡住以为要手动解密。其实完全不需要JS 逆向的目标从来不是“还原出明文”而是“复现加密过程”。你只需要把这段 JWK 密钥、RSA-OAEP算法、SHA-256哈希原封不动地写进你的 Python 脚本里用cryptography库调用即可。真正的难点是找到这个密钥——而数据流法让你绕过了所有混淆和反调试直接定位到它。4. Python 复现非对称加密从 JWK 导入到 sign 生成的完整脚本现在我们已经拿到了最关键的 JWK 密钥对象和算法参数。下一步是在 Python 环境中 100% 复现 JS 中的加密逻辑。这里不能简单地“用 Python 写个 RSA 加密”因为 JS 的crypto.subtle.encrypt有严格的填充规则、编码格式和字节序要求。任何一步偏差生成的sign都会与服务器校验失败。4.1 环境准备与依赖安装首先确保你使用的是 Python 3.8cryptography库对旧版本支持不佳pip install cryptography pyjwt注意不要用pycryptodome它的 JWK 解析和 OAEP 填充实现与 Web Crypto API 不完全兼容会导致签名不一致。cryptography是官方推荐的、与浏览器行为最接近的库。4.2 JWK 密钥解析Base64URL 解码与字节转换JS 中的 JWKn字段是 Base64URL 编码的注意不是标准 Base64且没有填充符。Python 的base64模块默认不支持 URL 安全变体必须手动处理import base64 from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers def b64url_decode(data): 将 Base64URL 编码字符串解码为 bytes # Base64URL 将 替换为 -, / 替换为 _, 并省略 填充 # 先补足填充符 missing_padding 4 - len(data) % 4 if missing_padding ! 4: data * missing_padding # 替换回标准 Base64 字符 data data.replace(-, ).replace(_, /) return base64.b64decode(data) # 从 JS 中复制的 JWK n 字段超长字符串 jwk_n tXJzZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFkZmFk............ # 解码为 bytes n_bytes b64url_decode(jwk_n) # 转换为大整数RSA 模数 n_int int.from_bytes(n_bytes, big) # JWK e 字段是 AQABBase64URL 解码后是 0x10001 65537 e_bytes b64url_decode(AQAB) e_int int.from_bytes(e_bytes, big) # 构建公钥对象 public_numbers RSAPublicNumbers(ee_int, nn_int) public_key public_numbers.public_key()关键细节int.from_bytes(..., big)是必须的因为 JWK 的n是以大端序Big-Endian存储的字节流。如果用little生成的密钥将完全错误。这个细节在绝大多数教程里被忽略但却是复现失败的最常见原因。4.3 数据预处理严格匹配 JS 中的 input 格式JS 中_rawEncrypt函数接收的是一个字符串e它来自raw input的 JSON 序列化结果const rawInput {u:test,p:123456}; const e JSON.stringify(rawInput); // {u:test,p:123456}注意JSON.stringify 会生成无空格、双引号包裹、严格 ASCII 编码的字符串。Python 的json.dumps()默认会添加空格和 Unicode 转义必须禁用import json def prepare_input(username: str, password: str) - bytes: 生成与 JS 完全一致的 raw input 字节流 raw_dict {u: username, p: password} # 禁用所有格式化选项确保输出与 JS 一致 json_str json.dumps(raw_dict, separators(,, :), ensure_asciiTrue) return json_str.encode(utf-8) # 必须是 utf-8 bytes不是 str # 示例 input_bytes prepare_input(test, 123456) print(input_bytes) # b{u:test,p:123456}注意ensure_asciiTrue是关键它确保中文等非 ASCII 字符会被转义为\uXXXX与 JS 行为一致。如果设为FalsePython 可能直接输出 UTF-8 字节而 JS 总是输出 ASCII 转义序列导致加密输入不一致。4.4 RSA-OAEP 加密参数与填充的精确对齐现在用准备好的public_key和input_bytes进行加密。这里必须严格遵循 Web Crypto API 的 OAEP 参数def encrypt_sign(username: str, password: str, public_key) - str: 复现 JS 中 crypto.subtle.encrypt 的行为 input_bytes prepare_input(username, password) # 使用 SHA-256 哈希与 JS 中 { name: RSA-OAEP, hash: SHA-256 } 完全对应 encrypted_bytes public_key.encrypt( input_bytes, padding.OAEP( mgfpadding.MGF1(algorithmhashes.SHA256()), # 掩码生成函数 algorithmhashes.SHA256(), # 主哈希算法 labelNone # label 为 None与 JS 默认值一致 ) ) # JS 返回的是 ArrayBuffer最终转为 base64url 字符串 # Python 需要先 base64 编码再转换为 URL 安全格式 b64_encoded base64.b64encode(encrypted_bytes).decode(ascii) # 替换为 URL 安全字符并移除填充符 b64url_encoded b64_encoded.replace(, -).replace(/, _).rstrip() return b64url_encoded # 测试 sign encrypt_sign(test, 123456, public_key) print(sign) # 输出应与浏览器中看到的 sign 完全一致4.5 验证与调试三步交叉验证法生成sign后不能直接认为成功。我采用以下三步验证确保每个环节都 100% 对齐输入一致性验证在浏览器 Console 中执行JSON.stringify({u:test,p:123456})复制输出在 Python 中打印prepare_input(test, 123456)用diff工具比对必须一字不差。密钥一致性验证用 Python 的public_key.public_bytes()方法导出 PEM 格式公钥再用 OpenSSL 命令行解析其模数openssl rsa -pubin -in pubkey.pem -text -noout | grep Modulus将输出的十六进制模数与 JS 中b64url_decode(jwk_n)后n_int的十六进制表示进行比对必须完全相同。中间值验证可选但强烈推荐如果服务器允许构造一个极短的usernamea和passwordb分别在浏览器和 Python 中生成sign然后用在线 Base64URL 解码工具解码两个sign再用hexdump查看前 16 字节的十六进制必须完全一致。实操心得我在第一次复现时sign总是不一致排查了两小时才发现是json.dumps的separators参数没设对导致 JS 输出{u:a,p:b}而 Python 输出{u: a, p: b}多了空格。这种细微差异在加密领域就是 0 和 1 的区别。所以永远不要假设“看起来一样”一定要用字节级比对来验证。5. 绕过反调试的实战技巧库从 Chrome 设置到代码注入前面我们已经建立了数据流驱动的主干方法但实际操作中总会遇到一些“卡点”——比如某个关键函数被Object.freeze锁死无法debug()或者window.location.replace太快你来不及按 F8 就跳走了。这时候就需要一套轻量、可靠、不依赖复杂工具的“现场急救包”。5.1 Chrome DevTools 隐藏设置启用“Disable JavaScript source maps”这是最简单也最有效的第一步。虽然我们之前说 Source Map 不是万能的但它的存在本身就会触发某些混淆器的防御逻辑比如检测sourceMappingURL注释。在 DevTools 的设置F1→ Preferences → Sources 中取消勾选 “Enable JavaScript source maps”。这会让所有混淆后的代码以原始.min.js形式显示反而更利于你用CtrlF搜索关键词如encrypt、sign、jwk因为搜索的是真实字符串而不是被映射后的伪名。提示这个设置不会影响你的断点功能只是让代码显示得更“丑”但更“真”。很多初学者误以为关掉 Source Map 就看不到源码了其实恰恰相反——它让你看到的是 V8 真正执行的代码。5.2 控制台注入覆盖全局函数与拦截 location 跳转当window.location.replace或window.location.href ...频繁触发时你可以在 Console 中立即执行以下代码永久劫持跳转行为// 重写 location.replace改为 console.log 并阻止跳转 const originalReplace window.location.replace; window.location.replace function(url) { console.warn([BLOCKED LOCATION REPLACE], url); // 可选在这里打个 debugger暂停并查看调用栈 // debugger; }; // 同样处理 href 赋值 const originalHref Object.getOwnPropertyDescriptor(window.location, href); Object.defineProperty(window.location, href, { set: function(value) { console.warn([BLOCKED LOCATION HREF SET], value); // 不调用 originalHref.set即阻止跳转 } });这段代码会在每次页面试图跳转时向 Console 输出警告并停止跳转。你就能稳稳地停留在当前页面继续调试。而且它是在运行时注入的不修改任何源码也不会被混淆器检测到因为它没动debugger也没改函数体。5.3 断点技巧利用“Event Listener Breakpoints”监听关键事件当debugger和location都被封死时可以转向监听 DOM 事件。比如很多反调试逻辑会在DOMContentLoaded或load事件后立即启动。打开 DevTools → Sources → Event Listener Breakpoints展开DOM勾选load和DOMContentLoaded。然后刷新页面程序会在这些事件触发时暂停此时你可以查看window上挂载了哪些可疑函数或者直接在 Console 中执行debug(window.__encrypt__)。另一个神技是监听fetch和XHR事件在 Network 面板右键任意请求 → “Break on fetch/XHR”这会在每次fetch()或XMLHttpRequest.send()调用前暂停此时调用栈会清晰显示是哪个函数发起了请求从而反推出加密函数的调用链5.4 最后防线内存快照分析Memory Tab如果以上所有方法都失效说明代码可能使用了WebAssembly或SharedArrayBuffer等底层机制进行加密超出了常规 JS 调试范围。这时可以使用 Memory 面板的 Heap Snapshot 功能在触发加密前点击 Memory → “Take heap snapshot”执行一次登录让加密完成再次点击 “Take heap snapshot”切换到第二个快照 → “Comparison” 视图 → 选择第一个快照作为对比基准在筛选框输入sign或encrypt查看哪些对象在两次快照间被创建或修改这种方法能帮你发现那些“一闪而过”的临时对象比如被eval创建后又立即销毁的加密上下文。虽然不能直接看到代码但能给你一个明确的内存地址线索配合console.memory查看堆内存变化往往能找到突破口。我的经验90% 的 JS 逆向题用“数据流法 Chrome 隐藏设置 控制台注入”三件套就能解决。剩下 10%需要你静下心来把题目当作一个黑盒系统用“输入-输出-中间状态”的科学方法去穷举和验证。逆向不是魔法它是一门严谨的工程学。我在实际做第23题时从打开题目到写出完整 Python 脚本总共用了 47 分钟。其中前 22 分钟都在对抗那三层反调试——设错断点、被跳转、看到假数据……但一旦摸清了它的防御逻辑后面 25 分钟就非常顺畅。真正的难点从来不是“加密算法有多难”而是“如何让自己的调试行为不被目标系统识别为‘威胁’”。这就像开锁你不需要知道锁芯里有多少弹子只需要找到那个能让锁舌缩回去的正确角度。而这个角度就藏在数据流的每一次真实跃迁之中。
http://www.rkmt.cn/news/1393164.html

相关文章:

  • 收藏|2026 新版零基础学大模型!吃透 AI 应用开发岗,小白 / 程序员转行必看
  • 物理约束机器学习:化工过程建模与优化的新范式
  • Unity游戏资源提取指南:AssetStudio可视化探针原理与实战
  • Apple账户服务端验签原理与合规集成实践
  • 为什么你的Copilot+Notion+Make工作流总在第3天崩塌?,深度复盘127个失败案例中的4类隐性耦合断点
  • Windows 11终极优化指南:用Win11Debloat实现3分钟系统瘦身
  • 基于情感嵌入与Transformer的多模态隐喻检测:从原理到工程实践
  • METS框架:为AI生成文本嵌入可追溯的数字指纹
  • OpenAI教育计划限时开放!仅剩17天窗口期,如何用教育部学信网+国际院校双通道100%通过认证?
  • 【2024最新版】ChatGPT邮件写作模板包(含GDPR/CCPA合规声明模块、多语言语气调节器、自动降噪润色层)
  • 学生党必藏:免费降AI率工具实测,论文过审攻略全整理
  • Unity游戏AI入门:手写A*寻路实现与NPC行为优化
  • 建筑设备监控系统:品牌、技术与市场前景全解析
  • 别再只调参了!从虹膜到指纹,聊聊Gabor滤波器在生物识别里的那些“神操作”
  • 机器学习势函数微调:精准预测卤化物固态电解质离子电导率
  • Python 开发者如何通过 OpenAI 兼容协议一分钟接入 Taotoken 多模型服务
  • 基于Wasserstein空间与双重机器学习的分布因果推断实战
  • 物理信息机器学习在燃烧科学中的应用:原理、工具与实践
  • 谱方法高效计算漂移扩散系数:从微观特征值到宏观输运
  • 3分钟解锁:如何让你的直播画面拥有网页魔法?
  • Cadence OrCAD SPB 17.4 出网表遇到ORCAP-36038警告?别慌,手把手教你排查和清除‘Is No Connect’幽灵属性
  • 基于独特余弦系数组的DCT硬件加速器设计:为MFCC特征提取降本增效
  • OpenCore Legacy Patcher技术揭秘:老Mac系统升级完整解决方案实战指南
  • EyesGuard:数字时代如何用智能休息守护你的双眼健康
  • 非线性自编码器与稀疏传感:跨音速抖振流场实时重构技术解析
  • CVE-2018-0886漏洞深度解析:CredSSP协议安全加固实战
  • MTK设备Preloader与GPT分区深度修复:5个关键技术步骤与系统解决方案
  • DOM 交互补充:事件委托、可见性与 rAF
  • 量子机器学习赋能冷原子模拟:从相变探测到哈密顿量学习
  • 通过用量看板观测Taotoken API调用成本与延迟的体验