深入浅出CPU眼中的函数调用——栈溢出攻击
原理解读
函数调用,大家再耳熟能详了,我们先看一个最简单的函数:
#include <stdio.h>
#include <stdlib.h>
int func1(int a, int b){
int c = a + b;
return c;
}
int main(){
int res = func1();
printf("%d", res);
}
函数调用前,调用者会先把需要传递的参数保存在对应的寄存器中,然后就会调用call函数,call函数会做两件事情,第一件事情是把下一条指令的地址入栈(对应的上图22行的指令地址),第二件事情是跳转到fun1所在的位置执行;跳转过去之后,首先要开辟当前函数的栈帧,对应push rbp; mov rbp, rsp
两条指令;再往后便是
我们知道,局部变量都是保存在栈上的,每个函数也有自己的栈帧,在整个函数调用的过程中,我们回顾一下栈帧的变化;
整个函数调用过程如上图
-
call指令先将调用者函数的下一条指令入栈,之后跳转到被调用函数执行。
-
被调用者函数先将调用者函数的栈帧基地址入栈。
-
然后开辟自己的栈帧,主要是将ebp设置为自己的栈帧基地址。
...执行相关的函数操作...
-
pop rbp将ebp恢复为main函数栈帧。
-
ret将下一条指令程序计数器PC,然后该指令出栈。(有点像pop)。
栈溢出攻击
实验环境:
经过上面的介绍,我们会发现,栈是从高地址向低地址增长的,并且局部变量都保存在当前栈帧的基地址之下;当前栈帧的基地址之上则包含了当前函数的返回地址,那么是不是可以通过某种方式,去修改这个返回地址,来实现栈溢出攻击呢,答案是可以的;
#include <stdio.h>
#include <stdlib.h>
void func2(){
printf("☠️☠️☠️☠️☠️");
exit(4);
}
void func1(){
long a[2];
a[1] = 1;
a[0] = 2;
// 修改返回地址
a[4] = (long)func2;
}
int main(){
func1();
printf("hello");
}
这段代码我们通过数组越界写,使用a[3]修改当前函数的返回地址,使得其去执行fun2。代码执行如下:
tackAttack.cpp:14:5: warning: array index 4 is past the end of the array (which contains 2 elements) [-Warray-bounds]
a[4] = (long)func2;
^ ~
stackAttack.cpp:10:5: note: array 'a' declared here
long a[2];
^
1 warning generated.
☠️☠️☠️☠️☠️
[Done] exited with code=4 in 0.231 seconds
我们可以发现虽然编译器提示我们数组发生越界,但是并没有阻止,将返回地址修改后,程序执行了我们的恶意代码,并且没有输出hello
;