1. 项目概述:从登录请求到JS逆向的实战之旅
做爬虫的朋友,估计没少在登录这一步上栽跟头。尤其是现在稍微有点规模的网站,登录接口早已不是当年那个简单的
POST /login
,传个用户名密码就完事了。各种加密参数、动态令牌、滑块验证,把请求包弄得像天书一样。最近在分析一个网站的登录流程时,就遇到了一个典型的案例:请求里带了一个长长的、看起来毫无规律的
signature
参数,明摆着是前端JS计算出来的。今天,我就以这个“某网站登录案例”为引子,带大家走一遍完整的JS逆向分析流程。这不是一个高深莫测的教程,而是一个一线爬虫工程师的实战记录,我会把思路、工具、踩过的坑都摊开来讲清楚。无论你是刚接触逆向的新手,还是想梳理一下分析思路的老手,相信都能从中找到有用的东西。我们的目标很简单:找到那个关键的加密函数,理解它的逻辑,最终在我们的代码里复现它,成功模拟登录。
2. 逆向前的准备:工具与思路梳理
工欲善其事,必先利其器。在开始逆向分析之前,准备好趁手的工具和清晰的分析思路,能让你事半功倍,避免在浩瀚的JS代码里迷失方向。
2.1 核心工具链选择
浏览器开发者工具是基石,但光靠它还不够。我常用的组合是 Chrome/Edge(内核一致,工具通用) + 油猴脚本辅助 + 本地Node.js调试环境。
-
浏览器开发者工具(DevTools)
:这是主战场。重点关注
Network(网络)
和
Sources(源代码)
面板。Network面板要勾选
Preserve log(保留日志),防止页面跳转后请求记录消失;同时注意观察请求的Initiator(发起者)列,它能帮你追踪是哪个JS文件发起了这个登录请求。Sources面板则是我们分析、断点调试JS代码的地方。 -
油猴脚本(Tampermonkey)
:这是一个浏览器插件,允许你运行自定义的JavaScript脚本。在逆向中,它的一个妙用是注入一些辅助函数到页面中。比如,我可以写一个脚本,在页面加载后,重写
XMLHttpRequest和fetch的原型方法,用来在控制台打印出所有发起请求时的URL和参数,这对于定位加密发生的位置非常有效。 -
Node.js 环境
:当我们找到加密函数后,需要把它“抠”出来,在本地独立运行测试。Node.js环境是最佳选择。你可以使用
vm2这个沙盒模块来安全地执行不确定的页面JS代码片段,也可以直接用node命令执行我们还原后的JS文件。像CryptoJS这样的库也常在Node环境中用于模拟浏览器的加密行为。
注意 :不要一上来就试图去理解整个JS文件。现代网站前端代码往往经过打包、压缩、混淆,一个文件可能就有上万行。我们的策略是“顺藤摸瓜”,从网络请求这个“果”出发,反向找到生成它的“因”。
2.2 分析思路与步骤拆解
一个清晰的逆向分析流程,可以概括为以下四步,我称之为“定位-锁定-还原-复现”循环:
-
抓包定位
:在登录页面,打开开发者工具的Network面板,清空记录,然后进行登录操作。捕获到登录的POST请求后,仔细检查其
Form Data或Payload。我们的目标就是找出那些看起来是随机生成或加密的参数,比如token,sign,encryptedPassword,_signature,timestamp等。记下这个请求的URL和所有可疑参数。 -
搜索与断点
:在Sources面板中,使用全局搜索(Ctrl+Shift+F)功能,搜索可疑参数的键名(如
signature)或一些固定的值(如参数名可能由var a = "signature"定义)。更高级的方法是,在Network面板中,找到那个登录请求,点击其Initiator列,它通常会指向一个JS调用栈,直接带你到发起请求的代码行附近。在这里打下断点(XHR/fetch断点更精准),重新触发登录,代码就会在此处暂停。 -
调用栈分析
:当代码在断点处暂停后,不要只看当前行。观察右侧的
Call Stack(调用栈)。调用栈展示了函数调用的层级关系,最上面是当前函数,下面是调用它的父函数。我们顺着调用栈一层层往上回溯,就像侦探追踪线索一样,最终目的就是找到那个最初生成加密参数的函数。这个过程中,要密切关注变量的值,使用控制台(Console)实时计算和验证。 -
逻辑还原与补环境
:找到关键函数后,尝试将其代码片段复制出来。但通常它会依赖浏览器的特有对象(如
window,document,navigator)或其他JS文件中定义的全局变量。我们需要在Node.js环境中,模拟(或称“补全”)这些环境,让函数能够独立运行。这可能包括定义一些假的DOM对象,或者引入必要的加密库(如CryptoJS,jsencrypt)。
3. 实战拆解:登录请求的加密参数追踪
理论说再多,不如一次实战。假设我们目标网站的登录请求包如下:
POST /api/v1/login HTTP/1.1
...
Content-Type: application/json
{
"username": "test_user",
"password": "123456",
"signature": "7a89f8d7e24c3b1a05f9e2c8b7d6a4f0c5e...(很长一串)",
"timestamp": 1689134567890
}
很明显,
password
是明文,这不太安全,但重点在于那个
signature
。
timestamp
很可能也是生成
signature
的原料之一。
3.1 从网络请求到源代码断点
首先,在Network面板找到这个
/api/v1/login
请求。右键点击它,选择
Copy
->
Copy as cURL
,这能帮我们快速在命令行或脚本中重放这个请求,方便后续测试。
然后,我们点击这个请求的
Initiator
列。假设它显示了一个调用栈,最顶端是一个名为
login.js
(可能是压缩后的名字,如
chunk-vendors.abc123.js
)文件中的某一行。点击它,会自动在Sources面板打开这个JS文件,并定位到相应行。这一行很可能就是执行
fetch
或
axios.post
的地方。
在这一行打上断点(点击行号左侧)。刷新登录页面,再次点击登录按钮,浏览器执行到这一行时会自动暂停。此时,我们把鼠标悬停在代码中的变量上,或者在Console里输入变量名,就能看到它们当前的值。你可能会发现,
signature
变量在这里已经有了值。
3.2 逆向追踪加密函数调用链
我们的目标不是接受这个现成的
signature
,而是要找到它是怎么算出来的。看调用栈(Call Stack)。
假设调用栈显示如下:
-
(anonymous) @ login.js:20(当前行,发送请求) -
submitForm @ login.js:50 -
generateSignature @ utils.js:120 -
encryptWithKey @ crypto.js:45
这说明,在
submitForm
函数里调用了
generateSignature
,而
generateSignature
又调用了
crypto.js
里的
encryptWithKey
。我们双击调用栈中的
generateSignature @ utils.js:120
,代码视图会跳转到
utils.js
的第120行,也就是生成签名的核心逻辑所在。
现在,我们可以在
generateSignature
函数的入口处也打上断点,然后取消之前的断点,重新触发登录。这次,代码会在计算
signature
之前暂停。这时,我们就能一步步(F10单步跳过,F11单步进入)执行,观察每一步的变量变化。
在
generateSignature
函数里,你可能会看到类似这样的逻辑:
function generateSignature(username, password, timestamp) {
// 1. 拼接字符串
let rawStr = username + "|" + password + "|" + timestamp;
// 2. 进行某种哈希或加密,这里假设是HMAC-SHA256
let key = window._globalConfig.apiSecret; // 注意!key来自一个全局变量
let hash = CryptoJS.HmacSHA256(rawStr, key);
// 3. 转换为十六进制字符串
let signature = hash.toString(CryptoJS.enc.Hex);
return signature;
}
关键发现 :
-
算法
:使用的是
CryptoJS.HmacSHA256。CryptoJS是一个前端常用的加密库。 -
原料
:签名由
username、password、timestamp用竖线拼接而成。 -
密钥
:密钥
key来自于window._globalConfig.apiSecret。这是一个至关重要的信息,说明密钥是前端加载时从服务器获取并存储在全局变量中的。我们需要找到这个_globalConfig是在哪里被赋值的。
3.3 关键依赖与全局变量查找
现在问题变成了:
window._globalConfig.apiSecret
从哪里来?我们回到Sources面板,在所有的JS文件里全局搜索
_globalConfig
或
apiSecret
。
很可能在更早加载的一个初始化JS文件里,或者是在HTML页面内联的
<script>
标签里,有这样一段代码:
window._globalConfig = {
apiKey: 'public_key_123',
apiSecret: 'this_is_a_secret_key_abc', // 这就是我们需要的密钥!
env: 'production'
};
或者,它可能是通过一个初始化的API请求获取的,比如
GET /api/init
,其返回的数据被赋值给了
window._globalConfig
。
至此,加密逻辑已经完全清晰: HMAC-SHA256(username + “|” + password + “|” + timestamp, secretKey) 。
4. 加密逻辑还原与本地复现
找到了算法和密钥,下一步就是把它搬出浏览器,在我们自己的爬虫脚本里实现。
4.1 使用Node.js模拟加密过程
我们选择Node.js环境来复现。首先,需要安装
crypto-js
库,它是CryptoJS的Node.js版本。
npm install crypto-js
然后,编写我们的签名生成函数:
// sign.js
const CryptoJS = require('crypto-js');
function generateSignature(username, password, timestamp, apiSecret) {
// 拼接字符串,顺序和分隔符必须和前端完全一致!
const rawStr = `${username}|${password}|${timestamp}`;
// 使用HMAC-SHA256计算签名
const hash = CryptoJS.HmacSHA256(rawStr, apiSecret);
// 转换为十六进制字符串
const signature = hash.toString(CryptoJS.enc.Hex);
return signature;
}
// 使用示例
const apiSecret = 'this_is_a_secret_key_abc'; // 从window._globalConfig中获取
const username = 'test_user';
const password = '123456'; // 注意:前端可能是明文,也可能是先MD5再参与签名,需确认
const timestamp = Date.now(); // 获取当前时间戳
const sig = generateSignature(username, password, timestamp, apiSecret);
console.log('生成的签名:', sig);
console.log('时间戳:', timestamp);
运行这个脚本,就能生成和浏览器端一模一样的
signature
。
4.2 处理动态密钥与反调试陷阱
事情不会总这么顺利。在实际操作中,你会遇到更多挑战:
-
动态密钥
:
apiSecret可能不是写死在JS里的,而是每次页面加载时通过一个加密的接口获取,或者本身就是一个有时效性的Token。这时,你的爬虫需要先模拟访问首页或初始化接口,解析出这个密钥,然后再用于登录签名。这相当于把“获取密钥”也变成了爬虫流程的一环。 -
代码混淆与反调试
:开发者会使用Webpack、Vue CLI等工具打包,并用Terser等工具进行代码混淆(变量名缩短、代码结构扁平化)。更厉害的会有反调试手段,比如在代码中检测开发者工具,如果打开就进入死循环或者跳转到错误页面。应对方法包括:
-
使用
debugger;语句 :在关键函数入口手动添加debugger;,即使代码被混淆,执行到这里也会暂停。 -
Hook技术
:使用油猴脚本或浏览器插件(如
Tampermonkey)提前注入代码,劫持(Hook)关键函数。例如,在页面加载之初就重写CryptoJS.HmacSHA256方法,让它先打印出参数和结果,再执行原逻辑。
// 油猴脚本示例:Hook CryptoJS.HmacSHA256 let originalHmacSHA256 = CryptoJS.HmacSHA256; CryptoJS.HmacSHA256 = function(message, key) { console.log('[Hook] HmacSHA256 called:'); console.log('Message:', message); console.log('Key:', key); let result = originalHmacSHA256(message, key); console.log('Result:', result.toString()); return result; };-
本地替换JS文件
:利用开发者工具的
Overrides(重写)功能,将线上混淆的JS文件,替换成本地格式化后的清晰版本,方便阅读。
-
使用
4.3 完整登录脚本示例
结合上面的分析,一个完整的Node.js登录脚本骨架如下:
const axios = require('axios');
const CryptoJS = require('crypto-js');
// 1. 首先,可能需要获取动态的apiSecret
async function getApiSecret() {
// 模拟访问首页,从响应HTML或后续接口中提取
// 这里假设从一个初始化接口获取
const initResp = await axios.get('https://target-site.com/api/init');
// 解析initResp.data,找到apiSecret。可能是JSON,也可能藏在JS变量里需要正则提取
const apiSecret = initResp.data.config.apiSecret; // 假设结构如此
return apiSecret;
}
// 2. 生成签名的函数
function generateSignature(username, password, timestamp, apiSecret) {
const rawStr = `${username}|${password}|${timestamp}`;
return CryptoJS.HmacSHA256(rawStr, apiSecret).toString(CryptoJS.enc.Hex);
}
// 3. 主登录函数
async function login(username, password) {
try {
const apiSecret = await getApiSecret(); // 获取密钥
const timestamp = Date.now();
const signature = generateSignature(username, password, timestamp, apiSecret);
const loginData = {
username: username,
password: password, // 注意:如果前端对password有额外处理(如RSA加密),这里也需要模拟
signature: signature,
timestamp: timestamp
};
const response = await axios.post('https://target-site.com/api/v1/login', loginData, {
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 ...' // 模拟浏览器UA
}
});
console.log('登录成功:', response.data);
// 通常response.data里会包含session token或cookie信息,后续请求需要携带
return response.data;
} catch (error) {
console.error('登录失败:', error.response?.data || error.message);
}
}
// 执行登录
login('your_username', 'your_password');
5. 常见问题排查与进阶技巧
即使按照流程走,也难免会遇到各种问题。这里记录几个我踩过的坑和对应的解决办法。
5.1 签名验证失败的原因分析
本地生成的签名和浏览器抓包看到的不一样?可以从以下几个方面排查:
-
原料(Message)不一致 :
-
顺序
:
username|password|timestamp和timestamp|username|password的结果天差地别。 -
分隔符
:是竖线
|,冒号:,还是空字符串''? -
数据格式
:
timestamp是字符串还是数字?前端可能是String(Date.now())。 -
密码预处理
:前端提交的
password字段可能是明文,但参与签名的password可能是MD5之后的值。务必在断点处检查参与计算的实际值。
-
顺序
:
-
密钥(Key)错误 :
-
你找到的
apiSecret可能不是最终使用的那个。可能有多个配置对象,或者密钥经过了二次解码(如Base64)。 - 密钥本身可能是动态的,你获取到的已经过期。
-
你找到的
-
算法识别错误 :
-
看起来像HMAC-SHA256,但可能是HMAC-SHA1或者MD5。仔细看断点处的代码,
CryptoJS.HmacSHA256这个函数名是明确的指示。 - 除了HMAC,也可能是普通的SHA256哈希,或者AES加密后再Base64。
-
看起来像HMAC-SHA256,但可能是HMAC-SHA1或者MD5。仔细看断点处的代码,
-
编码问题 :
-
哈希结果转成字符串,是
Hex(十六进制)还是Base64?前端代码里toString()的参数是关键。
-
哈希结果转成字符串,是
排查技巧 :在浏览器断点处,将参与计算的 所有变量 (原始字符串、密钥)以及 最终结果 都在控制台打印出来。然后,在你的Node.js脚本里,先用这些 完全相同的输入 去计算,看结果是否匹配。如果匹配,说明算法还原正确,问题出在输入参数的获取上;如果不匹配,说明算法还原有误。
5.2 应对复杂的代码混淆与加密
当代码被严重混淆,所有变量都变成
a, b, c, d
时,不要慌。
-
关注常量字符串
:混淆不会改变字符串常量。搜索像
"signature","HmacSHA256","toString","CryptoJS"这样的字符串,它们能帮你快速定位到加密相关的代码块。 -
跟栈走,不跟变量名
:不要试图去理解
a = b(c, d)是什么意思。而是关注 函数的输入和输出 。在断点处,记录下进入某个“黑盒函数”时的参数值,和它返回的结果值。即使你不知道这个函数内部怎么实现,你也可以在本地用其他方式(比如根据输入输出猜算法,或者直接把这个函数体代码整体抠出来)来模拟这个“黑盒”。 -
直接扣代码
:有时最简单粗暴的方法最有效。在Sources面板里,选中关键函数以及它所有依赖的函数和变量(可以通过右键
Save as...保存整个文件,或者仔细复制),尝试整体复制到一个新的JS文件中。然后在Node环境里,通过补全缺失的浏览器对象(如window,document)来让它运行。vm2沙盒模块可以帮助执行这种不安全的代码。
5.3 滑动验证码等交互式反爬的应对思路
本文案例是参数加密,但很多网站登录还有滑块、点选等验证码。对于这类问题,JS逆向的目标会发生变化:
- 识别验证触发 :首先需要逆向分析,提交登录请求时,是什么条件会触发验证码?可能是IP频率、Cookie状态、或请求头缺失某个字段。
-
分析验证流程
:验证码本身是一个独立的交互流程。需要分析:
- 获取验证参数 :前端如何获取滑块图片、缺口位置、令牌(token)?
- 轨迹生成 :滑动验证码需要模拟人的滑动轨迹。轨迹数据(包含一系列移动事件的时间、坐标)往往是加密后提交的。需要找到生成和加密轨迹的JS。
-
结果验证
:提交滑动结果后,前端会得到一个验证通过的
token,这个token需要随登录请求一起发送。
-
自动化方案
:纯逆向解决滑动验证码成本很高。通常的实战方案是:
- 打码平台 :将验证码图片发送到第三方打码平台,由人工或AI识别返回结果(如缺口位置)。
-
轨迹模拟库
:使用如
Puppeteer,Playwright等无头浏览器库,配合stealth-plugin躲避检测,真实地渲染页面并模拟鼠标移动。这不再是单纯的JS逆向,而是浏览器自动化。 - 协议逆向 :终极方案是彻底逆向整个验证码的通信协议,包括加密算法,完全用代码模拟。但这需要极高的逆向技巧,且一旦网站更新,维护成本巨大。
对于登录中的滑块,一个折中的思路是:通过逆向找到绕过滑块直接获取有效
token
的接口漏洞(可能性极低),或者识别出哪些账号/IP行为不会触发滑块(例如,维护一个活跃的会话Cookie)。大多数情况下,面对强滑块验证,引入无头浏览器自动化是更稳妥的选择。
JS逆向就像一场和网站开发者的博弈,关键在于耐心、细致的观察和逻辑推理。从一个小小的
signature
参数出发,层层剥茧,最终还原出前端的完整逻辑,这种成就感是无可替代的。每一次成功的逆向,不仅解决了一个具体的爬虫问题,更让你对Web前端的安全机制有了更深的理解。记住,工具和技巧是辅助,清晰的思路和解决问题的耐心才是核心。

237

被折叠的 条评论
为什么被折叠?



