JS逆向实战:从登录请求signature参数追踪到HMAC-SHA256加密复现

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 分析思路与步骤拆解

一个清晰的逆向分析流程,可以概括为以下四步,我称之为“定位-锁定-还原-复现”循环:

  1. 抓包定位 :在登录页面,打开开发者工具的Network面板,清空记录,然后进行登录操作。捕获到登录的POST请求后,仔细检查其 Form Data Payload 。我们的目标就是找出那些看起来是随机生成或加密的参数,比如 token , sign , encryptedPassword , _signature , timestamp 等。记下这个请求的URL和所有可疑参数。
  2. 搜索与断点 :在Sources面板中,使用全局搜索(Ctrl+Shift+F)功能,搜索可疑参数的键名(如 signature )或一些固定的值(如参数名可能由 var a = "signature" 定义)。更高级的方法是,在Network面板中,找到那个登录请求,点击其 Initiator 列,它通常会指向一个JS调用栈,直接带你到发起请求的代码行附近。在这里打下断点(XHR/fetch断点更精准),重新触发登录,代码就会在此处暂停。
  3. 调用栈分析 :当代码在断点处暂停后,不要只看当前行。观察右侧的 Call Stack (调用栈)。调用栈展示了函数调用的层级关系,最上面是当前函数,下面是调用它的父函数。我们顺着调用栈一层层往上回溯,就像侦探追踪线索一样,最终目的就是找到那个最初生成加密参数的函数。这个过程中,要密切关注变量的值,使用控制台(Console)实时计算和验证。
  4. 逻辑还原与补环境 :找到关键函数后,尝试将其代码片段复制出来。但通常它会依赖浏览器的特有对象(如 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)。

假设调用栈显示如下:

  1. (anonymous) @ login.js:20 (当前行,发送请求)
  2. submitForm @ login.js:50
  3. generateSignature @ utils.js:120
  4. 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;
}

关键发现

  1. 算法 :使用的是 CryptoJS.HmacSHA256 CryptoJS 是一个前端常用的加密库。
  2. 原料 :签名由 username password timestamp 用竖线拼接而成。
  3. 密钥 :密钥 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 签名验证失败的原因分析

本地生成的签名和浏览器抓包看到的不一样?可以从以下几个方面排查:

  1. 原料(Message)不一致

    • 顺序 username|password|timestamp timestamp|username|password 的结果天差地别。
    • 分隔符 :是竖线 | ,冒号 : ,还是空字符串 ''
    • 数据格式 timestamp 是字符串还是数字?前端可能是 String(Date.now())
    • 密码预处理 :前端提交的 password 字段可能是明文,但参与签名的 password 可能是MD5之后的值。务必在断点处检查参与计算的实际值。
  2. 密钥(Key)错误

    • 你找到的 apiSecret 可能不是最终使用的那个。可能有多个配置对象,或者密钥经过了二次解码(如Base64)。
    • 密钥本身可能是动态的,你获取到的已经过期。
  3. 算法识别错误

    • 看起来像HMAC-SHA256,但可能是HMAC-SHA1或者MD5。仔细看断点处的代码, CryptoJS.HmacSHA256 这个函数名是明确的指示。
    • 除了HMAC,也可能是普通的SHA256哈希,或者AES加密后再Base64。
  4. 编码问题

    • 哈希结果转成字符串,是 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逆向的目标会发生变化:

  1. 识别验证触发 :首先需要逆向分析,提交登录请求时,是什么条件会触发验证码?可能是IP频率、Cookie状态、或请求头缺失某个字段。
  2. 分析验证流程 :验证码本身是一个独立的交互流程。需要分析:
    • 获取验证参数 :前端如何获取滑块图片、缺口位置、令牌(token)?
    • 轨迹生成 :滑动验证码需要模拟人的滑动轨迹。轨迹数据(包含一系列移动事件的时间、坐标)往往是加密后提交的。需要找到生成和加密轨迹的JS。
    • 结果验证 :提交滑动结果后,前端会得到一个验证通过的 token ,这个 token 需要随登录请求一起发送。
  3. 自动化方案 :纯逆向解决滑动验证码成本很高。通常的实战方案是:
    • 打码平台 :将验证码图片发送到第三方打码平台,由人工或AI识别返回结果(如缺口位置)。
    • 轨迹模拟库 :使用如 Puppeteer , Playwright 等无头浏览器库,配合 stealth-plugin 躲避检测,真实地渲染页面并模拟鼠标移动。这不再是单纯的JS逆向,而是浏览器自动化。
    • 协议逆向 :终极方案是彻底逆向整个验证码的通信协议,包括加密算法,完全用代码模拟。但这需要极高的逆向技巧,且一旦网站更新,维护成本巨大。

对于登录中的滑块,一个折中的思路是:通过逆向找到绕过滑块直接获取有效 token 的接口漏洞(可能性极低),或者识别出哪些账号/IP行为不会触发滑块(例如,维护一个活跃的会话Cookie)。大多数情况下,面对强滑块验证,引入无头浏览器自动化是更稳妥的选择。

JS逆向就像一场和网站开发者的博弈,关键在于耐心、细致的观察和逻辑推理。从一个小小的 signature 参数出发,层层剥茧,最终还原出前端的完整逻辑,这种成就感是无可替代的。每一次成功的逆向,不仅解决了一个具体的爬虫问题,更让你对Web前端的安全机制有了更深的理解。记住,工具和技巧是辅助,清晰的思路和解决问题的耐心才是核心。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值