协议指纹
这部分是最容易被忽略的,因此放在最前面。
TCP/IP指纹
就是检测IP层和TCP层的包头,不同系统可能会有差异,不过能提供的熵并不大:

对于IP层,IHL(IP头长度)、TTL值可辨别,不过易受路由等影响。
对于TCP层,窗口大小、DF标志、选项字段(如MSS、窗口缩放、时间戳)等可用于辨识。
SSL/TLS指纹
TLS太复杂了,功能多,参数多,因此能提供大量的信息用于指纹识别。对TLS指纹研究可以追溯到Analysis of Googlebot's frugal cipher suite list - 2009,TLS fingerprintingSmarter Defending & Stealthier Attacking - 2015,再后来就有了Open Sourcing JA3,JA3收集了Client Hello里的更多信息--TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats
,最后通常会算一个JA3 Hash,不过在做风控时关注具体值就好。除了关注客户端指纹的JA3当然还有关注服务端的JA3S,不过这更多会在渗透测试中识别目标资产吧。
其实JA3面临一些问题,比如说RFC规定扩展顺序不影响握手,但它却可以改变JA3,另外它包括的参数不完全,所以还可以继续优化,比如peetprint就是分析更多信息,并将扩展顺序规范化后算出的。
总的来说这一块不应只满足于原始的JA3锁收集内容,每一个域都是可能的,而且不仅有静态信息,可通过协商进一步测试其能力,来识别一些粗糙的伪造。
对于这一部分指纹,可使用tls.peet.ws、browserleaks、scrapfly.io等工具检测
对抗简述
通常会用白名单和黑名单去做对抗,前者模拟真实的指纹而后者只是规避明显错误的指纹(如python原始发出的握手明显异常),所以成熟的攻击者都会使用前者,而ssl/tls太复杂了,要模拟好是个工程上的挑战。
例如chrome从110版开始默认启用GREASE机制(TLS中的GREASE机制),通过随机化扩展顺序(TLS RFC 4.2规定除pre_shared_key
必须是最后一个外不同类型的扩展顺序任意)来尽快发现服务端TLS实施错误,另外它还急急忙忙自己实现了alps,这些都是openssl未实现的扩展能力,因此若要用openssl模拟相关行为需要自己实现对应扩展。而有些人也会通过直接改底层ssl库依赖去获取所需能力,例如将openssl替换为boringssl,亦或者自己实现一套,如Go的uTLS库,它专为对抗指纹而生,不过其实也就是能发一些fake包,实际并未实现功能,如果服务端再主动识别依然得露馅儿。这里简单说下怎么改openssl去模拟boringssl吧~
1.cipher suite一致:tls1.2和tls1.3的加密套件格式存在区别且完全互斥(tls1.2及之前与tls1.3的套件不能混用),如(ECDHE-RSA-AES256-SHA vs TLS_AES_256_GCM_SHA384),
参考Nginx with TLS 1.3(OpenSSL) SSL_CTX_set_ciphersuites
2.添加grease值,openssl上有个未被合并的prAdd RFC8701 GREASE Support #19646也已经实现了部分GREASE能力,可以在此基础上修改,注意boringssl的实现未完全按rfc来,模仿就得跟google学
3.extensions乱序
HTTP协议
HTTP指纹
有些信息不一致不影响整个通信,比如每个浏览器可以有自己的请求头,有自己的请求头顺序,请求头有自己的值,这些都不影响通信但可以作为指纹
HTTP2指纹
HTTP2协议上的一些特性可被用做指纹识别,HTTP2协议细节见RFC9113,这里简单描述下(也可见hpbn)。HTTP2在上层语义和之前的版本没什么差别,主要升级在传输上,它不再是明文传输,而是用二进制格式,数据分三层:
1.frame帧:最小通信单元,H2有多种不同类型的帧,如HEADER/DATA帧就是用于传递请求头和数据的,而为了实现其他功能(如流控)还存在多种功能帧
2.message消息:有一个或多个帧组成,如请求或响应消息
3.stream流:虚拟通道,消息在指定的流上传递,可通过打开多个流来进行多路复用等
它们关系可见下图:
也可以在wireshark上下载几个sample体验下。这里涉及到的指纹分两点:请求头与akamai hash
cookie组成
在H2中cookie头可出现多次,也可以出现一次并用','分割多个值,前者是大多数实现的方式,但自己写代码很可能就无意识的改成后者的样子了。
akamai hash
这才是重点,原文可见AKAMAI WHITE PAPER - Passive Fingerprinting of HTTP/2 Clients,具体来说涉及到两个点:
1.伪头顺序:http头字段的顺序一直是个指纹识别点,而H2还额外加了4个伪头(:schema
/:method
/:authority
/:path
),它们的顺序也是个识别点
2.SETTING帧和WINDOW_UPDATE帧立大功:这些帧就相当于一个应用的默认配置,不同的应用可能不同,因此也能作为特征,具体语义看RFC吧
当然还有优先级帧和帧里的优先级字段,都得认真对比哦
应用级指纹
其实这个没有太多说的,因为它要采集的数据都得通过HTTP协议上报,分析这些数据即可,但这里还是列出一些常见的,只关注chromium哦。
通用静态数据
下面这些是必收集,又容易被伪造的静态数据:
navigator.userAgent // 浏览器、操作系统和其他相关信息
navigator.appVersion // 浏览器的版本
navigator.platform // 操作系统平台,如Win32
navigator.vendor/navigator.vendorSub // 供应商和子供应商信息,如Google Inc.,后者通常为空
navigator.product/navigator.productSub // 浏览器产品名
navigator.oscpu // CPU架构
navigator.language/languages/userLanguage/browserLanguage/systemLanguage // 当前语言和语言列表
navigator.hardwareConcurrency // 处理器数
navigator.deviceMemory // 内存大小 GB
navigator.plugins // 浏览器插件
navigator.mimeTypes // 支持的mime类型
navigator.doNotTrack // 不要追我,通常没人改它 "1" (启用), "0" (禁用), 或 null (未设置)
navigator.maxTouchPoints // 最大支持触控点数
'ontouchstart' in window // 是否支持触控
Error.stackTraceLimit // 栈回溯层数
navigator.pdfViewerEnabled // 是否支持pdf
performance.memory.jsHeapSizeLimit // 堆大小
下面再是和屏幕/窗口相关的:
screen.width/screen.height // 屏幕(显示器)的宽和高
screen.availWidth/screen.availHeight
screen.colorDepth // 屏幕的颜色深度
screen.pixelDepth // 屏幕的像素深度
window.innerWidth/window.innerHeight // 浏览器窗口的内部宽高(单位px),包含滚动条的可视内容区域
window.outerWidth/window.outerHeight // 浏览器窗口的外部宽高(单位px),是整个浏览器窗口的宽度,包括工具栏、地址栏等
window.devicePixelRatio
这些都是主屏的,如果要知道每个屏幕,需要window.getScreenDetails
接口,它需要授权。
主动一致性检测
静态数据容易被伪造,所以需要多方验证数据一致性,最好的方式是主动测试,下面是一些例子:
1.检测窗口宽高是否异常:
function deviceWidthLie() {
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
if (window.matchMedia) {
var mediaQuery = '(max-width:' + windowWidth + 'px) and (max-height:' + windowHeight + 'px)';
return !window.matchMedia(mediaQuery).matches;
}
}
2.检测CPU是否是32位:
// 定义常量
const NOT_SUPPORT = "-1";
const UNKNOWN_TYPE = "0";
const ARM_TYPE = "1";
const X86_TYPE = "2";
const ARM_VAL = 127;
const X86_VAL = 255;
function isFloat32ArraySupported(): boolean {
return typeof Float32Array === 'function';
}
function isUint8ArraySupported(): boolean {
return typeof Uint8Array === 'function';
}
function getCPUType(): string {
if (!isFloat32ArraySupported() || !isUint8ArraySupported()) {
return NOT_SUPPORT;
}
try {
// 创建一个 Float32Array 和与其共享缓冲区的 Uint8Array
const floatArray = new Float32Array(1);
const byteArray = new Uint8Array(floatArray.buffer);
// 将 Float32Array 的第一个元素设置为无穷大,然后减去自身
floatArray[0] = 1 / 0;
floatArray[0] = floatArray[0] - floatArray[0];
// 根据 Uint8Array 的第四个字节判断 CPU 类型
const value = byteArray[3];
if (value === ARM_VAL) {
return ARM_TYPE;
} else if (value === X86_VAL) {
return X86_TYPE;
} else {
return UNKNOWN_TYPE;
}
} catch (e) {
// 如果发生异常,返回不支持
return NOT_SUPPORT;
}
}
const cpuType = getCPUType();
浏览器识别
去识别不同浏览器类型、同类型不同版本,方法就是依赖不同浏览器(版本间的差异):
- 独有的api
- 内建属性顺序差异
- api行为差异
这一块能检测的地方太多了,下面列出部分:
1.标志变量
'MSCSSMatrix', 'msSetImmediate', "msIndexedDB", 'msMaxTouchPoints', 'msPointerEnabled' // Trident标志
'addBehavior' // ie标志
'webkitPersistentStorage', 'webkitTemporaryStorage', 'webkitResolveLocalFileSystemURL', 'webkitMediaStream', 'webkitSpeechGrammar' // webkit标志
'InstallTrigger' // firefox标志
2.支持的媒体类型
function getSupportedMediaTypes() {
const mediaTypes = [
'audio/mpeg',
'video/mp4; codecs="avc1.42E01E"',
'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'
].sort();
const videoElement = document.createElement('video');
const audioElement = new Audio();
return mediaTypes.reduce((supportedTypes, mimeType) => {
const supportInfo = {
mimeType: mimeType,
audioPlayType: audioElement.canPlayType(mimeType),
videoPlayType: videoElement.canPlayType(mimeType),
mediaSource: MediaSource?.isTypeSupported(mimeType),
mediaRecorder: MediaRecorder?.isTypeSupported(mimeType)
};
if (supportInfo.audioPlayType || supportInfo.videoPlayType || supportInfo.mediaSource || supportInfo.mediaRecorder) {
supportedTypes.push(supportInfo);
}
return supportedTypes;
}, []);
}
3.音频支持
function catchFunc(e) {
try {
return e
} catch (e) {
return 'error'
}
}
// audioCodecs
function audioCodecs() { // 还有很多,这里就列部分
var r = document.createElement("audio");
return r.canPlayType ? {
ogg: catchFunc(r.canPlayType('audio/ogg; codecs="vorbis"')),
mp3: catchFunc(r.canPlayType('audio/mpeg;')),
wav: catchFunc(r.canPlayType('audio/wav; codecs="1"')),
m4a: catchFunc(r.canPlayType('audio/x-m4a;')),
aac: catchFunc(r.canPlayType('audio/aac;'))
} : {}
}
// videoCodecs
function videoCodecs() {
var r = document.createElement("video");
return r.canPlayType ? {
ogg: catchFunc(r.canPlayType('video/ogg; codecs="theora"')),
h264: catchFunc(r.canPlayType('video/mp4; codecs="avc1.42E01E"')),
webm: catchFunc(r.canPlayType('video/webm; codecs="vp8, vorbis"'))
} : {}
}
// VoiceSys
function getVoiceSys() {
const VOICE_SYSTEMS = {
MAC: 'Mac',
WINDOWS: 'Windows',
CHROME_OS: 'Chrome OS',
ANDROID: 'Android'
};
const TIMEOUT = 500;
const ERROR_TYPES = {
NOT_SUPPORTED: 'not-sup',
TIMEOUT: 'time-out',
GENERAL: 'thr-err'
};
function detectVoiceSystem(voices) {
if (voices.some(voice => /lekha/i.test(voice.name))) return VOICE_SYSTEMS.MAC;
if (voices.some(voice => /microsoft/i.test(voice.name))) return VOICE_SYSTEMS.WINDOWS;
if (voices.some(voice => /chrome os/i.test(voice.name))) return VOICE_SYSTEMS.CHROME_OS;
if (voices.some(voice => /android/i.test(voice.name))) return VOICE_SYSTEMS.ANDROID;
return undefined;
}
return new Promise((resolve) => {
if (!('speechSynthesis' in window)) {
resolve(ERROR_TYPES.NOT_SUPPORTED);
return;
}
let voicesLoaded = false;
function handleVoicesChanged() {
const voices = speechSynthesis.getVoices();
if (voices.length) {
voicesLoaded = true;
const voiceSystem = detectVoiceSystem(voices);
resolve(voiceSystem);
}
}
handleVoicesChanged();
speechSynthesis.onvoiceschanged = handleVoicesChanged;
setTimeout(() => {
if (!voicesLoaded) resolve(ERROR_TYPES.TIMEOUT);
}, TIMEOUT);
}).catch(() => ERROR_TYPES.GENERAL);
}
4.css/h5接口特性支持度,可参考Modernizr/css3test:
function getEngine() {
var n = []["constructor"];
try {
(-1)["toFixed"](-1)
} catch (t) {
return t.message.length + (n+"").split(n.name).join("").length
}
}
const Platform = {
WINDOWS: 'Windows',
MAC: 'Mac',
LINUX: 'Linux',
ANDROID: 'Android',
CHROME_OS: 'Chrome OS'
};
function getPlatformDetected() {
if (!(80 == getEngine())) return [];
const supports = {
colorScheme: CSS.supports("color-scheme: initial"),
appearance: CSS.supports("appearance: initial"),
aspectRatio: CSS.supports("aspect-ratio: initial"),
borderEndEndRadius: CSS.supports("border-end-end-radius: initial"),
videoPlaybackQuality: "getVideoPlaybackQuality" in HTMLVideoElement.prototype,
randomUUID: "randomUUID" in Crypto.prototype,
barcodeDetector: "BarcodeDetector" in window,
downlinkMax: "downlinkMax" in navigator.connection,
contentIndex: "ContentIndex" in window,
contactsManager: "ContactsManager" in window,
eyeDropper: "EyeDropper" in window,
fileSystemWritableFileStream: "FileSystemWritableFileStream" in window,
hid: "HID" in window,
hidDevice: "HIDDevice" in window,
serialPort: "SerialPort" in window,
serial: "Serial" in window,
sharedWorker: "SharedWorker" in window,
touchEvent: "TouchEvent" in window,
setAppBadge: "setAppBadge" in navigator,
};
const platformFeatures = {
[Platform.ANDROID]: [
supports.aspectRatio && supports.barcodeDetector,
supports.appearance && supports.contentIndex,
supports.videoPlaybackQuality && supports.contactsManager,
supports.downlinkMax,
supports.randomUUID && !supports.fileSystemWritableFileStream,
supports.barcodeDetector && !supports.eyeDropper,
supports.barcodeDetector && !supports.hidDevice,
!supports.sharedWorker && supports.touchEvent,
!supports.sharedWorker && supports.setAppBadge,
],
[Platform.WINDOWS]: [
supports.aspectRatio && !supports.barcodeDetector,
supports.appearance && !supports.contentIndex,
supports.videoPlaybackQuality && !supports.contactsManager,
!supports.downlinkMax,
supports.randomUUID && supports.fileSystemWritableFileStream,
supports.randomUUID && supports.contactsManager,
supports.borderEndEndRadius && supports.hidDevice,
supports.borderEndEndRadius && supports.serial,
!supports.sharedWorker && !supports.touchEvent,
supports.colorScheme && !supports.setAppBadge,
],
[Platform.MAC]: [
supports.aspectRatio && supports.barcodeDetector,
supports.appearance && !supports.contentIndex,
supports.videoPlaybackQuality && !supports.contactsManager,
!supports.downlinkMax,
supports.randomUUID && !supports.fileSystemWritableFileStream,
supports.randomUUID && supports.contactsManager,
supports.borderEndEndRadius && supports.hidDevice,
supports.borderEndEndRadius && supports.serial,
supports.sharedWorker && !supports.touchEvent,
supports.colorScheme && supports.setAppBadge,
],
[Platform.LINUX]: [
!supports.aspectRatio && !supports.barcodeDetector,
!supports.appearance && !supports.contentIndex,
supports.videoPlaybackQuality && !supports.contactsManager,
!supports.downlinkMax,
supports.randomUUID && supports.fileSystemWritableFileStream,
supports.randomUUID && supports.contactsManager,
supports.borderEndEndRadius && supports.hidDevice,
supports.borderEndEndRadius && supports.serial,
supports.sharedWorker && (!supports.touchEvent || !supports.touchEvent),
supports.colorScheme && !supports.setAppBadge,
],
};
const platformScores = {};
for (const platform in platformFeatures) {
const features = platformFeatures[platform];
const supportedFeatures = features.filter(feature => feature);
const score = (supportedFeatures.length / features.length).toFixed(2);
platformScores[platform] = score;
}
return platformScores;
}
5.对错误的的显示
function getErrors(e) {
var t, n = [], o = e.length;
for (t = 0; t < o; t++) try {
e[t]()
} catch (e) {
n.push(e.message)
}
return n
}
var hashify = function (e) {
var t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 'SHA-256',
n = "" + JSON.stringify(e), o = (new TextEncoder).encode(n);
return crypto.subtle.digest(t, o).then((function (e) {
return Array.from(new Uint8Array(e)).map((function (e) {
return ("00" + e.toString(16)).slice(-2)
})).join("")
})).catch((function () {
}))
};
function getConsoleErrors() {
try {
var t = getErrors([function () {
return new Function('alert(")')()
}, function () {
return new Function('const foo;foo.bar')()
}, function () {
return new Function("null.bar")()
}, function () {
return new Function('abc.xyz = 123')()
}, function () {
return new Function('const foo;foo.bar')()
}, function () {
return new Function('(1).toString(1000)')()
}, function () {
return new Function("[...undefined].length")()
}, function () {
return new Function("var x = new Array(-1)")()
}, function () {
return new Function('const a=1; const a=2;')()
}]), r = {};
r.errors = t;
return r;
} catch (e) {
return
}
}
function getErrorHash() {
var e = hH;
try {
var t = getConsoleErrors();
if (!t) return;
var r = hashify(t.errors);
if (!r) return;
return r
} catch (e) {
return
}
}
6.对数学计算的精度
7.对DOM元素大小差异(利用getClientRects
)
8....
浏览器插件
早期的重灾区,有actionscript(flash)
/java applet
/plugins(npapi)
等,但都因为安全性问题退出了,现在一般就检查下plugins:
navigator.plugins
扩展识别
Chrome里网页API(相对于扩展API)无法直接获取插件信息,但是可以利用多种技术去探测特定插件是否存在[13] [9] [0] [18] [19]
XX
WAS
CSS注入
DOM改变
PostMessage通信
不属于当前页面的JS
function getChromeExtensionScripts() {
// 获取所有的 <script> 元素
const scripts = document.getElementsByTagName('script');
const chromeExtensionScripts = [];
// 如果没有找到任何 <script> 元素,直接返回空数组
if (!scripts || scripts.length === 0) return JSON.stringify(chromeExtensionScripts);
// 遍历所有 <script> 元素
for (let i = 0; i < scripts.length; i++) {
const script = scripts[i];
const src = script.getAttribute('src');
// 检查 src 属性是否存在且包含 "chrome-extension://"
if (src && src.indexOf("chrome-extension://") >= 0) {
chromeExtensionScripts.push(src);
}
}
// 返回包含 Chrome 扩展脚本源的 JSON 字符串
return JSON.stringify(chromeExtensionScripts);
}
结构分析
行为分析
时间和时区
比如IP是美国,时区却是中国,露馅:
const now = new Date();
now.toISOString() // ISO 8601格式的时间
now.toLocaleString() // 本地时间
const getYear = (date) => date.getFullYear(); // 最大时区偏移
const getTimezoneOffset = (date) => date.getTimezoneOffset();
const maxTimezoneOffset = () => {
const year = getYear(new Date());
const januaryOffset = getTimezoneOffset(new Date(year, 0, 1));
const julyOffset = getTimezoneOffset(new Date(year, 6, 1));
return Math.max(januaryOffset, julyOffset);
};
Intl.DateTimeFormat().resolvedOptions().timeZone // 时区
安全环境
有些会使用中间人强制ssl/tls降级(sslstrip)来获取数据,此时可检测当前环境是否还是https,方法太多了:
window.location/document.location // 直接从url里看
window.isSecureContext // 安全上下文
navigator.clipboard // 安全上下文才会出现的api,但是有些浏览器本身不支持...
document.querySelectorAll('a') // 查找页面所有链接,看是不是正常的
行为相关
传感器数据收集
这主要针对手机(如浏览器/webview,native本身提供的不在讨论范围),它们支持多种传感器且浏览器可使用,主要是加速度计、陀螺仪、环境光传感器和磁力计,不过并不是所有浏览器都支持,使用前需判断[15]:
// 加速度
window.addEventListener('devicemotion', function(event) {
console.log('加速度:', event.acceleration);
console.log('加速度(包括重力):', event.accelerationIncludingGravity);
console.log('旋转速率:', event.rotationRate);
}, true);
// 陀螺仪
window.addEventListener('deviceorientation', function(event) {
console.log('Alpha:', event.alpha);
console.log('Beta:', event.beta);
console.log('Gamma:', event.gamma);
}, true);
// 环境光
const sensor = new AmbientLightSensor();
sensor.onreading = () => {
console.log('光照强度:', sensor.illuminance);
};
sensor.start();
// 绝对方向 结合加速度计、陀螺仪和磁力计
const sensor = new AbsoluteOrientationSensor();
sensor.onreading = () => {
console.log('四元数:', sensor.quaternion);
console.log('欧拉角:', sensor.euler);
};
sensor.start();
// 磁力计
const sensor = new Magnetometer();
sensor.onreading = () => {
console.log('磁场强度:', sensor.x, sensor.y, sensor.z);
};
sensor.start();
Clipboard
在浏览器里剪切板的权限是严格受限的,无法直接读取里面的内容,但可以通过监听剪切板事件来发现一些异常行为(谁没事疯狂复制粘贴呀!):
document.addEventListener('copy', () => {});
document.addEventListener('paste', (e) => {console.log(e.clipboardData.getData('text');)});
用户输入
这里就是鼠标/键盘/触屏/触摸板等用户输入设备产生的事件,这是风控中的重中之重,也是最难处理的之一,下面简单描述:
- 鼠标事件:包括
mousedown
,mouseup
,mousemove
,mouseover
,mouseout
,click
,dblclick
等,捕获鼠标操作。 - 触控事件:包括
touchstart
,touchmove
,touchend
, 和touchcancel
,捕获触控操作。 - 键盘事件:包括
keydown
,keyup
,keypress
,可捕获输入的数据。 - 其它事件:包括
scroll
,dragstart
,drag
,dragend
,drop
,blur
等,它们是鼠标/触控板等发出的。
在对这些事件做收集时,通常包括单一信息和综合信息:
- 单一信息:比如一个点击操作,它的坐标是什么,距离当前元素的中心点有多远,该次的触摸点数量,大小和按压力度、倾斜角度,对键盘事件记录按下的键等
- 行为序列:对于鼠标和触控,它是可以采样行为轨迹的,还有点击前的悬停,按压的用时等,对于轨迹模拟,传统的方法是用贝塞尔曲线模拟,或者通过收集大量样本加偏重放,不过简单的用AI已经能识别咯,之后也得用AI对抗!像键盘输入,也是有规律的,需符合人/输入法的输入习惯!
这些都是事件,它们可以直接被JS创建,但是有个域会有问题,即Event.isTrusted, isTrusted
是 JavaScript Event
接口的一个只读属性,它是一个布尔值。它的作用是判断一个事件是否是由用户代理(即浏览器本身)生成的,包括用户真实的操作(如点击鼠标、敲击键盘)以及通过浏览器内部机制触发的合法事件(如 HTMLElement.focus()
)。简而言之,只要是用JS生成的事件(EventTarget.dispatchEvent()
)它的该属性一定会是false,对此只能通过修改浏览器源码做对抗,不过通过CDP发出的事件是true哦!
自动化相关
如果浏览器正在被自动化(如群控),那会有些特征:
-
navigator.webdriver
:它是一个布尔值,默认为false,根据W3C WebDriver要求,在被自动化时webdriver需要将其设置为true以便让document知道正在被自动化。 -
Page Visibility
:这是个容易被忽略的点,JS能通过document.visbilityState
去查看当前页面的可见性,也可通过document.visibilitychange
监听它的改变,它有三种值visible
/hidden
/prerender
,详见Page Visibility API 教程,需要关注的就是如果当前页面最小化/锁屏了/被完全遮挡,那么它就是不可见的!(另还有document.hidden
效果类似) -
domAutomation
:chrome开启自动化(如测试)时,会出现,可直接delete
属性,也可在启动参数里禁用--disable-blink-features=AutomationControlled
-
cdc_adoQpoasnfa76pfcZLmcfl_
:chrome里硬编码的自动化属性,含三个cdc_adoQpoasnfa76pfcZLmcfl_Array
/cdc_adoQpoasnfa76pfcZLmcfl_Promise
/cdc_adoQpoasnfa76pfcZLmcfl_Symbol
,直接delete
可删除。 -
performance.now()
:更详细内容可以看[10],这里主要关注常见的风控指纹点,它利用高精度时间performance.now()
,现在浏览器也在主动降低精度 -
window.webdriver
:这是selenium标记的,不要和1搞混了,也是可以删
电池信息
看是否支持电池信息,如果有就收集电池信息的情况,比看变化是否正常(如群控可能会一直满电且冲着电,注意桌面端也会有哦,例如笔记本电脑):
if (navigator.getBattery) {
navigator.getBattery().then(function(battery) {
console.log('当前(初始电量):', battery.level); // 0.0 到 1.0 之间的浮点数
console.log('电池是否正在充电:', battery.charging);
console.log('充满电所需的时间:', battery.chargingTime); // 单位秒
console.log('放电到完全没电所需的时间:', battery.dischargingTime);
battery.addEventListener('chargingchange', function() {
console.log('充电状态改变:', battery.charging);
});
battery.addEventListener('levelchange', function() {
console.log('电量改变:', battery.level);
});
battery.addEventListener('chargingtimechange', function() {
console.log('充满时间改变:', battery.chargingTime);
});
battery.addEventListener('dischargingtimechange', function() {
console.log('剩余可用时间变化:', battery.dischargingTime);
});
});
} else {
console.log('不支持哦~');
}
网络信息
获取网络有关的信息,主要是看wifi/蜂窝网络等:
navigator.connection.type // 获取网络类型
navigator.connection.downlink // 获取下行速度
navigator.connection.effectiveType // 获取有效网络类型,不是实际类型
navigator.connection.rtt // 获取往返时间
授权
获取用户授予了哪些权限,如地理位置,通知等,(摄像头、麦克风太敏感了,小心):
if (navigator.permissions) {
navigator.permissions.query({ name: 'geolocation' }).then(function(permissionStatus) {
console.log('Geolocation permission:', permissionStatus.state); // 例如: "granted", "denied", "prompt"
});
}
地理位置
位置信息显然是需要用户明确授权的,否则只能通过IP去拿,这里还是记一下吧:
navigator.geolocation.getCurrentPosition(
// navigator.geolocation.watchPosition 可持续监控变化
(position) => {
const { latitude, longitude } = position.coords;
},
(error) => {
...
},
{
enableHighAccuracy: true, // 启用高精度定位
timeout: 5000, // 超时时间,单位ms
maximumAge: 0 // 最大缓存时间,0表示不使用缓存
}
)
canvas指纹
接下来几个是当前的核心指纹,它们的熵值都挺高,以canvas开始,它是html5新出的标准api,该api用于绘制各种图形,包括2D和3D等。此处说的canvas主要是指2D图形绘制,受软硬件栈影响(字体渲染、图像抗锯齿、颜色管理、GPU驱动和硬件等)[16 ],生成的图形会有细微的差别,利用它即可检测:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Fingerprint</title>
</head>
<body>
<canvas id="fingerprintCanvas" width="200" height="100" style="display: none;"></canvas>
<script>
const canvas = document.getElementById('fingerprintCanvas');
const ctx = canvas.getContext('2d');
// 设置字体样式
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
// 绘制文本
ctx.fillText('Hello, World!', 10, 10);
// 绘制图形
ctx.fillStyle = '#f60';
ctx.beginPath();
ctx.moveTo(120, 10);
ctx.lineTo(120, 60);
ctx.lineTo(160, 60);
ctx.closePath();
ctx.fill();
// 获取Canvas内容
const canvasDataUrl = canvas.toDataURL();
// 计算哈希值
function calculateHash(dataUrl) {
const binaryString = atob(dataUrl.split(',')[1]);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const hashBuffer = crypto.subtle.digest('SHA-256', bytes);
return hashBuffer.then(hash => {
const hashArray = Array.from(new Uint8Array(hash));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
});
}
calculateHash(canvasDataUrl).then(fingerprint => {
console.log('Canvas Fingerprint:', fingerprint);
});
</script>
</body>
</html>
字体指纹
以前的浏览器可通过插件直接获取系统存在的字体列表,甚至有字体间顺序,但是现代浏览器已经不支持该行为了,不过还是能通过一些其它方式获取系统存在哪些字体,具体来讲就是给一个完整的字体列表,通过一些办法去判断这个字体是否存在,判断方式常见的有两种[2]:
1.通过CSS指定特定字体去修饰块,以一定存在和一定不存在的字体为基准,通过检测元素宽度识别字体存在性[4]:
class FontDetector {
private baseFonts: string[];
private testString: string;
private testSize: string;
private body: HTMLElement;
private span: HTMLSpanElement;
private defaultWidth: { [key: string]: number };
private defaultHeight: { [key: string]: number };
constructor() {
this.baseFonts = ['monospace', 'sans-serif', 'serif']; // 这三个字体最常见,以此为基准
this.testString = "mmmmmmmmmmlli"; // m/w宽,l/i窄
this.testSize = '72px'; // 这里用大点的尺寸来放大差异
this.body = document.getElementsByTagName("body")[0];
this.span = document.createElement("span"); // 创建span来做容器
this.span.style.fontSize = this.testSize;
this.span.innerHTML = this.testString;
this.defaultWidth = {};
this.defaultHeight = {};
for (const font of this.baseFonts) {
this.span.style.fontFamily = font;
this.body.appendChild(this.span);
this.defaultWidth[font] = this.span.offsetWidth;
this.defaultHeight[font] = this.span.offsetHeight;
this.body.removeChild(this.span);
}
}
public detect(font: string): boolean {
let detected = false;
for (const baseFont of this.baseFonts) {
this.span.style.fontFamily = `${font}, ${baseFont}`;
this.body.appendChild(this.span);
const matched = (this.span.offsetWidth !== this.defaultWidth[baseFont] || this.span.offsetHeight !== this.defaultHeight[baseFont]); // 如果与基准不同则认为存在
this.body.removeChild(this.span);
detected = detected || matched;
}
return detected;
}
}
const detector = new FontDetector();
console.log(detector.detect('Arial'));
2.通过canvas的measureText
去测量,同样以一定存在和一定不存在的字体为基准,通过分析生成的字体形状(如宽高、倾斜度等)即可推测字体是否存在[3]:
// 定义字体家族
const CSS_FONT_FAMILY = "\n\t'Segoe Fluent Icons',\n\t'Ink Free',\n\t'Bahnschrift',\n\t'Segoe MDL2 Assets',\n\t'HoloLens MDL2 Assets',\n\t'Leelawadee UI',\n\t'Javanese Text',\n\t'Segoe UI Emoji',\n\t'Aldhabi',\n\t'Gadugi',\n\t'Myanmar Text',\n\t'Nirmala UI',\n\t'Lucida Console',\n\t'Cambria Math',\n\t'Bai Jamjuree',\n\t'Chakra Petch',\n\t'Charmonman',\n\t'Fahkwang',\n\t'K2D',\n\t'Kodchasan',\n\t'KoHo',\n\t'Sarabun',\n\t'Srisakdi',\n\t'Galvji',\n\t'MuktaMahee Regular',\n\t'InaiMathi Bold',\n\t'American Typewriter Semibold',\n\t'Futura Bold',\n\t'SignPainter-HouseScript Semibold',\n\t'PingFang HK Light',\n\t'Kohinoor Devanagari Medium',\n\t'Luminari',\n\t'Geneva',\n\t'Helvetica Neue',\n\t'Droid Sans Mono',\n\t'Dancing Script',\n\t'Roboto',\n\t'Ubuntu',\n\t'Liberation Mono',\n\t'Source Code Pro',\n\t'DejaVu Sans',\n\t'OpenSymbol',\n\t'Chilanka',\n\t'Cousine',\n\t'Arimo',\n\t'Jomolhari',\n\t'MONO',\n\tsans-serif !important\n";
// 定义表情符号数组
const EMOJIS = [
[128512], [9786], [129333, 8205, 9794, 65039], [9832], [9784], [9895], [8265], [8505],
[127987, 65039, 8205, 9895, 65039], [129394], [9785], [9760], [129489, 8205, 129456],
[129487, 8205, 9794, 65039], [9975], [129489, 8205, 129309, 8205, 129489], [9752],
[9968], [9961], [9972], [9992], [9201], [9928], [9730], [9969], [9731], [9732],
[9976], [9823], [9937], [9e3], [9993], [9999], [128105, 8205, 10084, 65039, 8205, 128139, 8205, 128104],
[128104, 8205, 128105, 8205, 128103, 8205, 128102], [128104, 8205, 128105, 8205, 128102],
[128512], [169], [174], [8482], [128065, 65039, 8205, 128488, 65039], [10002],
[9986], [9935], [9874], [9876], [9881], [9939], [9879], [9904], [9905], [9888],
[9762], [9763], [11014], [8599], [10145], [11013], [9883], [10017], [10013], [9766],
[9654], [9197], [9199], [9167], [9792], [9794], [10006], [12336], [9877], [9884],
[10004], [10035], [10055], [9724], [9642], [10083], [10084], [9996], [9757], [9997],
[10052], [9878], [8618], [9775], [9770], [9774], [9745], [10036], [127344], [127359]
].map((codes) => String.fromCodePoint(...codes));
/**
* 获取 Canvas 2D 上下文并测量文本度量
* @returns 包含文本度量总和的对象
*/
function getCanvas2d(): { textMetricsSystemSum: number } | undefined {
try {
// 创建一个 canvas 元素并获取 2D 上下文
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (!context) return;
// 设置字体
context.font = '10px ' + CSS_FONT_FAMILY.replace(/!important/gm, "");
// 使用 Set 来存储唯一的度量值
const uniqueMetrics = new Set<string>();
const uniqueEmojis = EMOJIS.reduce((acc, emoji) => {
const metrics = context.measureText(emoji) || {};
const metricString = [
metrics.actualBoundingBoxAscent,
metrics.actualBoundingBoxDescent,
metrics.actualBoundingBoxLeft,
metrics.actualBoundingBoxRight,
metrics.fontBoundingBoxAscent,
metrics.fontBoundingBoxDescent,
metrics.width
].join(",");
if (!uniqueMetrics.has(metricString)) {
uniqueMetrics.add(metricString);
acc.add(emoji);
}
return acc;
}, new Set<string>());
// 计算所有唯一度量值的总和
const sum = Array.from(uniqueMetrics).reduce((total, metricString) => {
const metrics = metricString.split(",").map(Number);
return total + metrics.reduce((sum, value) => sum + (value || 0), 0);
}, 0);
// 返回包含文本度量总和的对象
return {
textMetricsSystemSum: (1e-5) * sum
};
} catch (error) {
console.log(error);
return;
}
}
// 使用示例
const result = getCanvas2d();
console.log(result); // 输出包含文本度量总和的对象
注1: 上面不仅探测了普通字体,还探测了emoji,这个在不同系统是有差异的,类似的还有数学符号!
注2: 其实还有个字体探测方式,它利用
@font-face
机制,无需JS也能探测,原理是它可指定字体的来源,优先使用本地的,若不存在再使用远端的,通过远端服务器的访问记录来探测,不过很少用,忽略~
WebGL指纹
Web图形库类似于OpenGL,它利用设备的图形硬件去高效渲染图像,主要用于3D场景,它的原理类似于Canvas指纹,可通过生成图像的细微差异做检测,另外它还提供接口直接访问硬件信息[16]!
// 检查canvas api和webgl上下文可用
const e = document.createElement('canvas');
if (typeof e.getContext !== 'function') return 'HTMLCanvasElement.getContext is not a function';
const n = e.getContext("webgl");
if (null === n) return 'WebGLRenderingContext is null';
if (typeof n.getParameter !== 'function') return 'WebGLRenderingContext.getParameter is not a function';
const r = n.getParameter(n.VENDOR),
s = n.getParameter(n.RENDERER),
a = n.getParameter(n.VERSION);
if (typeof window.InstallTrigger !== 'undefined') return JSON.stringify([r, s, a]);
const o = n.getExtension('WEBGL_debug_renderer_info');
if (null === o) return "WEBGL_debug_renderer_info extension is null";
const c = n.getParameter(o.UNMASKED_VENDOR_WEBGL),
u = n.getParameter(o.UNMASKED_RENDERER_WEBGL);
return JSON.stringify([c, u, r, s, a]);
它能访问的信息特别多,这里只列出了常见的
WebGPU
这是新版本出的接口,相比WebGL它能操控更底层的硬件,因此它也能获取更多的信息,使用时同样也是获取静态数据和渲染图片的差异信息,不过它需要用户授权,用得没那么多:
const adapter = await navigator.gpu.requestAdapter();// 请求 WebGPU 适配器,要用户授权
const device = await adapter.requestDevice();
const adapterInfo = { // 获取适配器的静态信息
name: adapter.name,
vendor: adapter.vendor,
device: adapter.device,
driver: adapter.driver,
limits: adapter.limits,
};
const supportedTextureFormats = device.getSupportedTextureFormats();// 获取设备支持的纹理格式
const shaderCode = `
[[stage(compute), workgroup_size(1)]]
fn main([[builtin(global_invocation_id)]] global_id : vec3<u32>) {
let i = global_id.x;
var data = array<i32>(10);
for (var j = 0u; j < 10u; j = j + 1u) {
data[i] = i32(j);
}
}
`;
const shaderModule = device.createShaderModule({ code: shaderCode,}); // 创建着色器模块
const computePipeline = await device.createComputePipelineAsync({ // 创建计算管线
layout: 'auto',
compute: {
module: shaderModule,
entryPoint: 'main',
},
});
const bufferSize = 40; // 创建缓冲区 10 * 4 字节
const buffer = device.createBuffer({
size: bufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const bindGroupLayout = device.createBindGroupLayout({ // 创建绑定组布局
entries: [{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'storage' },
}],
});
const bindGroup = device.createBindGroup({ // 创建绑定组
layout: bindGroupLayout,
entries: [{
binding: 0,
resource: { buffer },
}],
});
const commandEncoder = device.createCommandEncoder(); // 创建命令编码器
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatch(1);
passEncoder.end();
const gpuCommands = commandEncoder.finish(); // 提交命令
device.queue.submit([gpuCommands]);
const resultBuffer = new Uint8Array(bufferSize); // 读取缓冲区数据
await device.queue.onSubmittedWorkDone().then(() => {
device.queue.writeBuffer(buffer, 0, resultBuffer, 0, bufferSize);
});
const deviceInfo = {
...adapterInfo,
supportedTextureFormats,
computeResult: Array.from(resultBuffer),
};
const performanceTest = async () => { // 性能测试
const startTime = performance.now();
for (let i = 0; i < 100; i++) {
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatch(1);
passEncoder.end();
const gpuCommands = commandEncoder.finish();
device.queue.submit([gpuCommands]);
}
await device.queue.onSubmittedWorkDone();
const endTime = performance.now();
return endTime - startTime;
};
const performanceTime = await performanceTest();
deviceInfo.performanceTime = performanceTime;
WebAudio指纹
它通过Web Audio API生成音频信号并提取其特征,基于不同设备在音频处理过程中的动态压缩和频率分析,利用设备音频处理的细微差异生成唯一标识符。在生成指纹时,它就像搭积木,选择一个容器,给输入源,使用管道对声音做各种变换,再输出看细微差异[12]:
function canUseFunction(func, properties, args, apply) {
try {
let result = func();
for (let prop of properties) {
result = result[prop];
}
if (apply && args.length > 0) {
return result.apply(func(), args);
} else if (apply) {
return result.apply(func());
} else {
return result;
}
} catch (error) {
return undefined;
}
}
const hasOfflineAudioContext = typeof OfflineAudioContext !== 'undefined' || typeof webkitOfflineAudioContext !== 'undefined';
const hasFloat32Array = typeof Float32Array !== 'undefined';
function getOfflineAudioValues() {
try {
if (!hasOfflineAudioContext) return;
const offlineContext = new (OfflineAudioContext || webkitOfflineAudioContext)(1, 5000, 44100);
const analyser = offlineContext.createAnalyser();
const oscillator = offlineContext.createOscillator();
const dynamicsCompressor = offlineContext.createDynamicsCompressor();
const biquadFilter = offlineContext.createBiquadFilter();
if (!hasFloat32Array) return;
const frequencyData = new Float32Array(analyser.frequencyBinCount);
analyser.getFloatFrequencyData(frequencyData);
const uniqueFrequencyCount = new Set(frequencyData).size > 1;
const getPropertyValue = (property) => {
try {
return property();
} catch (error) {
return undefined;
}
};
const values = {
channelCount: getPropertyValue(() => analyser.channelCount),
channelCountMode: getPropertyValue(() => analyser.channelCountMode),
channelInterpretation: getPropertyValue(() => analyser.channelInterpretation),
fftSize: getPropertyValue(() => analyser.fftSize),
frequencyBinCount: getPropertyValue(() => analyser.frequencyBinCount),
maxDecibels: getPropertyValue(() => analyser.maxDecibels),
minDecibels: getPropertyValue(() => analyser.minDecibels),
numberOfInputs: getPropertyValue(() => analyser.numberOfInputs),
numberOfOutputs: getPropertyValue(() => analyser.numberOfOutputs),
smoothingTimeConstant: getPropertyValue(() => analyser.smoothingTimeConstant),
listenerForwardXMaxValue: getPropertyValue(() => canUseFunction(() => analyser.context.listener.forwardX, ['maxValue'])),
biquadFilterGainMaxValue: getPropertyValue(() => biquadFilter.gain.maxValue),
biquadFilterFrequencyDefaultValue: getPropertyValue(() => biquadFilter.frequency.defaultValue),
biquadFilterFrequencyMaxValue: getPropertyValue(() => biquadFilter.frequency.maxValue),
dynamicsCompressorAttackDefaultValue: getPropertyValue(() => dynamicsCompressor.attack.defaultValue),
dynamicsCompressorKneeDefaultValue: getPropertyValue(() => dynamicsCompressor.knee.defaultValue),
dynamicsCompressorKneeMaxValue: getPropertyValue(() => dynamicsCompressor.knee.maxValue),
dynamicsCompressorRatioDefaultValue: getPropertyValue(() => dynamicsCompressor.ratio.defaultValue),
dynamicsCompressorRatioMaxValue: getPropertyValue(() => dynamicsCompressor.ratio.maxValue),
dynamicsCompressorReleaseDefaultValue: getPropertyValue(() => dynamicsCompressor.release.defaultValue),
dynamicsCompressorReleaseMaxValue: getPropertyValue(() => dynamicsCompressor.release.maxValue),
dynamicsCompressorThresholdDefaultValue: getPropertyValue(() => dynamicsCompressor.threshold.defaultValue),
dynamicsCompressorThresholdMinValue: getPropertyValue(() => dynamicsCompressor.threshold.minValue),
oscillatorDetuneMaxValue: getPropertyValue(() => oscillator.detune.maxValue),
oscillatorDetuneMinValue: getPropertyValue(() => oscillator.detune.minValue),
oscillatorFrequencyDefaultValue: getPropertyValue(() => oscillator.frequency.defaultValue),
oscillatorFrequencyMaxValue: getPropertyValue(() => oscillator.frequency.maxValue),
oscillatorFrequencyMinValue: getPropertyValue(() => oscillator.frequency.minValue)
};
const result = {
values,
uniqueFrequencyCount,
bufferLength: 5000
};
return result;
} catch (error) {
return undefined;
}
}
Web Worker
applicationCache
Worklet
GamePad
检测连接的游戏手柄的信息,直接调用navigator.getGamepads()
不多说。
媒体设备
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
navigator.mediaDevices.enumerateDevices().then(function(devices) {
devices.forEach(function(device) {
console.log('Device:', device.kind, device.label, device.deviceId);
});
});
}
DOM树变化
这是检测协议指纹的方法,实际上效果一般,挺少见。
存储
现代浏览器有三类存储机制,如下:
- Cookie: 最常见,不多说;内容分为第一方、第三方和外部cookie,在用户追踪中lou见不止[6],且本地和三方组成联盟狼狈为奸相互同步/复活,可用
navigator.cookieEnabled
激活状态,或者通过写后读去检测可用性,不过它它重要了几乎没有禁用的。 - local storage/session storage:存储大点的键值对的(单域5M),
local storage
也经常被用于存储指纹/追踪数据。 - index db:存储大量结构化数据的,比如购物车/用户历史等信息。
还有个易被忽视的,ETAG,它是在其它地方存储的,本来用于优化性能(缓存)但是因为它不受cookie清空等的影响也可以用于追踪,还有的应用会起etag名字但是不用它的机制做指纹id,反正遇着需谨慎。
分析检测
JS也会上报异常环境,如果感知到当前正在被分析,那么一定是闪红色警报,这部分直接看另一篇JS动态分析的文章吧
WebRTC 真实IP泄漏
WebRTC 是一种让浏览器之间可以直接进行实时通信的技术,比如视频通话、语音聊天、文件传输等。它不需要通过服务器中转所有数据,因此效率更高、延迟更低。既然是P2P,就很容易出现真实IP泄漏的问题,WebRTC很不巧,有!关于WebRTC里P2P的原理,[14]做了很详细的描述,简单来讲它的ICE会报告本地的地址,便于P2P建立连接。建立连接有三种情况: 1. 没在NAT后面,能直接建立连接,此时使用的IP是chrome直接从本地网卡获取的。 2. 在NAT后面,使用STUN建立P2P连接,此时使用的IP是NAT转换后的公网IP,这个IP本机无法直接获取,它是先访问公网的一个服务器,由服务器返回源IP和端口,这个IP和端口就是NAT转换后的公网IP和端口。 3. 在NAT后面,但打不了洞,只能使用TURN建立中间转发连接
这里面提到的IP和端口信息都会存储于SDP,可被JS读取,对于1,在获取首选地址时,为了防止私有地址泄漏,chrome会使用mDNS匿名化域名(chrome://flags/#enable-webrtc-hide-local-ips-with-mdns
默认启用),具体来讲每次建立连接会随机生成一个域名(
目前chrome没有启动参数能阻止这种泄漏,但可使用一些其他办法阻止
使用chrome 扩展
如WebRTC Network Limiter/WebRTC Leak Shield等
API HOOK
直接HOOK RTCPeerConnection等API,提供虚假的candidate值。
强制代理UDP
chrome.privacy 提供四种选项:
1. "default":chrome的默认值,允许公网IP和私有IP私用。
2. "default_public_and_private_interfaces":和上面一样,名字写得更精确了。
3. "default_public_interface_only":只允许公网IP。
4. "disable_non_proxied_udp":禁用非代理的UDP连接,如果代理支持UDP则走代理,不支持则只使用UDP。
如:
chrome.privacy.network.webRTCIPHandlingPolicy.set({
value: 'disable_non_proxied_udp' // 强制代理 UDP
}, () => {});
网关处处理
由于ICE只有个简单的异或,没其他加密,因此可以在网关处直接修改数据包,返回错误的公网IP。[5]也是在网关搞事情,但是它是改的http/https里的JS....
其实WebRTC还有媒体接口,那个能拿到大量信息,但是需要用户授权,先忽略~
本地端口扫描
chrome可用http/websocket/webrtc去发请求,受限于同源策略它无法看到内容等,但是已经够扫端口了。
端口扫描有两种情况,是否有回显,有回显则看回显,没回显则看时间。
使用html标签
使用xhr
使用websocket
使用webrtc
根据[7],chrome可使用TURN去探测主机存活和端口开放状态,它利用icecandidateerror 事件,当主机不存活(或被防火墙拦截)则不会生成该事件,否则若端口开放则能显示主机和端口号,端口不开放则显示0.0.0.x:0
参考
[0] Unnecessarily Identifiable: Quantifying the fingerprintability of browser extensions due to bloat -- Oleksii Starov et al
[1] Browser Fingerprinting : Exploring Device Diversity to Augment Authentification and Build Client-Side Countermeasures -- Pierre Laperdrix
[2] Web browser fingerprinting : a framework for measuring the web browser -- Vanja Culafic
[3] privacycheck: fingerprintingCollection
[4] JavaScript/CSS Font Detector -- lalit.org
[5] Neither Denied nor Exposed: Fixing WebRTC Privacy Leaks -- Alexandros Fakis, Georgios Karopoulos, and Georgios Kambourakis
[6] The Web Never Forgets: Persistent Tracking Mechanisms in the Wild -- Gunes Acar, Christian Eubank et al
[7] Using WebRTC ICE Servers for Port Scanning in Chrome
[8] One Leak is Enough to Expose Them All From a WebRTC IP Leak to Web-based Network Scanning -- Mohammadreza Hazhirpasand, Mohammad Ghafari
[9] Fingerprinting in Style:Detecting Browser Extensions via Injected Style Sheets -- Pierre Laperdrix et al
[10] SoK: In Search of Lost Time: A Review of JavaScript Timers in Browsers -- Thomas Rokicki, Clémentine Maurice, Pierre Laperdrix
[11] FP-Radar: Longitudinal Measurement and Early Detection of Browser Fingerprinting -- Pouneh Nikkhah Bahrami, Umar Iqbal, and Zubair Shafiq
[12] Sounds of Silence: A Study of Stability and Diversity of Web Audio Fingerprints -- Shekhar Chalise
[13] Everyone is Different: Client-side Diversification for Defending Against Extension Fingerprinting -- Erik Trickel et al
[14] Peer-to-peer communication in web browsers using WebRTC: A detailed overview of WebRTC and what security and network concerns exists -- Christer Jakobsson
[15] The Web’s Sixth Sense:A Study of Scripts Accessing Smartphone Sensors -- Anupam Das et al
[16] Pixel Perfect: Fingerprinting Canvas in HTML5 -- Keaton Mowery and Hovav Shacham
[17] Poster: LockedApart: Faster GPU FingerprintingThrough the Compute API -- Tomer Laor and Yossi Oren
[18] Mystique: Uncovering Information Leakage from Browser Extensions -- Quan Chen et al
[19] Hulk: Eliciting Malicious Behavior in Browser Extensions -- Alexandros Kapravelos et al