美文网首页大数据 爬虫Python AI Sql
Python爬虫:JS逆向初学习

Python爬虫:JS逆向初学习

作者: 东东隆东抢 | 来源:发表于2024-03-03 17:32 被阅读0次

    在进行Python爬虫开发时,遇到需要从Youdao翻译网站抓取数据的情况,由于此翻译网站对其API请求进行了加密,并且对返回的数据也采用了加密措施,因此直接的HTTP请求抓取不能直接获取到翻译结果。这就需要了解其加密和解密机制,进而在Python代码中模拟这一过程,以实现数据的有效抓取和解密。

    以下是一个实践过程的描述:首先,访问网站,使用鼠标右键打开开发者工具,并切换到网络(Network)面板,然后选择Fetch/XHR过滤器。在翻译输入框中输入待翻译的文本,例如中午好,回车执行翻译操作。在这个过程中,可以清晰地观察到“中午好”被翻译为“good afternoon”的过程涉及了三个网络接口请求:

    1. https://dict.youdao.com/webtranslate/keyGET请求;
    2. https://dict.youdao.com/webtranslatehttps://dict.youdao.com/keyword/keyPOST请求。

    第一个接口的请求载荷(payload)如下,有些非必须,可尝试去除:

    keyid: webfanyi-key-getter
    sign: b7150d775d0039168fb116052f1f38ad
    client: fanyideskweb
    product: webfanyi
    appVersion: 1.0.0
    vendor: web
    pointParam: client,mysticTime,product
    mysticTime: 1709517202639
    keyfrom: fanyi.web
    mid: 1
    screen: 1
    model: 1
    network: wifi
    abtest: 0
    yduuid: abcdefg
    

    第二个webtranslate接口里的请求载荷如下:

    i: 中午好
    from: zh-CHS
    to: en
    domain: 0
    dictResult: true
    keyid: webfanyi
    sign: d08172c36481bbce6f1a2bb159ebc981
    client: fanyideskweb
    product: webfanyi
    appVersion: 1.0.0
    vendor: web
    pointParam: client,mysticTime,product
    mysticTime: 1709517203277
    keyfrom: fanyi.web
    mid: 1
    screen: 1
    model: 1
    network: wifi
    abtest: 0
    yduuid: abcdefg
    

    多次刷新,观察到在两个接口请求的载荷中,变化的参数主要是signmysticTime,其中mysticTime代表的是时间戳,sign`看起来像是经过某种加密算法处理的结果。

    为了深入了解sign参数的生成机制,可以采取全局搜索的方式,在浏览器开发者工具中通过快捷键Shift+Ctrl+F打开全局搜索功能,输入sign进行搜索。在搜索结果中,可能会出现大量与sign相关的匹配项,这时需要细心地筛选,寻找与sign生成逻辑相关的代码片段。

    通过这种方法,即便面对众多的搜索结果,也能有目的地缩小范围,逐步接近用于生成sign值的加密算法的实现代码。这个过程需要耐心和一定的运气,因为正确的代码片段可能隐藏在大量的匹配结果之中。找到这些关键代码后,就可以进一步分析其逻辑,理解sign是如何根据时间戳mysticTime以及可能的其他因素生成的。


    当在浏览器的开发者工具中全局搜索sign参数并注意到有key:value的组合形式,如sign: k(o,e),这表明你可能已经找到了生成sign值的关键代码片段。这个k(o,e)很可能是一个函数调用,其中k是一个函数,而oe是传入该函数的参数,这个函数负责生成sign的值。

    从JS代码中可得,k(o,e)方法里,o参数是时间戳,easdjnjfenknafdfsdfsd,暂时不确定其是否固定。
    client=${u}&mysticTime=${e}&product=${d}&key=${t},e是时间戳,ud是常量,tasdjnjfenknafdfsdfsd

      const u = "fanyideskweb"
                  , d = "webfanyi"
    

    j函数主要用于进行MD5加密,并将加密结果转换为十六进制(hex)格式。

     function j(e) {
                    return c.a.createHash("md5").update(e.toString()).digest("hex")
                }
    

    使用快捷键F8继续执行脚本,发现再次跑到断点这里,又执行了一次k函数,但是key对应的t值发生了变化,此时为fsdsogkndfokasodnaso


    看起来两个接口的sign值都是通过同一个函数k生成的,但关键在于它们使用key不同。第一次请求时使用的key是asdjnjfenknafdfsdfsd,而第二次请求使用的key是fsdsogkndfokasodnaso,而且这个第二次使用的key是从第一次接口请求的返回数据中获得的。

    翻译结果返回的是密文,这意味着翻译服务还采用了某种形式的响应加密。这是一个额外的安全措施,用于保护数据在传输过程中的安全,防止未经授权的访问者直接读取响应内容。要解密这些响应,需要了解加密和解密的具体机制。
    Z21kD9ZK1ke6ugku2ccWu4n6eLnvoDT0YgGi0y3g-v0B9sYqg8L9D6UERNozYOHqqXyAEo6co8ruGELvtq19adBTgmgtq9XKmTb3RUrbqN9QTNj_RBof8RxaKuaSRS63DlaZVeSgjC6HDrIjQM2yVqVOY1GtO-Re0xcRZML_FmM_6JKN9W6IDSn4K_5-Kfx3SUOxAZ90lJG8iBReRkH8OxCAPaKK2lG6DJlyoHkMHul1MJiAWkni2JX_FiRkypw7KdwvveOaJYsrwRQEIt2GJq8QjqNC8r2oluEzx36x0V20Pdj1HUleZ4uH0-AU8xNW2OmAnLOC7limxtYMKzdwx6GJz0ZqqEmhrmnMw-x1Xz2CFQ4XSJ09L1fsDYsX6uoidgRIq3CWRXIWkBh_9I0EA2D-hhk8m5JYOdLYPY3Pb5ncayIPXGfwFvdkooQYQuO41tfBeOitzdU0cz2z4g6_4A==
    

    有点懵,因为响应结果中并没有关键字,难以使用关键字去搜索。此时蓦然回首,会发现第一个接口请求返回的结果:



    在此接口请求响应中发现了aesIv和aesKey这样的关键字,这是否暗示了可能使用了AES加密算法呢?在AES加密中,aesKey用作加密和解密的密钥,而aesIv(初始化向量)用于确保即使多次使用相同的密钥加密相同的文本,加密结果也会不同,从而增加了加密数据的安全性。

    进行全局搜索aes关键字是一个直接且有效的方法来寻找解密逻辑的实现代码,容易发现:


    直接双击进入源码,打上断点单步调试:
     function y(e) {
                    return c.a.createHash("md5").update(e).digest() #进行MD5加密
                }
    

    从上图中,可看出R函数中的t就是翻译结果的加密数据,on就是第一次请求返回的aesKeyaesIv
    aes-128-cbc这个加密算法,我也不懂,直接借助于chatgpt,让其给出的解密示例:

    const crypto = require('crypto');
    
    // 你的初始化向量 (IV)
    const iv = '1234567812345678';
    
    // 你的密钥 (Key)
    const key = '1234567812345678';
    
    // 加密后的数据 (CipherText),这里使用的是Base64编码的字符串示例
    const cipherText = '加密数据的Base64编码字符串';
    
    // 创建一个解密器实例
    const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
    
    // 将加密数据转换为解密后的明文,使用了'base64'作为输入编码,'utf8'作为输出编码
    let decrypted = decipher.update(cipherText, 'base64', 'utf8');
    
    // 最后调用final方法完成解密过程,并获取剩余的解密内容
    decrypted += decipher.final('utf8');
    
    console.log(decrypted);
    

    仔细对照一下并发现,a Unit8Arry(16)就是密钥,iUnit8Arry(16)是初始化向量。创建有道翻译.js文件,粘贴、改写源码中抠出来的JS代码:

    var crypto = require('crypto');//内置模块
    
    // 第一次请求,key1是'asdjnjfenknafdfsdfsd',
    // 第二次请求,key2是'fsdsogkndfokasodnaso'
    
    const u = 'fanyideskweb';
    const d = 'webfanyi';
    
    const key = 'asdjnjfenknafdfsdfsd'
    
    function j(e) {
        return crypto.createHash("md5").update(e.toString()).digest("hex")
    }
    
    function k(time,key) {
        return j(`client=${u}&mysticTime=${time}&product=${d}&key=${key}`)
    }
    
    
    function y(e) {
        return crypto.createHash("md5").update(e).digest()
    }
    
    
    function aes_decrypt(cipherText,aesIv,aesKey) {
    
        const uint8Array_iv  = new Uint8Array(Buffer.from(y(aesIv)));
    
        const uint8Array_key = new Uint8Array(Buffer.from(y(aesKey)));
    
        // 创建一个解密器实例
        const decipher = crypto.createDecipheriv('aes-128-cbc', uint8Array_key, uint8Array_iv);
    
        // 将加密数据转换为解密后的明文,使用了'base64'作为输入编码,'utf8'作为输出编码
        let decrypted = decipher.update(cipherText, 'base64', 'utf8');
    
        // 最后调用final方法完成解密过程,并获取剩余的解密内容
        decrypted += decipher.final('utf8');
    
        return decrypted
    
    }
    

    Python代码如下:

    import json
    import time
    from random import uniform
    import requests
    import execjs
    
    class YouDaoTranslate:
        def __init__(self):
            self.session = requests.Session()
            self.session.headers.update({
                'Referer': 'https://fanyi.youdao.com/',
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
            })
    
            self.key_get_url = 'https://dict.youdao.com/webtranslate/key'
            self.translate_url = 'https://dict.youdao.com/webtranslate'
            self.get_cookie_url = 'https://rlogs.youdao.com/rlog.php?'
    
            self.get_cookie()
    
        def get_cookie(self):
            params = {
                "_npid": "fanyiweb",
                "_ncat": "event",
                "_ncoo": str(2147483647 * uniform(0, 1)),
                "nssn": "NULL",
                "_ntms": str(int(time.time() * 1000)),
            }
            try:
                self.session.get(self.get_cookie_url, params=params)
            except requests.RequestException as e:
                print(f"Error getting cookies: {e}")
    
        def get_sign(self, key='asdjnjfenknafdfsdfsd'):
            try:
                with open('有道翻译.js', 'r', encoding='utf-8') as f:
                    context = execjs.compile(f.read())
                current_time = str(int(time.time() * 1000))
                sign = context.call('k', current_time, key)
                return current_time, sign
            except Exception as e:
                print(f"Error generating sign: {e}")
                return None, None
    
        def get_keys(self):
            current_time, sign = self.get_sign()
            if current_time and sign:
                params = {
                    'keyid': 'webfanyi-key-getter',
                    'sign': sign,
                    'client': 'fanyideskweb',
                    'product': 'webfanyi',
                    'pointParam': 'client,mysticTime,product',
                    'mysticTime': current_time,
                }
                try:
                    response = self.session.get(self.key_get_url, params=params).json()
                    return response['data']['secretKey'], response['data']['aesKey'], response['data']['aesIv']
                except requests.RequestException as e:
                    print(f"Error getting keys: {e}")
            return None, None, None
    
        def get_translate_data(self, translate_text, cur_time, sign):
            data = {
                'i': translate_text,
                'keyid': 'webfanyi',
                'sign': sign,
                'client': 'fanyideskweb',
                'product': 'webfanyi',
                'appVersion': '1.0.0',
                'vendor': 'web',
                'pointParam': 'client,mysticTime,product',
                'mysticTime': cur_time,
                'keyfrom': 'fanyi.web'
            }
            try:
                response = self.session.post(self.translate_url, data=data).text
                return response
            except requests.RequestException as e:
                print(f"Error getting translation data: {e}")
                return None
    
        def main(self, translate_text):
            secretKey, aesKey, aesIv = self.get_keys()
            if secretKey and aesKey and aesIv:
                current_time, sign = self.get_sign(key=secretKey)
                cipherText = self.get_translate_data(translate_text, current_time, sign)
                if cipherText:
                    try:
                        with open('有道翻译.js', 'r', encoding='utf-8') as f:
                            context = execjs.compile(f.read())
                        result = context.call('aes_decrypt', cipherText, aesIv, aesKey)
                        translated_text = json.loads(result)['translateResult'][0][0]['tgt']
                        print(f'[{translate_text}]翻译的结果是:{translated_text}')
                    except Exception as e:
                        print(f"Error decrypting translation: {e}")
    
    if __name__ == '__main__':
        YouDaoTranslate().main('中午好')
    
    
    

    执行结果:[中午好]翻译的结果是:good afternoon.
    假设有成百上千个文本需要翻译,可使用进程池按此方法进行快速翻译。改天使用chatgpt随机生成100个复杂单词进行验证一下。

    相关文章

      网友评论

        本文标题:Python爬虫:JS逆向初学习

        本文链接:https://www.haomeiwen.com/subject/egzkzdtx.html