关于c:面试官不讲武德居然让我讲讲蠕虫和金丝雀

36次阅读

共计 9285 个字符,预计需要花费 24 分钟才能阅读完成。

蠕虫病毒是一种常见的利用 Unix 零碎中的毛病来进行攻打的病毒。缓冲区溢出一个常见的结果是:黑客利用函数调用过程中程序的返回地址,将寄存这块地址的指针精准指向计算机中寄存攻打代码的地位,造成程序异样停止。为了避免产生重大的结果,计算机会采纳栈随机化,利用金丝雀值查看毁坏栈,限度代码可执行区域等办法来尽量避免被攻打。尽管,古代计算机曾经能够“智能”查错了,然而咱们还是要养成良好的编程习惯,尽量避免写出有破绽的代码,以节俭贵重的工夫!

蠕虫病毒简介

  蠕虫是一种能够自我复制的代码,并且通过网络流传,通常无需人为干涉就能流传。蠕虫病毒入侵并齐全管制一台计算机之后,就会把这台机器作为宿主,进而扫描并感化其余计算机。当这些新的被蠕虫入侵的计算机被管制之后,蠕虫会以这些计算机为宿主持续扫描并感化其余计算机,这种行为会始终延续下去。蠕虫应用这种递归的办法进行流传,依照指数增长的法则散布本人,进而及时管制越来越多的计算机。(起源百度百科)

缓冲区溢出

  缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区自身的容量,溢出的数据笼罩在非法数据上。现实的状况是:程序会检查数据长度,而且并不容许输出超过缓冲区长度的字符。然而绝大多数程序都会假如数据长度总是与所调配的贮存空间相匹配,这就为缓冲区溢出埋下隐患。操作系统所应用的缓冲区,又被称为“堆栈”,在各个操作过程之间,指令会被长期贮存在“堆栈”当中,“堆栈”也会呈现缓冲区溢出。(起源百度百科)

缓冲区溢出举例

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″ 时,缓冲区曾经溢出,然而并没有毁坏程序的运行状态。

<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)会报错呢?

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

  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 那些事”]

正文完
 0