@rester159/blacktip 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +249 -0
- package/LICENSE +38 -0
- package/README.md +234 -0
- package/dist/behavioral/calibration.d.ts +145 -0
- package/dist/behavioral/calibration.d.ts.map +1 -0
- package/dist/behavioral/calibration.js +242 -0
- package/dist/behavioral/calibration.js.map +1 -0
- package/dist/behavioral-engine.d.ts +156 -0
- package/dist/behavioral-engine.d.ts.map +1 -0
- package/dist/behavioral-engine.js +521 -0
- package/dist/behavioral-engine.js.map +1 -0
- package/dist/blacktip.d.ts +289 -0
- package/dist/blacktip.d.ts.map +1 -0
- package/dist/blacktip.js +1574 -0
- package/dist/blacktip.js.map +1 -0
- package/dist/browser-core.d.ts +47 -0
- package/dist/browser-core.d.ts.map +1 -0
- package/dist/browser-core.js +375 -0
- package/dist/browser-core.js.map +1 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +226 -0
- package/dist/cli.js.map +1 -0
- package/dist/element-finder.d.ts +42 -0
- package/dist/element-finder.d.ts.map +1 -0
- package/dist/element-finder.js +240 -0
- package/dist/element-finder.js.map +1 -0
- package/dist/evasion.d.ts +39 -0
- package/dist/evasion.d.ts.map +1 -0
- package/dist/evasion.js +488 -0
- package/dist/evasion.js.map +1 -0
- package/dist/fingerprint.d.ts +19 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +171 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/logging.d.ts +13 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +42 -0
- package/dist/logging.js.map +1 -0
- package/dist/observability.d.ts +69 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +189 -0
- package/dist/observability.js.map +1 -0
- package/dist/proxy-pool.d.ts +101 -0
- package/dist/proxy-pool.d.ts.map +1 -0
- package/dist/proxy-pool.js +156 -0
- package/dist/proxy-pool.js.map +1 -0
- package/dist/snapshot.d.ts +59 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +91 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/types.d.ts +243 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/examples/01-basic-navigate.ts +40 -0
- package/examples/02-login-with-mfa.ts +68 -0
- package/examples/03-agent-serve-mode.md +98 -0
- package/package.json +62 -0
package/dist/evasion.js
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
// ── Helper: wrap code in a self-contained IIFE ──
|
|
2
|
+
function iife(body) {
|
|
3
|
+
return `(function(){${body}})();`;
|
|
4
|
+
}
|
|
5
|
+
// ── 1. Navigator overrides ──
|
|
6
|
+
function navigatorOverrides(profile) {
|
|
7
|
+
return iife(`
|
|
8
|
+
// Delete navigator.webdriver — make it undefined like a real browser
|
|
9
|
+
try {
|
|
10
|
+
const nd = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver');
|
|
11
|
+
if (nd) {
|
|
12
|
+
Object.defineProperty(Navigator.prototype, 'webdriver', {
|
|
13
|
+
get: function() { return undefined; },
|
|
14
|
+
configurable: false,
|
|
15
|
+
enumerable: true,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
} catch(e) {}
|
|
19
|
+
|
|
20
|
+
// Override navigator properties to match profile
|
|
21
|
+
var overrides = {
|
|
22
|
+
hardwareConcurrency: ${profile.hardwareConcurrency},
|
|
23
|
+
deviceMemory: ${profile.deviceMemory},
|
|
24
|
+
maxTouchPoints: ${profile.maxTouchPoints},
|
|
25
|
+
platform: ${JSON.stringify(profile.platform)},
|
|
26
|
+
vendor: ${JSON.stringify(profile.vendor)},
|
|
27
|
+
languages: Object.freeze(${JSON.stringify(profile.languages)}),
|
|
28
|
+
language: ${JSON.stringify(profile.languages[0] || 'en-US')},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
for (var key in overrides) {
|
|
32
|
+
try {
|
|
33
|
+
Object.defineProperty(Navigator.prototype, key, {
|
|
34
|
+
get: function(val) { return function() { return val; }; }(overrides[key]),
|
|
35
|
+
configurable: false,
|
|
36
|
+
enumerable: true,
|
|
37
|
+
});
|
|
38
|
+
} catch(e) {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Also override userAgent, appVersion on Navigator.prototype
|
|
42
|
+
try {
|
|
43
|
+
Object.defineProperty(Navigator.prototype, 'userAgent', {
|
|
44
|
+
get: function() { return ${JSON.stringify(profile.userAgent)}; },
|
|
45
|
+
configurable: false,
|
|
46
|
+
enumerable: true,
|
|
47
|
+
});
|
|
48
|
+
} catch(e) {}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// appVersion is userAgent minus "Mozilla/"
|
|
52
|
+
var appVer = ${JSON.stringify(profile.userAgent.replace('Mozilla/', ''))};
|
|
53
|
+
Object.defineProperty(Navigator.prototype, 'appVersion', {
|
|
54
|
+
get: function() { return appVer; },
|
|
55
|
+
configurable: false,
|
|
56
|
+
enumerable: true,
|
|
57
|
+
});
|
|
58
|
+
} catch(e) {}
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
// ── 2. Chrome runtime ──
|
|
62
|
+
function chromeRuntime() {
|
|
63
|
+
return iife(`
|
|
64
|
+
if (!window.chrome) {
|
|
65
|
+
window.chrome = {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!window.chrome.runtime) {
|
|
69
|
+
window.chrome.runtime = {
|
|
70
|
+
connect: function(extensionId, connectInfo) {
|
|
71
|
+
// Match Chrome's signature: returns a Port-like object
|
|
72
|
+
return {
|
|
73
|
+
name: (connectInfo && connectInfo.name) || '',
|
|
74
|
+
postMessage: function() {},
|
|
75
|
+
disconnect: function() {},
|
|
76
|
+
onMessage: { addListener: function() {}, removeListener: function() {}, hasListeners: function() { return false; } },
|
|
77
|
+
onDisconnect: { addListener: function() {}, removeListener: function() {}, hasListeners: function() { return false; } },
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
sendMessage: function(extensionId, message, options, responseCallback) {
|
|
81
|
+
// Chrome throws if no callback and no runtime ID
|
|
82
|
+
if (typeof responseCallback === 'function') {
|
|
83
|
+
setTimeout(function() { responseCallback(undefined); }, 0);
|
|
84
|
+
} else if (typeof options === 'function') {
|
|
85
|
+
setTimeout(function() { options(undefined); }, 0);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
id: undefined,
|
|
89
|
+
getManifest: function() { return {}; },
|
|
90
|
+
getURL: function(path) { return ''; },
|
|
91
|
+
onConnect: { addListener: function() {}, removeListener: function() {}, hasListeners: function() { return false; } },
|
|
92
|
+
onMessage: { addListener: function() {}, removeListener: function() {}, hasListeners: function() { return false; } },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// chrome.loadTimes
|
|
97
|
+
if (!window.chrome.loadTimes) {
|
|
98
|
+
var startTime = Date.now() / 1000;
|
|
99
|
+
window.chrome.loadTimes = function() {
|
|
100
|
+
return {
|
|
101
|
+
commitLoadTime: startTime,
|
|
102
|
+
connectionInfo: 'h2',
|
|
103
|
+
finishDocumentLoadTime: startTime + 0.3,
|
|
104
|
+
finishLoadTime: startTime + 0.6,
|
|
105
|
+
firstPaintAfterLoadTime: startTime + 0.65,
|
|
106
|
+
firstPaintTime: startTime + 0.35,
|
|
107
|
+
navigationType: 'Other',
|
|
108
|
+
npnNegotiatedProtocol: 'h2',
|
|
109
|
+
requestTime: startTime - 0.1,
|
|
110
|
+
startLoadTime: startTime,
|
|
111
|
+
wasAlternateProtocolAvailable: false,
|
|
112
|
+
wasFetchedViaSpdy: true,
|
|
113
|
+
wasNpnNegotiated: true,
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// chrome.csi
|
|
119
|
+
if (!window.chrome.csi) {
|
|
120
|
+
window.chrome.csi = function() {
|
|
121
|
+
return {
|
|
122
|
+
onloadT: Date.now(),
|
|
123
|
+
pageT: performance.now(),
|
|
124
|
+
startE: Date.now() - performance.now(),
|
|
125
|
+
tran: 15, // Navigation type: normal
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Make chrome non-writable to prevent overwrite detection
|
|
131
|
+
try {
|
|
132
|
+
Object.defineProperty(window, 'chrome', {
|
|
133
|
+
value: window.chrome,
|
|
134
|
+
writable: false,
|
|
135
|
+
configurable: false,
|
|
136
|
+
enumerable: true,
|
|
137
|
+
});
|
|
138
|
+
} catch(e) {}
|
|
139
|
+
`);
|
|
140
|
+
}
|
|
141
|
+
// ── 3. Plugins and MimeTypes ──
|
|
142
|
+
function pluginsAndMimeTypes(profile) {
|
|
143
|
+
const pluginsJson = JSON.stringify(profile.plugins);
|
|
144
|
+
return iife(`
|
|
145
|
+
var pluginData = ${pluginsJson};
|
|
146
|
+
|
|
147
|
+
// Build MimeType and Plugin objects that pass instanceof checks
|
|
148
|
+
var mimeTypes = [];
|
|
149
|
+
var plugins = [];
|
|
150
|
+
|
|
151
|
+
// We need to create objects whose prototype chains match the browser's native ones.
|
|
152
|
+
// PluginArray, Plugin, MimeTypeArray, MimeType prototypes already exist in the DOM.
|
|
153
|
+
|
|
154
|
+
for (var i = 0; i < pluginData.length; i++) {
|
|
155
|
+
var pd = pluginData[i];
|
|
156
|
+
var plugin = Object.create(Plugin.prototype);
|
|
157
|
+
|
|
158
|
+
var pluginMimes = [];
|
|
159
|
+
for (var j = 0; j < pd.mimeTypes.length; j++) {
|
|
160
|
+
var md = pd.mimeTypes[j];
|
|
161
|
+
var mime = Object.create(MimeType.prototype);
|
|
162
|
+
Object.defineProperties(mime, {
|
|
163
|
+
type: { value: md.type, enumerable: true },
|
|
164
|
+
suffixes: { value: md.suffixes, enumerable: true },
|
|
165
|
+
description: { value: md.description, enumerable: true },
|
|
166
|
+
enabledPlugin: { value: plugin, enumerable: true },
|
|
167
|
+
});
|
|
168
|
+
pluginMimes.push(mime);
|
|
169
|
+
mimeTypes.push(mime);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Object.defineProperties(plugin, {
|
|
173
|
+
name: { value: pd.name, enumerable: true },
|
|
174
|
+
description: { value: pd.description, enumerable: true },
|
|
175
|
+
filename: { value: pd.filename, enumerable: true },
|
|
176
|
+
length: { value: pluginMimes.length, enumerable: true },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Index access and namedItem for plugin's mimeTypes
|
|
180
|
+
for (var k = 0; k < pluginMimes.length; k++) {
|
|
181
|
+
Object.defineProperty(plugin, k, { value: pluginMimes[k], enumerable: false });
|
|
182
|
+
}
|
|
183
|
+
plugin.item = function(idx) { return pluginMimes[idx] || null; };
|
|
184
|
+
plugin.namedItem = function(name) {
|
|
185
|
+
for (var m = 0; m < pluginMimes.length; m++) {
|
|
186
|
+
if (pluginMimes[m].type === name) return pluginMimes[m];
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
};
|
|
190
|
+
// Symbol.iterator
|
|
191
|
+
plugin[Symbol.iterator] = function() {
|
|
192
|
+
var _i = 0; var _a = pluginMimes;
|
|
193
|
+
return { next: function() { return _i < _a.length ? { value: _a[_i++], done: false } : { done: true }; } };
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
plugins.push(plugin);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Build PluginArray
|
|
200
|
+
var pluginArray = Object.create(PluginArray.prototype);
|
|
201
|
+
Object.defineProperty(pluginArray, 'length', { value: plugins.length, enumerable: true });
|
|
202
|
+
for (var p = 0; p < plugins.length; p++) {
|
|
203
|
+
Object.defineProperty(pluginArray, p, { value: plugins[p], enumerable: false });
|
|
204
|
+
// Name-based access
|
|
205
|
+
Object.defineProperty(pluginArray, plugins[p].name, { value: plugins[p], enumerable: false });
|
|
206
|
+
}
|
|
207
|
+
pluginArray.item = function(idx) { return plugins[idx] || null; };
|
|
208
|
+
pluginArray.namedItem = function(name) {
|
|
209
|
+
for (var n = 0; n < plugins.length; n++) {
|
|
210
|
+
if (plugins[n].name === name) return plugins[n];
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
};
|
|
214
|
+
pluginArray.refresh = function() {};
|
|
215
|
+
pluginArray[Symbol.iterator] = function() {
|
|
216
|
+
var _i = 0;
|
|
217
|
+
return { next: function() { return _i < plugins.length ? { value: plugins[_i++], done: false } : { done: true }; } };
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Build MimeTypeArray
|
|
221
|
+
var mimeTypeArray = Object.create(MimeTypeArray.prototype);
|
|
222
|
+
Object.defineProperty(mimeTypeArray, 'length', { value: mimeTypes.length, enumerable: true });
|
|
223
|
+
for (var q = 0; q < mimeTypes.length; q++) {
|
|
224
|
+
Object.defineProperty(mimeTypeArray, q, { value: mimeTypes[q], enumerable: false });
|
|
225
|
+
Object.defineProperty(mimeTypeArray, mimeTypes[q].type, { value: mimeTypes[q], enumerable: false });
|
|
226
|
+
}
|
|
227
|
+
mimeTypeArray.item = function(idx) { return mimeTypes[idx] || null; };
|
|
228
|
+
mimeTypeArray.namedItem = function(name) {
|
|
229
|
+
for (var r = 0; r < mimeTypes.length; r++) {
|
|
230
|
+
if (mimeTypes[r].type === name) return mimeTypes[r];
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
};
|
|
234
|
+
mimeTypeArray[Symbol.iterator] = function() {
|
|
235
|
+
var _i = 0;
|
|
236
|
+
return { next: function() { return _i < mimeTypes.length ? { value: mimeTypes[_i++], done: false } : { done: true }; } };
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Override navigator.plugins and navigator.mimeTypes
|
|
240
|
+
Object.defineProperty(Navigator.prototype, 'plugins', {
|
|
241
|
+
get: function() { return pluginArray; },
|
|
242
|
+
configurable: false,
|
|
243
|
+
enumerable: true,
|
|
244
|
+
});
|
|
245
|
+
Object.defineProperty(Navigator.prototype, 'mimeTypes', {
|
|
246
|
+
get: function() { return mimeTypeArray; },
|
|
247
|
+
configurable: false,
|
|
248
|
+
enumerable: true,
|
|
249
|
+
});
|
|
250
|
+
`);
|
|
251
|
+
}
|
|
252
|
+
// ── 4. Permissions override ──
|
|
253
|
+
function permissionsOverride() {
|
|
254
|
+
return iife(`
|
|
255
|
+
var originalQuery = Permissions.prototype.query;
|
|
256
|
+
Permissions.prototype.query = function(permissionDesc) {
|
|
257
|
+
// Notification permission: headless returns 'denied', real Chrome returns 'prompt'
|
|
258
|
+
if (permissionDesc && permissionDesc.name === 'notifications') {
|
|
259
|
+
return Promise.resolve({
|
|
260
|
+
state: 'prompt',
|
|
261
|
+
name: 'notifications',
|
|
262
|
+
onchange: null,
|
|
263
|
+
addEventListener: function() {},
|
|
264
|
+
removeEventListener: function() {},
|
|
265
|
+
dispatchEvent: function() { return true; },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
// For all other permissions, delegate to the original implementation
|
|
269
|
+
return originalQuery.call(this, permissionDesc);
|
|
270
|
+
};
|
|
271
|
+
`);
|
|
272
|
+
}
|
|
273
|
+
// ── 5. WebGL override ──
|
|
274
|
+
function webglOverride(profile) {
|
|
275
|
+
return iife(`
|
|
276
|
+
var VENDOR = ${JSON.stringify(profile.webglVendor)};
|
|
277
|
+
var RENDERER = ${JSON.stringify(profile.webglRenderer)};
|
|
278
|
+
|
|
279
|
+
// Constants for WEBGL_debug_renderer_info
|
|
280
|
+
var UNMASKED_VENDOR_WEBGL = 0x9245;
|
|
281
|
+
var UNMASKED_RENDERER_WEBGL = 0x9246;
|
|
282
|
+
|
|
283
|
+
// Hook getParameter on both WebGL contexts
|
|
284
|
+
var contexts = ['WebGLRenderingContext', 'WebGL2RenderingContext'];
|
|
285
|
+
for (var c = 0; c < contexts.length; c++) {
|
|
286
|
+
var ctx = window[contexts[c]];
|
|
287
|
+
if (!ctx || !ctx.prototype) continue;
|
|
288
|
+
|
|
289
|
+
var origGetParameter = ctx.prototype.getParameter;
|
|
290
|
+
ctx.prototype.getParameter = (function(orig) {
|
|
291
|
+
return function(param) {
|
|
292
|
+
if (param === UNMASKED_VENDOR_WEBGL) return VENDOR;
|
|
293
|
+
if (param === UNMASKED_RENDERER_WEBGL) return RENDERER;
|
|
294
|
+
// Also override VENDOR and RENDERER base params
|
|
295
|
+
if (param === 0x1F00) return VENDOR; // gl.VENDOR
|
|
296
|
+
if (param === 0x1F01) return RENDERER; // gl.RENDERER
|
|
297
|
+
return orig.call(this, param);
|
|
298
|
+
};
|
|
299
|
+
})(origGetParameter);
|
|
300
|
+
|
|
301
|
+
// Hook getExtension to ensure WEBGL_debug_renderer_info is available
|
|
302
|
+
// and returns consistent values
|
|
303
|
+
var origGetExtension = ctx.prototype.getExtension;
|
|
304
|
+
ctx.prototype.getExtension = (function(orig) {
|
|
305
|
+
return function(name) {
|
|
306
|
+
var ext = orig.call(this, name);
|
|
307
|
+
if (name === 'WEBGL_debug_renderer_info') {
|
|
308
|
+
// If extension is null (as in some headless modes), create a shim
|
|
309
|
+
if (!ext) {
|
|
310
|
+
ext = {
|
|
311
|
+
UNMASKED_VENDOR_WEBGL: UNMASKED_VENDOR_WEBGL,
|
|
312
|
+
UNMASKED_RENDERER_WEBGL: UNMASKED_RENDERER_WEBGL,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return ext;
|
|
316
|
+
}
|
|
317
|
+
return ext;
|
|
318
|
+
};
|
|
319
|
+
})(origGetExtension);
|
|
320
|
+
}
|
|
321
|
+
`);
|
|
322
|
+
}
|
|
323
|
+
// ── 6. Canvas noise ──
|
|
324
|
+
function canvasNoise(profile) {
|
|
325
|
+
// Generate a deterministic seed from the profile name
|
|
326
|
+
let seed = 0;
|
|
327
|
+
for (let i = 0; i < profile.name.length; i++) {
|
|
328
|
+
seed = ((seed << 5) - seed + profile.name.charCodeAt(i)) | 0;
|
|
329
|
+
}
|
|
330
|
+
return iife(`
|
|
331
|
+
// Simple seeded PRNG (mulberry32)
|
|
332
|
+
var seed = ${seed >>> 0};
|
|
333
|
+
function prng() {
|
|
334
|
+
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
|
|
335
|
+
var t = Math.imul(seed ^ seed >>> 15, 1 | seed);
|
|
336
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
337
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Inject subtle noise into ImageData
|
|
341
|
+
function addNoise(imageData) {
|
|
342
|
+
var data = imageData.data;
|
|
343
|
+
// Only perturb a small fraction of pixels for subtlety
|
|
344
|
+
for (var i = 0; i < data.length; i += 4) {
|
|
345
|
+
// Perturb ~2% of pixels
|
|
346
|
+
if (prng() < 0.02) {
|
|
347
|
+
// Pick a random channel (R, G, or B — skip alpha)
|
|
348
|
+
var channel = (prng() * 3) | 0;
|
|
349
|
+
var delta = prng() < 0.5 ? -1 : 1;
|
|
350
|
+
var val = data[i + channel] + delta;
|
|
351
|
+
if (val < 0) val = 0;
|
|
352
|
+
if (val > 255) val = 255;
|
|
353
|
+
data[i + channel] = val;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return imageData;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Hook toDataURL
|
|
360
|
+
var origToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
361
|
+
HTMLCanvasElement.prototype.toDataURL = function() {
|
|
362
|
+
var ctx = this.getContext('2d');
|
|
363
|
+
if (ctx) {
|
|
364
|
+
try {
|
|
365
|
+
var imageData = ctx.getImageData(0, 0, this.width, this.height);
|
|
366
|
+
addNoise(imageData);
|
|
367
|
+
ctx.putImageData(imageData, 0, 0);
|
|
368
|
+
} catch(e) {
|
|
369
|
+
// Canvas may be tainted (cross-origin) — silently skip
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return origToDataURL.apply(this, arguments);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Hook toBlob
|
|
376
|
+
var origToBlob = HTMLCanvasElement.prototype.toBlob;
|
|
377
|
+
HTMLCanvasElement.prototype.toBlob = function() {
|
|
378
|
+
var ctx = this.getContext('2d');
|
|
379
|
+
if (ctx) {
|
|
380
|
+
try {
|
|
381
|
+
var imageData = ctx.getImageData(0, 0, this.width, this.height);
|
|
382
|
+
addNoise(imageData);
|
|
383
|
+
ctx.putImageData(imageData, 0, 0);
|
|
384
|
+
} catch(e) {}
|
|
385
|
+
}
|
|
386
|
+
return origToBlob.apply(this, arguments);
|
|
387
|
+
};
|
|
388
|
+
`);
|
|
389
|
+
}
|
|
390
|
+
// ── 7. AudioContext override ──
|
|
391
|
+
//
|
|
392
|
+
// Adds subtle noise to audio fingerprinting methods (getFloatFrequencyData,
|
|
393
|
+
// OfflineAudioContext.startRendering) so the audio fingerprint isn't a
|
|
394
|
+
// deterministic device-wide identifier. The noise is seeded by the device
|
|
395
|
+
// profile name so it's consistent across sessions for the same profile —
|
|
396
|
+
// a real human's audio fingerprint is stable, not random.
|
|
397
|
+
function audioContextOverride(profile) {
|
|
398
|
+
// Derive a seed from the profile name. Same seed → same PRNG stream →
|
|
399
|
+
// same audio fingerprint across sessions. Using Math.random() here (the
|
|
400
|
+
// pre-v2 behavior) would have made the fingerprint drift every session,
|
|
401
|
+
// which is itself a detectable "this user looks different every time" signal.
|
|
402
|
+
let seed = 0;
|
|
403
|
+
for (let i = 0; i < profile.name.length; i++) {
|
|
404
|
+
seed = ((seed << 5) - seed + profile.name.charCodeAt(i)) | 0;
|
|
405
|
+
}
|
|
406
|
+
return iife(`
|
|
407
|
+
// Mulberry32 PRNG seeded from profile.name — matches the canvas noise
|
|
408
|
+
// seeding approach so a given profile has a stable audio + canvas pair.
|
|
409
|
+
var audioSeed = ${seed >>> 0};
|
|
410
|
+
function audioPrng() {
|
|
411
|
+
audioSeed |= 0; audioSeed = audioSeed + 0x6D2B79F5 | 0;
|
|
412
|
+
var t = Math.imul(audioSeed ^ audioSeed >>> 15, 1 | audioSeed);
|
|
413
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
414
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (typeof AnalyserNode !== 'undefined') {
|
|
418
|
+
var origGetFloat = AnalyserNode.prototype.getFloatFrequencyData;
|
|
419
|
+
AnalyserNode.prototype.getFloatFrequencyData = function(array) {
|
|
420
|
+
origGetFloat.call(this, array);
|
|
421
|
+
for (var i = 0; i < array.length; i++) {
|
|
422
|
+
// Seeded noise, ~0.0001 magnitude — imperceptible to real audio
|
|
423
|
+
// use but breaks deterministic fingerprinting.
|
|
424
|
+
array[i] = array[i] + (audioPrng() * 0.0002 - 0.0001);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (typeof OfflineAudioContext !== 'undefined') {
|
|
430
|
+
var origOfflineRender = OfflineAudioContext.prototype.startRendering;
|
|
431
|
+
OfflineAudioContext.prototype.startRendering = function() {
|
|
432
|
+
return origOfflineRender.call(this).then(function(buffer) {
|
|
433
|
+
for (var ch = 0; ch < buffer.numberOfChannels; ch++) {
|
|
434
|
+
var data = buffer.getChannelData(ch);
|
|
435
|
+
for (var i = 0; i < data.length; i++) {
|
|
436
|
+
data[i] = data[i] + (audioPrng() * 0.0001 - 0.00005);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return buffer;
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
`);
|
|
444
|
+
}
|
|
445
|
+
// ── Public API ──
|
|
446
|
+
/**
|
|
447
|
+
* Generate all evasion scripts for a given device profile.
|
|
448
|
+
* Each script is a self-contained IIFE ready to be injected via
|
|
449
|
+
* `Page.addScriptToEvaluateOnNewDocument()`.
|
|
450
|
+
*/
|
|
451
|
+
/**
|
|
452
|
+
* Evasion script generator. With BlackTip v2 running on real Chrome
|
|
453
|
+
* (`channel: 'chrome'`) + patchright's CDP-level stealth patches, most of
|
|
454
|
+
* the original JS-shim evasion is unnecessary and some of it was actively
|
|
455
|
+
* creating signals (see L012 in planning/lessons.md for the chrome.runtime
|
|
456
|
+
* and launch-flag story).
|
|
457
|
+
*
|
|
458
|
+
* What we keep:
|
|
459
|
+
* - canvasNoise: privacy/anti-tracking, adds plausible noise to canvas
|
|
460
|
+
* rendering so we don't get a stable device-wide fingerprint.
|
|
461
|
+
* - audioContextOverride: same story for audio fingerprint.
|
|
462
|
+
*
|
|
463
|
+
* What we removed (and why):
|
|
464
|
+
* - chromeRuntime: real Chrome doesn't expose `chrome.runtime` on regular
|
|
465
|
+
* pages — only on extension-accessible contexts. Our shim was adding
|
|
466
|
+
* it everywhere, which CreepJS catches as `hasBadChromeRuntime: true`.
|
|
467
|
+
* Let patchright + real Chrome handle `window.chrome` naturally.
|
|
468
|
+
* - navigatorOverrides: real Chrome's navigator is already correct when
|
|
469
|
+
* we use `channel: 'chrome'`. Overriding with profile values creates
|
|
470
|
+
* mismatches (e.g., profile says hardwareConcurrency=8 but the actual
|
|
471
|
+
* CPU has 16, and other APIs like Performance.now timing can reveal
|
|
472
|
+
* the truth).
|
|
473
|
+
* - pluginsAndMimeTypes: real Chrome populates navigator.plugins with
|
|
474
|
+
* the real PDF viewer plugin. Our shim was adding fake plugins that
|
|
475
|
+
* didn't match.
|
|
476
|
+
* - permissionsOverride: real Chrome already returns 'prompt' for
|
|
477
|
+
* notifications on most states. Shim only needed on headless Chromium.
|
|
478
|
+
* - webglOverride: real Chrome reports the real GPU through ANGLE. Our
|
|
479
|
+
* shim was replacing AMD Radeon with a profile string, making the
|
|
480
|
+
* fingerprint inconsistent with the real DirectX pipeline signals.
|
|
481
|
+
*/
|
|
482
|
+
export function generateEvasionScripts(profile) {
|
|
483
|
+
return [
|
|
484
|
+
canvasNoise(profile),
|
|
485
|
+
audioContextOverride(profile),
|
|
486
|
+
];
|
|
487
|
+
}
|
|
488
|
+
//# sourceMappingURL=evasion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evasion.js","sourceRoot":"","sources":["../src/evasion.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DeviceProfile } from './types';
|
|
2
|
+
export declare class DeviceProfileManager {
|
|
3
|
+
private profiles;
|
|
4
|
+
constructor();
|
|
5
|
+
getProfile(name: string): DeviceProfile;
|
|
6
|
+
listProfiles(): string[];
|
|
7
|
+
addProfile(name: string, profile: DeviceProfile): void;
|
|
8
|
+
/**
|
|
9
|
+
* Generate a slightly randomized variant of a named profile.
|
|
10
|
+
*
|
|
11
|
+
* Randomization:
|
|
12
|
+
* - Picks a random UA string from a small pool for the same OS
|
|
13
|
+
* - Varies hardwareConcurrency by +/-2 (min 2)
|
|
14
|
+
* - Picks a random realistic deviceMemory (4, 8, or 16)
|
|
15
|
+
* - Everything else (GPU, fonts, plugins, platform) stays consistent
|
|
16
|
+
*/
|
|
17
|
+
randomizeProfile(name: string): DeviceProfile;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,SAAS,CAAC;AA2IzD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAA6B;;IAS7C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;IAQvC,YAAY,IAAI,MAAM,EAAE;IAIxB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI;IAItD;;;;;;;;OAQG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;CAmB9C"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// ── Shared plugin data (Chrome on all platforms) ──
|
|
2
|
+
const chromePlugins = [
|
|
3
|
+
{
|
|
4
|
+
name: 'Chrome PDF Plugin',
|
|
5
|
+
description: 'Portable Document Format',
|
|
6
|
+
filename: 'internal-pdf-viewer',
|
|
7
|
+
mimeTypes: [
|
|
8
|
+
{ type: 'application/x-google-chrome-pdf', suffixes: 'pdf', description: 'Portable Document Format' },
|
|
9
|
+
],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'Chrome PDF Viewer',
|
|
13
|
+
description: '',
|
|
14
|
+
filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
|
|
15
|
+
mimeTypes: [
|
|
16
|
+
{ type: 'application/pdf', suffixes: 'pdf', description: '' },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'Native Client',
|
|
21
|
+
description: '',
|
|
22
|
+
filename: 'internal-nacl-plugin',
|
|
23
|
+
mimeTypes: [
|
|
24
|
+
{ type: 'application/x-nacl', suffixes: '', description: 'Native Client Executable' },
|
|
25
|
+
{ type: 'application/x-pnacl', suffixes: '', description: 'Portable Native Client Executable' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
// ── UA pools per OS ──
|
|
30
|
+
const windowsUAs = [
|
|
31
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
32
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
|
33
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
34
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
|
|
35
|
+
];
|
|
36
|
+
const macosUAs = [
|
|
37
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
38
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
|
39
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
40
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
|
|
41
|
+
];
|
|
42
|
+
const linuxUAs = [
|
|
43
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
44
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
|
45
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
46
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
|
|
47
|
+
];
|
|
48
|
+
// ── Pre-configured profiles ──
|
|
49
|
+
const desktopWindows = {
|
|
50
|
+
name: 'desktop-windows',
|
|
51
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
52
|
+
platform: 'Win32',
|
|
53
|
+
hardwareConcurrency: 8,
|
|
54
|
+
deviceMemory: 8,
|
|
55
|
+
maxTouchPoints: 0,
|
|
56
|
+
screenWidth: 1920,
|
|
57
|
+
screenHeight: 1080,
|
|
58
|
+
devicePixelRatio: 1,
|
|
59
|
+
colorDepth: 24,
|
|
60
|
+
vendor: 'Google Inc.',
|
|
61
|
+
renderer: 'Google Inc. (NVIDIA)',
|
|
62
|
+
webglVendor: 'Google Inc. (NVIDIA)',
|
|
63
|
+
webglRenderer: 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
64
|
+
languages: ['en-US', 'en'],
|
|
65
|
+
plugins: chromePlugins,
|
|
66
|
+
fonts: [
|
|
67
|
+
'Arial', 'Calibri', 'Cambria', 'Consolas', 'Courier New',
|
|
68
|
+
'Georgia', 'Impact', 'Lucida Console', 'Segoe UI', 'Tahoma',
|
|
69
|
+
'Times New Roman', 'Trebuchet MS', 'Verdana',
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
const desktopMacos = {
|
|
73
|
+
name: 'desktop-macos',
|
|
74
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
75
|
+
platform: 'MacIntel',
|
|
76
|
+
hardwareConcurrency: 8,
|
|
77
|
+
deviceMemory: 16,
|
|
78
|
+
maxTouchPoints: 0,
|
|
79
|
+
screenWidth: 2560,
|
|
80
|
+
screenHeight: 1440,
|
|
81
|
+
devicePixelRatio: 2,
|
|
82
|
+
colorDepth: 30,
|
|
83
|
+
vendor: 'Google Inc.',
|
|
84
|
+
renderer: 'Google Inc. (Apple)',
|
|
85
|
+
webglVendor: 'Google Inc. (Apple)',
|
|
86
|
+
webglRenderer: 'ANGLE (Apple, Apple M1, OpenGL 4.1)',
|
|
87
|
+
languages: ['en-US', 'en'],
|
|
88
|
+
plugins: chromePlugins,
|
|
89
|
+
fonts: [
|
|
90
|
+
'Arial', 'Courier New', 'Georgia', 'Helvetica', 'Helvetica Neue',
|
|
91
|
+
'Lucida Grande', 'Menlo', 'Monaco', 'San Francisco', 'Times New Roman',
|
|
92
|
+
'Verdana',
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
const desktopLinux = {
|
|
96
|
+
name: 'desktop-linux',
|
|
97
|
+
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
98
|
+
platform: 'Linux x86_64',
|
|
99
|
+
hardwareConcurrency: 4,
|
|
100
|
+
deviceMemory: 8,
|
|
101
|
+
maxTouchPoints: 0,
|
|
102
|
+
screenWidth: 1920,
|
|
103
|
+
screenHeight: 1080,
|
|
104
|
+
devicePixelRatio: 1,
|
|
105
|
+
colorDepth: 24,
|
|
106
|
+
vendor: 'Google Inc.',
|
|
107
|
+
renderer: 'Google Inc. (Intel)',
|
|
108
|
+
webglVendor: 'Google Inc. (Intel)',
|
|
109
|
+
webglRenderer: 'ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.5)',
|
|
110
|
+
languages: ['en-US', 'en'],
|
|
111
|
+
plugins: chromePlugins,
|
|
112
|
+
fonts: [
|
|
113
|
+
'Arial', 'Courier New', 'DejaVu Sans', 'DejaVu Sans Mono', 'FreeMono',
|
|
114
|
+
'FreeSans', 'Liberation Mono', 'Liberation Sans', 'Liberation Serif',
|
|
115
|
+
'Noto Sans', 'Times New Roman', 'Ubuntu',
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
// ── UA pool lookup by profile name ──
|
|
119
|
+
const uaPoolByProfile = {
|
|
120
|
+
'desktop-windows': windowsUAs,
|
|
121
|
+
'desktop-macos': macosUAs,
|
|
122
|
+
'desktop-linux': linuxUAs,
|
|
123
|
+
};
|
|
124
|
+
// ── DeviceProfileManager ──
|
|
125
|
+
export class DeviceProfileManager {
|
|
126
|
+
profiles;
|
|
127
|
+
constructor() {
|
|
128
|
+
this.profiles = new Map();
|
|
129
|
+
this.profiles.set('desktop-windows', desktopWindows);
|
|
130
|
+
this.profiles.set('desktop-macos', desktopMacos);
|
|
131
|
+
this.profiles.set('desktop-linux', desktopLinux);
|
|
132
|
+
}
|
|
133
|
+
getProfile(name) {
|
|
134
|
+
const profile = this.profiles.get(name);
|
|
135
|
+
if (!profile) {
|
|
136
|
+
throw new Error(`Unknown device profile: "${name}". Available: ${this.listProfiles().join(', ')}`);
|
|
137
|
+
}
|
|
138
|
+
return { ...profile, plugins: profile.plugins.map(p => ({ ...p, mimeTypes: [...p.mimeTypes] })), fonts: [...profile.fonts], languages: [...profile.languages] };
|
|
139
|
+
}
|
|
140
|
+
listProfiles() {
|
|
141
|
+
return Array.from(this.profiles.keys());
|
|
142
|
+
}
|
|
143
|
+
addProfile(name, profile) {
|
|
144
|
+
this.profiles.set(name, profile);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Generate a slightly randomized variant of a named profile.
|
|
148
|
+
*
|
|
149
|
+
* Randomization:
|
|
150
|
+
* - Picks a random UA string from a small pool for the same OS
|
|
151
|
+
* - Varies hardwareConcurrency by +/-2 (min 2)
|
|
152
|
+
* - Picks a random realistic deviceMemory (4, 8, or 16)
|
|
153
|
+
* - Everything else (GPU, fonts, plugins, platform) stays consistent
|
|
154
|
+
*/
|
|
155
|
+
randomizeProfile(name) {
|
|
156
|
+
const base = this.getProfile(name); // already a shallow clone
|
|
157
|
+
// Pick a random UA from the pool (fall back to base UA if no pool)
|
|
158
|
+
const uaPool = uaPoolByProfile[name];
|
|
159
|
+
if (uaPool && uaPool.length > 0) {
|
|
160
|
+
base.userAgent = uaPool[Math.floor(Math.random() * uaPool.length)];
|
|
161
|
+
}
|
|
162
|
+
// Vary hardwareConcurrency by +/-2, min 2
|
|
163
|
+
const delta = Math.floor(Math.random() * 5) - 2; // -2..+2
|
|
164
|
+
base.hardwareConcurrency = Math.max(2, base.hardwareConcurrency + delta);
|
|
165
|
+
// Pick a random realistic deviceMemory
|
|
166
|
+
const memoryOptions = [4, 8, 16];
|
|
167
|
+
base.deviceMemory = memoryOptions[Math.floor(Math.random() * memoryOptions.length)];
|
|
168
|
+
return base;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=fingerprint.js.map
|