1。蠕虫简介2。缓冲区溢出3。缓冲区溢出的例子4。缓冲区溢出的危害5.6.计算机内存的排列computer7越界访问的后果。7.1堆栈随机化7.2检测堆栈是否损坏7.3限制可执行代码区域8.小结蠕虫病毒是一种利用Unix系统缺陷进行攻击的常见病毒。缓冲区溢出的一个常见后果是,黑客利用程序在函数调用时的返回地址,将存放该地址的指针准确指向计算机中存放攻击代码的位置,从而导致程序异常中止。为了防止严重后果,计算机会使用堆栈随机化,使用金丝雀值检查来破坏堆栈,限制代码可执行区域,尽可能避免被攻击。虽然现代计算机已经可以“智能”查错,但我们还是要养成良好的编程习惯,尽量避免编写有漏洞的代码,以节省宝贵的时间!1.蠕虫简介在网络上复制和传播的代码,通常无需人为干预。蠕虫侵入并完全控制一台计算机后,以该计算机为宿主扫描感染其他计算机。当这些新的感染蠕虫病毒的计算机被控制后,蠕虫病毒会继续扫描感染以这些计算机为宿主的其他计算机,这种行为会一直持续下去。蠕虫就是利用这种递归的方式进行传播,按照指数增长的规律分布自己,然后及时控制越来越多的计算机。2、缓冲区溢出缓冲区溢出是指当计算机向缓冲区中填充的数据位超过缓冲区本身的容量时,溢出的数据被覆盖在合法的数据上。理想的情况是:程序会检查数据长度,不允许输入超过缓冲区长度的字符。但是大多数程序都假定数据长度总是与分配的存储空间相匹配,这就造成了缓冲区溢出的隐患。操作系统使用的缓冲区也称为“堆栈”。指令在各个操作进程之间临时存放在“栈”中,“栈”也存在缓冲区溢出。3.缓冲区溢出示例voidcho(){charbuf[4];/*buf被故意设小了*/gets(buf);puts(buf);}voidcall_echo(){echo();}反汇编如下:/*echo*/000000000040069c:40069c:4883ec18sub$0x18,%rsp/*0X18==24,分配24字节内存。计算机会多分配一些给绑定冲区*/4006a0:4889e7mov%rsp,%rdiffe4qda34006a8::4889e7mov%RSP,%rdi4006ab:e850feffffcallqcallqcallq4005004006b0:4883c418Add$0x18,%RSP4006B4:C3RETQ/5CALL_ECHOB4:C3RETQ/*$0.BB5:c3retq/4006B5:/4006B5:/4006B5:/4006B5:/4006B5:/4006B5:/4006B5:e8d9ffffffcallq40069c4006c3:4883c408add$0x8,%rsp4006c7:c3retq在此示例中,我们故意将buf设置得较小。运行程序,我们在命令行中输入012345678901234567890123,程序会立即报错:Segmentationfault。要理解为什么会报错,我们需要分析反汇编,了解它在内存中是如何分布的。具体如下图所示:如下图,计算机给buf分配了24字节的空间,还有20字节还没有使用。此时,echo函数已准备好被调用,其返回地址被压入堆栈。当我们输入“01234567890123456789012”时,缓冲区已经溢出,但是程序的运行状态并没有被破坏。当我们输入:“012345678901234567890123”。缓冲区溢出,返回地址损坏,程序返回0x0400600。这样,程序就跳转到了计算机中的其他内存位置,很可能这块内存已经被占用了。跳转修改了原始值,因此程序将中止。黑客可以利用该漏洞将程序准确跳转到木马存放的位置(如nopsled技术),然后执行木马程序对我们的电脑造成破坏。4、缓冲区溢出的危害缓冲区溢出可以执行未经授权的指令,甚至获得系统权限,进而进行各种非法操作。第一个缓冲区溢出攻击——莫里斯蠕虫,发生在20年前,造成全球6000多台网络服务器瘫痪。在目前的网络和分布式系统安全中,被广泛使用的50%以上都是缓冲区溢出。最著名的例子是1988年利用fingerd漏洞的蠕虫。在缓冲区溢出中,最危险的是堆栈溢出。因为入侵者可以在函数返回时利用栈溢出改变返回程序的地址,使其跳转到任意地址。危害有两种,一种是程序崩溃导致拒绝服务,另一种是跳转执行一段恶意代码,比如得到一个shell,然后为所欲为。5、内存在计算机中的布局内存在计算机中的布局如下,从上到下分别是共享库、栈、堆、数据段、代码段。各段作用简述如下:共享库:共享库以.so结尾。(so==shareobject)程序在链接的时候,不会像静态库那样把使用函数的代码复制过来,只是做一些标记。然后当程序开始运行时,动态加载需要的模块。因此,应用程序在运行时仍然需要共享库的支持。从共享库链接的文件比静态库小得多。Stack:堆栈,又称堆栈,是用户为了保存程序而临时创建的变量,即我们函数{}中定义的变量,但不包括static声明的变量。静态是指在数据段中存储变量。另外,在调用函数时,其参数也会被压入调用进程栈,调用结束后,函数的返回值也会被存回栈中,由于先进后出栈的特性,所以栈对于调用场景的保存和恢复特别方便。从这个意义上说,我们可以把栈看成是一块内存区域,用来登记和交换临时数据。在X86-64Linux系统中,栈的大小一般为8M(可以用ulitmit-a命令查看)。堆:堆用于存放进程中动态分配的内存段。它的大小不固定,可以动态扩展或缩小。当进程调用malloc等函数分配内存时,新分配的内存被动态分配到堆中。当内存被free等函数释放时,释放的内存会从堆中移除。堆中存放新的对象,栈中的所有对象都指向堆中。如果栈中指向堆的指针被删除,那么堆中的对象也会被释放(C++需要手动释放)。当然,现在的面向对象程序都有“垃圾收集机制”,定期清理堆中无用的对象。数据段:数据段通常是一块内存区域,用于存放程序中初始化为非零的全局变量和静态变量,属于静态内存分配。直观的理解就是C语言程序中的全局变量(注意:全局变量是程序的数据,局部变量不是程序的数据,只能看作是函数的数据)代码段:代码段通常用来存放一段程序执行的代码区域。这部分区域的大小在程序运行之前就已经确定了。通常,这个内存区域是只读的,有些架构也允许可写。以下只读常量变量也可能包含在代码段中,例如字符串常量。让我们举个例子看看代码的各个部分在计算机中是如何排列的。#include#includecharbig_array[1L<<24];/*16MB*/charhuge_array[1L<<31];/*2GB*/intglobal=0;intuseless(){return0;}intmain(){void*phuge1,*psmall2,*phuge3,*psmall4;intlocal=0;phuge1=malloc(1L<<28);/*256MB*/pssmall2=malloc(1L<<8);/*256B*/phuge3=malloc(1L<<32);/*4GB*/pssmall4=malloc(1L<<8);/*256B*/}上面代码中,程序中变量在内存中的排列如下如图所示。根据颜色可以一一对应。由于局部变量存放在栈区,四个指针变量使用malloc分配空间,所以存放在堆上,big_array和huge_array这两个数组存放在data段,main和main的其他部分无用的函数存放在代码段中。6.计算机越界访问的后果我们再看一个例子,看看越界访问内存会造成什么后果。typedefstruct{inta[2];doubled;}struct_t;doublefun(inti){volatilestruct_ts;s.d=3.14;s.a[i]=1073741824;/*可能越界*/returns.d;}intmain(){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));return0;}printresult如下所示:fun(0):3.14fun(1):3.14fun(2):3.1399998664856fun(3):2.00000061035156fun(6):Segmentationfault在上面的程序中,我们定义了一个结构体,其中一个数组包含两个整数值,d是一个双精度浮点数。在函数fun中,fun函数根据传入的参数i初始化a数组,显然i的值只能是0和1。在fun函数中,d的值也设置为3.14。当我们将0和1传递给fun函数时,可以打印出正确的结果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时,修改对应内存的值。原来这块内存可能存放了其他用来维持程序运行的内容,而且已经分配了内存。因此,我们的程序会报Segmentationfault错误。7.三种避免缓冲区溢出的方法为了向系统中插入攻击代码,攻击者需要同时插入代码和指向代码的指针。该指针也是攻击字符串的一部分。生成这个指针需要知道放置字符串的栈地址。以前,程序的栈地址是很容易预测的。对于运行相同程序和操作系统版本的所有系统,堆栈的位置在机器之间是相当恒定的。因此,如果攻击者能够确定普通Web服务器所使用的堆栈空间,他就可以设计出一种适用于多台机器的攻击。7.1栈随机化栈随机化的思想使得每次程序运行时栈的位置都会发生变化。因此,即使多台机器运行相同的代码,它们的栈地址也不尽相同。实现方式是:程序启动时,在栈上分配一块0到n字节之间随机大小的空间,例如使用分配函数alloca在栈上分配指定字节数的空间。程序并没有使用这个空间,但是每次执行程序都会导致后续的栈位置发生变化。分配的范围n必须足够大以获得足够的堆栈地址变化,但又要足够小以免在程序中浪费太多空间。intmain(){longlocal;printf("localat%p\n",&local);return0;}这段代码简单地打印了主函数中局部变量的地址。在32位Linux上运行此代码10,000次,此地址从0xff7fc59c到0xffffd09c变化,范围约为在64位Linux机器上运行,这个地址范围从0x7fff0001b698到0x7ffffffaa4a8,范围大约是.事实上,一个优秀的黑客专家可以使用蛮力来破坏堆栈的随机化。对于32位机器,我们可以通过枚举一个地址来猜测栈的地址。对于64位机器,我们需要枚举时间。这样,堆栈的随机化降低了病毒或蠕虫的传播速度,但不能提供完全的安全性。7.2检测堆栈损坏??计算机的第二道防线是检测堆栈何时损坏的能力。我们在echo函数例子中看到,当访问缓冲区越界时,会破坏程序的运行状态。在C中,没有可靠的方法来防止越界写入数组。但是,我们可以尝试在越界写入发生时检测到它,以免造成任何有害后果。GCC在生成的代码中添加了堆栈保护机制来检测缓冲区越界。其思想是在栈帧中的任意一个本地缓冲区和栈状态之间存储一个特殊的金丝雀值,如下图所示:这个金丝雀值,也称为哨兵值,是每次程序运行时随机生成的,因此,攻击者很难猜测这个哨兵值。在恢复寄存器的状态并从函数返回之前,程序检查金丝雀值是否被函数的操作或函数调用的函数更改。如果是,则程序异常中止。英国矿山养殖金丝雀的历史始于1911年左右,当时矿山工作条件恶劣,矿工下矿时常冒着中毒生命危险。后来,JohnScottHaldane在对一氧化碳进行了一些研究之后,开始推荐在煤矿中使用金丝雀来检测一氧化碳和其他有毒气体。金丝雀极易受到有毒气体的伤害,因为它们通常在高空飞行,需要吸入大量空气才能吸入足够的氧气。因此,与老鼠或其他容易携带的动物相比,金丝雀会吸入更多的空气和其中可能含有的有毒物质。这样一来,一旦金丝雀出事,矿工们很快就会意识到矿井中有毒气体浓度过高,危在旦夕,及时撤离。GCC会尝试确定函数是否容易受到堆栈溢出攻击,并自动插入此类溢出检测。其实对于之前的堆栈溢出显示,我们可以使用命令行选项“-fno-stack-protector”来阻止GCC产生这样的代码。使用该选项编译echo函数时(允许使用栈保护),得到如下汇编代码//voidechosubq$24,%rspAllocate24bytesonstackmovq%fs:40,%raxRetrievecanarymovq%rax,8(%rsp)Storeonstackxorl%eax,%eaxZerooutregister//从内存中读取一个值movq%rsp,%rdiComputebufas%rspcallgetsCallgetsmovq‰rsp,%rdiComputebufas%rspcallputsCallputsmovq8(%rsp),%raxRetrievecanaryxorq%fs:40,%raxComparetostoredvalue//函数会比较存入的值stackpositionwithgoldCanaryvaluecomparisonje.L9If=,gotookcall__stack_chk_failStackcorrupted.L9addq$24,%rspDeallocatestackspaceret这个版本的函数从内存中读取一个值(第4行),然后存入栈中相对于%偏移量为8的位置那个地方。命令参数的每个fs:40表示使用段寻址从内存中读取canary值。段寻址可以追溯到80286寻址,在现代系统上运行的程序中很少见到。将金丝雀值存储在标记为只读的特殊段中,以便攻击者无法覆盖存储的金丝雀值。在恢复寄存器的状态并返回之前,该函数将存储在堆栈位置的值与canary值(通过第10行的xorq指令)进行比较。如果两个数字相同,则xorq指令将得到0,函数将以正常方式完成。非零值表示堆栈上的金丝雀值已被修改,代码调用错误处理例程。堆栈保护可以很好地防止缓冲区溢出攻击破坏存储在程序堆栈中的状态。一般只会造成很小的性能损失。7.3限制可执行代码区域最后的手段是消除攻击者向系统中插入可执行代码的能力。一种方法是限制哪些内存区域可以容纳可执行代码。在一个典型的程序中,只有保存编译器生成的代码的内存部分需要是可执行的。其他部分可以限制为只读和写。许多系统具有三种访问类型:读取(从内存中读取数据)、写入(将数据存储到内存中)和执行(将内存内容视为机器级代码)。以前,x86架构将读取和执行访问控制组合到一个1位标志中,因此任何标记为可读的页面也可以执行。栈必须是可读可写的,所以栈上的字节也是可执行的。已经实施了许多机制来限制某些页面可读但不可执行,但是这些机制通常会导致严重的性能损失。8.结论计算机提供了很多方法来弥补我们错误造成的严重后果,但最重要的是我们尽量减少错误。比如gets、strcpy等函数,我们应该换成fgets、strncpy等。在数组中,我们可以将数组的索引声明为size_t,本质上是防止它传递负数。另外,可以在访问数组前加上num小于ARRAY_MAX的语句来检查数组的上界。总之,要养成良好的编程习惯,可以节省很多宝贵的时间。同时在文末推荐两本相关书籍如下。代码百科(第二版)高质量编程指南本文参考:《深入理解计算机系统》https://baike.baidu.com/item/%E7%BC%93%E5%86%B2%E5%8C%BA%E6%BA%A2%E5%87%BA/678453?fr=aladdin#reference-[1]-36638-wraphttps://baike.baidu.com/item/%E8%A0%95%E8%99%AB%E7%97%85%E6%AF%92/4094075?fr=aladdinhttps://zhuanlan.zhihu.com/p/185792677本文转载自微信公众号《嵌入式与Linux那些事儿》,可以可通过以下二维码关注访问。转载本文请联系嵌入式和Linux那些东西公众号。