450 likes | 602 Views
缓冲区溢出技术. 堆栈. 从物理上讲,堆栈是就是一段连续分配的内存空间 静态全局变量是位于数据段并且在程序开始运行的时候被加载 动态的局部变量 则分配在堆栈里面 从操作上来讲,堆栈是一个先入后出的队列,其生长方向与内存的生长方向正好相反. 我们规定内存的生长方向为向上,则栈的生长方向为向下 压栈的操作 push = ESP-4 出栈的操作是 pop=ESP+4. 在一次函数调用中,堆栈中将被依次压入: 参数,返回地址, EBP 如果函数有局部变量,接下来,就在堆栈中开辟相应的空间以构造变量 函数执行结束,这些局部变量的内容将 被丢失。但是不被清除
E N D
从物理上讲,堆栈是就是一段连续分配的内存空间从物理上讲,堆栈是就是一段连续分配的内存空间 • 静态全局变量是位于数据段并且在程序开始运行的时候被加载 • 动态的局部变量 则分配在堆栈里面 • 从操作上来讲,堆栈是一个先入后出的队列,其生长方向与内存的生长方向正好相反
我们规定内存的生长方向为向上,则栈的生长方向为向下我们规定内存的生长方向为向上,则栈的生长方向为向下 • 压栈的操作push=ESP-4 • 出栈的操作是pop=ESP+4
在一次函数调用中,堆栈中将被依次压入: 参数,返回地址,EBP • 如果函数有局部变量,接下来,就在堆栈中开辟相应的空间以构造变量 • 函数执行结束,这些局部变量的内容将 被丢失。但是不被清除 • 在函数返回的时候,弹出EBP,恢复堆栈到函数调用的地址,弹出返回地址到EIP以继续执行程序
在C语言程序中,参数的压栈顺序是反向的:比如func(a,b,c)。在参数入栈的时候,是先压c,再压b,最后压a在C语言程序中,参数的压栈顺序是反向的:比如func(a,b,c)。在参数入栈的时候,是先压c,再压b,最后压a • 在取参数的时候,由于栈的先入后出,先取栈顶的a,再取b,最后取c
我们用gcc -S 来获得汇编语言输出,可以看到main函数的开头部分对应如下语句: pushl %ebp movl %esp,%ebp subl $8,%esp • 首先把EBP保存下来,然后EBP等于现在的ESP,这样EBP就可以用来访问本函数的 局部变量 • 之后ESP减8,就是堆栈向上增长8个字节,用来存放name[]数组。现在堆栈 的布局如下:
由于我们输入的name字符串太长,name数组容纳不下,只好向内存顶部继续写‘A’由于我们输入的name字符串太长,name数组容纳不下,只好向内存顶部继续写‘A’ • 由于堆栈的生长方向与内存的生长方向相反,这些‘A’覆盖了堆栈的老的元素 • EBP,ret都已经被‘A’覆盖了 • 在main返回的时候,就会把‘ AAAA’ 的ASCII码:0x41414141作为返回地址,CPU会试图执行0x41414141处的指令,结果出现错误,这就是一次堆栈溢出
Shellcode.c #include <stdio.h> void main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); }
execve( ) • execve函数将执行一个程序 • 程序的名字地址作为第一个参数 • 一个内容为该程序的argv[i](argv[n-1]=0)的指针数组作为第二个参数 • (char*) 0作为第三个参数
execve的汇编代码 $ gcc -o shellcode -static shellcode.c $ gdb shellcode (gdb) disassemble __execve Dump of assembler code for function __execve:
如何精简? • 经过以上的分析,可以得到如下的精简指令算法: movl $execve的系统调用号,%eax movl "bin/sh\0"的地址,%ebx movl name数组的地址,%ecx movl name[n-1]的地址,%edx int $0x80 ;执行系统调用(execve)
问题 • 当execve执行成功后,程序shellcode就会退出,/bin/sh将作为子进程继续执行 • 如果execve执行失败,(比如没有/bin/sh这个文件),CPU就会继续执行后续的 指令,结果不知道跑到哪里去了 • 所以必须再执行一个exit()系统调用, 结束shellcode.c的执行
exit(0) 的汇编代码精简 movl $0x1,%eax ;1号系统调用 movl 0,%ebx ;ebx为exit的参数0 int $0x80 ;引发系统调用
execve + exit movl $execve的系统调用号, %eax movl “bin/sh\0”的地址, %ebx movl name数组的地址, %ecx movl name[n-1]的地址, %edx int $0x80 ;执行系统调用(execve) movl $0x1,%eax ;1号系统调用 movl 0,%ebx ;ebx为exit的参数0 int $0x80 ;执行系统调用(exit)
万事具备,还欠什么? • 字符串“/bin/sh” • name数组 • execve + exit 问题 • 每一次程序都是动态加载,字符串和name数组的地址都不是固定的 • 在shellcode中如何知道它们的地址呢?
造成溢出,使返回地址变为buffer,而buffer的内容为shell代码。这样当程序试从 strcpy() 中返回时,就会转而执行shell
利用别人的程序的堆栈溢出获得rootshell • 以一个有strcpy堆栈溢出漏洞的程序,利用前面说过的方法来得到shell 同样必须完成两件事 • 把自己的shellcode提供给对方,让对方可以访问shellcode • 修改对方的返回地址为shellcode的入口地址
How • 必须知道strcpy(buffer,ourshellcode中,buffer的地址 • 因为当我们把shellcode提供给strcpy之后,buffer的开始地址就是shellcode的 开始地址 • 必须用这个地址来覆盖堆栈
How • 对于操作系统而言,一个shell下的每一个程序的堆栈段开始地址都是相同的 • 可以写一个程序,获得运行时的堆栈起始地址,这样,我们就知道了目标程序堆栈的开始地址
下面这个函数,用eax返回当前程序的堆栈指针。(所有C函数的返回值都放在eax寄存器 里面) --------------------------------------------------------------- unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } ---------------------------------------------------------------
猜? • 我们在知道了堆栈开始地址后,buffer相对于堆栈开始地址的偏移,是程序员自己写出来的程序决定的,我们不知道,只能靠猜测了 • 不过,一般的程序堆栈大约是几K 左右。所以,这个buffer与上面得到的堆栈地址,相差就在几K之间 • 显然猜地址这是一件很难的事情0-10K
前面我们用来覆盖堆栈的溢出字符串为: • 现在变为: 其中: • N为NOP.NOP指令意思是什么都不作,跳过一个CPU指令周期。在intel机器上,NOP指令的机器码为0x90 • S为shellcode • A为我们猜测的buffer的地址 这样,A猜大了也可以落在N上,并且最终会执行到 S