究竟是道德的沦丧,还是现实的骨感,让携程反爬工程师在代码里写下这句话-『爬虫进阶第二弹』

点这里排版好

本文仅用作学习交流不得用于任何商业用途


4 月的北京,这天气像是 80 岁的老奶奶 ?,捂得慌。
实验室/自习室的角落,清脆的打字声,夹杂着几声叹气,一个套着格子衬衫,头发不多的大叔直视着屏幕。
眼神坚定的他,快速的敲下了一行命令。‘这次一定要成了’。
而屏幕的那头,终于有了些反应,一个个字符鲜活的蹦了出来,像极了[黑镜:潘达斯奈基]中男主被操控的感觉。
可他原本充满期待的脸突然开始扭曲,放佛在屏幕上看到了什么恐怖的东西
只见屏幕上欣然出现这么一句话
在这里插入图片描述

上次发完你已经是一个成熟的爬虫了,应该学会自己去对抗反爬码农了 ?-『爬虫进阶指南』就不断有小伙伴向我我请教如何解决一些 js 逆向工程的问题

其实这个问题说小了涉及 js、py 基础语法,说大了涉及网络攻防,涉及对方公司架构,甚至涉密。

而且做这种逆向工程还特别费时间,(其实反爬工程师做加密方案也特别累, 所以一般做这种混淆加密的都是该公司的核心业务),所以这方便的资料其实特别少。

还是再想提醒下大家,爬虫是一个获取信息的好工具,但还请相互体谅,本文也仅用作学习使用。

本文分析两个案例,一个是去年被爬怕了的马蜂窝,一个是携程, 像这种公司体量很大,业务繁多,也不可能全部分析,具体来说:

乱入总结:

  • 携程 996
  • 马蜂窝看不出来 是不是 996

打个小广告, 求 star自带高可用 Proxy 库的 spider 代码 hhh

马蜂窝

在这里插入图片描述

我们要爬的是所有景点信息,这个信息是请求http://www.mafengwo.cn/ajax/router.php获取的

其中需要一坨参数,除了一眼能看出语义的参数之外,也就_sn, sAct(可惜它不变)可能是被加密过的。

image

google 了一下没发现有提供解决方案的~~(快速解决问题才是王道)~~,大概推测了一下马蜂窝在去年年底做了这次加密方案。

第一反应,不是先做逆向,而是猜测可能是通过前置请求从后端拿到的,然后翻了一下发现并没有,对比了一下前后几个请求,发现这个参数出现次数还挺多的,而且值还不一样,行了 js 加密石锤了

然后去筛哪些 js 对这个参数的生成有所影响(用 chrome 的开发者工具暴力 block)

image

饿 他们这 js 有点少,闭着眼睛 都能猜出来是哪个,jQuery 一般会放自己构造 cookie 构造 Header 头的逻辑(当然这里也有可能同时使用 Cookie & _sn一起做效验)

这个时候我们做一个实验,来看一下 Header 里面的内容有没有作用在 encoder 和 decoder 中。

打开 chrome 的无痕模式(有些时候需要 clear 一下 History, 这跟 ServiceWork 机制有关,有兴趣的同学可以查下相关资料), 先打开开发者模式,然后键入我们爬取的 URL。

(现在我们模拟的是首次进入该网站的用户,通常为了做到首次加载网页在几百 ms 内,都会对一些不必要的功能做 delay 加载操作,这个时候的条件能获得到信息,则之后也能)-这也是一个小技巧吧 ?

image

然后我们看一下,没有 Cookie(这也可以理解,JQuery.js 和混淆所需要的/js/hotel/sign/index.js两个文件是异步获取的,为了保证用户的用户体验,前端在首次加载做了妥协。

然后我们工作的重点,就是研究http://js.mafengwo.net/js/hotel/sign/index.js?1552035728这个 js 做了哪些混淆

首先,一看这个安全做的就不是特别好,这个 js 是通过静态的 url 来获取的,获取的过程没有任何加密,也就是说我解密出来一次,只要你不发版,我基本上都能用(所以大家看看就行了,别用做商业用途)

(然后,从时间戳上看,这个版本是2019-03-08 17:02:08发的,所以起码这一个多月他们没有做什么改动,3 月 8 日星期五,hhh 看不出来是不是 996

image

我们来看一下代码,首先,这种混淆也做的比较套路,就是最外层堆一层 Unicode 来让你不能直接在 Preview 中直接看出语义信息,然后丢一个数组来存使用的字符变量

所以我们一开始做的事情是,做 js 代码解混淆,起码先变成能看的代码(PS: 看 Vscode 中右边 ? 预览中,那一大坨很规律的代码块,这一定是做的是类似 DES 的多层加密计算

  • 解 Unicode \\x75 -> \x75 -> codecs.unicode_escape_decode(origin_js)[0]
  • 从第五行的数组__Ox2133f中,替换常量字符
  • 然后这种无意义的变量名称看的难受,而且太长,就做一个替换
  • 然后用 Vscode,Toggle Format插件做一下自动格式化
def decode_js_test():
    ''' decode js for test '''
    with open(decoder_js_path, 'r') as f:
        decoder_js = [codecs.unicode_escape_decode(ii.strip())[0] for ii in f.readlines()]
    __Ox2133f = [ii.strip() for ii in decoder_js[4][17:-2].replace('\"', '\'').split(',')]
    decoder_str = '|||'.join(decoder_js)
    params = re.findall(r'(\_0x\w{6,8}?)=|,|\)', decoder_str)
    params = sorted(list(set([ii for ii in params if len(ii) > 6])), key=lambda ii: len(ii), reverse=True)
    for ii, jj in enumerate(__Ox2133f):
        decoder_str = decoder_str.replace('__Ox2133f[{}]'.format(ii), jj)
    for ii, jj in enumerate(params):
        decoder_str = decoder_str.replace(jj, 'a{}'.format(ii))
    decoder_js = decoder_str.split('|||')
    with open(origin_js_path, 'w') as f:
        f.write('\n'.join(decoder_js))
    return decoder_js

image

经过初步的解耦之后,我们得到了上面的代码。可以看到在第 389 行出现了我们需要的 _sn 变量 ?

a40["ajaxPrefilter"](function(a410, a411) {
  var a23 = a40["extend"](true, {}, a411["data"] || {}); // 拿data
  if (a23["_sn"]) {
    delete a23["_sn"]; // 删去已有的`_sn`
  }
  a23["_ts"] = new Date()["getTime"](); // `_ts` = 13位时间戳
  var a13 = a425(a40["extend"](true, {}, a23)); // 加密了
  if ("data" in a410) {
    a410["data"] += "&_ts=" + a23["_ts"] + "&_sn=" + a13; // 拼接字符串
  } else {
    a410["data"] = "_ts=" + a23["_ts"] + "&_sn=" + a13;
  }
});

上面这坨代码实际上就是实现_sn的入口函数,逻辑比较简单,就是把拿到的拿到的 data 加上当前时间戳,丢给 a425 这个函数

a425就是上面那张图第 348 行定义的,a425 也就 40 多行代码,逻辑也很简单

他是调用了a36这个函数的hash方法,传进去了(一个把 Object 做了 JSON 序列化的字符串,在加上了一个字符常量),最后对结果截取了 2,12 位

然后a18就是上面那行调用了一下a427的返回值,大概扫一眼,a427做了一件按 dict 的keys排序的工作(因为后面要做 JSON 序列化,顺序很重要)

然后hash函数是在第 283 行定义的

a36["hash"] = function(a11, a36a) {
  if (/[-
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页