【python 逆向分析某有道翻译】分析有道翻译公开的密文内容,webpack类型,全程扣代码,最后实现接口调用翻译,仅供学习参考
文章日期:2024.12.24
使用工具:Python,Node.js
逆向类型:webpack类型
本章知识:sign模拟生成,密文的解密(webpack),全程扣代码,仅供学习参考
文章难度:低等(没难度)
文章全程已做去敏处理!!! 【需要做的可联系我】
AES解密处理(直接解密即可)(crypto-js.js 标准算法):在线AES加解密工具
仅供学习!!仅供测试!!仅供参考!!
声明:本文逆向的所有内容文件仅供学习,从网站扣的代码不会公开,但博主自己写的代码内容将会公开在文章最底部,望谅解!!!
先看成品图,效果就是这个样子,可以直接翻译,支持多种不同语言(没有试过高并发)
给大家提前讲一下,比较简单的内容我将会快速讲解,不会很细,小白可能会不懂,不懂只能去看我之前的逆向文章,或者留言都可以,有志向的人,一起学习发展。
1、打开某某网站(使用文章开头的AES在线工具解密):
OI1Y7IeHDHWVFvw4Yo05CnrGO+zxG4b5OFpdZvnuG2WEWj/TsHjdLb2bzhvR/PvI
2、【sign】我们打开页面后随便输入中文进行翻译,然后找到刻意的接口,我们打开看一下,没想到一上来就有个sign,而且返回的数据还是密文
3、【sign】别着急,我们把这个接口的请求先转移到python文件内,然后经过我不断的测试,发现他的cookie必须要加【OUTFOX_SEARCH_USER_ID】参数,而且这个参数是通过服务器返回的,而且有效期为20年,我决定直接生成,我从来没见过一个cookie有效期为20年的,纯扯淡,他一定不是在服务器存储着,所以我们直接随机生成即可
其次是载荷data里【from】【to】这两个参数我就不多介绍了,看注释即可。
剩下的就是sign和一个时间戳,直接全局搜索
4、【sign】全局搜索接口的链接路径,发现搜索到了两个,直接点进去都打上断点,然后再次触发翻译
5、【sign】断住了,看图说话,直接跟进去。
6、【sign】跟进来后发现了sign,而且加密方法就在头上。这就不用多说了,口算就能知道是md5加密。
7、【sign】直接用 python 模拟出来,标准的MD5,就不用讲太多了
8、【密钥】接下来看一下那个密钥,这个也不用多说,他是另一个请求返回的,而且这个请求里也有一个sign,用的也是md5加密,只是加密的内容有点不一样,直接看结果吧,没什么难度
9、【代码整理】我将代码进一步整合,方便下一步的调试
10、【解密密文】接下来重点来了,解密返回的密文,本次全程扣代码,文章结尾我将会附上模拟解密的代码,并非扣下来的代码,供大家参考学习
注意:文章结尾的代码并非官网扣下来的,放心测试学习!!!
11、遇到这种密文解密其实也没多难,我接下来用两种方法帮大家找到这个加密位置。
12、【找加密位置1】我们找到返回密文的接口,复制一下链接路径,然后ctrl+shift+f全局搜索,搜索到后,对第二个打上断点,第一个不用打断点,因为第一个很明显,他的链接路径和我要找到不一样,直接排除
13、【找加密位置1】我们打上断点后,随便输入内容使其触发翻译,然后会自动断住,这时候我们就一步一步跟进
14、【找加密位置1】大约跟进了5次左右,看到了一个加密方法,虽然目前没有看到密文在此处解密,但我的职业素养告诉我,“有疑问?直接打上断点好了”,记住,做逆向,只要看到疑似加解密的位置,直接打断点就好,这是找解密位置或加密位置的一种方法。我们直接打上断点,然会放开运行
14、【找加密位置2】使用HOOK大法(配合油猴插件会更方便好用),将HOOK代码直接在控制台执行,然会再次输入内容触发翻译,你会发现断住了(HOOK代码在文章结尾)
HOOK原理:首先我们要知道他解密密文要做什么事情,像这种一般都是字典传输,那么他在解密内容后,一定会执行【JSON.parse】,将字符串的字典转为格式化后的字典,如果他不执行转换,那他就读取不了内容,所以我们用这个HOOK代码进行拦截,相当于这个HOOK代码就是中间商,你所做的事情要经过这个中间商后,这个中间商在觉定是拦截还是放行,这个中间商把别人替代了,所以你不得不经过他,这就是大概的HOOK的原理(有些场景的HOOK需要搭配油猴)
15、【找加密位置2】我们一边放行一遍观察他的内容参数,才走了几步就看到了解密后的密文数据,我们在堆栈里看他上一步的操作,看看都干了什么,点击上一个堆栈,嘿,成功找到加密位置。
16、【解密密文】接下来我们开始解密,我们将他的解密函数在控制台运行,他会自动暴露源函数,这时我们点进去
17、【解密密文】点击进来后,可以看到他解密的方法,我们直接转移到本地js文件内,然会尝试运行看缺少什么,发现缺少【l】函数,我们直接去源代码里找
18、【解密密文】找到了,直接看图,直接复制到文件内,然后再次运行,发现缺少a函数,我们直接去找
19、【解密密文】我们对当时找到的参数位置的a打上断点,然后刷新页面,会发现断住了,然后我们控制台输出一下,然后点击进去,会发现他是一个webpack加载器文件,直接把当前js全部复制到js内
20、【解密密文】复制到了本地js文件内,运行后发现缺少两个环境,我直接补上了,就不细讲了。然后我看出来这个加载器的主函数是a,所以直接把他的主运行函数导出全局,让外部可以访问,既然a已经被导出为jzq了,那么下面的两个函数也有修改一下,都改为jzq即可,然后再次运行看看缺少什么
21、【解密密文】运行发现报错了,找到报错的行,然后打印一下他运行什么导致的报错,打印结果发现他缺少方法,加载器没有找到这个方法,无法调用,那我们直接去找
22、【解密密文】我们回到浏览器控制台,运行一下图片里的函数,然后随便打开任意字段,点击链接进到加载器的函数里
23、【解密密文】进来后,我们直接搜索缺少的方法,找到后,直接放入到本地js里,可以让加载器读取,但要注意了,看下面第二张图,你的加载器需要修改一下代码,否则他不会读取你导入的方法
24、【解密密文】这次运行成功了,他已经能正常读取到91565了,但他缺少另一个方法,我们继续去搜索,一种重复这种方法。由于太多,我就不一个一个尝试了,我直接把全部都复制过来
25、【解密密文】我把所有的都复制过来,运行后发现没有问题了,成功解密了。一般情况不要无脑全部复制,这会导致卡顿,具体怎么复制还是根据大家的个人习惯
26、【全部代码整理】直接看成品
注意:没有用扣下来的代码做解密是因为文件量太大,而且扣代码是教程并不是一定要用扣下来的代码,要懂得变通
【AES-128-CBC.js】 js文件,用于解密,扣下来的代码不能用于公开,只能给大家展示模拟出来的源代码
// 导入 crypto 模块
const crypto = require('crypto');
// 安装 express 服务包
// npm install express --save
const express = require('express');
const app = express();
app.use(express.json());
// AES-128-CBC 解密函数
function aesDecrypt(ciphertext, key, iv) {
try {
// 创建解密器
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
// 执行解密操作
let decrypted = decipher.update(ciphertext, "base64", "utf-8");
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
console.error('解密失败:', error);
}
}
function generateMD5Hash(input) {
// 创建 MD5 哈希
const hash = crypto.createHash('md5');
// 更新哈希对象内容
hash.update(input);
// 输出结果为 Buffer 对象
return hash.digest(); // 默认是二进制 Buffer 格式
}
app.post('/', (req, res) => {
const {text, key, iv} = req.body;
decrypt_data = aesDecrypt(text,generateMD5Hash(key),generateMD5Hash(iv))
console.log('【解密结果】 - ',decrypt_data)
res.status(200).json({code: 1, data: JSON.parse(decrypt_data)});
});
// 检测服务是否已打开
app.get('/run', (req, res) => {
res.status(200).json({code: 1});
});
app.listen(4000, () => {
console.log('Node.js 服务监听端口 127.0.0.1:4000');
});
【翻译请求.py】运行此代码前,请先运行js文件,js文件运行后会启动接口,python要调用接口实现解密
import json
import random
import requests
import time
import hashlib
import base64
# base64 解密
def base64_decode(str):
# print(base64.b64decode('MTIz'.encode()))
return base64.b64decode(str).decode('utf-8')
# 这个是链接被base64加密了,此步骤只用于去敏,无其他作用
URL = base64_decode('aHR0cHM6Ly9kaWN0LnlvdWRhby5jb20=')
class youdao:
# MD5 - SHA3_512 加密
def md5_encrypt(self, string):
'''
'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
'blake2b', 'blake2s',
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
'shake_128', 'shake_256'
'''
md5 = hashlib.md5()
md5.update(string.encode('utf-8'))
return md5.hexdigest()
# 获取 secretKey / aesKey / aesIv
def obtain_key(self):
'''获取 secretKey / aesKey / aesIv'''
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
}
url = URL + "/webtranslate/key"
# 13位时间戳
mysticTime = time.time()
# 立马的key是官网固定的
sign = self.md5_encrypt(
f'client=fanyideskweb&mysticTime={mysticTime}&product=webfanyi&key=asdjnjfenknafdfsdfsd')
params = {
"keyid": "webfanyi-key-getter",
"sign": sign,
"client": "fanyideskweb",
"product": "webfanyi",
"pointParam": "client,mysticTime,product",
"mysticTime": f"{mysticTime}",
}
response = requests.get(url, params=params, headers=headers).json()['data']
# sign密钥
self.secretKey = response['secretKey']
# key密钥
self.aesKey = response['aesKey']
# iv向量
self.aesIv = response['aesIv']
# 翻译请求 返回密文
def translate(self, text, froms, tos):
'''执行翻译请求,返回密文'''
headers = {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/x-www-form-urlencoded",
"Referer": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
}
# 13位时间戳
mysticTime = time.time()
sign = self.md5_encrypt(
f'client=fanyideskweb&mysticTime={mysticTime}&product=webfanyi&key={self.secretKey}')
# 随机,伪装一下
cookies = {
"OUTFOX_SEARCH_USER_ID": f"-{random.randint(10000000, 99999999)}@{random.randint(1, 255)}.{random.randint(1, 255)}.{random.randint(1, 255)}.{random.randint(1, 255)}"
# 防封,刷新一次可以使用20年有效期
}
url = URL + "/webtranslate"
data = {
"i": text,
# "from": "en", # 输入日文 en英文 ja日文 fr法语 zh-CHS中文 de德语 es西班牙语
# "to": "zh-CHS", # 翻译为中文
"dictResult": "true",
"keyid": "webfanyi",
"sign": sign,
"client": "fanyideskweb",
"product": "webfanyi",
"appVersion": "1.0.0",
"vendor": "web",
"pointParam": "client,mysticTime,product",
"mysticTime": f"{mysticTime}",
"keyfrom": "fanyi.web",
}
if froms:
data['from'] = froms
if tos:
data['to'] = tos
response = requests.post(url, headers=headers, cookies=cookies, data=data).text
self.ciphertext = response.strip()
# AES-128-CBC 解密
def decrypt_AES_128_CBC(self):
'''AES-128-CBC 解密'''
import requests
headers = {'Content-Type': 'application/json'}
data = {"text": self.ciphertext, "key": self.aesKey, "iv": self.aesIv}
url = "http://127.0.0.1:4000/"
response = requests.post(url, headers=headers, data=json.dumps(data)).json()
text = ''
if response['code'] == 1:
for data_list in response['data']['translateResult']:
text += data_list[0]['tgt']
print('----- 翻译结果 -----\n' + text + '\n')
main = youdao()
# 获取 secretKey / aesKey / aesIv 只需要运行一次即可,经过我的分析,他每天返回的key都不一样
main.obtain_key()
# 发送翻译请求 # 输入日文 en英文 ja日文 fr法语 zh-CHS中文 de德语 es西班牙语
# froms:当前输入的是什么语言 输入None则自动识别
# tos:要翻译为什么语言 输入None则自动识别
main.translate('你是谁,你叫什么名字?\n怎么称呼大哥\n我的名字是哈哈,请叫我哈哈哥', froms=None, tos=None) # 自动识别 翻译为 英文(默认)
# 解密请求内容
main.decrypt_AES_128_CBC()
【HOOK代码】
// 【json.parse 解密对象专用 JSON字符串转换为JS对象】
(function () {
var parse_ = JSON.parse;
JSON.parse = function (arg) {
console.log("[J] - 正在执行[*json.parse] -> ", arg);
debugger;
return parse_(arg); // 不改变原来的执行逻辑
}
})();