1

前边我们学习了诸多断面的机器级表示,如数据的传输、控制,过程调用、数组、结构体等,但是仅仅通过断面不能对程序在计算机中执行过程有统一、系统性的认识,因此我们介绍一些综合性案列,通过分析、讨论,看看程序在计算机中如何系统工作

进程的内存空间布局

  • 我们来看下进程用户空间的布局(空间包括用户空间和内核控件,内核空间主要储存操作系统的数据和指令)。linux中的用户空间非常大,提供2的48次方的空间

X86—64位linux系统的用户空间包括四个主要区域

  • 栈空间:
  1. 存储栈,位于用户空间的最高地址上。栈从高地址向低地址生长。在linux系统中默认分配栈空间8mb字节,主要存取过程栈帧。
  2. 由高级语言翻译成汇编语言过程中,由编译器插入相关栈帧管理指令进行栈帧分配和空间回收,不需要程序员手动管理
  3. 8mb空间不大,但对于程序已经够了。运行死递归时程序会崩溃,是由于递归分配大量栈帧,将栈的空间耗尽,而不是将所有空间填满。而且栈空间只有8mb,因此耗尽的时间不是很长。栈溢出时程序会崩溃,所以有些应用程序需要更大栈空间时,需要编译选项制定栈大小
  • 堆空间:
  1. 存储动态分配数据。在C语言中用malloc、calloc函数,在C++中用new关键字在堆空间中分配内存。
  2. 堆空间的内存分配和回收需要程序员在高级语言中手动管理。申请时要申请空间,释放时用free(C语言)、delete(C++)手动释放。如果不手动释放,将会伴随整个进程的全生命周期,直到进程结束才释放。一旦管理不慎,就会造成内存泄漏
  • 数据段:存储静态分配数据。程序加载时进行分配,程序结束时完成释放。包括全局变量(生命周期伴随整个过程)、静态变量(static关键字修饰的变量)、字符串类型常量
  • 代码区:包括text和共享库两个段
  1. text段:用于存储当前进程的指令,即在整个程序的二进制可执行文件内部的机器指令
  2. 共享库:存储程序执行时从外部加载的机器指令,如动态链接库就是属于共享库段。即不在整个程序的二进制可执行文件内部,而是运行时从外部加载的机器指令
  3. 值得注意的是,代码区的指令在内存中只读,不能被篡改。而其余几部分可读可改

结合例子看声明的变量在哪个区里

  • 开头3个全局变量存储在data数据段
  • 两个函数存储在代码区text段。函数内部包含机器指令,而这两个函数指向的函数入口,就是对应机器指令的开始地址,他们的入口地址即位于text段
  • main局部变量:
  1. local由于在后续有其他作用,被直接分配到栈空间
  2. 四个指针当前分配在寄存器中。但如果后续过程调用时需要寄存器保护,仍会压入栈中
  • 在内存的动态分配过程中,malloc将堆上的内存地址赋值给指针,虽然指针局部变量仍存储在寄存器中,但他们的值是地址,这四个地址指向堆空间中。值得一提的是,linux中对堆有特殊的空间管理策略:当分配内存大小小于某阈值,malloc分配空间在低地址位置,并向上生长;比较大时,分配在高地址位置,并向下生长。一般以128kb作为阈值

缓冲区的溢出

几个经典案例

  • 1988年的蠕虫病毒
  • 2014年的心脏滴血漏洞。互联网中的应用层传输协议https协议,其内部使用SSL方法进行数据加密,SSL方法被爆出巨大漏洞,即心血漏洞。导致所有使用此加密传输协议的主机可能被互联网黑客攻击,从而导致信息泄露
  • 17年的比特币蠕虫。利用Windows系统中的永恒芝兰漏洞,将磁盘上文件进行加密,通过比特币勒索进行解密
  • 即时通讯软件战争
  1. 微软推出MSN新版本。在新版本中,使MSN客户端不仅可以连接自己服务器,而且能够连接到AOL公司的AIM服务器上。也就是说,持有AIM账号的用户可通过登录MSN来和AIM、MSN中好友聊天,结果导致AIM用户减少
  2. 然而在1999年,MSN客户端不能再进入AIM服务器,即AIM服务器会拒绝MSN客户端连接。然而却是在AIM客户端不做升级和修改的情况下,就得到了客户端是哪个公司的。这样是不合乎常理的,因为从互联网通讯角度讲,只要遵循相同通讯协议,服务器便无法识别客户端到底是哪家产品。而如果要修改通讯协议的话,必须改变客户端信息。而AIM却只改变了服务器就可以拒绝MSN客户端的连接。
  3. 微软对应修改了MSN客户端程序,使其重新可以连接AIM服务器。AIM继续修改服务器使其不能连接。两家公司一共交手13个回合。
  4. AIM服务器升级是如何做到在兼容老版本客户端同时,能正确识别出MSN客户端的呢?即在不修改通讯协议的情况下,如何识别出不同厂商客户端呢?这就与缓冲区的溢出有关

何为缓冲区溢出

  • 缓冲区溢出问题是非常常见的程序设计缺陷,问题的产生主要由于在程序中分配了一个数组,但在操作数组时发生了越界访问,即数据访问超过了数组应有边界。其中数组称为缓冲区,超出边界称为溢出。
  • 缓冲区溢出会造成许多安全隐患,并被黑客利用,完成对于主机的攻击以及信息的窃取。主要由于程序员在设计时缺乏经验,或者忽略掉了这一问题从而产生。主要表现在对于输入的数据(如字符串)输入到数组中,没有对输入数据(字符串)长度与数组预留长度进行提前检查,有时可能因输入串过长从而导致数组溢出。当缓冲区分配在应用程序的栈空间中,这种溢出就可能导致栈的破坏,甚至整个程序崩溃
  • 案例:字符串库函数get(从标准输入中加载字符传到缓冲区的函数)

  1. 此函数实现读入一行数据,以回车或者文件结束作为结尾。读到的数据追加到dest缓冲区后,最终追加串尾符\0结尾
  2. 此程序隐藏着巨大的漏洞。因为get的内部循环结束条件完全依赖于外部输入,所以get函数内部从来不会了解dest缓冲区有多长。只有当外部输入结束时get函数才会返回,而当dest缓冲区过小就会形成溢出。
  3. 在C语言库函数中存在许多这样的函数,它们传入参数时只传入缓冲区首地址,不传入缓冲区长度,故而在内部循环中无法根据缓冲区长度做内部循环检查。这样的函数主要集中在字符串操作函数中。如字符串复制、追加、从标准输入解析数据、从文件解析数据,都可能导致解析出的字符串超出缓冲区长度问题。在最新版本C语言编译器中,如使用这些库函数,会提出警告错误,存在更安全的版本。在这些更安全函数中,提供新的参数输入,即缓冲区长度输入参数,以便函数在内部对缓冲区进行严格的边界检查

缓冲区溢出漏洞会对程序造成什么影响

  • 在案例“屏幕回显功能的实现”中分析溢出的影响。此函数使用了gets不安全函数,且声明的buf缓冲区过小,在程序运行过程中极有可能导致缓冲区的溢出。
  • 当我们输入23个字符串时,此时缓冲区已经溢出,程序却仍正确运行;而输入25个后,程序崩溃。可见有些情况下溢出会使程序崩溃,有些情况却不会,如何解释这种现象呢?
  • 分析:
  1. 首先将函数反汇编得到汇编语句
  2. 进入echo时,首先将当前rsp寄存器的值减去十进制数字24,实际为分配echo函数的栈帧。执行echo语句后,栈顶指针指向echo函数的返回地址位置(4006f6)。然后将rdi(即将调用过程get的参数buff缓冲区)分配到了以rsp作为起始地址的4个字节

 3、echo进程返回后,执行接下来的add指令。如上图,call-echo栈帧的顶部是echo的返回地址4006F6

4、get进程将数据填充缓冲区,当向后填充24个字节(包括串尾符0)时,此时非常巧合,数据溢出缓冲区后继续填充,恰好把预留空间填满,并不会影响其他内容。此时虽然有溢出产生,但返回地址未变,仍然可以正常返回

     填充26个(包括串尾符0)时,破坏了call-echo的栈帧内容,此时返回地址发生改变。如果400034是其他指令或者非法指令,就会导致程序崩溃,控制流被打乱

    当填充25个时,call-echo栈帧同样被破坏,但破坏的严重程度比上个例子小。如果破坏后返回地址恰巧为有效语句,仍然可以正常执行(如下图地址400600为有效指令,从400600地址处继续向下执行指令,直到过程返回)。但如果是有害语句,则有可能被黑客利用,从而进行代码注入攻击

缓冲区漏洞常见攻击方法

代码注入攻击

  • 原理:
  1. 在p函数中调用了另一个函数q,在q中声明缓冲区,并调用了一些不安全的函数。这样就提供了黑客攻击的可能
  2. p函数栈帧顶部存放q函数的返回地址
  3. 黑客伪造输入数据,伪造成字符串从标准输入中输入。此数据包含三个部分:开始部分包含对缓冲区发起攻击的恶意指令;然后是占位数据,占位数据内容不重要,大小是关键,要保证恶意指令+占位数据恰好占满从缓冲区开地址到q栈帧底部的空间;最后要构造 p栈顶的数据,实现返回地址的篡改。
  4. q的返回指令本来应指向调用q函数的接下来的语句对应的指令。但是攻击者输入数据后,在占位数据后构造地址,把缓冲区起始地址的绝对位置作为攻击数据的最后内容,放到攻击部分的序列中,恰好覆盖q的返回地址。q过程执行ret返回时,要从p栈帧顶部弹出返回地址,装在rip寄存器完成内容跳转,就跳到了缓冲区起始地址上
  • 案例:
  1. 缓冲区溢出漏洞可以被攻击者使用,且攻击者不需接触到物理主机,只需通过网络远程发送攻击数据,从而破坏联网的主机。
  2. 即时通讯软件战争。AOL利用了AIM客户端的缓冲区溢出漏洞,完成对MSN的防御。AIM客户端中存在缓冲区溢出漏洞,这就给AIM服务器留了一个可控制AIM客户端的后门。通讯连接时,AIM会对自己的客户端进行代码注入攻击,将一段代码注入客户端中。此代码对当前客户端进行数字签名,并把签名返回给服务器,服务器判断签名是否正确来识别客户端。微软又通过类似抓包手段匹配到正确签名,并且伪造签名,顺利进入AIM服务器

防御

下边,我们从三个角度讨论代码注入攻击的防御问题:

  • 从程序员角度,提高程序设计质量,使程序不留有缓冲区溢出的漏洞。最重要的在程序不要使用不安全库函数。如get函数可用fgets函数代替,strcopy用strncopy替代,在安全函数中增加了用于描述传入的缓冲区大小的参数,可以在函数内部检查缓冲区边界,防止溢出。而scanf对输入的字符串做解析时,采用%s的转换模式,将输入数据以字符串形式放入缓冲区中,无法做边界检查。对应有两种改进方式,如果是从输入中单纯加载字符串,用fgets替代;如果在输入过程中需要解析,用%ns代替%s的转换模式,其中n是整数,用于描述接受字符串的缓冲区长度,这样就可以对目标边界进行检查了。
  • 从系统级角度而言,在操作系统级别提供了栈随机化机制(栈偏移量随机化机制),对栈的基地址进行随机化分配。每个应用程序有一个栈的基地址,在早期没有防御策略时,基地址是固定分配到某个地址上,因而每次的缓冲区位置是确定位置,攻击相对简单。而栈偏移量随机机制在每次程序启动时,预先在栈中随机分配未使用空间的大小,进而使程序主函数的栈底位置每次运行都是不同的。代码注入攻击就很难猜到缓冲区精确位置。
  • 但是,道高一尺,魔高一丈。为了对付栈随机化机制,黑客又发明了一种滑板攻击方法。我们并非直接注入恶意指令,而是先注入空指令,再注入恶意指令。这样就不需精确的预测恶意指令的地址,只需要预测一个大致范围,使返回地址跳转到空指令,从空指令开始执行,一直运行到恶意指令
  • 为了应对滑板攻击方法,又出现了第二种防御方法,即通过处理器增加的新特性实现保护。在X86-32位处理器中,对于内存中指令的操作权限包括读权限和写权限,对于具有读权限的内存区域,都可以作为指令加载到处理器中执行,而栈空间既具备读权限又具备写权限。但是X86-64位处理器对此进行了改进,增加了可执行权限,当前内存区域必须具备可执行权限,里边数据才能被当做指令加载到处理器中执行。而对于普通栈空间是不需可执行权限的,仅需读写权限。由于代码注入攻击将代码注入栈空间中,所以即使发生了代码注入攻击,执行时也不会允许指令执行,是较为彻底的防御
  • 值得注意的是,我们为何说对于普通栈空间是不需可执行权限的?因为指令都放在text段,栈帧中只有参数、局部变量、返回地址、临时空间,不需要执行指令,所以不需可执行权限。初学时误以为栈帧中有指令,实际上给出的只是指令的地址而已
  • 从编译器角度而言,编译器提供了栈破坏监测功能,使编译后生成程序的每个过程返回前都可实现对当前栈帧是否破坏的监测,如果破坏则程序退出,防止执行恶意指令,以实现保护功能。
  • 如何实现栈破坏监测的呢?编译器提供了一个特殊值----金丝雀值,位于当前过程栈帧的底部,用于把下个过程的缓冲区与当前过程的返回地址进行间隔,在过程返回时检查金丝雀值,若金丝雀值破坏意味着当前过程栈帧被破坏,程序退出
  • 金丝雀值:早期,西方煤矿工人下矿前,用金丝雀监测煤矿内瓦斯浓度。金丝雀飞回表明矿井安全;没有飞回表明矿井co浓度过高,不能下井工作
  • gcc最新版本默认提供栈破坏监测功能,而早期的gcc编译器须使用编译选项-fstack- protector打开栈破坏检测功能。编译器中给每个过程插入初始化的金丝雀值代码,过程返回前,检查金丝雀值,确定过程是否能正确返回
  1. 开始两条指令将金丝雀值放到当前过程的栈帧底部。fs寄存器是段寄存器,是早期的实模式编程模式频繁使用的寄存器;而现在汇编指令都工作在保护模式下,fs寄存器用途较小。主要在当前程序中存放与当前进程相关的上下文信息所存放的机构体入口的地址。每次进程启动后随机分配初始化的金丝雀值(fs地址偏移0x28),再将金丝雀值放到当前过程的栈帧底部(rsp偏移8)。
  2. 在返回前,将金丝雀值取出,与fs寄存器中存储的初始化金丝雀值比较,如相等,过程返回;如不等,跳转到处理栈破坏后工作的过程中,此过程主要输出错误信息,结束当前程序

由上图可见,金丝雀值位于调用过程的返回地址与被调用过程的缓冲区之间,如buf溢出并破坏金丝雀值,程序结束;如buf溢出,但并未覆盖金丝雀值,此时返回地址不变,程序控制流不变,程序正常运行;如溢出覆盖的数据恰好等于金丝雀值,栈破坏功能就没用了,但是金丝雀值每次都是随机获取的,恰好相等几率太小,故而无妨大碍

面向返回编程攻击

  • 可执行权限的出现,使得注入代码攻击失去了生存的土壤。为此,黑客又发明了一种新的攻击方法,称为面向返回编程攻击。与注入代码攻击从远程注入指令完成攻击不同,面向返回编程攻击使用当前程序中已有的代码片段,并对这些代码片段进行重组,以实现恶意行为。这些片段可能本身没有恶意行为,但如果重组的话,会便显出恶意行为。
  • 主要思想:使用一些位于每个过程尾部之前(ret返回指令之前)的程序片段,将其进行重组,通过缓冲区溢出,将栈顶所在位置组织成一个个代码片段的起始地址。这样每个片段都是代码已有的,不位于栈中,从而避免了可执行权限的影响。

  • 过程:缓冲区位于顶部,从缓冲区起始位置,依次装入代码片段的起始地址。每次执行完代码片段后,都会执行C3返回指令返回到栈中,继续执行下一个片段,以此类推。 实际上是把ret返回指令当做跳转指令来用。即注入了一系列代码片段到栈中,通过过程返回指令串联在一起。值得注意的是,要保证注入的返回地址总长度不能超出金丝雀值的边界,否则无法成功。此方法防御难度大,实现难度也大。如果攻击成功,几乎没有任何机制防御
  • 由上可见,黑客们总有办法利用缓冲区溢出漏洞实现攻击。因此,最根本的解决方法就是避免程序中的缓冲区溢出,提高程序的质量
关注公众号,让我们携手共进0.5.jpg

无欲则刚
76 声望15 粉丝