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