محافظت در برابر سرریز بافر

محافظت در برابر سرریز بافر (به انگلیسی: Buffer overflow protection) یکی از شیوه‌های است که در زمان توسعه نرم‌افزار برای افزایش امنیت برنامه قابل اجرا با شناسایی سرریز شدن بافر در متغیرهای اختصاص شده از پشته استفاده می‌شود. این تکنیک از رفتارهای نادرست برنامه و آسیب پذیری‌های امنیتی جدی جلوگیری می‌کند. سرریز بافر پشته زمانی اتفاق می‌افتد که یک برنامه، اطلاعاتی را در آدرسی از حافظه (معمولاً بافر با طول ثابت)، خارج از ساختمان داده مورد نظر می‌نویسد. اشکال سرریز بافر پشته زمانی اتفاق می‌افتد که یک برنامه حجم داده‌ای بیشتر از حجم بافر اختصاص داده شده را در حافظه می‌نویسد. نتیجه این عمل خراب شدن و آسیب دیدن اطلاعات مجاور آن بافر در پشته است و در صورتی که این سرریز به‌طور ناخواسته و بر اساس یک اشتباه به وجود آمده باشد، اغلب باعث می‌شود که برنامه کرش کند، به صورت نادرست رفتار کند یا به منجر به مشکلات امنیتی شود.

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

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

چندین پیاده سازی از محافظت در برابر سرریز بافر شامل مجموعه کامپایلر گنو، LLVM، مایکروسافت ویژوال استودیو و سایر کامپایلرها وجود دارد.

بررسی اجمالی 
سرریز بافر پشته زمانی اتفاق می‌افتد که یک برنامه، اطلاعاتی را در آدرسی از حافظه (معمولاً بافر با طول ثابت)، خارج از ساختمان داده مورد نظر می‌نویسد. اشکال سرریز بافر پشته زمانی اتفاق می‌افتد که یک برنامه حجم داده ای بیشتر از حجم بافر اختصاص داده شده را در حافظه می‌نویسد. نتیجه این عمل خراب شدن و آسیب دیدن اطلاعات مجاور آن بافر در پشته است و در صورتی که این سرریز به‌طور ناخواسته و بر اساس یک اشتباه به وجود آمده باشد، اغلب باعث می‌شود که برنامه کرش کند یا به صورت نادرست رفتار کند. این نوع سرریز، بخشی از دسته بزرگتری از باگ‌های برنامه‌نویسی به نام سرریز بافر است. سرریز بافر در پشته به دلیل داشتن آدرس بازگشت توابع باعث به هم خوردن مسیر اجرای برنامه می‌شود در حالی که در سرریز بافر هیپ این اتفاق کم رنگ تر است. 

سرریز بافر پشته می‌تواند منجر به حملات پشته شود. اگر برنامه آسیب دیده با سطح دسترسی خاصی در حال اجرا باشد یا داده‌های ورودی خود را از شبکه‌های نامطمئن دریافت کند (به عنوان مثال یک وب سرور عمومی)، این اشکال یک آسیب پذیری امنیتی بالقوه است که به مهاجم اجازه می‌دهد کدهای قابل اجرا را به برنامه تزریق کند و کنترل روند برنامه را به دست بگیرد. این یکی از قدیمی‌ترین و مطمئن‌ترین روش‌ها برای مهاجمین برای دستیابی دسترسی غیرمجاز به کامپیوتر است. 

به طور معمول، محافظت در برابر سرریز بافر، ساختار داده‌های فراخوانی تابع در قاب پشته را برای افزودن مقدار canary تغیر می‌دهد. زمانی که این مقدار تخریب شود نشان دهنده این است که حافظه دچار سرریز بافر شده است و باعث جلوگیری از حملات سرریز بافر می‌شود. تاثیر این شیوه‌ها بر عملکرد برنامه ناچیر است. 

محافظت از پشته به روش smashing قادر به حفاظت در برابر حملات خاصی نیست. برای مثال، این روش نمی‌تواند در برابر سرریز بافر در هیپ حفاظت کند. هیچ روش کاربردی برای تغییر طرح داده در یک ساختار وجود ندارد، انتظار می‌رود ساختارها بین ماژول‌ها، به ویژه در کتابخانه‌های مشترک یکسان باشند. محافظت از داده‎ها در ساختار به روش مقدار canary پس از بافر شدن غیرممکن است. بنابراین، برنامه‌نویسان باید در سازماندهی متغیرها و ساختار آن‌ها دقت کافی را داشته باشند.

Canaries 
مقدار canary بین بافر و کنترل داده در پشته برای رصد کردن سرریز پشته قرار می‌گیرد. در زمان سرریز بافر، معمولاً اولین داده‌ای که خراب می‌شود مقدار canary است. پس از خرابی مقدار canary هشدار سرریز بافر منتشر می‌شود و سپس می‌توان این خطا را مدیریت کرد. به عنوان مثال می‌توان داده‌های خراب شده را اعتبار زدایی کرد. مقدار canary نباید با مقدار سینتیل اشتباه گرفته شود. 

سه نوع مقدار canary وجود دارد. خاتمه دهنده، تصادفی، تصادفی XOR. نسخه فعلی StackGuard از هر سه نوع پشتیبانی می‌کند در حالی که ProPolice صرفاً از نوع خاتمه دهنده و تصادفی پشتیبانی می‌کند.

canaryهای خاتمه‌دهنده 
canaryهای خاتمه‌دهنده از این مشاهدات استفاده می‌کنند که بیشتر حملات سرریز بافر براساس عملیات‌های خاص رشته‌ای هستند که در خاتمه‌دهنده‌های رشته‌ها هستند. واکنش این مشاهده این است که canaryها از خاتمه‌دهنده‌های تهی، CR، LF و -1 ایجاد شده است. در نتیجه، مهاجم باید قبل از آدرس بازگشت یک مقدار تهی بنویسد تا از تغییر canary جلوگیری کند. این کار از وقوع حملات با تابع strcpy() و سایر روش‌هایی که به محض کپی کردن، یک کاراکتر تهی برمی‌گردانند جلوگیری می‌کند، در حالی که نتیجه نامطلوب این است که canary شناخته می‌شود. حتی با این محافظت، مهاجم می‌تواند مقدار canary را با مقدار دلخواه خود جایگزین کند و کنترل داده‌ها را با مقادیر ناسازگار در دست بگیرد.

canaryهای تصادفی 
این مقدار بصورت تصادفی تولید می‌شود تا مقدار آن از دید مهاجمین مخفی بماند. معمولاً، خواندن این مقدار بصورت منطقی امکان پذیر نیست. این مقدار امن را صرفاً کسانی می‌دانند که (مثلاً کد حفاظت در برابر سرریز بافر) نیاز است. 

به طور نرمال، canary تصادفی در ابتدای شروع برنامه تولید می‌شود و در یک متغیر سراسری ذخیره می‌شود. این متغیر معمولاً توسط داده‌های بدون نقشه پر شده است، بنابراین تلاش برای خواندن آن از حافظه RAM سخت خواهد شد. این مقدار ممکن است هنوز قابل خواندن باشد، اگر مهاجم بتواند این مقدار را بخواند، می‌تواند برنامه در حال اجرا از پشته بخواند.

XOR canaryهای تصادفی 
XOR canary تصادفی، canaryهای تصادفی هستند که با همه یا بخشی از داده‌های کنترلی XOR شده‌اند. در این روش، زمانی که مقدار canary یا داده‌های کنترلی تغییر می‌کنند، مقدار canary اشتباه می‌شود. 

XOR canaryهای تصادفی، آسیب پذیری مشابه canary تصادفی را بجز "خواندن از پشته" دارند. در این روش خواندن از پشته کمی سخت‌تر و پیچیده‌تر شده است. مهاجم برای خنثی سازی محافظت باید مقدار canary، الگوریتم و داده‌های کنترلی را برای بدست آوردن canary اصلی داشته باشد. 

اگرچه این canaryها از داده‌های کنترلی در برابر تغییر توسط اشاره‌گرها محافظت می‌کند، آن‌ها از بقیه داده‌ها یا خود اشاره‌گرها محافظت نمی‌کنند. بخصوص اشاره‌گرهای توابع در اینجا مشکل ایجاد می‌کنند، آن‌ها می‌توانند سرریز شوند و در زمان فراخوانی shellCode اجرا کنند.

بررسی کردن مرزها 
بررسی مرزها یک شیوه مبتنی بر کامپایلر است که اطلاعات مرزها را در زمان اجرا برای هر بلوک اختصاص داده شده از حافظه اضافه می‌کند و همه اشاره‌گرها در مقابل آن‌ها در زمان اجرا بررسی می‌کند. برای C و C++ بررسی مرزها می‌تواند در زمان محاسبات یا ارجاع مجدد انجام شود. 

پیاده‌سازی این روش به دو روش زیر انجام می‌شود.
 مخزن مرکزی: هر بلوک اختصاص یافته از حافظه را توصیف می‌کند.
 اشاره‌گر Fat: شامل اشاره‌گر و داده‌های اضافی که ناحیه نقطه گذاری شده را توصیف می‌کند.

برچسب زدن 
برچسب زدن یک روش مبتنی بر کامپایلر یا سخت افزار (نیازمند معماری برچسب خورده) برای برچسب زدن نوع یک قطعه از داده در حافظه است که معمولاً برای بررسی نوع آن استفاده می‌شود. با علامت گذاری ناحیه مشخصی از حافظه به عنوان غیرقابل اجرا، بطور موثر از ذخیره کد قابل اجرا در حافظه جلوگیری می‌کند. همچنین ناحیه‌ای از حافظه را میتوان به عنوان غیر تخصیص یافته علامت گذاری کرد و از سرریز بافر جلوگیری نمود. 

از جنبه تاریخی، از برچسب زدن برای پیاده سازی زبان‌های برنامه نویسی سطح بالا استفاده شده است. با پشتیبانی مناسب از سمت سیستم عامل‌ها، از برچسب‌ها برای شناسایی سرریز بافر نیز استفاده می‌شود. نمونه آن ویژگی سخت افزاری NX bit است که توسط پردازندههای Intel ، AMD و ARM پشتیبانی می‌شود.

پیاده سازی‌ها

مجموعه کامپایلر گنو (GCC) 
محافظت از پشته بصورت smashing برای اولین بار توسط StackGuard در سال 1997 پیاده سازی و در سال 1998 توسط سمپوزیوم امنیتی USENIX منتشر شد. StackGuard یک سری وصله برای اینتل x86 که پس زمینه GCC 2.7 بود معرفی کرد. StackGuard برای توزیع لینوکس Immunix از سال 1998 تا 2003 نگهداری شد و با پیاده‌سازی‌های canary خاتمه‌دهنده، تصادفی و XOR تصادفی توسعه یافت. StackGuard در سال 2003 پیشنهاد اضافه شدن به GCC نسخه 3.x را مطرح کرد، اما این هرگز محقق نشد. 

از سال 2001 تا 2005، IBM وصله‌هایی برای GCC به منظور محافظت از پشته بصورت smashing توسعه داد و آن را به نام ProPolice معرفی کرد. ProPolice ایده StackGuard را با قرار دادن بافرها بعد از اشاره‌گرهای محلی و پارامترهای توابع در فریم پشته بهبود داد. این کار به اجتناب از خرابی اشاره‌گرها و جلوگیری از دسترسی به حافظه کمک می‌کند. 

مشکلات رد هات مشکلاتی را در ProPolice شناسایی کردند و در سال 2005 این شیوه را برای گنجاندن در GCC 4.1 بازنویسی کردند. در این بازنویسی پرچم‌های زیر معرفی شدند. 
 -fstack-protector: صرفاً از بعضی آسیب پذیری‌های توابع محافظت می‌کند. 
 protector-all: از همه توابع محافظت می‌کند 

در سال 2012 مهندسان گوگل پرچم -fstack-protector-strong را برای حفظ تعادل بین امنیت و کارایی برنامه پیاده‌سازی کردند. این پرچم از آسیب پذیری‌های توابع بیشتری نسبت به پرچم -fstack-protector محافظت می‌کند (ولی نه همه توابع) و کارایی بیشتری نسبت به پرچم -fstack-protector-all دارد. این پرچم از GCC نسخه 4.9 در دسترس است. 

تمامی بسته‌های فدورا از نسخه 5 با پرچم -fstack-protector و از نسخه 20 با پرچم -fstack-protector-strong کامپایل شده است. بیشتر بسته‌ها در اوبونتو از نسخه 6.10 با پرچم -fstack-protector کامپایل شده‌اند. همه بسته‌های آرچ لینوکس از سال 2011 با پرچم -fstack-protector و از سال 2014 با پرچم -fstack-protector-strong کامپایل شده‌اند. محافظت از پشته فقط برای بعضی از بسته‌های Debian استفاده می‌شود. محافظت از پشته در برخی از سیستم عامل‌های خاص از جمله OpenBSD ،Gentoo Gentoo و DragonFly BSD استاندارد است. 

StackGuard و ProPolice نمی‌توانند در برابر سرریز بافر در ساختارهای اختصاص یافته خودکار که درون اشاره‌گرهای توابع سرریز می‌شوند، محافظت کنند. در ProPolice ترتیب تخصیص ساختارها قبل از اشاره‌گرهای توابع مجدداً تنظیم می‌شوند. روش‌های جداگانه برای محافظت از اشاره گر در PointGuard و Microsoft Windows موجود است.

مایکروسافت ویژوال استدیو 
مجموعه کامپایلرهای مایکروسافت محافظت از سرریز بافر را از سال 2003 در خط فرمان، سوئیچ  پیاده سازی نمود. این سوئیچ از نسخه منتشر شده در سال 2005 بصورت پیشفرض فعال است. برای غیرفعال کردن آن می‌توان از  استفاده کرد.

کامپایلر IBM 
محافظت در برابر سرریز شدن پشته توسط پرچم کامپایلر -qstackprotect فعال می‌شود.

Clang / LLVM 
زبان C از سه تشخیص دهنده سرریز بافر زیر پشتیبانی می‌کند: 
 AddressSanitizer (-fsanitize=address) 
 -fsanitize=bounds  
 SafeCode 

این سیستم‎ها دارای تفاوت‌هایی در عملکرد، سربار حافظه و کلاس تشخیص باگ هستند. محافظت از پشته در برخی از سیستم عامل‌های خاص از جمله OpenBSD استاندارد است.

کامپایلر اینتل 
کامپایلرهای C و C++ اینتل از محافظت از پشته به روش smashing شبیه گزینه‌های مشابه ارائه شده توسط GCC و مایکروسافت ویژوال استدیو پشتیبانی می‌کند.

Fail-Safe C 
Fail-Safe C یک کامپایلر ANSI C حافظه-امن متن باز است که بررسی مرزها را بر اساس نشانگرهای fat و دسترسی شی گرا به حافظه انجام می‌دهد.

StackGhost (مبتنی بر سخت افزار) 
StackShost یک روش ساده برای برای ثبت نشست پنجره/پر کردن روال‌ها است که باعث می‌شود بهره‌برداری از سرریز بافر سخت‌تر شود. این روش از یک ویژگی منحصر به فرد در معماری Sun Microsystems SPARC برای تشخیص تغییرات اشاره‌گرهای بازگشت استفاده می‌کند. بدین ترتیب در این روش نیاز به تغییر سورس برنامه نیست و این محافظ بصورت خودکار تمام برنامه را محافظت می‌کند. تاثیر این روش بر کارایی برنامه ناچیز و در حدود یک درصد می‌باشد. این روش پس از بهینه سازی و رفع اشکالات در OpenBSD / SPARC ادغام شد.

مثال از canary 
تخصیص بافر معمولی برای معماری‌های x86 و سایر معماری‌های مشابه در سرریز بافر نشان داده شده است. در اینجا ما فرایند تغییر یافته را به صورتی که به StackGuard مربوط می‌شود را نشان خواهیم داد. 

زمانی که یک تابع فراخوانی می‌شود، یک فریم شته ایجاد می‌شود. یک فریم پشته از انتهای حافظه تا ابتدا آن ساخته شده است. هر فریم پشته در بالای پشته قرار داده می‌شود که نزدیکترین به ابتدا حافظه است. بنابراین، اجرای قطعه آخر از داده‌ها در یک فریم پشته، داده‌های قبلی که قبلاً وارد فریم پشته شدند را تغییر می‌دهد و اجرای انتها یک فریم پشته داده‌ها در فریم پشته قبلی قرار می‌دهد. یک فریم پشته معمولی ممکن است به شکل زیر باشد:
 آدرس بازگشت (RETA) در ابتدا
 دستورات (CTLI) در ادامه
(CTLI)(RETA) در C، یک تابع ممکن است شامل ساختارهای مختلف داده به ازای هر فراخوانی باشد. هر قطعه از داده‌ها که در فراخوانی ایجاد می‌شود به ترتیب در پشته قرار می‌گیرد، بنابراین از انتها تا ابتدا حافظه مرتب می‌شوند. در زیر یک تابع فرضی و فریم پشته آن وجود دارد. int foo() {
  int a;             /* integer */
  int *b;            /* pointer to integer */
  char c[10];        /* character arrays */
  char d[3];

  b = &a;            /* initialize b to point to location of a */
  strcpy(c,get_c()); /* get c from somewhere, write it to c */
  *b = 5;            /* the data at the point in memory b indicates is set to 5 */
  strcpy(d,get_d());
  return *b;         /* read from b and pass it to the caller */
}(d..)(c.........)(b...)(a...)(CTLI)(RETA) در این وضعیت فرضی، اگر بیش از ده بایت در آرایه c، یا بیش از 13 بایت در آرایه کاراکتری d نوشته شود، مازاد آن در اشاره‌گر عدد صحیح b، سپس در عدد صحیح a، پس از آن در دستورات کنترلی و در نهایت در آدرس بازگشت سرریز می‌شود. با بازنویسی b، اشاره‌گر می‌تواند به مکانی از حافظه اشاره کند و در نتیجه هر آدرس دلخواهی از حافظه را بخواند. با بازنویسی RETA، تابع می‌تواند کد دیگری را اجرا کند (در زمانی که قصد بازگشت دارد). این کد می‌تواند از بین توابع موجود، یا کدهای نوشته شده در پشته در زمان سرریز باشد. 

به طور خلاصه، مدیریت ضعیف متغیرهای d و c، مانند فراخوانی strcpy در بدون تعیین مرز بالا، می‌تواند به مهاجمین اجازه دهد برنامه را با مقادیر منتسب شده در c و d بطور مستقیم کنترل کند. هدف از محافظت در برابر سرریز بافر، شناسایی این مشکلات با کمترین هزینه می‌باشد. این کار با حذف مقادیر مضر غیر ضروری یا افزودن canary بعد از بافر انجام می‌شود. 

محافظت در برابر سرریز بافر در کامپایلر پیاده سازی می‌شود. به این ترتیب، برای محافظت می‌توان ساختار داده را در فریم پشته تغییر داد. این کار دقیقاً همان چیزی است که در سیستم‌هایی مانند ProPolice انجام می‌شود. برای امن کردن تابع بالا، ترتیب زیر بصورت خودکار تنظیم می‌شود: آرایه c و d در ابتدا فریم پشته قرار می‌گیرند که در نتیجه اعداد صحیح a و b قبل از آن‌ها در حافظه قرار می‌گیرند. بنابراین فریم پشته به شکل زیر می‌شود. 

حفاظت از سرریز بافر به عنوان تغییری در کامپایلر اجرا می‌شود. به این ترتیب ، برای محافظت می‌توان ساختار داده را در قاب پشته تغییر داد. این دقیقاً در سیستم‌هایی مانند ProPolice رخ می‌دهد . متغیرهای خودکار تابع بالا را بخواهند صفحاتی دوباره مرتب با خیال راحت تر است: آرایه‌ها  و  برای اولین بار در قاب پشته اختصاص داده، که مکان صحیح و اشاره گر عدد صحیح   قبل از آنها در حافظه است. بنابراین قاب پشته می‌شود (b...)(a...)(d..)(c.........)(CTLI)(RETA) از آنجا که انتقال CTLI یا RETA بدون تغییر سورس کد غیرممکن است، روش دیگری قابل استفاده می‌باشد. یک قطعه اطلاعات اضافی به نام canary (CNRY) پس از بافر در فریم پشته قرار می‌گیرد. زمانی که بافر سرریز می‌شود، مقدار canary تغییر می‌کند. بنابراین، برای یک حمله موثر به برنامه، مهاجم باید مقدار canary را دست نخورده باقی بگذارد. فریم پشته بصورت زیر است. (b...)(a...)(d..)(c.........)(CNRY)(CTLI)(RETA) در انتهای هر تابع یک دستوالعمل وجود دارد که اجرای برنامه را از آدرس مشخص شده در RETA ادامه می‌دهد. قبل از اجرای این دستورالعمل، مطمئن می‌شویم که مقدار CNRY تغییر نکرده باشد. در صورتی که این تست با خطا روبرو شود، اجرای برنامه سریعاً متوقف می‌شود. در اصل، هر دو حمله عمدی و اشکالات برنامه نویسی سهوی منجر به قطع برنامه می‌شوند.

روش canary قبل از هر اختصاص پویای بافر و بعد از هر جداسازی پویای بافر یکسری دستورالعمل سربار اضافه می‌کند. سربار تولید شده در این روش قابل توجه نیست. تا زمانی که این مقدار تغییر نکند برنامه کار می‌کند. اگر مهاجم بداند canary وجود دارد، مقدار آن چیست و موقعیت آن کجاست، می‌تواند مقدار آن را بدون تغییر نگه دارد. معمولاً این کار دشوار است.

موقعیت قرارگیری مقدار canary در پیاده‌سازی مشخص می‌شود، اما همیشه بین بافر و داده‌های محافظت‌شده قرار دارد. موقعیت و طول‌های متفاوت دارای مزایایی نیز هستند.

جستارهای وابسته 
 مقدار سنتینل (که نباید با مقدار canary اشتباه گرفته شود) 
 یکپارچگی کنترل جریان 
 تصادفی کردن طرح فضای آدرس 
 محافظت از فضای اجرایی 
 اشکال زدایی حافظه 
 PaX 
 تجزیه و تحلیل کد استاتیک

منابع 
<references group="" responsive="0">

پیوند به بیرون 
 مجموعه مقالات اجلاس سران GCC 2003 (PDF) 
 Smashing the Stack for Fun و سود توسط Aleph One 
 خانه رسمی ProPolice 
 صفحه اصلی Immunix StackGuard 
 مقاله اصلی StackGuard در USENIX Security 1998 
 StackGhost: سخت افزار تسهیل محافظت از پشته 
 اجرای پروانه FreeBSD 5.4 و 6.2 
 چهار ترفند مختلف برای دور زدن محافظت از StackShield و StackGuard 
 محافظ سر و صدا پشته

اشکال‌های برنامه‌نویسی
سوءاستفاده امنیتی از رایانه