پاکسازی ایمن داده های شخصی
دوشنبه 7 بهمن 1398ما اغلب باید داده های خصوصی را در برنامه ها ، به عنوان مثال رمزهای عبور ، کلیدهای مخفی و مشتقات آنها ذخیره کنیم و معمولاً ما باید پس از استفاده از آنها ، ردیابی آنها را در حافظه پاک کنیم تا یک متجاوز بالقوه نتواند به این داده ها دسترسی پیدا کند. در این مقاله ما بحث خواهیم کرد که چرا شما نمی توانید داده های خصوصی را با استفاده ازعملکرد ممتاز () پاک کنید.
ممتاز ()
شما ممکن است مقاله را در مورد آسیب پذیری در برنامه هایی که از حافظه () برای پاک کردن حافظه استفاده می شود ، خوانده باشید. با این حال ، این مقاله تمام سناریوهای ممکن برای استفاده نادرست از ممتاز را پوشش نمی دهد . شما ممکن است نه تنها در پاک کردن بافرهای اختصاص داده شده از پشته بلکه با پاک کردن بافرهای اختصاص داده شده پویا نیز مشکل داشته باشید.
پشته
برای شروع ، بیایید مثالی از مقاله فوق که در مورد استفاده از یک متغیر اختصاص داده شده از پشته است ، بحث کنیم.
در اینجا یک قطعه کد وجود دارد که یک رمز عبور دارد:
این مثال نسبتاً معمولی و کاملاً مصنوعی است.
اگر نسخه اشکال زدایی از آن کد ایجاد کنیم و آن را در اشکال زدایی اجرا کنیم (من از ویژوال استودیو 2015 استفاده کردم) ، خواهیم دید که به خوبی کار می کند: پسورد و مقدار هش محاسبه شده آن پس از استفاده از بین می روند.
بیایید نگاهی به نسخه مونتاژ کننده کد خود در debugger ویژوال استودیو بیندازیم:
doSmth(data);
000000013F3072BF lea rcx,[data]
000000013F3072C3 call doSmth (013F30153Ch)
memset(&data, 0, sizeof(PrivateData));
000000013F3072C8 mov r8d,70h
000000013F3072CE xor edx,edx
000000013F3072D0 lea rcx,[data]
000000013F3072D4 call memset (013F301352h)
return 1;
000000013F3072D9 mov eax,1
....
ما تابع فراخوانی () را مشاهده می کنیم ، که داده های خصوصی را پس از استفاده پاک می کند.
ما می توانیم در اینجا متوقف شود ، اما ما به کار خود ادامه خواهیم داد و سعی خواهیم کرد نسخه نسخه بهینه سازی شده را بسازیم. اکنون ، این همان چیزی است که در debugger می بینیم:
....
000000013F7A1035 call
std::operator>><><char> > (013F7A18B0h)
000000013F7A103A lea rcx,[rsp+20h]
000000013F7A103F call doSmth (013F7A1170h)
return 0;
000000013F7A1044 xor eax,eax
.... </char>
تمام دستورالعمل های مرتبط با تماس با عملکرد ممتاز () حذف شده اند. کامپایلر فرض می کند که دیگر نیازی به فراخوانی داده های پاک کننده تابع نیست زیرا آنها دیگر استفاده نمی شوند. این یک خطا نیست؛ این یک انتخاب قانونی کامپایلر است. از نظر زبانی ، تماس ممتاز () لازم نیست زیرا از بافر در برنامه استفاده نمی شود ، بنابراین حذف این تماس نمی تواند بر روی رفتار آن تأثیر بگذارد. بنابراین ، داده های خصوصی ما ناشناخته باقی می مانند ، و این بسیار بد است.
پشته
حالا بیایید عمیق تر حفاری کنیم. بیایید ببینیم چه اتفاقی می افتد هنگامی که آنها را با استفاده از عملکرد malloc یا اپراتور جدید در حافظه پویا اختصاص می دهیم .
بیایید کد قبلی خود را برای کار با malloc اصلاح کنیم :
#include <string>
#include <functional>
#include <iostream>
struct PrivateData
{
size_t m_hash;
char m_pswd[100];
};
void doSmth(PrivateData& data)
{
std::string s(data.m_pswd);
std::hash<std::string> hash_fn;
data.m_hash = hash_fn(s);
}
int funcPswd()
{
PrivateData* data = (PrivateData*)malloc(sizeof(PrivateData));
std::cin >> data->m_pswd;
doSmth(*data);
memset(data, 0, sizeof(PrivateData));
free(data);
return 1;
}
int main()
{
funcPswd();
return 0;
}</std::string></iostream></functional></string>
ما نسخه آزمایشی را آزمایش خواهیم کرد زیرا نسخه اشکال زدایی تمام تماسهایی را که می خواهیم در آنجا داشته باشند ، دارد. پس از گردآوری آن در Visual Studio 2015 ، کد اسمبلر زیر را دریافت می کنیم:
....
000000013FBB1021 mov rcx,
qword ptr [__imp_std::cin (013FBB30D8h)]
000000013FBB1028 mov rbx,rax
000000013FBB102B lea rdx,[rax+8]
000000013FBB102F call
std::operator>><><char> > (013FBB18B0h)
000000013FBB1034 mov rcx,rbx
000000013FBB1037 call doSmth (013FBB1170h)
000000013FBB103C xor edx,edx
000000013FBB103E mov rcx,rbx
000000013FBB1041 lea r8d,[rdx+70h]
000000013FBB1045 call memset (013FBB2A2Eh)
000000013FBB104A mov rcx,rbx
000000013FBB104D call qword ptr [__imp_free (013FBB3170h)]
return 0;
000000013FBB1053 xor eax,eax
.... </char>
ویژوال استودیو این بار خوب عمل کرده است: داده ها را طبق برنامه ریزی پاک می کند. اما در مورد سایر کامپایلرها چطور؟ بیایید gcc ، نسخه 5.2.1 و clang ، نسخه 3.7.0 را امتحان کنیم.
من کد خود را کمی برای gcc و clang اصلاح کرده ام و مقداری کد برای چاپ محتویات حافظه اختصاص داده شده قبل و بعد از پاکسازی اضافه کرده ام. من محتویات بلوک را که اشاره گر بعد از آزاد کردن حافظه به آن اشاره می کند چاپ می کنم ، اما نباید آن را در برنامه های واقعی انجام دهید زیرا هرگز نمی دانید چگونه برنامه پاسخ خواهد داد. با این حال ، در این آزمایش ، من از آزادی استفاده می کنم تا از این تکنیک استفاده کنم.
....
#include "string.h"
....
size_t len = strlen(data->m_pswd);
for (int i = 0; i < len;="" ++i)="" printf("%c",="" data-="">m_pswd[i]);
printf("| %zu \n", data->m_hash);
memset(data, 0, sizeof(PrivateData));
free(data);
for (int i = 0; i < len;="" ++i)="" printf("%c",="" data-="">m_pswd[i]);
printf("| %zu \n", data->m_hash);
....
حال بخشی از کد اسمبلر تولید شده توسط کامپایلر gcc در اینجا آمده است :
movq (%r12), %rsi
movl $.LC2, %edi
xorl %eax, %eax
call printf
movq %r12, %rdi
call free
تابع چاپ ( printf ) پس از تماس با تابع free () در حالی که تماس به عملکرد ممتست () از بین رفته است ، دنبال می شود. اگر کد را اجرا کنیم و یک رمز ورود دلخواه وارد کنیم (به عنوان مثال "MyTopSecret") ، پیام زیر را می بینید که روی صفحه چاپ می شود:
MyTopSecret | 7882334103340833743
MyTopSecret | 0
هش تغییر کرده است. من حدس می زنم این یک اثر جانبی از کار مدیر حافظه است. در مورد رمزعبور "MyTopSecret" ، در حافظه دست نخورده باقی می ماند.
بیایید بررسی کنیم که چگونه با چنگال کار می کند :
movq (%r14), %rsi
movl $.L.str.1, %edi
xorl %eax, %eax
callq printf
movq %r14, %rdi
callq free
درست مانند مورد قبلی ، کامپایلر تصمیم می گیرد تا تماس را به عملکرد ممتاز () حذف کند. این همان چیزی است که خروجی چاپی به نظر می رسد:
MyTopSecret | 7882334103340833743
MyTopSecret | 0
بنابراین ، هر دو gcc و clang تصمیم گرفتند کد ما را بهینه کنند. از آنجا که حافظه پس از فراخوانی عملکرد ممتاز () آزاد می شود ، کامپایلرها این تماس را بی ربطتلقی می کنند و آن را حذف می کنند.
همانطور که آزمایشات ما نشان می دهد ، کامپایلرها تمایل دارند به منظور بهینه سازی کار با هر دو پشته و حافظه پویا برنامه ، حافظه () را حذف کنند .
سرانجام ، خواهیم دید که کامپایلرها هنگام اختصاص حافظه با استفاده از اپراتور جدید چگونه واکنش نشان می دهند .
دوباره تغییر کد:
#include <string>
#include <functional>
#include <iostream>
#include "string.h"
struct PrivateData
{
size_t m_hash;
char m_pswd[100];
};
void doSmth(PrivateData& data)
{
std::string s(data.m_pswd);
std::hash<std::string> hash_fn;
data.m_hash = hash_fn(s);
}
int funcPswd()
{
PrivateData* data = new PrivateData();
std::cin >> data->m_pswd;
doSmth(*data);
memset(data, 0, sizeof(PrivateData));
delete data;
return 1;
}
int main()
{
funcPswd();
return 0;
}</std::string></iostream></functional></string>
ویژوال استودیو حافظه را مطابق پیش بینی پاک می کند:
000000013FEB1044 call doSmth (013FEB1180h)
000000013FEB1049 xor edx,edx
000000013FEB104B mov rcx,rbx
000000013FEB104E lea r8d,[rdx+70h]
000000013FEB1052 call memset (013FEB2A3Eh)
000000013FEB1057 mov edx,70h
000000013FEB105C mov rcx,rbx
000000013FEB105F call operator delete (013FEB1BA8h)
return 0;
000000013FEB1064 xor eax,eax
شورای همکاری خلیج فارس کامپایلر تصمیم به ترک تابع تمیز کردن، بیش از حد:
call printf
movq %r13, %rdi
movq %rbp, %rcx
xorl %eax, %eax
andq $-8, %rdi
movq $0, 0(%rbp)
movq $0, 104(%rbp)
subq %rdi, %rcx
addl $112, %ecx
shrl $3, %ecx
rep stosq
movq %rbp, %rdi
call _ZdlPv
تولید چاپ شده بر این اساس تغییر کرده است. داده هایی که وارد کردیم دیگر وجود ندارد:
MyTopSecret | 7882334103340833743
| 0
اما در مورد clang ، تصمیم گرفت که کد ما را در این حالت نیز بهینه کند و عملکرد "غیر ضروری" را قطع کند:
movq (%r14), %rsi
movl $.L.str.1, %edi
xorl %eax, %eax
callq printf
movq %r14, %rdi
callq _ZdlPv
بیایید محتوای حافظه را چاپ کنیم:
MyTopSecret | 7882334103340833743
MyTopSecret | 0
رمز عبور باقی می ماند و منتظر سرقت هستید.
بیایید جمع بندی کنیم ما فهمیدیم که یک کامپایلر بهینه سازی شده ممکن است بدون توجه به چه نوع حافظه ای از پشته یا پویا ، تماس به عملکرد ممتاز را حذف کند . اگرچه ویژوال استودیو هنگام استفاده از حافظه پویا در آزمون ما تماس های ممتاز را حذف نکرد ، اما نمی توانید انتظار داشته باشید که همیشه با کد واقعی زندگی اینگونه رفتار کند. اثر مضر ممکن است خود را با دیگر سوئیچ های تلفیقی نشان دهد. آنچه از تحقیقات کوچک ما نتیجه می گیرد این است که برای پاک کردن داده های شخصی نمی توان به عملکرد ممتاز ()تکیه کرد .
بنابراین ، راهی بهتر برای پاک کردن آنها چیست؟
شما باید از توابع پاک کننده ویژه حافظه استفاده کنید ، که هنگام بهینه سازی کد توسط کامپایلر قابل حذف نیست.
به عنوان مثال در ویژوال استودیو می توانید از RtlSecureZeroMemory استفاده کنید . با شروع C11 ، عملکرد memset_s نیز موجود است. علاوه بر این ، در صورت لزوم می توانید عملکرد ایمن خود را پیاده سازی کنید. نمونه ها و راهنماهای زیادی را می توان در سراسر وب یافت. در اینجا برخی از آنها آورده شده است.
راه حل شماره 1 .
errno_t memset_s(void *v, rsize_t smax, int c, rsize_t n) {
if (v == NULL) return EINVAL;
if (smax > RSIZE_MAX) return EINVAL;
if (n > smax) return EINVAL;
volatile unsigned char *p = v;
while (smax-- && n--) {
*p++ = c;
}
return 0;
}
راه حل شماره 2 :
void secure_zero(void *s, size_t n)
{
volatile char *p = s;
while (n--) *p++ = 0;
}
برخی از برنامه نویسان حتی فراتر رفته و کارکردهایی ایجاد می کنند که آرایه را با مقادیر شبه تصادفی پر می کند و زمان اجرای متفاوتی برای جلوگیری از حملات بر اساس اندازه گیری زمان دارد. اجرای این موارد را می توان در وب نیز یافت.
نتیجه
آنالایزر استاتیک PVS-Studio می تواند خطاهای مربوط به پاکسازی داده ها را که در اینجا مورد بحث قرار گرفته است ، تشخیص دهد و از سیگنال V597 برای نشان دادن مشکل استفاده می کند. این مقاله به عنوان توضیحی گسترده درباره اهمیت این تشخیص ، نوشته شده است. متأسفانه ، بسیاری از برنامه نویسان تمایل دارند تصور کنند که آنالایزر کد آنها را "انتخاب می کند" و در واقع چیزی برای نگرانی وجود ندارد. خوب ، به این دلیل است که آنها هنگام مشاهده کد در debugger ، حافظه () را دست نخورده می نامند و فراموش می کنند که آنچه می بینند هنوز نسخه ای برای اشکال زدایی است.
- C++
- 1k بازدید
- 1 تشکر