1 背景
在PC端反抓取过程中为了识别爬虫,其中一种方式通过上报请求者的设备信息,通过设备信息来识别是否是一个爬虫。

当请求目标网站后,必须请求一个指纹计算脚本,该脚本收集设备信息,根据这些设备信息计算出一段数值并作为指纹,最终将计算结果发送给服务器。

浏览器设备信息包括navigator.userAgent、window.screen、navigator.languages等。抓取者在抓取时通常会自己构造请求、或使用无头浏览器工具,例如phantomJs。而这些工具的设备信息实际上会和真实浏览器有一定差异。因此根据这些差异达到识别的目的。

2 指纹脚本
以github上一个指纹库[1]为例,其中检测虚假浏览器的代码片段如下。

var getHasLiedBrowser = function () {
    var userAgent = navigator.userAgent.toLowerCase()
    var productSub = navigator.productSub

    // we extract the browser from the user agent (respect the order of the tests)
    var browser
    if (userAgent.indexOf('firefox') >= 0) {
      browser = 'Firefox'
    } else if (userAgent.indexOf('opera') >= 0 || userAgent.indexOf('opr') >= 0) {
      browser = 'Opera'
    } else if (userAgent.indexOf('chrome') >= 0) {
      browser = 'Chrome'
    } else if (userAgent.indexOf('safari') >= 0) {
      browser = 'Safari'
    } else if (userAgent.indexOf('trident') >= 0) {
      browser = 'Internet Explorer'
    } else {
      browser = 'Other'
    }

    if ((browser === 'Chrome' || browser === 'Safari' || browser === 'Opera') && productSub !== '20030107') {
      return true
    }

    // eslint-disable-next-line no-eval
    var tempRes = eval.toString().length
    if (tempRes === 37 && browser !== 'Safari' && browser !== 'Firefox' && browser !== 'Other') {
      return true
    } else if (tempRes === 39 && browser !== 'Internet Explorer' && browser !== 'Other') {
      return true
    } else if (tempRes === 33 && browser !== 'Chrome' && browser !== 'Opera' && browser !== 'Other') {
      return true
    }

    // We create an error to see how it is handled
    var errFirefox
    try {
      // eslint-disable-next-line no-throw-literal
      throw 'a'
    } catch (err) {
      try {
        err.toSource()
        errFirefox = true
      } catch (errOfErr) {
        errFirefox = false
      }
    }
    return errFirefox && browser !== 'Firefox' && browser !== 'Other'
  }

当爬虫开发者使用的工具或自己构造的环境与真实情况不符,那么就会被检测出来。随着爬虫开发者的深入分析,这些脚本检测的数据最终都会被模拟处理。

3 混淆的指纹脚本
为了增大爬虫开发者处理难度,反爬者也对指纹脚本进行了混淆,其中的一个片段如下。

'isCanvasSupported': function() {
                var _0x4794d1 = document[_0x49f2('0x2f6', '\x37\x52\x6e\x36')](_0x537ca9[_0x49f2('0x2f7', '\x59\x5a\x23\x41')]);
                return !!(_0x4794d1[_0x49f2('0x2f8', '\x50\x63\x6f\x6f')] && _0x4794d1['\x67\x65\x74\x43\x6f\x6e\x74\x65\x78\x74']('\x32\x64'));
            },
            'isIE': function() {
                if (_0x537ca9[_0x49f2('0x2f9', '\x23\x44\x6f\x73')](navigator[_0x49f2('0x2fa', '\x4b\x4e\x4f\x52')], _0x537ca9['\x6b\x6a\x51\x4d\x6b'])) {
                    return !![];
                } else if (_0x537ca9['\x46\x4b\x4d\x65\x6a'](navigator[_0x49f2('0x2fb', '\x65\x65\x31\x75')], _0x537ca9['\x64\x69\x56\x68\x72']) && /Trident/[_0x49f2('0x2fc', '\x72\x6f\x69\x4d')](navigator['\x75\x73\x65\x72\x41\x67\x65\x6e\x74'])) {
                    if (_0x537ca9[_0x49f2('0x2fd', '\x72\x6f\x69\x4d')](_0x537ca9[_0x49f2('0x2fe', '\x75\x48\x50\x75')], _0x537ca9[_0x49f2('0x2ff', '\x2a\x74\x4d\x5e')])) {
                        return;
                    } else {
                        return !![];
                    }
                }
                return ![];
            },
            
            'isBot': function() {
                if (_0x537ca9[_0x49f2('0x300', '\x34\x49\x24\x57')](_0x5393a1)) {
                    if (_0x537ca9[_0x49f2('0x301', '\x31\x5e\x74\x25')](_0x537ca9['\x65\x62\x71\x44\x6f'], _0x537ca9[_0x49f2('0x302', '\x50\x52\x40\x63')])) {
                        return _0x537ca9['\x44\x5a\x77\x68\x4e'];
                    } else {
                        return _0x590ce8[_0x49f2('0x303', '\x40\x76\x32\x73')](this[_0x49f2('0x304', '\x50\x63\x6f\x6f')]());
                    }
                } else {
                    return _0x537ca9['\x50\x6f\x63\x46\x70'];
                }
            }

这段代码是来识别附录中的属性。当遇到这种代码时分析难度也就加大了。这种混淆机制利用了javascript对象属性访问[2]的特性。通过括号访问属性,并将属性名通过16进制形式经过函数再次映射获取真实的属性名来进行混淆。

遇到这种脚本我们先使js转换工具[3],一般转换工具无法将混淆的结果全部还原。主要目的是将16进制表示的符号转换,方便对代码分析猜测。转换后对于设备上报脚本只关注核心对象,不需要关注无关的复杂的计算逻辑。

pc端核心对象一般有window,document,navigator,通过chrome调试,出现这些对象的地方打上断点。然后将括号内的属性复制出来,在chrome控制台执行即可知道上报的属性。随后和真实浏览器对应的属性对比,看真实浏览器这些属性都是什么样即可。这样减轻分析难度,屏蔽掉无关的逻辑分析。

当调整完毕,尝试调整方法写入真实设备。这样交替对比分析,很快就能根据线索分析出问题点。

4 总结
设备信息上报就是通过设备信息的合法性来进行反爬,为了避免爬虫开发者快速分析,会增加混淆逻辑。而对于混淆后的设备上报脚本分析,仅需要关注核心对象,然后和真实浏览器属性比较。因此这种类型混淆脚本分析相对容易。

5 参考资料
[1]浏览器指纹库,https://github.com/Valve/fingerprintjs2/blob/master/fingerprint2.js
[2]javascript属性访问,https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_Accessors
[3]js转换工具,http://www.jsnice.org/

6 附录
无头工具常见识别属性
document对象中检测
[“__webdriver_evaluate”, “__selenium_evaluate”, “__webdriver_script_function”, “__webdriver_script_func”, “__webdriver_script_fn”, “__fxdriver_evaluate”, “__driver_unwrapped”, “__webdriver_unwrapped”, “__driver_evaluate”, “__selenium_unwrapped”, “__fxdriver_unwrapped”]

window对象中检测
[“_phantom”, “__nightmare”, “_selenium”, “callPhantom”, “callSelenium”, “_Selenium_IDE_Recorder”]

external检测
window[“external”] && window[“external”].toString && window[“external”].toString()
&& window[“external”].toString().indexOf(“Sequentum”) == -1

webdriver检测
window[“document”][“documentElement”][“getAttribute”][“webdriver”]

常用信息收集

function getDeviceInfo() {

    function getObjAttributes(obj, filter) {

        try {

            if (!obj) {
                return obj;
            }

            let keyList = [];
            for (let attr in obj) {
                keyList.push(attr);
            }

            if (keyList.length < 1) {
                return {}
            }

            return keyList.reduce(function(total, curr) {

                let value = null;

                if (!filter || !filter(obj, curr)) {
                    let objType = typeof obj[curr];
                    if (objType === 'function') {
                        value = obj[curr].toString();
                    } else if (objType === 'object') {
                        value = getObjAttributes(obj[curr], filter);
                    } else {
                        value = obj[curr];
                    }
                }
                return {
                    ...total,
                    [curr]: value
                };
            }, {});
        } catch (err) {
            return {
                "error": err.stack
            }
        }
    }

    function webglInfo() {
        try {
            let canvasEle = document.createElement("canvas");
            let webglCtx = canvasEle.getContext("experimental-webgl");
            let webglDrawBuffers = webglCtx.getExtension("WEBGL_draw_buffers");
            let webglDebugRenderInfo = webglCtx.getExtension("WEBGL_debug_renderer_info");
            let anisotropic = webglCtx.getExtension("EXT_texture_filter_anisotropic") || webglCtx.getExtension("WEBKIT_EXT_texture_filter_anisotropic") || webglCtx.getExtension("MOZ_EXT_texture_filter_anisotropic");
            let anisotropicExt = webglCtx.getParameter(anisotropic.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
            let maxVertexTextureImageUnits = webglCtx.getShaderPrecisionFormat ? webglCtx.getShaderPrecisionFormat(webglCtx.VERTEX_SHADER, webglCtx.MEDIUM_FLOAT).precision : 0;
            let fragmentShaderBestPrecision = webglCtx.getShaderPrecisionFormat ? webglCtx.getShaderPrecisionFormat(webglCtx.FRAGMENT_SHADER, webglCtx.MEDIUM_FLOAT).precision : 0;
            let fragmentShaderFloatIntPrecision = (webglCtx.getShaderPrecisionFormat(webglCtx.FRAGMENT_SHADER, webglCtx.HIGH_FLOAT).precision ? "highp/" : "mediump/") + (webglCtx.getShaderPrecisionFormat(webglCtx.FRAGMENT_SHADER, webglCtx.HIGH_INT).rangeMax ? "highp" : "lowp")

            return {
                "WEBGL_draw_buffers": webglDrawBuffers,
                "MAX_DRAW_BUFFERS_WEBGL": webglDrawBuffers ? webglCtx.getExtension(webglDrawBuffers.MAX_DRAW_BUFFERS_WEBGL) : null,
                "RENDERER": webglCtx.getParameter(webglCtx.RENDERER),
                "VENDOR": webglCtx.getParameter(webglCtx.VENDOR),
                "VERSION": webglCtx.getParameter(webglCtx.VERSION),
                "UNMASKED_RENDERER_WEBGL": webglCtx.getParameter(webglDebugRenderInfo.UNMASKED_RENDERER_WEBGL),
                "UNMASKED_VENDOR_WEBGL": webglCtx.getParameter(webglDebugRenderInfo.UNMASKED_VENDOR_WEBGL),
                "STENCIL_TEST": webglCtx.isEnabled(webglCtx.STENCIL_TEST),
                "SHADING_LANGUAGE_VERSION": webglCtx.getParameter(webglCtx.SHADING_LANGUAGE_VERSION),
                "RED_BITS": webglCtx.getParameter(webglCtx.RED_BITS),
                "GREEN_BITS": webglCtx.getParameter(webglCtx.GREEN_BITS),
                "BLUE_BITS": webglCtx.getParameter(webglCtx.BLUE_BITS),
                "ALPHA_BITS": webglCtx.getParameter(webglCtx.ALPHA_BITS),
                "MAX_RENDERBUFFER_SIZE": webglCtx.getParameter(webglCtx.MAX_RENDERBUFFER_SIZE),
                "MAX_COMBINED_TEXTURE_IMAGE_UNITS": webglCtx.getParameter(webglCtx.MAX_COMBINED_TEXTURE_IMAGE_UNITS),
                "MAX_CUBE_MAP_TEXTURE_SIZE": webglCtx.getParameter(webglCtx.MAX_CUBE_MAP_TEXTURE_SIZE),
                "MAX_FRAGMENT_UNIFORM_VECTORS": webglCtx.getParameter(webglCtx.MAX_FRAGMENT_UNIFORM_VECTORS),
                "MAX_TEXTURE_IMAGE_UNITS": webglCtx.getParameter(webglCtx.MAX_TEXTURE_IMAGE_UNITS),
                "MAX_TEXTURE_SIZE": webglCtx.getParameter(webglCtx.MAX_TEXTURE_SIZE),
                "MAX_VARYING_VECTORS": webglCtx.getParameter(webglCtx.MAX_VARYING_VECTORS),
                "MAX_VERTEX_ATTRIBS": webglCtx.getParameter(webglCtx.MAX_VERTEX_ATTRIBS),
                "MAX_VERTEX_UNIFORM_VECTORS": webglCtx.getParameter(webglCtx.MAX_VERTEX_UNIFORM_VECTORS),
                "ALIASED_LINE_WIDTH_RANGE": webglCtx.getParameter(webglCtx.ALIASED_LINE_WIDTH_RANGE),
                "ALIASED_POINT_SIZE_RANGE": webglCtx.getParameter(webglCtx.ALIASED_POINT_SIZE_RANGE),
                "MAX_VIEWPORT_DIMS": webglCtx.getParameter(webglCtx.MAX_VIEWPORT_DIMS),
                "anisotropicExt": anisotropicExt,
                "maxVertexTextureImageUnits": maxVertexTextureImageUnits,
                "MAX_VERTEX_TEXTURE_IMAGE_UNITS": webglCtx.getParameter(webglCtx.MAX_VERTEX_TEXTURE_IMAGE_UNITS),
                "fragmentShaderBestPrecision": fragmentShaderBestPrecision,
                "DEPTH_BITS": webglCtx.getParameter(webglCtx.DEPTH_BITS),
                "STENCIL_BITS": webglCtx.getParameter(webglCtx.STENCIL_BITS),
                "getSupportedExtensions": webglCtx.getSupportedExtensions(),
                "fragmentShaderFloatIntPrecision": fragmentShaderFloatIntPrecision
            };
        } catch (err) {
            return {
                "error": err.stack
            }
        }
    }

    let deviceInfo = {
        "window.navigator": getObjAttributes(navigator, (obj,name)=>name === 'enabledPlugin'),
        "window.screen": getObjAttributes(window.screen),
        "window.innerHeight": window.innerHeight,
        "window.innerWidth": window.innerWidth,
        "window.outerHeight": window.outerHeight,
        "window.outerWidth": window.outerWidth,
        "window.history.length": window.history.length,
        "window.performance": getObjAttributes(window.performance),
        "window.eval.toString().length": window.eval.toString().length,
        "window.devicePixelRatio": window.devicePixelRatio,
        "window.speed": window.speed,
        "window.deviceorientation": !window.deviceorientation ? (typeof window.deviceorientation) : window.deviceorientation,
        "window.ontouchstart": window.ontouchstart,
        "window.doNotTrack": window.doNotTrack,
        "window.chrome": getObjAttributes(window.chrome),
        "timezoneOffset": (new Date).getTimezoneOffset(),
        "timezone": getObjAttributes(new window.Intl.DateTimeFormat().resolvedOptions()),
        "webGlInfo": webglInfo()
    }

    return JSON.stringify(deviceInfo);
}

来源:https://blog.csdn.net/Revivedsun/java/article/details/89941267

作者 铁血 汉子 2020年4月9日
2024/12/21/05:23:56pm 2020/4/9/8:54:28
0 2029