Files
test-main/browser/fingerprint_protection.py
Claude Project Manager 08ed938105 Initial commit
2025-07-03 21:11:05 +02:00

721 Zeilen
32 KiB
Python

# browser/fingerprint_protection.py
"""
Schutz vor Browser-Fingerprinting - Erweiterte Methoden zum Schutz vor verschiedenen Fingerprinting-Techniken
"""
import random
import logging
import json
from typing import Dict, List, Any, Optional, Tuple
logger = logging.getLogger("fingerprint_protection")
class FingerprintProtection:
"""
Bietet erweiterte Schutzmaßnahmen gegen verschiedene Browser-Fingerprinting-Techniken.
Kann mit dem PlaywrightManager integriert werden, um die Anonymität zu verbessern.
"""
def __init__(self, context=None, stealth_config=None):
"""
Initialisiert den Fingerprint-Schutz.
Args:
context: Der Browser-Kontext, auf den die Schutzmaßnahmen angewendet werden sollen
stealth_config: Konfigurationseinstellungen für das Stealth-Verhalten
"""
self.context = context
self.stealth_config = stealth_config or {}
self.scripts = []
self.noise_level = self.stealth_config.get("noise_level", 0.5) # 0.0-1.0
# Standardwerte für Fingerprinting-Schutz
self.defaults = {
"webgl_vendor": "Google Inc. (Intel)",
"webgl_renderer": "Intel Iris OpenGL Engine",
"canvas_noise": True,
"audio_noise": True,
"webgl_noise": True,
"hardware_concurrency": 8,
"device_memory": 8,
"timezone_id": "Europe/Berlin"
}
# Einstellungen mit benutzerdefinierten Werten überschreiben
for key, value in self.stealth_config.items():
if key in self.defaults:
self.defaults[key] = value
# Schutzmaßnahmen initialisieren
self._init_protections()
def set_context(self, context):
"""Setzt den Browser-Kontext nach der Initialisierung."""
self.context = context
def _init_protections(self):
"""Initialisiert alle Fingerprint-Schutzmaßnahmen."""
self._init_canvas_protection()
self._init_webgl_protection()
self._init_audio_protection()
self._init_navigator_protection()
self._init_misc_protections()
def _init_canvas_protection(self):
"""
Initialisiert den Schutz gegen Canvas-Fingerprinting.
Dies modifiziert das Canvas-Element, um leicht abweichende Werte zurückzugeben.
"""
script = """
() => {
// Originalmethoden speichern
const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
const originalToBlob = HTMLCanvasElement.prototype.toBlob;
// Funktion zum Hinzufügen von Rauschen zu Bilddaten
const addNoise = (data, noise) => {
const noiseFactor = noise || 0.03; // Standardwert für das Rauschlevel
// Nur auf Canvas über Mindestgröße anwenden, um normale Canvas nicht zu stören
if (data.width > 16 && data.height > 16) {
const pixelCount = data.width * data.height * 4; // RGBA-Kanäle
// Präzise, aber wenig wahrnehmbare Änderungen
const minPixels = Math.max(10, Math.floor(pixelCount * 0.005)); // Mindestens 10 Pixel
const maxPixels = Math.min(100, Math.floor(pixelCount * 0.01)); // Höchstens 100 Pixel
// Zufällige Anzahl an Pixeln auswählen
const pixelsToModify = Math.floor(Math.random() * (maxPixels - minPixels)) + minPixels;
// Pixel modifizieren
for (let i = 0; i < pixelsToModify; i++) {
// Zufälligen Pixel auswählen
const offset = Math.floor(Math.random() * pixelCount / 4) * 4;
// Nur RGB modifizieren, Alpha-Kanal unverändert lassen
for (let j = 0; j < 3; j++) {
// Subtile Änderungen hinzufügen (±1-2)
const mod = Math.floor(Math.random() * 3) - 1;
data.data[offset + j] = Math.max(0, Math.min(255, data.data[offset + j] + mod));
}
}
}
return data;
};
// getImageData überschreiben
CanvasRenderingContext2D.prototype.getImageData = function() {
const imageData = originalGetImageData.apply(this, arguments);
return addNoise(imageData, NOISE_LEVEL);
};
// toDataURL überschreiben
HTMLCanvasElement.prototype.toDataURL = function() {
// Temporäre Modifikation für getImageData
const tempGetImageData = CanvasRenderingContext2D.prototype.getImageData;
CanvasRenderingContext2D.prototype.getImageData = originalGetImageData;
// toDataURL aufrufen
let dataURL = originalToDataURL.apply(this, arguments);
// Wenn das Canvas groß genug ist und nicht von kritischen Anwendungen verwendet wird
if (this.width > 16 && this.height > 16 && !this.hasAttribute('data-fingerprint-protect-ignore')) {
// Rauschen in binären Teil des DataURL einfügen
const parts = dataURL.split(',');
if (parts.length === 2) {
// Binäre Daten dekodieren
const binary = atob(parts[1]);
// Geringfügige Änderungen bei etwa 0,1% der Bytes
let modifiedBinary = '';
for (let i = 0; i < binary.length; i++) {
if (Math.random() < 0.001 * NOISE_LEVEL) {
// Byte leicht ändern
const byte = binary.charCodeAt(i);
const mod = Math.floor(Math.random() * 3) - 1;
modifiedBinary += String.fromCharCode(Math.max(0, Math.min(255, byte + mod)));
} else {
modifiedBinary += binary[i];
}
}
// Binäre Daten wieder kodieren
dataURL = parts[0] + ',' + btoa(modifiedBinary);
}
}
// getImageData wiederherstellen
CanvasRenderingContext2D.prototype.getImageData = tempGetImageData;
return dataURL;
};
// toBlob überschreiben
HTMLCanvasElement.prototype.toBlob = function(callback) {
// Original toBlob aufrufen
originalToBlob.apply(this, [function(blob) {
// Wenn das Canvas groß genug ist und nicht von kritischen Anwendungen verwendet wird
if (this.width > 16 && this.height > 16 && !this.hasAttribute('data-fingerprint-protect-ignore')) {
// Blob zu ArrayBuffer konvertieren
const reader = new FileReader();
reader.onload = function() {
const arrayBuffer = reader.result;
const array = new Uint8Array(arrayBuffer);
// Geringfügige Änderungen bei etwa 0,1% der Bytes
for (let i = 0; i < array.length; i++) {
if (Math.random() < 0.001 * NOISE_LEVEL) {
// Byte leicht ändern
const mod = Math.floor(Math.random() * 3) - 1;
array[i] = Math.max(0, Math.min(255, array[i] + mod));
}
}
// Neuen Blob erstellen
const modifiedBlob = new Blob([array], {type: blob.type});
callback(modifiedBlob);
};
reader.readAsArrayBuffer(blob);
} else {
// Unveränderten Blob zurückgeben
callback(blob);
}
}.bind(this)]);
};
}
""".replace("NOISE_LEVEL", str(self.noise_level))
self.scripts.append(script)
def _init_webgl_protection(self):
"""
Initialisiert den Schutz gegen WebGL-Fingerprinting.
Dies modifiziert WebGL-spezifische Werte, die für Fingerprinting verwendet werden.
"""
webgl_vendor = self.defaults["webgl_vendor"]
webgl_renderer = self.defaults["webgl_renderer"]
script = f"""
() => {{
// WebGL Vendor und Renderer spoofen
const getParameterProxies = [
WebGLRenderingContext.prototype,
WebGL2RenderingContext.prototype
];
getParameterProxies.forEach(contextPrototype => {{
if (!contextPrototype) return;
const originalGetParameter = contextPrototype.getParameter;
contextPrototype.getParameter = function(parameter) {{
// WebGL Vendor (VENDOR)
if (parameter === 0x1F00) {{
return "{webgl_vendor}";
}}
// WebGL Renderer (RENDERER)
if (parameter === 0x1F01) {{
return "{webgl_renderer}";
}}
// Unshaded Language Version (SHADING_LANGUAGE_VERSION)
if (parameter === 0x8B8C) {{
const originalValue = originalGetParameter.call(this, parameter);
// Subtile Änderungen an der Version
const versionMatch = originalValue.match(/^WebGL GLSL ES ([0-9]\\.[0-9][0-9])/);
if (versionMatch) {{
return originalValue.replace(versionMatch[1],
(parseFloat(versionMatch[1]) + (Math.random() * 0.01 - 0.005)).toFixed(2));
}}
}}
// VERSION
if (parameter === 0x1F02) {{
const originalValue = originalGetParameter.call(this, parameter);
// Subtile Änderungen an der Version
const versionMatch = originalValue.match(/^WebGL ([0-9]\\.[0-9])/);
if (versionMatch) {{
return originalValue.replace(versionMatch[1],
(parseFloat(versionMatch[1]) + (Math.random() * 0.01 - 0.005)).toFixed(1));
}}
}}
return originalGetParameter.apply(this, arguments);
}};
}});
// WebGL Vertex und Fragment Shader spoofen
const shaderSourceProxies = [
WebGLRenderingContext.prototype,
WebGL2RenderingContext.prototype
];
shaderSourceProxies.forEach(contextPrototype => {{
if (!contextPrototype) return;
const originalShaderSource = contextPrototype.shaderSource;
contextPrototype.shaderSource = function(shader, source) {{
// Füge geringfügige Unterschiede in Kommentaren ein, ohne die Funktionalität zu beeinträchtigen
if (source.indexOf('//') !== -1) {{
// Zufälligen Kommentar leicht modifizieren
source = source.replace(/\\/\\/(.*?)\\n/g, (match, comment) => {{
if (Math.random() < 0.1) {{
// Füge ein Leerzeichen oder einen Bindestrich hinzu oder entferne eines
const modifications = [
' ', '-', '', ' '
];
const mod = modifications[Math.floor(Math.random() * modifications.length)];
return `//` + comment + mod + `\\n`;
}}
return match;
}});
}}
// Ersetze bestimmte Whitespace-Muster
source = source.replace(/\\s{2,}/g, match => {{
if (Math.random() < 0.05) {{
return ' '.repeat(match.length + (Math.random() < 0.5 ? 1 : -1));
}}
return match;
}});
return originalShaderSource.call(this, shader, source);
}};
}});
// Canvas-Kontext spoofen
const getContextProxies = [
HTMLCanvasElement.prototype
];
getContextProxies.forEach(canvasPrototype => {{
const originalGetContext = canvasPrototype.getContext;
canvasPrototype.getContext = function(contextType, contextAttributes) {{
const context = originalGetContext.apply(this, arguments);
if (context && (contextType === 'webgl' || contextType === 'experimental-webgl' || contextType === 'webgl2')) {{
// Zufällige Werte für verschiedene Parameter einführen
if ({str(self.defaults["webgl_noise"]).lower()}) {{
// Zufällige Modifikation für MAX_VERTEX_UNIFORM_VECTORS
const MAX_VERTEX_UNIFORM_VECTORS = context.getParameter(context.MAX_VERTEX_UNIFORM_VECTORS);
Object.defineProperty(context, 'MAX_VERTEX_UNIFORM_VECTORS', {{
get: () => MAX_VERTEX_UNIFORM_VECTORS + Math.floor(Math.random() * 3) - 1
}});
// Zufällige Modifikation für MAX_FRAGMENT_UNIFORM_VECTORS
const MAX_FRAGMENT_UNIFORM_VECTORS = context.getParameter(context.MAX_FRAGMENT_UNIFORM_VECTORS);
Object.defineProperty(context, 'MAX_FRAGMENT_UNIFORM_VECTORS', {{
get: () => MAX_FRAGMENT_UNIFORM_VECTORS + Math.floor(Math.random() * 3) - 1
}});
}}
}}
return context;
}};
}});
}}
"""
self.scripts.append(script)
def _init_audio_protection(self):
"""
Initialisiert den Schutz gegen Audio-Fingerprinting.
Dies modifiziert die Audio-API-Funktionen, die für Fingerprinting verwendet werden.
"""
script = f"""
() => {{
// Audio-Kontext spoofen
if (window.AudioContext || window.webkitAudioContext) {{
const AudioContextProxy = window.AudioContext || window.webkitAudioContext;
const originalAudioContext = AudioContextProxy;
// AudioContext überschreiben
window.AudioContext = window.webkitAudioContext = function() {{
const context = new originalAudioContext();
// createOscillator überschreiben
const originalCreateOscillator = context.createOscillator;
context.createOscillator = function() {{
const oscillator = originalCreateOscillator.apply(this, arguments);
// Frequenz leicht modifizieren
const originalFrequency = oscillator.frequency;
Object.defineProperty(oscillator, 'frequency', {{
get: function() {{
return originalFrequency;
}},
set: function(value) {{
if (typeof value === 'number') {{
// Leichte Änderung hinzufügen
const noise = (Math.random() * 0.02 - 0.01) * value;
originalFrequency.value = value + noise;
}} else {{
originalFrequency.value = value;
}}
}}
}});
return oscillator;
}};
// getChannelData überschreiben
const originalGetChannelData = context.createBuffer.prototype?.getChannelData || OfflineAudioContext.prototype?.getChannelData;
if (originalGetChannelData) {{
context.createBuffer.prototype.getChannelData = function(channel) {{
const array = originalGetChannelData.call(this, channel);
if ({str(self.defaults["audio_noise"]).lower()} && this.length > 20) {{
// Sehr subtiles Rauschen hinzufügen (bei etwa 0,1% der Samples)
const noise = {self.noise_level} * 0.0001;
// Effiziente Implementierung
const samples = Math.min(200, Math.floor(array.length * 0.001));
for (let i = 0; i < samples; i++) {{
const idx = Math.floor(Math.random() * array.length);
array[idx] += (Math.random() * 2 - 1) * noise;
}}
}}
return array;
}};
}}
// AnalyserNode.getFloatFrequencyData überschreiben
if (context.createAnalyser) {{
const originalCreateAnalyser = context.createAnalyser;
context.createAnalyser = function() {{
const analyser = originalCreateAnalyser.apply(this, arguments);
const originalGetFloatFrequencyData = analyser.getFloatFrequencyData;
analyser.getFloatFrequencyData = function(array) {{
originalGetFloatFrequencyData.call(this, array);
if ({str(self.defaults["audio_noise"]).lower()} && array.length > 20) {{
// Sehr subtiles Rauschen hinzufügen
const noise = {self.noise_level} * 0.001;
// Effiziente Implementierung für große Arrays
const samples = Math.min(20, Math.floor(array.length * 0.01));
for (let i = 0; i < samples; i++) {{
const idx = Math.floor(Math.random() * array.length);
array[idx] += (Math.random() * 2 - 1) * noise;
}}
}}
return array;
}};
return analyser;
}};
}}
return context;
}};
}}
}}
"""
self.scripts.append(script)
def _init_navigator_protection(self):
"""
Initialisiert den Schutz gegen Navigator-Objekt-Fingerprinting.
Dies modifiziert verschiedene Navigator-Eigenschaften, die für Fingerprinting verwendet werden.
"""
hardware_concurrency = self.defaults["hardware_concurrency"]
device_memory = self.defaults["device_memory"]
script = f"""
() => {{
// Navigator-Eigenschaften überschreiben
// hardwareConcurrency (CPU-Kerne)
if (navigator.hardwareConcurrency) {{
Object.defineProperty(navigator, 'hardwareConcurrency', {{
get: () => {hardware_concurrency}
}});
}}
// deviceMemory (RAM)
if (navigator.deviceMemory) {{
Object.defineProperty(navigator, 'deviceMemory', {{
get: () => {device_memory}
}});
}}
// language und languages
if (navigator.language) {{
const originalLanguage = navigator.language;
const originalLanguages = navigator.languages;
Object.defineProperty(navigator, 'language', {{
get: () => originalLanguage
}});
Object.defineProperty(navigator, 'languages', {{
get: () => originalLanguages
}});
}}
// userAgent-Konsistenz sicherstellen
if (navigator.userAgent) {{
const userAgent = navigator.userAgent;
// Wenn der userAgent bereits überschrieben wurde, stellen wir
// sicher, dass die appVersion und platform konsistent sind
const browserInfo = {{
chrome: /Chrome\\/(\\d+)/.exec(userAgent),
firefox: /Firefox\\/(\\d+)/.exec(userAgent),
safari: /Safari\\/(\\d+)/.exec(userAgent),
edge: /Edg(e|)\\/(\\d+)/.exec(userAgent)
}};
// Platform basierend auf userAgent bestimmen
let platform = '{self.defaults.get("platform", "Win32")}';
if (/Windows/.test(userAgent)) platform = 'Win32';
else if (/Macintosh/.test(userAgent)) platform = 'MacIntel';
else if (/Linux/.test(userAgent)) platform = 'Linux x86_64';
else if (/Android/.test(userAgent)) platform = 'Linux armv8l';
else if (/iPhone|iPad/.test(userAgent)) platform = 'iPhone';
Object.defineProperty(navigator, 'platform', {{
get: () => platform
}});
// appVersion konsistent machen
if (navigator.appVersion) {{
Object.defineProperty(navigator, 'appVersion', {{
get: () => userAgent.substring(8)
}});
}}
// vendor basierend auf Browser setzen
let vendor = '{self.defaults.get("vendor", "Google Inc.")}';
if (browserInfo.safari) vendor = 'Apple Computer, Inc.';
else if (browserInfo.firefox) vendor = '';
Object.defineProperty(navigator, 'vendor', {{
get: () => vendor
}});
}}
"""
self.scripts.append(script)
def _init_misc_protections(self):
"""
Initialisiert verschiedene weitere Schutzmaßnahmen gegen Fingerprinting.
"""
timezone_id = self.defaults["timezone_id"]
script = f"""
() => {{
// Date.prototype.getTimezoneOffset überschreiben
const originalGetTimezoneOffset = Date.prototype.getTimezoneOffset;
Date.prototype.getTimezoneOffset = function() {{
// Zeitzonendaten für '{timezone_id}'
// Mitteleuropäische Zeit (CET/CEST): UTC+1 / UTC+2 (Sommerzeit)
const date = new Date(this);
// Prüfen, ob Sommerzeit
const jan = new Date(date.getFullYear(), 0, 1);
const jul = new Date(date.getFullYear(), 6, 1);
const standardOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
// Sommerzeit in Europa: Ende März bis Ende Oktober
const isDST = date.getMonth() > 2 && date.getMonth() < 10;
// Offset in Minuten: CET = -60, CEST = -120
return isDST ? -120 : -60;
}};
// Plugins und MimeTypes Schutz
if (navigator.plugins) {{
// Leere oder gefälschte Plugins
Object.defineProperty(navigator, 'plugins', {{
get: () => {{
// Plugins-Array-Eigenschaften simulieren
const plugins = {{
length: 0,
item: () => null,
namedItem: () => null,
refresh: () => {{}},
[Symbol.iterator]: function* () {{}}
}};
return plugins;
}}
}});
// MimeTypes ebenfalls leeren
if (navigator.mimeTypes) {{
Object.defineProperty(navigator, 'mimeTypes', {{
get: () => {{
// MimeTypes-Array-Eigenschaften simulieren
const mimeTypes = {{
length: 0,
item: () => null,
namedItem: () => null,
[Symbol.iterator]: function* () {{}}
}};
return mimeTypes;
}}
}});
}}
}}
// Performance.now() und Date.now() - Schutz gegen Timing-Angriffe
if (window.performance && performance.now) {{
const originalNow = performance.now;
performance.now = function() {{
const value = originalNow.call(performance);
// Subtile Abweichung hinzufügen
return value + (Math.random() * 0.01);
}};
}}
// Date.now() ebenfalls mit subtiler Abweichung
const originalDateNow = Date.now;
Date.now = function() {{
const value = originalDateNow.call(Date);
// Subtile Abweichung hinzufügen (±1ms)
return value + (Math.random() < 0.5 ? 1 : 0);
}};
// screen-Eigenschaften konsistent machen
if (window.screen) {{
const originalWidth = screen.width;
const originalHeight = screen.height;
const originalColorDepth = screen.colorDepth;
const originalPixelDepth = screen.pixelDepth;
// Abweichungen verhindern - konsistente Werte liefern
Object.defineProperties(screen, {{
'width': {{ get: () => originalWidth }},
'height': {{ get: () => originalHeight }},
'availWidth': {{ get: () => originalWidth }},
'availHeight': {{ get: () => originalHeight - 40 }}, // Taskleiste simulieren
'colorDepth': {{ get: () => originalColorDepth }},
'pixelDepth': {{ get: () => originalPixelDepth }}
}});
}}
}}
"""
self.scripts.append(script)
def apply_to_context(self, context=None):
"""
Wendet alle Skripte auf den Browser-Kontext an.
Args:
context: Der Browser-Kontext, falls er noch nicht gesetzt wurde
"""
if context:
self.context = context
if not self.context:
logger.warning("Kein Browser-Kontext zum Anwenden der Fingerprint-Schutzmaßnahmen")
return
for script in self.scripts:
self.context.add_init_script(script)
logger.info(f"Fingerprint-Schutzmaßnahmen auf Browser-Kontext angewendet ({len(self.scripts)} Skripte)")
def get_fingerprint_status(self) -> Dict[str, Any]:
"""
Gibt den aktuellen Status der Fingerprint-Schutzmaßnahmen zurück.
Returns:
Dict[str, Any]: Status der Fingerprint-Schutzmaßnahmen
"""
status = {
"active": self.context is not None,
"script_count": len(self.scripts),
"protections": {
"canvas": self.defaults["canvas_noise"],
"webgl": self.defaults["webgl_noise"],
"audio": self.defaults["audio_noise"],
"navigator": True,
"battery": "getBattery" in self.scripts[-1],
"timing": "performance.now" in self.scripts[-1]
},
"noise_level": self.noise_level,
"custom_values": {
"webgl_vendor": self.defaults["webgl_vendor"],
"webgl_renderer": self.defaults["webgl_renderer"],
"hardware_concurrency": self.defaults["hardware_concurrency"],
"device_memory": self.defaults["device_memory"],
"timezone_id": self.defaults["timezone_id"]
}
}
return status
def rotate_fingerprint(self, noise_level: Optional[float] = None):
"""
Rotiert den Fingerprint durch Neugenerierung der Schutzmaßnahmen.
Args:
noise_level: Optionales neues Rauschniveau (0.0-1.0)
Returns:
bool: True bei Erfolg, False bei Fehler
"""
if noise_level is not None:
self.noise_level = max(0.0, min(1.0, noise_level))
try:
# Skripte zurücksetzen
self.scripts = []
# Neues WebGL-Vendor/Renderer-Paar generieren
webgl_vendors = [
"Google Inc.",
"Google Inc. (Intel)",
"Google Inc. (NVIDIA)",
"Google Inc. (AMD)",
"Intel Inc.",
"NVIDIA Corporation",
"AMD"
]
webgl_renderers = [
"ANGLE (Intel, Intel(R) HD Graphics 620 Direct3D11 vs_5_0 ps_5_0)",
"ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 Direct3D11 vs_5_0 ps_5_0)",
"ANGLE (AMD, AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0)",
"Intel Iris OpenGL Engine",
"NVIDIA GeForce GTX 980 OpenGL Engine",
"AMD Radeon Pro 560 OpenGL Engine",
"Mesa DRI Intel(R) UHD Graphics 620 (KBL GT2)",
"Mesa DRI NVIDIA GeForce GTX 1650"
]
# Zufällige Werte wählen
self.defaults["webgl_vendor"] = random.choice(webgl_vendors)
self.defaults["webgl_renderer"] = random.choice(webgl_renderers)
# Hardware-Concurrency und Device-Memory variieren
self.defaults["hardware_concurrency"] = random.choice([2, 4, 6, 8, 12, 16])
self.defaults["device_memory"] = random.choice([2, 4, 8, 16])
# Schutzmaßnahmen neu initialisieren
self._init_protections()
# Auf Kontext anwenden, falls vorhanden
if self.context:
self.apply_to_context()
logger.info(f"Fingerprint erfolgreich rotiert (Noise-Level: {self.noise_level:.2f})")
return True
except Exception as e:
logger.error(f"Fehler bei der Rotation des Fingerprints: {e}")
return False