1. 项目概述与核心价值
最近在带几个刚入行的朋友做JS逆向的实战练习,发现一个很有意思的现象:很多新手一上来就想搞复杂的参数加密,比如sign、token,结果卡在第一步就进行不下去了。其实,逆向的入门,往往是从最不起眼的地方开始的,比如请求头。今天这个案例,我们就拿一个大家可能都接触过的“某查查”类网站(泛指企业信息查询平台)来开刀,目标不是破解复杂的登录或数据加密,而是搞定它请求头里一个看似简单、实则关键的参数。
这个参数通常叫x-apiKey、Authorization或者一个自定义的token,它静静地躺在请求头里,却是服务器验证请求合法性的第一道门。很多新手会直接复制浏览器里的值去用,结果发现这个值一会儿就失效了,爬虫跑几分钟就挂了。这就是典型的“动态请求头”问题,也是JS逆向最经典的入门场景。通过这个案例,你能清晰地看到前端JavaScript是如何生成这个关键参数的,理解基本的浏览器环境检测逻辑,并掌握一套从抓包、定位到扣代码、模拟的完整逆向流程。无论你是想学习爬虫应对反爬,还是前端开发想了解安全机制,这个案例都再合适不过了。
2. 逆向目标分析与抓包定位
2.1 目标网站与参数初探
我们以某个典型的企业信息查询网站为例(以下简称“目标站”)。当你打开其搜索页面,输入公司名并点击查询时,浏览器会向服务器发送一个POST或GET请求来获取数据。打开浏览器的开发者工具(F12),切换到Network(网络)选项卡,勾选Preserve log(保留日志),然后进行一次搜索操作。
很快,你会找到一个包含查询结果的请求,比如叫api/search/v1。点击这个请求,查看它的Headers(请求头)。在一堆诸如User-Agent、Content-Type的常见头信息中,你很可能会发现一个“不速之客”,例如:
x-apiKey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c或者
Authorization: Bearer xxxxx.yyyyy.zzzzz又或者是一个完全自定义的名字,比如X-Client-Token。这个参数的值通常是一长串看起来像随机字符的字符串,而且每次刷新页面或重新搜索,这个值都会变化。这就是我们的目标——一个动态生成的请求头参数。
2.2 为什么这个参数如此重要?
服务器端会校验这个参数。如果请求中没有这个参数,或者参数值无效、过期,服务器通常会返回401 Unauthorized(未授权)或403 Forbidden(禁止访问)的错误,或者返回一个看似正常但数据为空的结果。直接使用你抓包时复制下来的那个值,短时间内可能有效,但很快(可能是几分钟,也可能是一次会话结束)就会失效。因此,我们必须找到这个参数在前端是如何被计算出来的。
注意:在开始任何逆向操作前,请务必阅读目标网站的
robots.txt文件和使用条款,并确保你的行为符合法律法规以及网站的规定。本案例仅用于技术学习与交流,请勿用于任何非法或侵扰性用途。
3. 逆向思路与关键环节拆解
面对一个动态的请求头参数,标准的逆向思路可以概括为“搜索、定位、分析、扣取”四步。这个流程是通用的,适用于大多数JS逆向场景。
3.1 逆向核心思路四步法
第一步:全局搜索定位这是最直接的方法。在开发者工具的Sources(源代码)面板,按Ctrl+Shift+F(Windows/Linux)或Cmd+Opt+F(Mac)打开全局搜索框。直接输入你发现的参数名,比如x-apiKey。如果网站没有对代码进行严重的混淆,你很可能会直接找到设置这个请求头的地方。这通常是一个XMLHttpRequest或现代更常用的fetch请求,在发送前被拦截并添加了自定义头部。
第二步:调用栈回溯如果全局搜索没有结果,或者结果太多难以定位,那么“调用栈回溯”就是你的利器。回到Network面板,找到那个携带目标参数的请求,右键点击它,选择Copy->Copy as cURL(或类似选项)。然后,在开发者工具中打开Sources面板,找到并点击Event Listener Breakpoints(事件监听器断点)。展开XHR/Fetch分类,勾选onreadystatechange或onload等事件断点。重新触发请求(比如再次点击搜索),代码执行会在这个请求发出前或收到响应后暂停。此时,查看Call Stack(调用栈)面板,你能看到一长串函数调用关系。从栈顶(最上面、最近被调用的函数)往下逐一查看,寻找那些包含设置请求头逻辑(如setRequestHeader方法)的代码。
第三步:逻辑分析与扣取找到关键代码后,不要急着全部复制。你需要分析这段代码的依赖关系。这个x-apiKey的值是从哪里来的?可能是:
- 从某个全局变量或对象属性中读取:比如
window._globalToken。 - 调用某个函数生成:比如
generateApiKey()或encrypt(timestamp)。 - 从页面隐藏元素或Cookie中获取。 你需要顺着代码的引用关系,像剥洋葱一样,一层层找到最根源的生成逻辑。这个过程就是“扣代码”,即把生成这个参数所必需的所有JavaScript函数和变量,从庞大的网站源码中提取出来。
第四步:环境补全与模拟执行扣出来的代码往往不能直接在你的Node.js或Python环境中运行,因为它依赖浏览器环境。常见的依赖包括:
window、document、navigator等BOM对象。CryptoJS、jsencrypt等前端加密库。- 网站自定义的一些全局函数或对象。 你需要使用像
jsdom、PyExecJS、Node.js的vm模块,或者更专业的Selenium、Puppeteer(无头浏览器)来补全这些环境,让扣出来的代码能够正确执行。
3.2 本案例的针对性策略
针对“某查查”这类网站,其x-apiKey的生成逻辑通常不会特别复杂(相较于核心业务数据的加密),但往往会包含一些反爬策略:
- 时间戳参与:密钥很可能与当前时间戳(
Date.now())有关,可能是直接拼接,也可能是经过某种编码(Base64)或哈希(MD5, SHA256)。 - 固定盐值或密钥:在代码中可能会硬编码一个字符串(盐值
salt)或密钥(secret),用于和时间戳等变量进行组合运算。 - 轻度混淆:变量名可能被压缩成单字母(如
a, b, c),但核心逻辑(如CryptoJS.HmacSHA256(timestamp, secret).toString())仍然清晰可辨。 我们的策略就是通过上述四步法,找到这个生成函数,分析其输入(时间戳、固定盐值)和输出(最终的x-apiKey),然后模拟实现。
4. 实操过程:定位、分析与扣取关键代码
假设我们通过全局搜索x-apiKey,在某个名为app.8a9f1c.js的压缩文件中找到了如下代码片段(代码已进行格式化以便阅读):
function s(t) { var e = Date.now().toString(); var n = o(e, "your_secret_key_here123"); return r.setRequestHeader("x-apiKey", n), t } function o(t, e) { var n = i.enc.Utf8.parse(e); var r = i.enc.Utf8.parse(t); var s = i.AES.encrypt(r, n, { mode: i.mode.ECB, padding: i.pad.Pkcs7 }); return s.toString(); }4.1 代码逻辑解析
- 函数
s(t):这很可能就是设置请求头的函数。它首先获取当前时间戳e,然后调用函数o,传入时间戳e和一个固定的字符串"your_secret_key_here123"。最后,使用setRequestHeader方法将o函数的返回值设置为x-apiKey的值。 - 函数
o(t, e):这是核心的加密函数。它接收两个参数t(时间戳)和e(密钥)。代码中使用了i这个对象进行加密操作。从i.AES.encrypt、i.mode.ECB等属性可以判断,i就是前端常用的CryptoJS加密库。i.enc.Utf8.parse:将字符串转换为CryptoJS内部使用的“WordArray”格式。i.AES.encrypt:使用AES算法进行加密。参数分别是:明文(时间戳转换的WordArray)、密钥(固定密钥转换的WordArray)、配置项(模式为ECB,填充为Pkcs7)。s.toString():将加密后的密文对象转换为字符串,这个字符串就是最终的x-apiKey值。
4.2 依赖分析与扣取
现在我们知道,生成x-apiKey需要:
CryptoJS库。- 当前时间戳。
- 固定的密钥
"your_secret_key_here123"。 - 加密函数
o的逻辑。
因此,我们需要扣取的代码包括:
- 整个
o函数。 - 确保
CryptoJS可用。我们可以直接扣取网站上加载的CryptoJS源码,或者更简单的方法:在Node.js环境中,使用npm install crypto-js安装官方库。
实操心得:在扣取类似
CryptoJS这种大型库时,不建议手动复制压缩后的源码。最佳实践是识别出网站使用的库及其版本,然后在你的本地环境中通过包管理器安装同名同版本的库,这样能最大程度保证兼容性,避免因版本差异导致的加密结果不一致。
5. 环境模拟与本地复现
我们选择在Node.js环境中复现这个逻辑,因为Node.js的生态丰富,执行JavaScript非常方便。
5.1 项目初始化与依赖安装
首先,创建一个新的项目目录并初始化。
mkdir js-reverse-demo && cd js-reverse-demo npm init -y然后,安装我们需要的crypto-js库。
npm install crypto-js5.2 编写复现代码
创建一个名为generate_key.js的文件,将我们分析并扣取的逻辑写入。
// 引入crypto-js库 const CryptoJS = require('crypto-js'); // 扣取的加密函数 o(t, e) function generateApiKey(timestamp, secret) { // 将密钥和明文转换为CryptoJS可处理的格式 const key = CryptoJS.enc.Utf8.parse(secret); const message = CryptoJS.enc.Utf8.parse(timestamp.toString()); // 确保是字符串 // AES-ECB-Pkcs7 加密 const encrypted = CryptoJS.AES.encrypt(message, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); // 返回密文字符串 return encrypted.toString(); } // 模拟请求头设置函数 s(t) function setRequestHeader() { const timestamp = Date.now(); // 获取当前时间戳 const secret = "your_secret_key_here123"; // 硬编码的密钥(从源码中获取) const apiKey = generateApiKey(timestamp, secret); console.log(`Timestamp: ${timestamp}`); console.log(`Generated x-apiKey: ${apiKey}`); // 这里模拟设置请求头,实际使用时是放入headers对象 const headers = { 'User-Agent': 'Mozilla/5.0...', 'Content-Type': 'application/json', 'x-apiKey': apiKey }; return headers; } // 执行并测试 const myHeaders = setRequestHeader(); console.log('完整的请求头示例:', myHeaders);5.3 运行测试与验证
在终端运行这个脚本:
node generate_key.js你会看到输出类似:
Timestamp: 1712345678901 Generated x-apiKey: U2FsdGVkX19qBzH8Q9Q7w1+3R4a7LmZ... 完整的请求头示例: { 'User-Agent': 'Mozilla/5.0...', 'Content-Type': 'application/json', 'x-apiKey': 'U2FsdGVkX19qBzH8Q9Q7w1+3R4a7LmZ...' }关键验证步骤:
- 在同一时刻(几秒内),用浏览器访问目标网站,抓取真实的
x-apiKey值。 - 同时运行你的Node.js脚本,生成一个
x-apiKey。 - 对比两个值。如果它们完全一致,恭喜你,逆向成功!如果不一致,请检查:
- 时间戳精度:网站用的是秒级时间戳(
Math.floor(Date.now()/1000))还是毫秒级(Date.now())? - 密钥是否正确:确认你扣取的密钥字符串和源码中完全一致,包括大小写和特殊字符。
- 加密配置:AES的模式(ECB, CBC)、填充(Pkcs7, ZeroPadding)是否完全匹配?
- 编码问题:加密前的时间戳是否需要先进行Base64编码或其它处理?
- 时间戳精度:网站用的是秒级时间戳(
6. 集成到爬虫与请求发送
逆向的最终目的是为了应用。下面我们以Node.js的axios库为例,展示如何将动态生成的x-apiKey集成到实际的网络请求中。
6.1 安装axios并编写请求函数
npm install axios创建一个request_demo.js文件:
const axios = require('axios'); const CryptoJS = require('crypto-js'); // 复用之前的生成函数 function generateApiKey(timestamp, secret) { const key = CryptoJS.enc.Utf8.parse(secret); const message = CryptoJS.enc.Utf8.parse(timestamp.toString()); const encrypted = CryptoJS.AES.encrypt(message, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); } async function searchCompany(companyName) { const timestamp = Date.now(); const secret = "your_secret_key_here123"; // 从源码扣取的真实密钥 const apiKey = generateApiKey(timestamp, secret); const url = 'https://目标网站域名/api/search/v1'; // 替换为实际API地址 const payload = { keyword: companyName, page: 1, size: 20 }; const headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...', 'Content-Type': 'application/json', 'x-apiKey': apiKey, // 可能还需要其他固定或动态的头部,根据抓包结果补充 // 'Referer': 'https://目标网站域名/', // 'Origin': 'https://目标网站域名/' }; try { const response = await axios.post(url, payload, { headers }); console.log('请求成功!'); console.log('返回数据:', response.data); // 处理数据... return response.data; } catch (error) { console.error('请求失败!'); if (error.response) { // 服务器返回了错误状态码 console.error('状态码:', error.response.status); console.error('响应头:', error.response.headers); console.error('响应数据:', error.response.data); } else if (error.request) { // 请求发出了但没有收到响应 console.error('未收到响应:', error.request); } else { // 请求配置出错 console.error('请求配置错误:', error.message); } throw error; } } // 调用示例 (async () => { try { await searchCompany('示例科技有限公司'); } catch (e) { // 错误处理 } })();6.2 请求头管理的进阶技巧
在实际项目中,管理请求头会更复杂一些,这里分享几个技巧:
- 使用请求拦截器:如果你使用
axios,可以配置一个请求拦截器,自动为每个请求计算并添加x-apiKey,避免在每个请求函数里重复写生成逻辑。axios.interceptors.request.use(config => { const timestamp = Date.now(); const apiKey = generateApiKey(timestamp, SECRET_KEY); config.headers['x-apiKey'] = apiKey; return config; }); - 密钥管理:不要把密钥硬编码在代码里,尤其是上传到公共仓库时。应该使用环境变量或配置文件来管理。
// 使用 dotenv 从 .env 文件读取 require('dotenv').config(); const SECRET_KEY = process.env.API_SECRET_KEY; - 处理时效性:由于
x-apiKey基于时间戳生成,要确保你的服务器时间与目标网站服务器时间大致同步。如果遇到“过期”错误,可以尝试在生成时间戳时减去或加上一个小的偏移量(如几秒钟)进行校准。
7. 常见问题排查与避坑指南
在逆向和复现过程中,你几乎一定会遇到各种问题。下面是一个常见问题速查表,收录了我踩过的坑和解决方案。
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
生成的x-apiKey与浏览器抓包的值不一致。 | 1.时间戳单位/格式错误。 2.密钥错误(大小写、多余空格)。 3.加密算法或模式不匹配。 4.代码依赖环境缺失。 | 1. 核对时间戳:用console.log在浏览器端打印出参与加密的原始时间戳,与你的脚本输出对比。确认是秒还是毫秒,是否需要toString(16)(十六进制)。2. 仔细检查扣取的密钥字符串,确保完全一致。可以在浏览器控制台打印出该变量进行核对。 3. 仔细阅读加密部分的代码,确认算法(AES, DES, RSA)、模式(ECB, CBC)、填充(Pkcs7, ZeroPadding)、输出格式(Base64, Hex)等所有参数。 4. 检查是否漏掉了某个关键的初始化函数或全局变量赋值。 |
请求返回401或403错误。 | 1.x-apiKey格式或值错误。2.缺少其他必要请求头。 3.请求频率过高触发风控。 4.IP地址被限制。 | 1. 确保x-apiKey被正确放置在headers对象中,且键名完全正确(注意大小写和连字符)。用脚本生成的Key去替换浏览器请求中的Key,看浏览器请求是否还能成功,进行交叉验证。2. 检查浏览器成功请求的 Headers,看是否还有Cookie、Referer、Origin、X-Requested-With等其他重要头信息,一并模拟带上。3. 在请求间增加随机延迟(如 sleep(1-3秒)),模拟人类操作。4. 考虑使用代理IP池。 |
扣出来的代码在Node.js中执行报错,提示window/ document is not defined。 | 代码依赖浏览器环境(BOM/DOM)。 | 1.补环境:使用jsdom库来模拟window、document等对象。2.代码改写:分析代码,如果它只是用 window来存储一个全局变量,你可以直接在Node.js中定义一个同名全局变量。3.使用无头浏览器:对于环境依赖极其复杂的,直接用 Puppeteer控制浏览器执行,虽然重但最稳妥。 |
| 加密库(如CryptoJS)版本不兼容,导致加密结果不同。 | 网站使用的库版本与你本地安装的版本内部实现有差异。 | 1. 查看网站加载的crypto-js.js文件的URL,里面通常包含版本号(如crypto-js-4.1.1.min.js)。2. 在你的项目中安装指定版本: npm install crypto-js@4.1.1。3. 如果网站使用的是自定义打包或修改过的版本,最彻底的方法是直接将其源码扣取下来,保存为本地文件,然后在Node.js中用 require(‘./path/to/crypto-js.js’)引入。 |
| 算法看起来是标准的,但结果就是不对。 | 可能存在“盐值混淆”或“多步编码”。 | 1.仔细审计代码:在加密函数o之前,可能对时间戳或密钥进行了预处理,比如反转字符串、与一个固定数组进行异或运算、先进行了一次MD5哈希等。2.使用调试器:在浏览器Sources面板给加密函数打上断点,一步步跟踪每个变量的值,与你本地脚本的中间结果进行比对,找到第一个出现差异的步骤。 |
独家避坑技巧:在扣取代码时,我习惯使用一个“最小可执行单元测试”的方法。不要一次性扣取一大段代码。而是先只扣取最核心的加密函数(比如上面的
o函数),以及它直接依赖的一两个工具函数。然后写一个极简的Node.js脚本,用硬编码的输入去运行这个函数,将输出与浏览器在相同输入下的输出进行对比。如果一致,再逐步扩大扣取范围,添加更多的依赖。这种方法能帮你快速定位问题到底出在哪一层依赖上。
逆向工程就像解谜,耐心和细致是关键。这个“某查查请求头”的案例,完美涵盖了从抓包定位、逻辑分析、环境模拟到集成应用的完整链条。掌握了这套方法,你就拿到了打开JS逆向大门的钥匙,后续面对更复杂的参数加密、WebAssembly(Wasm)或OLLVM混淆时,就有了扎实的基础和清晰的排查思路。记住,多动手、多调试、多思考,每一个报错信息都是通往正确答案的线索。