欢迎光临
我们一直在努力

JS加密逆向实战:从浏览器调试到Python复现,破解数据接口签名

1. 项目概述:为什么我们要研究JS加密

最近在做一个金融数据聚合的小工具,需要从几个主流平台获取一些净值数据来做分析。私募排排网作为国内知名的私募基金信息平台,自然是我重点关注的对象。但和很多现代网站一样,它的数据接口都做了加密处理,直接用

requests

库去请求,拿回来的要么是乱码,要么就是一堆看不懂的加密字符串。这其实就是典型的JavaScript(JS)加密,前端通过JS对请求参数或响应数据进行混淆和加密,以此来增加数据抓取的难度,保护接口安全。

对于做数据分析、量化研究或者只是想自动化获取公开数据的开发者来说,这成了一个不大不小的门槛。你明明看到浏览器里数据展示得好好的,但自己写脚本去抓就是拿不到。所以,今天我就来分享一下,我是如何一步步逆向分析私募排排网某个数据接口的JS加密逻辑,并最终用Python成功复现,实现稳定数据抓取的整个过程。我会把核心的逆向思路、关键代码的定位方法,以及完整的Python实现代码都贴出来,即使你是刚接触逆向的新手,跟着做一遍也能掌握这套方法论。

2. 逆向分析的核心思路与准备工作

逆向JS加密,听起来很高深,其实核心思路就一句话:

找到浏览器中执行加密的那个关键函数,理解它的输入输出和内部逻辑,然后用另一种语言(这里是Python)重新实现它。

2.1 核心思路拆解

这个过程可以分解为几个明确的步骤:

  • 定位网络请求

    :在浏览器开发者工具中,找到我们想要获取数据的那条网络请求(通常是XHR或Fetch请求)。查看它的请求头、请求参数和响应体,确认哪些部分是被加密的。

  • 搜索加密入口

    :在网站的JS源代码中,搜索与加密参数相关的关键词(如参数名、加密后的字符串片段等),或者通过“调用栈”来定位生成这些参数的JS函数。

  • 分析加密函数

    :找到关键函数后,需要静态分析(阅读代码)和动态调试(打断点、单步执行)相结合,弄清楚这个函数接收什么参数,内部经过了哪些处理(比如用了什么加密算法,是否调用了外部库,是否有随机因子),最后输出了什么。

  • 模拟与复现

    :在完全理解算法后,用Python编写代码,模拟整个加密过程。这里的关键是确保Python环境拥有JS代码所依赖的所有算法库(如CryptoJS、各种哈希、AES、RSA等),并且能正确处理字符串编码、字节转换等细节。

  • 2.2 工具准备

    工欲善其事,必先利其器。你需要准备好以下工具:

    • 浏览器

      :推荐使用

      Google Chrome

      Microsoft Edge

      (Chromium内核)。它们的开发者工具功能最强大,对JS调试的支持也最好。

    • 开发者工具

      :重点是

      “Network”(网络)

      面板和

      “Sources”(源代码)

      面板。网络面板用来抓包,源代码面板用来调试JS。

    • 代码编辑器

      :用于编写Python代码,比如VS Code、PyCharm都可以。

    • Python环境

      :确保安装了Python 3.6以上版本。我们主要会用到

      requests

      库进行网络请求,以及

      execjs

      库来执行JS代码片段(有时直接调用JS引擎比完全用Python重写更简单)。当然,也可能用到

      hashlib

      ,

      hmac

      ,

      base64

      等标准库。
      # 安装必要的Python库
      pip install requests execjs

    • 一个能执行JS的环境(备选)

      execjs

      需要一个JS运行时。在Windows上,它通常会尝试使用系统自带的JScript(IE引擎),但功能有限。我强烈推荐安装

      Node.js

      execjs

      会自动检测并使用它,功能更完整。

      注意:如果你在分析过程中发现加密逻辑过于复杂,直接使用

      execjs

      调用原网站的JS代码可能是最稳妥快捷的方式。但我们的最终目标是尽可能用纯Python实现,这样部署和运行效率更高。

    3. 实战:逆向私募排排网数据接口

    下面,我们进入实战环节。我以抓取“私募排排网”上某个基金列表页的数据为例。请注意,网站的具体加密方式可能随时间更新,但方法论是通用的。

    3.1 第一步:抓包与观察

  • 打开Chrome浏览器,进入私募排排网的目标页面(例如某个策略的基金列表页)。
  • 按下

    F12

    Ctrl+Shift+I

    打开开发者工具,切换到

    Network(网络)

    面板。记得勾选上

    “Preserve log”(保留日志)

    ,防止页面跳转时请求记录被清空。

  • 刷新页面或触发数据加载(比如点击“下一页”)。在网络面板中,你会看到大量请求。我们需要找到那个真正返回列表数据的请求。
  • 通常,这类请求的

    Type(类型)

    xhr

    fetch

    Initiator(发起者)

    列会显示

    script

    或其他JS文件。你可以通过预览响应内容来快速定位。找到那个响应体是JSON格式,并且包含基金名称、代码、净值等数据的请求。

  • 点击这个请求,查看

    Headers(标头)

    Payload(负载)

    。重点看:

    • Request Headers

      : 有没有特殊的自定义Header,比如

      X-Sign

      ,

      X-Timestamp

      ,

      Authorization

      等。

    • Query String Parameters / Form Data

      : 查看URL参数或请求体参数。你可能会发现一些看起来像加密字符串的参数,例如

      sign

      ,

      token

      ,

      data

      等,它们的值通常是一长串无规律的字符。

  • 假设我们找到了一个关键的请求,它的URL里包含一个

    sign

    参数,或者请求体里有一个加密的

    data

    字段。我们的目标就是找出这个

    sign

    data

    是如何生成的。

    3.2 第二步:定位加密关键函数

    这是逆向中最需要耐心和技巧的一步。

    方法A:关键词搜索

  • 在开发者工具中,切换到

    Sources(源代码)

    面板。

  • Ctrl+Shift+F

    打开全局搜索框(注意,如果页面JS文件过多且被混淆,这个搜索可能会很慢或搜不到)。

  • 输入你怀疑是加密参数的参数名,比如

    sign

    ,进行搜索。在结果中,寻找那些看起来在

    赋值

    生成

    这个参数的代码行。例如

    sign: xxxx

    ,

    params['sign'] =

    ,

    var sign =

  • 方法B:调用栈回溯(更有效)

  • 回到

    Network

    面板,找到那个数据请求,右键点击它,选择

    “Copy” -> “Copy as cURL (bash)”

    。这能帮你快速在Python中构造一个相同的请求,但缺少动态参数。

  • 更关键的是,在这个请求的

    Initiator

    列,通常有一个链接,点击它可以跳转到发起这个请求的JS代码位置。

  • 或者,我们可以在可能被加密的参数生成的地方打上断点。在

    Sources

    面板,找到所有JS文件(通常被合并或混淆成一个

    app.xxxx.js

    vendor.xxxx.js

    )。在文件开头部分,输入

    debugger;

    语句(如果网站没有反调试)或者使用XHR断点。

  • Sources

    面板,找到右侧的

    “XHR/fetch Breakpoints”

    。点击

    +

    号,输入包含目标接口URL关键词的字符串(如

    /api/fund/list

    )。这样,当任何JS代码发起包含该关键词的请求时,浏览器会自动暂停(断住)。

  • 重新触发请求(如翻页)。代码会在发送请求前一刻断住。此时,查看右侧的

    Call Stack(调用栈)

    。调用栈显示了当前断点位置是由哪一系列函数调用导致的。从栈顶往下看,你就能一步步回溯到最初生成加密参数的那个函数。

  • 通过调用栈,我最终定位到了一个名为

    generateSign

    encryptData

    的函数(函数名可能被混淆,比如变成

    a0x12bc

    之类的)。点击它,我们就进入了核心的加密逻辑代码区。

    3.3 第三步:分析与理解加密逻辑

    现在,我们面对的可能是一段被混淆压缩的JS代码。它可能长这样:

    function s(t, e) {
    var n = (new Date).getTime();
    var r = Object.keys(t).sort().map(function(key) {
    return key + "=" + t[key]
    }).join("&");
    var i = r + "&timestamp=" + n + "&secret=" + e;
    return a(i)
    }
    function a(t) {
    // … 这里可能是MD5、SHA256或HMAC等哈希计算
    }

    我们需要分析这段代码:

  • 函数输入

    t

    是参数对象,

    e

    可能是一个固定的密钥(secret)。

  • 逻辑步骤


    a. 获取当前时间戳

    n


    b. 将参数对象

    t

    的键按字母排序,然后拼接成

    key1=value1&key2=value2…

    格式的字符串

    r


    c. 将排序后的参数字符串、时间戳和密钥用

    &

    连接,得到待加密字符串

    i


    d. 将字符串

    i

    传入函数

    a

    进行处理。

  • 函数

    a

    :我们需要进入

    a

    函数内部,看它具体做了什么。可能是

    CryptoJS.MD5(i).toString()

    ,也可能是

    CryptoJS.HmacSHA256(i, e).toString()

    。这里就需要动态调试了。

  • 动态调试

    :在

    generateSign

    函数内部和

    a

    函数入口打上断点(点击行号即可)。重新触发请求,让代码再次断住。然后使用

    Step Over (F10)

    ,

    Step Into (F11)

    单步执行,同时观察右侧

    Scope

    面板中变量的值变化。特别是查看

    i

    的值和

    a(i)

    的返回值。这能让你确切地知道输入是什么,输出是什么。

  • 经过分析,假设我们确定加密逻辑是:

    sign = MD5(排序后的参数字符串 + “&timestamp=” + 时间戳 + “&secret=固定的密钥”)

    实操心得:很多网站的签名算法并不复杂,核心就是“排序+拼接+哈希”。密钥(secret)有时是硬编码在JS里的,有时需要从其他接口或Cookie中获取。动态调试是验证猜想的不二法门。

    4. Python完整代码实现

    在完全理解算法后,我们就可以用Python来复现了。这里提供两种实现方式。

    4.1 方式一:纯Python实现(推荐)

    如果我们分析出算法是标准的MD5、SHA256或HMAC,Python标准库

    hashlib

    hmac

    就能完美应对。

    import hashlib
    import time
    import requests
    import json
    from urllib.parse import urlencode

    def generate_sign(params, secret_key):
    """
    生成请求签名
    :param params: 字典,请求参数(不包含sign本身)
    :param secret_key: 字符串,从JS中分析得到的密钥
    :return: 计算得到的sign字符串
    """
    # 1. 对参数键进行排序
    sorted_params = sorted(params.items(), key=lambda x: x[0])
    # 2. 拼接成 key1=value1&key2=value2 格式
    param_str = '&'.join([f'{k}={v}' for k, v in sorted_params])
    # 3. 获取当前时间戳(毫秒级,与JS的Date.getTime()一致)
    timestamp = int(time.time() * 1000)
    # 4. 拼接待加密字符串(根据实际分析的格式调整)
    # 例如: param_str + "×tamp=" + str(timestamp) + "&secret=" + secret_key
    string_to_sign = f"{param_str}×tamp={timestamp}&secret={secret_key}"
    # 5. 计算MD5(根据实际算法调整,可能是hashlib.sha256等)
    sign = hashlib.md5(string_to_sign.encode('utf-8')).hexdigest()
    # 有些网站会再将MD5结果转为大写
    # sign = sign.upper()
    return sign, timestamp

    def fetch_fund_data():
    """
    模拟请求私募排排网基金列表数据
    """
    # 假设的分析结果
    secret_key = "THIS_IS_A_SECRET_KEY" # !!! 这个需要你从JS代码里找出来 !!!

    # 构造请求参数(根据实际接口调整)
    query_params = {
    'page': 1,
    'size': 20,
    'strategy': '股票策略',
    # … 其他参数
    }

    # 生成签名和时间戳
    sign, timestamp = generate_sign(query_params, secret_key)

    # 将签名和时间戳加入最终请求参数
    final_params = query_params.copy()
    final_params['sign'] = sign
    final_params['timestamp'] = timestamp

    # 目标API地址(需要从Network面板复制)
    api_url = "https://api.simuwang.com/YourTargetPath" # 示例地址,请替换

    # 设置请求头,模拟浏览器
    headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
    'Referer': 'https://www.simuwang.com/', # 来源页,有时需要
    'Content-Type': 'application/json', # 根据接口实际情况调整
    }

    try:
    # 发送GET请求(如果是POST,则使用requests.post,并将参数放入data或json字段)
    response = requests.get(
    api_url,
    params=final_params, # GET请求参数放在params
    headers=headers,
    timeout=10
    )
    response.raise_for_status() # 检查请求是否成功
    data = response.json() # 解析JSON响应
    print("请求成功,数据示例:")
    print(json.dumps(data, indent=2, ensure_ascii=False))
    return data
    except requests.exceptions.RequestException as e:
    print(f"请求出错: {e}")
    print(f"响应状态码: {response.status_code if 'response' in locals() else 'N/A'}")
    print(f"响应文本: {response.text if 'response' in locals() else 'N/A'}")
    except json.JSONDecodeError as e:
    print(f"JSON解析出错: {e}")
    print(f"原始响应: {response.text}")

    if __name__ == '__main__':
    fetch_fund_data()

    4.2 方式二:使用execjs调用JS代码(适用于复杂或混淆严重的算法)

    当加密算法涉及复杂的JS库(如CryptoJS的特定配置)或混淆逻辑难以直接翻译时,直接调用原JS函数是最快最准的。

  • 提取关键JS代码

    :从Sources面板,找到包含

    generateSign

    函数及其所有依赖函数(比如上面的函数

    a

    ,以及

    a

    函数可能依赖的CryptoJS等)的代码块。将其复制到一个文本文件中,例如

    crypto_logic.js

  • 封装为可调用模块

    :在JS文件末尾,添加导出语句,方便Node.js或execjs调用。
    // crypto_logic.js
    // … 你复制过来的所有加密相关JS代码 …

    // 假设原函数叫 window.generateSign
    function generateSign(params, secret) {
    // … 原函数体 …
    }

    // 导出函数,使其可在其他上下文中被调用
    module.exports = {
    generateSign: generateSign
    };

  • Python调用


    import execjs
    import requests
    import json

    # 1. 读取JS文件
    with open('crypto_logic.js', 'r', encoding='utf-8') as f:
    js_code = f.read()

    # 2. 编译JS代码
    ctx = execjs.compile(js_code)

    def fetch_with_js_sign():
    secret_key = "THIS_IS_A_SECRET_KEY"
    query_params = {
    'page': 1,
    'size': 20,
    'strategy': '股票策略',
    }

    # 3. 调用JS函数生成签名
    # 注意:execjs调用时,参数需要传递给JS函数
    sign_result = ctx.call('generateSign', query_params, secret_key)
    # 假设JS函数返回一个包含sign和timestamp的对象
    sign = sign_result['sign']
    timestamp = sign_result['timestamp']

    final_params = query_params.copy()
    final_params.update({'sign': sign, 'timestamp': timestamp})

    api_url = "https://api.simuwang.com/YourTargetPath"
    headers = {
    'User-Agent': 'Mozilla/5.0 …',
    }

    response = requests.get(api_url, params=final_params, headers=headers)
    data = response.json()
    print(json.dumps(data, indent=2, ensure_ascii=False))
    return data

    if __name__ == '__main__':
    fetch_with_js_sign()

  • 注意事项:使用

    execjs

    性能不如纯Python,且依赖Node.js环境。在部署到服务器时,需要确保环境一致。但对于快速验证和破解复杂加密,它是利器。

    5. 常见问题与排查技巧实录

    在实际操作中,你几乎一定会遇到下面这些问题。这里是我的排查实录。

    5.1 问题一:生成的签名总是无效,服务器返回签名错误

    • 可能原因1:参数排序规则不一致

      。JS的

      Object.keys().sort()

      和Python的

      sorted(dict.items())

      默认都是按字符串Unicode码点排序,理论上一致。但要注意,如果参数值本身是嵌套对象或数组,两者的序列化方式可能不同。

      解决方案

      :确保拼接前的参数字符串完全一致。可以将JS中待加密的字符串和Python中拼接的字符串都打印出来,进行逐字符比对。

    • 可能原因2:空格或特殊字符编码问题

      。JS和Python对空格、加号

      +

      、中文等字符的URL编码处理可能不同。

      解决方案

      :在拼接前,确保所有参数值都经过统一的URL编码(或都不编码,严格按照JS中的原始值处理)。使用

      urllib.parse.quote

      但要小心,有时JS用的是

      encodeURIComponent

      ,两者有细微差别。

    • 可能原因3:时间戳格式或精度不对

      。JS的

      Date.getTime()

      是毫秒级(13位),Python的

      time.time()

      是秒级浮点数(10位小数)。

      解决方案

      :使用

      int(time.time() * 1000)

      来获取13位毫秒时间戳。

    • 可能原因4:密钥(Secret)不对或过期

      。密钥可能不是硬编码,而是从其他接口动态获取,或者有有效期。

      解决方案

      :动态调试时,检查传入

      generateSign

      函数的第二个参数

      e

      的值,确认其来源。可能需要先请求一个获取token的接口。

    5.2 问题二:断点打不上,或者代码被混淆得无法阅读

    • 解决方案

      :使用

      “XHR/fetch Breakpoints”

      全局断点,这是最可靠的方法。对于混淆代码,不要试图完全读懂,关注

      变量值的变化

      。在关键函数入口和出口打上断点,观察输入和输出。可以借助浏览器的

      “Pretty print”

      按钮(

      {}

      图标)格式化混淆的代码,使其有一定可读性。

    5.3 问题三:算法中使用了浏览器特有的对象或API(如

    window.btoa

    ,

    Crypto.subtle


    • 解决方案

      • 对于

        btoa

        (base64编码)、

        atob

        (解码),Python可以用

        base64.b64encode

        b64decode

        替代,但要注意字符串到字节的转换。

      • 对于

        Crypto.subtle

        (Web Crypto API),这是一个较新的标准,在Node.js或普通JS引擎中可能不可用。如果算法依赖于此,

        execjs

        方案可能失效。此时需要寻找该API的Polyfill(垫片)库,或者更深入地分析其实现的算法(如AES-GCM),然后用Python的

        cryptography

        库来实现。

    5.4 问题四:请求被风控,返回403或验证码

    • 解决方案

      :单纯的签名破解可能不足以绕过高级风控。你需要让你的Python请求看起来更像一个真实的浏览器会话。

      • 携带完整的Headers

        :复制浏览器请求中的所有Headers,特别是

        Cookie

        ,

        User-Agent

        ,

        Referer

        ,

        Accept-Language

        等。

      • 维持会话

        :使用

        requests.Session()

        对象,它会自动处理Cookies,保持登录态。

      • 模拟更真实的行为

        :在连续请求间添加随机延时,模拟人的操作间隔。

      • 处理动态Cookie

        :有些网站的Cookie中也有加密参数,可能需要先访问首页或登录页获取。

    import time
    import random

    session = requests.Session()
    # 首先,可能要先访问一下首页,获取初始Cookie
    session.get('https://www.simuwang.com/', headers=headers)

    # 后续请求都使用同一个session
    response = session.get(api_url, params=final_params, headers=headers)

    # 请求间随机延时
    time.sleep(random.uniform(1, 3))

    逆向分析JS加密是一个需要耐心、观察力和逻辑推理的过程。它没有一成不变的公式,但核心思路——定位、分析、模拟——是通用的。私募排排网的这个案例,涵盖了最常见的签名生成场景。当你成功跑通代码,拿到数据的那一刻,成就感是非常足的。记住,逆向的目的是为了学习与自动化,务必遵守网站的

    robots.txt

    协议,合理控制请求频率,不要对目标服务器造成压力。

    赞(0)
    未经允许不得转载:171主机测评 » JS加密逆向实战:从浏览器调试到Python复现,破解数据接口签名
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址