1.1 函数调用栈情况
缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢出的数据覆盖在合法数据上。
理想的情况是程序检查数据长度并不允许输入超过缓冲区长度的字符,但是绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患。
缓冲区溢出有堆缓冲区和栈缓冲区溢出,二者有些不同,大部分情况下都是讨论栈溢出。
1.原理 1.1 函数调用栈情况
程序运行时,为了实现函数之间的相互隔离,需要在调用新函数时保存当前函数的状态,这些信息全在栈上,为此引入栈帧。每一个栈帧保存者一个未运行完的函数的信息,包括局部变量等等。栈帧的边界由ebp/rbp(栈底指针)和esp/rsp(栈顶指针)确定。
先看一看函数调用时的栈情况,以func1调用func2为例。假如func2有2个形参。
当func1调用 func2会执行如下汇编码
push arg2
push arg1
call func2
add esp, 8
一般一个函数(func2)的起始和终止汇编代码会有如下操作
push ebp
mov ebp, esp
sub esp, xxx
...
mov esp, ebp
pop ebp
retn
func1调用func2之前,栈中只有func1的局部变量。
func2执行完sub esp, xxx后整个栈空间布局如下,此时ebp指向的是func1的ebp。从func2返回地址到func1的局部变量都属于func1的栈帧。
栈
func2局部变量
func1的ebp
func2返回地址(func1某条指令)
func2 2个参数
func1局部变量
1.2 缓冲区溢出
void function(char *str) { char buffer[16]; strcpy(buffer,str);
}
上述代码从str向复制数据,当str长度超过16时,就会溢出。问题根源在于没有限制复制数据长度,存在类似的问题还有(),(),(),gets(),scanf()等。
不过随便溢出并不能造成很大的危害,不能达到攻击目的。所以一般攻击者需要利用缓冲区溢出漏洞运行危险函数(比如("/bin/sh");)获取对面shell。
2.攻击方式 2.1 利用
将希望执行的指令输入到栈空间中,利用跳板指令jmp esp执行。jmp esp在.dll中可以找到。
正常情况下,栈空间内容为(从上到下增长)
栈空间
栈变量
ebp
返回地址
函数形参
现在存在溢出风险的缓冲区在栈变量中,那么想执行应该怎么做呢?如何利用jmp esp呢?
一般来说函数调用最后几句有
pop ebp
retn(或pop eip)
retn之后esp指向函数形参,而eip此时假如指向esp,那么cpu就会来函数形参区取指令。就可以通过 (栈变量 + ebp + jmp esp指令地址 + )这样的组合,一路覆盖栈变量,ebp,返回地址和函数形参。即返回地址用jmp esp的地址取代。 jmp esp在动态链接库里有出现,其地址需要搜索下。
2.2 跳到其它函数
假如源代码中存在可以利用的函数,比如如下代码
#include
#include void copyout(const char *input)
{char buf[10];strcpy(buf, input);printf("%s \n", buf);
}void bar()
{system("/bin/sh");printf("hacked done\n");
}int main(int argc, char *argv[])
{copyout(argv[1]);return 0;
}
源代码会执行main->,中存在缓冲区溢出漏洞,此时只要将的返回地址替换成bar的起始地址就行。
执行时,栈空间如下
栈空间
局部变量buf
main函数ebp
返回地址,该地址为main函数一个指令的地址
函数形参input
输入的input只要能把ebp覆盖并把返回地址替换成bar的起始地址就行,不需要考虑参数。
2.3 其它情况
理想很丰满,现实很骨干,一般代码往往不会有("/bin/sh");摆在一个函数内部,所以需要自己构造。
当代码中引入了函数时,可以利用函数来构造,比如攻防世界,plt表有,这里地址为(是不是),可以用来当返回地址,接下来还需要/bin/sh字符串。
/bin/sh地址是,(data节)
漏洞函数如下
这个函数栈如下
栈空间
局部变量buf,0x88字节
main函数ebp,4字节
返回地址,该地址为main函数一个指令的地址,4字节
就是要将返回地址变成的地址,并且还附上参数的地址。完整的应为('a' * (0x88 + 0x04) + p32() + p32(0) + p32())
当然,有时候,/bin/sh都不会出现。这个时候只能通过plt表构造了,一般可执行文件都会引入动态链接库,read,write,等系统函数都会引入,而这些函数在动态链接库里的相对位置不变,所以可以通过它们的地址获取的地址。
获取地址之后,/bin/sh一般也在动态链接库里有,此时就可以构造了。或者修改某个函数的got地址,比如把换成,把输出的可控参数输入成/bin/sh。(这个不一定每次有用,首先调用和调用的参数要一致,其次,参数属于输入参数)。当然,这些操作也更加复杂。
3.防护