پاکسازی ایمن داده های شخصی

  • mwx278
  • C++
  • 100 بازدید
  • 1 تشکر
  • 0 نظر

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

آکادمی برنامه نویسان ، برگزار کننده دوره های آموزش برنامه نویسی با استفاده از اساتید مجرب و حرفه ای در سراسر ایران .

[ جهت مشاهده دوره های درحال ثبت نام کلیک کنید ]

ارائه مدارک معتبر آموزشی و ورود به بازار کار .

ممتاز ()

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

پشته

برای شروع ، بیایید مثالی از مقاله فوق که در مورد استفاده از یک متغیر اختصاص داده شده از پشته است ، بحث کنیم.

در اینجا یک قطعه کد وجود دارد که یک رمز عبور دارد:

#include <string>
#include <functional>
#include <iostream>

//Private data
struct PrivateData
{
  size_t m_hash;
  char m_pswd[100];
};

//Function performs some operations on password
void doSmth(PrivateData& data)
{
  std::string s(data.m_pswd);
  std::hash<std::string> hash_fn;

  data.m_hash = hash_fn(s);
}

//Function for password entering and processing
int funcPswd()
{
  PrivateData data;
  std::cin >> data.m_pswd;

  doSmth(data);
  memset(&data, 0, sizeof(PrivateData));
  return 1;
}

int main()
{
  funcPswd();
  return 0;
}</std::string></iostream></functional></string>

این مثال نسبتاً معمولی و کاملاً مصنوعی است.

اگر نسخه اشکال زدایی از آن کد ایجاد کنیم و آن را در اشکال زدایی اجرا کنیم (من از ویژوال استودیو 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 ، حافظه () را دست نخورده می نامند و فراموش می کنند که آنچه می بینند هنوز نسخه ای برای اشکال زدایی است.