JS逆向有道翻译&wxPython制作便宜翻译器
该文章讲述了如何逆向有道翻译,并且使用wxPython库来制作一个便宜翻译器。
事先说明:
- 本文仅供学习交流,严禁用于商业用途,否则后果自负!
- 本文仅供学习交流,严禁用于商业用途,否则后果自负!
- 本文仅供学习交流,严禁用于商业用途,否则后果自负!
- 你要先有Python、requests库和一定的JS基础
- 你要先知道如何使用网页开发者工具
逆向有道翻译
我们首要知道的是有道翻译客户端是如何发送请求给服务器的,才能知道如何逆向它。
随便在有道翻译上输入一个单词,再在网络选项卡里翻找,你会发现有一个请求是这样的:
该请求发送的表单数据中赫然出现了我们输入的单词,这就是我们要找的东西了。
再来看看这个请求的响应:
嗯……是一大串加密过的东西,完全看不懂捏。解密的事情之后再说,我们先来想:要如何发出请求才能得到类似的响应呢?
重新来看该请求的表单数据,参数i
顾名思义便是我们输入的单词的明文,而参数mysticTime
一眼就能看出是一个时间戳。我们可以此时再输入一个单词,来看看表单数据的哪些参数是不变的、哪些是变化的。
除了时间戳外,还有个参数sign
也是动态的。也就是说整个请求中,仅有sign
和mysticTime
是动态的。又因为mysticTime
是一个时间戳,所以我们只需要关注、逆向sign
就行了。
既然客户端需要发送请求给服务器,那自然就需要一个加密算法来加密sign
、便于服务器解密。我们可以在网页开发者工具中找到这个加密算法。有道翻译还是很厚道的,一搜索“sign”这个字眼就能找到客户端封装表单数据的函数:
是不是很熟悉?这个函数中的每个变量都能在“webtranslate”这个请求的表单数据里找到。
让我们来分析一下这个函数:
o
是一个时间戳sign
值由k(o, e)
得到,o
是时间戳、e
我们暂且不知道
再来看看k
函数,还真别说,就在E
函数的上面:
1 | function k(e, t) { |
需要注意一点,
sign: k(o, e)
中o
是时间戳、e
是不知名的变量;而到k
函数中,e
是时间戳,t
才是不知名的变量。
切记不要搞混了!
能看出,被传入k
函数中的不知名的变量实际上是key,并且还多了两个不知名的变量d
和u
。我们可以直接打断点,看看这些变量的值:
再走一步我们便能看到k
函数的两个参数。e
属实是时间戳,千万不要混淆了!
停在该断点,我们可以看到d
和u
的值:
相当于在k
函数中,会生成这么一个字符串:client=fanyideskweb&mysticTime=${t}&product=webfanyi&key=fsdsogkndfokasodnaso
,仅有时间戳是动态,我们无脑塞时间戳就完事了。
但是!这个字符串被生成后,还会被传入j
函数进行加密,所以我们还需要看看j
函数的内容:
1 | function j(e) { |
嗯……无比熟悉有木有,这就是MD5加密算法;j
函数把k
函数拼凑的字符串进行了MD5加密(注意是hex格式的),然后返回加密后的结果。
继续走两步,E
函数到了尾声,果断打开控制台查询一下sign
的值,正是加密后的内容,而不是一串明文:
继续码字的时候页面被刷新了,所以sign
值和用上面的时间戳以及key加密出来的结果不同,懒得再截图了,大家自己试试吧。
至此我们已经搞清楚了有道翻译客户端是拿着什么参数去请求服务器的,接下来我们可以用Python来模拟这个请求。
Python模拟请求
比起直接用requests.post()
,我们这次要使用requests.session.post()
,否则不会成功;headers
和data
之后再怼:
1 | session = requests.session() |
headers
的内容可以直接从开发者工具那边复制,别忘了加上Referer
。
data
的内容我们需要模拟一下:
1 | # 时间戳 |
接着来看一下响应的内容~
居然是一大串看不懂的东西?!当然,相信大家看到这一大串中结尾的=
时,应该就知道这是base64编码了。我们可以用Python来解码,不过还要插一句题外话:这个是base64变种,叫做URL-safe base64,它把+
和/
换成了-
和_
。这个变种的诞生是为了让base64编码后的内容能够安全地放在URL中。
简单来说就是:我们还需要把-
和_
换回来才能解码:
1 | res = res.replace('-', '+').replace('_', '/') |
再次运行,好家伙,得到了一串更过分的字节。
解密
既然有道翻译客户端已经准备好了表单数据,那证明这之后它就会发送请求给服务器,我们且看客户端是如何发送请求、又如何处理响应的。
继续往下走一步,我们就能看到客户端发送请求的代码了:
中间的链接是不是很熟悉?再看看右边的E(t)
,这正是会返回sign
值的E
函数。再用控制台一一检查过去,会发现老面孔和新面孔:
(e,t)
是key值a["d"]
返回一个Promise
对象,等下细说e
是表单数据的一部分,里面包含了我们要翻译的单词的明文n["a"]
可以不深究,你只需要知道它可以把多个对象合并成一个。事实上,它的作用就是把e
和E(t)
返回的两个表单数据合并成一个
这里说一下a["d"]
,它的结构是这样的:
1 | function l(e, t, o) { |
e
是请求的链接t
是表单数据o
是请求头,也就是上上图中那个孤零零的"Content-Type": "application/x-www-form-urlencoded"
如果你不了解
Promise
,那么你可以把它理解成一个异步函数,它的作用是等待a["a"].post(e, t, o)
这个请求完成,然后返回响应的内容。
如果这个请求成功了,那么e
就是响应的内容,给到n()
;如果这个请求失败了,那么e
就是错误信息,给到c()
。
n()
和c()
的内容都是空函数,所以我们不需要管它们。
那么a["d"]
返回的Promise
对象去了哪里咧?回到客户端发送请求的那一步(也就是上上图的I = (e,t)=>略略略
),看开发者工具的调用堆栈。
调用堆栈:在开发者工具中你可以看到每个函数被调用的顺序。
点击Po
便会自动引导到这个函数的定义处:
Mo["a"].getTextTranslateResult()
正是函数I
!
刚才提到的a["d"]
所返回的Promise
对象去了此处的Mo["a"].getTextTranslateResult()
,服务器成功响应,走到then
,也就是我们解密的关键:有道翻译客户端对这个响应做了什么处理?
我们来看一下then
中的代码:
1 | .then(o=>{ |
Mo["a"].cancelLastGpt()
不需要管,感兴趣的可以自己去看看const n = Mo["a"].decodeData...略
才是我们要看的,decodeData
,是不是立即就明白这行代码的作用了?
有道翻译客户端使用了Ko["a"].state.text.decodeKey
和Ko["a"].state.text.decodeIv
来解密响应的内容。Key和Iv,好好好,这不就是AES加密算法吗?!
打开控制台打印一下这两个变量的值:
万事俱备,只欠东风,让我们最后看一眼Mo["a"].decodeData()
函数:
- 接收
t
、o
、n
三个参数,分别代表了base64编码后的数据、用于解密的key和用于解密的iv
- 分别将解密用的key和iv转化为16进制的字节
- 用key和iv创建一个AES解密器
r
- 用解密器
r
解密base64编码后的数据t
,得到JSON数据,也就是原始的响应数据 - 最终返回明文
搞清楚逻辑后我们便可以继续用Python解密:
1 | def digest_key(value): |
得到的结果:
制作翻译器
经过以上逆向过程,我们还可以制作一个翻译器,让它能够翻译我们输入的单词。
首先我们需要一个GUI库,这里我选择了wxPython。安装方法:
1 | pip install wxPython |
其次我们需要定义一个窗口类,该类继承自wx.Frame
:
1 | class MyFrame(wx.Frame): |
运行一下,布局长这样:
用户需要在左侧输入单词、点击按钮,右侧才能够显示翻译的结果。因此我们还需要给按钮绑定一个事件:
1 | # ...继def __init__()的内容... |
接下来的内容我相信大家都心知肚明,正是上述的所有内容的结合体。我把那些代码封装成了函数塞在按钮事件中,大家则可以自己动动手、试试看(绝对不是因为我懒)。
1 | # 继def on_button_click()的内容 |
完工!