JS逆向入门:从猿人学第一题掌握签名参数破解全流程
2026/9/19 0:45:37 网站建设 项目流程

“JS逆向”这四个字,在刚刚接触爬虫技术的人眼里,常常带着一种奇特的吸引力。我见过太多人一上来就囤一堆资料,看各种混淆、AST、补环境的教程文章,结果看十个小时理论都不如亲手把猿人学第一题完整做一遍。这个题在圈内基本上被当成入门必修课来打,公开题题库把它排在第一道位置是有原因的:题目难度不高、签名逻辑清晰、整体解题链路完整,刚好能把“抓包—定位—断点—还原—脚本化”这条JS逆向主流程全部走通一遍。

它在业务侧模拟的是非常常见的一类场景:后端接口要求请求里带一个签名参数,这个参数由前端JS动态生成。你用普通网络请求库直接请求拿不到数据,必须先把生成签名的逻辑逆向出来,再在 Python 或 Node 脚本里复现。对新手来说,这是第一次把浏览器调试、JavaScript 语法阅读、Python 脚本编写三样东西串起来的完整实践,做明白这一题,后面再遇到签名、加密参数时至少心里有底,知道该从哪里下手。

1. 猿人学第一题的核心考点:签名参数与JS逆向入门链路

1.1 这一题到底在模拟什么场景

做爬虫时间长了你会发现,只要目标站点稍微有点防御意识,接口请求就不会那么“裸奔”。常见手段有几种:限制请求频率、校验 Cookie、校验 User-Agent、给请求参数加签名。猿人学第一题模拟的就是最后一种。

我在实际项目中遇到过的签名校验,基本流程是这样的:

  • 前端 JS 在页面加载时或请求发出前,根据当前时间戳、固定字符串、页面里的一些变量拼出一个原始字符串;
  • 对原始字符串做某种摘要算法或加密算法,得到一个签名参数;
  • 签名参数随请求一起发给后端,后端用同样的算法重新算一遍,比对结果;
  • 如果签名不对、内容为空、算法不一致,后端直接返回错误或空数据。

猿人学第一题里的形态也差不多:请求某个分页数据时,地址里除了页码这类明面参数,还带着一个动态计算出来的签名值。第一页这个签名看起来是一个固定格式的字符串,但你把它原样带到第二页,就会失效。它和时间戳有关,每翻一页、每次请求,签名都会变。

这种设计在真实业务里太常见了,很多 App 的接口、网页端的 Web API,都会有类似的签名参数。所以做这一题练到的东西,本质上可以直接迁移到真实项目里。

1.2 做这道题需要的能力清单

有些新手看题解会觉得“这也不难啊”,但轮到自己上手就卡住,原因往往是基本功没到位。我把做猿人学第一题需要的东西梳理一下,你可以先自查:

  • Chrome 开发者工具的基本使用:Network 面板看请求、Sources 面板看代码、Console 跑表达式,这个是基础中的基础;
  • 断点调试能力:在 XHR 请求发出前打一个断点,让浏览器暂停在发送前的那一帧,然后顺着调用栈往上翻;
  • JavaScript 基础阅读能力:至少能看懂函数声明、字符串拼接、函数调用这三件事;
  • Python 的 requests 和 execjs 基础:requests 负责发请求,execjs 负责把抠出来的 JS 拿到本地执行;
  • 一点耐心和细心:拼接顺序差一个字符、编码方式差一个字节,签名结果都会变,这个在工作里也同样是排查重点。

这五项都不是什么高深理论,全是实操技能。所以我把这一题定义为“JS逆向入门链路”的完整演练——你第一次独立把浏览器里的一段逻辑变成脚本里的可用能力,成就感是看多少冷冰冰的代码都换不来的。

2. 从抓包到定位:三步锁定加密函数

2.1 先看请求,确定哪个参数是动态的

拿到任何一道逆向题,第一步不是急着去翻 JS,而是先把请求看住。我见过很多新手一上来就全局搜“sign”三个字母,在压缩得密密麻麻的 JS 文件里一通乱找,浪费大量时间。正确的顺序是:先到 Network 面板把 XHR 请求过滤出来,刷新页面,找到接口的请求信息。

打开浏览器开发者工具以后,切到 Network,刷新一下页面,你会看到页面发出若干个请求。把过滤条件切成 XHR,这样请求会少很多;然后逐个点开看 Payload 或者 Query String Parameters。第一题的接口请求通常会带这样的参数形式:

  • page:页码,比如 1、2、3;
  • sign:一串看起来像摘要值的字符串,每次请求都不一样。

如果你点击下一页再翻一次,会看到 page 变了,sign 也变了。这就说明 sign 是和请求绑定在一起的动态逻辑,不是写死的常量。签到这一步,目标就已经非常明确了:找到这个 sign 的生成函数。

一个小技巧:你可以先把请求参数复制下来,用 Python 原样请求一次,看看服务端返回什么。如果返回参数错误、签名错误之类的提示,说明服务端确实在校验 sign。这一下就能确认,这个接口必须带着合法签名才能拿到数据。

2.2 用 XHR 断点卡住请求发出的瞬间

定位加密函数,最常用的手段是 XHR 断点,也叫 XHR/fetch Breakpoint。它的原理很简单:在某个 XHR 请求即将发出之前,浏览器会强制暂停 JavaScript 的执行,让你看到当前是哪一行代码触发了请求,以及当前作用域里所有的变量值。

操作路径是这样的:

  • 打开开发者工具,切换到 Sources 面板;
  • 在右侧找到 XHR/fetch Breakpoints,点最右边的加号;
  • 输入请求 URL 里的关键片段,比如接口路径里出现的字符串,推荐填一部分稳定的内容,不要包含会变化的参数;
  • 添加完成后,回到页面触发一次翻页操作。

页面刷新或者翻页的时候,浏览器就会在发送请求的那一行代码处停下来。这时候 Web 开发人员常说的“断点生效”就到了。你会在 Sources 面板的 Call Stack 区域看到一串调用栈。

这里需要理解调用栈的含义:它记录的是一个函数调用另一个函数、一层套一层的完整链条。最底层通常是浏览器原生的 open/send 方法,越往上越接近你的业务代码。我们的任务就是顺着这个栈往上找,找到真正计算 sign 的那一层。

2.3 顺着 Call Stack 找到真正算参数的地方

停在断点后,不要急着点“继续执行”,先冷静做三件事:

第一,看右侧的 Scope 面板。这里会列出当前作用域里的局部变量、闭包变量和全局变量。如果某个变量叫 sign、_sign、m、token 之类的,八成就是我们要找的东西。

第二,看 Call Stack 面板。从上往下数,找哪一层的函数里有明显的字符串拼接、加密函数调用、或时间戳计算逻辑。你可以一层一层点进去,观察这层函数的参数和返回值。

第三,在 Console 面板手动验证。如果你猜测某个函数在生成 sign,可以直接在 Console 里调用一下它,传一个测试参数,看输出值是否像签名。这种验证方式比反复刷新页面快得多。

以第一题常见的形态为例,定位到的核心函数逻辑通常类似这样:

function getSign(timestamp) { var raw = timestamp + "yuanrenxue" + "2024"; return md5(raw); }

当然这只是我为了讲解写的一个“典型化示例”,具体拼接内容要以你实际调试时看到的内存值为准。你需要关注的是这一类函数的结构:输入时间戳、拼接固定字符串、调用摘要函数、返回结果。看清楚这个过程,整个题的核心就算拿下了。

3. 把加密逻辑从浏览器搬到脚本里

3.1 抠代码:从页面里取出核心加密函数

加密函数定位到了,接下来要把它从页面环境里抠出来。这一步在界内叫“抠代码”,话题竞争起来也很讲究。

最笨也最稳的方法:在断点暂停到核心加密函数那一层时,当前函数的整个实现已经加载在内存里了。你可以在 Console 里对目标函数名调用 toString(),直接把函数源码打印出来,再手动复制下来保存到本地。比如:

getSign.toString()

这个操作会把函数的完整源码以字符串形式打印出来,比在压缩的源码里手工找快得多。

保存 JS 文件的时候要注意:如果 getSign 内部还调用了 md5 这类外部函数,你需要把 md5 的实现也一并抠下来。第一题的 md5 通常不会引用太多外部依赖,最多是两个函数相互调用,把它们放在同一个 JS 文件里就行。抠完以后保存为 sign.js,后面 Node 或 Python 要用。

如果你发现加密函数里引用了 window、document 这些浏览器环境对象,说明它依赖浏览器 API。这时候有两种处理方式:一种是尽量把纯计算的函数(比如只做字符串拼接和摘要计算)提取出来,绕开 DOM 依赖;另一种是后续在 Node 里补一个简单的 window 对象,但第一题一般用不到补环境这么高端的操作,遇到 window 相关报错时先怀疑是不是抠多了。

3.2 本地验证:用 Node 把 JS 先跑起来

代码抠出来先别急着连 Python,先用 Node 验证一遍。打开终端,写好入口调试代码:

// test.js const fs = require("fs"); const vm = require("vm"); const code = fs.readFileSync("./sign.js", "utf-8"); const context = {}; vm.createContext(context); vm.runInContext(code, context); const timestamp = "1710000000"; const sign = context.getSign(timestamp); console.log(sign);

为什么在这里用 Node 的 vm 模块而不是直接 require?因为抠出来的 JS 文件通常不是规范的 Node 模块,里面没有 module.exports,直接用 require 拿不到函数。vm.runInContext 可以把代码丢进一个沙箱环境里执行,然后通过 context 获取函数引用。

跑一次,记下输出结果。然后回到浏览器,在 Console 里用同样一个时间戳调用 getSign,对比一下两个结果是否一致。一致,说明抠下来的代码环境没问题;不一致,多半是拼接顺序、时间戳数据或者字符编码出了问题。

这一步验证非常关键,因为如果 JS 这边就没抠对,后面接 Python 再调,问题定位就麻烦了。我习惯把“先验证 JS 再接入上层”作为一条铁律,任何逆向之后写脚本,都先保证单点函数输出和网页一致。

3.3 两种方案收尾:execjs 调用 JS,还是用 Python 重写

JS 函数验证通过以后,就进入了工程化阶段。这里你会有两条路,选哪条取决于你自己的场景:

方案 A:Python 里用 execjs 直接调用 JS 文件。

这是最省事的方案,特别适合算法复杂、不想手动重写的情况。安装依赖:

pip install PyExecJS

然后写一个简单的调用:

import execjs import requests with open("sign.js", "r", encoding="utf-8") as f: js_code = f.read() ctx = execjs.compile(js_code) timestamp = "1710000000" sign = ctx.call("getSign", timestamp) resp = requests.get( "https://目标接口路径", params={"page": 1, "sign": sign}, headers={"User-Agent": "Mozilla/5.0"}, ) print(resp.json())

方案 B:直接把 JS 逻辑用 Python 重写。

以第一题常见的 MD5 摘要逻辑为例,Python 标准库就能搞定:

import hashlib import requests import time timestamp = str(int(time.time())) raw = timestamp + "yuanrenxue" + "2024" # 以实际调试结果为准 sign = hashlib.md5(raw.encode()).hexdigest() resp = requests.get( "https://目标接口路径", params={"page": 1, "sign": sign}, headers={"User-Agent": "Mozilla/5.0"}, ) print(resp.json())

两条路怎么选?我给一个参考思路:

场景推荐方案原因
加密算法复杂,比如 AES、RSA,代码量大execjs 调用 JS重写成本高,容易出错
算法简单,比如 MD5、SHA 系列Python 重写性能更好,不依赖 Node 环境,部署简单
本地已经装了 Node,追求稳定execjs避免重写过程引入偏差
目标是放到服务器上长期跑Python 重写减少外部依赖,方便容器化

第一题里无论你选哪条路都能走通。我的建议是:你都试一遍。先 execjs 调通,再重写一遍,这个过程会让你彻底理解“JS 逆向之后的两条落地路径”,后面遇到真实需求时就能根据情况迅速选型。

4. 新手容易踩的坑与排查实录

4.1 常见报错速查表

新手第一次完整跑通第一题,大概率会经历几个报错。我把高频问题整理成一个速查表,方便你对照排查:

报错或现象可能原因排查方式
Cannot read properties of undefinedJS 里某个依赖对象没拿到,常见于补的代码不全看报错行号,找到是哪个变量为 undefined,把它依赖的函数一起抠出来
window is not defined抠出的代码引用了浏览器全局对象提取纯计算函数,绕开 DOM API;或者简单声明一个 globalThis.window = {} 兜底
execjs 返回空或编码报错中文或特殊字符在 Node 与 Python 之间编码不一致JS 和 Python 统一用 UTF-8,检查 raw 字符串里的中文是否被编码转义
签名和服务端返回的校验不一致拼接字符串顺序、时间戳数值、固定盐值有问题用同一个时间戳分别跑网页和本地脚本,逐段对比拼接过程
请求返回 403 / bad request服务端还校验了请求头或 Cookie,不止签名带上浏览器的完整请求头,重点看 User-Agent、Referer、Cookie
第一页正常,第二页失败时间戳不是当前时间,是页面加载时固定的值抓包看清楚当前页面 actual sign 对应的时间戳,别盲目取 int(time.time())

我遇到过最坑的一种情况:签名算法里拼接的固定字符串是中文,浏览器端拿到的时候是 UTF-8 编码的中文,但我在 Python 重写时默认用了 GBK 编码,结果签名怎么都对不上。最后把原字符串打印出来才发现是编码问题。所以遇到签名不一致,我建议先把拼接的原始字符串完整打印出来,对比一下浏览器里实际拼接的字符串,逐字符比对,问题立刻就能发现。

4.2 三个只有实际动手才知道的细节

有些东西不实际操作很难意识到,我拿出来单独说说,都是踩过之后才能真正明白的经验:

细节一:签名用的时间戳不一定是“当前”时间戳。第一次写脚本时,我很自然地用了 int(time.time()),结果怎么请求都报签名错误。后来反复看页面才发现,页面加载时 JS 就已经计算好了一个时间戳变量,后续几个请求都复用它。你在调试时要注意看真正参与签名计算的变量值,哪怕接口地址里可能也有时间戳,也别想当然认为它就是当前时间。

细节二:Call Stack 不是越深越好,也不是越浅越好。新手容易在调用栈里看到哪层是原生方法,就往下找,结果越找越深;或者看到最顶层有个匿名函数,就直接埋头去读。真正合理的做法是在每一层点开看它的局部变量:当你在某一层的局部变量里看到了即将参与计算的时间戳、盐值、结果值的雏形,那这一层就准是逻辑中心。业务函数里一定带了业务的气息,只传参数的中间层反而干净。

细节三:Console 里的 toString() 是抠代码的利器。很多压缩后的混淆 JS 很难直接阅读,但如果你在准确的作用域里拿到了函数引用,一行 toString() 就直接展开还原了函数的真实逻辑。利用这一点,比在臃肿的压缩代码里用格式化工具找人名快得多。具体操作是:断点停在关键函数里时,在 Console 输入这个函数的名字,比如 signFn,然后执行 signFn.toString(),浏览器就会完整打印源码。

4.3 调试时的运行环境准备

如果你是用 Windows 本机来操作,需要先确保 Node.js 环境可用。因为 execjs 的底层原理是把 JS 丢给系统里的 JS 引擎执行,在 Windows 上默认用 Node 作为运行引擎,所以必须先装好 Node.js,Python 端才能正常调用。

检查方式很简单:

node -v

终端能输出版本号,说明环境可用了。有些新手只装了 Python 依赖就调用 execjs,结果直接报找不到 JS 引擎,这一类安装问题虽然不难,但确实拦住了不少第一次接触 execjs 的人。

5. 做完第一题之后,怎么继续往前走

猿人学第一题只是 JS 逆向的起点。做明白这一题之后,你的能力其实已经跨越了一个重要门槛:以前你只会“用 requests 请求明文接口”,现在你已经具备“从浏览器里逆向出某个参数生成逻辑,并把它复现到脚本里”的完整能力。这个能力在真实的工作场景里非常常用,无论是数据采集、接口分析、还是安全测试,都会用得着。

顺着这个方向往上走,我建议按这个路径去进阶:

  • 先吃透浏览器的调试技巧。把 XHR 断点、DOM 断点、事件监听器断点都玩明白,这些是定位逻辑的基本功。
  • 再学常见加密算法。第一题里如果只是摘要算法,相对好办;后面的题目会出现 AES、RSA、Base64、字符混淆、自定义算法等,你需要了解每个算法的特征和判断方法。
  • 然后是混淆对抗。很多网站会用 obfuscator 工具把 JS 变量名改成无意义的 a、b、c,甚至会做控制流平坦化。这时候你可能会接触到 AST 抽象语法树、反混淆脚本、还原工具,游戏规则就升级了。
  • 最后是补环境。当网站检测到你的脚本环境跟真实浏览器差异太大,甚至可以抛出很隐蔽的异常来干扰逻辑。这种情况下你要补 window、document、navigator 等环境对象,甚至用浏览器指纹相关知识去规避检测。

但这些都是后话。眼下最重要的是把第一题完整地、独立地拿下——不看题解、不查资料,自己从打开开发者工具开始,一步步把流程走通。如果你能把这道题做三遍,每一遍都让自己独立完成,后面遇到的很多逆向题都会有“似曾相识”的感觉。

另外多说一句:逆向技术本身是中性的,练手时尽量选公开的、允许测试的题库,不要对线上生产站点做超出授权范围的请求调试,这不是胆小,是专业性的体现。技术能力越强,越要懂得边界,这也是我在这个行业里一贯坚持的做法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询