admin管理员组文章数量:1643178
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/Valve/fingerprintjs2/blob/master/fingerprint2.js
[2]javascript属性访问,https://developer.mozilla/en-US/docs/Web/JavaScript/Reference/Operators/Property_Accessors
[3]js转换工具,http://www.jsnice/
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://m.elefans.com/dianzi/1725201330a1012998.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论