ProxynetProxynet

اثر انگشت TLS و ⁦JA3⁩ چیست و چگونه کار می‌کند؟

تاریخ انتشار:

19 دقیقه مطالعه

Acar Diveroli
نویسنده: Acar Diveroli
از فیلدهای ⁨ClientHello⁩ در بدنه دروازه خوانشگر چکیده ⁨JA3⁩ به پنل بالا می‌رود و کلاینت اسکریپتی رد نمی‌شود

اسکریپتی به زبان پایتون نوشته‌اید. از یک پروکسی بیرون می‌رود، آدرس IP در هر درخواست عوض می‌شود و در هدر User-Agent هم یک رشته به‌روز از کروم گذاشته‌اید. با این حال سایت همان نخستین درخواست را با 403 پاسخ می‌دهد. وقتی همان آدرس را با همان IP در یک مرورگر واقعی باز می‌کنید، صفحه می‌آید. تفاوت در بسته‌ای است که پیش از هر هدر HTTP حرکت می‌کند: پیام ClientHello که اتصال رمزگذاری‌شده را آغاز می‌کند. سایت از روی چیدمان همین پیام تشخیص می‌دهد که طرف مقابلش کروم است، پایتون است یا curl، بی‌آنکه حتی یک هدر را بخواند.

در این نوشته دست‌دادن TLS، فیلدهای داخل ClientHello، شیوه محاسبه رشته ⁦JA3⁩ از این فیلدها و تفاوت کار ⁦JA4⁩ را می‌بینید. سپس به پرسشی می‌رسیم که بیش از همه شنیده‌ایم: آیا پروکسی اثر انگشت TLS را عوض می‌کند؟ در پایان یک نمونه آزموده‌شده پایتون هست که رشته ⁦JA3⁩ کلاینت خودتان را به‌صورت محلی نشان می‌دهد.

دست‌دادن TLS چیست؟

TLS پروتکلی است که ترافیک میان مرورگر و سایت را رمزگذاری می‌کند؛ همان https:// نوار آدرس نشان می‌دهد که از آن استفاده می‌کنید. پیش از آغاز رمزگذاری، دو طرف باید بر سر نسخه، مجموعه رمز و کلید به توافق برسند. به این چانه‌زنی کوتاه دست‌دادن (handshake) می‌گویند. نسخه کنونی، یعنی ⁦TLS 1.3⁩، در ⁦RFC 8446⁩ تعریف شده است و دست‌دادن تقریباً چنین پیش می‌رود:

  1. کلاینت با TCP به سرور وصل می‌شود و پیام ClientHello می‌فرستد. در آن مجموعه‌های رمز پشتیبانی‌شده، افزونه‌ها و سهم کلید قرار دارد.
  2. سرور از فهرست یکی را برمی‌گزیند و با ServerHello پاسخ می‌دهد. از این نقطه هر دو طرف می‌توانند کلید مشترک را استخراج کنند.
  3. سرور گواهی خود و پیام Finished را که درستی دست‌دادن را ثابت می‌کند، هر دو رمزگذاری‌شده، می‌فرستد.
  4. کلاینت گواهی را بررسی می‌کند و پیام Finished خودش را می‌فرستد.
  5. نخستین درخواست HTTP (GET /، هدرها، User-Agent) تنها پس از این مرحله و از درون کانال رمزگذاری‌شده عبور می‌کند.

از نگاه اثر انگشت، گام نخست اهمیت دارد. ClientHello پیش از وجود هر کلید مشترکی فرستاده می‌شود، پس رمزگذاری‌نشده است؛ سرور، CDN جلوی آن و هر دستگاه شبکه در مسیر می‌تواند آن را همان‌گونه که هست بخواند، پیش از هر هدر، هر کوکی و هر خط جاوااسکریپت.

پیام ClientHello چه فیلدهایی دارد؟

ClientHello فهرستی است که کلاینت در آن می‌گوید «این‌ها را می‌توانم صحبت کنم». فیلدهایی که در اثر انگشت به کار می‌روند این‌ها هستند:

فیلدچه چیزی دارد؟چرا متمایزکننده است؟
legacy_versionفیلد نسخه قدیمی. کلاینت‌های ⁦TLS 1.3⁩ برای سازگاری هنوز مقدار ⁦TLS 1.2⁩ یعنی 771 را اینجا می‌نویسندبه‌تنهایی اطلاعات کمی دارد؛ نسخه واقعی در یک افزونه است
cipher_suitesمجموعه‌های رمز پشتیبانی‌شده، به‌ترتیب اولویتهم فهرست و هم ترتیب از کتابخانه‌ای به کتابخانه دیگر فرق می‌کند
extensionsشماره نوع افزونه‌ها: server_name (0)، supported_groups (10)، signature_algorithms (13)، ALPN (16)، supported_versions (43)، key_share (51) و بقیهاینکه چه افزونه‌هایی هستند و با چه ترتیبی، ویژه هر نرم‌افزار است
supported_groupsمنحنی‌ها و گروه‌های قابل استفاده در تبادل کلید (29 = x25519، 23 = secp256r1)گروه‌های تازه زودتر به مرورگرها می‌رسند
ec_point_formatsقالب‌های نقطه منحنی بیضویبرخی کتابخانه‌ها سه مقدار می‌فرستند و برخی تنها یکی
signature_algorithmsالگوریتم‌های امضای پذیرفته‌شده، به‌ترتیب⁦JA3⁩ از آن استفاده نمی‌کند، ⁦JA4⁩ می‌کند
ALPNپروتکل کاربردی که کلاینت می‌خواهد صحبت کند (h2، http/1.1)مرورگر h2 می‌خواهد؛ کلاینت‌های ساده بیشتر وقت‌ها هیچ نمی‌خواهند

پرکننده این فهرست‌ها برنامه شما نیست، بلکه کتابخانه TLS زیر آن است. کروم از BoringSSL و فایرفاکس از NSS استفاده می‌کند. ماژول ssl پایتون و Node.js روی OpenSSL بنا شده‌اند. curl همراه ویندوز از Schannel، یعنی پشته TLS خود سیستم‌عامل، بهره می‌برد. هر کتابخانه فهرست رمز پیش‌فرض، مجموعه افزونه و ترتیب متفاوتی دارد؛ هویت کلاینت این‌گونه به نخستین بسته دست‌دادن نشت می‌کند.

اثر انگشت TLS چیست؟

اثر انگشت TLS همین فهرست‌های ClientHello است که به رشته‌ای کوتاه و قابل مقایسه فشرده شده‌اند. سه ویژگی آن را از دیگر سیگنال‌ها جدا می‌کند:

  • منفعل است. سایت چیزی را در کلاینت اجرا نمی‌کند، فقط نخستین بسته رسیده را می‌خواند. خاموش کردن جاوااسکریپت یا پاک کردن کوکی‌ها در نتیجه اثری ندارد.
  • از هدرها مستقل است. User-Agent یک خط متن است و در هر کتابخانه HTTP با یک خط کد عوض می‌شود. ClientHello از رفتار کامپایل‌شده کتابخانه می‌آید.
  • نرم‌افزار را می‌شناساند، نه شخص را. هر کسی که نسخه یکسانی از کروم را روی سیستم‌عامل یکسانی اجرا کند همان مقدار را تولید می‌کند. لایه جاوااسکریپت (canvas، فونت، WebGL) که هدفش جدا کردن تک‌تک دستگاه‌هاست را در اثر انگشت مرورگر چیست؟ شرح داده‌ایم.

این روش برای تشخیص بات ابداع نشد. نخستین کاربردش امنیت شبکه بود: بدافزار ترافیکش را رمز می‌کند، اما کتابخانه TLS و تنظیماتش بیشتر وقت‌ها ثابت است. تیم امنیتی که محتوا را نمی‌بیند، همان خانواده نرم‌افزاری را از چیدمان ClientHello آن بازمی‌شناسد.

⁦JA3⁩ چیست و چگونه محاسبه می‌شود؟

⁦JA3⁩ نخستین روش فراگیری است که این ایده را به قالبی استاندارد بست. در سال 2017 در Salesforce به دست John Althouse، Jeff Atkinson و Josh Atkins ساخته و به‌صورت متن‌باز منتشر شد. آن‌گونه که در مخزن ⁦JA3⁩ در Salesforce توضیح داده شده، محاسبه پنج گام دارد:

  1. از پیام ClientHello پنج فیلد برداشته می‌شود: نسخه TLS، مجموعه‌های رمز، افزونه‌ها، منحنی‌ها (supported_groups) و قالب‌های نقطه منحنی بیضوی.
  2. مقدارهای هر فیلد به عدد ده‌دهی تبدیل و به‌ترتیب حضورشان در پیام با - به هم چسبانده می‌شوند.
  3. پنج فیلد با , به هم می‌پیوندند. اگر فیلدی خالی باشد، جایش خالی می‌ماند.
  4. مقدارهای GREASE (پایین‌تر توضیح می‌دهیم) اصلاً وارد فهرست‌ها نمی‌شوند.
  5. هش MD5 رشته حاصل گرفته می‌شود. این هش 32 نویسه‌ای همان اثر انگشت ⁦JA3⁩ است.

نمونه خود مخزن چنین است:

text
769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0
→ ada70206e40642a3e4461f35503241d5

از چپ که بخوانیم: 769 یعنی ⁦TLS 1.0⁩؛ سپس دوازده مجموعه رمز، سه افزونه، سه منحنی و یک قالب نقطه می‌آید. MD5 اینجا برای امنیت نیست، برای تبدیل رشته‌ای بلند به کلیدی جستجوپذیر با طول ثابت است.

دو نکته: چون کلاینت‌های ⁦TLS 1.3⁩ مقدار ⁦TLS 1.2⁩ را در فیلد نسخه می‌نویسند، تقریباً همه رشته‌های ⁦JA3⁩ امروزی با 771 آغاز می‌شوند. Salesforce مخزن را در 1 مه 2025 بایگانی کرد؛ ابزارهایی مانند Wireshark همچنان این مقدار را محاسبه می‌کنند، اما روش دیگر نگهداری نمی‌شود.

GREASE چیست و چرا ⁦JA3⁩ آن را نادیده می‌گیرد؟

GREASE سازوکاری برای استحکام است که در ⁦RFC 8701⁩ تعریف شده. کلاینت به فهرست مجموعه‌های رمز، افزونه‌ها و گروه‌های خود چند مقدار رزروشده تصادفی با الگوی 0x0A0A، 0x1A1A0xFAFA می‌افزاید. این مقدارها هیچ معنایی ندارند. هدف آن است که پیوسته آزموده شود آیا سرورها مقدارهای ناشناخته را بی‌صدا نادیده می‌گیرند یا نه: سروری معیوب که با دیدن مقدار ناشناخته اتصال را قطع می‌کند، امروز شناسایی می‌شود، نه روزی که قابلیت تازه‌ای در TLS منتشر شود.

چون این مقدارها تصادفی انتخاب می‌شوند، اگر ⁦JA3⁩ آن‌ها را به حساب می‌آورد همان مرورگر در هر اتصال هش دیگری می‌ساخت. به همین دلیل مستندات روش می‌خواهد مقدارهای GREASE کنار گذاشته شوند؛ در کد پایین این کار را تابع is_grease انجام می‌دهد.

چرا کروم ترتیب افزونه‌ها را به هم می‌ریزد؟

GREASE محتوای فهرست‌ها را نگه می‌دارد، نه ترتیبشان را. تکیه نرم‌افزارهای سرور و دستگاه‌های میانی بر ترتیب ثابت افزونه‌های کروم همان خطر را داشت، پس تیم کروم ترتیب را هم تصادفی کرد. بنا بر رکورد موجود در Chrome Platform Status، قابلیت «TLS ClientHello extension permutation» در ⁦Chrome 110⁩ به‌صورت پیش‌فرض روشن شد. دلیل ثبت‌شده فرار از اثر انگشت نیست، جلوگیری از شکنندگی اکوسیستم است: ترتیب ثابت، توسعه‌دهندگان سرور را به شناختن کروم و فرض کردن رفتاری مشخص می‌راند و تغییرهای آینده TLS را دشوار می‌کند. ⁦RFC 8446⁩ از پیش می‌گوید افزونه‌ها می‌توانند به هر ترتیبی بیایند؛ تنها استثنا pre_shared_key است که در صورت حضور باید آخر بایستد.

چون ⁦JA3⁩ افزونه‌ها را به‌ترتیب پیام به هم می‌چسباند، این تغییر هش ⁦JA3⁩ مرورگرهای مبتنی بر Chromium را ناپایدار کرد. با شنونده پایین اندازه گرفتیم: 24 اتصال پیاپی یک مرورگر مبتنی بر Chromium به 24 هش ⁦JA3⁩ متفاوت رسید. وقتی شماره افزونه‌ها را مرتب کردیم، در همه یک مجموعه بود؛ تنها چیزی که عوض می‌شد ترتیب بود.

⁦JA4⁩ چیست و با ⁦JA3⁩ چه فرقی دارد؟

⁦JA4⁩ قالب تازه‌تری است که FoxIO منتشر کرده و به این مشکل پاسخ می‌دهد. بنا بر سند فنی ⁦JA4⁩ اثر انگشت از سه بخش به شکل a_b_c ساخته می‌شود. نمونه داخل سند، یعنی t13d1516h2_8daaf6152771_e5627efa2ab1، چنین خوانده می‌شود:

  • t: TLS روی TCP (q یعنی QUIC و d یعنی DTLS).
  • 13: ⁦TLS 1.3⁩. ⁦JA4⁩ نسخه را از افزونه supported_versions می‌خواند، نه از فیلد قدیمی.
  • d: افزونه SNI هست، یعنی کلاینت به یک نام دامنه وصل می‌شود (i نبودن SNI را نشان می‌دهد).
  • 15 و 16: 15 مجموعه رمز و 16 افزونه، بدون شمردن GREASE.
  • h2: نویسه نخست و آخر نخستین مقدار فهرست ALPN، یعنی HTTP/2.
  • 8daaf6152771: کدهای شانزده‌شانزدهی مجموعه‌های رمز مرتب می‌شوند، با ⁦SHA-256⁩ هش می‌شوند و 12 نویسه نخست نگه داشته می‌شود.
  • e5627efa2ab1: کدهای افزونه مرتب می‌شوند (بدون SNI و ALPN)، الگوریتم‌های امضا با ترتیب اصلی به آن افزوده و نتیجه به همان شکل هش می‌شود.

مرتب‌سازی اثر به‌هم‌ریختن افزونه‌ها را از میان می‌برد. بخش نخست خوانا هم اجازه می‌دهد بدون نگاه به هش‌ها تفکیکی درشت انجام دهید: دو بخش نخست که یکی به h2 و دیگری به‌دلیل نفرستادن ALPN به 00 ختم می‌شود، از یک نرم‌افزار نیامده‌اند.

⁦JA3⁩⁦JA4⁩
منتشرکنندهSalesforce (2017)، مخزن در 2025 بایگانی شدFoxIO، توسعه ادامه دارد
قالبیک هش MD5 با 32 نویسهa_b_c: پیشوند خوانا + دو هش کوتاه‌شده ⁦SHA-256⁩
ترتیب افزونه‌هاترتیب داخل پیاممرتب‌شده؛ از به‌هم‌ریختن اثر نمی‌گیرد
نسخه TLSفیلد نسخه قدیمی (771 حتی در ⁦TLS 1.3⁩)افزونه supported_versions
ALPN و SNIفقط به‌شکل شماره افزونه دیده می‌شوندهر دو در بخش نخست نوشته می‌شوند
الگوریتم‌های امضااستفاده نمی‌شودوارد بخش سوم می‌شود
GREASEنادیده گرفته می‌شودنادیده گرفته می‌شود

بنا بر یادداشت مجوز در مخزن FoxIO، ⁦JA4⁩ که اثر انگشت کلاینت TLS است با مجوز ⁦BSD 3-Clause⁩ باز است؛ دیگر اعضای خانواده (JA4S، JA4H، JA4X، JA4T و ادامه آن) به مجوز جداگانه FoxIO وابسته‌اند.

آیا پروکسی اثر انگشت TLS را عوض می‌کند؟

نه. دلیلش در شیوه حمل ترافیک HTTPS به دست پروکسی است.

در پروکسی HTTP، کلاینت نخست درخواست CONNECT example.com:443 را به پروکسی می‌فرستد. پروکسی یک اتصال TCP به مقصد باز می‌کند، 200 Connection Established می‌گوید و از آن پس تنها بایت جابه‌جا می‌کند. ClientHello از درون این تونل می‌گذرد: کلاینت شما آن را می‌سازد، سایت مقصد آن را می‌خواند و پروکسی نه محتوایش را عوض می‌کند و نه دوباره می‌نویسد. در SOCKS5 هم وضع همین است؛ پروتکل اتصال TCP را یک لایه پایین‌تر جابه‌جا می‌کند و اصلاً نمی‌داند داده‌ای که حمل می‌کند TLS است. جریان گام‌به‌گام هر دو پروتکل را در تفاوت پروکسی SOCKS و HTTP شرح داده‌ایم.

نتیجه این است: سایت هم‌زمان آدرس IP پروکسی و اثر انگشت TLS کلاینت شما را می‌بیند. استفاده از پروکسی HTTPS یا پروکسی SOCKS5 این را عوض نمی‌کند؛ رزیدنشیال، موبایل یا دیتاسنتر بودن پروکسی هم فرقی نمی‌سازد. با یک پروکسی آزمایشی محلی امتحان کردیم: همان کلاینت curl به شنونده پایین یک بار مستقیم، یک بار با تونل CONNECT در HTTP و یک بار روی SOCKS5 وصل شد و شنونده هر سه بار همان رشته ⁦JA3⁩ را ثبت کرد.

تنها حالتی که اثر انگشت عوض می‌شود، وجود واسطه‌ای در میانه است که TLS را پایان می‌دهد:

چه چیزی در میانه استچه کسی TLS را با مقصد برقرار می‌کند؟سایت کدام اثر انگشت را می‌بیند؟
پروکسی HTTP (تونل CONNECT)کلاینت شمااثر انگشت کلاینت شما
پروکسی SOCKS5کلاینت شمااثر انگشت کلاینت شما
VPNکلاینت شمااثر انگشت کلاینت شما
دروازه سازمانی با بازرسی TLSدروازهاثر انگشت دروازه
آنتی‌ویروس با پویش HTTPS روشنآنتی‌ویروساثر انگشت آنتی‌ویروس
سرویسی که صفحه را به‌جای شما می‌گیردکلاینت خود سرویساثر انگشت کلاینت سرویس

سطر پنجم را هنگام آماده کردن همین نوشته روی دستگاه خودمان دیدیم: curl در ویندوز وقتی مستقیم به سرویس بازتاب وصل می‌شد یک اثر انگشت و وقتی از تونل پروکسی محلی می‌گذشت اثر انگشت دیگری برمی‌گرداند. علت پویش HTTPS آنتی‌ویروس بود که اتصال مستقیم را با پشته TLS خودش دوباره برقرار می‌کرد. اگر مقدار سرویس بازتاب به کتابخانه‌ای که انتظار داشتید شبیه نیست، ببینید گواهی را چه کسی امضا کرده است.

رشته ⁦JA3⁩ خودتان را چگونه ببینید؟

کوتاه‌ترین راه یک سرویس بازتاب است: صفحه TLS در BrowserLeaks را در مرورگر باز کنید تا مقدارهای ⁦JA3⁩ و ⁦JA4⁩ خود را ببینید. اگر می‌خواهید در سطح بسته نگاه کنید، Wireshark این مقدارها را روی بسته‌های ClientHello خودش محاسبه می‌کند؛ در مرجع فیلترهای نمایش این فیلدها با نام‌های tls.handshake.ja3 و tls.handshake.ja4 فهرست شده‌اند.

برای دیدن خود محاسبه می‌توانید از اسکریپت پایین استفاده کنید. تنها با کتابخانه استاندارد پایتون کار می‌کند و چیزی به بیرون نمی‌فرستد: روی پورت 8443 یک شنونده خام TCP باز می‌کند، پیام ClientHello هر کلاینت متصل را تجزیه می‌کند و رشته ⁦JA3⁩ و هش آن را چاپ می‌کند. هنگام شروع، کلاینت urllib خود پایتون را یک بار وصل می‌کند. شنونده دست‌دادن را کامل نمی‌کند، نخستین بسته را می‌خواند و می‌بندد؛ خطای اتصال در سمت کلاینت وضعیتی مورد انتظار است.

python
import hashlib
import socket
import threading
import urllib.request

HOST, PORT = "127.0.0.1", 8443


def is_grease(value):
    # RFC 8701: 0x0a0a, 0x1a1a, ... 0xfafa
    return (value & 0x0F0F) == 0x0A0A and (value >> 8) == (value & 0xFF)


def read_client_hello(conn):
    data = b""
    while len(data) < 5:
        data += conn.recv(4096)
    if data[0] != 22:  # 22 = handshake record
        raise ValueError("not a TLS handshake")
    record_length = int.from_bytes(data[3:5], "big")
    while len(data) < 5 + record_length:
        chunk = conn.recv(4096)
        if not chunk:
            break
        data += chunk
    return data[5 : 5 + record_length]


def ja3_from_client_hello(hello):
    if hello[0] != 1:  # 1 = ClientHello
        raise ValueError("not a ClientHello")
    pos = 4  # message type (1) + length (3)
    version = int.from_bytes(hello[pos : pos + 2], "big")
    pos += 2 + 32  # version + random
    pos += 1 + hello[pos]  # session_id
    size = int.from_bytes(hello[pos : pos + 2], "big")
    pos += 2
    ciphers = [int.from_bytes(hello[i : i + 2], "big") for i in range(pos, pos + size, 2)]
    pos += size
    pos += 1 + hello[pos]  # compression_methods
    end = pos + 2 + int.from_bytes(hello[pos : pos + 2], "big")
    pos += 2

    extensions, groups, point_formats = [], [], []
    while pos < end:
        ext_type = int.from_bytes(hello[pos : pos + 2], "big")
        ext_size = int.from_bytes(hello[pos + 2 : pos + 4], "big")
        body = hello[pos + 4 : pos + 4 + ext_size]
        pos += 4 + ext_size
        extensions.append(ext_type)
        if ext_type == 10:  # supported_groups
            groups = [int.from_bytes(body[i : i + 2], "big") for i in range(2, len(body), 2)]
        elif ext_type == 11:  # ec_point_formats
            point_formats = list(body[1:])

    def join(values):
        return "-".join(str(v) for v in values if not is_grease(v))

    fields = [str(version), join(ciphers), join(extensions), join(groups), join(point_formats)]
    ja3_text = ",".join(fields)
    return ja3_text, hashlib.md5(ja3_text.encode()).hexdigest()


def own_python_client():
    try:
        urllib.request.urlopen(f"https://localhost:{PORT}", timeout=5)
    except OSError:
        pass  # the listener closes without replying, which is expected


def main():
    server = socket.create_server((HOST, PORT))
    print(f"listening on https://localhost:{PORT}, press Ctrl+C to quit")
    threading.Thread(target=own_python_client, daemon=True).start()
    while True:
        conn, _ = server.accept()
        with conn:
            try:
                ja3_text, ja3_hash = ja3_from_client_hello(read_client_hello(conn))
            except (ValueError, IndexError, OSError) as exc:
                print("could not read:", exc)
                continue
        print(ja3_hash, ja3_text)


if __name__ == "__main__":
    main()

هنگام اجرای اسکریپت، از ترمینالی دیگر کلاینت‌های گوناگون را به همان آدرس بفرستید. در آدرس localhost بنویسید؛ کلاینتی که به آدرس IP وصل شود افزونه SNI را نمی‌فرستد و اثر انگشت عوض می‌شود.

bash
curl -k https://localhost:8443
node -e "require('https').get('https://localhost:8443', { rejectUnauthorized: false }).on('error', () => {})"

روی دستگاه ما (⁦Windows 11⁩، ⁦Python 3.13⁩ با ⁦OpenSSL 3.0⁩، ⁦Node.js 24⁩ و ⁦curl 8⁩ کامپایل‌شده با Schannel) خروجی چنین بود. برای خوانایی، فیلد مجموعه‌های رمز را کوتاه کردیم:

text
331a436afb23d4e31134c11b301bdcb5 771,4866-4867-4865-…,0-11-10-35-16-22-23-49-13-43-45-51-21,29-23-30-25-24-256-257-258-259-260,0-1-2
2e6c64f66822fc35b6a7a128b557f1de 771,4866-4865-49196-…,0-43-13-35-10-11-16-51-49-23-65281-45,29-23-24,0
944d1e1858cd278718f8a46b65d3212f 771,4866-4867-4865-…,65281-0-11-10-35-22-23-13-43-45-51,4588-29-23-30-24-25-256-257,0-1-2

یک دستگاه، یک IP و سه هویت جدا: به‌ترتیب پایتون، curl و Node.js. سرویس بازتاب برای پایتون دقیقاً همان هش را برگرداند، یعنی تجزیه‌گر درست کار می‌کند. عدد 4588 در سطر Node.js همان گروه ترکیبی پساکوانتومی تبادل کلید است که در ثبت‌های IANA با نام X25519MLKEM768 می‌آید؛ این نسخه پایتون آن را نمی‌فرستد. در همان Node.js، https.get و fetch درون‌ساخت هم مقدار ⁦JA3⁩ متفاوتی دادند، چون fetch افزونه ALPN را می‌افزاید: اثر انگشت به کتابخانه HTTP بستگی دارد، نه به زبان. تفاوت کتابخانه‌ها در سمت پایتون را در مقایسه HTTPX، Requests و AIOHTTP سنجیده‌ایم.

برای سنجش اثر پروکسی، شنونده به کار نمی‌آید (پروکسی دوردست به localhost شما نمی‌رسد)، پس دو بار از سرویس بازتاب بپرسید:

python
import json
import urllib.request

PROXY = "http://user:pass@pr.proxynet.io:8000"
URL = "https://tls.browserleaks.com/json"


def fingerprint(opener):
    with opener.open(URL, timeout=15) as response:
        result = json.load(response)
    return result["ja3_hash"], result["ja4"]


direct = urllib.request.build_opener(urllib.request.ProxyHandler({}))
proxied = urllib.request.build_opener(urllib.request.ProxyHandler({"https": PROXY}))

print("direct :", *fingerprint(direct))
print("proxied:", *fingerprint(proxied))

در هر دو سطر مقدارهای یکسانی می‌بینید؛ تنها چیزی که عوض می‌شود آدرس IP است که سرویس می‌بیند.

صاحب سایت این داده را چگونه به کار می‌برد؟

محصولات CDN و فایروال مقدار ⁦JA3⁩ یا ⁦JA4⁩ را در کنار هر درخواست به‌صورت یک فیلد ارائه می‌کنند. صاحب سایت این فیلد را هنگام نوشتن قاعده و بررسی گزارش‌ها به کار می‌برد. کاربردهای رایج:

  • بررسی سازگاری. اگر User-Agent می‌گوید «کروم» اما اثر انگشت با هیچ نسخه شناخته‌شده کروم جور نیست، هدر دستکاری شده است. به‌تنهایی دلیل قطعی نیست، ولی نشانه‌ای نیرومند است که بر امتیاز اثر می‌گذارد. خود این هدر را در User-Agent چیست؟ شرح داده‌ایم.
  • محدودیت نرخ مستقل از IP. اگر ترافیکی پخش‌شده روی صدها IP همان اثر انگشت را داشته باشد، شمارنده به‌جای IP به اثر انگشت بسته می‌شود. IP چرخشی این شمارنده را صفر نمی‌کند.
  • شناختن ابزارهای شناخته‌شده. برای خانواده‌های بدافزار و ابزارهای پویش، فهرست‌های گردآوری‌شده اثر انگشت وجود دارد؛ تیم‌های امنیتی آن‌ها را با گزارش‌ها تطبیق می‌دهند.

محدودیت هم دارد. سایتی که مقدار یک مرورگر به‌روز را مسدود کند، همه بازدیدکنندگان آن مرورگر را مسدود کرده است. به‌روزرسانی مرورگر مقدار را عوض می‌کند و دروازه‌های سازمانی و آنتی‌ویروس‌ها اثر انگشت خودشان را در میانه می‌گذارند. به همین دلیل اثر انگشت TLS به‌تنهایی تصمیم نمی‌سازد؛ یکی از سیگنال‌هایی است که همراه اعتبار IP و رفتار وارد امتیاز می‌شود. کل این امتیازدهی را در تشخیص بات چگونه کار می‌کند؟ و لایه‌های Cloudflare را در Cloudflare Precursor چیست؟ بررسی کرده‌ایم.

برای توسعه‌دهنده‌ای که داده جمع می‌کند چه معنایی دارد؟

به وضعیت آغاز نوشته برگردیم. کلاینت پایتون در سطر User-Agent می‌گوید کروم است و ClientHello آن می‌گوید OpenSSL است. سایت این تناقض را در نخستین بسته می‌بیند. واکنش درست پنهان کردن تناقض نیست، برداشتن آن است:

  • ادعا نکنید مرورگری هستید که نیستید. مقدار User-Agent اسکریپت شما باید خود اسکریپت را معرفی کند: یک نام، یک نسخه و یک نشانی تماس. کلاینتی که با هویت صادقانه می‌آید، به robots.txt پایبند است و آرام کار می‌کند را صاحب سایت می‌شناسد و می‌تواند در فهرست مجاز بگذارد. قاعده‌ها را در فایل robots.txt چیست و چگونه آن را بخوانیم؟ شرح داده‌ایم.
  • اگر API رسمی هست، از آن استفاده کنید. در درخواستی که با کلید API می‌آید هویت شما از پیش روشن است؛ بحث اثر انگشت برداشته می‌شود.
  • اگر صفحه واقعاً مرورگر می‌خواهد، مرورگر واقعی به کار ببرید. وقتی صفحه‌ای را که با جاوااسکریپت ترسیم می‌شود با Playwright در یک Chromium واقعی باز می‌کنید، کلاینت شما واقعاً همان مرورگر است؛ هدرها، لایه TLS و محیط جاوااسکریپت با هم سازگارند. نصب آن در Playwright چیست و چگونه با پروکسی استفاده می‌شود؟ آمده است.
  • سرعت را پایین بیاورید و اجازه بگیرید. برای کاری منظم و پرحجم، نوشتن به صاحب سایت بیشتر وقت‌ها ماندگارترین راه‌حل است.

دیگر دلیل‌های مسدود شدن و راه‌های مشروع آن را در وب اسکرپینگ بدون مسدود شدن گرد آورده‌ایم.

کاربردها

  • تشخیص 403 در اسکرپر: اگر با همان IP مرورگر رد می‌شود و اسکریپت نمی‌شود، تفاوت به احتمال زیاد در هویت کلاینت است. تفکیک کدهای وضعیت در کدهای وضعیت HTTP در وب اسکرپینگ و طرح کلی در صفحه راهکار استخراج داده ماست.
  • جدا کردن ترافیک بات در سایت خودتان: گروه‌بندی گزارش‌ها بر پایه اثر انگشت، ترافیک پخش‌شده‌ای را نشان می‌دهد که گروه‌بندی بر پایه IP از دست می‌دهد.
  • معرفی خزنده (crawler) خودتان: نسخه ثابت کتابخانه اثر انگشتی پایدار می‌سازد و خزنده شما در گزارش‌ها این‌گونه بازشناخته می‌شود. سمت زیرساخت در صفحه راهکار وب کراولر ماست.
  • درست کردن انتظار از پروکسی: پروکسی مسکونی اعتبار IP و موقعیت را عوض می‌کند؛ هویت کلاینت شما بر عهده خودتان می‌ماند.

خطاهای رایج

  • گمان اینکه با عوض کردن User-Agent کلاینت عوض می‌شود. هدر یک متن است؛ ClientHello رفتار کتابخانه است و پیش از هدر می‌رود.
  • انتظار عوض شدن اثر انگشت TLS از پروکسی. پروکسی تونل‌ساز IP را عوض می‌کند و به دست‌دادن کاری ندارد.
  • انتظار ثابت ماندن هش ⁦JA3⁩ در مرورگر مبتنی بر Chromium. ترتیب افزونه‌ها در هر اتصال عوض می‌شود؛ برای مقایسه از ⁦JA4⁩ یا فهرست افزونه مرتب‌شده استفاده کنید.
  • پذیرفتن نتیجه سرویس بازتاب بدون پرسش. آنتی‌ویروس یا دروازه سازمانی که HTTPS را می‌پوید، اثر انگشت خودش را به سرویس نشان می‌دهد.
  • استفاده از 127.0.0.1 در اندازه‌گیری. SNI فرستاده نمی‌شود و مقداری متفاوت با اتصال‌های واقعی به دست می‌آید.

راهنمای انتخاب

وضعیتپیشنهاد
با پروکسی IP عوض می‌شود ولی در نخستین درخواست 403 می‌گیریدبه هویت کلاینت نگاه کنید: User-Agent و کتابخانه‌ای که به کار می‌برید یک چیز می‌گویند؟
می‌خواهید مقدار ⁦JA3⁩ یا ⁦JA4⁩ کلاینت خودتان را بدانیدسرویس بازتاب یا همان شنونده محلی بالا
می‌خواهید ترافیک کروم را در گزارش‌ها گروه‌بندی کنید⁦JA4⁩ به‌جای ⁦JA3⁩؛ از به‌هم‌ریختن افزونه اثر نمی‌گیرد
صفحه جاوااسکریپت و مرورگر واقعی می‌خواهدخودکارسازی با مرورگر واقعی (Playwright)، سرعت معقول و دامنه‌ای که سایت اجازه می‌دهد
سرویس بازتاب مقداری غیرمنتظره نشان می‌دهدببینید گواهی را چه کسی امضا کرده؛ ممکن است نرم‌افزاری در میانه TLS را پایان دهد
سایت شما ترافیک بات پخش‌شده داردمحدودیت نرخ را به‌جای IP به جفت IP + اثر انگشت ببندید و بر پایه یک هش مسدود نکنید

پرسش‌های متداول

تفاوت ⁦JA3⁩ و ⁦JA3S⁩ چیست؟

⁦JA3⁩ از پیام ClientHello کلاینت و ⁦JA3S⁩ از پیام ServerHello سرور ساخته می‌شود. چون سرور به کلاینت‌های گوناگون پاسخ‌های گوناگون می‌دهد، ⁦JA3S⁩ به‌تنهایی سرور را نمی‌شناساند؛ اما پاسخ آن به یک کلاینت همیشه یکسان است. تیم‌های امنیتی به همین دلیل این دو را جفتی به کار می‌برند.

آیا استفاده از VPN اثر انگشت TLS را عوض می‌کند؟

نه. VPN ترافیک را از تونلی رمزگذاری‌شده می‌گذراند، اما دست‌دادن TLS با سایت را باز هم مرورگر یا اسکریپت شما انجام می‌دهد. سایت آدرس IP سرور VPN و اثر انگشت کلاینت شما را می‌بیند. تفاوت این دو ابزار را در تفاوت پروکسی و VPN شرح داده‌ایم.

آیا اثر انگشت TLS مرا شخصاً می‌شناساند؟

به‌تنهایی نه. هر کسی که نسخه یکسانی از مرورگر را روی سیستم‌عامل یکسانی اجرا کند همان مقدار را می‌سازد. هویتی که به شخص نزدیک می‌شود از ترکیب این مقدار با آدرس IP، کوکی‌ها و سیگنال‌های لایه جاوااسکریپت پدید می‌آید.

آیا به‌روزرسانی مرورگر اثر انگشت را عوض می‌کند؟

می‌تواند عوض کند. وقتی نسخه تازه یک مجموعه رمز را بردارد یا افزونه‌ای تازه بیفزاید، فهرست و هش عوض می‌شود. فهرست‌های اثر انگشت به همین دلیل نسخه‌به‌نسخه نگه داشته می‌شوند.

آیا پنجره ناشناس یا پاک کردن کوکی‌ها بر اثر انگشت TLS اثر دارد؟

نه. پیام ClientHello را کتابخانه TLS مرورگر می‌سازد؛ تاریخچه، کوکی‌ها و نوع پنجره وارد این فهرست‌ها نمی‌شوند.

اگر به‌خاطر اثر انگشتم مسدود می‌شوم چه کنم؟

نخست اندازه بگیرید: ببینید در سرویس بازتاب چه مقداری دیده می‌شود و آیا نرم‌افزاری در میانه TLS را پایان می‌دهد. اگر اسکریپت می‌نویسید، هویت خود را صادقانه اعلام کنید، سرعت را پایین بیاورید و از API رسمی یا کانال اجازه سایت استفاده کنید. اگر با مرورگر معمولی مسدود می‌شوید، مشکل به احتمال زیاد در اعتبار IP است، نه در اثر انگشت.

خلاصه

اثر انگشت TLS از ClientHello بیرون می‌آید، یعنی نخستین بسته رمزگذاری‌نشده یک اتصال رمزگذاری‌شده. ⁦JA3⁩ پنج فهرست این پیام را به‌ترتیب به هم می‌چسباند و هش MD5 را می‌گیرد؛ وقتی کروم ترتیب افزونه‌ها را به هم ریخت این هش ناپایدار شد و ⁦JA4⁩ با مرتب کردن فهرست‌ها آن را درست کرد. این مقدار نرم‌افزار را نشان می‌دهد نه شخص را، پیش از هدرها می‌رود و هیچ پروکسی تونل‌سازی به آن دست نمی‌زند: پروکسی IP را عوض می‌کند و هویت کلاینت نزد شما می‌ماند. برای افزودن نوع درست IP در کنار هویت کلاینت پایدار و صادقانه می‌توانید خدمات پروکسی ما را ببینید.

پرسش از ChatGPTپرسش از Claude