缓冲区溢出攻击
程序的内存布局
对于一个典型的C语言程序,内存由5个段组成,分别是
代码段,数据段,堆,栈,bss段
代码段:放可执行代码,通常只读
数据段:已经初始化全局变量,静态变量
BSS段:未初始化的全局变量和静态变量
堆:用于动态内存分配
栈:用于存放与函数调用相关的数据
栈与函数调用
栈的内存布局
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qwAsV2hd-1612085707431)(%E7%BC%93%E5%86%B2%E5%8C%BA%E6%BA%A2%E5%87%BA%E6%94%BB%E5%87%BB.assets/%7DPW@MJ%5B_AG@ABGE1%7BT70149.png)]
参数:保存传递给函数的参数,参数的值是按相反的顺序压入栈中
返回地址:当函数执行完后需要回到主程序的地址
前帧指针:存着返回地址程序对应的栈帧地址
局部变量:用于存放函数的局部变量
帧指针
ebp指向栈帧中的一个固定地址,因此参数和局部变量的地址可以通过这个寄存器加上一个偏移量计算得到
前栈指针和函数调用链:通常会在一个函数里调用另一个函数,俗称函数调用链,
栈的缓冲区溢出攻击
对于一些函数,没有边界检测的功能,如C语言的strcpy(),它设定为遇到’\0’结束,这时候就可以通过输入过量的数据,将要执行的恶意代码的地址覆盖到返回地址。
所以定位是非常重要的,定位读取数据前自己的位置,定位返回地址的地址,
才能确定要输入多少数据,这前提是输入的数据是按规律的连续填充在栈空间内。
对此,一成不变的堆栈结构会给程序带来很多不必要的风险,所以现代操作系统往往都有地址随机化这一功能
而许多编译器也有自己的防护措施,如gcc的不可执行栈,对栈的内存部做了一个标记,在这标记下系统是不会执行的
除此之外还有一个gcc的StackGuard的保护机制,在栈空间中添加一些些特定的的数据和验证机制,来查看栈空间是否被改写
如何寻找恶意代码的位置
我们只能知道他在我们输入中的相对位置,也就是缓冲区的相对位置
但是不能知道他在内存中的绝对地址
这样让我们除了猜测别无他法,面对巨大的搜索空间,有2个因素大幅减少了工作量
1,在引入一些防御机制前大多数的操作系统会把栈放在固定起始的地址
该地址是一个虚拟地址,对于不同的进程,它会被映射到不同的物理地址,因此不同进程可以使用同一个虚拟地址作为栈的起始地址而不会产生冲突
2,只要不是循环调用的函数链,那栈的空间不会太深
提高猜测成功的概率Nop指令
NOP指令是让CPU啥也不做空转,执行下一条。在恶意代码的头部空间添加许多Nop指令,相当于让正确的入口变多了,只要猜中任意Nop指令的地址或者头部地址都可以运行恶意代码
通过调试获得地址
注意,当以一个普通用户调试特权程序的时候,程序并不会运行在特权态
(这样看来,栈溢出一般发生的场景是在,远程控制了目标主机后,进行提权的进一步操作,寻找特权程序,然后借此提权或者越权做一些操作)
实现简单的栈溢出
在没有杂七杂八的防护措施前提下
我们先通过gdb调试获得目标函数的入口地址
使用 b foo
忽然感慨,那我们不是还得知道目标程序的源码。。。。
而特权程序的源码怕是我们普通用户弄不到的
不要在意这些细节,假如我们确定了源码。知道了目标函数名
在使用 p$ebp 打印出16进制的ebp地址
再使用 p &buffer打印缓冲区起始地址。这是知道源码前提下。。
然后算出ebp-buffer得到偏移量
ebp指向前栈指针开头
ebp+4指向返回地址开头
所以ebp+4都是杂鱼,可以随便填充。
然后开始填入4个字节大小的返回地址,也是恶意代码入口地址
然后后面都可以填充大量的Nop指令
在加上恶意代码部分
因为我们gdb调试,会在运行前在栈中压入一些奇怪的东西,所以不能把计算得来的东西直接当做准确地址。所以需要大量的Nop
我们使用Python脚本编写输入,需要不断试的就是返回地址。
防护机制
如Ubuntu16.04中就有当系统发现特权程序在运行shell程序时候,她会把有效用户id变成实际用户id,这时候如果不是root用户运行该程序,那就不会得到一个root权限的shell
构造shellcode
shellcode难点之一是,他是16进制的数字混合字符,你要熟悉汇编
但是我们可以想到用c语言编写恶意程序,然后编译成二进制文件。
但是一个程序的运行需要被加载,这样的二进制是没有被系统加载
没有系统加载的初始化的准备工作,是无法正确运行的。
而且在上述程序被编译为二进制文件的后,至少会出现0,如
字符串‘‘/bin/sh’‘末尾一个0
程序中有null也会有0
而字符串复制函数strcpy遇到0就会停止
所以构造shellcode核心方法是使用汇编语言
核心思想是使用execve()系统调用来执行’/bin/sh’
使用这个系统调用必须:
eax必须保存11,11是execve的系统调用号
ebx必须保存命令字符串的地址,“/bin/sh”地址
ecx必须保存参数数组的地址
edx必须保存想要穿给新程序的环境变量的地址,可以将它设置成0.
设置寄存器的值不难,难在数据的准备和数据的地址
首先需要知道命令字符串的地址,设置ebx,一个方法是动态压入命令字符串,因为esp永远指向栈顶,这样来获得地址
为了保证代码能完整的复制进入缓冲区,不能存在0,这是因为一些函数把0当做复制末尾。尽管程序中需要用到0,但是不能让0出现,可以让寄存器自己进行异或运算,这样获得0,但是没有0出现在指令里。
防御措施概述
更安全的函数
之前一些函数是根据数据来决定复制的长度,比如0
现在由开发者制定大小。不过只能减低攻击的可能性,不行真正避免
更安全的动态链接库
上面的方法需要改动程序,而采用动态链接库的程序。库函数代码存在动态链接库,构建一个更安全的库,可以基于ebp边界检测,不允许复制超出帧指针的边界
程序静态分析
在程序开发者编写不安全的程序时候就发出提醒
编程语言
java和Python自动提供边界检测。被认为更安全的语言
编译器
编译器将源码转为二进制,它能控制最后放入二进制代码中的指令
有两种比较出名的措施,stackshield和stackguard
stackshield将返回地址备份到一个安全的地方,当函数返回时候,比对返回地址和备份地址是否相同
stackguard是在返回地址和缓冲区中设置一个哨兵,他是随机数,并且被备份在一个安全的地方,返回时候比对哨兵值
操作系统
程序加载时候的地址空间随机化
硬件体系系统
一种把代码和数据分离开的技术,操作系统可以把某些内存区域设置成不可执行,会拒绝执行这些内存区域中的任何代码。