当前位置: 首页 > 科技观察

阅读文章|栈溢出攻击

时间:2023-03-19 02:25:11 科技观察

什么是栈简单来说,栈就是一种后进先出(LIFO)形式的数据结构。栈一般从高地址向低地址增长,栈支持两种操作:push(入栈)和pop(出栈)。如下图所示:push操作先将栈顶(sp指针)下移一位,然后将数据写入新的栈顶;而pop操作则是从栈顶读取数据,写入栈顶(sp指针))向上移动一位。例如将0x100入栈,流程如下图所示:我们来看一下入栈操作,如下图所示:stackframe一个栈帧,即一个SackFrame,本质上是一个一种栈,但是这种栈是专门用来保存函数调用时的各种信息(参数、返回地址、局部变量等)的。栈帧分为栈顶和栈底。栈顶地址最低,栈底地址最高。SP(栈指针)总是指向栈顶。在x8632位CPU中,我们使用%ebp寄存器指向栈底,也就是基指针;使用%esp寄存器指向栈顶,也就是栈指针。下面是一个栈帧的示意图:一般来说,我们把%ebp和%esp之间的区域看成是一个栈帧。整个栈空间不只有一个栈帧。每次调用一个函数,都会产生一个新的栈帧。在函数调用的过程中,我们把调用函数的函数称为:调用者(caller),将要调用的函数:被调用者(callee)。在这个过程中:调用者需要知道从哪里得到被调用者返回的值(通常存放在%eax寄存器中)。被调用者需要知道传入参数在哪里,调用后返回地址在哪里。我们需要保证被调用者返回后,%ebp和%esp寄存器的值应该和调用前一样。函数调用现在,让我们看看调用函数时栈帧是如何变化的。我们以一个函数调用的例子来说明,代码如下://stack.cintadd_func(inta,intb){intc,d;c=一个;d=b;返回c+d;}intmain(intargc,char*argv[]){inttotal;总计=add_func(1,2);return0;}我们使用命令gcc-S-m32stack.c编译以上代码,得到的汇编代码如下(去掉一些无关信息):add_func:pushl%ebp//将ebp寄存器存入栈movl%esp,%ebp//设置ebp进程为esp的值subl$16,%esp//为局部变量申请空间movl8(%ebp),%eax//将参数a保存到eax寄存器movl%eax,-8(%ebp)//将eax寄存器的值保存到局部变量c(c=a)movl12(%ebp),%eax//将参数b保存到eax寄存器movl%eax,-4(%ebp)//将eax寄存器的值保存到局部变量d(d=b)movl-8(%ebp),%edx//将d的值保存到edx寄存器movl-4(%ebp),%eax//将c的值保存到eax寄存器addl%edx,%eax//保存eax寄存器和edx寄存器的值添加并保存到eax(返回值)leaveret//functionreturn...可能汇编代码比较难理解,我们用下图来说明说明调用过程:如上图所示,调用过程如下:在main()函数中调用add_func()函数,将调用add_func()函数的参数压入栈中。当调用add_func()函数时,会将返??回地址压栈,然后进入add_func()函数。当执行add_func()函数时,会将原来ebp寄存器的值压入栈中,然后将ebp寄存器的值设置为esp寄存器的值。然后add_func()函数会为局部变量申请空间,也就是将esp寄存器下移。然后将局部变量c设置为参数a的值,将局部变量d设置为参数b的值。最后将局部变量c和d的值相加,放入eax寄存器(C语言规定返回值通过eax寄存器传递),然后调用ret指令返回main()功能。函数返回上面介绍了函数调用的过程。下面介绍一下函数调用完成后如何从被调用函数返回到原函数。从add_func()函数的汇编代码可以看出,当被调用函数执行完毕返回调用函数时,leave指令就会被执行。这条指令相当于两条汇编指令:movl%ebp,%esppopl%ebp的意思,将esp寄存器和ebp寄存器恢复为函数调用前的值。然后调用ret指令返回原函数。ret指令会从栈顶获取返回地址,然后跳转到(jmp指令)这个地址继续执行。此时的栈帧结构如下图所示:栈溢出攻击上面说的都是栈溢出攻击部分。通过前面的学习,我们知道调用函数的参数,函数执行后的返回地址,被调用函数的局部变量,都是存放在栈中的。如果调用函数时返回地址不小心被覆盖,则函数调用后,不会跳转到原函数继续执行,而是跳转到被覆盖的地址执行。如下图所示:那么,如何覆盖返回地址呢?我们可以用下面的例子来说明:#include#include#include#includeh>#include#definePTR_SIZE8//指针的大小#defineEBP_SIZE8//ebp寄存器的大小voidinject_callback(){printf("inject_callbackcalled...\n");exit(0);}voidfunc_call(char*addr,intlen){chartmpBuf[16]={0xff};memcpy(tmpBuf+16+EBP_SIZE,地址,len);printf("func_callcalled...\n");}intmain(intargc,char**argv){uint64_tinjectPtr=(uint64_t)&inject_callback;func_call(&injectPtr,PTR_SIZE);printf("主退出...\n");return0;}我们用代码编译上面的代码,执行:$gccstack-overflow.c-fno-stack-protector-ostack-overflow$./stack-overflowfunc_callcalled...inject_callbackcallback...以上程序,一定要加上-fno-stack-protector参数,否则会触发栈溢出保护,执行失败。在上面的代码中,我们并没有直接调用inject_callback()函数,而是将inject_callback()函数的地址复制到func_call()函数的局部变量tmpBuf中。由于局部变量tmpBuf的类型是一个字符串数组,它的大小是16个字节。但是我们从24(16+8)开始复制数据,已经超过了局部变量tmpBuf的大小,如下图:从上图可以看出,当func_call()函数调用memcpy()复制数据的函数,由于不小心把返回地址覆盖了inject_callback()函数的地址,执行完func_call()函数后,跳转到inject_callback()函数执行。这就是栈溢出攻击的原理,栈溢出攻击的原因是:调用memcpy()、strcpy()等函数复制数据时,没有校验数据的长度,所以返回地址为被复制的数据覆盖。黑客可以利用栈溢出攻击,将函数的返回地址更改为入侵代码的地址,从而达到攻击的目的。