【零声教育】Linux内存架构分析之1.蠕虫病毒

推荐视频:

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 设置为小。运行程序,我们在命令行输入012345678901234567890123,程序马上会报错:Segmentation fault。

要了解为什么会报错,我们需要分析反汇编来了解它在内存中是如何分布的。具体如下图所示:

如下图所示,此时计算机已经为buf分配了24字节的空间,其中20字节还没有使用。

此时c语言中怎么动态分配空间,您已准备好调用 echo 函数并将其返回地址压入堆栈。

当我们输入“0123456789012345678 9012”时,缓冲区已经溢出,但程序的运行状态并没有被破坏。

当我们输入:“012345678901234567 890123”。缓冲区溢出,返回地址损坏,程序返回 0x0400600。

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

黑客可以利用该漏洞将程序跳转到木马存放的位置(如nop sled技术),然后执行木马程序对我们的电脑造成破坏。

4. 缓冲区溢出的危险

缓冲区溢出可以执行未经授权的指令,甚至获得系统权限来执行各种非法操作。第一次缓冲区溢出攻击,莫里斯蠕虫,发生在 20 年前,它导致全球 6,000 多台 Web 服务器瘫痪。

在当前的网络和分布式系统安全中,超过 50% 被广泛利用的是缓冲区溢出,其中最著名的例子是 1988 年利用fingerd 漏洞的蠕虫。在缓冲区溢出中,最危险的是堆栈溢出。因为入侵者可以利用堆栈溢出并在函数返回时更改返回例程的地址,从而允许它跳转到任意地址。危害有两种,一种是程序崩溃导致拒绝服务,另一种是跳转执行一段恶意代码,比如得到一个shell,然后为所欲为。

【文章福利】C/C++ Linux服务器架构师必学资料加群812855908(资料包括C/C++、Linux、golang技术、内核、Nginx、ZeroMQ、MySQL、Redis、fastdfs、MongoDB、ZK、流媒体、CDN 、P2P、K8S、Docker、TCP/IP、协程、DPDK、ffmpeg等)

5. 内存在计算机中是如何排列的

计算机中内存的排列顺序是,从上到下依次是共享库、栈、堆、数据段、代码段。各段的作用描述如下:

共享库:共享库以 .so 结尾。(so==share object) 程序链接时,不会像静态库那样复制使用该函数的代码,只是做一些标记。然后当程序开始运行时,动态加载所需的模块。因此,应用程序在运行时仍然需要共享库的支持。从共享库链接的文件比静态库小得多。

栈:栈,也称栈,是用户临时创建的用来存放程序的变量,也就是我们函数{}中定义的变量,但不包括static声明的变量,也就是说变量存储在数据段中。

另外,函数在被调用时,其参数也会被压入调用进程的栈中,调用结束后,函数的返回值也会被存回栈中。特点,所以堆栈保存和恢复调用场景特别方便。从这个意义上说,我们可以把栈看成一个寄存器,一个用来交换临时数据的内存区域。在X86-64 Linux系统中,栈的大小一般为8M(可以用ulitmit -a命令查看)。

堆:堆用于存储进程中动态分配的内存段。它的大小不是固定的,可以动态扩大或缩小。当进程调用malloc等函数分配内存时,新分配的内存会动态分配给堆。当使用free等函数释放内存时,释放的内存会从堆中移除。

堆存储新的对象,堆栈中的所有对象在堆中都有指针。如果堆栈中指向堆的指针被删除,堆中的对象也被释放(C++需要手动释放)。当然,面向对象的程序现在有一个“垃圾收集机制”,可以定期清除堆中无用的对象。

数据段:数据段通常用于存放程序中已初始化的全局变量和已初始化为非零静态变量的内存区域。它属于静态内存分配。直观理解就是C语言程序中的全局变量(注意:全局变量只是程序的数据,局部变量不是程序的数据,只是函数的数据)

图片[1]-【零声教育】Linux内存架构分析之1.蠕虫病毒-老王博客

代码段:代码段通常用于存放程序执行代码的一个区域。这部分区域的大小在程序运行之前已经确定。通常,这个内存区域是只读的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*/
}

上述代码中,程序中的各个变量在内存中的排列如下图所示。可以根据颜色一一搭配。由于局部变量存放在栈区,四个指针变量使用malloc分配空间,所以存放在堆上,数据段存放两个数组big_array、huge_array,其他部分主要和没用的函数存储在代码段中。.

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):%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

在上面的程序中,我们定义了一个结构体,其中包含一个包含两个整数值和一个双精度浮点数的数组。在函数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 时,修改对应内存的值。事实证明,这块内存可能存储了其他用于保持程序运行的内容,并且它已经被分配了内存。因此,我们的程序会报一个 Segmentation fault 错误。

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

为了将攻击代码插入系统,攻击者必须同时插入代码和指向该代码的指针。该指针也是攻击字符串的一部分。生成此指针需要知道放置字符串的堆栈地址。过去,程序的堆栈地址是非常可预测的。对于运行相同程序和操作系统版本的所有系统,堆栈位置在机器之间是相当固定的。因此,如果攻击者可以确定一个普通 Web 服务器使用的堆栈空间,他就可以设计一种可以在多台机器上进行的攻击。

7.1 堆栈随机化

栈随机化的思想使得每次程序运行时栈的位置都会发生变化。因此,即使许多机器运行相同的代码,它们的堆栈地址也是不同的。实现的方法是:在程序开始时,在栈上分配一个 0 到 n 字节之间的随机大小的空间,例如使用分配函数 alloca 在栈上分配指定字节数的空间。堆。程序不使用这个空间,但它会导致后续的堆栈位置在每次程序执行时发生变化。分配的范围 n 必须足够大,才能获得足够的堆栈地址更改,但又要足够小,以免在程序中浪费太多空间。

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

这段代码只是在主函数中打印出局部变量的地址。在 32 位 Linux 上运行此代码 10,000 次,此地址从 0xff7fc59c 到 0xffffd09c 不等,范围大小约为。在 64 位 Linux 机器上运行,此地址从 0x7ffff0001b698 到 0x7ffffffaa4a8 不等,范围大小约为。

实际上,一个好的黑客可以使用暴力破解堆栈的随机性。对于 32 位机器,我们可以通过枚举一个地址来猜测堆栈的地址。对于 64 位机器,我们需要枚举时间。这样,堆栈的随机化减少了病毒或蠕虫的传播,但不提供完全的安全性。

7.2 检查堆栈是否损坏

计算机的第二道防线是检测堆栈何时损坏的能力。我们在 echo 函数示例中看到,越界访问缓冲区时,程序的运行状态被破坏。在 C 中,没有可靠的方法来防止越界写入数组。但是,我们可以尝试检测何时发生越界写入,以免造成任何有害结果。

GCC 在生成的代码中添加了堆栈保护机制来检测缓冲区越界。想法是在堆栈帧中的任何本地缓冲区和堆栈状态之间存储一个特殊的金丝雀值,如下图所示:

这个金丝雀值,也称为哨兵值,是每次程序运行时随机生成的,所以攻击者很难猜到这个哨兵值。在恢复寄存器状态并从函数返回之前,程序检查金丝雀值是否被函数的某些操作或函数调用的函数更改。如果是这样,程序中止。

英国矿山养殖金丝雀的历史大约始于 1911 年。当时矿山的工作条件很差,矿工们经常在下井时冒着生命危险。后来,在对一氧化碳进行了一些研究后,约翰·斯科特·霍尔丹开始建议在煤矿中使用金丝雀来检测一氧化碳和其他有毒气体。金丝雀的特点是极易受到有毒气体的侵害,因为它们通常在高空飞行,需要吸入大量空气才能吸入足够的氧气。结果,金丝雀比老鼠或其他容易携带的动物吸入更多的空气和空气中潜在的有毒物质。这样一来,万一金丝雀发生事故,矿工们就会很快意识到矿井中有毒气体浓度过高,他们处于危险之中,

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类型,从本质上防止它传递负数。此外,您可以通过在访问数组之前添加一个小于 ARRAY_MAX 的语句来检查数组的上限。总之,养成良好的编程习惯,可以节省很多宝贵的时间。

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论