-->
当前位置:首页 > 实验 > 正文内容

内存管理安全实验

Luz4年前 (2020-12-11)实验3356

实验目的

  • 理解内存管理安全

  • 了解常见的C&C++内存管理错误

  • 掌握安全的内存管理操作

实验原理

1.动态内存存储
  一些C和C++的程序需要对可变数量的数据元素进行操作,这就要求使用动态内存来管理这种数据。绝大多数非安全关键型的应用程序都使用动态存储分配。

2.内存管理安全
  内存管理一直是难以捉摸的编程缺陷飞安全缺陷和漏洞的来源之一。例如,将同一块内存释放两次就是一个编程缺陷,这种行为会导致可利用的漏洞。并非只有覆写校中内存的缓冲区溢出才是危险的,堆中发生缓冲区溢出同样会造成危险的后果。

3.常见的C内存管理错误
  C程序中的动态内存管理,可以非常复杂,从而容易出现缺陷。常见的与内存管理相关的编程缺陷包括: 初始化错误、未检查返回值、对空指针或无效指针解引用、引用己释放的内存、对同一块内存释放多次、内存泄漏和零长度分配。

4.常见的C++内存管理错误
  C++程序的动态内存管理非常复杂,因此容易出现缺陷。常见的与内存管理相关的编程缺陷,包括未能正确处理分配失败、解引用空指针、写入已经释放的内存、对相同的内存释放多次、不当配对的内存管理函数、未区分标量和数组以及分配函数使用不当。

实验步骤

步骤1:禁止引用未初始化的内存

  有些函数如malloc分配出来的内存是没有初始化的,可以使用memset进行清零,或者使用calloc进行内存分配,calloc分配的内存是清零的。当然,如果后面需要对申请的内存进行全部赋值,就不要清零了,但要确保内存被引用前是被初始化的。此外,分配内存初始化,可以消除之前可能存放在内存中的敏感信息,避免敏感信息的泄露。

  1.1 错误示例:如下代码没有对malloc的y内存进行初始化,所以功能不正确。

/* return y = Ax */

int * Noncompliant(int **A, int *x, int n)

{

   if(n <= 0)

       return NULL;

   int *y = (int*)malloc (n * sizeof (int));

   if(y == NULL)

       return NULL;

   int i, j;

   for (i = 0; i < n; ++i)

   {

       for (j = 0; j < n; ++j)

       {

           y[i] += A[i][j] * x[j];

       }

   }

   return y;

}

/*...申请的内存使用后free...*/

  推荐做法:使用memset对分配出来的内存清零。

int * Compliant(int **A, int *x, int n)

{

   if(n <= 0)

       return NULL;

   int *y = (int*)malloc(n * sizeof (int));

   if(y == NULL)

       return NULL;

   int i, j;

   memset (y, 0, n * sizeof(int)); //【修改】确保内存被初始化后才被引用

   for (i = 0; i < n; ++i)

   {

       for (j = 0; j < n; ++j)

       {

           y[i] += A[i][j] * x[j];

       }

   }

   return y;

}

/*...申请的内存使用后free...*/

步骤2:禁止访问已经释放的内存

  2.1 访问已经释放的内存,是很危险的行为,主要分为两种情况:
  (1)堆内存:一块内存释放了,归还内存池以后,就不应该再访问。因为这块内存可能已经被其他部分代码申请走,内容可能已经被修改;直接修改释放的内存,可能会导致其他使用该内存的功能不正常;读也不能保证数据就是释放之前写入的值。在一定的情况下,可以被利用执行恶意的代码。即使是对空指针的解引用,也可能导致任意代码执行漏洞。如果黑客事先对内存0地址内容进行恶意的构造,解引用后会指向黑客指定的地址,执行任意代码。
  (2)栈内存:在函数执行时,函数内局部变量的存储单元都可以在栈上创建,函数执行完毕结束时这些存储单元自动释放。如果返回这些已释放的存储单元的地址(栈地址),可能导致程序崩溃或恶意代码被利用。
  错误示例1:解引用一个已经释放了内存的指针,会导致未定义的行为。

typedef struct _tagNode

{

   int    value;

   struct _tagNode * next;

}Node;

Node *  Noncompliant()

{

   Node * head = (Node *)malloc(Node);

   if (head==NULL)

   {

       /* ...do something... */

       return NULL;

   }

   /* ...do something... */

   free(head);

   /* ...do something... */

   head->next = NULL;  //【错误】解引用了已经释放的内存

   return head;

}

  错误示例2:函数中返回的局部变量数据有可能会被覆盖掉,导致未定义的行为。

char *  Noncompliant()

{

   char msg[128];

   /* ...do something... */

   return msg;  //【错误】返回了局部变量

}

步骤3:禁止重复释放内存
  3.1 重复释放内存(double-free)会导致内存管理器出现问题。重复释放内存在一定情况下,有可能导致"堆溢出"漏洞,可以被用来执行恶意代码,具有很大的安全隐患。
  错误示例:如下代码两次释放了ptr。

void  Noncompliant()

{

   char *ptr = (char*)malloc(size);

   if (ptr)

   {

       /* ...do something... */

       free(ptr);

   }

   /* ...do something... */

   free(ptr); //【错误】有可能出现2次释放内存的错误

}

  推荐做法:申请的内存应该只释放一次。

void  Compliant()

{

   char *ptr = (char*)malloc(size);

   if (ptr)

   {

       /* ...do something... */

       free(ptr);

       ptr = NULL;

   }

   /* ...do something... */

   //【修改】删掉free(ptr)

}

步骤4:必须对指定申请内存大小的整数值进行合法性校验
  4.1 申请内存时没有对指定的内存大小整数作合法性校验,会导致未定义的行为,主要分为两种情况:
  (1)使用 0 字节长度去申请内存的行为是没有定义的,在引用内存申请函数返回的地址时会引发不可预知或不能立即发现的问题。对于可能出现申请0地址的情况,需要增加必要的判断,避免出现这种情况
  (2)使用负数长度去申请内存,负数会被当成一个很大的无符号整数,从而导致因申请内存过大而出现失败,造成拒绝服务。
  错误示例:下列代码进行内存分配时,没有对内存大小整数作合法性校验。

int * Noncompliant(int x)

{

   int i;

   int * y = (int *)malloc( x * sizeof(int));  //未对x进行合法性校验

   for(i=0; i<x; ++i)

   {

       y[i] = i;

   }

   return y;

}

/*...申请的内存使用后free...*/

  推荐做法:调用malloc之前,需要判断malloc的参数是否合法。确保x为整数后才申请内存,否则视为参数无效,不予申请,以避免出现申请过大内存而导致拒绝服务。

int * Compliant(int x)

{

   int i;

   int *y;

   if(x > 0)   //【修改】增加对x进行合法性校验

   {

       y = (int *)malloc( x * sizeof(int));

       if (y == NULL)

           return NULL;

   }

   else

   {

       return NULL;

   }

   for(i=0; i<x; ++i)

   {

       y[i]=i;

   }

   return y;

}

/*...申请的内存使用后free...*/

步骤5:禁止释放非动态申请的内存
  5.1 非动态申请的内存并不是由内存分配器管理的,如果使用free函数对这块内存进行释放,会对内存分配器产生影响,造成拒绝服务。如果黑客能控制非动态申请的内存内容,并对其进行精心的构造,甚至导致程序执行任意代码。
  错误示例:非法释放非动态申请的内存。

void  Noncompliant()

{

   char str[] = "this is a string";

   /* ...do something... */

   free(str);    //【错误】str不是动态申请的内存,因此不能释放

}

  推荐做法:非动态分配的内存不需要释放,把原来释放函数free()去掉。

void  Compliant ()

{

   char str[] = "this is a string";

   /* ...do something... */

   //【修改】删除free(str)

}

步骤6:避免使用alloca函数申请内存
  6.1 POSIX和C99 均未定义 alloca 的行为,在不支持的平台上运行会有未定义的后果,且该函数在栈帧里申请内存,申请的大小可能越过栈的边界而无法预知。
  错误示例:使用了alloca从堆栈分配内存。

void  Noncompliant(char *buff, int len)

{

   int size = len * 3 + 1, i;

   char *ptr = alloca (size), *p; //【不推荐】避免使用alloca函数申请内存

   if (len <= 0)

       return;

   if (ptr == NULL)

       return;

   p = ptr;

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

   {

       p += _snprintf(p, 4, "%02x ", buff[i]);

   }

   *p = NULL;

   printf ("%s", ptr);

}

  推荐做法:alloca函数返回后,使用指向函数局部堆栈内存区也会出现问题,改用malloc从堆分配内存。

void  Compliant(char *buff, int len)

{

   int size = len * 3 + 1, i;

   char *ptr = malloc(size), *p; //【修改】使用malloc代替alloca申请内存

   if (len <= 0)

       return;

   if (ptr == NULL)

       return;

   p = ptr;

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

   {

       p += _snprintf (p, 4, "%02x ", buff[i]);

   }

   *p = NULL;

   printf ("%s", ptr);

   free (ptr);

}

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。