شبحی در کامپیوتر؛ ماجرای تروجان خالق یونیکس در قلب کامپایلر C
نفوذی هوشمندانه و پلید را تصور کنید که نه در سایههای دارک وب، بلکه در دل ابزارهایی رخ داده باشد که خود، مسئول ساخت نرمافزارهای امنیتیاند. این داستانِ یک درِ پشتی است که نه در دل کدها، بلکه در «کامپایلر» جا خوش کرده است؛ معماری نامرئی که برنامههای ما را به زبان صفر و یک ماشین ترجمه میکند. طراح این نفوذ هم کسی نبود بهجز کن تامپسون، یکی از پیشگامان سیستمعامل یونیکس و از اسطورههای دنیای کامپیوتر.
در سال ۱۹۸۴، تامپسون در جریان سخنرانی دریافت جایزهی تورینگ برای پیادهسازی UNIX، در اقدامی غیرمنتظره، ماجرای طراحی نفوذ خود را فاش کرد؛ تروجانی چنان ظریف که همچون شعبدهبازی به نظر میرسید. او توضیح داد که چگونه کامپایلر زبان C را دستکاری کرده بود تا هرگاه مشغول کامپایل کردن دستور login یونیکس باشد، بهصورت پنهانی یک درِ پشتی در آن تعبیه کند. این درِ پشتی به هر کسی که رمز عبور خاصی را وارد میکرد، اجازه میداد بدون احراز هویت وارد سیستم شود.
پیامدهای کشف شدن درِ پشتی یونیکس هولناک بود. حتی اگر توسعهدهندگان هر خط از کد یونیکس را بازرسی میکردند، چیزی غیرعادی پیدا نمیشد. درِ پشتی فقط در خروجی کامپایلر، یعنی نسخهی صفر و یکی که معمولاً کسی آن را بررسی نمیکند، وجود داشت. برای پاکسازی آن، به یک کامپایلر «مطمئن» نیاز بود. اما وقتی نسخههای پیشین هر کامپایلر هم ممکن است آلوده شده باشد، چگونه میتوان به آنها اعتماد کرد؟
تامپسون با طراحی این نفوذ و برملا کردن آن قصد داشت این پیام را برساند که هرگز نمیتوان به کدی که خودتان بهطور کامل ننوشتهاید، اعتماد کرد؛ حتی اگر آن کد را فرد مطمئنی چون کن تامپسون نوشته باشد.
هرگز نمیتوان به کدی که خودتان ننوشتهاید، اعتماد کرد
درِ پشتی یونیکس یک فرضیه یا شوخی نبود. تامپسون اعتراف کرد سالها قبل، هنگام کار روی یونیکس در آزمایشگاههای بل، این نفوذ را اجرایی کرده بود. وقتی از او پرسیدند «آیا کسی متوجه شد؟» پاسخ محکمی داد: «نه».
اکنون پس از ۴۰ سال، نفوذی که بهخاطر مقالهی تامپسون به «اعتماد به اعتماد» معروف شد، همچنان نمونهای کلیدی در امنیت سایبری است؛ نه بهخاطر رواج آن، بلکه بهدلیل آشکار کردن حقیقتی که امروز هم مصداق دارد: در دنیایی ساختهشده از لایههای کد، اعتماد، دروغی بیش نیست.
طرز کار کامپایلرها؛ نگهبانان نامرئی کدها
هر نرمافزاری که استفاده میکنید (از برنامهی موبایل گرفته تا بازیهای ویدیویی یا حتی مرورگری که با آن دارید این مقاله را میخوانید) در ابتدا به شکل متنی نوشتهشده به زبانهای برنامهنویسی مانند پایتون، جاوا یا C است؛ اما کامپیوتر این زبانها را مستقیماً درک نمیکند. پردازندهها تنها صفر و یک را میفهمند: زبان ماشین. پل ارتباطی بین این دو دنیا، «کامپایلر» نام دارد؛ برنامهای که در نقشی حیاتی ظاهر میشود و هر چیزی که در دنیای دیجیتال توسعه مییابد، به آن وابستگی دارد.
در بیان دقیقتر، کامپایلر یک مترجم است که کدهای قابل خواندن برای انسان (مثلاً یک برنامهی C) را میگیرد و به «زبان ماشین» تبدیل میکند؛ دستورالعملهای صفر و یکی که پردازندهی کامپیوتر اجرا میکند. اما این ترجمه، تبدیل سادهی واژهبهواژه نیست. کامپایلرهای امروزی بیشتر شبیه به کارخانههای پیشرفتهاند که مواد خام (کدها) را طی مراحلی پردازش میکنند.
ابتدا «پیشپردازنده»، کد را سازماندهی، میانبرها را باز و کتابخانههای خارجی را وارد میکند. سپس کامپایلر، ساختار کد را تحلیل میکند، خطاها را مییابد و بهینهسازیهایی را برای کارایی بهتر در منطق کد در نظر میگیرد. سپس، «لینکر» قطعات جداگانهی کد را به یک فایل اجرایی واحد تبدیل میکند. درنهایت برنامه با سرعت بالایی اجرا میشود، بیآنکه کاربران بدانند که این سرعت، وابسته به لایههایی از بررسیهای کامپایلر بوده است که به آن اعتماد داشتند.
کامپایلر بهعنوان اصلیترین معمار، در رأس نیازمندیهای هر برنامه قرار میگیرد و برنامهها را شکل میدهد
اعتماد به لایههای کامپایلر، در عین ضروری بودن، خطراتی نیز دارد؛ زیرا کامپایلرها جزو اجزای پایه در مهندسی نرمافزار هستند و اگر آلوده شوند، هر برنامهای که میسازند، مشکوک خواهد بود. تصور کنید هکری یک پرینتر را هک کند تا محتوای همهی کتابها (حتی دستورالعملهای ساخت پرینترهای جدید) را تغییر دهد. این دقیقاً قدرت کامپایلر است. نفوذ تامپسون همچون ویروسی، DNA آلوده را به هرچه که میساخت، تزریق میکرد.
اولین کامپایلرها، مثل نمونههایی که گریس هاپر در دههی ۱۹۵۰ ساخت، انقلابی بودند؛ چون برنامهنویسان را از نوشتن دستی کد ماشینِ پر از خطا، نجات دادند. امروزه کامپایلرهایی مانند GCC و LLVM پروژههای متنبازی هستند که هزاران توسعهدهنده، کدشان را بررسی میکنند.
درک عملکرد کامپایلرها فقط یک مفهوم فنی نیست، بلکه به ما نشان میدهد که چقدر موضوع «اعتماد» در دنیای کامپیوتر مهم است. تمام پیشرفتهای نرمافزاری به ابزارهای اساسی مثل کامپایلرها وابستهاند؛ اما همانطور که تامپسون نشان داد، همین تکیهگاه میتواند به نقطهضعف بزرگی تبدیل شود؛ ضعفی که نه فقط برنامهها، بلکه امنیت کل صنعت فناوری را نشانه میرود.
درِ پشتی تامپسون؛ مهندسی یک فریبِ خودتکثیر
نفوذ تامپسون به کامپایلر C، نمونهی کلاسیک بهرهبرداری از حلقهی اعتماد در مهندسی نرمافزار است. برای درک عمق این نفوذ، باید فرایند «بوت استرپ» (Bootstrap) کامپایلر را بررسی کنیم: روشی که کامپایلرها با آن نسخههای جدید خود را میسازند. تامپسون از این فرایند سوءاستفاده کرد تا یک درِ پشتی را نهتنها در برنامههای تولیدشده، بلکه در هستهی ابزارهای توسعه، جاودانه کند.
گام اول: آلودهسازی هدفمند
تامپسون کد منبع کامپایلر را تغییر داد تا هر بار با کامپایل فایل کد منبع login در یونیکس، یک شرط مخفی به آن اضافه کند. این شرط بررسی میکرد آیا کاربر رمز عبور خاصی (مثلاً «۱۲۳۴۵») را وارد کرده است یا خیر. اگر پاسخ مثبت بود، بدون توجه به صحت رمز اصلی، دسترسی داده میشد. این تغییر در کد منبع کامپایلر اعمال شد، اما تنها زمانی فعال میشد که کامپایلر مشغول پردازش فایل login.c بود؛ یعنی دقیقاً هنگام ورود به سیستم.
گام دوم: خودتکثیری با بازنویسی کامپایلر
نبوغ تامپسون در گام بعدی آشکار شد. او کامپایلر را طوری اصلاح کرد که هنگام کامپایل خودش، کد درِ پشتی را دوباره به نسخهی جدید اضافه کند. این کار با نوشتن یک «شناسه» در کد منبع کامپایلر انجام میشد: اگر کامپایلر متوجه میشد که کد خودش را پردازش میکند، بلافاصله کد مخرب را به خروجی تزریق میکرد.
کامپایلرها برای توسعه، خودشان را کامپایل میکنند
اوج نبوغ تامپسون در اینجا بود که حتی اگر توسعهدهندگان، کد منبع کامپایلر را از نو بازنویسی میکردند، نسخهی باینری (کامپایلشده) جدید همچنان حاوی درِ پشتی بود؛ زیرا کامپایلر قدیمی، هنگام ساختِ نسخهی جدید، دوباره درِ پشتی را تعبیه میکرد.
گام سوم: پاکسازی ردپاها
پس از اینکه کامپایلر آلوده ساخته شد، تامپسون تمام کدهای مربوط به درِ پشتی را از نسخهی منبع پاک کرد. اکنون، تنها نسخهی باینری کامپایلر، که بهصورت صفر و یک ذخیره شده بود، این کد مخرب را در خود داشت. ازآنجاکه بازرسی باینری بهصورت دستی تقریباً غیرممکن است (برخلاف کد منبع که خوانا نوشته میشود)، هیچ راهی برای تشخیص نفوذ باقی نماند.
چرا نفوذ تامپسون غیرقابل شناسایی بود؟
نفوذ به کامپایلر C نه یک نقص فنی، بلکه سوءاستفاده از منطق زیرساختی بود که کل صنعت نرمافزار بر آن بنا شده است: اعتماد به ابزارهایی که خودشان را میسازند. تامپسون نشان داد که امنیت، گاهی در سایهی مفاهیمی پنهان میشود که حتی به ذهن برنامهنویس نیز خطور نمیکند.
نفوذ تامپسون، ضعف نظارت بر کد ماشین را برجسته کرد. حتی اگر کد منبع کامپایلر بینقص نوشته شود، نسخهی باینری آن ممکن است دستکاری شده باشد. مانند مُهری جعلی که روی موم داغ گذاشته شود، یک کامپایلر آلوده هیچ ردپایی در کد منبع باقی نمیگذارد.
بازخوانی یک نفوذ؛ افسانه یا واقعیتی ترسناک؟
در کنفرانس «Southern California Linux Expo» در مارس ۲۰۲۳، تامپسون بهعنوان سخنران پایانی، سخنرانی جذابی دربارهی تلاش ۷۵ سالهی خود برای گردآوری احتمالاً بزرگترین مجموعهی موسیقی دیجیتال خصوصی دنیا ارائه داد که شامل جعبههای موسیقی قدیمی و پیانوی خودکار بود.
در بخش پرسش و پاسخ، یکی از حضار بهشوخی دربارهی سخنرانی جایزهی تورینگ پرسید: «آیا میتوانید بگویید امروز هم درِ پشتی در هر نسخه از GCC و لینوکس قرار دارد؟» تامپسون پاسخ داد:
فرض میکنم دربارهی مقالهی قدیمی من صحبت میکنید. نه، من هیچ درِ پشتیای ندارم. آن نفوذ با کنترل دقیق انجام شد، چون پیش از آن اشتباهات فاحشی رخ داده بود. من آن را منتشر کردم، یا به شکلی کنترلشده اجازه دادم کسی آن را از من بدزدد و سپس ردیابی کردم که آیا آن را پیدا میکنند یا نه. [خوشبختانه] آنها موفق نشدند. به دلایل فنی، سیستم از کار افتاد و نتوانستند منشأ مشکل را تشخیص دهند؛ پس هرگز لو نرفت. در حضور این جمعیت باید بگویم که از زمانی که آن مقاله را نوشتم، منتظر این پرسش بودم: «کدش را هنوز داری؟» اما هیچکس نپرسید. من هنوز کد را نگه داشتهام.
در سپتامبر ۲۰۲۳، راس کاکس، مهندس نرمافزار برجسته و یکی از رهبران فنی اصلی تیم توسعه زبان برنامهنویسی Go، بلافاصله پس از تماشای ویدیو در یوتیوب، به تامپسون ایمیل زد و از او کد را خواست. پس از تأخیری ششماهه، تامپسون پاسخ داد که او بهعنوان اولین نفر، این درخواست را مطرح کرده است.
تامپسون فایلی به نام nih.a را برای کاکس ارسال کرد؛ نامی مرموز برای برنامهای مرموز. تامپسون بعدها تأیید کرد این نام مخفف «Not Invented Here» است؛ بهمعنی «اینجا اختراع نشده» برای اشاره به این طرز تفکر که افراد یا سازمانها به محصولات بیرونی بیاعتماد هستند. امروزه کامپایلرها معمولاً فایلهای .a را بهعنوان آرشیو دادهها تولید میکنند، اما فایل ارسالی تامپسون حاوی دو کد منبع قدیمی بود.
کد محتوای nih.a با کامپایلر C در نسخهی پنجم یونیکس (منتشرشده در ژوئن ۱۹۷۴) کار نمیکرد، زیرا آن زمان پیشپردازندهی C فقط فایلهایی را پردازش میکرد که با کاراکتر # شروع میشدند. درِ پشتی در پیشپردازنده قرار داشت و فایل cc.c در نسخهی پنجم با # شروع نمیشد، بنابراین نمیتوانست خود را اصلاح کند.
کاکس تلاش کرد با وصل کردن نقاط روشن ماجرا، از جمله تاریخهای تولید فایل nih.a، اصالت آن را تأیید کند
اما محتوای nih.a با کامپایلر C در نسخهی ششم سیستمعامل در دست توسعهی یونیکس سازگاری داشت. بنابراین کد ارسالی به دورهی یکساله بین ژوئن ۱۹۷۴ تا ژوئن ۱۹۷۵ تعلق داشت (احتمالاً اوایل ۱۹۷۵). کاکس یک شبیهساز آنلاین از برنامههای نسخهی ششم یونیکس ایجاد و آن را با فایلهای قدیمی از تامپسون و دنیس ریچی (از جمله nih.a) پر کرد تا کد را ردیابی کند.
اگرچه کد nih.a در نسخهی ششم یونیکس استفاده نشد، آرشیو nih.a زمان اصلاح هر فایل را ثبت کرده بود. با استفاده از سیستمهای مدرن یونیکس، میتوان این زمانبندی را مستقیماً از آرشیو استخراج کرد:
براساس گفتههای تامپسون در پرسش و پاسخ و روایتهای عمومی متعدد (گاه با جزئیات متناقض)، به نظر میرسد برنامهنویسان اولیهی یونیکس (Programmer's Workbench یا PWB) نسخهی آلودهشده کامپایلر را کپی کردند. نهایتاً درِ پشتی به برنامهی login نیز راه یافت؛ اما گروه PWB متوجه شدند هر بار که کامپایلر خودش را کامپایل میکند، حجمش افزایش مییابد. درنهایت، آنها فرایند تکثیر را مختل کردند و کامپایلر پاکی به دست آوردند.
طبق گفتهی جان مَشِی، که در توسعه سیستمعامل UNIX و طراحی معماریهای RISC نقش داشته، نسخهی آلوده در آزمایشگاهای بل باقی ماند. همهی روایتها، بهجز یکی، تأیید میکنند کامپایلر آلوده، فراتر از آزمایشگاههای بل منتشر نشد. بااینحال، مقالهی اریک ریموند حاوی اطلاعاتی بود که بیان میداشت درِ پشتی login به بیرون از آزمایشگاههای بل نشت کرده بود. او خطاب به تامپسون نوشت:
سردبیر مقاله تامپسون، از دو گزارش جداگانه اطلاع یافت که نسخهی آلودهی login از آزمایشگاههای بل خارج شده و حداقل یک بار امکان ورود شبانه به شبکه را برای کاربری با نام «kt» فراهم کرده بود.
تامپسون ادعای ریموند را قویاً رد کرد و گفت از نظر فنی ممکن نبوده است، زیرا درِ پشتی فقط برای حسابهای موجود با رمز «codenih» کار میکرد؛ اما ماه هیچ وقت پشت ابر نمیماند.
سالها قبل (۱۹۹۷)، ریچی مجموعهای از نسخههای قدیمی نوارهای دادههای شخصی از زمان توسعهی یونیکس را به وارن تومی، مدیر آرشیو جامعهی میراث یونیکس (TUHS) اهدا کرده بود. در جولای ۲۰۲۳، تومی این مجموعه را در اینترنت منتشر کرد. یکی از نوارها، حاوی فایلهای قدیمی تامپسون بود، از جمله فایل nih.a با تاریخ ویرایش ۳ جولای ۱۹۷۵. گویا تامپسون نسخهای کمی متفاوت از nih.a را (با تاریخ ویرایش ۲۸ ژانویهی ۱۹۹۸) برای راس کاکس فرستاده بود؛ بازنویسی تاریخ!
برای مخاطبان کنجکاو: دادههای ریچی در این لینک قرار دارد. با کمی حوصله و وصل کردن نکاتی که خواندید، میتوانید به فایل nih.a دسترسی پیدا کنید. (راهنمایی: زمانی که هر دلار، ۳۰۲٫۷ یِن بود.)
ابعاد ترسناک «اعتماد به اعتماد»
در بطن نفوذ تامپسون، کابوس امنیت سایبری ترسناکی وجود دارد: «چگونه به چیزی اعتماد کنیم که نمیتوانیم ببینیم؟» برای ساخت نرمافزارهای امن، به ابزارهای قابلاعتماد نیاز دارید، اما اگر همان ابزارها نیز آلوده باشند، هر چیزی که را میسازند (حتی ابزارهای جدیدی که باید جایگزینشان شوند) آلوده میکنند. این چرخهای معیوب است که راه گریزی از آن نیست. فقط یک راهحل وجود دارد: برنامهنویس دیگری، کامپایلر C را از نو و با زبان اسمبلی بنویسد؛ اما آیا این بار به برنامهنویس جدید اعتماد دارید؟
بهطور خلاصه، با هر بار کامپایل، مشکل بهطور مداوم تکرار میشود. یک کامپایلر آلوده نهتنها برنامهها، بلکه کامپایلرهای آینده، سیستمعاملها و حتی ابزارهای امنیتی را نیز آلوده میکند. بازرسی کد منبع هم فایدهای ندارد، چون رخنه در باینری کامپایلر نهفته است.
آسیبپذیری login یونیکس، یک تئوری انتزاعی نیست. اکوسیستمهای نرمافزاری امروز به زنجیرههای تأمین گستردهای متکی هستند: کتابخانهها، فریمورکها و کامپایلرهایی که اغلب توسط کامپایلرها ساخته میشوند. نمونهی نزدیک به واقعیت در سال ۲۰۲۴، کتابخانهی XZ Utils بود که یک درِ پشتی در فرایند ساخت آن پنهان شده بود. امروزه مهاجمان نیازی به نفوذ مستقیم به هدف ندارند؛ کافی است ابزارهایی را که هدف به آنها وابستگی دارد، آلوده کنند.
آنچه «اعتماد به اعتماد» را بهطور خاص ترسناک میکند، نامرئی بودن آن است. برخلاف حمله باجافزار یا نشت داده، هیچ خطای آشکار، هشدار، یا پیغام قرمزی رخ نمیدهد. فقط نفوذِ خاموش و دراختیارگیریِ کنترل، بیصدا صورت میگیرد.
درسهایی از یک نفوذ ۴۰ ساله
نفوذ کامپایلری تامپسون تنها بخشی از تاریخ فناوری باقی نماند، بلکه به نقشهی راهی تبدیل شد که حملات سایبری مدرن را شکل داد. اصولی که در سال ۱۹۸۴ این نفوذ را ممکن کردند، امروزه پایهی حملات زنجیرهی تأمین مانند نفوذ به «سولارویندز» در سال ۲۰۲۰ هستند، جایی که کدهای مخرب روی بهروزرسانیهای قانونی سوار شدند و به هزاران سازمان نفوذ کردند.
جامعهی امنیتی با الهام از هشدار تامپسون، راهکارهایی را پیدا کرده است؛ مثل پروژههایی مانند ساخت تکرارپذیر (Reproducible Builds) که به توسعهدهندگان اجازه میدهد نسخهی باینری را با کد منبع مقایسه کنند تا از نبود دستکاری اطمینان یابند یا اکوسیستمهای متنوع کامپایلرها (مثل GCC و LLVM) که مانند استفاده از مترجمهای چندزبانه برای کاهش وابستگی به یک ابزار واحد عمل میکنند. روشهای «درستیابی صوری» (Formal Verification) که با منطق ریاضی، صحت کامپایلرها را اثبات میکنند، لایهی دیگری از حفاظت هستند. بااینحال، خطرات جدیدی در حال ظهورند: کدهای تولیدشده از سوی هوش مصنوعی (مثل GitHub Copilot).
معضل اعتماد با پیشرفت فناوری پیچیدهتر میشود. آیا مدلهای هوش مصنوعی که کد مینویسند، میتوانند ناخواسته آسیبپذیریهای نامرئی را یاد بگیرند و تقلید کنند؟ آیا رمزنگاری کوانتومی میتواند جلوی حملات آینده را بگیرد، یا خود بستری برای تهدیدات جدید میشود؟
شاید درسی که از نفوذ تامپسون میگیریم، این باشد که گاهی باید اعتماد خود را به هرچیز در دنیای فناوری زیر سؤال ببریم؛ زیرا هرچه تکنولوژی بیشتر به سمت سیستمهای خودکار پیش میرود، ما هم باید نگاه پرسشگرانهی بیشتری داشته باشیم تا بتوانیم ابزارهای مطمئنتری بسازیم. در دنیای امروز، امنیت سایبری در حذف کامل خطرها تعریف نمیشود، بلکه رویارویی آگاهانه با خطرها هم نوعی دفاع در برابر ریسکهای امنیتی است.
درس اخلاقی خود تامپسون هم در پایان سخنرانیاش برای دریافت جایزهی تورینگ همین بود: «نباید به کدی که خودت آن را بهطور کامل ننوشتهای، اعتماد کرد.»