前几天有个做数据服务的哥们儿问我,说“企名科技”这类企业信息查询平台的前端加密到底怎么搞,为什么别人能批量拿企业工商数据,他用postman模拟接口死活调不通。我听完就有画面了——这不是某个网站的个例,而是现在几乎所有企业信息类站点共同的套路:核心参数全部走JS加密,接口带着动态签名,cookie和header互相校验,直接构造请求基本等于撞墙。这篇文章就用“企名科技”这类平台作为完整案例,把JS逆向的整个链路从头到尾拆一遍,从抓包、定位加密函数、跟栈、补环境,到最后把加密参数还原成可用的Node脚本,全流程落地。适合有一定前端基础、想系统性入门JS逆向,或者正在被某个企业数据站卡住的同学参考。我会把每一步的思路和踩过的坑都写清楚,尤其是那些文档里不会写但实际调试时一定会遇到的细节。
1. 项目背景与整体思路解析
1.1 目标是什么:一个典型的企业信息查询平台
先说明一下我这边遇到的具体目标形态。“企名科技”在这里我当作一类企业信息查询平台的代称来处理,它和市面上的企查查、天眼查、爱企查这类产品的数据模型基本一致:支持按公司名称、法人、经营范围、统一社会信用代码等维度搜索企业,详情页展示工商信息、股东、主要人员、对外投资、变更记录、司法风险、招投标等等多维度的公开数据。对于做风控、市场调研、供应链管理的团队来说,这类数据一旦能量产采集,价值非常大。
这类平台的共同特点是:数据本身来自公开的工商注册信息,理论上谁都能查,但批量采集是被严格限制的。技术上怎么限制?核心就是前端JS加密。你在浏览器里正常搜索,数据能正常展示,但你把同样的请求复制到命令行里跑,接口直接拒绝,提示参数不合法或者签名校验失败。原因就是请求里有一个或多个参数不是固定值,而是由前端JavaScript在运行时动态计算出来的,这个动态参数的生成逻辑,就是我们要通过JS逆向去还原的东西。
1.2 为什么不能直接请求接口
很多刚接触逆向的朋友会先走一条弯路:打开浏览器开发者工具,Network面板里找到XHR请求,看到返回的JSON里整整齐齐的公司列表,心想这不就完事了吗?复制URL、复制请求头、用requests或者axios重放一遍,结果发现返回401或者“invalid request”。为什么?因为请求URL里多了一个sign参数,或者请求头里多了x-token、nonce一类的动态字段,而这些字段在你复制的那个时刻还有效,过了时间窗口或者换了IP就失效了。
更深一层,这类平台还常常做“前置接口校验”:你先要调用一个初始化接口拿到一个种子值或token,然后再用这个值去算签名,最后才带着签名访问数据接口。如果跳过前置接口,就算算出了签名也照样被拒。所以,直接请求接口失败的根本原因不是你在偷懒,而是前端在请求链路里埋了一条完整的“加密链路”,这个链路不还原出来,就没法稳定拿到数据。
1.3 逆向方案选型:扣代码、重放还是浏览器自动化
面对这个加密链路,行业里有几套方案,适用场景完全不同:
第一是纯浏览器自动化,用Playwright或Puppeteer模拟点击、等待渲染、再抓取页面上的内容。这套方案简单粗暴,几乎不碰JS逆向,但缺点也很明显:速度慢、资源消耗大,页面一旦改版就要跟着改选择器,而且现在很多平台会检测webdriver特征,容易被识别后弹出验证码。
第二是直接把前端加密JS“扣”下来,放到Node或Python环境里执行。这是最正统的JS逆向做法。你把包含加密算法的JS文件抠出来,补齐缺失的环境变量,在Node里调用它生成签名,然后用HTTP库直接请求接口。优点是快、稳定、不受页面结构变化影响,缺点是需要理解JS本身,要会用断点调试、跟栈、补环境。
第三是请求重放式的“中间人”思路,辅助或模拟浏览器生成签名后再交给自己的HTTP层请求。这个往往用作过渡方案,先把流程跑通,后续再逐步抠代码。
我的建议是:从长期稳定性和学习价值两个角度看,第二套方案是首选。特别是“企名科技”这类数据接口结构清晰、加密逻辑相对独立的站点,扣代码完全可行。下面整个实操过程都围绕第二套方案来展开。
2. 目标分析与工具准备
2.1 从页面到接口:先弄清楚“逆向什么”
我在拿到一个目标站点时,不会急着打开JS文件逐行读,而是先花十分钟做“接口摸底”。具体操作很简单:打开Chrome开发者工具,切到Network面板,勾选Preserve log,然后去页面搜索一家公司,观察请求列表。
通常是这样的:输入关键词回车后,浏览器会并行发出好几个请求,有的是统计打点接口,有的是联想词接口,真正返回列表数据的是类似/api/company/search这样的接口。点开这个请求,看三个东西:
第一是Query String Parameters,也就是URL上的参数。一般可以看到keyword、page、size,以及一个看起来很“突兀”的字段——比如sign、sig、_t、nonce。这个字段的值是一串看不出规律的字符串或十六进制数字,基本就是我们要逆向的目标。
第二是Request Headers,看看有没有自定义的header字段,比如x-token、x-version、x-sign。这些字段的值往往和页面登录态或请求时生成的签名有关。注意不要只关注参数,很多平台的校验其实藏在header里,参数对了但header不对照样被拦。
第三是Response。如果接口正常返回,Response里的JSON应当整齐干净——这代表服务端把数据直接回传了,没有做二次加密。但有些平台会在响应体里做“数据混淆”或“字体反爬”,那就要再多走一步,后面专门讲。
做完这三步,你手里就有了一张“请求-参数-响应”的对照表,逆向目标就锁定了。不要跳步,这一步做扎实了,后面能省一半时间。
2.2 全局搜索与特征定位
锁定了目标参数名之后,第二步是找到生成这个参数的JS代码。技巧就一个词:特征搜索。在DevTools的Sources面板里,按下Ctrl+Shift+F打开全局搜索,直接搜参数名,比如sign或者"sign"。
搜出来会有很多结果,不要被吓到。先按文件排序,优先看主JS文件(通常是webpack打包生成的app.js、chunk.js这类体积较大的文件),再看业务相关的JS。点进命中行之后,看这个参数名出现在什么位置——是对象字面量的key,还是某个赋值语句的右值?如果是赋值语句的右值,比如sign: e.createSign(params),那恭喜你,已经找到了加密函数的入口。
这里有一个非常实用的小技巧:如果直接搜sign命中的结果太多太杂,就缩小特征范围,去搜它可能的取值特征。比如这个sign值看起来像32位MD5,那就在全局搜索里搜md5、hex_md5、CryptoJS;如果看起来是带有=号的Base64字符串,就搜encrypt、setPublicKey、JSEncrypt。实际调试中,通过“参数名+加密算法特征词”组合搜索,定位效率会快很多。
2.3 工具链:DevTools、Hook脚本与本地覆盖
接下来把工具链备齐。我的日常逆向工装很简单,但很够用。
Chrome DevTools是主战场,最常用的是Sources面板的Call Stack(调用栈)、Scope(变量作用域)和Watch(变量监视)三个子面板。断点命中之后,调用栈告诉你当前函数是被谁调起来的,层级关系一目了然;监视面板可以随时输入变量名查看值,不用翻代码找console.log。
第二个是油猴脚本(Tampermonkey)配Hook注入。很多时候,我们希望让加密函数在运行时“自动暴露”它的输入输出,或者拦截它的结果,这时候就需要Hook。最常见的Hook是重写Object.defineProperty或者改写某些对象原型的方法,比如在加密前把参数打印出来。用油猴脚本往页面注入Hook代码,比每次手动在DevTools里改代码要方便得多,保存一次脚本,后续刷新页面自动生效。
第三个工具是Local Overrides(本地覆盖)。DevTools里可以把线上JS文件的某个片段改写成本地版本并持久化保存。在扣代码调试阶段非常有用:你想知道某个加密函数被调用时传入了哪些参数,直接用本地覆盖在函数体里加上console.log(arguments),刷新页面,调用点信息全部打出来,比用断点一次性看完所有数据更高效。
这套工具链组合下来,基本覆盖了“定位-观察-拦截-改写”四个阶段。接下来进入正题,把加密逻辑一层层剥开。
3. 核心加密逻辑拆解
3.1 第一层:参数签名sign
以我复现的一个版本为例(具体站点信息已做脱敏处理):搜索接口/api/company/search的请求URL长这样:
/api/company/search?keyword=张三&page=1&size=10&sign=88f8c...&_t=1718192000000其中_t是毫秒级时间戳,sign是32位十六进制字符串。第一次看到这种结构,直觉就是MD5签名。怎么确认?复制sign的值去搜,或者直接在加密函数附近找CryptoJS.MD5的调用。定位到类似代码:
function createSign(params) { var timestamp = new Date().getTime(); var raw = params.keyword + "&" + params.page + "&" + params.size + "&" + timestamp + "&" + secretKey; return CryptoJS.MD5(raw).toString(); }这种属于最基础的签名方式,它把业务参数加上时间戳和一把写在前端代码里的固定密钥secretKey,拼成一个字符串,再做MD5。服务端收到请求后,用同样的逻辑算一遍,比对结果是否一致。如果一致,再检查时间戳和当前时间的差是否在容差范围(一般是几十秒)内。这里要特别留心拼接顺序和是否有大小写转换。很多新手栽在“拼出来的结果跟浏览器里不一样”这个问题上,原因往往是原始字符串里某个参数名的顺序或分隔符不对。
怎么验证你找到的逻辑是对的?把浏览器里生成的一次真实请求的参数拿下来,用同样的密钥和拼接规则,在你自己的脚本里跑一遍MD5,看结果和浏览器里的sign是否一致。如果一致,说明这段逻辑已经被你吃透了。
3.2 第二层:动态Token与Cookie校验
签名搞定了,但你以为直接照着这个逻辑写脚本就能无限调接口,那就太天真了。实际请求中还有一层很隐蔽的校验:请求头里有一个x-token字段,它的值看起来像一段JWT或者随机字符串,而且每次刷新页面之后会变。这个token是怎么来的?
跟踪x-token的生成位置,用2.2节的全局搜索大法,搜x-token或者token。通常会找到一个初始化接口,比如/api/init或者/api/config,页面加载时会先调用它,服务端返回一个临时token和一组配置参数,前端再把这个token放进后续所有请求的header里。服务端拿到token后,会校验它是否在有效期内、是否是来自同一会话,如果发现token对应的会话没有在浏览器里正常初始化,就会判定为伪造请求。
处理方法有两个方向。第一是模拟初始化流程:先用HTTP库请求/api/init拿到token,再带着token去访问数据接口。第二是把初始化逻辑也逆向出来,在本地直接生成token。大多数情况下,初始化接口本身没有太强的加密,直接用第一种方式就能跑通。但要注意,初始化接口通常也埋了简单的签名或风控参数,本质上就是常说的“动态token”机制。遇到困难不要硬刚,先看init请求里多出来的参数是从哪来的,再决定是否需要再逆向一层。
这里我分享一个经验:先把浏览器里的完整请求链路拍下来,记录下来每个请求的发生顺序、依赖关系、cookies的变化,然后“照着顺序模拟”,比逐个逆向每个字段要快得多。有的token是服务端下发的,你本地怎么模拟都生成不出来,模拟整个流程拿token反而是最优解。
3.3 第三层:JS代码混淆与动态执行
如果只是MD5签名加token,这类站点的逆向难度就太低了。真实情况下,企名科技这类平台还会给JS代码做一层混淆,让你不容易直接读出加密逻辑。
常见手法有这么几种:
第一种是字符串数组混淆。所有的字符串(比如密钥、参数名)被打散存储在一个大数组里,通过下标引用,每次加载时数组顺序还可能被随机打乱。比如代码里写atob("aGFoYQ==")而不是直接写明文,或者写成_0x3f2a["0x1f"]这种下标取值。读起来非常痛苦。
第二种是控制流平坦化。把原本清晰的if-else逻辑改写成while循环加switch-case的结构,每次循环根据一个状态变量决定执行哪一段逻辑。代码从“顺序读”变成“跳着读”,一眼看不出程序流程。
第三种是动态执行,即把关键加密代码转成字符串,再用eval或new Function执行。你在JS文件里看到的只是一堆字符串拼接,真正执行时才拼出可运行的代码。
遇到混淆后的代码,我的经验是一定不要用肉眼慢慢读,而是先在浏览器里让它运行起来,再在关键位置下断点,直接看运行时的真实变量值。或者用AST工具,比如Babel,把混淆后的代码解析成AST,再写规则做变量名还原和控制流还原。对于“企名科技”这种级别的站点,大部分加密逻辑本质还是在浏览器里“活着”的,让它现出原形最直接的手段就是运行调试,而不是静态分析。
4. 完整实操过程:从断点到可复现的脚本
4.1 断点定位:在加密函数处打断点
拿一个具体场景走一遍全流程。假设我已经通过全局搜索锁定了这样一个片段(这是经过简化和脱敏的示意结构):
getSign: function(t) { var e = t.params , r = (0, n.a)(JSON.stringify(e)); return r }这里的n.a是某个导入的方法,JSON.stringify提示我们它可能是对参数整体做了某种哈希或加密。在r = ...这一行打一个断点,然后回到页面重新触发一次搜索请求,断点命中后:
看Scope面板,t.params是一个对象,里面有keyword、page、size,还有一个secretKey字段——这个字段在最终请求URL里没出现,但它参与了加密计算,这就是“隐藏参数”。再看r的值,是32位的字符串,和请求URL里的sign一致。好,加密入口找到了。
接下来单步执行(F11),进入n.a内部。这里面可能又是一层函数,比如先对对象key做固定排序,再用JSON.stringify转成字符串,最后拼上一个固定盐值做MD5。这就回到了3.1节描述的场景。
断点调试的威力在于:你可以亲眼看到每一步的输入和输出,不需要去猜中间变量。我在实操中甚至遇到过加密函数内部有多层嵌套,每层都混淆了变量名,但通过Watch面板盯住几个关键变量,硬是把整个调用链摸了出来。
4.2 环境补全:让扣下来的代码跑起来
加密逻辑找到了,接下来就是把它搬出浏览器。这一步最常见的问题是:你把JS代码放进Node里,一运行就报错——window is not defined、document is not defined、navigator is not defined。
为什么会这样?因为前端代码运行在浏览器环境里,它的加密函数可能依赖浏览器的全局对象来获取随机数、当前时间、浏览器指纹等。而这些对象在Node原生环境里不存在。这时候就要“补环境”。
补环境有两种思路:
第一种是轻量级的“手动补齐”。在Node脚本里定义缺失的全局对象,比如:
global.window = global; global.navigator = { userAgent: 'Mozilla/5.0 ...' }; global.document = { cookie: '', createElement: function(){ return {}; } };把缺失的变量补到不报错为止。然后再调用扣出来的加密函数。这个方式适合依赖全局对象较少的场景,是“企名科技”这类站点的首选。
第二种是使用现成的DOM模拟库,比如jsdom。它能相对完整地模拟出一个“最小浏览器环境”,对依赖DOM操作的代码兼容性更好。但缺点是重量级、性能差,某些Node版本下还有兼容问题。我的建议是:先手动补齐,如果错误太多再上jsdom,不要一上来就引入重工具。
另外还有一个偏方:如果加密函数内部引用了很多跨模块的代码,你不想费力去一个个扣依赖,可以试试用vm2这类沙箱模块创建一个隔离的JS执行环境,把整个压缩的JS文件扔进去跑,再把加密函数暴露出来。这种方式能快速验证扣下来的代码是否可行,缺点是环境模拟得再好也和真实浏览器有差异,一旦遇到拿系统时间戳比对、检查performance.now()之类细节,还是会翻车。
4.3 请求流程重构与验证
代码在Node里能跑通,拿到了正确的sign,这时就要把完整请求流程串起来。我习惯写成这样一个Node脚本骨架:
const axios = require('axios'); const { getSign, getToken } = require('./encrypt'); // 扣下来的加密模块 async function searchCompany(keyword, page = 1) { // 1. 初始化,拿token const initRes = await axios.get('https://api.example.com/api/init', { headers: { 'User-Agent': 'Mozilla/5.0 ...' } }); const token = initRes.data.data.token; // 2. 构造业务参数并生成签名 const params = { keyword, page, size: 10, _t: Date.now() }; const sign = getSign(params); // 3. 请求数据接口 const headers = { 'x-token': token, 'User-Agent': 'Mozilla/5.0 ...', }; const url = 'https://api.example.com/api/company/search'; const res = await axios.get(url, { params, headers }); if (res.data.code === 0) { return res.data.data.list; } throw new Error(res.data.message); }写完脚本之后,一定要做一次“比对验证”:同一关键词,在浏览器里搜索的结果和脚本请求的结果,分页数是否一致,字段是否相同。我习惯把两边的原始JSON各存一份,用脚本做字段级diff,确认没有缺字段、乱码或翻页错位的问题。只有两边数据一致,这套逆向才算走通了。
这里还有一个细节:请求头里的字段顺序和大小写,某些平台的服务端在取header值时会做校验,顺序不重要,但值的前后空格会导致签名不一致。所以脚本里写headers时,建议直接从浏览器Network面板复制原始请求头,再去掉干扰字段,而不是凭感觉手敲。
5. 常见问题与排查技巧实录
5.1 加密参数顺序和大小写问题
我在调试过程中遇到最多的坑,就是拼接字符串时的大小写和顺序。比如MD5结果是32位小写十六进制,但某天接口忽然变了,服务端要求大写签名,你如果没有重新从浏览器里抓一次请求对比,就会一直得到“参数无效”的提示。
排查方法很简单:把浏览器里真实请求的sign值和你自己算出来的值放在一起,逐字符对比。如果完全一致,说明算法无误;如果不一致,就拿着两边用来拼接的原始字符串做逐字符diff。这里我推荐在加密函数里临时打一个console.log,把拼出来的原始字符串原样打印出来,再和你在Node里拼接的字符串对比,差异一目了然。很多看似神秘的报错,都是因为某个分隔符从&变成了|,或者漏了一个参数。
5.2 补环境时各种“is not defined”
“XX is not defined”是扣代码阶段最常见的报错。每次遇到这种报错,第一反应不是去定义这个变量,而是想清楚它为什么会存在。比如加密代码里出现location.href,它可能只是在取当前页面URL来参与拼接。如果你在Node里补一个假的location,值跟浏览器不一样,算出来的签名也会不一样。这种情况下,补环境反而引入了新问题。
正确做法是:看目标变量是否真的参与了核心加密计算。如果它只是被引用但结果不影响最终签名,就直接补一个空值或字符串占位;如果它参与了计算,就要从浏览器真实环境里取这个值,或者在脚本里另行构造。比如navigator.userAgent这种变量,很多时候只是参与请求头拼接,你只要在请求头里保持一致就行,不需要在环境里拼得一模一样。
5.3 请求频率和风控策略
逆向跑通了接口,不代表万事大吉。企业数据类平台对请求频率的检测非常敏感,尤其是对固定IP的高频访问,很容易触发滑块验证码或IP封禁。我在压测阶段遇到过几分钟内连续请求几十次后,接口返回“操作频繁”的提示,并且在页面里要求完成一次滑块验证。
处理思路有两个层面。第一是控制节奏:单个IP的并发数控制在个位数,请求间隔加随机延时,比如2到5秒之间随机,不要用固定间隔。第二是打散流量:轮换多个出口IP、轮换User-Agent列表,尽量模拟真实用户行为。但这里要特别提醒一句,逆向技术本身是中性工具,使用时要严格遵守目标网站的条款与法律法规,控制频率、尊重平台规则,不要用于商业侵权或破坏性采集。
5.4 字体反爬与数据混淆
部分企业信息平台会在详情页做“字体反爬”。页面文字看起来正常,但复制出来全是乱码或生僻字。原理是页面用自定义字体文件,把真实的字符编码映射成了另一个字形,服务端返回的文本是“错的”,而浏览器用自定义字体渲染成“对的”。
如果遇到这种情况,需要在逆向链路里增加一步:下载字体文件,解析字体映射表。用Python可以做,用Node也能做。思路是拿字体文件里的glyph和Unicode码做对照,建立“页面字符 -> 真实字符”的映射字典,再把爬取文本通过这个字典还原成可读文字。判断是否遇到字体反爬的最快方式是看接口返回的JSON里,公司名称这一栏是否包含&#x开头的HTML实体或看起来完全无意义的生僻字。
5.5 常见问题速查表
我把这段时间踩过的坑整理成一张速查表,方便大家对照排查:
| 现象 | 可能原因 | 排查与解决办法 |
|---|---|---|
| sign校验失败 | 拼接顺序或大小写错误 | 打印两边的原始拼接串逐字符diff |
| 请求返回401 | token过期或未初始化 | 先请求init接口刷新token |
| Node里报window未定义 | 环境缺失 | 手动补齐global.window等基础对象 |
| 计算出的签名和浏览器不一致 | 环境变量参与了加密 | 检查navigator、location等变量是否被使用 |
| 请求频率高被拦截 | 触发风控 | 降低并发、加随机延时、轮换UA |
| 页面文字复制乱码 | 字体反爬 | 解析自定义字体映射表 |
| JS文件完全不能阅读 | 代码混淆 | 用AST工具还原,或运行时断点分析 |
| 接口返回数据为空 | 参数缺隐藏字段 | 对比浏览器完整参数列表,找隐藏拼接字段 |
6. 个人经验与延伸思考
把一个企业信息查询平台从“接口被加密”到“脚本稳定跑数”,这整个过程中最让我感慨的不是某个加密算法有多难,而是调试思路的选择。很多人一上来就抱着混淆后的JS逐行静态分析,结果看了三天也看不出所以然。正确的姿势是“让它先跑起来”:借助断点、Hook和本地覆盖,在真实浏览器环境里观察它的运行轨迹,等逻辑清楚之后,再决定扣哪些代码、补哪些环境。
另外想提醒一点:逆向工作一定要把合规放在前面。我每次做这类项目,都会先看目标网站的Robots协议和用户协议,只做学习研究和公开数据的正常获取,绝不碰涉及个人信息、账号权限绕过或破坏服务的行为。技术在公开领域是用来提高效率的,不是用来钻空子的。
如果你对“企名科技”这类站点感兴趣,复现完上面这套流程之后,还可以再往前推一步:试着把加密函数模块化,封装成通用的签名服务;或者研究一下它对webpack模块的依赖关系,看能不能把整个加载器逻辑识别的自动化程度做得更高。这个领域的知识点很多,但万变不离其宗——理解JS在浏览器里的真实运行方式,就掌握了钥匙。希望这篇文章能给你在逆向路上省下几个通宵。