
“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、3sign一串看起来像摘要值的字符串每次请求都不一样。如果你点击下一页再翻一次会看到 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 函数验证通过以后就进入了工程化阶段。这里你会有两条路选哪条取决于你自己的场景方案 APython 里用 execjs 直接调用 JS 文件。这是最省事的方案特别适合算法复杂、不想手动重写的情况。安装依赖pip install PyExecJS然后写一个简单的调用import execjs import requests with open(sign.js, r, encodingutf-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.jsPython 端才能正常调用。检查方式很简单node -v终端能输出版本号说明环境可用了。有些新手只装了 Python 依赖就调用 execjs结果直接报找不到 JS 引擎这一类安装问题虽然不难但确实拦住了不少第一次接触 execjs 的人。5. 做完第一题之后怎么继续往前走猿人学第一题只是 JS 逆向的起点。做明白这一题之后你的能力其实已经跨越了一个重要门槛以前你只会“用 requests 请求明文接口”现在你已经具备“从浏览器里逆向出某个参数生成逻辑并把它复现到脚本里”的完整能力。这个能力在真实的工作场景里非常常用无论是数据采集、接口分析、还是安全测试都会用得着。顺着这个方向往上走我建议按这个路径去进阶先吃透浏览器的调试技巧。把 XHR 断点、DOM 断点、事件监听器断点都玩明白这些是定位逻辑的基本功。再学常见加密算法。第一题里如果只是摘要算法相对好办后面的题目会出现 AES、RSA、Base64、字符混淆、自定义算法等你需要了解每个算法的特征和判断方法。然后是混淆对抗。很多网站会用 obfuscator 工具把 JS 变量名改成无意义的 a、b、c甚至会做控制流平坦化。这时候你可能会接触到 AST 抽象语法树、反混淆脚本、还原工具游戏规则就升级了。最后是补环境。当网站检测到你的脚本环境跟真实浏览器差异太大甚至可以抛出很隐蔽的异常来干扰逻辑。这种情况下你要补 window、document、navigator 等环境对象甚至用浏览器指纹相关知识去规避检测。但这些都是后话。眼下最重要的是把第一题完整地、独立地拿下——不看题解、不查资料自己从打开开发者工具开始一步步把流程走通。如果你能把这道题做三遍每一遍都让自己独立完成后面遇到的很多逆向题都会有“似曾相识”的感觉。另外多说一句逆向技术本身是中性的练手时尽量选公开的、允许测试的题库不要对线上生产站点做超出授权范围的请求调试这不是胆小是专业性的体现。技术能力越强越要懂得边界这也是我在这个行业里一贯坚持的做法。