وقتی مهاجم همهٔ کد سمت کلاینتت رو خونده
خلاصهٔ کاملتر
نویسنده از یه فرض شروع میکنه که شکستخورده بهنظر میرسه ولی نیست: مهاجم همهٔ خطوط کد سمت کلاینت رو خونده. چون کد تشخیص بات مجبوره توی صفحه و روی ماشین مهاجم اجرا بشه (سیگنالهایی مثل حرکت موس، جیتر زمانبندی و ریزهکاریهای رندر فقط اونجان)، مهاجم CPU، دیباگر و زمان رو در اختیار داره. پس obfuscation یه قفل نیست، فقط یه هزینهست: هدف اینه که خوندن کد تا حد ممکن بیارزش بشه.
اصل اول، هدف متحرک بودنه. هر build داخلش رو دوباره میچینه؛ نگاشت بین معنی یه عملیات و نحوهٔ کدگذاریش روی سیم هر بار از صفر تولید میشه، پس امضایی که از یه نسخه درمیآری به نسخهٔ بعدی نمیخوره. حتی عمیقتر: بخشی از ماشین حساس per-session تصادفی میشه، پس چیزی که تو دیباگرته مخصوص یه بازدید یه نفره. نویسنده تاکید میکنه این تغییرها باید رفتار رو حفظ کنن، وگرنه obfuscationای که بیسروصدا پروتکل رو خراب کنه از هیچی بدتره؛ برای همین با تستوکتور و fuzz روی یه نسخهٔ ساده اعتبارسنجی میشه.
اصل دوم، شرط نذار، مهروموم کنه. روش طبیعی نوشتن یه چک اینه که با یه if تصمیم بگیری:
if (debuggerDetected) {
zero_all_memory();
}ولی این یکی از patchپذیرترین خطاییه که میشه نوشت؛ مهاجم لازم نیست بفهمه چرا فایر شده، فقط اون jump رو پیدا میکنه و یه بایت رو فلیپ میکنه. به گفتهٔ نویسنده، تو بخشهای مهم بهجای شاخه با رمزنگاری تصمیم میگیرن: سیگنال رو تو مادهٔ کلیدیای میبافن که مرحلهٔ بعدی اجرا رو رمزگشایی میکنه. اگه همهچی درست باشه ریاضی جواب میده و برنامه ادامه پیدا میکنه؛ اگه دستکاری شده باشه کلید مشتقشده غلط میشه و خروجی آشغال درمیآد، بدون هیچ ifی برای فلیپ کردن.
همین ایده روی یه دنباله از مرحلهها هم گسترش پیدا میکنه: هر مرحله هویت و نتیجهش رو تو یه accumulator یکطرفه میبافه و هر کلید بعدی از اون مشتق میشه. اگه مرحلهها رو بیترتیب اجرا کنی، یکی رو رد کنی یا replay بچسبونی، accumulator غلط میشه و رمزگشایی شکست میخوره؛ ترتیب خودش بخشی از راز میشه. نویسنده هشدار میده که نباید سیگنالهایی که بین دستگاههای سالم طبیعتاً فرق دارن رو تو این محاسبه بریزی، وگرنه کاربرای واقعی روی سختافزار کندتر قفل میشن.
اصل سوم، راز رو اصلاً نفرسته. باارزشترین رشتهها و منطق اصلاً تو bundle نیستن؛ بعضی lookup tableها و بخشهایی از منطق اجرایی per-session سمت سرور موقع handshake ساخته میشن، همون لحظه استفاده و بعد دور انداخته میشن. مهاجمی که bundle رو جدا اجرا میکنه نه اون table رو داره نه کلیدش رو. اسمای APIهای مرورگر هم که هستهٔ کامپایلشده صداشون میزنه، از چند صد اسم به چند ده کانال بینام و شمارهگذاریشده تبدیل میشن که هر build دوباره چیده میشن، پس دیگه معلوم نیست ماژول داره چی رو صدا میزنه.
اصل چهارم، به آفلاین اعتماد نکنه. بدترین حالت اینه که مهاجم یه بار reverse کنه و برای همیشه آفلاین اجرا کنه، بدون rate limit و بدون هزینه. راه فرار اینه که بخشی از تصمیم از کلاینت جدا بشه: تو امنترین حالت، منطقی که سیگنالها رو به یه verdict تبدیل میکنه بدون مادهٔ سروری کامل نمیشه، و هر مرحله پشت یه اثبات liveness ارزون گیت شده. این «reverse once, run forever» رو تبدیل میکنه به «هر مرحله با سرور ما حرف بزن»؛ چیزی که میشه rate-limit و مشاهدهش کرد. نویسنده جالب اشاره میکنه که خود توضیح دادن این طراحی ضعفش نیست، چون کل ایده از اول فرض کرده مهاجم همهچی رو میدونه.
نکات کلیدی:
- کد تشخیص بات سمت کلاینت روی ماشین مهاجم اجرا میشه، پس نمیشه واقعاً مخفیش کرد
- بهجای قفل، obfuscation یه هزینهست؛ هدف اینه که مهندسی معکوس نتیجهٔ ماندگاری نده
- هر build و هر session داخل کد رو دوباره تصادفی میچینه تا امضاها منتقل نشن
- تصمیمهای حساس بهجای if با مشتق کلید رمزنگاری گرفته میشن، پس branchی برای فلیپ کردن نیست
- باارزشترین رازها اصلاً تو bundle نیستن و per-session سمت سرور ساخته میشن
- کلاینت به یه رفتوبرگشت زندهٔ سرور وابسته میشه تا اجرای آفلاین بیفایده بشه




