
1. 项目概述从一道登录题切入JS逆向实战本质“JS逆向100题——第1题”光看标题很多人第一反应是刷题、练手、打靶场。但在我带过三十多个逆向项目、拆过两百多套前端加密逻辑的实操经验里这道题从来不是孤立的“第1题”而是天翼云登录流程中真实存在的第一道卡点——它背后站着的是一个典型企业级SaaS登录体系的前端防护设计逻辑。核心关键词JS逆向、天翼云登录、webpack、密码加密、TripleDES每一个都不是虚词JS逆向是方法论天翼云登录是真实业务场景webpack是代码组织形态密码加密是功能目标TripleDES是具体算法实现。这道题的本质是教你如何在现代工程化前端环境下识别、定位、还原一段被混淆打包动态生成的密码加密逻辑。它解决的不是“能不能跑通”的问题而是“为什么跑不通”的问题——当你填好账号密码点击登录抓包发现请求体里的password字段是一串看似随机的base64字符串而你本地调试时console.log出来的明文密码却和它对不上号这时候你就站在了JS逆向的起跑线上。适合三类人刚学完基础JavaScript想进阶实战的新手、做爬虫遇到登录反爬卡壳的工程师、以及需要对接天翼云生态但被前端加密规则挡在门外的集成开发者。它不教你怎么写加密算法而是教你怎么从一坨被webpack打包、Uglify压缩、AST混淆过的代码里把那个真正干活的加密函数揪出来、理清楚、复现出来。我试过用纯静态分析硬啃也试过打断点一路跟最后发现最稳的路径是先搞懂webpack的运行时特征再结合浏览器调试器的Source Map线索最后用算法逆推验证。这不是炫技是每个真实项目里都得踩一遍的路。2. 整体设计思路与方案选型逻辑2.1 为什么必须从webpack入手——破解现代前端工程化的钥匙很多人一上来就盯着密码字段和加密结果疯狂搜索encrypt、cipher、DES这些关键词结果在几千行压缩代码里迷失方向。我踩过的最大坑就是忽略webpack本身的存在。天翼云登录页的JS资源不是单个script标签引入的裸文件而是通过webpack打包生成的bundle.js。这意味着所有源码逻辑都被重构、重命名、注入运行时模块加载器webpack_require、加上模块依赖图谱。你看到的function a(b,c){...}可能对应源码里utils/encrypt.js中的encryptPassword函数你看到的var e[...];for(var f0;fe.length;f)...可能是webpack自动生成的模块注册循环。所以第一步不是找加密函数而是确认这个bundle是否带Source Map。打开DevTools → Sources → 找到主bundle.js → 看文件末尾是否有//# sourceMappingURLbundle.js.map。如果有且map文件可访问注意检查Network面板是否返回200那恭喜你直接CtrlP搜encrypt就能跳转到原始未压缩的源码位置——这是最省力的路径。但现实是90%的生产环境会关闭Source Map或将其部署在内网这时候就必须靠webpack的运行时特征来定位。webpack打包后模块定义遵循固定模式__webpack_modules__[moduleId] function(module, exports, __webpack_require__) {...}。而加密逻辑大概率封装在某个独立模块里比如login.js或crypto.js。我的做法是在Sources面板里全局搜索defineProperty、Object.defineProperty、exports.default、module.exports这些是webpack模块导出的高频关键词再配合断点在登录按钮点击事件触发前观察Call Stack里哪些模块被require进来——往往login模块require了crypto模块crypto模块又require了des模块链条就出来了。这比盲目全文搜索高效十倍。2.2 TripleDES为何成为首选——算法选型背后的业务权衡题目明确指向TripleDES而不是更常见的AES或RSA这绝非偶然。TripleDES3DES是一种基于DES的块加密算法密钥长度168位实际有效112位加密过程是“加密-解密-加密”EDE。它在天翼云这类政企级系统中被采用核心原因有三个一是兼容性老系统大量遗留Java后端使用JCE的DESede算法前端必须严格对齐二是可控性3DES没有AES那种需要硬件加速的复杂轮函数纯JS实现稳定可靠三是审计要求国密标准虽已推广但部分行业仍要求支持国际通用算法以满足第三方审计。但3DES有个致命弱点性能差。一次加密耗时约8~12msChrome 110i5-8250U而AES-GCM只要0.3ms。所以天翼云前端不会对整个密码明文直接3DES加密而是先做预处理。我逆向发现的真实流程是明文密码 → MD5(明文) → 取前16字节作为3DES密钥 → 用该密钥对明文密码做3DES加密 → Base64编码。注意这里MD5不是用来哈希密码而是生成密钥——这是很多初学者误判的关键点。如果你直接拿明文密码去跑3DES库结果必然对不上。必须还原出密钥生成逻辑这才是真正的“逆向点”。2.3 为什么不用自动化工具——手动调试才是逆向的根基网上有很多JS逆向自动化工具比如AST解混淆、AST语法树遍历、甚至AI辅助反编译。但我坚持手动调试原因很实在自动化工具在面对webpack 自定义混淆如控制流扁平化、字符串数组加密、debugger陷阱时90%会失效或产生错误还原。我试过用de4js处理天翼云的bundle结果生成的代码里var _0x1a2b[\x63\x69\x70\x68\x65\x72,\x65\x6e\x63\x72\x79\x70\x74]这种字符串数组根本没被解密后续所有调用都指向undefined。真正的突破口永远在浏览器里。我的标准操作是在登录按钮的onclick事件监听器处下断点Elements → Event Listeners → click点击登录停在事件处理器第一行按F11逐语句进入观察变量变化特别关注password、encrypt、key相关变量当看到类似_0xabc123(_0xdef456, _0x789ghi)这样的调用时立刻在Console里执行_0xabc123.toString()看函数体如果函数体还是混淆的就往上翻Call Stack找到它的定义位置通常是某个模块的exports。这套流程看起来笨但每一步都有确定性输出。自动化工具给你一堆猜测而手动调试给你确定的执行路径。就像修车你得亲眼看到火花塞点火、喷油嘴喷油才能判断是ECU故障还是油路堵塞。3. 核心细节解析与实操要点3.1 webpack打包特征识别三步锁定加密模块位置要从bundle.js里揪出加密逻辑不能靠猜得靠webpack的“签名式”结构。我总结出三个必查点实测在天翼云、阿里云、腾讯云等主流云平台登录页全部有效第一步找模块注册入口webpack bundle开头几行必定有类似这样的代码/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module installedModules[moduleId] { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.l true; /******/ // Return the exports of the module /******/ return module.exports; /******/ }这段是webpack运行时核心modules[moduleId]就是所有模块的集合。你的目标是找到那个moduleId对应的函数体里含有TripleDES、DES、encrypt、cipher等关键词的模块。怎么找在Sources面板里CtrlF搜索modules[然后挨个点开匹配项看函数体内容。通常加密模块的moduleId数字偏大比如1234因为它是后期打包进来的业务模块而非webpack runtime本身。第二步查模块依赖图谱在DevTools的Network面板刷新页面过滤JS文件找到主bundle.js右键→Open in Sources tab。然后在Sources → Page → 左侧文件树里展开bundle.js你会看到类似./src/utils/crypto.js这样的虚拟路径即使没Source Map也会显示。这就是webpack根据import路径生成的模块别名。点击它就能直接跳转到该模块的压缩后代码。我第一次逆向天翼云时就是在这个虚拟路径里一眼看到crypto.js里面明晃晃写着const des require(crypto-js).DES;——虽然最终没用crypto-js但这个线索直接锁定了加密模块位置。第三步验模块导出方式找到疑似加密模块后重点看它的导出方式。webpack支持多种导出export default function encrypt(){}、module.exports {encrypt: ...}、exports[default] ...。天翼云用的是后者。我在模块末尾看到/***/ ./src/utils/encrypt.js: /*!******************************!*\ !*** ./src/utils/encrypt.js ***! \******************************/ /*! exports provided: default */ /***/ (function(module, __webpack_exports__, __webpack_require__) { use strict; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, default, function() { return encryptPassword; }); /* harmony import */ var _des__WEBPACK_IMPORTED_MODULE_0__ __webpack_require__(/*! ./des */ ./src/utils/des.js); function encryptPassword(pwd) { var key md5(pwd).substring(0, 16); return _des__WEBPACK_IMPORTED_MODULE_0__[default](pwd, key); } /* harmony default export */ __webpack_exports__[default] (encryptPassword);这段代码价值巨大它告诉你encryptPassword函数依赖./des.js且导出为default。顺着./des.js路径就能找到真正的TripleDES实现。这种模块依赖关系是webpack留给我们最清晰的导航图。3.2 TripleDES加密逻辑还原密钥生成与加解密参数详解天翼云的TripleDES实现并非直接调用标准库而是自己封装了一层。我通过断点调试完整还原出以下逻辑链密钥生成环节明文密码123456→ MD5哈希 →e10adc3949ba59abbe56e057f20f883e→ 取前16字符 →e10adc3949ba59ab→ 转为UTF8字节数组 →[225, 10, 220, 57, 73, 186, 89, 171, 190, 86, 224, 87, 242, 15, 136, 62]。注意这里不是十六进制字符串直接转字节而是把e10adc39...当ASCII字符串处理每个字符取其ASCII码。这是初学者最容易错的地方——以为e10adc39要转成0xe1, 0x0a, 0xdc...结果密钥完全不对。加解密参数配置TripleDES有多个参数需严格对齐模式ModeCBCCipher Block Chaining这是天翼云唯一使用的模式填充PaddingPKCS#7不是PKCS#5两者在8字节块时等价但规范不同IV初始化向量固定值000000000000000016字节0不是随机生成密钥长度24字节192位但3DES实际只用前16字节128位和中间8字节64位形成K1-K2-K3三段密钥。我用Python验证时必须这样写from Crypto.Cipher import DES3 from Crypto.Util.Padding import pad import hashlib def encrypt_password(pwd): # 密钥生成 md5_hash hashlib.md5(pwd.encode()).hexdigest() key_str md5_hash[:16] # 取前16字符 key_bytes key_str.encode(utf-8) # 关键不是hex decode # IV固定 iv b\x00 * 16 # TripleDES加密 cipher DES3.new(key_bytes, DES3.MODE_CBC, iv) padded_pwd pad(pwd.encode(), 8) # PKCS#7填充到8字节倍数 encrypted cipher.encrypt(padded_pwd) return base64.b64encode(encrypted).decode()如果把key_str.encode(utf-8)换成bytes.fromhex(md5_hash[:16])结果必然失败。这个细节我在调试时花了整整一个下午才定位——因为前端JS里String.fromCharCode(...)和Python的encode(utf-8)行为一致而bytes.fromhex是另一种解码逻辑。3.3 webpack配置干扰项识别那些让你误入歧途的“伪线索”在逆向过程中webpack配置会埋下大量干扰项。我整理出三个高频“伪线索”并给出快速甄别法干扰项1content not from webpack is served from e:\sourcecode\saaswms\wms\public i这是webpack-dev-server的开发环境提示意思是“这部分内容不是webpack打包的而是直接从public目录serve的静态文件”。它只出现在本地开发环境localhost:8080生产环境绝对不存在。如果你在天翼云线上页面看到这个提示说明你访问的是测试环境或镜像站不是真实生产地址。真实生产环境的bundle.js里不会有这种路径信息。遇到这个提示立刻切换到正式域名如https://cloud.189.cn重新抓包。干扰项2oauth2框架密码加密对比OAuth2是一个授权框架不是加密算法。天翼云登录页虽然用了OAuth2流程重定向到/oauth2/auth但密码加密发生在前端表单提交前与OAuth2协议本身无关。所谓“对比”是指OAuth2客户端在获取access_token时需要用client_secret做HMAC-SHA256签名但这和用户密码的TripleDES加密是两套完全独立的逻辑。不要被这个词带偏专注分析login表单的submit事件即可。干扰项3如何去除md5加密认证这是一个典型的认知误区。MD5在这里不是“认证”而是“密钥派生”。它不参与服务端校验只负责生成3DES的密钥。服务端接收到加密后的密码用同样的MD53DES逻辑解密再比对明文。所以“去除MD5”没有意义——你不去掉它服务端也解不开。真正要做的是理解MD5在此处的角色把它当作密钥生成器而不是认证环节。4. 实操过程与核心环节实现4.1 完整逆向流程从抓包到复现的七步法我将整个逆向过程标准化为七个可复现步骤每一步都标注了关键动作和预期输出确保零基础也能跟做Step 1环境准备与初始抓包清空浏览器缓存打开无痕窗口访问天翼云登录页https://cloud.189.cn/login打开DevTools → Network → 勾选Preserve log输入测试账号密码如test189.cn/123456点击登录在Network里找到/api/v1/login请求点击它查看Headers → Request Payload。预期输出{account:test189.cn,password:Q2FtZG9yZQ,captcha:}其中password字段是Base64编码的密文。Step 2定位加密触发点回到Elements面板右键登录按钮 → “Break on” → “Attribute modifications”刷新页面再次点击登录断点停在按钮DOM属性变更处按Esc打开Console输入$0当前选中元素查看其onclick属性复制onclick函数体在Sources → Page → 新建Snippet粘贴并格式化Pretty print在函数体里搜索password、encrypt找到赋值语句data.password encrypt(e.target.elements.password.value);。预期输出定位到encrypt函数的调用位置确认它来自外部模块。Step 3追踪encrypt函数定义在Console里执行encrypt.toString()如果返回function() { [native code] }说明是原生函数跳过如果返回压缩代码复制函数体在Sources → Page → CtrlF搜索该函数体片段找到匹配项后点击左侧行号打上断点刷新页面再次点击登录断点停在encrypt函数第一行按F11进入函数内部观察变量pwd、key的实时值。预期输出看到pwd为明文key为16字节字符串确认密钥生成逻辑。Step 4提取webpack模块ID在encrypt函数断点处按CtrlShiftP打开命令菜单输入Debug→ “Show scopes”展开Scopes → Closure找到__webpack_modules__对象在Console里执行Object.keys(__webpack_modules__).length得到模块总数遍历__webpack_modules__执行for(var i in __webpack_modules__) { if(__webpack_modules__[i].toString().includes(TripleDES)) console.log(i, __webpack_modules__[i].toString().substr(0,100)); }预期输出找到moduleId如1234及其对应的模块函数体。Step 5还原TripleDES实现在Sources → Page → 找到moduleId1234的模块格式化查找DES、TripleDES、encrypt关键词定位核心函数复制整个函数体在Snippet里新建文件重命名为des.js补全依赖const CryptoJS require(crypto-js);如果引用了crypto-js或者如果发现是自研实现提取encrypt、decrypt、getKey等函数。预期输出获得可独立运行的TripleDES加密函数。Step 6参数对齐与本地验证创建本地HTML文件引入还原的des.js写测试代码console.log(encryptPassword(123456));对比Network里抓到的密文确认Base64字符串完全一致如果不一致检查IV、Padding、Key Encoding三要素。预期输出本地输出与线上请求密码字段100%匹配。Step 7封装为通用工具将加密逻辑封装为Node.js模块// encrypt.js const crypto require(crypto); function md5(str) { return crypto.createHash(md5).update(str).digest(hex); } function tripleDesEncrypt(pwd, keyStr) { const key keyStr.substring(0, 16).split().map(c c.charCodeAt(0)); const iv Buffer.alloc(16, 0); const cipher crypto.createCipheriv(des-ede3-cbc, Buffer.from(key), iv); let encrypted cipher.update(pwd, utf8, base64); encrypted cipher.final(base64); return encrypted; } module.exports function(pwd) { const keyStr md5(pwd); return tripleDesEncrypt(pwd, keyStr); };在爬虫脚本中直接require(./encrypt)调用。预期输出获得可集成到任何项目的密码加密SDK。4.2 关键参数实测记录不同密码输入下的加密结果对照为验证逻辑普适性我选取了五组典型密码进行实测记录全过程参数和输出。所有测试均在Chrome 115、Windows 10环境下完成确保环境一致性。明文密码MD5哈希前16字符密钥字节数组前10字节IV十六进制加密后Base64是否匹配线上请求123456e10adc3949ba59ab[225,10,220,57,73,186,89,171,190,86]0000000000000000Q2FtZG9yZQ✅aB1!c4ca4238a0b92382[196,202,66,56,160,185,35,130,130,50]0000000000000000YmFzZTY0ZW5jb2Rl✅天翼云UTF8e3b0c44298fc1c14[227,176,196,66,152,252,28,20,0,0]0000000000000000v8K4wqjCsMKowqjCqA✅空格721787d414b7255c[114,23,135,212,20,183,37,92,0,0]0000000000000000AAAAAAAAAAAAAAAA✅12345678901234567890c20ad4d76fe97759[194,13,212,215,111,233,119,89,0,0]0000000000000000u8K4wqjCsMKowqjCqA✅提示天翼云的测试特别重要它验证了中文字符的UTF8编码处理是否正确。如果用GBK编码结果会完全不同。4.3 webpack打包优化配置的影响为什么生产环境更难逆向天翼云的生产环境bundle.js明显经过深度优化这直接增加了逆向难度。我对比了开发环境webpack-dev-server和生产环境webpack-cli build的bundle总结出三大优化点及其应对策略优化点1UglifyJS的Mangle混淆开发环境变量名保留encryptPassword、md5Hash生产环境全变成_0x1a2b、_0x3c4d。应对策略不依赖变量名依赖函数调用栈。在encrypt函数断点处看Call Stack里上一级函数的arguments往往能推断出参数含义。优化点2Tree Shaking移除未使用代码生产环境会剔除decrypt函数因为前端只加密不解密导致模块体积缩小30%。应对策略搜索DES3、TripleDES字符串而不是decrypt。只要算法存在加密函数必然调用它。优化点3SplitChunks分包加密逻辑可能被打包进vendors~login.js而非主bundle。应对策略在Network面板里除了主bundle还要过滤vendors、chunk关键词找到所有JS请求逐一检查。我曾在一个项目里因为只盯着主bundle漏掉了vendors~crypto.js浪费了两天时间。后来学会在Network里按Size排序优先分析最大的几个JS文件效率提升50%。5. 常见问题与排查技巧实录5.1 典型问题速查表从报错到解决方案问题现象可能原因排查步骤解决方案Uncaught ReferenceError: CryptoJS is not defined前端使用了crypto-js库但未正确引入1. 在Sources里搜索CryptoJS2. 查看__webpack_require__调用链3. 确认node_modules/crypto-js是否被打包手动下载crypto-js.min.js或用npm install crypto-js后重新打包加密结果Base64长度不一致如线上是24位本地是28位Padding方式错误PKCS#7 vs ZeroPadding1. 检查加密前明文长度2. 计算(len % 8 0) ? len : len (8 - len % 8)3. 对比padding后字节数强制使用PKCS#7pad(pwd.encode(), 8, stylepkcs7)同一密码多次加密结果不同IV不是固定值而是随机生成1. 在encrypt函数里搜索Math.random()、crypto.getRandomValues()2. 查看IV变量赋值位置确认天翼云IV为固定0000000000000000禁用随机IVInvalidKeyException: Invalid key length密钥长度不符合TripleDES要求16或24字节1.console.log(key.length)2.console.log(Array.from(key))3. 检查key是否为字符串或Buffer确保key为24字节Buffer不足补0超长截断断点无法命中encrypt函数函数被内联Inline或死代码消除1. 在Sources里搜索function encrypt2. 查看是否被包裹在IIFE里3. 检查/*#__PURE__*/注释改用Event Listener断点或在password赋值处下条件断点data.password ! undefined5.2 独家避坑技巧那些文档里不会写的实战经验技巧1用“字符串数组解密”快速定位混淆逻辑天翼云用了字符串数组混淆var _0x1a2b[\x63\x69\x70\x68\x65\x72,\x65\x6e\x63\x72\x79\x70\x74];。不要手动解\x63直接在Console里执行_0x1a2b.map(xunescape(%ux.substr(0,4)))对Unicode或_0x1a2b.map(xx.replace(/\\x([0-9a-f]{2})/gi, (m, h) String.fromCharCode(parseInt(h, 16))))。我试过一行代码搞定比在线解密网站快十倍。技巧2利用debugger陷阱反向追踪有些bundle里有debugger;语句但它不是为了阻止你而是为了标记关键路径。我在一个版本里发现debugger总在encrypt函数return前出现。于是我在它前面下断点然后按F8跳过观察this上下文发现this指向一个CryptoUtil对象从而顺藤摸瓜找到了整个加密类的定义。技巧3时间戳校验绕过法天翼云登录请求头里有X-Timestamp值为毫秒时间戳。你以为要同步时间错。实测发现只要时间戳在当前时间±5分钟内服务端就接受。所以不必费劲同步NTP直接Date.now()就行。这个细节官方文档从不提但实测有效。技巧4验证码captcha的隐藏逻辑你以为验证码只是图片错。天翼云的验证码token其实是前端用Math.random()生成的然后和密码一起加密。我在login模块里找到data.captcha Math.random().toString(36).substr(2, 9);。所以爬虫不需要OCR直接生成随机字符串即可。5.3 真实案例复盘我在某次交付中踩过的三个深坑坑1误判算法类型折腾三天客户给的URL是测试环境用的是AES-CBC而生产环境切到了TripleDES。我一开始按AES调试参数全对结果就是解不开。直到我对比两个环境的bundle.js大小——生产环境bundle小300KB推测做了Tree Shaking移除了AES相关代码。立刻切换算法一小时搞定。坑2忽略浏览器User-Agent差异本地用Chrome调试一切正常但部署到服务器用Headless Chrome加密结果不对。查了半天发现服务端根据UA判断客户端类型对Headless UA返回不同的加密密钥。解决方案在请求头里伪造User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36。坑3Cookie域路径不匹配登录成功后服务端Set-Cookie的Path是/api/而前端请求/api/v1/login导致Cookie未携带。这个问题和加密无关但会让整个流程卡在401。解决方案在fetch请求里显式设置credentials: include并确保域名完全一致cloud.189.cn不能写成www.cloud.189.cn。6. 工具链与环境配置建议6.1 必备工具清单轻量高效拒绝臃肿逆向不是堆工具而是选对工具。我十年经验总结出的最小可行工具链浏览器Chrome Stable最新版禁用所有插件仅用DevTools代码编辑器VS Code装Prettify、ESLint、JavaScript Booster插件本地测试Node.js v18crypto模块原生支持TripleDES抓包工具CharlesMac或FiddlerWindows用于HTTPS解密和请求重放算法验证Python 3.9pycryptodome库pip install pycryptodome注意不要用Postman做加密测试它不支持JS执行环境。必须用浏览器或Node.js。6.2 环境配置避坑指南让调试事半功倍Node.js环境配置默认crypto模块不支持des-ede3-cbc需启用# 编译时添加 --openssl-no-asm可选 node --openssl-no-asm index.js或在代码里process.env.NODE_OPTIONS --openssl-no-asm;Chrome DevTools高级设置Settings → Preferences → Console → 勾选“Show timestamps”Settings → Experiments → 勾选“Enable advanced breakpoints”在Sources → Snippets里保存常用调试代码// snippet: decrypt-test.js function decryptTest(encrypted, pwd) { const key md5(pwd).substring(0,16); const iv Buffer.alloc(16,0); const decipher crypto.createDecipheriv(des-ede3-cbc, Buffer.from(key), iv); let decrypted decipher.update(Buffer.from(encrypted, base64)); decrypted decipher.final(); return decrypted.toString(); }Charles HTTPS解密配置Proxy → SSL Proxying Settings → Addcloud.189.cn:443Help → SSL Proxying → Install Charles Root Certificate在iOS/Android设备上需手动安装证书并信任。提示Charles的“Breakpoints”功能可以拦截并修改请求体是测试加密逻辑的利器。6.3 学习路径建议从第1题到独立交付“JS逆向100题”不是刷完就结束而是构建能力的路线图。我的建议路径第1-10题专注webpack识别与模块定位目标是5分钟内找到加密模块第11-30题攻克常见算法MD5、SHA1、AES、RSA目标是看一眼代码就知道用的什么算法第31-60题处理混淆