- 1. 蠕虫病毒简介
- 2. 缓冲区溢出
- 3. 缓冲区溢出的例子
- 4. 缓冲区溢出的危害
- 5. 计算机中存在内存的排列模式
- 6. 计算机中越界访问的后果
- 7. 三种避免缓冲区溢出的 ***
- 7.1 栈随机化
- 7.2 检测栈是否损坏
- 7.3 限制可执行代码区域
- 8. 总结
蠕虫病毒是一种常见的利用Unix该系统的缺点是攻击病毒。缓冲区溢出的一个常见后果是:黑客使用函数调用程序的返回地址,准确地将存储地址的指针指向计算机中存储攻击代码的位置,导致程序异常停止。为了防止严重后果,计算机将使用栈随机化,使用金丝雀值检查损坏栈,限制代码可执行区域,尽量避免攻击。虽然,现代计算机已经可以了“智能”检查错误,但我们仍然需要养成良好的编程习惯,尽量避免写漏洞代码,以节省宝贵的时间!
1. 蠕虫病毒简介
蠕虫是一种可以自我复制的代码,通过 *** 传播,通常不需要人为干预。蠕虫病毒入侵并完全控制计算机后,将该机器作为宿主,然后扫描并感染其他计算机。当这些被蠕虫入侵的新计算机被控制时,蠕虫将继续扫描并感染其他计算机作为宿主,这种行为将继续下去。蠕虫使用这种递归法传播,根据指数增长的规律分布自己,然后及时控制越来越多的计算机。
2. 缓冲区溢出
缓冲区溢出是指计算机在向缓冲区填充数据位数时超过缓冲区本身的容量,溢出的数据覆盖在合法数据上。理想情况是,程序将检查数据长度,不允许输入超过缓冲区长度的字符。但绝大多数程序假设数据长度总是与分配的存储空间相匹配,这为缓冲区溢出埋下了隐患。操作系统中使用的缓冲区也被称为“堆栈”,指令将在每个操作过程之间临时存储“堆栈”当中,“堆栈”缓冲区也会溢出。
3. 缓冲区溢出的例子
反汇编如下:
在这个例子中,我们故意buf设置很小。运行此程序时,我们将012345678901234567890123输入命令行,程序将立即报错:Segmentation fault。
要了解为什么会报错,需要通过分析反汇编来了解其内存是如何分布的。具体如下图所示:
如下图所示,此时计算机为buf24字节空间分布,其中20字节尚未使用。
此时,准备调用echo函数,将其返回地址压栈。
当我们输入“0123456789012345678 9012"缓冲区已经溢出,但并没有破坏程序的运行状态。
当我们输入:“012345678901234567 890123"。缓冲区溢出,返回地址损坏,程序返回 0x0400600。
这样,程序跳转到计算机中其他内存的位置,很可能已经使用了。跳转修改了原始值,因此程序将停止运行。
黑客可以利用这个漏洞准确地将程序跳转到存储木马的位置(例如nop sled技术),然后执行木马程序,损坏我们的计算机。
4. 缓冲区溢出的危害
缓冲区溢出可以执行非授权指令,甚至获得系统特权,然后进行各种非法操作。之一个缓冲区溢出攻击--Morris蠕虫发生在20年前,它瘫痪了世界上6000多台 *** 服务器。
在目前的 *** 和分布式系统安全中,超过50%的缓冲区溢出,其中最著名的例子是1988年fingerd漏洞蠕虫。缓冲区溢出最危险的是堆栈溢出。因为入侵者可以在函数返回时使用堆栈溢出来改变返回程序的地址,并将其跳转到任何地址。有两种危害,一种是程序崩溃导致拒绝服务,另一种是跳转并执行恶意代码,如获取shell,然后为所欲为。
5. 内存在计算机中的排布方式
计算机中内存的排列方式如下,从上到下依次为共享库、栈、堆、数据段、代码段。各段的功能简介如下:
共享库:共享库.so结尾.(so==share object)当程序链接时,它不像静态库那样复制使用函数的代码,而只是做一些标记。然后,当程序开始运行时,动态地加载所需的模块。因此,应用程序仍然需要共享库的支持。共享库链接的文件远小于静态库。
栈:栈,又称栈,是用户存储程序临时创建的变量,即我们函数{}中定义的变量,但不包括static声明的变量,static变量存储在数据段中。
此外,当函数被调用时,其参数也会被压入启动调用过程中的栈中,调用结束后,函数的返回值也会存储在栈中。由于栈的先进特点,栈特别方便保存和恢复调用现场。从这个意义上说,我们可以把栈看作是一个存储和交换临时数据的内存区域。X86-64 Linux在系统中,栈的大小一般为8M(用ulitmit - a可以查看命令)。
堆:堆是存储过程中动态分布的内存段,其尺寸不固定,可动态扩展或减少。当过程调用时malloc当函数分配内存时,新分配的内存被动态分配到堆上,当使用时free当函数释放内存时,从堆中删除释放的内存。
堆存放new堆栈中的所有物体都在堆栈中指向。如果堆栈中指向堆的指针被删除,堆栈中的对象也应释放(C 需要手动释放)。当然,现在面向对象的程序都有垃圾回收机制,会定期清除堆里没用的对象。
数据段:数据段通常用于存储程序中初始化的全球变量和初始化为非0的静态变量的内存区域,属于静态内存分配。直观的理解是C语言程序中的全局变量(注:全局变量是程序数据,局部变量不是程序数据,只是函数数据)
代码段:代码段通常用于存储程序执行代码的区域。这部分区域的大小在程序运行前就已经确定了。通常,这个内存区域是只读的,一些架构也可以写。代码段还可能包含以下常数变量,如字符串常量。
以下是代码各部分在计算机中的例子如何排布的。
上述代码中,程序中的各个变量在内存的排布方式如下图所示。根据颜色可以一一对应起来。由于了local变量存储在栈区,四个指针变量使用malloc分配空间 所以存放在堆上,两个数组big_ array,huge_array存储在数据段中,main,useless代码段中存储了函数的其他部分。
6. 计算机中越界访问的后果
再看一个例子,看看越界访问内存的结果。
打印结果如下:
在上面的程序中,我们定义了一个结构体,其中 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默认情况下,不要检查数组越界(除非添加编译选项)。越界将修改一些内存值,以获得意想不到的结果。即使一些数据相距数千英里,它也可能受到影响。当系统在最近几天正常运行时,它可能会在几天内崩溃。(如果该系统运行在我们的起搏器或太空飞机上,它无疑会造成巨大的损失!
如上图所示,对于以下两个元素,每个块代表 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的错误。
7. 避免缓冲区溢出的三种 ***
为了在系统中插入攻击代码,攻击者不仅要插入代码,还要插入指向该代码的指针。该指针也是攻击字符串的一部分。要生成这个指针,你需要知道字符串中的堆栈地址。在过去,程序的堆栈地址非常容易预测。对于所有操作相同程序和操作系统版本的系统,堆栈的位置相当固定在不同的机器之间。因此,如果攻击者能确定一个常见的系统Web服务器使用的栈空间可以设计多机器上设计攻击。
7.1 栈随机化
栈的随机化理念改变了程序每次运行时栈的位置。因此,即使许多机器运行相同的代码,它们的栈地址也是不同的。实现的 *** 是在程序开始时在栈上分配0 ~ n例如,使用分配函数之间的随机大小空间alloca在堆栈上分配指定字节数量的空间。程序不使用这个空间,但它会改变程序每次执行时的堆栈位置。分配范围n为了获得足够大的栈地址变化,但又要足够小,以免浪费太多的程序空间。
这个代码只是简单打印出来的main函数中局部变量的地址。32位 Linux代码上运行1万次,地址变更范围为0xff7fc59c到0xffffd09c,大小在64位 。Linux在机器上运行时,地址的变化范围是0x7fff0001b698到0x7ffffffaa4a8,范围大小约是 。
事实上,一个好的黑客专家可以利用暴力破坏栈的随机化。对于32台机器,我们可以通过列出地址来猜测栈的地址。我们需要列出64台机器的数量。这样,栈的随机化降低了病毒或蠕虫的传播速度,但不能提供完全的安全保障。
7.2 检测栈是否损坏
??计算机的第二道防线是检测栈何时被破坏。echo函数示例显示,当访问缓冲区越界时,程序的运行状态将被破坏。C在语言中,没有可靠的 *** 来防止数组的越界写。然而,在任何有害结果之前,我们可以尝试检测到它。
GCC在生成的代码中增加了一种栈保护机制来检测缓冲区的越界。其思想是在栈帧中的任何局部缓冲区和栈状态之间存储一个特殊的金丝雀值,如下图所示:
这个金丝雀值,又称哨兵值,是在程序每次运行时随机产生的。因此,攻击者很难猜出哨兵值。在恢复寄存器状态并返回函数之前,程序检查金丝雀值是否被函数的某个操作或函数调用的某个函数的某个操作所改变。如果是这样,程序将异常停止。
英国矿井饲养金丝雀的历史始于1911年左右。当时矿井工作条件差,矿工下井时经常冒生命危险。后来,约翰·斯科特·霍尔丹(John Scott Haldane)经过对一氧化碳的研究,建议在煤矿中使用金丝雀检测一氧化碳和其他有毒气体。金丝雀的特点是容易受到有毒气体的侵害,因为它们通常飞得很高,需要吸入大量的空气来吸入足够的氧气。因此,与老鼠或其他容易携带的动物相比,金丝雀会吸入更多的空气和空气中可能含有的有毒物质。这样,一旦金丝雀出了问题,矿工们就会迅速意识到矿井中的有毒气体浓度过高,并陷入危险,以便及时疏散。
GCC试着确定一个函数是否容易被栈溢出攻击,并自动插入这种溢出检测。事实上,我们可以使用命令行选项来显示前面的栈溢出“-fno- stack- protector”来阻止GCC生成此代码。当编译此选项时echo函数(允许使用栈保护)获得以下汇编代码
这个版本的函数从内存中读取一个值(第4行),然后存储在堆栈中,相对于%rsp偏移量为8的地方。每个指令参数的指令参数fs:40说明金丝雀值是通过段搜索从内存中读取的。段搜索机制可以追溯到80286年的搜索,在现代系统运行的程序中很少见到。将金丝雀值存储在一个特殊的段落中,标记为只读取,使攻击者无法覆盖存储金丝雀值。在恢复存储状态和返回之前,函数将存储在堆栈位置的值与金丝雀值进行比较(通过第10行)xorq指令)。如果两个数字相同,xorq指令为0,函数以正常方式完成。非零值表明栈上的金丝雀值已经修改,因此代码将调用错误的处理程序。
栈保护可以很好地防止缓冲区溢出攻击,破坏存储在程序栈上的状态。一般只会带来很小的性能损失。
7.3 限制可执行代码区域
最后一步是消除攻击者将可执行代码插入系统的能力。一种 *** 是限制存储可执行代码的内存区域。在典型程序中,只有保存编译器生成代码的内存才需要执行。其他部分可以限制为只允许读写。
许多系统有三种访问形式:阅读(从内存阅读数据)、写作(从存储数据到内存)和执行(将内存内容视为机器级代码)。x86系统结构将读取和执行访问控制合并为一位标志,使任何标记为可读页面也可执行。堆栈必须是可读的和可写的,所以堆栈上的字节也是可执行的。许多已经实现的机制可以限制可读但不可执行的页面,但这些机制通常会带来严重的性能损失。
8. 总结
计算机提供了各种 *** 来弥补我们可能犯错误的严重后果,但最重要的是尽量减少错误。
例如,对于gets,strcpy我们应该取代 fgets,strncpy等等。在数组中,我们可以将数组的索引声明为size_t类型,从根本上防止它传递负数。此外,还可以添加访问数组num小于ARRAY_MAX 语句检查数组的上界。简言之,养成良好的编程习惯可以节省大量宝贵的时间。同时,最后推荐以下两本相关书籍。
代码大全(第二版)
高质量的程序设计指南
本文参考:对计算机系统的深入理解 https://baike.baidu.com/item/缓冲区溢出/678453?fr=aladdin#reference-[1]-36638-wrap https://baike.baidu.com/item/蠕虫病毒/4094075fr=aladdin https://zhuanlan.zhihu.com/p/185792677
本文转载自微信公众号「嵌入式与Linux那些事」,请注意以下二维码。请联系嵌入式和Linux微信官方账号。