JS逆向入门实战:从定位加密函数到Python复现签名参数
2026/6/24 19:41:31 网站建设 项目流程

1. 项目概述与目标拆解

最近在带新人入门JS逆向,发现很多朋友卡在了“知道要扣代码,但不知道从哪下手”的阶段。正好,图灵爬虫练习平台的第三题是一个典型的、用于教学练手的JS逆向案例,它模拟了真实环境中一种常见的参数加密场景。这个项目标题“【js逆向入门】图灵爬虫练习平台第三题”本身,就指向了一个非常明确的学习路径:通过一个结构清晰、难度适中的靶场,来掌握JS逆向的核心分析思路和工具链使用。

这个平台第三题的核心,通常是模拟一个请求,其关键参数(比如一个叫token或者sign的字段)是由前端JavaScript经过一系列计算生成的。我们的目标不是简单地拿到数据,而是理解这个生成逻辑,并用Python(或其他语言)复现出来,从而让我们的爬虫程序能够自主构造出合法的请求参数,实现自动化数据抓取。这对于想从基础爬虫转向处理动态渲染、反爬机制更复杂网站的朋友来说,是必须跨过的一道坎。它适合已经了解HTTP协议、会用Python的requests库发起简单请求,但对浏览器开发者工具“Sources”和“Debugger”面板还比较陌生的初学者。

2. 逆向环境准备与工具链解析

工欲善其事,必先利其器。在开始逆向之前,搭建一个顺手的分析环境至关重要。很多人一上来就对着混淆的代码硬看,效率极低且容易放弃。我的习惯是准备一套组合拳工具。

2.1 浏览器开发者工具深度使用

Chrome或Edge的开发者工具是核心。关键不在于打开它,而在于怎么用。

  • Network面板:这是起点。清空记录,点击页面上的触发按钮(如“查询”、“提交”),观察产生的XHR/Fetch请求。重点关注请求头(Headers)和负载(Payload)。在这个练习中,你一定会发现一个携带了加密参数的请求。把该请求的URL、方法、以及所有参数(特别是那个看起来乱码的加密参数)完整记录下来。右键请求,选择“Copy as cURL”是个好习惯,方便后续在Python里转换或重放测试。
  • Sources面板:这是主战场。不要被众多的JS文件吓到。通常有两种策略找入口:
    1. 事件监听器断点:在Sources面板右侧的“Event Listener Breakpoints”里,勾选“Mouse”下的“click”事件。然后去点击页面上触发加密请求的按钮,代码会自动在对应的点击事件处理函数处断下。这是最直观的找入口方式。
    2. 搜索关键参数名:在Sources面板按Ctrl+Shift+F进行全局搜索。搜索你在Network里看到的那个加密参数的名字,比如tokensignencryptData等。这能快速定位到生成或设置该参数的代码片段。

2.2 辅助调试与反混淆工具

  • Pretty Print:在Sources面板打开一个压缩过的JS文件,点击左下角的{}图标(美化代码),这能将单行、无格式的代码还原成可读的格式,是分析的第一步。
  • Overrides功能:这是本地持久化修改JS代码的神器。允许你将在线JS文件映射到本地磁盘的一个副本,并在本地修改、保存。刷新页面后,浏览器会加载你修改后的本地文件,极大方便了代码调试和逻辑验证。具体操作是在Sources面板的“Overrides”标签中添加一个本地文件夹,然后右键网络请求中的JS文件,选择“Save for overrides”。
  • Node.js环境:很多加密算法(如MD5、SHA、AES、RSA)在Node.js中有原生或稳定的第三方库(如crypto-js)实现。当我们扣出关键JS函数后,可以尝试在Node.js环境中运行和调试,验证其功能是否与浏览器一致,这比直接在Python里移植更接近原环境。

注意:不要一开始就尝试使用自动化的反混淆工具(如ast解析还原)。对于入门练习和大多数商业网站,先尝试通过调试和理解逻辑来解决问题,这能锻炼最重要的代码跟踪和逻辑分析能力。自动化工具是后续应对极端混淆时的备选方案。

3. 针对第三题的具体逆向流程实录

假设我们通过Network面板发现,点击查询按钮后,向/api/getdata发送了一个POST请求,其负载中有一个关键参数sign: "80b8c8f6a1e74c4a8c6f5d8b9a1e2f3c"(示例值)。我们的逆向流程就此展开。

3.1 定位加密函数入口

按照上一节的方法,我们首先尝试事件监听器断点。点击按钮后,代码在app.js的某一行断下。我们可以在右侧的“Call Stack”(调用堆栈)中看到函数调用链。逐步向上查看,寻找与参数组装或sign赋值相关的代码。

或者,我们直接在Sources面板全局搜索sign:。可能会找到类似这样的代码片段:

$.ajax({ url: '/api/getdata', type: 'POST', data: { page: pageNum, timestamp: new Date().getTime(), sign: generateSign(pageNum, new Date().getTime()) }, success: function(res){...} });

太好了,这里清晰地显示sign是由一个叫generateSign的函数生成的,参数是pageNum和当前时间戳。我们的目标立刻聚焦到这个generateSign函数上。

3.2 分析与扣取关键函数

在源代码中搜索function generateSign,找到其定义。假设它看起来是这样的:

function generateSign(page, timestamp) { var key = "turing_platform_secret"; var str = page + "|" + timestamp + "|" + key; return md5(str).toUpperCase(); }

这是一个非常典型的案例:将业务参数(页码)、时间戳和一个固定的密钥(key)用特定分隔符拼接,然后进行MD5哈希,最后转为大写。逻辑清晰明了。

“扣代码”在这里的含义,就是把这个逻辑移植到Python中。但在此之前,我们需要验证。在开发者工具的Console面板,我们可以直接测试:输入generateSign(1, 1646389472000),看看输出是否与Network中捕获的sign值(或类似规律)一致。如果一致,说明我们找对了。

3.3 Python代码复现与验证

现在,在Python中复现这个逻辑。我们需要hashlib库进行MD5计算。

import hashlib import time def generate_sign(page, timestamp): key = "turing_platform_secret" # 严格按照JS中的拼接顺序和分隔符 original_str = f"{page}|{timestamp}|{key}" # 创建md5对象,注意update需要bytes类型 m = hashlib.md5() m.update(original_str.encode('utf-8')) # 获取十六进制哈希值,并转为大写 sign = m.hexdigest().upper() return sign # 测试:使用一个已知的时间戳进行对比 test_timestamp = 1646389472000 my_sign = generate_sign(1, test_timestamp) print(f"生成的sign: {my_sign}") # 将打印的sign与浏览器Network捕获的对应请求的sign进行比对

如果输出结果与浏览器捕获的请求中的sign值完全一致,那么恭喜,逆向成功。你的爬虫现在可以动态生成这个参数了。

3.4 处理更复杂的情况

当然,实际的第三题可能比这个例子复杂。可能会遇到:

  • 引入外部库函数:比如sign的计算用到了CryptoJS.MD5。这时,你需要确认CryptoJS这个库在JS环境中是如何实现的。如果是标准用法,Python的hashlibpycryptodome库可以对应。有时需要把CryptoJS的一小部分辅助函数(比如字符串到WordArray的转换)也扣出来。
  • 包含浏览器环境对象:比如函数中使用了window.navigator.userAgent的一部分参与计算。这时,你需要在Python代码中模拟一个相同的UA字符串。
  • 多层函数调用generateSign内部可能又调用了_encrypt_base64Encode等其它函数。你需要沿着调用链,将所有依赖的函数都找到并扣出来,直到最底层的标准算法(如MD5、SHA256)或简单的逻辑运算为止。

实操心得:在扣取嵌套函数时,善用开发者工具的调试功能。在关键函数入口打上断点(点击行号即可),然后单步执行(F10步过,F11步入),观察每一步的变量值变化。这能帮你精准理解每一行代码的作用,以及数据的流转过程。把每一步的输入输出记录下来,就是最好的分析笔记。

4. 请求构造与数据抓取实现

逆向出参数生成算法后,爬虫的构造就水到渠成了。但这里依然有一些细节需要注意,这些细节往往是爬虫稳定性的关键。

4.1 构建完整的请求参数

我们需要模拟一个完整的、合法的请求。除了逆向得到的sign,还要注意其他参数:

import requests import time def get_data(page_num): url = "https://练习平台域名/api/getdata" # 替换为实际URL # 1. 获取当前时间戳(毫秒级),与JS中`new Date().getTime()`对应 current_timestamp = int(time.time() * 1000) # 2. 生成签名 sign = generate_sign(page_num, current_timestamp) # 3. 构造请求负载 payload = { "page": page_num, "timestamp": current_timestamp, # 确保这里传递的时间戳与生成sign时用的是同一个 "sign": sign } # 4. 构造请求头(非常重要!) headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...", # 模拟浏览器 "Content-Type": "application/x-www-form-urlencoded", # 根据实际请求的Content-Type调整 "Referer": "https://练习平台域名/第三题页面地址", # 来源页,有时会校验 # "X-Requested-With": "XMLHttpRequest" # 如果是Ajax请求,有时需要添加 } # 5. 发送请求 # 注意:如果实际请求是Form Data格式,用`data`参数;如果是JSON格式,用`json`参数,并调整headers中的Content-Type response = requests.post(url, data=payload, headers=headers) # 6. 处理响应 if response.status_code == 200: data = response.json() # 假设返回的是JSON return data else: print(f"请求失败,状态码:{response.status_code}") print(response.text) return None # 抓取第一页数据 data_page_1 = get_data(1) print(data_page_1)

4.2 时间戳同步问题

这是一个常见的坑。sign的生成依赖于时间戳。如果服务器时间与你的本地时间有较大偏差,或者服务器对时间戳的有效期有校验(例如只接受最近10秒内的请求),那么你生成的sign可能会失效。解决方案是:从服务器响应中获取时间。很多网站会在接口的响应里返回一个服务器时间戳。首次请求可以是一个不带签名或签名简单的“握手”请求,获取服务器时间后,后续请求都用这个服务器时间来计算签名。

4.3 请求头与会话保持

  • User-Agent:使用一个常见的浏览器UA字符串,避免使用python-requests这类默认UA。
  • Cookies:有些平台虽然主要验证sign,但登录状态可能仍依赖Cookie。可以使用requests.Session()来保持会话,先模拟登录获取Cookie,再用这个session去调用数据接口。
  • 其他自定义头:仔细检查浏览器中的原始请求,看是否有X-Token,X-Signature等自定义头部,这些都可能需要模拟。

5. 常见问题排查与进阶技巧

即使按照流程操作,你也可能会遇到各种问题。这里记录一些典型的排查思路和进阶技巧。

5.1 问题排查清单

问题现象可能原因排查思路
生成的sign与浏览器不一致1. 参数拼接顺序或分隔符错误。
2. 字符串编码不一致。
3. 使用了不同的MD5库(某些库会进行额外处理)。
4. 依赖的某个变量值在Python和浏览器环境中不同。
1. 在JS调试器中,在generateSign函数入口和return前打上断点,精确记录输入的参数和输出的结果。
2. 在Python中,将准备进行MD5的字符串原样打印出来,与JS调试器中记录的字符串进行逐字符对比(包括不可见字符)。
3. 确保在Python中使用encode('utf-8')
请求返回“签名错误”或“无效参数”1. 时间戳问题(见4.2)。
2. 请求头不完整,缺少RefererContent-Type不正确。
3. 负载(payload)格式错误,应用data却用了json参数,或反之。
1. 使用抓包工具(如Fiddler、Charles)或开发者工具的“Copy as cURL”功能,将浏览器成功的请求导出,并与你的Python请求代码进行逐行对比
2. 检查服务器返回的错误信息,有时会给出更具体的提示。
找不到加密函数入口1. 加密逻辑被Webpack等打包工具包裹。
2. 使用了全局事件委托,监听器不在按钮上。
3. 加密是异步的,或在Web Worker中执行。
1. 尝试搜索整个JS文件中的关键常量,如密钥key、加密函数名的一部分(如encryptsign)。
2. 在Network面板,对疑似加密的请求右键选择“Break on” -> “XHR/Fetch Breakpoint”,然后重新触发请求,代码会在发起请求前断住。
3. 查看是否加载了额外的chunk.js文件,加密逻辑可能在里面。

5.2 进阶技巧:Hook技术辅助定位

当搜索和断点都难以定位时,可以尝试使用“Hook”(钩子)技术。这相当于在浏览器环境中“埋点”,监听特定函数或属性的调用。例如,我们可以在Console中执行以下代码来Hookmd5函数(假设它挂载在window上):

(function() { var originalMd5 = window.md5; // 先保存原函数 window.md5 = function(str) { console.trace('md5被调用,参数是:', str); // 打印调用栈和参数 var result = originalMd5(str); // 调用原函数 console.log('md5结果是:', result); // 打印结果 return result; // 返回结果 }; })();

执行这段代码后,再点击页面按钮,Console会输出所有对md5的调用信息,包括从哪里调用的(调用栈)以及输入输出,这能帮你快速定位加密发生的位置。这种方法对JSON.stringifyDate.getTimebtoa等标准API同样有效。

5.3 扣代码的优化与重构

直接翻译JS代码到Python有时会很冗长,特别是当JS代码涉及复杂的位操作或特定的编码方式时。在理解算法本质后,应寻求用Python更优雅的方式实现。

  • 识别标准算法:如果经过分析,发现最终是AES加密、RSA加密或标准的Base64,那么应该直接使用Python成熟的库(如pycryptodomersabase64)来实现,而不是去扣JS里那套复杂的模拟代码。关键在于确认算法模式、密钥、填充方式等参数与JS端一致。
  • 提取核心逻辑:只扣取自定义的业务逻辑部分,比如参数拼接顺序、密钥混合方式等。对于标准的加密哈希函数,调用Python内置库。

逆向的最终目的不是完美复现一段晦涩的JS代码,而是理解其生成规则。规则一旦被掌握,就可以用任何语言、以最简洁高效的方式实现它。图灵平台的第三题,正是训练这种“理解规则”能力的绝佳起点。当你成功跑通第一个案例后,那种透过混淆代码看清本质的成就感,会支撑你面对更复杂的真实战场。

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

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

立即咨询