1. 项目概述与目标最近在分析一个网站的前端登录逻辑时发现它的密码在提交前被加密成了一串看不懂的密文。作为一名开发者我本能地想知道这背后是怎么实现的是简单的哈希还是复杂的加密为了搞清楚这个问题我决定在 Windows 11 环境下从零开始搭建一个 Node.js 环境并利用crypto-js这个库来模拟和逆向分析这个加密过程。这不仅仅是一个技术探索更是理解现代 Web 应用安全机制的一个绝佳实践。通过这个项目你将学会如何在 Windows 11 上配置 Node.js如何使用crypto-js进行常见的加密操作并最终掌握一套逆向分析前端加密逻辑的通用方法。无论你是前端开发者想深入了解安全还是对 Web 逆向感兴趣这篇实战指南都能给你提供清晰的路径和可复现的代码。2. Windows 11 上的 Node.js 环境搭建2.1 为什么选择 Node.js 和 crypto-js在开始动手之前我们先明确一下工具选型。Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时它让我们能够在服务器端或者本地开发环境运行 JavaScript。选择它来分析前端加密有两大优势一是语言一致前端是 JavaScript我们用 Node.js 模拟语法和核心 API如Buffer高度统一降低了学习成本二是生态丰富npm上有海量的库其中crypto-js是一个纯 JavaScript 实现的加密算法库支持 AES、DES、SHA256、HMAC 等多种标准算法完美契合我们分析前端加密的需求。至于操作系统Windows 11 是目前个人电脑的主流系统其内置的 PowerShell 终端和 WSLWindows Subsystem for Linux支持使得开发环境配置非常灵活。我们这次就使用原生的 PowerShell 进行所有操作确保流程对广大 Windows 用户友好。2.2 安装 Node.js 与 npmNode.js 的安装过程在 Windows 上已经非常傻瓜化。我强烈建议从官网nodejs.org下载 LTS长期支持版本的安装程序。为什么是 LTS因为它更稳定兼容性更好适合开发和逆向分析这种对环境一致性要求高的场景。下载完成后双击安装程序基本上一路“Next”即可。这里有个关键点需要注意安装向导会询问是否将 Node.js 和 npm 添加到系统 PATH 环境变量务必勾选上。这能让你在任意位置的 PowerShell 中直接使用node和npm命令。安装完成后我们需要验证一下。打开 PowerShell可以在开始菜单搜索“PowerShell”并以管理员或普通用户身份运行输入以下命令node --version npm --version如果正确显示了版本号例如v18.20.0和10.7.0恭喜你第一步已经成功。如果提示“不是内部或外部命令”说明环境变量可能未生效可以尝试重启 PowerShell 或者手动检查系统环境变量 PATH 中是否包含了 Node.js 的安装路径通常类似C:\Program Files\nodejs\。注意有些公司的电脑或旧系统可能设置了严格的执行策略导致脚本无法运行。如果你在后续使用npm install时遇到权限错误可以尝试以管理员身份运行 PowerShell并执行Set-ExecutionPolicy RemoteSigned来更改执行策略选择[A] 全是操作完成后记得改回默认值以保安全。2.3 初始化项目与安装 crypto-js环境准备好后我们创建一个专门的项目目录来操作。在 PowerShell 中导航到你常用的工作目录例如D:\Projects然后执行mkdir password-crypto-analysis cd password-crypto-analysis npm init -ynpm init -y命令会快速创建一个默认的package.json文件其中包含了项目的基本信息和依赖管理配置。-y参数表示接受所有默认选项省去交互式问答。接下来安装我们本次实战的核心库crypto-jsnpm install crypto-js这个命令会从 npm 仓库下载crypto-js库及其依赖并保存在项目下的node_modules文件夹中同时在package.json的dependencies字段里记录这个依赖。安装过程通常很快网络通畅的话几秒钟就能完成。为了更方便地编写和测试我们的分析脚本我建议再安装一个辅助工具nodemon。它可以监视文件变化并自动重启 Node.js 应用在开发调试时非常省心。使用--save-dev参数将其安装为开发依赖npm install --save-dev nodemon安装完成后你的package.json文件应该类似于这样{ name: password-crypto-analysis, version: 1.0.0, description: , main: index.js, scripts: { test: echo \Error: no test specified\ exit 1 }, keywords: [], author: , license: ISC, dependencies: { crypto-js: ^4.2.0 }, devDependencies: { nodemon: ^3.1.0 } }现在基础环境已经就绪。我们创建两个核心文件analyze.js用于编写主要的逆向分析逻辑test.html作为一个简单的模拟前端页面用来生成加密后的密码供我们分析。在项目根目录下执行New-Item -Path . -Name analyze.js -ItemType file New-Item -Path . -Name test.html -ItemType file或者直接用你喜欢的代码编辑器如 VSCode在项目中新建这两个文件。至此一个干净、专注的 Node.js 分析环境就在你的 Windows 11 上搭建完成了。3. 前端密码加密常见模式与 crypto-js 基础在动手逆向之前我们必须先了解“敌人”可能使用的武器。现代 Web 前端对密码的加密更准确地说是哈希或编码通常不是为了实现高强度的机密性那是 HTTPS 和后端加密该做的事而是为了增加攻击者直接获取明文密码的难度或者满足后端特定的验证格式。常见的模式有以下几种而crypto-js库都能很好地支持对这些模式的模拟和验证。3.1 哈希Hash算法单向不可逆哈希是前端处理密码最常见的方式。它将任意长度的输入密码通过一个数学函数哈希算法转换成固定长度的字符串哈希值。核心特性是单向性从哈希值几乎无法反推出原始密码。常用的算法有MD5: 产生 128 位32位十六进制字符哈希值。已不安全因其碰撞漏洞已被广泛证实但一些老旧系统可能仍在用。SHA-1: 产生 160 位40位十六进制字符哈希值。安全性也已受到威胁逐渐被淘汰。SHA-256: SHA-2 家族的一员产生 256 位64位十六进制字符哈希值。目前是行业标准广泛应用于密码存储和数字签名。SHA-512: 更长的 512 位哈希更安全但计算量稍大。使用crypto-js计算哈希非常简单。我们在analyze.js中写一段测试代码// analyze.js const CryptoJS require(crypto-js); const password MySecretPassword123; // 计算不同算法的哈希 const md5Hash CryptoJS.MD5(password).toString(); const sha1Hash CryptoJS.SHA1(password).toString(); const sha256Hash CryptoJS.SDHA256(password).toString(); // 注意正确的属性名是 SHA256 const sha512Hash CryptoJS.SHA512(password).toString(); console.log(MD5:, md5Hash); console.log(SHA1:, sha1Hash); console.log(SHA256:, sha256Hash); console.log(SHA512:, sha512Hash);运行node analyze.js你会看到四串不同的十六进制输出。一个关键细节是单纯的哈希很容易受到彩虹表攻击预先计算好的哈希字典。因此实战中通常会加“盐”Salt。3.2 HMAC基于密钥的哈希消息认证码HMAC 可以看作是一种“带密钥的哈希”。它需要一个密钥Secret Key和消息密码一起计算哈希。即使相同的密码使用不同的密钥也会产生完全不同的 HMAC 值安全性更高。后端通常会提供一个动态或固定的密钥给前端用于计算 HMAC。const secretKey aFixedSecretFromBackend; const hmacSha256 CryptoJS.HmacSHA256(password, secretKey).toString(); console.log(HMAC-SHA256:, hmacSha256);3.3 AES 对称加密可逆的加密AES高级加密标准是一种对称加密算法意味着加密和解密使用同一个密钥。前端有时会用 AES 加密密码后再传输但这要求密钥在前端代码中本质上是不安全的因为前端代码对用户是透明的只能起到增加逆向难度或编码转换的作用。crypto-js支持 AES 加密并通常需要指定模式如 CBC和填充方式如 Pkcs7。const aesKey CryptoJS.enc.Utf8.parse(1234567890123456); // 密钥必须是16/24/32字节 const iv CryptoJS.enc.Utf8.parse(1234567890123456); // 初始化向量通常16字节 const encrypted CryptoJS.AES.encrypt(password, aesKey, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }).toString(); console.log(AES Encrypted (Base64):, encrypted); // 解密验证 const decrypted CryptoJS.AES.decrypt(encrypted, aesKey, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, }).toString(CryptoJS.enc.Utf8); console.log(AES Decrypted:, decrypted); // 应该输出 MySecretPassword1233.4 Base64 编码不是加密Base64 是一种编码方式将二进制数据转换成由 64 个字符A-Z, a-z, 0-9, , /组成的字符串。它常被用来“伪装”数据使其在纯文本环境如 HTTP 请求中安全传输。Base64 不是加密因为它没有密钥任何人都可以轻松解码。前端可能先对密码进行哈希或加密再将结果进行 Base64 编码后传输。const wordArray CryptoJS.enc.Utf8.parse(password); const base64Encoded CryptoJS.enc.Base64.stringify(wordArray); console.log(Base64 Encoded:, base64Encoded); // TXlTZWNyZXRQYXNzd29yZDEyMw const decoded CryptoJS.enc.Base64.parse(base64Encoded).toString(CryptoJS.enc.Utf8); console.log(Base64 Decoded:, decoded);掌握了这些基础模式我们就有了逆向分析的“武器库”。在实际网站中加密逻辑往往是这些基础模式的组合比如SHA256(密码 固定盐)、HMAC-SHA256(密码, 动态令牌)或者Base64(AES_ECB(密码))。我们的任务就是通过观察网络请求和前端代码找出它具体使用了哪种或哪几种组合。4. 逆向分析实战定位与模拟加密逻辑理论准备就绪现在进入实战环节。我们的目标是给定一个目标网站找出其登录时密码字段的加密方式并用 Node.js crypto-js 复现这一过程。这个过程就像侦探破案需要细心观察和逻辑推理。4.1 第一步捕获网络请求观察加密特征打开你浏览器的开发者工具F12切换到“网络”Network选项卡。清空现有记录然后在目标网站的登录框输入一个测试密码比如test123点击登录。在网络请求列表中找到登录请求通常是POST类型URL 可能包含login、signin等关键词。点击这个请求查看其“载荷”Payload或“请求体”Request Body。你会看到提交的数据重点关注password、pwd、encryptedPassword这类字段。它的值很可能是一长串看似随机的字符。记录下这个值这是我们分析的起点。观察要点长度与字符集如果全是十六进制字符0-9, a-f可能是简单的哈希MD5 32位SHA1 40位SHA256 64位。如果包含、/和很可能是 Base64 编码。如果还有其它特殊字符可能是经过加密后的二进制数据再以十六进制或 Base64 形式表示。参数名像sign、hmac这样的参数名强烈暗示使用了 HMAC 算法。其他参数注意请求中是否还有salt、nonce、timestamp、key等字段这些很可能是加密过程所需的盐或密钥。4.2 第二步审查前端 JavaScript 源码加密逻辑一定在前端的 JavaScript 代码中执行。在开发者工具的“源代码”Sources选项卡中搜索与密码字段或加密相关的关键词。你可以使用全局搜索CtrlShiftF搜索词可以尝试密码字段的id或name如#password、input[namepassword]加密函数名如encrypt、hash、CryptoJS、SHA、HMAC、AES提交表单时触发的事件如onsubmit、$.ajax、fetch、axios.post找到疑似加密的代码段后仔细阅读。现代网站可能会对 JavaScript 进行压缩和混淆变量名可能是单个字母增加了解读难度。这时需要耐心关注函数调用和字符串常量。如果看到了CryptoJS这个对象那几乎可以确定使用了我们正在研究的这个库。4.3 第三步构建模拟加密函数假设我们通过分析推测出目标网站的加密逻辑是password Base64(SHA256(明文密码 固定盐值))。其中盐值salt在代码中硬编码为my_fixed_salt。我们现在在analyze.js中编写复现函数// analyze.js - 模拟加密函数 function simulateWebsiteEncrypt(plainPassword) { const salt my_fixed_salt; // 1. 密码与盐拼接 const dataToHash plainPassword salt; // 2. 计算 SHA256 哈希 const hashDigest CryptoJS.SHA256(dataToHash); // 3. 将哈希结果转换为 Base64 字符串 const base64Output CryptoJS.enc.Base64.stringify(hashDigest); return base64Output; } // 测试我们的模拟函数 const testPassword test123; const simulatedOutput simulateWebsiteEncrypt(testPassword); console.log(模拟加密输出: ${simulatedOutput});运行这个脚本得到输出结果。然后回到浏览器用同样的密码test123再次尝试登录或使用开发者工具的重放请求功能捕获到的加密密码值应该与我们模拟输出的结果完全一致。如果一致恭喜你成功破译如果不一致说明我们的推测有误需要回到第二步重新审查代码看看是否漏掉了某些步骤比如密码是否先进行了 UTF-8 或 Unicode 转换盐的拼接顺序是salt password还是password salt哈希结果是否被转换成了十六进制toString()而不是 Base64是否进行了多轮哈希是否使用了 HMAC 而不是简单哈希4.4 第四步处理动态参数与复杂逻辑很多网站的加密逻辑不会这么简单。它们可能会引入动态参数比如从服务器获取一个临时的token或nonce将其作为盐或密钥的一部分。这时你需要分析这个动态参数是如何获取的可能来自上一个 API 响应或一个隐藏的 HTML 字段并在你的 Node.js 脚本中模拟这一获取过程。另一种复杂情况是加密逻辑被封装在一个庞大的、混淆过的 JavaScript 文件里直接解读非常困难。这时可以尝试一种“黑盒”方法在浏览器控制台中直接调用你找到的加密函数。首先在源代码中找到加密函数的入口比如window.encryptPassword。然后在控制台输入// 在浏览器控制台执行 var encrypted window.encryptPassword(test123); console.log(encrypted);如果能够成功输出并且输出值与网络请求中的一致那么你可以直接“借用”这个函数。更进阶的做法是使用puppeteer或playwright这类无头浏览器库在 Node.js 环境中自动化执行这段前端代码来获取加密结果这适用于加密逻辑极度复杂或严重混淆的情况。5. 编写健壮的分析脚本与调试技巧当我们初步确定了加密算法后需要编写一个健壮的、可配置的 Node.js 脚本来固化我们的分析成果。这个脚本应该能够灵活地处理不同的加密参数并方便地进行测试和调试。5.1 创建可配置的加密模块我们将加密逻辑抽象成一个模块将可变的部分如盐值、密钥、算法类型作为配置项。在项目根目录创建encryptor.js// encryptor.js const CryptoJS require(crypto-js); class PasswordEncryptor { constructor(config {}) { this.algorithm config.algorithm || SHA256; // 算法类型 this.salt config.salt || ; // 静态盐 this.secretKey config.secretKey || ; // HMAC密钥或AES密钥 this.iv config.iv || ; // AES初始化向量 this.outputEncoding config.outputEncoding || Base64; // 输出编码Hex, Base64 this.useHmac config.useHmac || false; this.useAES config.useAES || false; this.aesMode config.aesMode || CryptoJS.mode.CBC; this.aesPadding config.aesPadding || CryptoJS.pad.Pkcs7; } encrypt(plainText, dynamicSalt ) { let processedData plainText; let output; // 1. 处理盐值拼接 const finalSalt this.salt dynamicSalt; // 假设是后缀盐 if (finalSalt) { processedData processedData finalSalt; } // 2. 核心加密/哈希 if (this.useAES this.secretKey) { const key CryptoJS.enc.Utf8.parse(this.secretKey); const iv this.iv ? CryptoJS.enc.Utf8.parse(this.iv) : undefined; const encryptedObj CryptoJS.AES.encrypt(processedData, key, { iv: iv, mode: this.aesMode, padding: this.aesPadding, }); output encryptedObj.ciphertext; // 获取CipherParams对象中的密文WordArray } else if (this.useHmac this.secretKey) { output CryptoJS.HmacSHA256(processedData, this.secretKey); } else { // 使用指定的哈希算法 switch (this.algorithm.toUpperCase()) { case MD5: output CryptoJS.MD5(processedData); break; case SHA1: output CryptoJS.SHA1(processedData); break; case SHA256: default: output CryptoJS.SHA256(processedData); break; case SHA512: output CryptoJS.SHA512(processedData); break; } } // 3. 输出编码转换 if (this.outputEncoding.toUpperCase() HEX) { return output.toString(CryptoJS.enc.Hex); } else if (this.outputEncoding.toUpperCase() BASE64) { // 注意对于AES加密CryptoJS默认返回的就是Base64格式的字符串 if (this.useAES) { return output.toString(); } else { return CryptoJS.enc.Base64.stringify(output); } } else { return output.toString(); // 默认返回十六进制 } } } module.exports PasswordEncryptor;5.2 编写测试用例与调试创建test.js来系统性地测试我们的加密模块并与从目标网站捕获的真实数据进行比对// test.js const PasswordEncryptor require(./encryptor.js); // 场景1模拟简单的 SHA256 盐 Base64 console.log( 场景1: SHA256 Salt Base64 ); const config1 { algorithm: SHA256, salt: my_fixed_salt, outputEncoding: Base64 }; const encryptor1 new PasswordEncryptor(config1); const testPwd test123; const result1 encryptor1.encrypt(testPwd); console.log(输入: ${testPwd}); console.log(输出: ${result1}); console.log(预期需替换为实际抓包值: YzI0OG...此处填写你抓包得到的值); console.log(匹配: ${result1 YzI0OG... ? 是 : 否}); // 手动替换预期值 // 场景2模拟 HMAC-SHA256 console.log(\n 场景2: HMAC-SHA256 (Hex输出) ); const config2 { useHmac: true, secretKey: dynamic_key_from_server, outputEncoding: Hex }; const encryptor2 new PasswordEncryptor(config2); const result2 encryptor2.encrypt(testPwd); console.log(HMAC输出: ${result2}); // 场景3模拟 AES-CBC 加密 console.log(\n 场景3: AES-CBC 加密 ); const config3 { useAES: true, secretKey: 1234567890123456, // 16字节 iv: 1234567890123456, // 16字节 aesMode: CryptoJS.mode.CBC, aesPadding: CryptoJS.pad.Pkcs7 }; const encryptor3 new PasswordEncryptor(config3); const result3 encryptor3.encrypt(testPwd); console.log(AES加密输出(Base64): ${result3}); // 动态盐测试 console.log(\n 动态盐测试 ); const encryptorDynamic new PasswordEncryptor({ algorithm: SHA256, outputEncoding: Hex }); const dynamicSalt Date.now().toString(); // 模拟一个时间戳盐 const resultDynamic encryptorDynamic.encrypt(testPwd, dynamicSalt); console.log(动态盐: ${dynamicSalt}); console.log(输出: ${resultDynamic});通过运行node test.js我们可以快速验证不同配置下的输出并与抓包数据对比。调试的核心在于比对让你的脚本输出与浏览器实际发送的数据一模一样。实操心得在比对时务必注意字符的大小写和编码格式。有些网站传输的十六进制可能是大写而crypto-js默认输出小写这时需要用.toUpperCase()转换。Base64 字符串也可能存在 URL 安全变种将和/替换为-和_需要使用CryptoJS.enc.Base64.parse后再做比较或者进行字符串替换。5.3 使用 Nodemon 进行热重载调试在开发过程中频繁修改代码并重启 node 进程很麻烦。我们可以利用之前安装的nodemon。修改package.json中的scripts部分scripts: { dev: nodemon test.js, analyze: node analyze.js }然后在终端运行npm run dev。nodemon会监视test.js及其依赖文件的变化一旦你保存修改它会自动重启应用让你能立刻看到新代码的输出效果极大提升调试效率。6. 常见问题排查与安全思考在逆向分析和模拟加密的过程中你几乎一定会遇到各种问题导致输出对不上。这里我总结了一份常见问题排查清单以及一些重要的安全启示。6.1 加密结果不一致的排查清单当你模拟的加密结果与网站实际发送的值不匹配时请按照以下顺序检查排查项可能原因验证方法1. 输入源是否一致前端可能对密码进行了trim()去除首尾空格或使用了encodeURIComponent。在控制台打印加密函数输入前的密码值确保与你输入的完全一致。2. 盐/密钥的拼接方式盐可能是前缀 (saltpassword)、后缀 (passwordsalt)或者更复杂的插值。尝试不同的拼接顺序查看前端代码中字符串连接 () 或模板字符串的操作。3. 字符编码问题crypto-js默认使用 UTF-8但有些老旧系统可能用escape/unescape或 Latin1 编码。尝试CryptoJS.enc.Latin1.parse(password)代替默认的 UTF-8 转换。4. 哈希输出格式网站可能将哈希后的 WordArray 对象直接转为字符串默认十六进制也可能先转成 Base64。在浏览器控制台查看加密函数的返回值类型和原始值。用toString()和toString(CryptoJS.enc.Base64)分别尝试。5. 是否有多轮哈希可能是hash(hash(password) salt)这样的多轮操作。仔细阅读前端加密函数看是否有循环或多次调用哈希函数。6. 是否使用了特殊的 AES 参数模式除了 CBC还可能是 ECB、CFB、OFB 等填充方式除了 Pkcs7还可能是 Iso97971、AnsiX923 等。查看CryptoJS.AES.encrypt的第三个参数配置对象。crypto-js支持的模式和填充在CryptoJS.mode和CryptoJS.pad对象下。7. 是否有动态参数干扰加密可能依赖页面上的一个随机数、时间戳或服务器下发的令牌。分析网络请求序列在登录请求前是否有获取密钥或令牌的请求。将获取到的动态值作为参数传入你的加密函数。8. 代码混淆与反调试关键函数名被重命名逻辑被分割和隐藏。使用浏览器开发者工具的“源代码映射”Source Map功能如果有。或者在加密函数入口设置断点单步跟踪执行观察中间变量值。6.2 逆向分析中的安全与法律边界在进行此类技术探索时必须时刻牢记安全与法律的边界仅用于授权测试与学习你只应该对你拥有明确授权测试的网站如公司内部系统、公开的测试环境或纯粹为了个人学习目的分析其公开的前端代码。未经授权对他人网站进行逆向分析和攻击是非法行为。不要尝试破解密码我们分析的是加密方式而不是为了破解某个特定用户的密码。哈希算法的单向性保证了从哈希值反推密码在计算上是不可行的弱密码除外。我们的目标是理解技术实现而非破解。前端加密不等于安全通过这个项目你应该深刻认识到任何在前端 JavaScript 中实现的加密其密钥和逻辑对用户都是可见的。因此前端加密不能替代 HTTPSTLS传输安全也不能替代后端对密码进行加盐哈希存储。它的主要作用往往是增加攻击复杂度防止简单的网络抓包直接获取明文密码。满足合规要求某些场景要求密码在传输过程中不能以明文形式出现。统一数据格式将密码处理成后端期望的固定格式如特定的哈希值。保护自己的分析成果你编写的分析脚本可能包含敏感的加密逻辑。不要将其公开上传到 GitHub 等公共仓库除非已完全脱敏。避免在脚本中硬编码真实的网站域名、密钥等敏感信息。6.3 从分析到防御给开发者的建议作为开发者从攻击者分析者的角度审视自己的系统能更好地构建防御避免在前端使用硬编码的对称加密密钥这形同虚设。如果必须前端加密使用非对称加密后端生成公钥前端用公钥加密后端用私钥解密。这样即使加密逻辑暴露攻击者没有私钥也无法解密。强化哈希过程后端存储密码时务必使用强哈希算法如 Argon2、bcrypt、PBKDF2并为每个用户使用独立、随机的盐。启用并正确配置 HTTPS这是保护传输过程中数据安全的基石。考虑使用 Web Crypto API这是一个浏览器原生支持的加密 API比引入第三方库可能更高效但同样无法隐藏逻辑。通过这个从环境搭建到逆向实战的完整流程你不仅掌握了crypto-js在 Node.js 中的用法更重要的是获得了一套分析前端加密行为的通用方法论。下次再遇到神秘的登录加密你就能有条不紊地揭开它的面纱了。记住工具和技巧是中立的如何使用它们取决于你的意图和所遵守的规则。