برنامه نویسی امن با زبان C – رشته ها
عنصر
اصلی در برنامه نویسی امن با زبان های مختلف برنامه نویسی، مستند سازی خوب
و استفاده از استانداردهای قابل اجرا است. استانداردهای کدنویسی، برنامه
نویسان را ترغیب به پیروی از مجموعه ای متحدالشکل از قوانین و راهنماییها
می کند که بر اساس نیازمندی های پروژه و سازمان تعیین شده است، نه بر اساس
سلایق و مهارت های مختلف برنامه نویسان. به محض تعیین استانداردهای مذکور،
می توان از آن به عنوان معیاری برای ارزیابی کدهای منبع، چه به صورت دستی و
چه به صورت اتوماتیک استفاده کرد.
از استانداردهای معروف در این زمینه
می توان به استانداردCERT برای کدنویسی امن اشاره کرد که یک سری از قوانین و
پیشنهادات را برای کد نویسی امن با زبان های برنامه نویسی C، C++ و جاوا
ارائه می دهد. هدف از این قوانین و پیشنهادات، حذف عادت های کدنویسی ناامن و
رفتارهای تعریف نشده است که منجر به آسیب پذیری های قابل سوءاستفاده می
شود. به کارگیری استانداردهای مذکور منجر به تولید سیستم های با کیفیت
بالاتر می شود که در برابر حملات بالقوه، پایدارتر و مقاوم تر هستند.
در مقاله آشنایی با استاندارد CERT برای برنامه نویسی امن،
کلیات استاندارد CERT در زمینه مزبور را توضیح دادیم و در سری مقاله های
برنامه نویسی امن به زبان C به صورت تخصصی تر شیوه برنامه نویسی امن با این
زبان را مورد بررسی قرار می دهیم. قابل ذکر است که در این استاندارد 89
قانون و 134 پیشنهاد برای برنامه نویسی امن با زبان C ارائه شده است که در
این سری مقالات، مهمترین آنها را که در سطح یک قرار دارند، شرح خواهیم داد.
برای کسب اطلاعات بیشتر در مورد سطح بندی قوانین و پیشنهادات به مقاله
"آشنایی با استاندارد CERT برای برنامه نویسی امن" مراجعه فرمایید. در
مقاله قبلی در مورد قوانین و پیشنهادات سطح اول ارائه شده در مورد آرایه ها
و همچنین پیشنهادات سطح اول ارائه شده برای رشته ها صحبت کردیم و در مقاله
حاضر به قوانین ارائه شده سطح اول در مورد رشته ها خواهیم پرداخت.
رشته ها
رشته
ها یکی از مفاهیم پایه در مهندسی نرم افزار هستند، اما در زبان C به صورت
پیش فرض تعریف نشده اند. فرمتی که برای پشتیبانی متغیرهای رشته ای در C
استفاده می شودNull-terminated byte string یا NTBS نامیده می شود. این
نوع، حاوی توالی پیوسته ای از کاراکترها است که اولین و آخرین آنها null
است. زبان برنامه نویسی C رشته های کاراکتری تک بایتی، چند بایتی و Wide را
پشتیبانی می کند. هر دو رشته های کاراکتری تک بایتی و چند بایتی به عنوان
رشته های منتهی به null شناخته می شوند و اصطلاحاً به آنها رشته های
کاراکتری narrow گفته می شود.
رشته ها در زبان برنامه نویسی C همچون
آرایه ای کاراکترها پیاده سازی می شوند و لذا بسیاری از مشکلات آنها همانند
آرایه ها است. در نتیجه، قوانین و پیشنهادات ارائه شده برای آرایه ها در
اینجا نیز باید به کار گرفته شوند. در ادامه به توضیح قوانین ارائه شده در
استاندارد برنامه نویسی امنCERT برای رشته ها می پردازیم.
قوانین
18. قانون STR31-C: از کافی بودن فضای حافظه تخصیص داده شده برای رشته ها و کاراکتر پایانی null اطمینان حاصل کنید.
کپی
کردن داده در یک بافر که به اندازه کافی برای نگهداری داده مذکور بزرگ
نیست، منجر به خطای سرریز بافر می شود. با وجودی که خطای مذکور به رشته های
null-terminated byte (NTBS) محدود نمی شود، سرریز بافر معمولاً در
دستکاری داده های NTBS رخ می دهد. برای پیشگیری از خطای مذکور لازم است از
کافی بودن فضای مقصد برای نگهداری داده های کاراکتری اطمینان حاصل کنید.
برنامه
آسیب پذیری که در زیر آمده حاوی خطایی است که به Off-By-one مشهور است. در
این برنامه، حلقه For داده ها را از src در dest کپی می کند. در اینجا
بایت null پایانی ممکن است به اشتباه بعد از dest نوشته شود زیرا حلقه،
کاراکتر پایانی null را که باید در انتهای dest قرار داده شود، محاسبه
نکرده است.
char src[ARRAY_SIZE];
size_t i;
/* … */
for (i=0; src[i] && (i < sizeof(dest)); i++) {
dest[i] = src[i];
}
dest[i] = ‘’;
/* … */
برای اصلاح کد مذکور، لازم است شرط پایان یافتن حلقه به صورتی تغییر کند تا کاراکتر پایانی null را نیز در نظر گیرد:
char src[ARRAY_SIZE];
size_t i;
/* ... */
for (i=0; src[i] && (i < sizeof(dest)-1); i++) {
dest[i] = src[i];
}
dest[i] = '';
/* ... */
19. قانون STR32-C: استفاده از بایت null-terminate در صورت نیاز.
رشته
های NTBS باید حاوی یک کاراکتر null در آخرین عنصر آرایه و یا قبل از آن
باشند تا بتوان آنها را به طرز صحیحی به عنوان آرگومان توابع دستکاری رشته
ها همچون strcpy() یا strlen() ارسال کرد. این التزام به این خاطر است که
توابع مذکور همچون دیگر توابع تعریف شده در C99، برای مشخص کردن طول یک
رشته، وابسته به وجود کاراکتر null در انتهای رشته هستند. همچنین انواع
NTBS، قبل از اینکه بر روی یک آرایه کاراکتری تکرار شوند که در آن شرط
پایان حلقه به وجود کاراکتر Null در انتهای رشته بستگی دارد، باید حاوی
کاراکتر Null در انتهای رشته باشند، مانند آنچه در حلقه زیر مشاهده می کنید
که یکی از شروط پایان حلقه مشاهده کاراکتر null است:
char ntbs[16];
/* ... */
for (i = 0; i < sizeof(ntbs); ++i) {
if (ntbs[i] == '') break;
/* ... */
}
20. قانون STR33-C: طول رشته های کاراکتری wide را صحیح اندازه بگیرید.
اغلب
ممکن است رشته های کاراکتری wide به صورت نادرست اندازه گیری شوند به این
دلیل که با رشته های narrow و یا رشته های کاراکتری چند بایتی اشتباه گرفته
می شوند. اندازه گیری اشتباه می تواند منجر به خطای سرریز بافر شود.
برای مثال در برنامه زیر از strlen() برای مشخص کردن طول رشته کاراکتری wide استفاده شده است:
wchar_t *wide_str2 = (wchar_t *)malloc(strlen(wide_str1) + 1);
if (wide_str2 == NULL) {
/* Handle Error */
}
/* ... */
free(wide_str2);
wide_str2 = NULL;
تابع strlen() تعداد کاراکترها در یک رشته منتهی به
null را می شمرد، در حالی که کاراکترهای wide حاوی بایت های null هستند، به
خصوص زمانی که از مجموعه کاراکترهای ASCII استفاده می کنند. در نتیجه تابع
strlen() تعداد کاراکترهای قبل از اولین کاراکتر null را بر می گرداند.
برای اصلاح کد مذکور باید از توابع مخصوص رشته های کاراکتری wide استفاده کرد:
wchar_t *wide_str2 = (wchar_t *)malloc(
(wcslen(wide_str1) + 1) * sizeof(wchar_t)
);
if (wide_str2 == NULL) {
/* Handle Error */
}
/* ... */
free(wide_str2);
wide_str2 = NULL;
21. قانون STR35-C: داده ها را از یک منبع با اندازه نامشخص در یک آرایه با اندازه مشخص کپی نکنید.
توابعی
که کپی های نامحدود انجام می دهند، اغلب بر روی منطقی بودن اندازه ورودی
خود تکیه می کنند. ثابت شده است که چنین فرض هایی اشتباه است و منجر به
خطای سرریز بافر می شود. به همین دلیل در استفاده از توابعی که کپی های
نامحدود انجام می دهند باید دقت کافی را مبذول داشت.
استفاده از توابع
strcat() و strcpy() برای کپی کردن یک رشته با اندازه نامشخص در یک بافر با
اندازه محدود یکی از متداول ترین دلایل بروز خطای سرریز بافر و یکی از
منابع اصلی ایجاد آسیب پذیری های امنیتی است. کد آسیب پذیر زیر فرض می کند
که طول آرگومان های ورودی خط فرمان از 4096 بایت فراتر نمی رود و آرگومان
های برنامه را در یک بافر با اندازه ثابت، بدون اینکه احتمال بروز سرریز
بافر را بررسی کند، به هم متصل می کند. فراخوانی strcat() و یا هر تابع
دستکاری رشته دیگر که تلاش می کند ورای اندازه مشخص شده بافر بنویسد، منجر
به رفتارهای تعریف نشده برای برنامه می شود.
char cmdline [4096];
cmdline[0] = '';
for (int i = 1; i < argc; ++i) {
strcat(cmdline, argv [i]);
strcat(cmdline, " ");
}
/* ... */
return 0;
}
در کد اصلاح شده زیر، فرضی در مورد طول حداکثر آرگومان های خط فرمان در نظر گرفته نشده است و به جای آن از ترکیب توابع malloc() و memset() برای اختصاص اتوماتیک فضای حافظه کافی برای همه آرگومان ها استفاده شده است.
size_t bufsize = 0;
size_t buflen = 0;
char* cmdline = NULL;
for (int i = 1; i < argc; ++i) {
const size_t len = strlen(argv[i]);
if (bufsize - buflen <= len) {
bufsize = (bufsize + len) * 2;
cmdline = realloc(cmdline, bufsize);
if (NULL == cmdline)
return 1; /* realloc failure */
}
memcpy(cmdline + buflen, argv[i], len);
buflen += len;
cmdline[buflen++] = ' ';
}
cmdline[buflen] = '';
/* ... */
free(cmdline); return 0;
}
22. قانون STR36-C: برای یک آرایه کاراکتری که با string literal مقداردهی اولیه شده است، حد بالا تعیین نکنید.
زبان
برنامه نویسی C استاندارد، اجازه می دهد یک آرایه متغیر هم توسط یک ایندکس
محدود کننده و هم توسط یک عبارت اولیه تعریف شود. عبارت اولیه همچنین
اندازه آرایه را با توجه به تعداد عناصر مشخص می کند. برای رشته ها، اندازه
آرایه، توسط تعداد کاراکترهای موجود در رشته اولیه به اضافه یک کاراکتر
Null پایانی محاسبه می شود. در زبان C مصطلح است که یک آرایه متغیر از رشته
ها توسط یک عبارت اولیه با اندازه مشخص که مطابق تعداد کاراکترهای موجود
در رشته اولیه است، تعریف شود. با این وجود این مسئله در مورد کاراکترهایی
که با یک بایت null پایان می پذیرند، منجر به بروز آسیب پذیری می شود.
بهترین راهکار برای پیشگیری از آسیب پذیری های مذکور، مشخص نکردن اندازه
رشته در تعریف آرایه است، زیرا کامپایلر به صورت اتوماتیک فضای حافظه کافی
با توجه به کاراکترهای Null پایانی را برای در بر گرفتن کل عبارت رشته ای،
به آن اختصاص می دهد.
در زیر کدی را می بینید که از یک عبارت اولیه رشته ای استفاده می کند که یک کاراکتر بیش از حد تعیین شده دارد:
برای اصلاح کد مذکور به راحتی می توان حدی را برای آرایه مذکور در نظر نگرفت:
23. قانون STR38-C: از توابع مربوط به رشته های کاراکتری wide بر روی رشته های کاراکتری narrow و بالعکس استفاده نکنید.
کاراکترهای
wide اغلب حاوی کاراکترهای null هستند به خصوص وقتی از مجموعه کاراکتر
ASCII استفاده می کنند. در نتیجه استفاده از توابع کاراکترهای narrow که بر
روی کاراکتر پایانی Null تکیه می کنند، منجر به رفتارهای غیرقابل پیش بینی
می شود. همین طور، از توابع کاراکترهای wide نیز نباید بر روی رشته های
کاراکتری narrow که دارای بایت پایانی null هستند، استفاده کرد. استفاده
نادرست از رشته های کاراکتری wide و narrow منجر به خطای سرریز بافر می
شود.