Python反反爬虫:JavaScript 逆向爬虫(一)了解前端 JS 混淆,加密等技术:

网页是运行在浏览器端的, 当我们浏览一个网页时, 其HTML 代码, JavaScript 代码都会被下载到浏览器中执行, 借助浏览器的开发者工具, 我们可以看到网页加载过程中所有网络请求的详细信息, 也能清楚地看到网站运行的HTML 代码 和 js 代码, 这些代码里就包含了网站加载的全部逻辑, 比如加载哪些资源,请求接口是如何构造的, 页面是如何渲染的, 等等, 正是因为代码是完全透明的,所以如果我们能研究明白其中的执行逻辑, 就可以模拟各个网络请求, 进行数据爬取了。

然而, 事情并没有想象中的那么简单, 随着前端技术的发展, 前端代码的打包技术, 混淆技术,加密技术也越来越多, 各个公司使用这些技术可以在前端对js代码采取一定的保护, 比如变量名的混淆, 执行逻辑混淆, 反调试, 核心逻辑加密等, 这些保护手段使得我们没法很轻易的找出 js 代码中包含的执行逻辑。

对于一些有着加密的网站, 我们可以借助一些自动化工具,例如selenium等 来获取数据,但是这样性能将会大打折扣, 当然,我们还有一个方案, 就是 逆向js 代码, 找出其中的加密逻辑, 实现该加密逻辑, 但是这种方案难度很大, 但是在获取数据的速度性能上, 将会很快

接下来, 让我们一起来探索一些常用的  js 逆向技巧, 包括浏览器工具的使用, Hook 技术, AST技术, 特殊混淆技术的处理, WebAssembly 技术的处理, 了解完这些技术, 我们就可以更从容的应对 JS 反爬技术了

网站加密和混淆技术简介:

在爬取网站的时候,会遇到一些需要分析接口或URL信息的情况, 这时会有各种各样类似加密的情形:

某个网站的URL带有一些看不太懂的长串加密参数,要抓取就必须懂得这些参数是怎么构造的,否则我们连完整的URL都构造不出来,更不用说爬取了。

在分析某个网站的Ajax接口时, 可以看到接口的一些参数也是加密的, Request Headers 里面也可能带有一些加密参数, 如果不知道这些参数的具体构造逻辑, 就没法直接用程序来模拟这些Ajax请求。

翻看网站的JS源代码, 可以发现很多压缩了或者看不太懂的字符, 比如 js 文件名被编码, 文件的内容被压缩成几行, 变量被修改成单个字符或者一些 十六进制的字符, 这些导致我们无法轻易根据 js 源代码找出某些接口的加密逻辑

以上情况基本上是网站为了保护其数据而采取的一些措施,我们可以将它分为两大类:

URL / API 参数加密

Js 压缩混淆和加密

网站数据防护方案:

当今大数据时代,数据也越来越重要了, 网页和APP现在是主流的数据载体, 如果其数据的API 没有设置任何保护措施, 那么在爬虫工程师解决了一些基本的反爬:如IP,验证码 问题之后数据将会很轻易的被爬取到

URL / API 参数加密:

网站运营者首先想到的防护措施可能是对某些数据接口的参数进行加密, 比如说给某些URL的参数加上校验码, 给一些ID信息编码,给某些API请求加上token, sign等签名, 这样这些请求发送给服务器的时候, 服务器会通过客户端发送来的一些请求信息以及双方约定好的秘钥等来对当前的请求进行校验, 只要校验通过, 才返回对应数据的结果

比如说客户端和服务端约定了一种接口校验逻辑, 客户端在每次请求服务端接口的时候都会附带一个sign 参数, 这个 sign 参数可能是由当前的时间信息, 请求的URL, 请求单数据, 设备的ID, 双方约定好的秘钥 经过一些加密算法构成的, 客户端会实现这个加密算法来构造 sign, 然后每次请求服务器的时候附带上这个参数, 服务端会根据约定好的算法和请求的数据对sign 进行校验, 只有校验通过, 才返回对应的数据,否则拒绝响应

当然, 登录状态下的校验也可以看做此类方案, 比如一个API的调用必须传入一个token, 这个token必须在用户登陆之后才能获取, 如果请求的时候不带该token, API 就不会返回任何数据了

倘若没有这种措施, 那么URL或者API接口基本上是完全可以公开访问的, 这意味着任何人都可以直接调用来获取数据, 这样是很危险的, 而且数据也可以被轻易的被爬虫爬取

JS 压缩, 混淆和加密:

接口加密技术看起来的确是一个不错的解决方案, 但单纯依靠它宁不能很好的解决问题, 对于网页来说, 其逻辑是依赖于JS来实现的,而JS有一些特点:

JS代码运行于客户端, 也就是它必须在用户浏览器端加载运行

JS代码是公开透明的, 也就是说浏览器可以直接获取正在运行的JS源码

基于这两个原因, JS代码是不安全的, 任何人都可以读,分析,赋值,盗用甚至篡改代码,

如果不想让自己的数据被轻易的获取, 那么就需要用到JS压缩,混淆,和加密技术了

代码压缩:取出JS代码中不必要的空格, 换行等内容, 使源码都压缩为几行内容,降低代码的可读性, 当然同时也能提高网站的加载速度

代码混淆:使用变量替换,字符串阵列化,控制流平坦化,多态变异,僵尸函数,调试保护等手段,使代码变得难以阅读和分析,达到最终保护的目的,但这不影响代码的原有功能,是理想,使用的JS 保护方案,

代码加密:可以通过某种手段将JS代码进行加密, 转成人无法阅读或者解析的代码,如借用WebAssembly技术, 可以直接将JavaScript代码用 C/C++ 实现, JS调用其编译后形成的文件来执行相应的功能

URL/API 参数加密:

现在绝大多数网站的数据一般都是通过服务器提供的API来获取的, 网站或APP可以请求某个数据API获取到对应的数据, 然后再把获取的数据展示出来, 但是有些数据是比较宝贵或私密的, 这些谁肯定需要一定层面上的保护, 所以不同API的实现也就对应着不同安全防护级别, 这里我们总结下:

为了提升接口的安全性, 客户端会和服务端约定一种接口校验方式, 一般来说会用到各种加密和编码算法, 例如:Base64, Hex编码, MD5, AES, DES, RSA 等对称或非对称加密。

比如说 客户端和服务端双方约定一个sign 用作接口的签名校验, 其生成逻辑是客户端将 url 路径进行md5 加密, 然后拼接上 URL 的某个参数再进行 Base64编码, 最后得到一个字符串sign, 这个 sign 会通过Request URL 的某个参数或 Request Headers 发送给服务器, 服务器接收到请求后, 对URL路径同样进行MD5加密, 然后拼接上URL的某个参数, 再进行base64编码, 也会得到一个sign, 接着比对生成的sign和客户端发送来的sign是否一致, 如果一致,就返回正确的结果,否则拒绝响应, 如果有人想要调用这个接口的话, 必须定义好sign的生成逻辑, 否则是无法正常调用接口的

当然,上面的实现思路比较简单, 这里还可以增加一些时间戳信息增加时效性判断, 或增加一些非对称加密进一步提高加密的复杂程度, 但是不管怎么样, 只要客户端和服务端约定好了加密和校验逻辑,任何形式的加密算法都是可以的

这里要实现接口参数加密, 就需要用到一些加密算法, 客户端和服务端肯定也都有对应的SDK实现这些加密算法, 如 JS的crypto-js, Python的 hashlib, Crypto, 等等

JavaScript 压缩:

JS 压缩即去除 JS 代码中不必要的空格, 换行等内容 或者把一些可能公用的代码进行处理实现共享, 最后输出的结果都压缩为几行内容, 代码的可读性变得很差,通杀也能提高网站的加载速度

如果仅仅是去除空格,换行这样的压缩方式, 其实几乎是没有任何防护作用点,因为可以使用一些格式化工具,可以轻松的将其格式化,例如 Chrome 浏览器都能还原格式化的代码

目前主流的前段开发技术大多都会利用 webpack, Rollup 等工具进行打包, webpack, Rollup会对源代码进行编译和压缩, 输出几个打包好的JavaScript文件, 其中我们可以看到输出的JS文件名带有一些不规则的字符串, 同时文件内容可能只有几行, 变量名都用一些简单的字母便是, 这其中就包含JS 压缩技术, 比如一些公共的库输出成 bundle文件, 一些调用逻辑压缩和转义成几行代码, 这都属于JS压缩, 另外,其中也包含了一些很基础的JS混淆技术, 比如把变量名,方法名替换成一些简单字符, 降低代码的可读性

JavaScript 混淆:

JS混淆完全是在JS上面进行的处理, 它的目的就是使得JS变得难以阅读和分析, 大大降低代码的可读性,

JS混淆技术注意有以下几种:

变量名混淆:将带有含义的变量名,方法名,常量名随机变为无意义的类乱码字符串,降低代码的可读性,如转成单个字符或十六进制字符串

字符串混淆:将字符串阵列化几种放置并可进行MD5或base64 加密存储, 使代码中不出现明文字符串,这样可以避免使用全局搜索字符串的方式定位入口

对象键名替换: 针对JS对象的属性进行加密转化, 隐藏代码之间的调用关系

控制流平摊化:打乱函数原有代码的执行流程及函数调用关系,使代码逻辑变得混乱无序

无用代码注入:随机在代码中插入不会被执行到的无用代码, 进一步使代码看起来更加混乱

调试保护:基于调试器特性,对当前运行环境进行检验, 加入一些debugger语句, 使其在调试模式下难以顺利执行JS 代码

多台变异: 使JS代码每次被调用时, 将代码自身立刻自动发生变异, 变为与之前完全不同的代码, 即功能完全不变,只是代码形式变异,以此杜绝代码被动态分析和调试

域名锁定:使JS代码只能在指定域名下执行

代码自我保护:如果对JS代码进行格式化,则无法执行,导致浏览器假死

特殊编码:将JS完全编码为人不可读的代码,如表情符号,特殊表示内容等等

总之,以上方案都是JavaScript 混淆的实现方式, 可以在不同程度上保护JS代码

现在JS混淆主流实现是 javascript-obfuscator 和 terser 这两个库,他们都能提供一些代码混淆功能, 也都有对应的webpack 和Rollup 打包工具的插件, 我们可以非常方便的实现页面混淆, 最从输出压缩和混淆后的JS代码

下面以javescript-obfuscator为例来介绍一些代码混淆的实现:

首先,我们需要安装好 Node.js 12.x 及以上的版本, 确保可以正常使用 npm命令,具体安装可以参考:https://setup.scrape.center/nodejs

接着新建一个文件夹, 比如 js-obfuscate, 然后进入该文件夹,初始化工作空间:

npm init

接着命令行会提示我们输入一些信息,然后创建package.json文件,这就完成了项目的初始化了

Python反反爬虫:JavaScript 逆向爬虫(一)了解前端 JS 混淆,加密等技术:_第1张图片

接下来我们安装 javascript-obfuscator这个库:

npm i -D javascript-obfuscator

安装成功后, 就可以看到本地文件夹中生成了一个 node_modules文件夹, 里面就包含了 javascript-obfuscator这个库, 这就说明安装成功了:

Python反反爬虫:JavaScript 逆向爬虫(一)了解前端 JS 混淆,加密等技术:_第2张图片

接下来,我们就可以编写代码来实现一个混淆样例了,比如,新建一个main.js文件:

const code = `
let x = '1' + 1
console.log('x', x)
`
const options = {
    compact: false,
    controlFlowFlattening: true
}

const obfuscator = require('javascript-obfuscator')
function obfuscate(code, options){
    return obfuscator.obfuscate(code, options).getObfuscatedCode()
}

console.log(obfuscate(code, options))

这里我们定义了两个变量,一个是code, 即需要被混淆的代码, 另一个是混淆选项 options, 使一个Object, 接下来, 我们引入了javascript-obfuscator这个库, 然后定义了一个方法,给其传入code, options 来获取混淆后的代码, 最后控制台输出混淆后的代码, 让我们执行以下这个js 脚本:

node main.js

输出结果如下:

(function (_0x27ffd4, _0x451b2f) {
    const _0x4177f0 = _0x3d1f, _0x1676a4 = _0x27ffd4();
    while (!![]) {
        try {
            const _0x2bfb3e = parseInt(_0x4177f0(0x13e)) / 0x1 + parseInt(_0x4177f0(0x13c)) / 0x2 * (parseInt(_0x4177f0(0x13b)) / 0x3) + parseInt(_0x4177f0(0x141)) / 0x4 + -parseInt(_0x4177f0(0x143)) / 0x5 * (parseInt(_0x4177f0(0x144)) / 0x6) + parseInt(_0x4177f0(0x140)) / 0x7 * (parseInt(_0x4177f0(0x142)) / 0x8) + parseInt(_0x4177f0(0x13f)) / 0x9 * (-parseInt(_0x4177f0(0x13d)) / 0xa) + -parseInt(_0x4177f0(0x145)) / 0xb;
            if (_0x2bfb3e === _0x451b2f)
                break;
            else
                _0x1676a4['push'](_0x1676a4['shift']());
        } catch (_0x207dd2) {
            _0x1676a4['push'](_0x1676a4['shift']());
        }
    }
}(_0x25ec, 0x8f89c));
function _0x3d1f(_0x2f9c0b, _0x41bd56) {
    const _0x25ec70 = _0x25ec();
    return _0x3d1f = function (_0x3d1f54, _0x16abbb) {
        _0x3d1f54 = _0x3d1f54 - 0x13b;
        let _0x3b1185 = _0x25ec70[_0x3d1f54];
        return _0x3b1185;
    }, _0x3d1f(_0x2f9c0b, _0x41bd56);
}
let x = '1' + 0x1;
console['log']('x', x);
function _0x25ec() {
    const _0x1ffdc3 = [
        '1086852FShUtp',
        '13560ZaljuY',
        '613640cvAQgV',
        '24vIEKHY',
        '8041550gEVuoV',
        '1509jWZfgl',
        '1446sgsLgB',
        '7730YGufqH',
        '639667cSKavV',
        '3285ECwXbb',
        '3374KtNHtW'
    ];
    _0x25ec = function () {
        return _0x1ffdc3;
    };
    return _0x25ec();
}

如此简单的代码, 被我们混淆成这个样子, 其实这里我们就设定了 “控制流平坦化” 选项,整体看来, 代码的可读性大大降低了, js 调试的难度也大大增加了

接下来就让我们跟着 javascript-obfuscator 走一遍, 就能具体知道JS 混淆到底有多少方法了!

        ~~~~~~~~~~~~~~~~~~~~~~~~  请查看第二章   ~~~~~~~~~~~~~~~~~~~~~~~~~

你可能感兴趣的:(爬虫)