前端JS加密逆向实战:从算法特征识别到调试技巧全解析
1. 项目概述:从“黑盒”到“白盒”的逆向思维
在安全测试、渗透测试乃至日常的代码审计工作中,我们经常会遇到一个核心挑战:面对一串经过加密或编码的“天书”般的字符串,如何理解其背后的逻辑,甚至还原出原始信息?这就是“算法逆向”要解决的问题。它不是一个简单的“解密”动作,而是一套系统的分析思维和实战方法。今天,我们就来深入拆解这个领域的基础入门知识,涵盖散列、对称加密、非对称加密三大算法家族,并聚焦于前端JavaScript源码逆向这个高频且关键的实战场景。
无论你是刚入门的安全爱好者,还是希望提升代码审计能力的开发者,掌握这套“由密文反推逻辑”的技能都至关重要。它能帮你理解数据在传输和存储过程中是如何被保护的,更能让你在授权测试中,精准定位前端加密逻辑,从而构造有效的测试载荷。我们将从算法特征识别开始,逐步深入到JS代码的定位、分析与调试,手把手带你搭建一套可复用的逆向分析工作流。
2. 核心密码学算法特征与逆向思路
在动手逆向之前,我们必须先学会“认人”。不同的加密算法和编码方式,其产生的密文具有不同的特征。就像侦探通过指纹锁定嫌疑人一样,我们可以通过密文特征快速缩小排查范围。
2.1 单向散列函数:MD5、SHA家族的“指纹”识别
单向散列,常被称为哈希(Hash),其核心特点是不可逆。你可以把它理解为一个高效的“数字指纹生成器”。输入任意长度的数据,输出一个固定长度的、看似随机的字符串(哈希值)。理论上,无法从这个哈希值反推出原始数据。
1. 核心特征识别:
- 字符集:通常由小写字母(a-f)和数字(0-9)组成,即十六进制表示。例如:
e10adc3949ba59abbe56e057f20f883e。 - 固定长度:这是最显著的特征。
- MD5:32位字符(128位二进制),或16位字符(去掉每字节高4位的零,较少见)。
- SHA-1:40位字符(160位二进制)。
- SHA-256:64位字符(256位二进制)。
- SHA-512:128位字符(512位二进制)。
- 加盐(Salt):为了抵御彩虹表攻击,实践中常在明文哈希前或后拼接一个随机字符串(盐值)。密文本身可能看不出,但算法逻辑会体现为
md5(password + salt)或md5(md5(password) + salt)。在逆向JS时,需要找到这个盐值。
2. 逆向需求与可行性:严格来说,无法逆向。所谓的“MD5解密网站”实际上是庞大的“明文-密文”预计算数据库(彩虹表)的查询结果。对于弱口令(如123456、password)或常见组合,可能被“碰撞”出来。但对于复杂密码或加了唯一盐值的哈希,几乎不可能破解。
实操心得:遇到哈希值,首先尝试用
cmd5.com、somd5.com等网站查询。若查不到,且目标系统是知名CMS(如Discuz!),则需结合源码或习惯,分析其加盐模式。例如,Discuz! X3.2的密码哈希模式是md5(md5(password).salt),知道了盐值(可能在数据库或cookie中)才能进行验证。
2.2 对称加密:AES、DES的“钥匙与锁”
对称加密就像用一个钥匙上锁和开锁。加密和解密使用同一个密钥。算法是公开的,安全性完全依赖于密钥的保密性。
1. 核心特征识别:
- 密文形态:对称加密(如AES、DES)的输出是二进制数据,为了便于传输,通常会再进行一次Base64编码。因此,密文常表现为Base64字符串,可能包含
/、+、=等字符。例如:U2FsdGVkX19qBzV6H5g7J8Z7cGvq7NlE5QYjF7aK10w=。 - 模式与初始向量(IV):除了密钥,还需关注加密模式(如ECB、CBC)和初始向量(IV,CBC等模式需要)。ECB模式不需要IV,但安全性较低,相同明文会产生相同密文。CBC模式是更安全的选择,需要IV。
- 数据长度:明文长度增加,密文长度也会成块增加(如AES每16字节一块)。
2. 逆向需求与可行性:可以逆向,但前提是获取密钥。逆向的目标不是破解算法(AES本身是安全的),而是逆向出加密逻辑和找到密钥。
- 有源码:直接分析后端或前端源码,定位密钥、IV、模式等参数。
- 无源码,看前端:这是Web安全测试中最常见的场景。登录、提交等操作的参数加密往往在前端JavaScript中完成。我们需要从JS代码中找到加密函数、密钥和模式。
2.3 非对称加密:RSA的“公钥锁,私钥开”
非对称加密使用一对密钥:公钥和私钥。公钥加密的数据,只有对应的私钥能解密;私钥加密(即签名)的数据,任何人都可以用公钥验证其真实性。
1. 核心特征识别:
- 密文长度:RSA加密的密文长度很长,与密钥长度直接相关。一个1024位的RSA密钥,加密后的密文(经Base64编码)长度通常在180字符以上。例如一段明显的Base64长字符串。
- 密文随机性:即使每次加密相同的明文,由于填充机制(如OAEP)的存在,产生的密文也是不同的。这与ECB模式的对称加密形成对比。
- 密钥格式:在代码中,公钥或私钥常以
-----BEGIN PUBLIC KEY-----或-----BEGIN PRIVATE KEY-----这样的PEM格式出现。
2. 逆向需求与可行性:逆向的目标通常是获取公钥或分析加密逻辑。因为公钥往往是公开或可从前端获取的。用公钥加密的数据,在没有私钥的情况下无法解密,这保证了传输的安全性。在测试中,我们更多是确认加密方式,并尝试利用获取的公钥来加密我们的测试载荷。
2.4 编码:Base64的“伪装术”
Base64不是加密,而是一种编码。目的是将二进制数据用64个可打印ASCII字符表示,便于在不支持二进制的环境中(如HTTP、XML)传输。
1. 核心特征识别:
- 字符集:由大小写字母(A-Z, a-z)、数字(0-9)以及
+和/组成。 - 填充符:末尾常出现一个或两个
=,用于填充至4字节的倍数。 - 长度变化:编码后数据长度约为原长的4/3。
2. 逆向需求与可行性:完全可逆。逆向就是解码,任何编程语言或在线工具都能轻松做到。在逆向中,Base64常作为其他加密算法输出后的“包装”,看到Base64要先解码,再看里面的数据是否还有结构。
3. JS源码逆向实战:定位、分析与调试
前端JS加密逆向是Web安全测试的必修课。核心思路是:从网络请求出发,逆向追踪到生成该请求参数的JavaScript代码。
3.1 逆向分析工作流搭建
一个高效的逆向流程可以概括为以下四步:
- 捕获:使用浏览器开发者工具(F12)的Network面板,捕获目标请求(如登录的POST请求)。重点关注
Form Data或Payload中被加密的参数。 - 搜索:在Sources面板中,全局搜索(Ctrl+Shift+F)加密参数的键名(如
password、encryptedData)或一段已知的密文。 - 定位:在搜索结果的JS文件中,找到加密函数定义的位置。通常会是类似
function encrypt(data){...}、var encrypted = CryptoJS.AES.encrypt(...)或password: md5(pwd)的代码。 - 分析:理解该函数的逻辑,确定算法、密钥、IV、模式等所有参数。可能需要结合调试(Debugger)来动态跟踪变量的值。
3.2 常用工具与技巧
- 浏览器开发者工具:是核心工具。除了Network和Sources,Console面板可以执行JS代码片段进行测试,Debugger可以设置断点单步跟踪。
- Overrides功能:在Sources面板的Overrides标签中,可以将线上JS文件映射到本地副本。你可以在本地文件中插入
debugger;语句或修改代码逻辑,刷新页面后浏览器会执行你的本地版本,极大方便调试。 - Hook技术:对于混淆严重或动态加载的代码,可以使用Hook。例如,在Console中重写关键函数,捕获其输入输出。
// 示例:Hook CryptoJS.AES.encrypt var _encrypt = CryptoJS.AES.encrypt; CryptoJS.AES.encrypt = function(data, key, cfg){ console.log("[HOOK] AES Encrypt Called:"); console.log("Data:", data); console.log("Key:", key); console.log("Cfg:", cfg); var result = _encrypt(data, key, cfg); console.log("Result:", result.toString()); return result; } - AST(抽象语法树)反混淆:对于经过
obfuscator等工具重度混淆的代码,直接阅读是天书。需要借助像astexplorer.net或jsnice.org这样的在线反混淆工具,或者使用Python的js2py、esprima等库进行自动化分析。这属于进阶内容,但思路是先将混乱的代码还原成结构清晰的AST,再进行分析还原。
3.3 实战案例:逆向一个登录表单的AES加密
假设我们遇到一个登录页面,提交的密码字段password被加密成了一长串Base64。
- 捕获请求:输入测试账号
test和密码123456,点击登录,在Network中找到POST请求,发现password: "U2FsdGVkX19v4iRk5D8c9v4V1j5KbHp6Lm9nC..."。 - 全局搜索:在Sources面板全局搜索
password或encrypt。可能会找到类似引用了crypto-js.js库的代码。 - 定位加密点:进一步搜索
CryptoJS.AES.encrypt,找到核心代码段:function encryptPassword(pwd) { var key = CryptoJS.enc.Utf8.parse('1234567890123456'); // 密钥 var iv = CryptoJS.enc.Utf8.parse('abcdefgh12345678'); // IV var encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(pwd), key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); // 返回Base64字符串 } // 调用 $('#loginBtn').click(function(){ var pwd = $('#password').val(); $('#hiddenPassword').val(encryptPassword(pwd)); // 加密后放入隐藏域或直接提交 }); - 验证与利用:在Console面板中,复制上面的
encryptPassword函数并执行encryptPassword("123456"),看输出是否与捕获的密文一致。确认后,我们就掌握了加密算法(AES-CBC-Pkcs7)、密钥和IV。之后,我们可以用Python的pycryptodome库或任何支持AES的语言,使用相同的参数加密我们想要测试的密码,从而绕过前端加密验证,直接向接口发送有效载荷。from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 def encrypt_password(password): key = b'1234567890123456' iv = b'abcdefgh12345678' cipher = AES.new(key, AES.MODE_CBC, iv) ct_bytes = cipher.encrypt(pad(password.encode(), AES.block_size)) return base64.b64encode(ct_bytes).decode() print(encrypt_password("my_test_password"))
4. 各类算法逆向实战详解与代码示例
理论结合实践,我们通过具体代码来理解不同算法的逆向关注点。
4.1 散列(MD5)逆向:加盐的破解困境
如前所述,单纯的MD5可查询彩虹表。但加盐MD5的安全性取决于盐的保密性和唯一性。
// 常见加盐模式:md5(password + salt) 或 md5(md5(password) + salt) $password = '123456'; $salt = 'random_salt_here'; $hash1 = md5($password . $salt); // 模式1 $hash2 = md5(md5($password) . $salt); // 模式2 (Discuz!经典模式)逆向要点:逆向的目标是发现盐值和哈希模式。这通常需要源码泄露、目录遍历、或通过其他信息(如数据库泄露)推测。在JS逆向中,盐值可能硬编码在JS文件里,也可能通过一个API请求动态获取。
4.2 对称加密(AES/DES)逆向:关键在密钥
逆向对称加密,就是找密钥、IV和模式。
// 前端CryptoJS AES加密示例(CBC模式) var encrypted = CryptoJS.AES.encrypt( CryptoJS.enc.Utf8.parse("plain text"), CryptoJS.enc.Utf8.parse("16bytekey12345678"), // 密钥,必须是16/24/32字节 { iv: CryptoJS.enc.Utf8.parse("16byteiv12345678"), // IV,16字节 mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ).toString(); // 默认输出OpenSSL兼容格式的Base64字符串逆向要点:
- 识别库:确认使用的是
CryptoJS、forge还是其他加密库。 - 定位参数:找到
encrypt函数的三个关键参数:明文、密钥、配置项(含IV、mode、padding)。 - 密钥可能动态生成:有时密钥不是硬编码,而是通过
Math.random()、时间戳或某个API响应计算得出,需要跟踪其生成逻辑。
4.3 非对称加密(RSA)逆向:获取公钥
前端RSA加密通常用公钥加密,私钥留在服务器解密。
// 使用JSEncrypt库进行RSA加密的典型代码 var encryptor = new JSEncrypt(); encryptor.setPublicKey('-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSq...\n-----END PUBLIC KEY-----'); var encrypted = encryptor.encrypt('password123');逆向要点:
- 公钥暴露:公钥往往直接写在JS里或通过接口返回,很容易获取。
- 验证加密:获取公钥后,可以用相同库在本地加密任意数据,与前端加密结果比对,确认算法和填充方式(通常是PKCS#1 v1.5)。
- 无法解密:没有私钥,无法解密。测试时,我们使用获取的公钥来加密我们的测试载荷即可。
4.4 Base64与组合编码识别
单纯的Base64解码很简单。但实战中,它常与其他算法组合。
- AES -> Base64:这是标准操作。
- Base64 -> 二次自定义编码:有些开发者会进行自定义的字符替换(如
/换成_,+换成-),这需要观察规律并逆向替换回来。 - Hex -> Base64:先将十六进制字符串解码为二进制,再进行Base64编码。
逆向要点:养成习惯,对任何可疑字符串先尝试Base64解码。解码后如果仍是乱码,观察其是否像十六进制(0-9, a-f),尝试Hex解码。或者,如果看到U2FsdGVkX1开头,这很可能是OpenSSL格式的Salted AES加密结果的特征头。
5. 常见问题排查与实战避坑指南
在实际逆向过程中,你会遇到各种“坑”。这里记录一些典型问题和解决思路。
5.1 问题一:搜索不到加密函数
- 可能原因:代码被压缩(Minify)或混淆(Obfuscate),函数名和变量名被替换成了
a、b、c等无意义字符。 - 解决思路:
- 使用浏览器的“Pretty Print”功能(Sources面板中,
{}图标)格式化压缩代码,使其可读。 - 搜索加密参数的值(如
encryptedData:后面的部分),或者搜索加密库特有的常量字符串(如CryptoJS.mode.CBC)。 - 在加密操作发生的网络请求处打XHR/Fetch断点,然后触发请求,代码会自动在加密函数执行前暂停。
- 使用浏览器的“Pretty Print”功能(Sources面板中,
5.2 问题二:加密逻辑在WebAssembly或动态加载的JS中
- 可能原因:核心加密算法用C/C++编写并编译成WebAssembly(.wasm)以提高性能或增加逆向难度。或者,关键的JS代码是通过
eval、Function构造函数或动态<script>标签加载的。 - 解决思路:
- Wasm:在Network面板过滤
.wasm文件,下载后使用wasm2c、wasm-decompile等工具进行初步分析,但难度较大。更实用的方法是Hook WebAssembly的导出函数,在浏览器控制台中拦截其输入输出。 - 动态JS:在Sources面板的“Event Listener Breakpoints”中勾选“Script -> Script First Statement”,或在所有脚本加载前设置断点。也可以搜索
eval、new Function等关键字。
- Wasm:在Network面板过滤
5.3 问题三:密钥是动态的,每次都不一样
- 可能原因:前端先请求一个接口获取本次会话的密钥或Nonce。
- 解决思路:
- 清空Network记录,刷新页面,观察在提交加密数据之前,有哪些XHR/Fetch请求。重点关注返回JSON或文本的API。
- 分析这些请求的响应,寻找看起来像密钥、IV、时间戳或随机数的字段。
- 在Console中,可以在获取该响应的代码处下断点,查看其如何被后续的加密函数使用。
5.4 问题四:本地复现加密结果与线上不一致
- 可能原因:
- 字符编码:JS中字符串是UTF-16,而你的Python/Go代码可能按UTF-8处理。确保编解码一致。
- 填充方式:AES的填充可能是PKCS7、PKCS5或ZeroPadding。必须与前端完全一致。
- 输出格式:CryptoJS的
.toString()默认输出OpenSSL兼容格式,它实际是Salted__+盐值+密文的组合,再Base64。而你的代码可能只加密了数据本身。 - 密钥/IV处理:密钥和IV字符串转换成字节数组的方式必须一致。JS的
CryptoJS.enc.Utf8.parse等同于Python的.encode('utf-8')。
- 解决思路:使用最“笨”但最有效的方法——对照调试。在浏览器加密代码处打上断点,记录下明文字符串、密钥字节数组、IV字节数组的精确值。然后在你本地代码中,用完全相同的字节数组进行加密,再比对结果。
5.5 实战避坑技巧
- 从简单入手:先找一个最简单的、未混淆的网站练手,比如一些开源CMS的演示站。理解完整流程后,再挑战难度高的。
- 善用Console:Console不仅是输出日志的地方,更是一个强大的JS执行环境。你可以在这里重写函数、调用函数、查看对象结构,是动态分析的神器。
- 记录与归档:将成功逆向的站点的JS代码片段、密钥、算法逻辑保存下来。很多网站的加密方式大同小异,积累的代码片段会成为你宝贵的工具库。
- 理解业务逻辑:加密是为了保护什么?是登录密码、支付信息还是通信令牌?理解业务有助于你判断加密可能发生的位置和强度。
- 合法性红线:所有逆向分析必须在合法授权的范围内进行。未经授权对他人系统进行逆向、解密是违法行为。