溢出是个古老而经典的话题,栈溢出是指在栈内写入超出长度限制的数据,从而破坏程序运行甚至获得系统控制权的攻击手段。为了实现栈溢出,要满足两个条件,第一,程序要有向栈内写入数据的行为;第二,程序并不限制写入数据的长度。
函数调用栈是指程序运行时内存一段连续的区域,用来保存函数运行时的状态信息,包括函数参数与局部变量等。
称之为“栈”是因为发生函数调用时,调用函数(caller)的状态被保存在栈内,被调用函数(callee)的状态被压入调用栈的栈顶;在函数调用结束时,栈顶的函数(callee)状态被弹出,栈顶恢复到调用函数(caller)的状态。函数调用栈在内存中从高地址向低地址生长,所以栈顶对应的内存地址在压栈时变小,退栈时变大。
举个例子:
我们看到,在函数调用栈中,首先逆序压栈了main()函数的两个参数argv和argc,接着压入main调用strcpy之后的下一条指令的地址,这样在strcpy运行结束后可以知道系一条需要执行指令的地址。再压入main函数的基地址,为什么要压入main的基地址进行保存?因为后面ebp寄存器需要保存strcpy函数的基地址信息,在调用结束后ebp需要再次指向main,所以这里先要对main函数的基地址进行保存,方便恢复main函数的状态。最后再压入strcpy函数的信息。
我们可以利用shellcode的攻击方式进行溢出,在溢出数据内包含一段攻击指令,用攻击指令的起始地址覆盖掉返回地址。可以把Shellcode放在缓冲区开头,然后通过覆盖返回地址跳转至Shellcode,也可以把Shellcode放在返回地址之后,然后通过覆盖返回地址跳转至Shellcode。
有人会问什么是shellcode?攻击指令一般都是用来打开 shell,从而可以获得当前进程的控制权,所以这类指令片段也被成为“shellcode”。shellcode 可以用汇编语言来写再转成对应的机器码,也可以上网搜索直接复制粘贴,这里就不再赘述。
这里以jarvisoj(https://www.jarvisoj.com/challenges) PWN中的level1为例进行栈溢出实战:
将程序下载下来,用file命令看一下程序的基本信息:
再用checksec命令看看程序开启了哪些保护,可以发现这个程序编译时关了栈不可执行保护,于是我们就可以在buf里面输入shellcode和填充字符。
将文件下载下来放入ida,F5分析一波:
main函数没有什么问题,里面有一个vulnerable_function函数:
可以看到在vulnerable_function中泄漏了buf的内存,所以我们可以利用对buf的溢出进行攻击,程序关闭的栈可执行保护,所以我们可以写入shellcode,并将返回地址指向我们的shellcode。我们现在知道了buf的地址,可以在buf一开始的地方写入shellcode,还需要确定填充长度,确定return address,并将return address写为buf的地址。
我们首先生成150个字符的测试数据:
python pattern.py create 150
然后用gdb调试level1,并将生成的payload填充进去,使得程序报错:
我们可以得到内存出错的地址为0×37654136。随后我们使用命令:
python pattern.py offset 0x37654136
就可以非常容易的计算出level1返回值的覆盖点为140个字节。我们只要构造一个shellcode+A*(140-len(shellcode)) +ret字符串,就可以让level1执行ret地址上的代码了,其中ret就是程序泄露的buf地址。
最终的payload如下:
执行一下,成功pwn掉服务器:
其实pwn的攻击手段有很多,包括:
l 修改返回地址,让其指向溢出数据中的一段指令(shellcode)
l 修改返回地址,让其指向内存中已有的某个函数(return2libc)
l 修改返回地址,让其指向内存中已有的一段指令(ROP)
l 修改某个被调用函数的地址,让其指向另一个函数(hijack GOT)
本文只是简单介绍了栈溢出的基础以及shellcode的攻击手法,后续将会继续学习补充。