面试官不讲武德,竟然让我讲讲蠕虫和金丝雀!

蠕虫病毒是一种常见的利用Unix系统中的缺点来进行攻击的病毒。缓冲区溢出一个常见的后果是:黑客利用函数调用过程当中程序的返回地址,将存放这块地址的指针精准指向计算机中存放攻击代码的位置,形成程序异常停止。为了防止发生严重的后果,计算机会采用栈随机化,利用金丝雀值检查破坏栈,限制代码可执行区域等方法来尽可能避免被攻击。虽然,现代计算机已经能够“智能”查错了,可是咱们仍是要养成良好的编程习惯,尽可能避免写出有漏洞的代码,以节省宝贵的时间!

蠕虫病毒简介

  蠕虫是一种能够自我复制的代码,而且经过网络传播,一般无需人为干预就能传播。蠕虫病毒入侵并彻底控制一台计算机以后,就会把这台机器做为宿主,进而扫描并感染其余计算机。当这些新的被蠕虫入侵的计算机被控制以后,蠕虫会以这些计算机为宿主继续扫描并感染其余计算机,这种行为会一直延续下去。蠕虫使用这种递归的方法进行传播,按照指数增加的规律分布本身,进而及时控制愈来愈多的计算机。 (来源百度百科)git

缓冲区溢出

  缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区自己的容量,溢出的数据覆盖在合法数据上。理想的状况是:程序会检查数据长度,并且并不容许输入超过缓冲区长度的字符。可是绝大多数程序都会假设数据长度老是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患。操做系统所使用的缓冲区,又被称为“堆栈”,在各个操做进程之间,指令会被临时储存在“堆栈”当中,“堆栈”也会出现缓冲区溢出。 (来源百度百科)shell

缓冲区溢出举例

void echo()
{
  char buf[4];   /*故意设置很小*/
  gets(buf);
  puts(buf);
}
void call_echo(){
  echo();
}

  反汇编以下:编程

/*echo*/
000000000040069c <echo>: 
40069c:48 83 ec 18         sub $0x18,%rsp  /*0X18 == 24,分配了24字节内存。计算机会多分配一些给缓冲区*/
4006a0:48 89 e7            mov %rsp,%rdi   
4006a3:e8 a5 ff ff ff      callq 40064d <gets>
4006a8::48 89 e7           mov %rsp,%rdi
4006ab:e8 50  fe ff ff     callq callq 400500 <puts@plt>
4006b0:48 83 c4 18         add $0x18,%rsp 
4006b4:c3                  retq
/*call_echo*/
4006b5:48 83  ec 08             sub $0x8,%rsp 
4006b9:b8 00 00 00 00           mov $0x0,%eax
4006be:e8 d9 ff ff ff           callq 40069c <echo>
4006c3:48 83 c4 08              add $0x8,%rsp 
4006c7:c3                       retq

  在这个例子中,咱们故意把buf设置的很小。运行该程序,咱们在命令行中输入012345678901234567890123,程序立马就会报错:Segmentation fault。数组

  要想明白为何会报错,咱们须要经过分析反汇编来了解其在内存是如何分布的。具体以下图所示:安全

  以下图所示,此时计算机为buf分配了24字节空间,其中20字节还未使用。服务器

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/%E7%BC%93%E5%86%B2%E5%8C%BA%E6%BA%A2%E5%87%BA%E4%B8%BE%E4%BE%8Becho_%E8%B0%83%E7%94%A8gets%E4%B9%8B%E5%89%8D.png" alt="image-20201111215122537" style="zoom: 50%;" />网络

  此时,准备调用echo函数,将其返回地址压栈。数据结构

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/%E7%BC%93%E5%86%B2%E5%8C%BA%E6%BA%A2%E5%87%BA%E4%B8%BE%E4%BE%8Becho_%E8%B0%83%E7%94%A8gets%E4%B9%8B%E5%89%8D2.png" alt="image-20201111214702010" style="zoom: 67%;" />架构

  当咱们输入“01234567890123456789012"时,缓冲区已经溢出,可是并无破坏程序的运行状态。less

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/%E7%BC%93%E5%86%B2%E5%8C%BA%E6%BA%A2%E5%87%BA%E4%B8%BE%E4%BE%8Becho_%E8%B0%83%E7%94%A8gets%E4%B9%8B%E5%90%8E.png" alt="image-20201111214811039" style="zoom: 67%;" />

  当咱们输入:“012345678901234567890123"。缓冲区溢出,返回地址被破坏,程序返回 0x0400600。

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/%E7%BC%93%E5%86%B2%E5%8C%BA%E6%BA%A2%E5%87%BA%E4%B8%BE%E4%BE%8Becho_%E8%B0%83%E7%94%A8gets%E4%B9%8B%E5%89%8D%E5%90%8E2.png" alt="image-20201111214914863" style="zoom: 67%;" />

  这样程序就跳转到了计算机中其余内存的位置,很大可能这块内存已经被使用。跳转修改了原来的值,因此程序就会停止运行。

  黑客能够利用这个漏洞,将程序精准跳转到其存放木马的位置,而后就会执行木马程序,对咱们的计算机形成破坏。

缓冲区溢出的危害

  能够利用它执行非受权指令,甚至能够取得系统特权,进而进行各类非法操做。缓冲区溢出攻击有多种英文名称:buffer overflow,buffer overrun,smash the stack,trash the stack,scribble the stack, mangle the stack, memory leak,overrun screw;它们指的都是同一种攻击手段。第一个缓冲区溢出攻击--Morris蠕虫,发生在二十年前,它曾形成了全世界6000多台网络服务器瘫痪。

  在当前网络与分布式系统安全中,被普遍利用的50%以上都是缓冲区溢出,其中最著名的例子是1988年利用fingerd漏洞的蠕虫。而缓冲区溢出中,最为危险的是堆栈溢出,由于入侵者能够利用堆栈溢出,在函数返回时改变返回程序的地址,让其跳转到任意地址,带来的危害一种是程序崩溃致使拒绝服务,另一种就是跳转而且执行一段恶意代码,好比获得shell,而后随心所欲。 (来源百度百科)

内存在计算机中的排布方式

  内存在计算机中的排布方式以下,从上到下依次为共享库,栈,堆,数据段,代码段。各个段的做用简介以下(更详细的内容总结见嵌入式软件开发知识点总结.pdf):

  共享库:共享库以.so结尾.(so==share object)在程序的连接时候并不像静态库那样在拷贝使用函数的代码,而只是做些标记。而后在程序开始启动运行的时候,动态地加载所需模块。因此,应用程序在运行的时候仍然须要共享库的支持。共享库连接出来的文件比静态库要小得多。

  :栈又称堆栈,是用户存放程序临时建立的变量,也就是咱们函数{}中定义的变量但不包括static声明的变量,static意味着在数据段中存放变量。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,而且待到调用结束后,函数的返回值也会被存放回栈中,因为栈的先进后出特色,因此栈特别方便用来保存、恢复调用现场。从这个意义上讲,咱们能够把堆栈当作一个寄存,交换临时数据的内存区。在X86-64 Linux系统中,栈的大小通常为8M(用ulitmit - a命令能够查看)。

  :堆是用来存放进程中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态分配到堆上,当利用free等函数释放内存时,被释放的内存从堆中被剔除。

  堆存放new出来的对象、栈里面全部对象都是在堆里面有指向的、假如栈里指向堆的指针被删除、堆里的对象也要释放(C++须要手动释放)、固然咱们如今好面向对象程序都有'垃圾回收机制'、会按期的把堆里没用的对象清除出去。

  数据段:数据段一般用来存放程序中已初始化的全局变量和已初始化为非0的静态变量的一块内存区域,属于静态内存分配。直观理解就是C语言程序中的全局变量(注意:全局变量才算是程序的数据,局部变量不算程序的数据,只能算是函数的数据

  代码段:代码段一般用来存放程序执行代码的一块区域。这部分区域的大小在程序运行前就已经肯定了,一般这块内存区域属于只读,有些架构也容许可写,在代码段中也有可能包含如下只读的常数变量,例如字符串常量等。程序段为程序代码在内存中映射一个程序能够在内存中有多个副本。

<center>
<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/X86-64_%E5%86%85%E5%AD%98%E7%9A%84%E6%8E%92%E5%B8%83.png" alt="image-20201111151446190" style = "zoom:67%;" />

  下面举个例子来看下代码中各个部分再计算机中是如何排布的。

#include <stdio.h>
#include <stdlib.h>

char big_array[1L<<24];     /*16 MB*/
char huge_array[1L<<31];    /*2 GB*/

int global = 0;

int useless() {return 0;}

int main()
{
  void *phuge1,*psmall2,*phuge3,*psmall4;
  int local = 0;
  phuge1 = malloc(1L<<28);    /*256 MB*/
  psmall2 = malloc(1L<<8);    /*256 B*/
  phuge3 = malloc(1L<<32);    /*4 GB*/
  psmall4 = malloc(1L<<8);    /*256 B*/
  /*some print statements....*/
}

  上述代码中,程序中的各个变量在内存的排布方式以下图所示。根据颜色能够一一对应起来。因为了local变量存放在栈区,四个指针变量使用了malloc分配了空间,因此存放在堆上,两个数组big_array,huge_array存放在数据段,main,useless函数的其余部分存放在代码段中。

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/X86-64_%E4%B8%BE%E4%BE%8B%E5%9C%B0%E5%9D%80%E6%8E%92%E5%B8%83.png" alt="image-20201111153803357" style="zoom: 50%;" />

计算机中越界访问的后果

  下面再看一个例子,看下越界访问内存会有什么结果。

typedef struct 
{
  int a[2];
  double d;
}struct_t;

double fun(int i){

  volatile struct_t s;
  s.d = 3.14;
  s.a[i] = 1073741824;  /*可能越界*/
  return s.d;
}

int main()
{
  printf("fun(0):%lf\n",fun(0));
  printf("fun(1):%lf\n",fun(1));
  printf("fun(2):%lf\n",fun(2));
  printf("fun(3):%lf\n",fun(3));
  printf("fun(6):%lf\n",fun(6));
  return 0; 
}

  打印结果以下所示

fun(0):3.14
fun(1):3.14
fun(2):3.1399998664856
fun(3):2.00000061035156
fun(6):Segmentation fault

  在上面的程序中,咱们定义了一个结构体,其中 a 数组中包含两个整数值,还有 d 一个双精度浮点数。在函数fun中,fun函数根据传入的参数i来初始化a数组。显然,i的值只能为0和1。在fun函数中,同时还设置了d的值为3.14。当咱们给fun函数传入0和1时能够打印出正确的结果3.14。可是当咱们传入2,3,6时,奇怪的现象发生了。为何fun(2)和fun(3)的值会接近3.14,而fun(6)会报错呢?

  要搞清楚这个问题,咱们要明白结构体在内存中是如何存储的,具体以下图所示。

image-20201111165638477

  GCC默认不检查数组越界,除非加编译选项。这也是C的bug之一,越界会修改某些内存的值,得出咱们意想不到的结果。即便有些数据相隔万里,也可能受到影响。当一个系统这几天运行正常时,过几天可能就会崩溃。(若是这个系统是运行在咱们的心脏起搏器,又或者是航天飞行器上,那么这无疑将会形成巨大的损失!)

  如上图所示,对于最下面的两个元素,每一个块表明 4 字节。a数组占用8个字节,d变量占用8字节,d排布在a数组的上方。因此咱们会看到,若是我引用 a[0] 或者 a[1],会按照正常修改该数组的值。可是当我调用 fun(2) 或者 fun(3)时,实际上修改的是这个浮点数 d 的字节。这就是为何咱们打印出来的fun(2)和fun(3)的值如此接近3.14。当输入 6 时,就修改了对应的这块内存的值。原来这块内存可能存储的其余用于维持程序运行的内容,并且是已经分配的内存。所示,咱们程序就会报出Segmentation fault的错误。当咱们理解了数据结构的机器级表示以及它们是如何运行的,处理这些漏洞也就很轻松了。

避免缓冲区溢出的三种方法

  为了在系统中插入攻击代码,攻击者既要插入代码,也要插入指向这段代码的指针。这个指针也是攻击字符串的一部分。产生这个指针须要知道这个字符串放置的栈地址。在过去,程序的栈地址很是容易预测。对于全部运行一样程序和操做系统版本的系统来讲,在不一样的机器之间,栈的位置是至关固定的。所以,若是攻击者能够肯定一个常见的Web服务器所使用的栈空间,就能够设计一个在许多机器上都能实施的攻击。

栈随机化

  栈随机化的思想使得栈的位置在程序每次运行时都有变化。所以,即便许多机器都运行一样的代码,它们的栈地址都是不一样的。实现的方式是:程序开始时,在栈上分配一段0 ~ n字节之间的随机大小的空间,例如,使用分配函数alloca在栈上分配指定字节数量的空间。程序不使用这段空间,可是它会致使程序每次执行时后续的栈位置发生了变化。分配的范围n必须足够大,才能得到足够多的栈地址变化,可是又要足够小,不至于浪费程序太多的空间。

int main(){
    long local;
    printf("local at %p\n",&local);
    return 0;
}

  这段代码只是简单地打印出main函数中局部变量的地址。在32位 Linux上运行这段代码10000次,这个地址的变化范围为0xff7fc59c到0xffffd09c,范围大小大约是${2^{23}}$。在更新一点儿的机器上运行64位 Linux,这个地址的变化范围为0x7fff0001b698到0x7ffffffaa4a8,范围大小大约是 ${2^{32}}$。

  其实,一个好的黑客专家,可使用蛮力破坏栈的随机化。对于32位的机器,咱们枚举${2^{15}} = 32768$个地址就能猜出来栈的地址。对于64位的机器,咱们须要枚举${2^{24}} = 16777216$次。如此看来,栈的随机化下降了病毒或者蠕虫的传播速度,可是也不能提供彻底的安全保障。

检测栈是否被破坏

  计算机的第二道防线是可以检测到什么时候栈已经被破坏。咱们在echo函数示例中看到,当访问缓冲区越界时,会破坏程序的运行状态。在C语言中,没有可靠的方法来防止对数组的越界写。可是,咱们可以在发生了越界写的时候,在形成任何有害结果以前,尝试检测到它。

  GCC在产生的代码中加人了一种栈保护者机制,来检测缓冲区越界。其思想是在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀( canary)值,以下图所示:

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/%E9%87%91%E4%B8%9D%E9%9B%80%E5%80%BC_%E8%B0%83%E7%94%A8gets%E4%B9%8B%E5%89%8D.png" alt="image-20201112085448688" style="zoom: 67%;" />

  这个金丝雀值,也称为哨兵值,是在程序每次运行时随机产生的,所以,攻击者没有简单的办法可以知道它是什么。在恢复寄存器状态和从函数返回以前,程序检查这个金丝雀值是否被该函数的某个操做或者该函数调用的某个函数的某个操做改变了。若是是的,那么程序异常停止。

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/%E9%87%91%E4%B8%9D%E9%9B%80_%E8%B0%83%E7%94%A8gets%E4%B9%8B%E5%90%8E.png" alt="image-20201112085829359" style="zoom: 67%;" />

英国矿井饲养金丝雀的历史大约起始1911年。当时,矿井工做条件差,矿工在下井时时常冒着中毒的生命危险。后来,约翰·斯科特·霍尔丹(John Scott Haldane)在通过对一氧化碳一番研究以后,开始推荐在煤矿中使用金丝雀检测一氧化碳和其余有毒气体。金丝雀的特色是极易受有毒气体的侵害,由于它们日常飞行高度很高,须要吸入大量空气吸入足够氧气。所以,相比于老鼠或其余容易携带的动物,金丝雀会吸入更多的空气以及空气中可能含有的有毒物质。这样,一旦金丝雀出了事,矿工就会迅速意识到矿井中的有毒气体浓度太高,他们已经陷入危险之中,从而及时撤离。

  GCC会试着肯定一个函数是否容易遭受栈溢出攻击,而且自动插入这种溢出检测。实际上,对于前面的栈溢出展现,咱们不得不用命令行选项“-fno- stack- protector”来阻止GCC产生这种代码。当不用这个选项来编译echo函数时,也就是容许使用栈保护,获得下面的汇编代码

/*void echo */
subq $24,%rsp Allocate 24 bytes on stack
movq  %fs:40,%rax  Retrieve canary 
movq %rax,8(%rsp) Store on stack
xorl %eax, %eax Zero out register 
movq %rsp, %rdi  Compute buf as %rsp 
call gets Call gets 
movq ‰rsp,%rdi Compute buf as %rsp
call puts Call puts 
movq 8(%rsp),%rax Retrieve canary 
xorq %fs:40,%rax Compare to stored value
je .L9  If =, goto ok 
call __stack_chk_fail Stack corrupted 
.L9
addq $24,%rsp Deallocate stack space 
ret

  这个版本的函数从内存中读出一个值(第4行),再把它存放在栈中相对于%rsp偏移量为8的地方。指令参数各fs:40指明金丝雀值是用段寻址( segmented addressing)从内存中读入的,段寻址机制能够追溯到80286的寻址,而在现代系统上运行的程序中已经不多见到了。将金丝雀值存放在一个特殊的段中,标志为“只读”,这样攻击者就不能覆盖存储金丝雀值。在恢复寄存器状态和返回前,函数将存储在栈位置处的值与金丝雀值作比较(经过第12行的xorq指令)。若是两个数相同,xorq指令就会获得0,函数会按照正常的方式完成。非零的值代表栈上的金丝雀值被修改过,那么代码就会调用一个错误处理例程。

  栈保护很好地防止了缓冲区溢出攻击破坏存储在程序栈上的状态。一般只会带来很小的性能损失。

限制可执行代码区域

  最后一招是消除攻击者向系统中插入可执行代码的能力。一种方法是限制哪些内存区域可以存放可执行代码。在典型的程序中,只有保存编译器产生的代码的那部份内存才须要是可执行的。其余部分能够被限制为只容许读和写。许多系统容许控制三种访问形式:读(从内存读数据)、写(存储数据到内存)和执行(将内存的内容看做机器级代码)。之前,x86体系结构将读和执行访问控制合并成一个1位的标志,这样任何被标记为可读的页也都是可执行的。栈必须是既可读又可写的,于是栈上的字节也都是可执行的。已经实现的不少机制,可以限制一些页是可读可是不可执行的,然而这些机制一般会带来严重的性能损失。

总结

  计算机提供了多种方式来弥补咱们犯错可能产生的严重后果,可是最关键的仍是咱们尽可能减小犯错。例如,对于gets,strcpy等函数咱们应替换为 fgets,strncpy等。在数组中,咱们能够将数组的索引声明为size_t 类型,从根本上防止它传递负数。此外,还能够在访问数组前来加上num<ARRAY_MAX语句来检查数组的上界。总之,要养成良好的编程习惯,这样能够节省不少宝贵的时间。同时最后也推荐两本相关书籍,代码大全(第二版) 高质量程序设计指南

  养成习惯,先赞后看!若是以为写的不错,欢迎关注,点赞,转发,谢谢!

如遇到排版错乱的问题,能够经过如下连接访问个人CSDN。

**CSDN:[CSDN搜索“嵌入式与Linux那些事”]

相关文章
相关标签/搜索