推荐视频:
90分钟了解Linux显存构架,numa的优势,slab的实现,vmalloc原理
显存泄露的3个解决方案与原理实现,晓得一个可以轻松应对开发
c/c++linux服务器开发学习地址:C/C++Linux服务器开发/后台构架师【零声教育】-学习视频教程-腾讯课堂
蠕虫病毒是一种常见的借助Unix系统中的缺点来进行功击的病毒。缓冲区溢出一个常见的后果是:黑客借助函数调用过程中程序的返回地址,将储存这块地址的表针精准指向计算机中储存功击代码的位置,导致程序异常终止。为了避免发生严重的后果,计算机会采用栈随机化,借助金丝雀值检测破坏栈,限制代码可执行区域等方式来尽量避开被功击。其实,现代计算机早已可以“智能”查错了,而且我们还是要养成良好的编程习惯,尽量避开写出有漏洞的代码,以节约宝贵的时间!
1.蠕虫病毒简介
蠕虫是一种可以自我复制的代码,但是通过网路传播,一般无需人为干预才能传播。蠕虫病毒入侵并完全控制一台计算机以后,都会把这台机器作为寄主,因而扫描并感染其他计算机。当这种新的被蠕虫入侵的计算机被控制以后,蠕虫会以这种计算机为寄主继续扫描并感染其他计算机,这种行为会仍然延续下去。蠕虫使用这些递归的方式进行传播,根据指数下降的规律分布自己,因而及时控制越来越多的计算机。
2.缓冲区溢出
缓冲区溢出是指计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上。理想的情况是:程序会检测数据宽度,但是并不容许输入超过缓冲区宽度的字符。并且绝大多数程序就会假定数据宽度总是与所分配的存储空间相匹配,这就为缓冲区溢出埋下隐患。操作系统所使用的缓冲区,又被称为“堆栈”,在各个操作进程之间,指令会被临时储存在“堆栈”当中,“堆栈”也会出现缓冲区溢出。
3.缓冲区溢出举例
void echo()
{
char buf[4]; /*buf故意设置很小*/
gets(buf);
puts(buf);
}
void call_echo()
{
echo();
}
反汇编如下:
/*echo*/
000000000040069c :
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
4006a8::48 89 e7 mov %rsp,%rdi
4006ab:e8 50 fe ff ff callq callq 400500
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
4006c3:48 83 c4 08 add $0x8,%rsp
4006c7:c3 retq
在这个事例中,我们故意把buf设置的很小。运行该程序,我们在命令行中输入23,程序立刻都会报错:Segmentationfault。
要想明白为何会报错,我们须要通过剖析反汇编来了解其在显存是怎样分布的。具体如右图所示:
如右图所示,此时计算机为buf分配了24字节空间,其中20字节还未使用。
此时,打算调用echo函数,将其返回地址压栈。
当我们输入“123456789012"时,缓冲区早已溢出,然而并没有破坏程序的运行状态。
当我们输入:“23"。缓冲区溢出,返回地址被破坏,程序返回0x0400600。
这样程序就跳转到了计算机中其他显存的位置,很大可能这块显存早已被使用。跳转更改了原先的值,所以程序都会终止运行。
黑客可以借助这个漏洞,将程序精准跳转到其储存木马的位置(如nopsled技术)linux内核打印调用栈,之后才会执行木马程序,对我们的计算机导致破坏。
4.缓冲区溢出的害处
缓冲区溢出可以执行非授权指令,甚至可以取得系统特权,因而进行各类非法操作。第一个缓冲区溢出功击--Morris蠕虫,发生在二六年前,它曾引起了全世界6000多台网路服务器截瘫。
在当前网路与分布式系统安全中,被广泛借助的50%以上都是缓冲区溢出,其中最知名的事例是1988年借助fingerd漏洞的蠕虫。而缓冲区溢出中,最为危险的是堆栈溢出。由于入侵者可以借助堆栈溢出,在函数返回时改变返回程序的地址,让其跳转到任意地址。带来的害处有两种,一种是程序崩溃造成拒绝服务,另外一种就是跳转而且执行一段恶意代码,例如得到shell,之后为所欲为。
【文章福利】需要C/C++Linux服务器构架师学习资料加群812855908(资料包括C/C++,Linux,golang技术,内核,Nginx,ZeroMQ,MySQLlinux伊甸园,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,解释器,DPDK,ffmpeg等)
5.显存在计算机中的排布方法
显存在计算机中的排布方法如下,从上到下依次为共享库,栈,堆,数据段,代码段。各个段的作用简介如下:
共享库:共享库以.so结尾.(so==shareobject)在程序的链接时侯并不像静态库那样在拷贝使用函数的代码,而只是作些标记。之后在程序开始启动运行的时侯,动态地加载所需模块。所以,应用程序在运行的时侯依然须要共享库的支持。共享库链接下来的文件比静态库要小得多。
栈:栈又称堆栈,是用户储存程序临时创建的变量,也就是我们函数{}中定义的变量,但不包括static申明的变量,static意味着在数据段中储存变量。
除此之外,在函数被调用时,其参数也会被压入发起调用的进程栈中,但是待到调用结束后,函数的返回值也会被储存回栈中,因为栈的先进后出特征,所以栈非常便捷拿来保存、恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存,交换临时数据的显存区。在X86-64Linux系统中,栈的大小通常为8M(用ulitmit-a命令可以查看)。
堆:堆是拿来储存进程中被动态分配的显存段,它的大小并不固定,可动态扩张或削减。当进程调用malloc等函数分配显存时,新分配的显存就被动态分配到堆上,当借助free等函数释放显存时,被释放的显存从堆中被剔除。
堆储存new下来的对象,栈上面所有对象都是在堆上面有指向的。如果栈里指向堆的表针被删掉,堆里的对象也要释放(C++须要自动释放)。其实现今面向对象程序都有'垃圾回收机制',会定期的把堆里没用的对象清理出去。
数据段:数据段一般拿来储存程序中已初始化的全局变量和已初始化为非0的静态变量的一块显存区域,属于静态显存分配。直观理解就是C语言程序中的全局变量(注意:全局变量才算是程序的数据,局部变量不算程序的数据,只能算是函数的数据)
代码段:代码段一般拿来储存程序执行代码的一块区域。这部份区域的大小在程序运行前就早已确定了,一般这块显存区域属于只读,有些构架也容许可写,在代码段中也有可能包含以下只读的常数变量,比如字符串常量等。
下边举个事例来看下代码中各个部份在计算机中是怎样排布的。
#include
#include
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*/
}
上述代码中,程序中的各个变量在显存的排布方法如右图所示。按照颜色可以一一对应上去。因为了local变量储存在栈区,四个表针变量使用了malloc分配了空间,所以储存在堆上,两个字段big_array,huge_array储存在数据段,main,useless函数的其他部份储存在代码段中。
6.计算机中越界访问的后果
下边再看一个反例,看下越界访问显存会有哪些结果。
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):%lfn",fun(0));
printf("fun(1):%lfn",fun(1));
printf("fun(2):%lfn",fun(2));
printf("fun(3):%lfn",fun(3));
printf("fun(6):%lfn",fun(6));
return 0;
}
复印结果如下所示:
fun(0):3.14
fun(1):3.14
fun(2):3.1399998664856
fun(3):2.00000061035156
fun(6):Segmentation fault
在前面的程序中,我们定义了一个结构体linux内核打印调用栈,其中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时,就更改了对应的这块显存的值。原先这块显存可能储存了其他用于维持程序运行的内容,并且是早已分配的显存。所以,我们程序都会报出Segmentationfault的错误。
7.防止缓冲区溢出的三种方式
为了在系统中插入功击代码linux 版本,功击者既要插入代码,也要插入指向这段代码的表针。这个表针也是功击字符串的一部份。形成这个表针须要晓得这个字符串放置的栈地址。在过去,程序的栈地址十分容易预测。对于所有运行同样程序和操作系统版本的系统来说,在不同的机器之间,栈的位置是相当固定的。因而,假如功击者可以确定一个常见的Web服务器所使用的栈空间,就可以设计一个在许多机器上都能施行的功击。
7.1栈随机化
栈随机化的思想促使栈的位置在程序每次运行时都有变化。因而,虽然许多机器都运行同样的代码,它们的栈地址都是不同的。实现的方法是:程序开始时,在栈上分配一段0~n字节之间的随机大小的空间,比如,使用分配函数alloca在栈上分配指定字节数目的空间。程序不使用这段空间,而且它会造成程序每次执行时后续的栈位置发生了变化。分配的范围n必须足够大,能够获得足够多的栈地址变化,而且又要足够小,不至于浪费程序太多的空间。
int main()
{
long local;
printf("local at %pn",&local);
return 0;
}
这段代码只是简单地复印出main函数中局部变量的地址。在32位Linux上运行这段代码10000次,这个地址的变化范围为0xffxffxff77fcfc5959c到0xffffdxffffd0909c,范围大小大概是。在64位Linux机器上运行,这个地址的变化范围为0xx77ffffff00010001bb698698到0x7ffffffaa4a8,范围大小大概是。
虽然,一个好的黑客专家,可以使用暴力破坏栈的随机化。对于32位的机器,我们枚举个地址能够猜下来栈的地址。对于64位的机器,我们须要枚举次。这么看来,栈的随机化增加了病毒或则蠕虫的传播速率,并且也不能提供完全的安全保障。
7.2检查栈是否被破坏
计算机的第二道防线是才能测量到何时栈早已被破坏。我们在echo函数示例中听到,当访问缓冲区越界时,会破坏程序的运行状态。在C语言中,没有可靠的方式来避免对链表的越界写。并且,我们才能在发生了越界写的时侯,在引起任何有害结果之前,尝试测量到它。
GCC在形成的代码中加人了一种栈保护者机制,来测量缓冲区越界。其思想是在栈帧中任何局部缓冲区与栈状态之间储存一个特殊的金丝雀值,如右图所示:
这个金丝雀值,称作为哨兵值,是在程序每次运行时随机形成的,因而,功击者很难猜出这个哨兵值。在恢复寄存器状态和从函数返回之前,程序检测这个金丝雀值是否被该函数的某个操作或则该函数调用的某个函数的某个操作改变了。假如是的,这么程序异常终止。
日本煤矿繁殖金丝雀的历史大概起始1911年。当时,煤矿工作条件差,矿工在下井时经常冒着中毒的生命危险。后来,约翰·斯科特·霍尔丹(JohnScottHaldane)在经过对一氧化碳一番研究以后,开始推荐在矿山中使用金丝雀检查一氧化碳和其他有毒二氧化碳。金丝雀的特征是极易受有毒二氧化碳的侵犯,由于它们平时飞行高度很高,须要吸入大量空气吸入足够氢气。因而,相比于老鼠或其他容易携带的植物,金丝雀会吸入更多的空气以及空气中可能含有的有毒物质。这样,一旦金丝雀出了事,矿工还会迅速意识到煤矿中的有毒二氧化碳含量过低,她们早已身陷危险之中,因而及时撤出。
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指明金丝雀值是用段轮询从显存中读入的。段轮询机制可以溯源到80286的轮询,而在现代系统上运行的程序中早已甚少看见了。将金丝雀值储存在一个特殊的段中,标记为只读,这样功击者就不能覆盖储存金丝雀值。在恢复寄存器状态和返回前,函数将储存在栈位置处的值与金丝雀值做比较(通过第10行的xorq指令)。假如两个数相同,xorq指令都会得到0,函数会依照正常的方法完成。非零的值表明栈上的金丝雀值被更改过,这么代码都会调用一个错误处理类库。
栈保护挺好地避免了缓冲区溢出功击破坏储存在程序栈上的状态。通常只会带来很小的性能损失。
7.3限制可执行代码区域
最后一招是去除功击者向系统中插入可执行代码的能力。一种方式是限制什么显存区域才能储存可执行代码。在典型的程序中,只有保存编译器形成的代码的那部份显存才须要是可执行的。其他部份可以被限制为只容许读和写。
许多系统都有三种访问方式:读(从显存读数据)、写(储存数据到显存)和执行(将显存的内容看作机器级代码)。曾经,x86体系结构将读和执行访问控制合并成一个1位的标志,这样任何被标记为可读的页也都是可执行的。栈必须是既可读又可写的,因此栈上的字节也都是可执行的。早已实现的好多机制,才能限制一些页是可读并且不可执行的,但是这种机制一般会带来严重的性能损失。
8.总结
计算机提供了多种方法来填补我们错事可能形成的严重后果,而且最关键的还是我们尽量减低错事。
比如,对于gets,strcpy等函数我们应替换为fgets,strncpy等。在链表中,我们可以将字段的索引申明为size_t类型,从根本上避免它传递正数。据悉,还可以在访问链表前来加上num大于ARRAY_MAX句子来检测链表的下界。其实,要养成良好的编程习惯,这样可以节约好多宝贵的时间。
文章评论