js逆向实战-有道翻译逆向

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!

思路

1.通过F12观察有道翻译时的请求,找到请求接口
2.查看接口信息,包括request headers,请求参数,返回值
3.如果返回值直接就是明文就方便了,如果不是,查找加密方式进行还原

实现过程

观察接口

打开有道翻译的链接:https://fanyi.youdao.com/index.html#/
按F12
在页面上输入一个文字,点击翻译,查看请求
1

很明显上图中的这个接口就是翻译接口
查看它的传参
2

很明显"i"就是我们需要翻译的词,"sign"是一段密文,"mysticTime"可能是一个时间戳
查看该接口的返回值
3

可以看出,返回值是加密的。

通过观察初步能确定两点
1.sign值需要找到方法去获取
2.返回值需要找到加密方式进行解密

具体过程

sign值逆向获取

明显sign值是通过js生成的,先全局搜索sign,查看是否能找到对应的地方
4
5

css文件肯定不是了,查找js文件中的内容。
点击一个js文件,然后点击花括号进行格式化,方便查看。
在这里插入图片描述
6

运气不错,这个h(t,e)方法就很像是生成sign的方法
通过观察这个O函数也确定了mysticTime确实是时间戳
打上断点,然后点击页面的“翻译” , 程序就运行到断点处卡住
按F11进入h函数
7

可以看到h函数return了一个v函数
正好v函数就在h函数上边,是个获取md5值的方法
现在就看h函数的传参,e是时间戳,t是一个字符串"fsdsogkndfokasodnaso",目前不确定t是否是固定值,需要确定t是固定值还是生成的值
放开断点,换一个单词,从新翻译再次进入h函数,查看t的值是否变化
8

经观察t的值不变,由此确定t是固定值"fsdsogkndfokasodnaso"
可以看出就是对client=KaTeX parse error: Expected 'EOF', got '&' at position 4: {d}&̲mysticTime={e}&product=KaTeX parse error: Expected 'EOF', got '&' at position 4: {u}&̲key={t}这么一个拼接的字符串进行md5加密,其中t是固定值了,e是时间戳,d,u通过观察O函数确定就是接口请求参数的client,product的值,
所以最终待加密的字符串是’client=fanyideskweb&mysticTime=时间戳&product=webfanyi&key=fsdsogkndfokasodnaso’,所以每次进行md5之前替换时间戳即可
下面要看这个v函数返回的md5值有没有进行特殊处理
v函数内容如下:
8

在python中对同一个字符串进行md5,查看最终的值是否与v函数的值相同

import hashlib
ss = 'client=fanyideskweb&mysticTime=1684413806802&product=webfanyi&key=fsdsogkndfokasodnaso'
m = hashlib.md5()
m.update(ss.encode())
print(m.hexdigest())

执行结果:
9

然后在浏览器控制台执行v函数。
10

结果一样,说明没有魔改md5值。

那么请求参数中的sign的值就确定了,下面就是python代码实现过程了
把headers都粘贴过来

headers = {
    "Accept":"application/json, text/plain, */*",
    "Accept-Encoding":"gzip, deflate, br",
    "Accept-Language":"zh-CN,zh;q=0.9",
    "Connection":"keep-alive",
    "Content-Length":"239",
    "Content-Type":"application/x-www-form-urlencoded",
    "Cookie":"OUTFOX_SEARCH_USER_ID=-2143278774@10.105.137.202; OUTFOX_SEARCH_USER_ID_NCOO=1076884992.3280332",
    "Host":"dict.youdao.com",
    "Origin":"https://fanyi.youdao.com",
    "Referer":"https://fanyi.youdao.com/",
    "sec-ch-ua":'"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"',
    "sec-ch-ua-mobile":"?0",
    "sec-ch-ua-platform":"Windows",
    "Sec-Fetch-Dest":"empty",
    "Sec-Fetch-Mode":"cors",
    "Sec-Fetch-Site":"same-site",
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
}

base_url = 'https://dict.youdao.com/webtranslate'

定义一个函数生成请求参数,word是待翻译字符串,sign是获取的md5值,tamp是毫秒级时间戳

def set_request_data(word,sign,tamp):
    return {
        "i":word,
        "from":"auto",
        "to":"",
        "dictResult":"true",
        "keyid":"webfanyi",
        "sign":sign,
        "client":"fanyideskweb",
        "product":"webfanyi",
        "appVersion":"1.0.0",
        "vendor":"web",
        "pointParam":"client,mysticTime,product",
        "mysticTime":tamp,
        "keyfrom":"fanyi.web",
    }

发送请求查看是否能正常获取相应
11

可以正常获取相应结果,下一步就是解密响应结果了

返回值解密

观察返回值:

Z21kD9ZK1ke6ugku2ccWu-MeDWh3z252xRTQv-wZ6jddVo3tJLe7gIXz4PyxGl73nSfLAADyElSjjvrYdCvEP4pfohVVEX1DxoI0yhm36ytQNvu-WLU94qULZQ72aml6JKK7ArS9fJXAcsG7ufBIE0gd6fbnhFcsGmdXspZe-8whVFbRB_8Fc9JlMHh8DDXnskDhGfEscN_rfi-A-AHB3F9Vets82vIYpkGNaJOft_JA-m5cGEjo-UNRDDpkTz_NIAvo5PbATpkh7PSna2tHcE6Hou9GBtPLB67vjScwplB96-zqZKXJJEzU5HGF0oPDY_weAkXArzXyGLBPXFCnn_IWJDkGD4vqBQQAh2n52f48GD_cb-PSCT_8b-ESsKUI9NJa11XsdaUZxAc8TzrYnXwdcQbtl_kZGKhS6_rCtuNEBouA_lvM2CbS7TTtV2U4zVmJKpp-c6nt3yZePK3Av01GWn1pH_3sZbaPEx8DUjSbdp4i4iK-Mj4p2HPoph67DR7B9MFETYku_28SgP9xsKRRvFH4aHBHESWX4FDbwaU=

看起来像为了url安全的base64编码,尝试用python的base64.urlsafe_b64decode解码
12

解析出来后是乱码,说明应该还有加密
继续打断点,跟踪请求的返回值
13

很明显y是发送请求了,那么查看调用栈中y的上一层,也就是ln,查看其中是否有对返回值的处理
14

可以看到then()是对返回值进行处理
在这打上断点,继续跟踪
15

可以看到decodeData函数就是解密函数,传入的第一个参数o就是接口返回的加密字符串
跟踪进来后可以看到该函数
16

可以看到这是一个aes加密,用的cbc模式,有密钥和偏移量,那么先确定a和c的值
跟踪g函数,发现还是个md5
17

继续跟踪alloc函数
18

继续跟
19

发现a和c就是o和n的md5摘要值(byte不是十六进字符串)
变换不同的单词进行翻译,查看这两个值会不会变化
20

发现这两个值不会变化,那么python代码中直接用这两个值就可以了

decodeKey = 'ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl'
decodeIv = 'ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4'
password = get_md5_bytes(decodeKey)
iv = get_md5_bytes(decodeIv)
aes = AES.new(password,AES.MODE_CBC,iv)
decode_bytes = aes.decrypt(res_str)
print(decode_bytes)
print(decode_bytes.decode())

最后结果如下:
21

发现啊 结尾有很多\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c,多还几个单词尝试后发现最后多出来的字符都不一样。

这就涉及到了AES加解密的填充模式,这是因为在AES标准规范中,分组长度只能是128位,也就是说,每个分组为16个字节。这就导致待加密的明文,字节长度不够16字节或者16字节倍数的时候需要进行补全填充,对密文解密后又需要去掉填充的字符。
可以使用Crypto.Util.Padding里提供的方法:

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad,pad
text = pad(text, AES.block_size) # 填充
text = unpad(text, AES.block_size) # 去填充

这样就没有最后的填充内容了
最后就是获取translateResult的结果就可以了

优化一下代码,最终如下

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import requests
import json
import time
import base64
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

# 直接复制的浏览器requestheader
headers = {
    "Accept":"application/json, text/plain, */*",
    "Accept-Encoding":"gzip, deflate, br",
    "Accept-Language":"zh-CN,zh;q=0.9",
    "Connection":"keep-alive",
    "Content-Length":"239",
    "Content-Type":"application/x-www-form-urlencoded",
    "Cookie":"OUTFOX_SEARCH_USER_ID=-2143278774@10.105.137.202; OUTFOX_SEARCH_USER_ID_NCOO=1076884992.3280332",
    "Host":"dict.youdao.com",
    "Origin":"https://fanyi.youdao.com",
    "Referer":"https://fanyi.youdao.com/",
    "sec-ch-ua":'"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"',
    "sec-ch-ua-mobile":"?0",
    "sec-ch-ua-platform":"Windows",
    "Sec-Fetch-Dest":"empty",
    "Sec-Fetch-Mode":"cors",
    "Sec-Fetch-Site":"same-site",
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
}
# 请求地址
base_url = 'https://dict.youdao.com/webtranslate'


def get_md5_hexstr(ss):
    """
    获取字符串的md5摘要,返回的摘要值是16进制字符串
    :param ss: 字符串
    :return: 16进制字符串
    """
    m = hashlib.md5()
    m.update(ss.encode())
    return m.hexdigest()


def get_md5_bytes(ss):
    """
    获取字符串的md5摘要,返回的摘要值是bytes
    :param ss: 字符串
    :return: bytes
    """
    m = hashlib.md5()
    m.update(ss.encode())
    return m.digest()


def set_request_data(word,sign,tamp):
    """
    设置请求参数
    :param word: 待翻译对象
    :param sign: sign值
    :param tamp: 时间戳
    :return: 设置好的请求参数
    """
    return {
        "i":word,
        "from":"auto",
        "to":"",
        "dictResult":"true",
        "keyid":"webfanyi",
        "sign":sign,
        "client":"fanyideskweb",
        "product":"webfanyi",
        "appVersion":"1.0.0",
        "vendor":"web",
        "pointParam":"client,mysticTime,product",
        "mysticTime":tamp,
        "keyfrom":"fanyi.web",
    }


def get_aes_decrypt_cbc(res_bytes):
    """
    AES解密 使用CBC模式
    :param res_bytes: 
    :return: 
    """
    # js逆向获取到的密钥和偏移量字符串
    decodeKey = 'ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl'
    decodeIv = 'ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4'
    # 对密钥和偏移量字符串进行md5,获取bytes当作AES的密钥和偏移量
    password = get_md5_bytes(decodeKey)
    iv = get_md5_bytes(decodeIv)
    aes = AES.new(password,AES.MODE_CBC,iv)
    # 对入参进行解密
    decode_bytes = aes.decrypt(res_bytes)

    json_str = unpad(decode_bytes, 16).decode().strip() # AES加解密涉及到补位,因此需要去掉补位字符

    # 将解密结果转成字典
    result = json.loads(json_str)
    # 经过抓包发现如果翻译的结果比较长的话,会分成多个元素放到列表中
    result_len = len(result['translateResult'][0])
    # 如果翻译结果很短,只有一个内容则直接输出
    if result_len == 1:
        print(f"翻译后的内容是:{result['translateResult'][0][0]['tgt']}")
    else:
        # 如果翻译内容有很多 需要逐个获取结果进行拼接
        # 其实可以不考虑长度直接for循环,但是如果只有一个内容其实没必要让它走个for循环
        result_list = []
        for i in result['translateResult'][0]:
            data = i['tgt']
            result_list.append(data)
        print(f"翻译后的内容是:{''.join(result_list)}")
    print(f"翻译模式是:{result['type']}")



if __name__ == '__main__':
    tamp_ms = int(time.time() * 1000)
    s = f'client=fanyideskweb&mysticTime={tamp_ms}&product=webfanyi&key=fsdsogkndfokasodnaso'
    sign = get_md5_hexstr(s)
    word = "Southern Africarly African American: Jumping the Broom In the times of slavery in this country, African American couples were not allowed to formally marry and live together. To make a public declaration of their love and commitment, a man and woman jumped over a broom into matrimony, to the beat of drums. The broom has long held significant meaning for the various Africans, symbolizing, the start of home - making for the newlywed couple. In Southern Africa, the day after the wedding, the bride assisted the other women in the family in sweeping the courtyard, indicating her dutiful willing ness to help her in-laws with housework till the newlyweds could move to their new home. Some African-American couples today are choosing to include this symbolic rite in their wedding ceremony"
    resp = requests.post(url=base_url, data=set_request_data(word, sign, tamp_ms), headers=headers)
    print(resp.status_code)
    # print(resp.text)
    res_bytes = base64.urlsafe_b64decode(resp.text)
    print(f"要翻译的内容是:{word}")
    get_aes_decrypt_cbc(res_bytes)
  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值