前面我们学习了很多section的机器级表示,比如数据传输、控制、进程调用、数组、结构等等,但是仅仅通过section是不能用来执行程序的对计算机有了统一系统的认识,所以我们介绍一些比较综合的案例,通过分析讨论,看看程序在计算机上是如何运行的。进程的内存空间布局再来看进程用户空间的布局(空间包括用户空间和内核控制,内核空间主要存放操作系统的数据和指令)。linux中的用户空间很大,提供了2的48次方的空间。X86-64位linux系统的用户空间包括四个主要区域:stackspace:存储栈,位于用户空间的最高地址。堆栈从高地址向低地址增长。在linux系统中,默认分配的栈空间是8mb字节,主要是访问进程栈帧。在从高级语言翻译成汇编语言的过程中,编译器插入相关的栈帧管理指令,进行栈帧分配和空间回收。不需要程序员手动管理这8mb的空间,空间不大,但是对于程序来说已经足够了。运行死递归时程序会崩溃,因为大量栈帧的递归分配会耗尽栈的空间,而不是填满所有空间。而且栈空间只有8mb,用完用不了多久。栈溢出时程序会崩溃,所以当一些应用程序需要更多的栈空间时,编译选项需要指定栈的大小堆空间:存放动态分配的数据。C语言使用malloc和calloc函数,C++使用new关键字在堆空间分配内存。堆空间的内存分配和回收需要程序员用高级语言手动管理。申请的时候需要申请空间,释放的时候用free(C语言)和delete(C++)手动释放。如果不是手动释放,它会伴随整个流程的整个生命周期,直到流程结束才会释放。一旦管理不慎,就会造成内存泄漏数据段:存放静态分配的数据。它在程序加载时分配,程序结束时释放。包括全局变量(生命周期伴随整个过程)、静态变量(被static关键字修饰的变量)、字符串类型常量代码区:包括两段文本和共享库文本段:用于存放当前进程的指令,即在整个程序的二进制可执行文件里面的机器指令共享库:存放程序执行时从外部加载的机器指令,比如动态链接库,属于共享库段。也就是说,它不在整个程序的二进制可执行文件里面,而是在运行时从外部加载的机器指令。值得注意的是,代码区的指令在内存中是只读的,不可篡改。其余部分是可读和可更改的。结合例子看看声明的变量位于哪个区域。前三个全局变量存放在数据段,两个函数存放在代码区文本段。函数中包含机器指令,这两个函数指向的函数入口就是对应机器指令的起始地址。它们的入口地址位于text段的main局部变量中:local因为后续还有其他函数,所以直接分配到栈空间中,目前有四个指针分配在寄存器中。但是,如果后续过程调用需要寄存器保护,它仍然会被压入堆栈。在动态分配内存时,malloc将堆上的内存地址分配给指针。虽然指针局部变量仍然保存在寄存器中,但是它们的值是Address,这四个地址指向堆空间。值得一提的是,Linux对堆有一个特殊的空间管理策略:当分配的内存大小小于某个阈值时,malloc在低地址分配空间并向上增长;当它较大时,它分配在高地址并向下增长。一般使用128kb作为阈值缓冲区溢出。几个经典案例包括1988年的蠕虫病毒和2014年的Heartbleed漏洞。HTTPS协议是Internet中的应用层传输协议,内部采用SSL方式进行数据加密。SSL方式有一个巨大的漏洞,即Heartbleed漏洞。比特币蠕虫导致所有使用这种加密传输协议的主机被互联网黑客攻击,导致信息泄露长达17年之久。利用Windows系统的永恒之蓝漏洞对磁盘文件进行加密,通过比特币勒索解密。即时通讯软件大战微软推出了新版MSN。在新版本中,MSN客户端不仅可以连接到自己的服务器,还可以连接到AOL的AIM服务器。也就是说,拥有AIM账号的用户可以登录MSN与AIM和MSN中的好友聊天,导致AIM用户减少。但是在1999年,MSN客户端已经无法进入AIM服务器,即AIM服务器会拒绝MSN客户端。连接。但是,在AIM客户端没有升级或修改的情况下,是可以知道客户端属于哪个公司的。这是不合理的,因为从互联网通信的角度来看,只要遵循相同的通信协议,服务器就无法识别客户端是哪个产品。而如果要修改通信协议,则必须更改客户端信息。而AIM只能通过更换服务器来拒绝MSN客户端的连接。微软相应修改了MSN客户端程序,使其可以重新连接到AIM服务器。AIM继续修改服务器,使其无法连接。两家公司一共打了13轮。如何升级AIM服务器,在兼容老版本客户端的同时,正确识别MSN客户端?即如何在不修改通信协议的情况下识别不同厂商的客户端?这与缓冲区溢出有关。什么是缓冲区溢出?缓冲区溢出是一种非常常见的编程缺陷。问题主要是因为程序中分配了一个数组,但是在操作数组的时候出现了越界访问,即对数据的访问超出了数组的预期范围。数组称为缓冲区,溢出称为溢出。缓冲区溢出会带来很多安全隐患,被黑客用来完成对主机的攻击,窃取信息。主要是程序员设计经验不足,或者忽视了这个问题。主要表现为当输入数据(如字符串)输入数组时,没有事先检查输入数据(字符串)的长度和数组的预留长度,有时数组可能会溢出,因为输入字符串太长。当在应用程序的栈空间分配缓冲区时,这种溢出可能会导致栈的破坏,甚至整个程序的崩溃将它们传输到缓冲区)此函数实现读取一行数据,以回车符或文件结束符结束。读取数据追加到destbuffer后,最后追加字符串结束符\0。这个程序隐藏着一个巨大的漏洞。因为get内部循环的结束条件完全依赖于外部输入,所以get函数永远不知道destbuffer有多长。get函数只有在外部输入结束时才会返回,destbuffer太小时会溢出。C语言库函数中有很多这样的函数。它们在传入参数时,只传入缓冲区的首地址,不传入缓冲区的长度。因此,无法根据内部循环中缓冲区的长度进行内部循环检查。此类函数主要集中在字符串操作函数中。如字符串复制、追加、从标准输入解析数据、从文件解析数据等都可能导致解析出的字符串超过缓冲区的长度。在最新版本的C语言编译器中,如果使用这些库函数,会出现警告错误,存在更安全的版本。在这些比较安全的函数中,提供一个新的参数输入,即缓冲区长度输入参数,使得函数内部可以对缓冲区进行严格的边界检查。缓冲区溢出漏洞对程序有什么影响?Implementation”来分析溢出的影响。这个函数使用了getsunsafe函数,声明的buf缓冲区太小,程序运行时极有可能造成缓冲区溢出。当我们输入23个字符串时,此时缓冲区已经溢出时间,但程序仍然正确运行;输入25个字符串后,程序崩溃。可见,在某些情况下溢出会导致程序崩溃,但在某些情况下则不会。如何解释这种现象?分析:首先,函数反汇编得到汇编语句进入echo时,先将当前rsp寄存器的值减去十进制数24,其实就是分配给echo函数的栈帧。echo语句执行后,栈顶指针指向echo函数的返回地址(4006f6)。然后将rdi(将调用进程get的参数buffbuffer)赋值给4个字节,以rsp为起始地址。3.echo过程返回后,执行下一条add指令。如上图所示,call-echo栈帧的顶部是echo4006F64的返回地址,get进程用数据填充缓冲区。当向后填充24个字节(包括结束字符0)时,数据溢出缓冲区是巧合。区域后继续填充,只填充预留空间,不会影响其他内容。此时虽然发生了溢出,但是返回地址没有变化,还是可以正常返回的。当填充26(包括结束字符0)时,call-echo的栈帧内容被破坏,此时返回地址发生变化。如果400034是另一条指令或非法指令,将导致程序崩溃,控制流中断。当填满25时,call-echo栈帧也会被破坏,但破坏的严重程度比前面的例子要小。如果销毁后返回地址恰好是一条有效语句,仍然可以正常执行(下图中地址400600为一条有效命令,继续从地址400600开始执行指令,直到进程返回)。但是,如果是有害语句,则可能会被黑客用来执行代码注入攻击。缓冲区漏洞的常见攻击方法代码注入攻击原理:在p函数中调用另一个函数q,在q中声明缓冲区,调用一些不安全的函数。这就提供了黑客攻击的可能。q函数的返回地址存放在p函数栈帧的顶部。黑客伪造输入数据,从标准输入中伪造出要输入的字符串。该数据由三部分组成:开始部分包含攻击缓冲区的恶意指令;然后是占位数据,占位数据的内容不重要,大小才是关键,需要保证恶意指令+占位数据刚好填满slavebuffer打开地址到最底层的空间q栈帧;最后构造p栈顶的数据,实现返回地址的篡改。q的返回指令应该指向下一条调用q函数的语句对应的指令。但是,攻击者输入数据后,构造占用数据后的地址,将缓冲区起始地址的绝对位置作为攻击数据的最后内容,放在攻击部分的序列中,正好覆盖q的返回地址。当q过程执行ret返回时,返回地址需要从p栈帧顶部弹出,加载到rip寄存器中完成内容跳转,跳转到buffer起始地址。案例:缓冲区溢出漏洞可被攻击者利用,攻击者无需接触物理主机,只需通过网络远程发送攻击数据即可破坏联网主机。即时通讯战争。AOL利用AIM客户端的缓冲区溢出漏洞完成了对MSN的防御。AIM客户端存在缓冲区溢出漏洞,为AIM服务器留下控制AIM客户端的后门。当通信连接上后,AIM会对自己的客户端进行代码注入攻击,向客户端注入一段代码。此代码对当前客户端进行数字签名,并将签名返回给服务器,服务器通过判断签名是否正确来识别客户端。微软通过类似抓包的方式匹配到正确的签名,并伪造签名,成功进入AIM服务器的防御。我们从三个角度讨论代码注入攻击的防御:从程序员的角度,提高程序设计的质量,使程序不留有缓冲区溢出漏洞。最重要的是不要在程序中使用不安全的库函数。比如get函数可以换成fgets函数,strcopy可以换成strncopy,在security函数中增加一个用来描述传入缓冲区大小的参数,里面可以检查缓冲区的边界防止溢出的功能。scanf在解析输入字符串时,采用%s转换方式,将输入数据以字符串形式放入缓冲区,不能进行边界检查。相应地,有两种改进方法。如果您只是从输入中加载一个字符串,请改用fgets;如果在输入过程中需要解析,使用%ns而不是%s转换方式,其中n是一个整数,用来描述接受字符串缓冲区的长度,这样可以检查目标边界。从系统层面来看,在操作系统层面提供了栈随机化机制(stackoffsetrandomizationmechanism)来随机化栈的基址。每个应用程序都有一个堆栈基址。前期没有防御策略的时候,基地址固定分配到某个地址,所以每次都确定缓冲区位置,攻击比较简单。栈偏移随机机制在程序每次启动时,预先随机分配栈中未使用空间的大小,使得每次运行时程序main函数的栈底位置都不一样。代码注入攻击使得猜测缓冲区的精确位置变得困难。然而,道高一尺,魔高一尺。为了应对堆栈随机化机制,黑客发明了一种滑板攻击方式。我们不是直接注入恶意指令,而是先注入空指令,再注入恶意指令。这样就不需要准确预测恶意指令的地址,只需要预测一个大概的范围,使返回地址跳转到空指令,从空指令开始执行,一直运行到恶意指令。为了对付滑板攻击的方法,还有第二种防御方法,即通过给处理器增加新的特性来实现保护。在X86-32位处理器中,指令对内存的操作权限包括读权限和写权限。对于有读权限的内存区,可以作为一条指令加载到处理器中执行,而栈空间既有读权限又有写权限。但是,X86-64位处理器通过添加可执行权限改进了这一点。当前内存区域必须具有可执行权限,才能将其中的数据作为指令加载到处理器中执行。对于普通的栈空间,不需要可执行权限,只需要读写权限即可。因为代码注入攻击是将代码注入栈空间,所以即使发生代码注入攻击,指令的执行也不会被允许,算是比较彻底的防御。值得注意的是为什么我们说普通栈空间不需要可执行。允许?因为指令放在text段中,栈帧中只有参数、局部变量、返回地址和临时空间,不需要执行指令,所以不需要可执行权限。初学的时候,误以为栈帧里有指令。其实给出的只是指令的地址。从编译器的角度来看,编译器提供了堆栈损坏监控功能,使得编译后的程序的每个进程都可以在返回前实现。它监视当前栈帧是否被破坏,如果被破坏则程序退出,阻止恶意指令的执行,从而实现保护功能。如何实现堆栈损坏监控?编译器提供了一个特殊的值——金丝雀值,它位于当前进程栈帧的底部,用于将下一个进程的缓冲区和当前进程的返回地址分开,在进程返回时检查金丝雀值,如果金丝雀值被销毁,则表示当前进程栈帧被销毁,程序退出。金丝雀飞回来,说明矿场安全;不飞返说明矿井CO浓度过高,矿井无法工作。编译器将初始化金丝雀值代码插入到每个进程中。在进程返回前,检查canary值,判断进程是否可以正确返回。前两条指令将canary值放在当前进程栈帧的底部。fs寄存器是一个段寄存器,在早期的实模式编程模式中被频繁使用;现在汇编指令在保护模式下工作,fs寄存器用处不大。主要是在当前程序中,存放当前进程相关上下文信息的机制体入口地址。每个进程启动后,随机分配初始化的canary值(fs地址偏移0x28),然后将canary值放在当前进程的栈帧底部(rsp偏移8)。返回前,取出canary值,与fs寄存器中存放的初始canary值进行比较。如果相等,则返回;如果不相等,则跳转到处理栈销毁后的工作流程。该进程主要输出Error信息,结束当前程序从上图可以看出,canary值位于调用进程的返回地址和被调用进程的buffer之间,如果buf溢出破坏了canary值,程序结束;如果buf溢出,但没有覆盖canary值,则返回地址不变,程序控制流不变,程序正常运行;时间是随机获取的,相等的概率太小,所以问题回归到编程攻击可执行权限的出现,让代码注入攻击失去生存的土壤。为此,黑客发明了一种新的攻击方法,称为面向返回的编程攻击。与从远程注入指令完成攻击的代码注入攻击不同,面向返回的编程攻击利用当前程序中已有的代码片段,并重组这些代码片段来实现恶意行为。这些片段本身可能没有恶意,但如果重新组合起来,它们就会显得有恶意。主要思想:利用一些位于每个进程结束之前(retreturn指令之前)的程序片段,将它们重新组织起来,通过缓冲区溢出将栈顶的位置组织成每个代码片段的起始地址。这样每个片段都存在于代码中,不位于栈中,从而避免了可执行权限的影响。流程:缓冲区在顶部,从缓冲区的起始位置开始,依次加载代码片段的起始地址。每个代码片段执行完后,会执行C3返回指令返回栈,继续执行下一个片段,以此类推。ret返回指令实际上是作为跳转指令使用的。即向栈中注入一系列代码片段,通过过程返回指令串联起来。值得注意的是,注入的返回地址总长度不能超过金丝雀值的边界,否则会失败。此法防卫难,实施难。如果攻击成功,几乎没有防御机制。从上面可以看出,黑客总有办法利用缓冲区溢出漏洞来实现攻击。所以,最根本的解决办法就是避免程序中缓冲区溢出,提高程序质量关注公众号,让我们一起努力
