/** * stealth-engine.js — Motor de Stealth Completo para Zelin * ========================================================== * Basado en investigación real 2025-2026: * * HALLAZGO CLAVE: "Blocking or randomizing canvas fingerprinting is easily detected * because the fingerprint changes on every page load, which is itself suspicious. * Natural fingerprints that match real device configurations are superior." * — Multilogin Research, 2025 * * HALLAZGO CLAVE 2: "BotBrowser's key advantage: Canvas, WebGL, AudioContext, * and Fonts must be CONSISTENT across the session. Inconsistencies instantly * expose fake fingerprints." * — GeeTest Anti-Bot Research, Nov 2025 * * ESTRATEGIA: Fingerprint consistente + realista, NO aleatorio en cada request * → Generar UNA identidad de dispositivo por sesión * → Mantenerla IDÉNTICA en todas las páginas de esa sesión * → Que corresponda a una configuración de hardware que PODRÍA EXISTIR */ import { createHash } from 'crypto'; // ── Pool de configuraciones reales de dispositivos ─────────────────────────── // Extraídas de datasets de fingerprints reales (BrowserLeaks, CreepJS stats 2025) // CRÍTICO: cada configuración debe ser INTERNAMENTE CONSISTENTE // (GPU vendor debe coincidir con canvas output, etc.) const REAL_DEVICE_CONFIGS = [ { // Windows 11 + Chrome 120 + Intel UHD 770 (configuración más común ~18% de usuarios) platform : 'Win32', oscpu : 'Windows NT 10.0', vendor : 'Google Inc.', renderer : 'ANGLE (Intel, Intel(R) UHD Graphics 770 Direct3D11 vs_5_0 ps_5_0, D3D11)', vendorUnmasked : 'Intel', rendererUnmasked: 'Intel(R) UHD Graphics 770', hardwareConcurrency: 8, deviceMemory : 8, maxTouchPoints : 0, screenRes : [1920, 1080], colorDepth : 24, pixelRatio : 1, timezone : 'America/Mexico_City', language : 'es-MX', fonts : ['Arial', 'Calibri', 'Cambria', 'Comic Sans MS', 'Consolas', 'Courier New', 'Georgia', 'Segoe UI', 'Tahoma', 'Times New Roman', 'Trebuchet MS', 'Verdana'], plugins : ['Chrome PDF Plugin', 'Chrome PDF Viewer', 'Native Client'], chromeVersion : '120.0.6099.130', userAgent : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', }, { // Windows 10 + Chrome 119 + NVIDIA RTX 3060 (gamer setup) platform : 'Win32', oscpu : 'Windows NT 10.0', vendor : 'Google Inc.', renderer : 'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)', vendorUnmasked : 'NVIDIA Corporation', rendererUnmasked: 'NVIDIA GeForce RTX 3060', hardwareConcurrency: 12, deviceMemory : 16, maxTouchPoints : 0, screenRes : [2560, 1440], colorDepth : 24, pixelRatio : 1, timezone : 'America/Mexico_City', language : 'es-MX', fonts : ['Arial', 'Calibri', 'Cambria', 'Consolas', 'Courier New', 'Georgia', 'Impact', 'Segoe UI', 'Tahoma', 'Times New Roman', 'Verdana'], plugins : ['Chrome PDF Plugin', 'Chrome PDF Viewer'], chromeVersion : '119.0.6045.200', userAgent : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', }, { // macOS 14 + Chrome 120 + Apple M2 platform : 'MacIntel', oscpu : 'Intel Mac OS X 10_15_7', vendor : 'Google Inc.', renderer : 'ANGLE (Apple, ANGLE Metal Renderer: Apple M2, Unspecified Version)', vendorUnmasked : 'Apple', rendererUnmasked: 'Apple M2', hardwareConcurrency: 8, deviceMemory : 8, maxTouchPoints : 0, screenRes : [2560, 1664], colorDepth : 30, pixelRatio : 2, timezone : 'America/Mexico_City', language : 'es-MX', fonts : ['Arial', 'Courier New', 'Geneva', 'Georgia', 'Helvetica', 'Monaco', 'Times New Roman', 'Verdana'], plugins : ['Chrome PDF Plugin', 'Chrome PDF Viewer'], chromeVersion : '120.0.6099.129', userAgent : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', }, ]; // ── Generar identidad de sesión (una sola vez, CONSISTENTE toda la sesión) ─── let _sessionIdentity = null; export function generateSessionIdentity(seed = null) { if (_sessionIdentity && !seed) return _sessionIdentity; // Seleccionar configuración base (misma para toda la sesión) const configIndex = seed ? parseInt(createHash('md5').update(seed).digest('hex').slice(0, 2), 16) % REAL_DEVICE_CONFIGS.length : Math.floor(Math.random() * REAL_DEVICE_CONFIGS.length); const base = REAL_DEVICE_CONFIGS[configIndex]; // Añadir variación SUTIL y CONSISTENTE (mismos valores toda la sesión) const sessionSeed = seed || Date.now().toString(); const noise = (str, range) => { const h = parseInt(createHash('md5').update(str + sessionSeed).digest('hex').slice(0, 4), 16); return (h % (range * 2)) - range; }; _sessionIdentity = { ...base, // Pequeña variación en resolución de pantalla (±8px) — realista screenRes: [ base.screenRes[0] + noise('sw', 8), base.screenRes[1] + noise('sh', 8), ], // Variación en memoria (+/- una banda) — realista deviceMemory: base.deviceMemory, // Canvas noise value — fijo para toda la sesión (no cambia entre páginas) canvasNoise: noise('canvas', 3) * 0.001, audioNoise : noise('audio', 5) * 0.0001, sessionId : sessionSeed, }; return _sessionIdentity; } export function resetSessionIdentity() { _sessionIdentity = null; } // ── Script de inyección completo ───────────────────────────────────────────── // Se inyecta ANTES de que cualquier script de la página se ejecute export function generateStealthScript(identity) { const id = identity || generateSessionIdentity(); return ` (function() { 'use strict'; // ── 1. Ocultar webdriver completamente ───────────────────────────────────── Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: false }); delete window.navigator.__proto__.webdriver; // ── 2. Chrome runtime y permisos (ausentes en headless real) ────────────── if (!window.chrome) { window.chrome = { app: { isInstalled: false, getDetails: function() {}, getIsInstalled: function() {}, runningState: function() { return 'cannot_run'; } }, runtime: { onMessage: { addListener: function() {}, removeListener: function() {}, hasListeners: function() { return false; } }, connect: function() { return { onMessage: { addListener: function() {} }, onDisconnect: { addListener: function() {} }, postMessage: function() {} }; }, sendMessage: function() {}, id: undefined, }, loadTimes: function() { return { commitLoadTime: ${Date.now() / 1000 - 1.2 - Math.random()}, connectionInfo: 'http/1.1', finishDocumentLoadTime: 0, finishLoadTime: 0, firstPaintAfterLoadTime: 0, firstPaintTime: 0, navigationType: 'Other', npnNegotiatedProtocol: 'unknown', requestTime: ${Date.now() / 1000 - 1.5 - Math.random()}, startLoadTime: ${Date.now() / 1000 - 1.4 - Math.random()}, wasAlternateProtocolAvailable: false, wasFetchedViaSpdy: false, wasNpnNegotiated: false }; }, csi: function() { return { onloadT: ${Date.now() - 800}, pageT: 1500, startE: ${Date.now() - 1200}, tran: 15 }; }, }; } // ── 3. Navigator properties — CONSISTENTES con el perfil ───────────────── const navigatorProps = { platform : '${id.platform}', vendor : '${id.vendor}', hardwareConcurrency: ${id.hardwareConcurrency}, deviceMemory : ${id.deviceMemory}, maxTouchPoints : ${id.maxTouchPoints}, languages : ['${id.language}', '${id.language.split('-')[0]}', 'en'], language : '${id.language}', plugins : { length: ${id.plugins.length} }, mimeTypes : { length: 4 }, doNotTrack : null, }; for (const [key, value] of Object.entries(navigatorProps)) { try { Object.defineProperty(navigator, key, { get: () => value, configurable: true }); } catch(e) {} } // ── 4. Canvas fingerprint — ruido CONSISTENTE (mismo en cada carga) ──────── // CRÍTICO: NO aleatorio en cada llamada, sino fijo para la sesión const CANVAS_NOISE = ${id.canvasNoise}; const _origToDataURL = HTMLCanvasElement.prototype.toDataURL; const _origGetImageData = CanvasRenderingContext2D.prototype.getImageData; HTMLCanvasElement.prototype.toDataURL = function(type, quality) { const result = _origToDataURL.apply(this, arguments); // Solo añadir ruido si parece fingerprinting (canvas pequeño) if (this.width <= 300 && this.height <= 150) { try { const ctx = this.getContext('2d'); if (ctx) { const id = ctx.getImageData(0, 0, this.width, this.height); // Ruido imperceptible pero consistente for (let i = 0; i < id.data.length; i += 4) { id.data[i] = Math.max(0, Math.min(255, id.data[i] + Math.round(CANVAS_NOISE * 255))); id.data[i+1] = Math.max(0, Math.min(255, id.data[i+1] + Math.round(CANVAS_NOISE * 127))); } ctx.putImageData(id, 0, 0); } } catch(e) {} } return _origToDataURL.apply(this, arguments); }; // ── 5. WebGL fingerprint — valores REALES del perfil ───────────────────── const _origGetParam = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(parameter) { if (parameter === 0x9245) return '${id.vendorUnmasked}'; // UNMASKED_VENDOR_WEBGL if (parameter === 0x9246) return '${id.rendererUnmasked}'; // UNMASKED_RENDERER_WEBGL if (parameter === 0x1F00) return '${id.vendor}'; // VENDOR if (parameter === 0x1F01) return '${id.renderer}'; // RENDERER return _origGetParam.apply(this, arguments); }; // WebGL2 también try { const _origGetParam2 = WebGL2RenderingContext.prototype.getParameter; WebGL2RenderingContext.prototype.getParameter = function(parameter) { if (parameter === 0x9245) return '${id.vendorUnmasked}'; if (parameter === 0x9246) return '${id.rendererUnmasked}'; if (parameter === 0x1F00) return '${id.vendor}'; if (parameter === 0x1F01) return '${id.renderer}'; return _origGetParam2.apply(this, arguments); }; } catch(e) {} // ── 6. Audio fingerprint — ruido consistente ────────────────────────────── const AUDIO_NOISE = ${id.audioNoise}; try { const _origGetChanData = AudioBuffer.prototype.getChannelData; AudioBuffer.prototype.getChannelData = function(channel) { const data = _origGetChanData.apply(this, arguments); for (let i = 0; i < data.length; i += 100) { data[i] += AUDIO_NOISE; } return data; }; } catch(e) {} // ── 7. Timing attacks — usar valores realistas ──────────────────────────── const _origNow = Date.now; const timeOffset = ${Math.floor(Math.random() * 200) - 100}; // Solo añadir offset mínimo para no romper lógica de timers // Date.now = function() { return _origNow() + timeOffset; }; // deshabilitado: rompe demasiado // ── 8. Permissions API — igual que Chrome real ──────────────────────────── if (navigator.permissions) { const _origQuery = navigator.permissions.query.bind(navigator.permissions); navigator.permissions.query = async function(params) { if (params.name === 'notifications') return { state: Notification.permission, onchange: null }; return _origQuery(params); }; } // ── 9. Screen properties consistentes ──────────────────────────────────── try { Object.defineProperty(screen, 'width', { get: () => ${id.screenRes[0]}, configurable: true }); Object.defineProperty(screen, 'height', { get: () => ${id.screenRes[1]}, configurable: true }); Object.defineProperty(screen, 'availWidth', { get: () => ${id.screenRes[0]}, configurable: true }); Object.defineProperty(screen, 'availHeight', { get: () => ${id.screenRes[1] - 40}, configurable: true }); Object.defineProperty(screen, 'colorDepth', { get: () => ${id.colorDepth}, configurable: true }); Object.defineProperty(window, 'devicePixelRatio', { get: () => ${id.pixelRatio}, configurable: true }); } catch(e) {} // ── 10. Eliminar rastros de Playwright/CDP ──────────────────────────────── try { delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array; delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise; delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol; delete window.__playwright; delete window.__pw_manual; delete window.__PW_inspect; } catch(e) {} // ── 11. iframe contentWindow stealth ───────────────────────────────────── // Anti-bot checks en iframes (como Cloudflare Turnstile) const _origContentWindow = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow'); if (_origContentWindow) { Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', { get: function() { const cw = _origContentWindow.get.call(this); if (!cw) return null; try { Object.defineProperty(cw.navigator, 'webdriver', { get: () => undefined, configurable: true }); } catch(e) {} return cw; }, configurable: true, }); } // ── 12. Ocultar stack traces de automation ──────────────────────────────── // Algunos sites detectan errores con stack traces de Playwright const _origPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = function(err, stack) { const filtered = stack.filter(frame => { const name = frame.getFileName?.() || ''; return !name.includes('playwright') && !name.includes('puppeteer') && !name.includes('@playwright'); }); return _origPrepareStackTrace ? _origPrepareStackTrace(err, filtered) : filtered.map(f => f.toString()).join('\n'); }; window._stealthApplied = true; window._sessionId = '${id.sessionId}'; })(); `; } export function getIdentityStats(identity) { const id = identity || generateSessionIdentity(); return { platform : id.platform, renderer : id.rendererUnmasked, screen : id.screenRes.join('x'), language : id.language, userAgent : id.userAgent.slice(0, 60) + '...', }; }