چطور FreeBSD با استفاده از “-fbounds-safety” امنیت TCP/IP Kernel را افزایش میدهد؟
امنیت Kernel یکی از مهمترین بخشهای امنیت یک سیستمعامل است. اگر برنامهای در فضای Userland دچار خطا شود، معمولاً با Crash شدن همان برنامه مواجه میشویم؛ اما وقتی یک خطای مشابه در Kernel رخ دهد، پیامد آن میتواند بسیار جدیتر باشد.
این موضوع در Network Stack اهمیت بیشتری پیدا میکند، زیرا بخش قابلتوجهی از دادههایی که Kernel پردازش میکند مستقیماً از شبکه میآیند و در بسیاری از موارد میتوانند توسط یک مهاجم ساخته یا دستکاری شوند.
در همین راستا، بنیاد FreeBSD پروژهای را برای استفاده از قابلیت “-fbounds-safety” در بخش TCP/IP Kernel دنبال میکند؛ پروژهای که هدف آن کاهش خطر Out-of-Bounds Memory Access و در نتیجه افزایش مقاومت Kernel در برابر برخی حملات مبتنی بر Memory Corruption است.
—
“-fbounds-safety” چیست؟
برای درک این پروژه ابتدا باید با یک مشکل قدیمی در زبان C آشنا شویم.
بخش بزرگی از Kernel سیستمعاملها، از جمله FreeBSD، با زبان C نوشته شده است. C کنترل بسیار زیادی روی حافظه، Pointerها و ساختار دادهها در اختیار برنامهنویس قرار میدهد؛ اما در حالت عادی، اطلاعات کاملی درباره محدودهی معتبر یک Pointer در اختیار Compiler قرار نمیدهد.
برای مثال، فرض کنید چنین آرایهای داشته باشیم:
char data[10];
در این حالت تنها عناصر “data[0]” تا “data[9]” معتبر هستند. اگر کد برنامه تلاش کند به “data[15]” دسترسی پیدا کند، در واقع خارج از محدودهی آرایه به حافظه دسترسی پیدا کرده است.
این نوع خطا را Out-of-Bounds Access مینامیم.
مشکل اینجاست که زبان C در حالت عادی لزوماً جلوی چنین دسترسیای را نمیگیرد.
—
چرا Out-of-Bounds Access در Kernel خطرناک است؟
Kernel بالاترین سطح دسترسی را در سیستمعامل دارد و مستقیماً با منابعی مانند حافظه، پردازنده، Deviceها و Network Interfaceها کار میکند.
بنابراین یک خطای Memory Access در Kernel میتواند پیامدهایی بسیار جدیتر از Crash شدن یک برنامه معمولی داشته باشد.
برای مثال، یک مسیر آسیبپذیر ممکن است به شکل زیر باشد:
دادهی مخرب شبکه
↓
پردازش Packet
↓
طول یا اندازهی اشتباه
↓
Out-of-Bounds Access
↓
Memory Corruption
↓
Crash یا در شرایط خاص سوءاستفاده امنیتی
هدف “-fbounds-safety” این است که چنین مسیرهایی تا حد امکان به جای یک دسترسی غیرمجاز و خاموش به حافظه، به یک Bounds Violation قابل تشخیص تبدیل شوند.
—
TCP/IP Stack چه نقشی دارد؟
برای اینکه اهمیت پروژه را بهتر درک کنیم، باید بدانیم Packetهای شبکه چگونه وارد سیستم میشوند.
بهصورت ساده، مسیر یک Packet را میتوان اینطور تصور کرد:
Network
↓
Network Interface
↓
Driver
↓
FreeBSD Kernel
↓
TCP/IP Stack
↓
TCP / UDP / IP Processing
↓
Application
TCP/IP Stack مسئول پردازش پروتکلهایی مانند IP و TCP و مدیریت بخشهای مختلف ارتباطات شبکه است.
برای مثال، وقتی یک TCP Packet دریافت میشود، Kernel باید اطلاعاتی مانند Header، طول داده، TCP Options و سایر فیلدهای Packet را بررسی کند.
این یعنی بخش زیادی از TCP/IP Stack دائماً در حال پردازش Input غیرقابل اعتماد است.
—
یک Packet مخرب چه مشکلی میتواند ایجاد کند؟
فرض کنیم یک Packet دارای اطلاعاتی درباره طول یک بخش از داده باشد.
Kernel ممکن است بر اساس این اطلاعات تصور کند که مثلاً ۵۰ بایت داده در اختیار دارد:
Length = 50
اما Packet واقعی فقط ۱۰ بایت داده داشته باشد.
اگر کد بدون بررسی مناسب تلاش کند ۵۰ بایت را بخواند، ممکن است به حافظهای خارج از محدودهی واقعی داده دسترسی پیدا کند.
بهصورت ساده:
Packet واقعی:
┌──────────────────────┐
│ Header │ 10 Bytes │
└──────────────────────┘
↑
پایان
اما اطلاعات داخل Packet میگویند:
Length = 50
در یک پیادهسازی ناامن، ممکن است نتیجه چنین چیزی باشد:
Parser
↓
Read 50 Bytes
↓
Out-of-Bounds Access
↓
Memory Corruption
اما با استفاده از Bounds Safety، هدف این است که چنین شرایطی قابل تشخیص باشد:
Parser
↓
Bounds Check
↓
Not Enough Data
↓
Trap / Diagnostic
در این حالت، به جای اینکه Kernel بهصورت خاموش وارد حافظهی خارج از محدوده شود، نقض محدوده شناسایی میشود.
—
“__counted_by” چه کاری انجام میدهد؟
یکی از مفاهیم مهم در این پروژه، Annotationهایی مانند “__counted_by()” است.
فرض کنید ساختاری شبیه این داشته باشیم:
struct packet {
char *data;
size_t length;
};
در کد معمولی C، “data” و “length” دو متغیر جداگانه هستند و Compiler الزاماً نمیداند که مقدار “length” اندازهی دادهای است که “data” به آن اشاره میکند.
اما با استفاده از Annotation مناسب میتوان این رابطه را به Compiler اعلام کرد.
مفهوم کلی این است:
Pointer
│
└──> Data
Count
│
└──> تعداد عناصر معتبر
در نتیجه Compiler اطلاعات بیشتری دربارهی محدودهی Pointer در اختیار خواهد داشت.
این موضوع به Compiler اجازه میدهد در برخی شرایط تشخیص دهد که یک Memory Access معتبر است یا خارج از محدوده قرار دارد.
—
چرا Compiler در این پروژه اهمیت زیادی دارد؟
هدف این پروژه این نیست که برنامهنویس مجبور شود برای تکتک Memory Accessها بهصورت دستی Check بنویسد.
بخشی از کار بر عهدهی Compiler قرار میگیرد.
اگر Compiler بتواند از روی اطلاعات موجود ثابت کند که یک Access قطعاً در محدوده است، نیازی به انجام یک Check اضافی در زمان اجرا نخواهد بود.
برای مثال:
Access
↓
Compiler میداند Access معتبر است
↓
بدون Check اضافی
اما اگر Compiler نتواند اعتبار Access را ثابت کند:
Access
↓
Bounds Check
↓
Valid → ادامه اجرا
Invalid → Trap / Diagnostic
این قابلیت به پروژه اجازه میدهد میان امنیت و Performance تعادل مناسبی برقرار کند.
—
Soft Mode چیست؟
یکی از چالشهای مهم در تبدیل یک Kernel بزرگ به کدی با Bounds Safety این است که نمیتوان انتظار داشت تمام مشکلات موجود در یک مرحله شناسایی و اصلاح شوند.
به همین دلیل، پروژه از مفهومی به نام Soft Mode نیز استفاده میکند.
در این حالت، اگر یک Bounds Violation رخ دهد، سیستم میتواند آن را ثبت و گزارش کند، بدون اینکه الزاماً همان لحظه Kernel را متوقف کند.
این قابلیت برای توسعهدهندگان بسیار مفید است، زیرا امکان شناسایی تدریجی بخشهایی از کد را که هنوز به اصلاح نیاز دارند فراهم میکند.
در این فرآیند میتوان از ابزارهایی مانند DTrace برای مشاهده و ثبت این رخدادها استفاده کرد.
—
DTrace چه کمکی میکند؟
DTrace یکی از ابزارهای قدرتمند FreeBSD برای مشاهده و تحلیل رفتار سیستم در زمان اجرا است.
در این پروژه، میتوان از آن برای بررسی مواردی مانند Bounds Violation استفاده کرد.
به این ترتیب توسعهدهندگان میتوانند بفهمند:
– کدام مسیر کد باعث نقض Bounds شده است؟
– این اتفاق در چه شرایطی رخ داده است؟
– آیا مشکل مربوط به یک Bug واقعی است؟
– آیا یک Annotation ناقص است؟
– آیا باید منطق کد تغییر کند؟
این اطلاعات به تیم توسعه اجازه میدهد مهاجرت به Bounds Safety را بهصورت تدریجی انجام دهد.
—
چرا مهاجرت فایلبهفایل انجام میشود؟
TCP/IP Stack در Kernel FreeBSD یک بخش کوچک و مستقل نیست؛ بلکه مجموعهای بزرگ از کدها و ساختارهای مختلف است.
تغییر همزمان تمام این کدها ریسک زیادی دارد.
به همین دلیل رویکرد پروژه Incremental است:
File
↓
Annotation
↓
Build
↓
Test
↓
Performance Check
↓
Next File
یعنی هر بخش بهتدریج اصلاح، Build و آزمایش میشود.
این روش علاوه بر کاهش ریسک، امکان پیدا کردن مشکلات ناشی از تغییرات را نیز سادهتر میکند.
—
کدام بخشهای TCP/IP مورد توجه هستند؟
این پروژه روی بخشهای حساسی از TCP/IP Stack تمرکز دارد؛ بهخصوص قسمتهایی که دادههای ورودی شبکه را Parse و پردازش میکنند.
از جمله حوزههای مورد توجه میتوان به موارد زیر اشاره کرد:
TCP Options
TCP میتواند دارای Optionهای مختلفی باشد و Kernel باید آنها را از داخل Packet استخراج و بررسی کند.
چون این اطلاعات از شبکه میآیند، بررسی دقیق طول و محدوده آنها اهمیت زیادی دارد.
SACK Processing
SACK یا Selective Acknowledgment یکی از قابلیتهای TCP است که به گیرنده اجازه میدهد بخشهایی از داده را که دریافت کرده است به شکل دقیقتری اعلام کند.
پردازش این اطلاعات نیز یکی از قسمتهای مورد توجه پروژه است.
Segment Reassembly
دادههای TCP ممکن است در چند Segment دریافت شوند و Kernel باید آنها را مدیریت و در صورت نیاز دوباره کنار هم قرار دهد.
در چنین فرآیندهایی مدیریت صحیح Pointerها، Lengthها و Bufferها اهمیت زیادی دارد.
SYN Cache
SYN یکی از نخستین پیامها در فرآیند برقراری TCP Connection است.
از آنجا که SYN از شبکه دریافت میشود، مسیر پردازش آن نیز باید در برابر ورودیهای غیرقابل اعتماد مقاوم باشد.
TCP Fast Open Cookie Validation
TCP Fast Open یا TFO مکانیزمی برای کاهش هزینه برقراری برخی Connectionهای TCP است.
Cookie مربوط به TFO نیز باید به شکل صحیح Parse و Validate شود.
—
چرا TCP/IP یک Attack Surface مهم است؟
یکی از دلایل اصلی اهمیت این پروژه این است که Network Stack مستقیماً با دادههایی سروکار دارد که ممکن است توسط مهاجم ارسال شوند.
برای مثال:
Attacker
│
│ Malicious Packet
↓
Internet
↓
Network Interface
↓
FreeBSD Kernel
↓
TCP/IP Stack
مهاجم لازم نیست الزاماً روی خود سیستم دسترسی داشته باشد تا Packet را به آن ارسال کند.
به همین دلیل، یک Bug در مسیر پردازش Network Packet میتواند از نظر امنیتی اهمیت بالایی داشته باشد.
به زبان ساده:
«هر چیزی که از شبکه وارد Kernel میشود، باید تا حد ممکن ورودی غیرقابل اعتماد در نظر گرفته شود.»
—
آیا “-fbounds-safety” تمام مشکلات Memory Safety را حل میکند؟
خیر.
این نکته بسیار مهم است.
Bounds Safety تنها یکی از بخشهای بزرگتر مفهوم Memory Safety است.
برای مثال، مشکلات دیگری نیز وجود دارند:
– Use-after-free
– Double-free
– Memory Leak
– Uninitialized Memory
– Race Condition
– Integer Overflow
– Logic Bugs
بنابراین نباید “-fbounds-safety” را راهحلی برای تمام مشکلات امنیتی Kernel دانست.
هدف این پروژه مشخصتر است:
«کاهش خطر دسترسی خارج از محدودهی حافظه و جلوگیری از تبدیل برخی خطاهای Memory Access به Memory Corruption.»
—
Compatibility با کد قدیمی چگونه حفظ میشود؟
یکی از چالشهای مهم پروژه این است که FreeBSD نمیتواند تمام Kernel را یکباره به یک مدل جدید Memory Safety منتقل کند.
بخشی از کدها ممکن است هنوز Instrument نشده باشند و بخشهای دیگر بهتدریج با Bounds Safety ساخته شوند.
بنابراین باید امکان تعامل میان کد جدید و قدیمی وجود داشته باشد.
این موضوع به پروژه اجازه میدهد مسیر مهاجرت را مرحلهبهمرحله طی کند:
Old Code
↕
New Bounds-Safe Code
↕
Old Code
این رویکرد برای پروژهای در مقیاس Kernel بسیار مهم است.
—
Performance چه میشود؟
هر مکانیزم امنیتی ممکن است نگرانیهایی درباره Performance ایجاد کند.
اگر قرار باشد برای هر Memory Access یک Bounds Check اضافه شود، در صورت پیادهسازی نادرست میتواند هزینهی قابلتوجهی ایجاد کند.
اما یکی از مزیتهای رویکرد Compiler-based این است که Compiler در مواردی که میتواند ایمن بودن Access را اثبات کند، قادر است Checkهای غیرضروری را حذف کند.
به همین دلیل پروژه علاوه بر امنیت، Performance را نیز اندازهگیری میکند.
مقایسه میان Kernel معمولی و Kernel دارای Instrumentation یکی از بخشهای مهم ارزیابی پروژه است.
—
تست روی معماریهای مختلف
تغییرات Kernel باید روی معماریهای مختلف نیز بررسی شوند.
در این پروژه، تستها روی معماریهایی مانند:
– “amd64”
– “arm64”
مورد توجه قرار گرفتهاند.
این موضوع اهمیت زیادی دارد، زیرا تغییرات مرتبط با Pointer، ABI، Compiler و Memory Access ممکن است روی معماریهای مختلف رفتار متفاوتی داشته باشند.
—
Regression Test چرا مهم است؟
فرض کنید تیم توسعه یک Bug مربوط به Out-of-Bounds را پیدا و اصلاح کند.
اگر فقط کد را اصلاح کنند، ممکن است در آینده با یک تغییر دیگر همان Bug دوباره برگردد.
برای جلوگیری از این اتفاق، یک Regression Test ایجاد میشود.
ایده ساده است:
Bug پیدا شد
↓
Bug Fix
↓
Regression Test
↓
تغییرات آینده
↓
Test دوباره اجرا میشود
اگر Bug دوباره ایجاد شود، تست شکست میخورد و تیم متوجه مشکل میشود.
پروژه نیز روی ایجاد تستهایی برای اطمینان از شناسایی Bounds Violationهای عمدی تمرکز دارد.
—
CI چه نقشی دارد؟
Continuous Integration یا CI باعث میشود تغییرات جدید بهصورت خودکار Build و Test شوند.
در پروژهای که بهتدریج تعداد زیادی فایل Kernel را تغییر میدهد، CI اهمیت زیادی دارد.
یک مسیر ساده را میتوان اینطور تصور کرد:
Code Change
↓
Build
↓
Unit / Regression Tests
↓
Architecture Tests
↓
Performance Checks
↓
Result
این فرآیند کمک میکند مشکلات خیلی زودتر از زمانی که وارد نسخه نهایی شوند، شناسایی شوند.
—
در نهایت این پروژه چه چیزی را تغییر میدهد؟
هدف نهایی را میتوان با مقایسه دو مسیر توضیح داد.
حالت سنتی
Network Packet
↓
Parser
↓
Incorrect Length
↓
Out-of-Bounds Access
↓
Memory Corruption
↓
Crash / Potential Exploitation
با Bounds Safety
Network Packet
↓
Parser
↓
Incorrect Length
↓
Bounds Check
↓
Violation Detected
↓
Trap / Diagnostic
تفاوت اصلی این دو مسیر در یک مفهوم خلاصه میشود:
Failing safely بهتر از Corrupting memory است.
—
چرا این پروژه برای FreeBSD مهم است؟
FreeBSD سالهاست در محیطهایی استفاده میشود که پایداری، Performance و امنیت اهمیت بالایی دارند.
Network Stack نیز یکی از مهمترین بخشهای Kernel آن است.
در نتیجه، اضافه کردن یک لایهی دفاعی برای شناسایی Out-of-Bounds Access میتواند بهخصوص در بخشهایی که مستقیماً با Network Input سروکار دارند، ارزش امنیتی بالایی داشته باشد.
از طرف دیگر، اهمیت این پروژه فقط به FreeBSD محدود نمیشود.
این پروژه نمونهای از یک چالش بزرگ در دنیای سیستمعاملهاست:
«چطور میتوان یک کدبیس بزرگ و قدیمی C را بدون بازنویسی کامل، بهتدریج امنتر کرد؟»
پاسخ FreeBSD در این پروژه، استفاده تدریجی از قابلیتهای Compiler، Annotationها، تستهای خودکار و ابزارهای Runtime برای حرکت به سمت Memory Safety بیشتر است.
—
جمعبندی
پروژه Adopt “-fbounds-safety” in the FreeBSD Kernel TCP/IP Stack تلاش میکند بخشهایی از TCP/IP Stack کرنل FreeBSD را در برابر یکی از کلاسهای مهم خطاهای Memory Safety مقاومتر کند.
ایده اصلی ساده است:
Pointer
+
Length / Bounds
↓
Compiler Awareness
↓
Bounds Checking
↓
Detect Invalid Access
در صورتی که یک Packet مخرب یا یک Bug داخلی باعث شود کد بخواهد خارج از محدودهی معتبر حافظه را Access کند، هدف این است که به جای وقوع یک Memory Corruption خاموش، نقض محدوده شناسایی شود.
این کار بهصورت تدریجی و با استفاده از Annotationهایی مانند “__counted_by”، ابزارهایی مانند DTrace، Regression Testها، CI و تست روی معماریهای مختلف انجام میشود.
البته “-fbounds-safety” به معنی حل شدن تمام مشکلات Memory Safety نیست؛ بلکه یک لایه دفاعی مهم در برابر Out-of-Bounds Memory Access است.
و شاید مهمترین نکته همین باشد:
«امنیت واقعی فقط به معنی حذف تمام Bugها نیست؛ بلکه باید کاری کرد که وقتی یک Bug وجود دارد، نتواند بهسادگی به یک آسیبپذیری جدی تبدیل شود.»
—
منبع
این مطلب بر اساس توضیحات رسمی پروژه در بنیاد FreeBSD تهیه شده است:
FreeBSD Foundation — Adopt “-fbounds-safety” in the FreeBSD Kernel TCP/IP Stack
برای مطالعه جزئیات فنی و وضعیت پروژه، به صفحه رسمی پروژه در FreeBSD Foundation مراجعه کنید.
