@anonympins/fingerprint 0.6.7 → 0.7.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/CHANGELOG.md +16 -0
- package/README.md +3 -7
- package/package.json +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +1 -1
- package/src/js/dynamic-wasm.js +7 -7
- package/src/js/fingerprint.client.js +1463 -1447
- package/src/js/fingerprint.js +369 -95
- package/src/js/library.js +15 -8
- package/src/js/pow.solver.inline.js +56 -10
- package/src/js/pow.solver.js +56 -10
- package/src/js/pow.worker.js +35 -13
- package/src/js/tests/fingerprint.test.js +69 -35
- package/src/php/Challenge/ChallengeUtils.php +148 -22
- package/src/php/Config/SecurityProfiles.php +1 -0
- package/src/php/FingerprintClient.php +148 -148
- package/src/php/FingerprintEngine.php +1834 -1880
- package/src/php/Ja3AnomalyDetector.php +257 -227
- package/src/php/Optimization/OptimizationOperators.php +2 -2
- package/src/php/Tests/FingerprintEngineTest.php +2 -33
- package/src/php/Utils/RequestUtils.php +113 -18
- package/src/php/Utils/TLSClientHelloParser.php +158 -1
- package/src/js/fingerprint.client.obfuscated.js +0 -1
|
@@ -1025,13 +1025,20 @@ class RequestUtils
|
|
|
1025
1025
|
|
|
1026
1026
|
public static function getThreatIntelScore(RequestContext $context, array $threatIntelConfig): array
|
|
1027
1027
|
{
|
|
1028
|
+
$score = 0.0;
|
|
1028
1029
|
$zkpY = $context->zkpY;
|
|
1029
|
-
if (empty($zkpY)) {
|
|
1030
|
-
|
|
1030
|
+
if (!empty($zkpY)) {
|
|
1031
|
+
$store = StoreManager::getStore();
|
|
1032
|
+
$isBanned = $store->has("banned-zkp-y:{$zkpY}");
|
|
1033
|
+
if ($isBanned) {
|
|
1034
|
+
$score = 100.0;
|
|
1035
|
+
}
|
|
1031
1036
|
}
|
|
1032
|
-
|
|
1033
|
-
$
|
|
1034
|
-
|
|
1037
|
+
|
|
1038
|
+
$rttProxyScore = \Anonympins\Fingerprint\Ja3AnomalyDetector::getRttProxyScore($context);
|
|
1039
|
+
$score = max($score, (float)$rttProxyScore);
|
|
1040
|
+
|
|
1041
|
+
return ['threatIntelScore' => $score];
|
|
1035
1042
|
}
|
|
1036
1043
|
|
|
1037
1044
|
/**
|
|
@@ -1355,23 +1362,24 @@ class RequestUtils
|
|
|
1355
1362
|
return ['subnetScore' => 0.0];
|
|
1356
1363
|
}
|
|
1357
1364
|
|
|
1358
|
-
|
|
1359
|
-
$
|
|
1365
|
+
// 1. Régularisation bayésienne continue de la densité
|
|
1366
|
+
$bayesianDensity = ((float)$highScoreCount + 0.5) / ((float)$deviceCount + 2.5);
|
|
1360
1367
|
|
|
1361
|
-
//
|
|
1362
|
-
$
|
|
1363
|
-
$
|
|
1368
|
+
// 2. Dispersion IP / Terminal (CGNAT vs Proxy Pool)
|
|
1369
|
+
$ipDispersion = min(2.0, (float)$ipCount / (float)$deviceCount);
|
|
1370
|
+
$ipMultiplier = 0.6 + 0.4 * tanh($ipDispersion);
|
|
1364
1371
|
|
|
1365
|
-
|
|
1372
|
+
// 3. Dispersion User-Agent / Terminal
|
|
1373
|
+
$uaDispersion = min(3.0, (float)max(1, $uaCount) / (float)$deviceCount);
|
|
1374
|
+
$uaMultiplier = 0.7 + 0.3 * tanh($uaDispersion - 1.0);
|
|
1366
1375
|
|
|
1367
|
-
|
|
1368
|
-
$
|
|
1376
|
+
// 4. Intensité continue de la menace
|
|
1377
|
+
$rawIntensity = (float)$highScoreCount * $bayesianDensity * $ipMultiplier * $uaMultiplier;
|
|
1369
1378
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
$finalScore = min(100.0, round($baseScore * $densityMultiplier * $distributionMultiplier * $uaMultiplier * $dampening * 10.0) / 10.0);
|
|
1379
|
+
// 5. Asymptote continue via tanh (99.9 maximum strict, dérivable et sans saut)
|
|
1380
|
+
$asymptote = 99.9;
|
|
1381
|
+
$scale = 4.0;
|
|
1382
|
+
$finalScore = round($asymptote * tanh($rawIntensity / $scale) * 10.0) / 10.0;
|
|
1375
1383
|
|
|
1376
1384
|
return ['subnetScore' => $finalScore];
|
|
1377
1385
|
}
|
|
@@ -2031,4 +2039,91 @@ class RequestUtils
|
|
|
2031
2039
|
|
|
2032
2040
|
return !empty($deviceId) && !empty($deviceHash); // Match d'identité matérielle stricte (deviceId + deviceHash validés par HMAC)
|
|
2033
2041
|
}
|
|
2042
|
+
|
|
2043
|
+
private static function imul(int $a, int $b): int
|
|
2044
|
+
{
|
|
2045
|
+
return ($a * $b) & 0xffffffff;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
public static function cyrb53(string $str, int $seed = 0): int
|
|
2049
|
+
{
|
|
2050
|
+
$h1 = (0xdeadbeef ^ $seed) & 0xffffffff;
|
|
2051
|
+
$h2 = (0x41c6ce57 ^ $seed) & 0xffffffff;
|
|
2052
|
+
for ($i = 0; $i < strlen($str); $i++) {
|
|
2053
|
+
$ch = ord($str[$i]);
|
|
2054
|
+
$h1 = self::imul($h1 ^ $ch, 2654435761);
|
|
2055
|
+
$h2 = self::imul($h2 ^ $ch, 1597334677);
|
|
2056
|
+
}
|
|
2057
|
+
$h1 = self::imul($h1 ^ ($h1 >> 16), 2246822507) ^ self::imul($h2 ^ ($h2 >> 13), 3266489909);
|
|
2058
|
+
$h2 = self::imul($h2 ^ ($h2 >> 16), 2246822507) ^ self::imul($h1 ^ ($h1 >> 13), 3266489909);
|
|
2059
|
+
$unsigned_h1 = $h1 & 0xffffffff;
|
|
2060
|
+
return 4294967296 * (2097151 & $h2) + $unsigned_h1;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
public static function getVirtualizationAnomalyScore(RequestContext $context): float
|
|
2064
|
+
{
|
|
2065
|
+
$clientFp = $context->headers['x-device-fingerprint'] ?? null;
|
|
2066
|
+
if (!$clientFp) {
|
|
2067
|
+
return 0.0;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
$fpMap = [];
|
|
2071
|
+
foreach (explode('|', $clientFp) as $part) {
|
|
2072
|
+
$pair = explode(':', $part, 2);
|
|
2073
|
+
if (count($pair) === 2) {
|
|
2074
|
+
$fpMap[$pair[0]] = $pair[1];
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
$score = 0.0;
|
|
2079
|
+
$clientGpuHash = $fpMap['gpu'] ?? null;
|
|
2080
|
+
if ($clientGpuHash) {
|
|
2081
|
+
$virtualGpus = [
|
|
2082
|
+
"Google SwiftShader", "SwiftShader",
|
|
2083
|
+
"Mesa llvmpipe", "llvmpipe", "Mesa Gallium",
|
|
2084
|
+
"Microsoft Basic Render Driver", "HeadlessChrome",
|
|
2085
|
+
"Intel(R) HD Graphics"
|
|
2086
|
+
];
|
|
2087
|
+
$virtualGpuHashes = [];
|
|
2088
|
+
foreach ($virtualGpus as $gpu) {
|
|
2089
|
+
$virtualGpuHashes[] = (string)self::cyrb53($gpu);
|
|
2090
|
+
}
|
|
2091
|
+
if (in_array($clientGpuHash, $virtualGpuHashes, true)) {
|
|
2092
|
+
$score += 75.0;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
$clientScreenHash = $fpMap['scr'] ?? null;
|
|
2097
|
+
if ($clientScreenHash) {
|
|
2098
|
+
$headlessResolutions = ["800x600_24", "1024x768_24"];
|
|
2099
|
+
$headlessHashes = [];
|
|
2100
|
+
foreach ($headlessResolutions as $res) {
|
|
2101
|
+
$headlessHashes[] = (string)self::cyrb53($res);
|
|
2102
|
+
}
|
|
2103
|
+
if (in_array($clientScreenHash, $headlessHashes, true)) {
|
|
2104
|
+
$score += 25.0;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
return min(100.0, $score);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
/**
|
|
2112
|
+
* Calcule un score d'incohérence analogique et lisse plafonnant à 99.9 max.
|
|
2113
|
+
*/
|
|
2114
|
+
public static function calculateAnalogInconsistencyScore(float $consistencyScore, float $inflectionPoint = 0.72, float $steepness = 12.0): float
|
|
2115
|
+
{
|
|
2116
|
+
$s = max(0.0, min(1.0, $consistencyScore));
|
|
2117
|
+
if ($s >= 0.98) {
|
|
2118
|
+
return 0.0;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
$asymptote = 99.9;
|
|
2122
|
+
$raw = 1.0 / (1.0 + exp($steepness * ($s - $inflectionPoint)));
|
|
2123
|
+
$minVal = 1.0 / (1.0 + exp($steepness * (1.0 - $inflectionPoint)));
|
|
2124
|
+
$maxVal = 1.0 / (1.0 + exp($steepness * (0.0 - $inflectionPoint)));
|
|
2125
|
+
$normalized = (($raw - $minVal) / ($maxVal - $minVal)) * $asymptote;
|
|
2126
|
+
|
|
2127
|
+
return min($asymptote, round($normalized, 1));
|
|
2128
|
+
}
|
|
2034
2129
|
}
|
|
@@ -14,6 +14,150 @@ class TLSClientHelloParser
|
|
|
14
14
|
35466, 39578, 43690, 47802, 51914, 55926, 60038, 64150
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Lit un entier encodé en longueur variable QUIC (RFC 9000 section 16).
|
|
19
|
+
*/
|
|
20
|
+
public static function readVarInt(string $data, int &$offset): ?int
|
|
21
|
+
{
|
|
22
|
+
$len = strlen($data);
|
|
23
|
+
if ($offset >= $len) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
$first = ord($data[$offset]);
|
|
28
|
+
$prefix = $first >> 6;
|
|
29
|
+
$firstVal = $first & 0x3f;
|
|
30
|
+
|
|
31
|
+
if ($prefix === 0) {
|
|
32
|
+
$offset += 1;
|
|
33
|
+
return $firstVal;
|
|
34
|
+
} elseif ($prefix === 1) {
|
|
35
|
+
if ($offset + 2 > $len) return null;
|
|
36
|
+
$val = ($firstVal << 8) | ord($data[$offset + 1]);
|
|
37
|
+
$offset += 2;
|
|
38
|
+
return $val;
|
|
39
|
+
} elseif ($prefix === 2) {
|
|
40
|
+
if ($offset + 4 > $len) return null;
|
|
41
|
+
$val = ($firstVal << 24) | (ord($data[$offset + 1]) << 16) | (ord($data[$offset + 2]) << 8) | ord($data[$offset + 3]);
|
|
42
|
+
$offset += 4;
|
|
43
|
+
return $val;
|
|
44
|
+
} else {
|
|
45
|
+
if ($offset + 8 > $len) return null;
|
|
46
|
+
$val = $firstVal;
|
|
47
|
+
for ($i = 1; $i < 8; $i++) {
|
|
48
|
+
$val = ($val << 8) | ord($data[$offset + $i]);
|
|
49
|
+
}
|
|
50
|
+
$offset += 8;
|
|
51
|
+
return $val;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse les paramètres de transport QUIC (RFC 9000 section 18.2).
|
|
57
|
+
*/
|
|
58
|
+
public static function parseQuicTransportParameters(string $data): array
|
|
59
|
+
{
|
|
60
|
+
$params = [];
|
|
61
|
+
$offset = 0;
|
|
62
|
+
$len = strlen($data);
|
|
63
|
+
|
|
64
|
+
while ($offset < $len) {
|
|
65
|
+
$paramId = self::readVarInt($data, $offset);
|
|
66
|
+
if ($paramId === null) break;
|
|
67
|
+
$paramLen = self::readVarInt($data, $offset);
|
|
68
|
+
if ($paramLen === null || $offset + $paramLen > $len) break;
|
|
69
|
+
|
|
70
|
+
$paramValBytes = substr($data, $offset, $paramLen);
|
|
71
|
+
$offset += $paramLen;
|
|
72
|
+
|
|
73
|
+
if ($paramLen > 0 && in_array($paramId, [1, 3, 4, 5, 6, 7, 8, 9, 11, 14], true)) {
|
|
74
|
+
$valOffset = 0;
|
|
75
|
+
$numVal = self::readVarInt($paramValBytes, $valOffset);
|
|
76
|
+
$params[$paramId] = $numVal ?? $paramValBytes;
|
|
77
|
+
} else {
|
|
78
|
+
$params[$paramId] = bin2hex($paramValBytes);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return $params;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Analyse l'ordre des trames de contrôle QUIC / HTTP/3 (SETTINGS, MAX_STREAMS, PRIORITY).
|
|
87
|
+
*/
|
|
88
|
+
public static function parseQuicControlFrames(string $streamData): array
|
|
89
|
+
{
|
|
90
|
+
$frames = [];
|
|
91
|
+
$frameOrder = [];
|
|
92
|
+
$settings = [];
|
|
93
|
+
$offset = 0;
|
|
94
|
+
$len = strlen($streamData);
|
|
95
|
+
|
|
96
|
+
if ($len === 0) {
|
|
97
|
+
return ['frames' => [], 'frame_order' => '', 'settings' => []];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (ord($streamData[0]) === 0x00) {
|
|
101
|
+
$offset = 1; // Stream type Control Stream
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
while ($offset < $len) {
|
|
105
|
+
$frameType = self::readVarInt($streamData, $offset);
|
|
106
|
+
if ($frameType === null) break;
|
|
107
|
+
$frameLen = self::readVarInt($streamData, $offset);
|
|
108
|
+
if ($frameLen === null || $offset + $frameLen > $len) break;
|
|
109
|
+
|
|
110
|
+
$payload = substr($streamData, $offset, $frameLen);
|
|
111
|
+
$offset += $frameLen;
|
|
112
|
+
|
|
113
|
+
$abbr = match ($frameType) {
|
|
114
|
+
0x04 => 's', // SETTINGS
|
|
115
|
+
0x12, 0x02 => 'm', // MAX_STREAMS
|
|
116
|
+
0x0f, 0xaf, 0xf0700 => 'p', // PRIORITY_UPDATE
|
|
117
|
+
0x10, 0x0d => 'd', // MAX_DATA
|
|
118
|
+
0x07 => 'g', // GOAWAY
|
|
119
|
+
default => 'u'
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
$frames[] = ['type' => $frameType, 'length' => $frameLen];
|
|
123
|
+
$frameOrder[] = $abbr;
|
|
124
|
+
|
|
125
|
+
if ($frameType === 0x04) {
|
|
126
|
+
$sOffset = 0;
|
|
127
|
+
$sLen = strlen($payload);
|
|
128
|
+
while ($sOffset < $sLen) {
|
|
129
|
+
$sId = self::readVarInt($payload, $sOffset);
|
|
130
|
+
if ($sId === null) break;
|
|
131
|
+
$sVal = self::readVarInt($payload, $sOffset);
|
|
132
|
+
if ($sVal === null) break;
|
|
133
|
+
$settings[$sId] = $sVal;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return [
|
|
139
|
+
'frames' => $frames,
|
|
140
|
+
'frame_order' => implode(',', $frameOrder),
|
|
141
|
+
'settings' => $settings
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public static function formatQuicFingerprint(array $params, string $priority = '', string $frameOrder = ''): string
|
|
146
|
+
{
|
|
147
|
+
$paramParts = [];
|
|
148
|
+
foreach ($params as $k => $v) {
|
|
149
|
+
$paramParts[] = "{$k}={$v}";
|
|
150
|
+
}
|
|
151
|
+
$fp = "1;" . implode(',', $paramParts);
|
|
152
|
+
if ($priority !== '' || $frameOrder !== '') {
|
|
153
|
+
$fp .= ";{$priority}";
|
|
154
|
+
}
|
|
155
|
+
if ($frameOrder !== '') {
|
|
156
|
+
$fp .= ";{$frameOrder}";
|
|
157
|
+
}
|
|
158
|
+
return $fp;
|
|
159
|
+
}
|
|
160
|
+
|
|
17
161
|
/**
|
|
18
162
|
* Parse le Client Hello brut et retourne l'empreinte JA3 et une approximation JA4.
|
|
19
163
|
*
|
|
@@ -72,6 +216,7 @@ class TLSClientHelloParser
|
|
|
72
216
|
$supportedVersions = [];
|
|
73
217
|
$hasSni = false;
|
|
74
218
|
$alpnProtocol = '';
|
|
219
|
+
$quicParams = null;
|
|
75
220
|
|
|
76
221
|
$extLimit = $offset + $extensionsLen;
|
|
77
222
|
while ($offset < $extLimit && $offset + 4 <= $len) {
|
|
@@ -127,6 +272,11 @@ class TLSClientHelloParser
|
|
|
127
272
|
}
|
|
128
273
|
}
|
|
129
274
|
}
|
|
275
|
+
} elseif ($extType === 57 || $extType === 0xffa5) { // quic_transport_parameters (RFC 9001 / draft)
|
|
276
|
+
if ($extLen > 0 && $offset + $extLen <= $len) {
|
|
277
|
+
$quicData = substr($binary, $offset, $extLen);
|
|
278
|
+
$quicParams = self::parseQuicTransportParameters($quicData);
|
|
279
|
+
}
|
|
130
280
|
}
|
|
131
281
|
$offset += $extLen;
|
|
132
282
|
}
|
|
@@ -204,10 +354,17 @@ class TLSClientHelloParser
|
|
|
204
354
|
|
|
205
355
|
$ja4_hash = $ja4_a . "_" . $ja4_b . "_" . $ja4_c;
|
|
206
356
|
|
|
207
|
-
|
|
357
|
+
$result = [
|
|
208
358
|
'ja3_string' => $ja3String,
|
|
209
359
|
'ja3_hash' => md5($ja3String),
|
|
210
360
|
'ja4_raw' => $ja4_hash
|
|
211
361
|
];
|
|
362
|
+
|
|
363
|
+
if ($quicParams !== null) {
|
|
364
|
+
$result['quic_params'] = $quicParams;
|
|
365
|
+
$result['quic_fp'] = self::formatQuicFingerprint($quicParams);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return $result;
|
|
212
369
|
}
|
|
213
370
|
}
|