JS加密在反爬领域是一种常见的反爬方法。由于现在很多利用Python写爬虫的人对JS并不是很熟悉,所以利用JS加密来做反爬对网站来说是一种很经济适用的方法,属于居家必备良品。由于JS语言本身的复杂性,并且很多网站还会对JS做混淆来加大逆向的难度,很多爬虫编写者对JS加密的反爬措施都有一种谈虎色变的感觉。其实,除了淘宝、京东等大型网站,大部分网站所做的JS反爬本身还是不太难的,只需要具备一定的JS基础和JS调试技巧即可逆向JS得到结果。另外,“JS功底要非常厉害才能做逆向”是一种常见误区,其实不是要精通js才可以js逆向,懂一点js就可以,重要的是逆向思维,对问题的思考方式,这是值得我们反复练习和总结提高的,大家一定要记住这句话。

    在正式开始爬取百度翻译的页面前,先总结下利用chrome进行JS断点调试的技巧以及JS逆向操作一般流程。

    chrome进行JS断点调试技巧:
    1. 通过search 打开全局查询面板,通过关键字可以在全局查询面板中查找所有出现该关键字的语句。
    2. 直接点击搜索出现的代码,可以跟踪代码并且可以利用自带格式器把JS代码格式化便于查看。
    3. 观察语句,然后对需要进行调试的代码设置断点后,重新运行页面。
    4. 将鼠标光标移动到语句上可以查看当前运行代码变量值,定位函数原始代码等等。

    JS一般逆向过程:
    1. 通过关键词切入到代码中,切入到发送请求的代码行,一般我们会通过请求的url中提取关键字找到对应的代码
    2. 在发送请求的代码前添加断点,并且再次触发发送请求,确认寻找的代码是否正确
    3. 往上逆向,寻找目标参数以及生成逻辑(目标参数指我们在post或get请求中需要携带的变化的参数)
    4. 利用js2py模拟执行生成逻辑获取想要的内容

    明白了这些基本的技巧和调试流程,接下来我们用百度翻译作为目标网站来演示一下整个JS逆向破解的过程。百度翻译的访问地址为:https://fanyi.baidu.com/。在爬取之前我们肯定需要先抓包分析,看看获取翻译结果的真实请求地址和对应的参数是什么。随便输入一个待翻译的单词或中文,这里我以中译英为例,输入一个中文单词”学习”,抓包分析看看其请求过程。抓包后我们可以看到,翻译请求本身是个异步的XHR的post请求,从返回结果来看,应该是下图中v2transapi这个请求就是我们要模拟的请求。
    image.gif
    image.png

    再来看看这个请求需要携带哪些参数,参数并不多,经过多次请求后,发现规律如下,除了sign参数外,其他基本都是已知或固定的,不用关心,现在关键是要找出sign参数的值是怎么生成的。

    image.png

    先查看页面源代码,没有发现sign的值,所以初步判定这个sign是通过JS函数生成的。我们把v2transapi作为查询关键字,打开chrome的全局搜索,可以找到v2transapi出现的位置。

    image.png
    image.png

    如果这里搜索结果不止一条,那么我们就要依次点开看究竟哪里的语句是有效的,这个过程可能会很复杂,但没有办法,只能一条一条去调试。幸好我们这里搜索出来只有一条结果,那么我们就直接点击这个结果,chrome会自动打开Sources标签并且为我们显示对应的JS代码。如下图所示:

    image.png

    由于是混淆过的代码,所以简直是不能看的,幸好chrome非常贴心地提供了格式化代码的功能,我们可以点击下面的{}号标志来格式化JS代码。格式化后,代码逻辑会清晰很多,并且自动帮我们定位到带有搜索关键字的那行代码上。

    image.png

    原来这个请求是个ajax请求。先在语句的最左边点击一下设置好断点,然后重新刷新页面,让代码断在当前语句位置,再来分析里面的data的值。当我们把鼠标放到data上对应的p位置查看data的值时,值如下图所示:

    image.png

    可以看到,这时sign的值已经生成了,那么接下来我们就要找到哪里生成的sign值。按ctrl+f,在当前页面搜索sign值的位置,可以找到下面这个语句,

    image.png

    sign后面那个m(a)就是生成sign值的函数。把鼠标放在函数m上,点击上面的f e(r)可以定位到m函数的内容,是一个叫“function e(r)”的一个函数。如下图所示:
    image.png

    现在既然知道了sign值是这个函数生成的,所以接下来我们只需要将这个函数完整地从源码中拷贝出来,然后让js2py库来运行它即可得到最终的sign值。大家看不懂这个函数没有关系,因为我们会直接通过js2py这个库来运行它。另外,这个函数在运行时可能还依靠了其他的变量或者函数,没有关系,运行时提示缺什么我们再从源码中去找就可以了。把拷贝出来的函数粘贴到一个本地js文件里面,比如我取名叫translate.js,待会我们将通过js2py来运行这个translate.js文件。translate.js文件的内容如下:

    1. function e(r) {
    2. var o = r.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
    3. if (null === o) {
    4. var t = r.length;
    5. t > 30 && (r = "" + r.substr(0, 10) + r.substr(Math.floor(t / 2) - 5, 10) + r.substr(-10, 10))
    6. } else {
    7. for (var e = r.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), C = 0, h = e.length, f = []; h > C; C++)
    8. "" !== e[C] && f.push.apply(f, a(e[C].split(""))),
    9. C !== h - 1 && f.push(o[C]);
    10. var g = f.length;
    11. g > 30 && (r = f.slice(0, 10).join("") + f.slice(Math.floor(g / 2) - 5, Math.floor(g / 2) + 5).join("") + f.slice(-10).join(""))
    12. }
    13. var u = void 0
    14. , l = "" + String.fromCharCode(103) + String.fromCharCode(116) + String.fromCharCode(107);
    15. u = null !== i ? i : (i = window[l] || "") || "";
    16. for (var d = u.split("."), m = Number(d[0]) || 0, s = Number(d[1]) || 0, S = [], c = 0, v = 0; v < r.length; v++) {
    17. var A = r.charCodeAt(v);
    18. 128 > A ? S[c++] = A : (2048 > A ? S[c++] = A >> 6 | 192 : (55296 === (64512 & A) && v + 1 < r.length && 56320 === (64512 & r.charCodeAt(v + 1)) ? (A = 65536 + ((1023 & A) << 10) + (1023 & r.charCodeAt(++v)),
    19. S[c++] = A >> 18 | 240,
    20. S[c++] = A >> 12 & 63 | 128) : S[c++] = A >> 12 | 224,
    21. S[c++] = A >> 6 & 63 | 128),
    22. S[c++] = 63 & A | 128)
    23. }
    24. for (var p = m, F = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(97) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(54)), D = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(51) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(98)) + ("" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(102)), b = 0; b < S.length; b++)
    25. p += S[b],
    26. p = n(p, F);
    27. return p = n(p, D),
    28. p ^= s,
    29. 0 > p && (p = (2147483647 & p) + 2147483648),
    30. p %= 1e6,
    31. p.toString() + "." + (p ^ m)
    32. }

    image.gif
    接下来,我们来编写python代码来模拟整个请求过程,代码如下。如果你的环境中没有安装js2py库的话,可以通过pip install -i https://pypi.douban.com/simple js2py来安装一下。

    1. import requests
    2. import js2py
    3. context = js2py.EvalJs()
    4. class BaiDuTranslater(object):
    5. """
    6. 百度翻译爬虫
    7. """
    8. def __init__(self, query):
    9. # 初始化
    10. self.url = "https://fanyi.baidu.com/basetrans"
    11. self.query = query
    12. self.headers = {
    13. "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
    14. "Referer": "https://fanyi.baidu.com/",
    15. "Cookie": "BAIDUID=714BFAAF02DA927F583935C7A354949A:FG=1; BIDUPSID=714BFAAF02DA927F583935C7A354949A; PSTM=1553390486; delPer=0; PSINO=5; H_PS_PSSID=28742_1463_21125_18559_28723_28557_28697_28585_28640_28604_28626_22160; locale=zh; from_lang_often=%5B%7B%22value%22%3A%22en%22%2C%22text%22%3A%22%u82F1%u8BED%22%7D%2C%7B%22value%22%3A%22zh%22%2C%22text%22%3A%22%u4E2D%u6587%22%7D%5D; to_lang_often=%5B%7B%22value%22%3A%22en%22%2C%22text%22%3A%22%u82F1%u8BED%22%7D%2C%7B%22value%22%3A%22zh%22%2C%22text%22%3A%22%u4E2D%u6587%22%7D%5D; REALTIME_TRANS_SWITCH=1; FANYI_WORD_SWITCH=1; HISTORY_SWITCH=1; SOUND_SPD_SWITCH=1; SOUND_PREFER_SWITCH=1; Hm_lvt_afd111fa62852d1f37001d1f980b6800=1553658863,1553766321,1553769980,1553770442; Hm_lpvt_afd111fa62852d1f37001d1f980b6800=1553770442; Hm_lvt_64ecd82404c51e03dc91cb9e8c025574=1553766258,1553766321,1553769980,1553770442; Hm_lpvt_64ecd82404c51e03dc91cb9e8c025574=1553770442"
    16. }
    17. def make_sign(self):
    18. # js逆向获取sign的值
    19. with open("translate.js", "r", encoding="utf-8") as f:
    20. context.execute(f.read())
    21. # 调用js中的函数生成sign
    22. sign = context.e(self.query)
    23. # 将sign加入到data中
    24. return sign
    25. def make_data(self, sign):
    26. data = {
    27. "query": self.query,
    28. "from": "zh",
    29. "to": "en",
    30. "token": "6f5c83b84d69ad3633abdf18abcb030d",
    31. "sign": sign
    32. }
    33. return data
    34. def get_content(self, data):
    35. # 发送请求获取响应
    36. response = requests.post(
    37. url=self.url,
    38. headers=self.headers,
    39. data=data
    40. )
    41. return response.json()["trans"][0]["dst"]
    42. def run(self):
    43. """运行程序"""
    44. # 获取sign的值
    45. sign = self.make_sign()
    46. # 构建参数
    47. data = self.make_data(sign)
    48. # 获取翻译内容
    49. content = self.get_content(data)
    50. print(content)
    51. if __name__ == '__main__':
    52. query = input("请输入您要翻译的内容:")
    53. translater = BaiDuTranslater(query)
    54. translater.run()

    image.gif
    这段代码的逻辑并不复杂,主要是看生成的sign参数是否正确,如果不正确话肯定会报错,我们再根据报错的信息来进行修改即可。点击运行,可以看到报错了,报错信息如下:

    image.png

    提示是变量i未定义,那么我们就在代码里面找找i是哪里定义的,值是什么。在代码中我们可以找到变量i的定义,如下图所示,

    image.png

    i的值同样可以通过打断点来查看,我们在i变量所在那行打个断点,重新刷新页面执行一遍代码,在中断的时候把鼠标放到变量i上可以查看i的值,也可以通过在右边的watch窗口把i变量设置为监控变量,当i被赋值后,这里可以直接显示i的值。如下图所示:

    image.png

    可以看到,i的值是320305.121221201。由于这个i是个常量,所以我们直接把这个值加到之前的translate.js文件前面即可,代码如下:

    1. var i = '320305.131321201'
    2. function e(r) {
    3. var o = r.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
    4. if (null === o) {
    5. var t = r.length;
    6. t > 30 && (r = "" + r.substr(0, 10) + r.substr(Math.floor(t / 2) - 5, 10) + r.substr(-10, 10))
    7. } else {
    8. for (var e = r.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), C = 0, h = e.length, f = []; h > C; C++)
    9. "" !== e[C] && f.push.apply(f, a(e[C].split(""))),
    10. C !== h - 1 && f.push(o[C]);
    11. var g = f.length;
    12. g > 30 && (r = f.slice(0, 10).join("") + f.slice(Math.floor(g / 2) - 5, Math.floor(g / 2) + 5).join("") + f.slice(-10).join(""))
    13. }
    14. var u = void 0
    15. , l = "" + String.fromCharCode(103) + String.fromCharCode(116) + String.fromCharCode(107);
    16. u = null !== i ? i : (i = window[l] || "") || "";
    17. for (var d = u.split("."), m = Number(d[0]) || 0, s = Number(d[1]) || 0, S = [], c = 0, v = 0; v < r.length; v++) {
    18. var A = r.charCodeAt(v);
    19. 128 > A ? S[c++] = A : (2048 > A ? S[c++] = A >> 6 | 192 : (55296 === (64512 & A) && v + 1 < r.length && 56320 === (64512 & r.charCodeAt(v + 1)) ? (A = 65536 + ((1023 & A) << 10) + (1023 & r.charCodeAt(++v)),
    20. S[c++] = A >> 18 | 240,
    21. S[c++] = A >> 12 & 63 | 128) : S[c++] = A >> 12 | 224,
    22. S[c++] = A >> 6 & 63 | 128),
    23. S[c++] = 63 & A | 128)
    24. }
    25. for (var p = m, F = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(97) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(54)), D = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(51) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(98)) + ("" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(102)), b = 0; b < S.length; b++)
    26. p += S[b],
    27. p = n(p, F);
    28. return p = n(p, D),
    29. p ^= s,
    30. 0 > p && (p = (2147483647 & p) + 2147483648),
    31. p %= 1e6,
    32. p.toString() + "." + (p ^ m)
    33. }

    image.gif
    接下来再次运行整个python脚本,看看还有没有报错信息。运行后,发现仍然报错,信息如下:
    image.png

    这次是n未定义。那么n是个什么东西呢?我们再看源码,可以找到n其实是一个function,如下所示,

    image.png

    同样把这个function的定义也拷贝到translate.js文件中,此时translate.js文件内容如下:

    1. function n(r, o) {
    2. for (var t = 0; t < o.length - 2; t += 3) {
    3. var a = o.charAt(t + 2);
    4. a = a >= "a" ? a.charCodeAt(0) - 87 : Number(a),
    5. a = "+" === o.charAt(t + 1) ? r >>> a : r << a,
    6. r = "+" === o.charAt(t) ? r + a & 4294967295 : r ^ a
    7. }
    8. return r
    9. }
    10. var i = '320305.131321201'
    11. function e(r) {
    12. var o = r.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
    13. if (null === o) {
    14. var t = r.length;
    15. t > 30 && (r = "" + r.substr(0, 10) + r.substr(Math.floor(t / 2) - 5, 10) + r.substr(-10, 10))
    16. } else {
    17. for (var e = r.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), C = 0, h = e.length, f = []; h > C; C++)
    18. "" !== e[C] && f.push.apply(f, a(e[C].split(""))),
    19. C !== h - 1 && f.push(o[C]);
    20. var g = f.length;
    21. g > 30 && (r = f.slice(0, 10).join("") + f.slice(Math.floor(g / 2) - 5, Math.floor(g / 2) + 5).join("") + f.slice(-10).join(""))
    22. }
    23. var u = void 0
    24. , l = "" + String.fromCharCode(103) + String.fromCharCode(116) + String.fromCharCode(107);
    25. u = null !== i ? i : (i = window[l] || "") || "";
    26. for (var d = u.split("."), m = Number(d[0]) || 0, s = Number(d[1]) || 0, S = [], c = 0, v = 0; v < r.length; v++) {
    27. var A = r.charCodeAt(v);
    28. 128 > A ? S[c++] = A : (2048 > A ? S[c++] = A >> 6 | 192 : (55296 === (64512 & A) && v + 1 < r.length && 56320 === (64512 & r.charCodeAt(v + 1)) ? (A = 65536 + ((1023 & A) << 10) + (1023 & r.charCodeAt(++v)),
    29. S[c++] = A >> 18 | 240,
    30. S[c++] = A >> 12 & 63 | 128) : S[c++] = A >> 12 | 224,
    31. S[c++] = A >> 6 & 63 | 128),
    32. S[c++] = 63 & A | 128)
    33. }
    34. for (var p = m, F = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(97) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(54)), D = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(51) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(98)) + ("" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(102)), b = 0; b < S.length; b++)
    35. p += S[b],
    36. p = n(p, F);
    37. return p = n(p, D),
    38. p ^= s,
    39. 0 > p && (p = (2147483647 & p) + 2147483648),
    40. p %= 1e6,
    41. p.toString() + "." + (p ^ m)
    42. }

    image.gif
    然后,我们再运行python脚本,发现这时终于可以成功运行了,百度翻译网站JS逆向破解成功。

    image.png

    最后总结一下,这个例子中的请求和逆向过程属于是比较简单的情况,但同时它也是一个JS逆向很典型的应用场景,解决过程也算是比较常规和标准。其实大家从这个过程看到,逆向过程本身对JS的要求并不是很高,我们不需要去阅读那些经过混淆后的JS代码,我们只需要具备基本的JS知识,能够识别变量来自哪里,由什么函数生成,然后再使用js2py这样的库来模拟这个生成过程而已。有些网站的JS加密过程是非常复杂的,依赖关系也很多,很多还涉及到加解密算法,单靠一个人很难搞出来。爬虫和反爬永远是矛与盾的关系,要想提高爬虫技术,也必须经常了解和学习反爬技术,从而让自己写的爬虫能够爬取更多的数据。我也会不断地把各种不同的反爬技术暴露出来,大家一起进行学习。