前言
深入学习js逆向中,这里详细记录下个人针对 有道翻译的 加解密全过程
一、定位接口
- 首先还是清除浏览器cookie后,刷新页面;
- 输入你好,查看响应的接口
如果无关的请求数量很多,可以先清除,选择筛选Fetch/XHR, 再输入翻译,能最迅速定位
- 这里看一眼请求和响应,很显然翻译接口就是webtranslate
- 但是响应是一串莫名其妙的字符,显然是加密了,那就先破解加密。要是响应内容解不了密,请求再顺利也没有用
二、响应数据解密
首先还是先尝试搜索decrypt( 或decrypt,看下有没有明显的解密方法
确实有几个看起来可疑的,定位到对应位置,打上断点,重新输入翻译内容,再看是否断住。如果断住,看一下decrypt(相关参数和执行结果是否是我们想要的。这里可以自己试一下。这里试了下没有找到相关的方法,说明解密方法名可能不是decrypt。
或者也可能就是太多了我们不好找,都没关系,那就换个思路。借用接口名称定位
-
直接搜索webtranslate,找一下对应构造请求的位置
-
或者用XHR断点,添加断点后,重新输入翻译就会断住
上述两种方法都可以。但是还是有一些区别。其中全局搜索定位到的位置,一般是构造请求的地方;XHR断点断住的地方,是send请求的地方,会更靠后。所以假设如果要通过接口名称定位请求参数加密,用全局搜索会更接近;如果要通过接口名称定位响应的解密,用XHR断点会更接近。一般还是都用XHR断点会更好 -
首先我们要注意下正常请求的加密响应是什么样子的
-
加上XHR断点后,输入“你好”进行翻译,断住了,但是显然这里的地址是/webtranslate/key
-
点击继续执行脚本到下一次断点,可以看到这次的地址是/webtranslate
-
可以开始往下走了,一直点击向下执行,目的是找到返回响应,进行解密处理的地方。这里可以多注意下作用域,有没有我们关注的变量
-
到这里就看到加密的响应了,继续往下走看什么时候解密出来
-
很明显就能看到,在这里就是数据解出来的地方,这里decodeData方法应该就是我们要找的方法
-
光标放到方法上,跳转到方法内部,左侧打个断点。因为函数已经执行过了,所以要重新刷新,输入“你好”,重新开始执行,一直跳到这个断点就行
-
简单看下这个方法,很明显写了aes-128-cbc, 就是个 AES加密,CBC模式的(如果对AES什么的不熟,后面我会补充一种直接不管它什么加密,硬抠下来的方式,主要是看思路)。
那么关键就在于秘钥key和偏移量vi,断点往后打。关键就是a,i参数 -
这里a, i是Unit8Array, 用alloc方法生成的,那我们直接用nodejs的Buffer.alloc替换。再看下y()函数,就是简单的md5加密,返回二进制的值,可以直接拿下来。
-
y()方法传参o和n显然就是原始的秘钥和偏移量,是一串特殊的字符,全局搜索一下看一下能不能找到哪里来的。很显然是固定的字符串
-
然后就是c.a.createDecipheriv方法,那就直接用node中内置的crypto加密库替换c.a。
-
整个解密函数就梳理完了,简单整理下,js代码如下 。执行没有问题
const crypto=require('crypto')
const o = 'ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl';
const n = 'ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4';
function y(e) {
return crypto.createHash("md5").update(e).digest()
}
function decodeData(t){
const a = Buffer.alloc(16, y(o))
, r = Buffer.alloc(16, y(n))
, i = crypto.createDecipheriv("aes-128-cbc", a, r);
let s = i.update(t, "base64", "utf-8");
return s += i.final("utf-8"),
s
}
text='Z21kD9ZK1ke6ugku2ccWu4n6eLnvoDT0YgGi0y3g-v0B9sYqg8L9D6UERNozYOHqnYdl2efZNyM6Trc_xS-zKtfTK4hb6JP8XwCzNh0avc8qItQUiIU_4wKKXJlIpvMvfKvJaaZzaX6VEtpkr2FdkfoT_Jgbm2GRSVj3r40autIdlImENG8hC0ZH4ww7utwuTt3Oo_ZpXg0BSq9wePSAB75-ChkiGKF9HTIPeCl2bl84SBD1XDfFCZpkKQhecYSs0JLoXOqP2ltavxRrg58Hp1q5uIgZZ_Oo2-Jmd-t1r4es40drcAq5bjmS62M2VJF8D6ojtOh9JTfNwgzD3CxYn-Pd7-TgHMyNEJEkFXTAyxzpjlFqtrCYDE3SZUYlENkqsL8Wrra1hM-1nTfiB-BLcWAdRBynNpP5_54aq_-GBsq8bB_9yEX5ovzDB4_Ry_spVVuUnb39iplMHCdCnjOD3ngiIDbl9SUz-9npjBX05ZYRdPmFPAl424qdoaxeVqnVoH8jQFPZVqaHMzu4mJg0SICDWFH7GP1zqGRbXd3ESjT_iBInl3gICt2XVuhh_nubcELkTEC6xbqEDRQkPUNMpzXJHjcvsLHtcmSW0S9F0445ho9kT2qZYdMBC3Fs0OaHpUtFu77gZpQn7sGiqh8VliXIcUtfvvop-1c-Vu5QjfUbLn2-s5POR9fGYG6rt6ioe_PGmwWj-Cc00zUM7FybfarKTr4D3Rk57R72qpXN4Ja86ZsCAMmDG-m5z31RQh_V7echJ8Kna3Go3yWKCK4vtSwOWrFhiS5RTz6EkrGc3SkFKbb5vp8Wop_84myBtgnBmj4CczhTq2HcOxrJf4def6yDt2uBxyv4bTVGx9Yx3uB4Gx0iK5kYvfma6B_LnkRWk331wjuXKQtBGYIuWkR8J5QtvBmIRVaa7AA19Z4xMIEAqbcuQ5p4I9FCElthBrJd9YOcouHK4U27xxYWJJXcJjAU6hR_oB1nwjAlwdreYSrxqhhnMfxUlzwXnjkeHIQsIrXmyDqn1ecy2NyzUnoIscC4EigujhLKbuFQIzbD5YNhKxCwU1RSNRYYy_A1hktX'
console.log(decodeData(text))
这个代码也可以用python实现
import base64
from Crypto.Cipher import AES
import hashlib
def decode_data(text):
# 偏移量
decodeiv = "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4"
# 秘钥
decodekey = "ydsecret://query/key/B*RGygVywfNBwpmBaZg*WT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl"
# 先把密匙和偏移量进行md5加密 digest()是返回二进制的值
key = hashlib.md5(decodekey.encode(encoding='utf-8')).digest()
iv = hashlib.md5(decodeiv.encode(encoding='utf-8')).digest()
# AES解密 CBC模式解密
aes_en = AES.new(key, AES.MODE_CBC, iv)
# 将已经加密的数据放进该方法
data_new = base64.urlsafe_b64decode(text)
# 参数准备完毕后,进行解密
a = aes_en.decrypt(data_new).decode('utf-8').strip()
return a
- 对AEC转nodejs代码更多靠经验。最后面会讲下不管AES硬抠的方式,比较麻烦一些
三、构造请求
接下来破解请求参数加密。重头开始,还是先清除cookie 重新刷新,然后输入“你好”,定位到翻译接口/webtranslate。
首先生成cookie
首先还是将请求转成python代码执行。会发现有请求携带了cookie,并且删除cookie后无法请求成功。那就先看下cookie是什么。
搜索请求可以看到OUTFOX_SEARCH_USER_ID是某个请求set的,而OUTFOX_SEARCH_USER_ID_NCOO是在代码某处生成的。
那就先挖一下OUTFOX_SEARCH_USER_ID_NCOO。最后会发现生成OUTFOX_SEARCH_USER_ID_NCOO就是用固定数和随机数相乘得到的,那就是一串随机的数字,不重要。
然后可以很容易发现OUTFOX_SEARCH_USER_ID好像就是某个数字加上某个ip地址。尝试手动改一下,结果发现,只要保持一定的格式就能用,最后改成下面这样完全没问题。
cookies = {
"OUTFOX_SEARCH_USER_ID_NCOO": "",
"OUTFOX_SEARCH_USER_ID": "1@1.1.1.1"
}
那就不管他的生成逻辑了,固定写死就行。
生成表单参数
- 查看请求参数,虽然参数很多,但比较明显能看出来,大部分都是固定的字符串,多试几遍也能看出来。其中真正需要的只有sign、mysticTime(比较明显就是时间戳)。 i是要翻译的内容,关键就是sign
- 搜索一下sign:(一般这种参数加上:或者=去搜更快定位)
- 在一些看起来可疑的地方都打上断点,然后重新输入、断住,看是不是我们想要的。最可疑的就是这里,很显然这几个参数的定义都是我们的请求中包含的。直接在第一行sign这里打上断点
- 执行到这一行成功断住,划取选中 k(o, e)看执行结果,能看到是一串数字字母组合。不确定是不是这个,直接跳过断点执行,拿到翻译结果后,对比请求中的sign
?怎么不一样,难道不是这里吗?
实际上我们在跳断点的时候会发现,在sign: k(o, e)这一行,断住了两次,所以我们拿第二次断住时生成的sign去对比webtranslate接口参数的sign才是一样的。第一次执行sign: k(o, e),是因为在调用webtranslate/key接口时也用到了该方法。对比第二次sign: k(o, e)的结果,可以确定sign就是这里生成的 - 为了方便观察全部参数,我们直接在该函数结束位置也打个断点,执行下去。然后选中k, 跳转到函数内部,这个k函数就是核心的方法
- 可以看到k方法内部非常简单,就是拼接字符串,然后调用j方法进行MD5加密。参数也很简单,k(o, e)的o实际上就是上面定义的(new Date).getTime()是个时间戳。k(o, e)的e是一串奇怪的字符串‘fsdsogkndfokasodnaso’,还不知道是什么
- 全局搜一下fsdsogkndfokasodnaso,发现跟前面解密时用到的AES的key和vi是一起的,也是个key,可以确定是固定的
- 整个sign生成的逻辑就很清晰了:用时间戳+固定的key(可以认为变量只有时间戳)构造字符串进行md5加密生成的。简单。其中c.a还是用crypto加密库替换,key固定,保留时间戳传参(没有直接在这里生成时间戳,因为请求的表单参数里有个单独的时间戳字段mysticTime,两边保持一致)
function j(e) {
return C.createHash("md5").update(e.toString()).digest("hex")
}
function getSign(timestamp) {
return j(`client=fanyideskweb&mysticTime=${timestamp}&product=webfanyi&key=fsdsogkndfokasodnaso`)
}
请求加密部分的代码还是比较简单的。把写好的请求和响应内容解密的代码放一起,试一下,没有问题。
整个过程也不难,完整代码就不贴了。有兴趣的可以自行尝试下。
总结
以上方法仅用于学习参考