Fonctions natives remplacées et ce que voit une liste de toString
Une sonde sur des API remplacées : ce qu’attrape une liste de formes natives, quels descripteurs trahissent le correctif, et pourquoi un Proxy ou une fonction liée passent.
BotBrowser Team
Vous voulez la documentation structurée pour Empreinte ?
Cet article fait partie de la bibliothèque éditoriale. Pour les étapes de configuration, la référence et les mises à jour continues, passez directement à la section docs.
BotBrowser permet de rejouer un fixture de même origine autorisé sous un profil déclaré et de garder une trace bornée des API qu’une page a lues, mais il ne peut pas cacher un remplacement écrit en JavaScript à un détecteur de mensonges, ne peut pas faire imprimer à un emballage Proxy le code de la fonction enveloppée, ni changer ce que Function.prototype.toString rapporte pour une fonction que le moteur considère encore native. Cette revue reproduit le jeu de contrôles d’une bibliothèque d’empreintes open source, l’applique à dix cas fixes et consigne quels remplacements la liste blanche attrape, quels descripteurs trahissent encore le correctif, et les deux emballages qui ressortent propres alors que la page a bien changé la valeur reçue par le lecteur.
Résumé
- Une native intacte passe tous les contrôles, donc la sonde imprime
control:Math.random -> cleanet de même pourArray.prototype.mapetFunction.prototype.toString. - Remplacer
Math.randompar une fonction ordinaire laisse une seule marque :naive:replaced Math.random -> failed toString, parce que le texte source n’a plus la forme de code natif. - Lui ajouter un
toStringfactice ne corrige rien : la sonde imprimefailed toString | failed toString toString | extra own keys: toString, donc le correctif se trahit maintenant de trois façons au lieu d’une. - Une fonction qui renvoie
0.42montre encore son corps quand le détecteur appelle le vraitoStringvia une référence liée, doncclone:fnToString-callimprime le code de la page et le clone reste marqué. - Un
Proxyqui intercepteapplypasse toute la batterie, doncproxy:Math.random -> cleanalors que la valeur reçue par la page vient du piège. - Une fonction liée passe aussi, parce que la sonde imprime
bind:fnToString -> function () { [native code] }etbind:checks -> clean. - Une native propre et une remplacée diffèrent d’exactement une marque sur le chemin simple, donc
separation -> cleanFlags=0 patchedFlags=1est toute la marge dont dispose la détection. - L’hôte lui-même peut produire une marque qui n’est pas un remplacement de la page : la sonde imprime
getter:checks -> failed toString hostShape=truepour un accesseur natif dont le code est un corps d’implémentation.
Sommaire
- Ce que la liste blanche accepte
- Ce qu’un remplacement simple laisse
- Ce qu’un toString factice laisse encore filtrer
- Les deux emballages que la liste ne voit pas
- Consigner une lecture de détection de mensonges
Ce que la liste blanche accepte
Le contrôle d’intégrité le moins coûteux du monde de l’empreinte lit le texte source de chaque fonction d’API et le compare à un ensemble fixe de formes que le moteur peut produire. Une bibliothèque d’empreintes open source publie exactement cette liste dans son module de détection de mensonges, et les formes sont assez courtes pour être recopiées telles quelles :
// les trois formes que la liste accepte, NAME étant le nom de l’API
`function ${name}() { [native code] }``function get ${name}() { [native code] }`;
('function () { [native code] }');
Un contrôle qui lit le texte source ne voit pas une valeur qu’un emballage calcule, et source aléatoire de crypto-js et entropie de la bibliothèque consigne la même limite pour une fonction de bibliothèque dont on lit la sortie plutôt que le code. Chacune de ces trois apparaît aussi sous une forme coupée en lignes, avec des sauts de ligne et quatre espaces avant [native code], ce que certains moteurs émettent. Tout ce qui sort de cet ensemble est consigné comme un mensonge contre le nom de l’API, et le rapport regroupe les mensonges par l’API auquel ils appartiennent.
Le contrôle exécute le vrai Function.prototype.toString sur la fonction, puis le réexécute sur le toString de la valeur elle-même. Ce second appel est la partie qui compte, parce qu’une page qui remplace le toString d’une fonction doit aussi le remplacer sur le toString du remplacement, et toute la chaîne doit concorder. Une page qui ne corrige que le niveau extérieur est attrapée par le contrôle intérieur.
Les cas témoins de la sonde confirment que la liste blanche ne se déclenche pas sur des natives intactes. control:Math.random -> clean, control:Array.prototype.map -> clean et control:Function.prototype.toString -> clean sortent sans marque. Cela compte pour la lecture : un détecteur qui se déclencherait sur le témoin serait inutile, donc les trois lignes témoins sont la preuve que les autres marques signifient quelque chose.
Le témoin fixe aussi ce que la liste blanche mesure. Elle ne mesure pas si la fonction se comporte correctement, et elle ne compare pas le comportement à une référence. Elle lit une chaîne par fonction et compare cette chaîne à un ensemble fixe. Une page dont la fonction remplacée renverrait exactement ce que la native aurait renvoyé échoue quand même le contrôle, parce que le texte source diffère ; une page dont l’emballage renvoie tout autre chose peut passer, comme le montre la dernière section, tant que l’emballage est une forme que le moteur imprime lui-même.
Ce qu’un remplacement simple laisse
Le premier correctif qu’écrit une page est le plus simple : affecter une nouvelle fonction sur la propriété. La sonde le fait sur Math.random et lance la batterie :
naive:replaced Math.random -> failed toString
separation -> cleanFlags=0 patchedFlags=1
Une marque, et c’est celle du code source. Le code du remplacement est function random() { return 0.42; }, qui ne correspond à aucune des six formes acceptées, donc il est consigné. Tous les autres contrôles passent, parce que le remplacement est une fonction ordinaire avec le length et le name habituels et sans clés supplémentaires. Toute la détection repose sur cette unique comparaison de chaînes, et la dernière ligne le dit : une native propre donne zéro marque et une remplacée en donne une.
Cette marge d’une marque a une conséquence pratique pour qui lit un rapport. Une liste qui dit random: failed toString dit une chose précise, que le texte source n’avait pas la forme du moteur. Elle ne dit pas ce que fait maintenant la fonction, quand elle a été corrigée, ni si le correctif a changé une valeur. Un rapport qui n’imprime que le nombre de mensonges cache quel contrôle s’est déclenché, et c’est le contrôle qui porte la preuve.
La marge fait aussi que l’ordre des contrôles compte pour le lecteur. Si le contrôle de texte source était le seul, une page pourrait le nettoyer en corrigeant le toString de la fonction, ce qui est le cas suivant. Comme le détecteur lit aussi le jeu de descripteurs et les clés propres, ce second correctif coûte à la page plus de marques qu’il n’en économise. La batterie est construite pour que le correctif le moins coûteux agrandisse le rapport au lieu de le réduire.
Il existe un cas légitime que le contrôle simple de texte source lit mal, et la sonde le trouve dans l’hôte et non dans une page. Une ligne lit un accesseur natif et imprime getter:checks -> failed toString hostShape=true, parce que le code de l’accesseur est un corps d’implémentation avec des champs privés au lieu de la forme get native() { [native code] } du navigateur. Cette marque décrit l’hôte où la sonde a tourné, pas un remplacement de la page. Un détecteur qui prendrait chaque marque pour une preuve de manipulation condamnerait un hôte qui n’a rien corrigé, et c’est pourquoi un rapport doit dire quel environnement a produit les marques.
Ce qu’un toString factice laisse encore filtrer
La réponse évidente au contrôle de texte source est de donner au remplacement un toString qui renvoie la forme acceptée. La sonde le fait et obtient trois marques au lieu d’une :
fake-tostring:Math.random -> failed toString | failed toString toString | extra own keys: toString
fake-tostring:inner -> failed toString
La première marque reste failed toString, et la raison vaut d’être suivie. Le détecteur appelle Function.prototype.toString sur la fonction, pas le toString de la fonction, donc il lit le texte source réel quoi que dise la propriété. Une page qui définit toString sur la fonction change ce que renvoie fn.toString() mais pas ce que renvoie Function.prototype.toString.call(fn), et le détecteur utilise la seconde forme.
La deuxième marque est le contrôle intérieur. Le détecteur lit aussi le code du toString de la fonction, et le faux de la page pour cela est une autre fonction de la page dont le code n’a pas non plus la forme acceptée. Le correctif censé faire taire la première marque crée donc une seconde marque sur la valeur installée.
La troisième marque est le contrôle des clés propres. Une vraie fonction native expose exactement length et name comme clés propres, et parfois prototype. Ajouter une propriété toString fait apparaître une troisième clé propre, et la sonde imprime extra own keys: toString pour elle. Le correctif est donc visible depuis la liste des propriétés, même pour un lecteur qui ne regarde jamais le texte source.
Les trois marques ensemble expliquent pourquoi le correctif bon marché est un mauvais échange. Une page part avec une marque, corrige toString pour la retirer, et finit avec trois. Un détecteur qui publierait les noms de ses contrôles laisserait la page voir que le correctif empire le rapport, et le mouvement suivant de la page serait d’arrêter de corriger et de changer la propriété autrement, ce qui est là où commence la section suivante.
Le cas du clone de la sonde essaie une version plus soignée de la même idée, en liant le vrai Function.prototype.toString à la vraie fonction et en installant la référence liée comme toString du clone. La ligne clone:fnToString-call imprime le corps du clone, function random() { return 0.42; }, donc le lien ne transporte pas le texte source de la cible dans l’appel. Le clone reste marqué sur failed toString et de nouveau sur extra own keys: toString, soit la même paire qu’un correctif simple, le travail en plus n’achetant rien.
Les deux emballages que la liste ne voit pas
Deux formes passent toute la batterie tout en changeant la valeur qu’une page reçoit, et la sonde consigne les deux.
La première est un Proxy qui intercepte apply. La sonde enveloppe le vrai Math.random dans un proxy dont le piège renvoie 0.42, affecte le proxy et lance les contrôles :
proxy:Math.random -> clean
Aucune marque. L’appel Function.prototype.toString du proxy est transmis à la cible, qui est la native intacte, donc le texte source a la forme acceptée. Le proxy n’a pas de toString à lui, donc le contrôle intérieur lit aussi la forme native. Et les clés propres de la cible du proxy sont celles de la native, donc le contrôle des clés propres passe. Chaque signal lu par la batterie décrit la cible, alors que la valeur reçue par la page vient du piège.
Les descripteurs qui changent vraiment quand une page redéfinit une propriété sont ceux dont traite l’ordre des clés d’objet. Notez qu’il s’agit d’un proxy sur une native appelable, et l’article précédent sur les pièges Proxy et les invariants couvre ce que le moteur impose pour cette forme. Les invariants limitent ce qu’un proxy peut rapporter des propriétés d’une cible, et aucun ne limite la valeur renvoyée par un piège apply. C’est pourquoi le proxy est invisible à un contrôle de code et de descripteurs : il n’y a rien d’incohérent à attraper, parce que le proxy est un emballage légal et cohérent autour de la native.
La seconde forme est une fonction liée. La sonde lie Math.random sans argument et lance les contrôles :
bind:fnToString -> function () { [native code] }
bind:checks -> clean
Une fonction liée est créée par le moteur lui-même, donc son texte source a la forme acceptée function () { [native code] }, et elle porte le jeu de clés propres de la native. Une page peut donc prendre n’importe quelle native, la lier, et obtenir une valeur qui passe un contrôle d’intégrité qui ne lirait que code et descripteurs. Que le lien change le comportement dépend de la cible : lier Math.random à un this ne fait rien, mais lier une méthode qui lit this à un proxy ou à un récepteur fabriqué change bien ce que voit l’appelant.
Le comportement d’un lien dépend de ce que la fonction liée lit, et les pièges Proxy et les invariants montrent ce que le moteur vérifie quand le récepteur est un proxy. Les deux points aveugles partagent une cause : la batterie lit des faits statiques sur un objet fonction, et la page contrôle ce que reçoit réellement l’appelant. Un contrôle qui veut fermer les deux doit comparer une valeur renvoyée à une référence que la page ne peut pas atteindre, pas seulement lire le texte de la fonction. C’est la différence entre un contrôle d’intégrité statique et un contrôle de comportement, et un rapport qui n’exécute que la moitié statique ne le dit que s’il nomme le contrôle exécuté.
Consigner une lecture de détection de mensonges
Une lecture de détection de mensonges soutient une affirmation bornée : ce détecteur, ces contrôles, cet environnement ont produit ces marques. Quatre champs la gardent vérifiable :
- Les noms des contrôles déclenchés, pas seulement un compte, parce que
failed toStringetextra own keysdécrivent des modifications différentes et seul le nom dit laquelle. - Les formes de code acceptées, parce que la liste blanche est un ensemble fixe et un rapport sans lui ne se rejoue pas sur un autre moteur qui imprimerait une forme que l’ensemble omet.
- Si la valeur a été comparée à une référence, parce qu’un contrôle statique laisse passer un
Proxyet une fonction liée alors qu’un contrôle de comportement peut ne pas le faire. - L’hôte qui a produit les marques, parce qu’un accesseur natif peut imprimer un corps d’implémentation qui ressemble à un correctif de la page, et une lecture sans l’hôte ne distingue pas les deux.
Sources publiques
- MDN : Function.prototype.toString()
- ECMAScript : Function.prototype.toString
- ECMAScript : OwnPropertyKeys
- MDN : Proxy
- MDN : Function.prototype.bind()
- BotBrowser : référence de cohérence d’automatisation
Conclusion pratique
Lisez un rapport de mensonges par nom de contrôle, parce que le compte seul ne distingue pas un remplacement simple d’un toString factice ou d’une troisième clé propre. Attendez-vous à ce qu’un Proxy et une fonction liée passent un contrôle de code et de descripteurs, puisque les deux sont des formes créées par le moteur autour d’une native intacte, et traitez un résultat propre de ce contrôle comme « le texte source ressemble à du natif » et non comme « la valeur est celle de la native ». Les deux articles précédents sur les pièges Proxy et les invariants et sur l’ordre des clés d’objet couvrent la mécanique dont dépend le contrôle des clés, et la trace doit toujours nommer les contrôles exécutés et l’hôte qui a produit les marques, parce qu’un accesseur natif peut imprimer un corps qui ressemble exactement à un correctif.
Cet article fait partie du guide : Qu'est-ce qu'une empreinte de navigateur ?
Articles Connexes
Source aléatoire de crypto-js : quelle branche
EmpreinteObservabilité de l’aléa cryptographique et empreintes du navigateur
EmpreinteBotD : quels détecteurs s’exécutent vraiment
EmpreinteFingerprinting de la Network Information API : revue de politique
EmpreinteWeakMap et WeakSet : garder et lâcher des clés
EmpreinteClient Hints Fingerprinting : en-têtes HTTP et identité
Faites passer BotBrowser de la recherche à la production
Utilisez ces guides pour comprendre le modèle, puis passez à la validation multi-plateforme, aux contextes isolés et au déploiement navigateur prêt pour l'échelle.