Web登录参数逆向分析:从加密定位到算法还原实战
1. 项目概述一次典型的Web登录参数逆向之旅最近在分析一个Web平台的登录流程时遇到了一个经典的挑战登录请求中的密码参数pwd并非明文传输而是经过前端加密处理的一串“乱码”。对于安全研究、自动化测试或是理解前端安全机制来说逆向分析这个加密过程是必经之路。这次我们就来完整复盘一次针对某平台已做脱敏处理登录请求中pwd参数的逆向分析过程。整个过程不涉及任何具体平台的敏感信息纯粹是技术思路和方法的探讨适合对Web逆向、JavaScript分析感兴趣的朋友参考。无论你是想学习如何定位前端加密逻辑还是想了解常见的JS混淆与对抗手段这篇文章都能提供一个清晰的实操路径。2. 逆向分析的核心思路与准备工作2.1 目标定义与分析边界我们的核心目标是找到网页在提交登录表单时是如何将用户输入的明文密码转换成为HTTP请求体中那个pwdxxxxxx参数的。这通常意味着我们需要找到负责这个转换的JavaScript函数理解其算法和可能的密钥。在开始之前必须明确几点合法性所有分析应基于自己拥有测试权限的环境或公开的、允许安全测试的靶场。未经授权的逆向分析可能违反法律法规和服务条款。工具化现代Web前端大量使用混淆、压缩和动态加载技术纯靠肉眼阅读代码效率极低必须借助工具。场景化加密逻辑往往与登录页面的具体版本、加载的资源文件强相关。分析时需要锁定一个具体的、稳定的页面版本。2.2 工具链准备工欲善其事必先利其器。以下是本次分析会用到的核心工具它们构成了Web逆向的基础设施浏览器开发者工具Chrome DevTools / Firefox Developer Tools这是我们的主战场。特别是Network网络面板和Sources源代码面板。代码美化与格式化工具浏览器Sources面板自带的“Pretty Print”功能那个{}图标是第一步。对于更复杂的混淆可能需要使用像js-beautify这样的库进行本地处理。断点调试能力熟练使用XHR/Fetch Breakpoints网络请求断点、Event Listener Breakpoints事件监听器断点和常规的JavaScript行断点。“油猴”脚本与浏览器扩展例如Tampermonkey可以用于注入我们自己的调试脚本比如重写关键函数、打印中间变量值等。Node.js环境用于将逆向出来的JavaScript加密函数剥离出来在本地进行验证和模拟确保其独立运行。3. 定位加密入口从网络请求到关键函数3.1 捕获登录请求首先我们打开目标登录页面。在提交登录表单之前先打开浏览器的开发者工具并切换到Network网络面板确保勾选了“Preserve log”保留日志以防止页面跳转后请求记录被清除。输入测试用的账号密码例如用户名test 密码123456点击登录按钮。此时Network面板中会立刻出现一个新的请求通常是POST类型请求URL类似于/login或/api/login。点击这个请求查看其Headers请求头和Payload请求负载。在Payload标签页下我们通常能看到Form Data或Request Payload。这里就是我们寻找的pwd参数。它很可能是一串长长的、看似随机的Base64字符串或十六进制字符串例如pwd4QrcOUm6WauVuBX8gIPg。记下这个值这是我们逆向的终点也是验证我们成果的基准。3.2 设置XHR断点拦截加密瞬间知道加密后的结果后我们需要找到生成它的代码位置。最有效的方法是设置XHR/Fetch Breakpoint。在开发者工具的Sources源代码面板中找到右侧的XHR/Fetch Breakpoints区域点击号添加一个新的断点。由于我们不知道具体的请求URL全路径但知道它包含login关键字我们可以输入*login*作为断点条件。这样任何URL中包含login的XHR或Fetch请求在发起前都会被浏览器暂停。再次点击登录按钮。此时浏览器执行会立刻中断并跳转到Sources面板暂停在发起这个网络请求的JavaScript代码行上。这里的调用栈Call Stack就是我们的“藏宝图”。3.3 回溯调用栈寻找加密函数在Sources面板的右侧查看Call Stack调用栈面板。调用栈展示了从底层网络API一路回溯到最顶层调用者的函数链。我们通常不需要关注最底层的浏览器内部代码如send、fetch而是从最接近我们业务逻辑的匿名函数或某个具体的函数名开始看起。我们需要在调用栈中自上而下地寻找看看哪个函数执行后pwd参数的值变成了我们之前捕获的那个加密字符串。一个非常实用的技巧是将鼠标悬停在调用栈中各个函数内的变量上或者使用控制台Console在当前断点上下文中打印变量的值。我们可能会看到formData、payload、data这样的对象检查它们里面pwd字段的值。如果某个函数执行前pwd是明文123456执行后变成了加密字符串那么这个函数就是我们要找的加密函数。注意现代前端框架如React, Vue和构建工具Webpack会产生大量匿名函数和模块化的代码调用栈可能看起来非常冗长和混乱。关键是要有耐心并关注那些与“提交”、“序列化”、“加密”相关的自定义函数名。4. 深入加密函数解析算法与对抗混淆4.1 分析函数逻辑一旦我们通过调用栈定位到了疑似加密函数假设它叫encryptPassword或是一个匿名函数接下来就是深入分析其内部逻辑。首先点击该函数所在的行号设置一个常规的行断点。然后取消XHR断点重新触发登录。代码会直接在我们关心的函数入口处暂停。现在我们可以一步步F10单步跳过F11单步进入执行观察输入参数函数的参数是什么通常就是明文密码字符串。内部变量关注所有新声明的变量它们可能是临时计算结果、密钥或初始化向量IV。关键函数调用函数内部是否调用了其他函数比如CryptoJS.AES.encrypt、btoa、md5、SHA256或者是一些自定义的_0xabc123这样的混淆函数。返回值最终返回的是什么是否经过了toString(base64)或toHex()之类的处理4.2 对抗JavaScript混淆我们很少能幸运地遇到清晰可读的代码。更常见的情况是代码被严重混淆变量名变成了_0x12ab3c字符串被拆散、编码逻辑被分割成无数个小函数。这时需要一些策略动态Hook与日志注入与其艰难地静态反混淆不如让代码自己“说出”秘密。我们可以在加密函数被调用时动态注入日志。在Sources面板找到函数体在开头部分直接编辑代码浏览器允许临时编辑添加如console.log([Encrypt] Input:, password);这样的语句。然后让代码继续执行在Console中查看输出。这种方法可以快速获得输入输出对对于黑盒分析非常有帮助。重写关键函数如果发现加密依赖于某个全局对象下的方法如window.crypto或一个名为enc的模块我们可以写一个Tampermonkey脚本在页面加载早期就重写这个函数。在新函数中我们记录参数和结果然后调用原始函数最后将原始结果返回。这能实现无侵入式的监控。关注核心算法标识即使变量名混淆一些核心算法的特征常量或操作是难以完全隐藏的。例如AES可能会看到mode: CBCpadding: Pkcs7以及一个固定的128/256位密钥。RSA可能会看到new JSEncrypt()setPublicKey和一个很长的Base64格式的公钥。哈希可能会看到update、digest(hex)的调用模式。Base64可能会看到btoa或atob函数或者一些自定义的字母表。使用AST还原工具进阶对于极其复杂的混淆可以尝试将整个JS文件下载下来使用像de4js或jsnice这样的在线工具或本地AST抽象语法树解析库进行一定程度的反混淆和格式化还原出可读性更好的代码结构。但这需要一定的JavaScript语法树知识。4.3 提取并验证加密函数当我们基本弄清了加密流程后例如密码先经过SHA256哈希再将结果用AES-CBC模式加密最后输出Base64下一步就是尝试将这个函数“剥离”出来在Node.js环境中独立运行验证。环境模拟检查加密函数是否依赖浏览器特有的对象如window、document、navigator。如果依赖需要在Node.js中模拟这些对象或者更简单地将相关逻辑替换成Node.js的等效实现如用crypto模块替代CryptoJS。补全依赖如果加密函数调用了其他自定义函数_0x1a2b3c我们需要将这些被调用的函数也一并提取出来。有时这些函数就定义在同一个文件或同一个闭包作用域内。密钥与参数这是最关键的一步。加密算法通常是公开的如AES但密钥Key和初始化向量IV是保密的。它们可能硬编码在JS文件的某个常量数组中。通过一个固定的算法从某个字符串如网站域名生成。由服务器在页面加载时动态下发例如隐藏在某个HTML标签的>function encryptPassword(plainPwd) { // 1. 对密码进行MD5哈希假设观察到调用了CryptoJS.MD5 const md5Hash CryptoJS.MD5(plainPwd).toString(); // 2. 将MD5结果与一个固定盐值拼接 const salted md5Hash aStaticSaltValue; // 3. 对拼接后的字符串再进行SHA256哈希 const sha256Hash CryptoJS.SHA256(salted).toString(CryptoJS.enc.Hex); // 4. 取前32位作为AES-256-CBC加密的密钥 const key CryptoJS.enc.Hex.parse(sha256Hash.substr(0, 64)); // 64 hex chars 32 bytes // 假设IV是硬编码的 const iv CryptoJS.enc.Hex.parse(00000000000000000000000000000000); // 5. 使用密钥和IV对原始密码进行AES加密 const encrypted CryptoJS.AES.encrypt(plainPwd, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // 6. 输出Base64格式的密文 return encrypted.toString(); }我们的剥离与验证步骤识别依赖函数明显依赖CryptoJS库。Node.js实现在Node.js项目中安装crypto-js库 (npm install crypto-js)。编写等效代码const CryptoJS require(crypto-js); function encryptPassword(plainPwd) { const md5Hash CryptoJS.MD5(plainPwd).toString(); const salted md5Hash aStaticSaltValue; const sha256Hash CryptoJS.SHA256(salted).toString(CryptoJS.enc.Hex); const key CryptoJS.enc.Hex.parse(sha256Hash.substr(0, 64)); const iv CryptoJS.enc.Hex.parse(00000000000000000000000000000000); const encrypted CryptoJS.AES.encrypt(plainPwd, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); } // 测试 const testPwd 123456; const result encryptPassword(testPwd); console.log(加密结果:, result); // 将输出与抓包得到的 pwd 值对比运行与对比运行脚本查看输出是否与抓包数据一致。6. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到下面这些问题。这里记录了我的排查思路和解决方法。6.1 问题一设置XHR断点后代码没有在期望的位置暂停可能原因1登录请求不是通过标准的XMLHttpRequest或fetch发起的可能是通过navigator.sendBeacon、form submit或者一个WebSocket。对于表单提交可以尝试使用Event Listener Breakpoints中的submit事件断点。可能原因2请求URL不包含你设置的断点关键词。尝试使用更通用的关键词如*或者仔细查看Network中请求的完整URL使用其路径的一部分。排查技巧即使断点没生效也要在Network面板仔细查看那个登录请求。在请求的Initiator发起者列通常会有一个链接点击它可以跳转到发起该请求的JavaScript代码行。这是定位源码的另一个重要入口。6.2 问题二调用栈非常深且全是匿名函数无法定位可能原因代码经过Webpack等模块打包工具处理所有函数都被包裹在闭包中。排查技巧关注栈顶附近即使函数是匿名的其所在的文件如chunk-vendors.xxxx.js和行号是真实的。在栈顶附近寻找看起来像是业务代码的文件文件名可能包含login、user、auth等字样。使用“忽略列表”在开发者工具的设置中可以添加脚本忽略列表将node_modules、webpack等第三方库的源码过滤掉让调用栈只显示你自己的业务代码如果可能。搜索关键字符串在Sources面板的整个工作区CtrlShiftF搜索你抓包到的加密后的pwd值的一部分或者搜索encrypt、password、pwd等关键词可能会直接找到相关的代码片段。6.3 问题三加密函数中使用了未定义的变量或函数可能原因这些变量或函数来自其他JS文件或者是在一个大的闭包作用域内定义我们只提取了一部分。排查技巧全局搜索在Sources面板全局搜索这个未定义变量或函数的名字找到它的定义位置。作用域链查看在加密函数的断点处查看Scope作用域面板。这里会显示当前断点可访问的所有作用域局部、闭包、全局内的变量。你可能发现需要的函数就在闭包作用域里可以直接从那里复制其定义。整体提取有时最简单粗暴的方法是不是提取单个函数而是将整个包含加密逻辑的JS文件在Sources面板中可找到保存到本地然后通过Node.js的vm模块或简单修改后require进来直接调用其中的函数。6.4 问题四本地验证时加密结果与线上抓包结果不一致这是最令人头疼的情况意味着你的逆向模型与实际情况有偏差。检查清单密钥/IV是否正确这是最常见的错误。确认你使用的密钥和IV与线上完全一致包括其来源硬编码、计算生成、网络获取和格式字符串、Hex、Base64。编码问题密码字符串在加密前是否被转换了编码比如从UTF-8转成了UTF-16LE在JavaScript中字符串通常是UTF-16但加密库可能要求UTF-8字节数组。使用CryptoJS.enc.Utf8.parse(plainPwd)明确指定。盐值或随机数算法中是否引入了随机数Nonce或时间戳每次加密结果不同是正常的。你需要分析这个随机数是如何生成并传递的有时会作为另一个参数nonce或salt发送给服务器。多步加密或嵌套你找到的加密函数可能只是其中一环。密码可能先经过一次RSA加密用服务器公钥结果再经过一次AES加密。需要仔细梳理整个调用链。环境差异浏览器端的CryptoJS版本和你Node.js中安装的版本是否有细微差异尝试在浏览器控制台中直接执行你提取的代码片段与线上结果对比以排除环境问题。逆向分析是一个需要耐心、细心和反复验证的过程。它没有一成不变的公式更像是一场与代码设计者的“对话”。每一次成功的逆向不仅让你获得了某个参数的生成方法更深化了你对前端安全、密码学和JavaScript运行机制的理解。记住思路和方法的锻炼远比破解某一个特定的加密更有价值。