欢迎光临
我们一直在努力

爬虫实战:逆向分析JavaScript中的AES加密接口与Python解密实现

1. 项目概述:当爬虫遇上AES加密

做爬虫的朋友,尤其是从入门开始往进阶方向走的,迟早会遇到一个绕不开的坎儿:加密接口。你兴冲冲地打开开发者工具,准备分析网络请求,结果发现关键的请求参数或者返回的数据,是一串你看不懂的“乱码”。这串“乱码”很可能就是AES加密的产物。今天,我就以一个真实的爬虫场景为例,手把手带你走一遍从发现加密、分析加密逻辑,到最终用Python代码成功破解并拿到明文数据的完整流程。这不是一个简单的“调用库解密”的教程,而是聚焦于实战中最关键的一步:

如何从纷繁复杂的网页JavaScript代码中,找到那个关键的密钥(Key)和初始化向量(IV)

。毕竟,给你一个AES解密函数很简单,但告诉你密钥在哪,才是真正的“干货”。

AES(高级加密标准)作为一种对称加密算法,在Web前端数据保护中应用非常广泛。网站用它来加密传输敏感数据,比如登录凭证、交易信息、列表数据等,以防止被轻易窥探。这对安全是好事,但对数据采集工作来说,就多了一道需要破解的屏障。不过别担心,只要加密发生在客户端(即浏览器里),理论上我们就有办法通过逆向工程还原其逻辑。整个过程,更像是一场在代码丛林里的“寻宝游戏”,目标就是找到开启数据宝箱的那把“钥匙”。

2. 逆向分析:在JavaScript的海洋里寻找密钥

面对一个加密接口,直接莽撞地去尝试各种解密算法是不现实的。第一步永远是冷静分析,而浏览器开发者工具(F12)就是我们最强大的武器。我们的目标很明确:找到生成加密参数或解密响应的JavaScript代码,并从中提取出AES加密所用的模式、填充方式、密钥和IV。

2.1 抓包与初步定位

首先,我们通过浏览器正常操作,触发目标数据的加载。在开发者工具的“网络”(Network)面板中,筛选XHR或Fetch请求,找到那个携带加密数据(可能是一个长字符串放在

data

、

encryptData

、

params

等字段里)的请求。同时,观察服务器返回的数据,如果也是加密的,通常会有一个字段(如

isEncrypt: true

)或直接就是一段Base64编码的密文。

一个非常实用的技巧是

搜索关键字段

。在开发者工具的“源代码”(Sources)面板,按

Ctrl+Shift+F

(Windows)或

Cmd+Opt+F

(Mac)开启全局搜索。你可以尝试搜索:

  • 加密请求参数的名称,如

    "data"

    、

    "encryptData"

    。

  • 可能标识加密的字段,如

    "isEncrypt"

    、

    "encrypted"

    。

  • 加密算法名称,如

    "AES"

    、

    "CryptoJS"

    、

    "encrypt"

    、

    "decrypt"

    。

  • 甚至是一些可能的关键词,如

    "mode"

    、

    "padding"

    、

    "iv"

    。

注意

:现代网站大量使用Webpack等打包工具,源代码可能被压缩、混淆。你搜到的代码可能变量名都是

a

、

b

、

c

,函数名也被缩短。这时不要慌,我们关注的是

字符串常量

和

核心API调用

,这些通常不会被混淆。比如搜索

"AES"

或

"CBC"

仍然可能直接定位到相关代码块。

2.2 深入分析与逻辑追踪

假设我们通过搜索

"isEncrypt"

定位到了一段疑似解密响应的代码。它可能长这样(已格式化):

function decryptData(encryptedData, keyStr) {
var key = CryptoJS.enc.Utf8.parse(keyStr);
var iv = CryptoJS.enc.Utf8.parse(keyStr); // 注意这里,IV和Key相同
var decrypted = CryptoJS.AES.decrypt(encryptedData, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
}

这段代码信息量极大:

  • 加密库

    :使用了

    CryptoJS

    ,这是前端最常用的加密库之一。

  • 模式与填充

    :

    CBC

    模式,

    Pkcs7

    填充。这是AES非常常见的组合。

  • 密钥与IV

    :

    key

    和

    iv

    都来自

    keyStr

    这个参数,且都是通过UTF-8编码转换而来。

    这里揭示了一个关键点:IV等于Key

    。很多简单的实现为了省事,会直接用密钥作为初始化向量,这其实降低了安全性,但对我们分析来说是好事。

  • 输入输出

    :

    encryptedData

    是密文,解密后通过

    toString(CryptoJS.enc.Utf8)

    转回字符串。

  • 那么,下一个核心问题来了:这个

    keyStr

    从哪里来?我们需要继续向上追踪。可能它来自某个全局变量,可能是对某个接口返回值的处理(比如时间戳),也可能是写死在代码里的一个固定值。这时,我们需要在代码中搜索

    keyStr

    或者观察这个解密函数被调用时传入的参数是什么。

    2.3 关键密钥的获取

    在实战中,密钥的来源五花八门,但常见的有以下几种:

  • 固定值

    :直接硬编码在JS文件里。搜索一些可能的密钥格式(如16/24/32位的字符串)或

    "key"

    字段。

  • 接口返回

    :在某个初始化请求或登录请求的响应中,服务器会返回一个密钥或用于生成密钥的种子。你需要关联多个请求来看。

  • 时间戳衍生

    :非常常见!密钥可能是当前时间戳,或者时间戳经过简单运算(如取前N位、后N位、乘以/除以一个固定数)后的结果。例如,你可能会看到

    key = Date.now().toString()

    或

    key = "prefix_" + timestamp

    。

  • 其他参数计算

    :用请求中的其他参数(如用户ID、页面ID)通过一个固定算法计算得出。

  • 在我们的示例中,假设通过追踪发现

    keyStr

    的值就是请求URL中的一个参数

    lastFetchTime

    ,或者直接是服务器返回的一个字段

    lastFetchTime

    。那么,密钥就是

    1572353144793000

    这样的时间戳字符串。

    实操心得

    :在逆向时,善用开发者工具中的“调试器”(Debugger)。在疑似关键函数处打上断点,然后重新触发请求,程序会在断点处暂停。这时你可以查看此时的调用栈(Call Stack)、作用域(Scope)里所有变量的值,这是追踪数据流向最直观的方法。你可以看到传入

    decryptData

    函数的

    keyStr

    具体是什么值,从而确认密钥来源。

    3. Python复现:构建我们的解密工具

    一旦我们在前端JavaScript中摸清了加密/解密的全部逻辑(算法、模式、填充、密钥、IV),下一步就是用Python将其复现出来。Python拥有强大的

    cryptography

    和

    pycryptodome

    库,可以完美实现AES加解密。

    3.1 环境准备与库选择

    首先,确保你的Python环境已经安装了必要的库。我们推荐使用

    pycryptodome

    ,它是

    pycrypto

    的一个维护更活跃的分支。

    pip install pycryptodome

    如果安装速度慢,可以使用国内镜像源:

    pip install pycryptodome -i https://pypi.tuna.tsinghua.edu.cn/simple

    3.2 核心代码实现

    根据我们逆向分析的结果:AES-128-CBC模式,PKCS7填充,Key和IV相同,且都是UTF-8编码的字符串。下面我们来编写对应的Python解密函数。

    import base64
    from Crypto.Cipher import AES
    import hashlib

    def pkcs7_padding(data):
    """
    实现 PKCS7 填充。
    AES 块大小为 16 字节,需要将数据填充至其整数倍。
    """
    block_size = AES.block_size # 16
    padding_length = block_size – len(data) % block_size
    # PKCS7 填充的字节值等于需要填充的字节数
    padding = bytes([padding_length]) * padding_length
    return data + padding

    def pkcs7_unpadding(data):
    """
    去除 PKCS7 填充。
    """
    padding_length = data[-1] # 最后一个字节的值就是填充长度
    # 验证填充是否有效(可选但推荐)
    if padding_length < 1 or padding_length > AES.block_size:
    raise ValueError("Invalid padding length.")
    if data[-padding_length:] != bytes([padding_length]) * padding_length:
    raise ValueError("Invalid padding bytes.")
    return data[:-padding_length]

    def decrypt_aes_cbc_with_same_key_iv(encrypted_b64, key_str):
    """
    解密 AES-CBC 模式,且 Key 和 IV 相同的密文。
    :param encrypted_b64: Base64编码的密文字符串
    :param key_str: 密钥字符串(同时也是IV)
    :return: 解密后的明文字符串
    """
    # 1. 将密钥字符串转换为字节,并确保长度符合AES要求(16, 24, 32字节)
    # 通常前端CryptoJS.enc.Utf8.parse会按UTF-8编码成字节。
    key_bytes = key_str.encode('utf-8')

    # 2. 检查密钥长度,如果不够,可以用特定方式补齐(如MD5),具体需根据JS逻辑判断。
    # 这里假设 key_str 本身长度就是16(AES-128)。如果不是,需要按JS中的逻辑处理。
    # 例如,JS中可能用了 MD5 哈希一个字符串得到16字节密钥。
    # 本例假设 key_str 直接作为密钥,且长度已满足要求。
    if len(key_bytes) not in [16, 24, 32]:
    # 常见情况:key_str是一个时间戳字符串,JS里直接用了。
    # 但AES要求密钥长度固定。此时需要看JS源码如何处理。
    # 假设JS里用CryptoJS.enc.Utf8.parse,它不会自动补长。
    # 如果密钥长度不对,解密会失败。可能需要用MD5生成固定长度密钥。
    # 例如:key_bytes = hashlib.md5(key_str.encode()).digest() # 生成16字节密钥
    # 这里我们先按原样处理,如果失败再调整。
    pass

    iv_bytes = key_bytes # IV 等于 Key

    # 3. 创建 AES 解密器
    cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)

    # 4. Base64 解码密文
    encrypted_bytes = base64.b64decode(encrypted_b64)

    # 5. 执行解密
    decrypted_padded_bytes = cipher.decrypt(encrypted_bytes)

    # 6. 去除 PKCS7 填充
    decrypted_bytes = pkcs7_unpadding(decrypted_padded_bytes)

    # 7. 解码为字符串(假设原文是UTF-8)
    plaintext = decrypted_bytes.decode('utf-8')

    return plaintext

    # 使用示例
    if __name__ == '__main__':
    # 这是我们从网络请求中捕获的加密数据(Base64格式)
    encrypted_data_b64 = "WPZVRdF+hMHReWs0rgM+SR+uV3TjVPhsVKOCrW+cOF8jfhT/JL/faU3tYyBVPkyJsV+P6ReJ46/Pi0…"
    # 这是我们从JS逆向中找到的密钥(假设是时间戳字符串)
    secret_key = "1572353144793000"

    try:
    result = decrypt_aes_cbc_with_same_key_iv(encrypted_data_b64, secret_key)
    print("解密成功!")
    print("明文内容:", result)
    # 通常结果是一个JSON字符串
    import json
    data_obj = json.loads(result)
    print("解析为JSON:", json.dumps(data_obj, indent=2, ensure_ascii=False))
    except Exception as e:
    print("解密失败,错误信息:", e)
    print("可能的原因:密钥长度不对、IV不对、填充方式不对、密文损坏等。")

    3.3 代码关键点解析

  • 编码一致性

    :JavaScript的

    CryptoJS.enc.Utf8.parse

    将字符串按UTF-8编码转换为二进制字数组。在Python中,我们用

    key_str.encode('utf-8')

    实现完全相同的效果。

    这是最容易出错的地方之一

    ,必须确保编码方式一致。

  • 密钥长度

    :AES标准密钥长度是128位(16字节)、192位(24字节)或256位(32字节)。如果前端传入的密钥字符串长度不符合,CryptoJS内部可能会进行哈希(如MD5)或裁剪来适配。

    你必须通过阅读JS代码确认其处理逻辑

    。上面的代码注释中给出了用MD5处理不定长字符串的示例。

  • IV处理

    :本例中IV等于Key,所以直接

    iv_bytes = key_bytes

    。如果JS中IV是独立生成的(比如全零

    \\x00

    或另一个字符串),则需要单独获取并处理。

  • 填充模式

    :PKCS7是标准填充。我们的

    pkcs7_unpadding

    函数不仅去除了填充,还加入了简单的验证,提高了代码的健壮性。

  • 错误处理

    :解密过程可能因各种原因失败(密钥错误、密文格式错误、编码问题等)。用

    try…except

    包裹并打印详细错误信息,对于调试至关重要。

  • 4. 实战演练与问题排查

    理论说得再多,不如一次实战。假设我们遇到了一个真实网站,其列表数据通过AJAX加载,返回的JSON数据中,

    data

    字段是一个加密的字符串。

    4.1 完整爬虫流程整合

    我们的爬虫脚本将整合请求和解密过程:

    import requests
    import json
    from decrypt_utils import decrypt_aes_cbc_with_same_key_iv # 假设我们把解密函数放在这个模块里

    def fetch_encrypted_data(page):
    """模拟请求获取加密数据"""
    url = "https://target-website.com/api/getList"
    # 通常加密接口会需要一些参数,这些参数可能也参与了密钥生成
    params = {
    'page': page,
    't': get_current_timestamp(), # 时间戳,可能就是密钥的一部分
    # … 其他必要参数
    }
    headers = {
    'User-Agent': 'Mozilla/5.0…',
    # 可能需要Cookie或其他认证头
    }
    resp = requests.get(url, params=params, headers=headers)
    resp.raise_for_status()
    return resp.json() # 假设返回的是JSON,里面有一个encrypted_data字段

    def get_current_timestamp():
    """生成与JS逻辑一致的时间戳"""
    # 注意:JS的 Date.now() 返回毫秒级时间戳(13位)。
    # Python的 time.time() 返回秒级浮点数(10位+小数)。
    # 需要乘以1000并取整来匹配。
    import time
    return str(int(time.time() * 1000))

    def main():
    all_data = []
    for page in range(1, 6):
    print(f"正在抓取第 {page} 页…")
    encrypted_json = fetch_encrypted_data(page)

    # 提取加密字符串和密钥(这里需要根据实际逆向结果调整)
    encrypted_b64 = encrypted_json['data']
    # 密钥来源:可能是当前时间戳,也可能是响应里的另一个字段
    secret_key = get_current_timestamp() # 示例:直接用请求时的时间戳
    # 或者 secret_key = encrypted_json['keyField']

    try:
    decrypted_text = decrypt_aes_cbc_with_same_key_iv(encrypted_b64, secret_key)
    page_data = json.loads(decrypted_text)
    all_data.extend(page_data['list'])
    print(f"第 {page} 页解密成功,获取到 {len(page_data['list'])} 条数据。")
    except Exception as e:
    print(f"第 {page} 页解密失败:{e}")
    # 可以记录日志,或者尝试其他密钥生成方式
    break

    print(f"总共获取到 {len(all_data)} 条数据。")
    # 保存数据
    with open('result.json', 'w', encoding='utf-8') as f:
    json.dump(all_data, f, ensure_ascii=False, indent=2)

    if __name__ == '__main__':
    main()

    4.2 常见问题与排查清单

    即使你严格按JS逻辑复现了代码,解密过程仍可能失败。下面是一个问题排查指南:

    问题现象

    可能原因

    排查思路与解决方案

    ValueError: Invalid padding bytes.

    或解密后乱码

    1. 密钥错误

    :Key或IV不正确。

    2. 密文损坏

    :Base64解码出错或传输中有更改。

    3. 算法/模式/填充不匹配

    :不是CBC模式或PKCS7填充。

    1.

    核对密钥

    :在JS调试器中,在解密函数断点处,确认传入的

    keyStr

    和

    iv

    的

    精确值

    (包括类型,是字符串还是对象)。

    2.

    检查密文

    :确保从网络请求中复制的密文完整无误,没有遗漏等号

    =

    。

    3.

    验证算法

    :确认JS代码中

    CryptoJS.AES.decrypt

    的第三个参数(配置对象)里的

    mode

    和

    padding

    属性。

    TypeError: Incorrect IV length (must be 16 bytes long)

    IV长度不对

    。AES-CBC模式的IV必须是16字节。

    1. 检查JS中IV的生成方式。如果IV来自一个字符串,确认

    CryptoJS.enc.Utf8.parse

    后的字节长度。如果不足16字节,CryptoJS可能会怎么处理?可能需要用特定值(如

    \\x00

    )填充。

    2. 如果IV是

    CryptoJS.lib.WordArray.random(16)

    生成的,则需要在Python中模拟生成16字节随机数,但这通常不可行。需要看IV是否在通信中传递(如密文前16字节作为IV)。

    解密出的字符串开头有乱码,但后面是正确JSON

    IV不正确

    ,导致第一块数据解密错误,但由于CBC模式链式影响,后续块可能“歪打正着”。

    重点检查IV。确认它是否真的是密钥本身。有时IV可能是密钥的MD5哈希值,或者是一个固定字符串。
    解密结果为空或非常短

    填充去除逻辑错误

    。可能不是PKCS7填充,或者是NoPadding(无填充),但密文长度不是16的倍数。

    1. 尝试不解密,直接打印

    decrypted_padded_bytes

    ,看看末尾字节是否有规律(如

    \\x04\\x04\\x04\\x04

    )。

    2. 查看JS代码,确认padding配置。可能是

    CryptoJS.pad.NoPadding

    、

    CryptoJS.pad.ZeroPadding

    等。

    3. 如果是NoPadding,需要确保明文长度原本就是16的倍数,在Python解密后直接解码即可,无需

    unpadding

    。

    能解密但中文是乱码

    字符编码问题

    。JS解密后是UTF-8字节,但Python解码时用了错误编码(如

    decode('gbk')

    )。

    确保最后一步

    decrypted_bytes.decode('utf-8')

    。如果原文不是UTF-8,则需要换成对应的编码(如

    gb2312

    )。

    踩坑心得

    :最让人头疼的不是复杂的算法,而是

    细节不一致

    。JS的

    CryptoJS.enc.Utf8.parse('abc')

    和 Python的

    'abc'.encode('utf-8')

    在99%的情况下是一样的,但如果你遇到的密钥字符串里包含特殊字符或多字节字符,就需要格外小心。另一个大坑是

    时间戳

    。JS的

    Date.now()

    和Python的

    int(time.time()*1000)

    在绝大多数时候是同步的,但如果服务器时间与本地时间有较大偏差,或者密钥使用的是服务器返回的某个特定时间戳(而非本地生成),就会导致密钥对不上。

    务必在JS运行时通过断点拿到“此时此刻”用于加密的真实密钥值,与你的Python脚本生成的密钥进行比对

    ,这是最直接的验证方法。

    5. 进阶思考与策略优化

    成功破解一个加密接口很有成就感,但作为爬虫工程师,我们不能只满足于一次性的成功。我们需要思考更稳健、更高效的策略。

    5.1 密钥的动态性处理

    很多网站的加密密钥是动态变化的,比如每次会话都不同,或者每分钟变化一次。应对策略有:

    • 关联请求

      :密钥可能藏在另一个初始化接口的响应里。你需要先请求接口A拿到密钥

      key

      ,再用这个

      key

      去请求接口B解密数据。

    • 本地计算

      :密钥可能由页面加载时生成的某个随机数(

      nonce

      )或用户令牌(

      token

      )计算得出。你需要把这个计算过程也复现到Python中。

    • 同步时间戳

      :如果密钥是时间戳,要确保你的爬虫服务器时间与目标网站服务器时间基本同步。可以考虑在首次请求时,从响应头里获取服务器时间,并计算本地与服务器的时间差,后续请求根据这个差值来校准本地生成的时间戳。

    5.2 提升逆向效率的工具与技巧

    • 使用自动化工具

      :对于简单的、固定模式的CryptoJS加密,可以尝试使用

      execjs

      库直接在Python中调用原始的JavaScript解密函数,省去复现算法的步骤。但这会稍微影响性能,且依赖Node.js环境。
      import execjs
      with open('decrypt.js', 'r', encoding='utf-8') as f:
      js_code = f.read()
      ctx = execjs.compile(js_code)
      result = ctx.call('decryptData', encrypted_b64, key_str)

    • 关注加密库版本

      :CryptoJS有不同的版本,其API和默认行为可能有细微差别。在JS代码里搜索

      CryptoJS.version

      或查看文件引用路径,有助于确定版本。

    • Hook关键函数

      :对于反调试能力强的网站,可以尝试在控制台提前注入代码,“钩住”(Hook)

      CryptoJS.AES.encrypt

      或

      decrypt

      方法,直接打印出其调用参数和结果,这是最“暴力”但最有效的获取密钥和明文的方式。
      // 在页面JS加载前,在控制台执行
      var _encrypt = CryptoJS.AES.encrypt;
      CryptoJS.AES.encrypt = function(message, key, cfg){
      console.log('AES Encrypt Called:');
      console.log('Message:', message);
      console.log('Key:', key);
      console.log('Cfg:', cfg);
      var result = _encrypt(message, key, cfg);
      console.log('Result:', result.toString());
      return result;
      }

    5.3 关于爬虫道德的再次强调

    在兴奋于技术突破的同时,我们必须时刻牢记爬虫的伦理与法律边界。破解加密接口是为了学习逆向工程技术和应对常见的反爬机制,但绝不能用于:

    • 侵犯个人隐私。
    • 窃取未公开的、受版权保护的数据。
    • 对目标网站进行超出其承受能力的频繁请求,导致服务器瘫痪(DDoS效应)。
    • 绕过付费墙,获取本应付费的内容。

    在编写爬虫时,务必遵守网站的

    robots.txt

    协议,合理设置请求间隔(如添加

    time.sleep(2)

    ),模拟正常用户行为,并使用清晰的User-Agent标识自己。技术是一把双刃剑,用它来学习、研究和在合法合规的范围内获取公开数据,才是长久之道。

    整个破解AES加密接口的过程,就像一场侦探游戏,需要耐心、细心和对细节的执着。从抓包定位到JS逆向,再到Python复现和问题排查,每一步都可能遇到坑。但每当你成功解密,看到原本杂乱无章的密文变成结构清晰的JSON数据时,那种解决问题的成就感是无与伦比的。希望这篇详细的实战指南,能为你下次遇到加密接口时提供清晰的路径和实用的工具。记住,核心永远在于仔细阅读和理解前端JavaScript代码的逻辑,任何一步的想当然都可能导致失败。

    赞(0)
    未经允许不得转载:171主机测评 » 爬虫实战:逆向分析JavaScript中的AES加密接口与Python解密实现
    分享到: 更多 (0)

    评论 抢沙发

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