申明
本文章中所有内容仅供学习交换应用,不用于其余任何目标,不提供残缺代码,抓包内容、敏感网址、数据接口等均已做脱敏解决,严禁用于商业用途和非法用处,否则由此产生的所有结果均与作者无关!
本文章未经许可禁止转载,禁止任何批改后二次流传,擅自应用本文解说的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K 哥爬虫】分割作者立刻删除!
前言
明天在查看某平台私信的时候,发现有位粉丝示意本人在逆向某站的过程中,有一些纳闷,态度非常敌对,K 哥一贯是尽力满足粉丝需要的,本文就对该站进行逆向钻研,该案例不难,不过为了便于粉丝了解,会写的绝对具体点,大佬们间接跳过就能够了~
逆向指标
- 指标:某词霸翻译,sign、content 参数逆向剖析
- 网址:
aHR0cHM6Ly93d3cuaWNpYmEuY29tL3RyYW5zbGF0ZQ==
抓包剖析
进入翻译页,右边输出查问单词,左边即会翻译出中文释义,很显然通过接口传输的数据:
F12 关上开发者人员工具,从新输出一个英文单词,比方 ratel,进行抓包,/index.php
接口的 Form Data 中有个 q 参数,很显著,就是咱们输出的待翻译的英文单词,申请参数 sign 是通过加密的:
- Payload:申请中携带的理论数据局部;
- Query String Parameters:URL 中的查问字符串局部所蕴含的参数;
- Form Data:HTTP 申请中发送数据的形式,通常用于提交表单数据。
那么,响应返回的天然就是翻译后的中文释义,点到 Preview 响应预览中查看一下,发现并没有呈现设想中的 蜜罐
两字,显然 content 就是翻译后果,只不过被加密解决了:
接下来,咱们别离对 sign 参数和 content 参数进行逆向剖析。
逆向剖析
sign 参数
定位的形式有很多,因为该接口是 XHR(XMLHttpRequest)类型的申请,咱们能够间接下个 XHR 断点,这样定位到的地位通常在加密解决实现之后,曾经筹备发送申请了,长处是便于踪栈,更容易找到加密的中央:
在开发者人员工具 Source 面板右侧的 XHR/fetch Breakpoints 中增加截取的接口 URL:
从新输出单词,即会断住,能够看到,此时 sign 参数曾经生成了:
向上跟栈到 takeRusult 中,以下局部中,看起来相当像在拼接 sign 参数:
// encodeURIComponent —> 将特殊字符(例如冒号、斜杠、问号、等号、以及非 ASCII 字符)转换成 UTF-8 编码的十六进制示意
"/index.php?c=trans&m=fy&client=6&auth_user=key_web_new_fanyi&sign=".concat(encodeURIComponent(r))
在该行打下断点,开释掉 XHR 断点,从新输出查问单词即会断住,encodeURIComponent(r) 就是 sign 值:
看看 r 参数是什么,就定义在下面:
r = (0, _.$Q)(r)
断到该行,先选中 _.$Q
,跟进去,看看是什么加密算法:
很显著的 AES 加密,mode 为 ECB,padding 为 PKCS7,key 是通过一系列编码失去的,为定值 L4fBtD5fLC9FQw22
:
- mode:加密模式,ECB 是一种根底的加密形式,密文被宰割成分组长度相等的块(有余补齐),而后独自一个个加密,一个个输入组成密文;
- padding:填充形式,PKCS7 在填充时首先获取须要填充的字节长度 = 块长度 –(数据长度 % 块长度),在填充字节序列中所有字节填充为须要填充的字节长度值;
- 相干文章浏览:【爬虫常识】爬虫常见加密解密算法。
间接引库复现即可:
// 援用 crypto-js 加密模块
const CryptoJS = require('crypto-js')
function aesEncrypt(aesKey, text) {let key = CryptoJS.enc.Utf8.parse(aesKey),
srcs = CryptoJS.enc.Utf8.parse(text),
// ECB 加密形式,Pkcs7 填充形式
encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();}
// 后果验证
let aesKey = 'L4fBtD5fLC9FQw22';
let text = '679eddc40bc55be3'
let encryptResult = aesEncrypt(aesKey, text);
console.log(encryptResult);
再来剖析下 (0, _.$Q)(r)
中的 r,定义在上一行,内容如下:
var r = u()("6key_web_new_fanyi".concat(s.LI).concat(t.q.replace(/(^\s*)|(\s*$)/g, ""))).toString().substring(0, 16);
逐段剖析一下这部分:
.toString().substring(0, 16)
:将之前局部转为字符串后,截取了前 16 个字符;s.LI
:定值6dVjYLFyzfkFkk
;6key_web_new_fanyi6dVjYLFyzfkFkkratel
:6key_web_new_fanyi + 6dVjYLFyzfkFkk + 查问单词;
要害就在于这部分了:
u()("6key_web_new_fanyi".concat(s.LI).concat(t.q.replace(/(^\s*)|(\s*$)/g, ""))).toString()
这一段将 6key_web_new_fanyi6dVjYLFyzfkFkkratel
加密了,长度为 32 位:
32 位就比拟特地了,依据教训,猜想是 MD5 加密,去 www.kgtools.cn 验证一下,果然,后果统一:
当然还能够搜寻 MD5 摘要算法源码中的一些特色,1732584193、4023233417 之类的,大多数都是规范的算法,MD5 算法的源码可于公众号回复关键词 MD5
获取:
残缺算法:
// 援用 crypto-js 加密模块
const CryptoJS = require('crypto-js')
function aesEncrypt(aesKey, text) {let key = CryptoJS.enc.Utf8.parse(aesKey),
srcs = CryptoJS.enc.Utf8.parse(text),
// ECB 加密形式,Pkcs7 填充形式
encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();}
function getSign(){
let searchWord = 'ratel'
let aesKey = 'L4fBtD5fLC9FQw22';
let concatSearchParams = '6key_web_new_fanyi' + '6dVjYLFyzfkFkk' + searchWord;
let md5EncryptResult = CryptoJS.MD5(concatSearchParams).toString().substring(0, 16);
let sign = aesEncrypt(aesKey, md5EncryptResult);
return encodeURIComponent(sign);
}
console.log(getSign()); // C0W0FqJdhxUeFmgdJ162GdRriqVIAJSQ%2BxmfU0q7dIE%3D
当然,也能够间接应用 Python 复现:
import base64
import hashlib
import urllib.parse
from loguru import logger
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
def aes_encrypt(aes_key: str, text: str) -> str:
key = aes_key.encode('utf-8')
srcs = text.encode('utf-8')
cipher = AES.new(key, AES.MODE_ECB)
# 在加密之前进行填充
padded_data = pad(srcs, AES.block_size)
encrypted = cipher.encrypt(padded_data)
# 返回 base64 编码后的密文
return base64.b64encode(encrypted).decode('utf-8')
def get_sign() -> str:
search_word = 'ratel'
aes_key = 'L4fBtD5fLC9FQw22'
concat_search_params = '6key_web_new_fanyi' + '6dVjYLFyzfkFkk' + search_word
md5_encrypt_result = hashlib.md5(concat_search_params.encode()).hexdigest()[:16]
sign = aes_encrypt(aes_key, md5_encrypt_result)
return urllib.parse.quote(sign)
logger.info(get_sign()) # C0W0FqJdhxUeFmgdJ162GdRriqVIAJSQ%2BxmfU0q7dIE%3D
content 参数
咱们来看看 takeResult 的构造,这就是一个异步操作链:
takeResult: function (e) {
........
v("/index.php?c=trans&m=fy&client=6&auth_user=key_web_new_fanyi&sign=".concat(encodeURIComponent(r)), {........}).then((function (e) {var t = 1 === (null === e || void 0 === e ? void 0 : e.status) ? A(A({}, e), {}, {
content: JSON.parse((0,
_.B6)(e.content))
}) : e;
return console.log(t),
t
}
)).catch((function (e) {return e}
))
}
跟进到 v 函数中,返回了一个 Promise 对象,用于异步解决申请的后果:
Promise.then 用于注册当异步操作胜利实现时执行的回调,这里承受了一个参数,即胜利时的回调函数:
function (e) {var t = 1 === (null === e || void 0 === e ? void 0 : e.status) ? A(A({}, e), {}, {
content: JSON.parse((0,
_.B6)(e.content))
}) : e;
return console.log(t),
t
}
大伙应该留神到了,content: JSON.parse((0, _.B6)(e.content))
,这里是否就是 content 参数解密还原出释义的算法地位呢?跟进到 _.B6
中去看看,同样是 AES 算法,断住后就会发现,解密的地位的确是这里,out 通过了 Unicode 编码,key 为定值 aahc3TfyfCEmER33
:
import base64
from loguru import logger
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
def aes_decrypt(ciphertext):
key = 'aahc3TfyfCEmER33'.encode('utf-8')
# 将密文进行 base64 解码
ciphertext = base64.b64decode(ciphertext)
# 创立 AES 解密器对象
cipher = AES.new(key, AES.MODE_ECB)
# 对密文进行解密
decrypted_data = cipher.decrypt(ciphertext)
# 对解密后的数据进行去填充操作
decrypted_data = unpad(decrypted_data, AES.block_size)
# 返回解密后的明文
return decrypted_data.decode('utf-8')
content = 'X2NheRsV7GVaBbfK/jxZ6h6rWRz0J268vfthunwKmlJIHB687XwU1lxRMBgI+YF5652luVBNTUhVKvlnLYwMqSstRS4f5IYpz1a9YcYdXa/rXx65frmbDe5TKkih255dl8RwKe97E/JowqGPq1d5qnpm1rhY96/4IBwcpvtQFVgVrvcAP+RyIHOwRAByBR30Pzh9NPptSnQZm/n0/GSpnPbmR1WWZ0v5WOlCsDSYjWgzXOg/54z83oI+Yj/bKoR66YbMab+mmtIXcnhp+Uwb2rCoWF0whrcrbM5CHyCEuZ52pHYJ3cRzPNgFvf6GqoEWgeF4SMo22JYGqDKK6VrhJyCvs1BFAUNdYrsGlLNmlrsQkuYxQlM40/3DTZX4cxav'
logger.info(aes_decrypt(content))