手动修改栈帧数据
我们可以获取某一局部变量在内存中的地址,然后对该地址进行偏移来修改栈中其他位置的数据。只要我们对栈结构足够了解,就可以修改你想修改的数据。
修改局部存储
由于C/C++不检查数组越界访问,因此我们可以借助这一特点来修改栈帧中的数据。
有如下代码:
int main() {
long foo = 10;
long arr[2] = {1, 2};
arr[-1] = 20;
printf("foo is %ld", foo);
return 0;
}
代码中数组arr
和变量foo
它们在栈中是连续存放的:
图中还发现变量foo
放在了更低的地址上,所以在修改arr[-1]
时也是修改变量foo
。
代码输出到控制中的结果:
foo is 20
stack canary
这个例子的栈帧最底部多了一个canary
(译为金丝雀),它用来防止栈溢出攻击。在进入函数时会先往返回地址
的上面放置一个任意值,然后在返回时检查这个值是否有被修改,如果被修改就说明栈帧中其他数据也可能被修改过,然后触发相关异常。这个值就叫canary
。
只有在函数内有指针操作时,编译器才会生成canary
相关指令。我们这里使用了数组,数组操作在C/C++中其实也是指针操作,所以这里生成了canary
。
修改返回地址
同样的方式我们也可以对一个内存地址进行适当偏移来修改函数的返回地址,从而控制函数返回路径。
有如下代码:
void bar() {
printf("This is bar!\n");
}
void foo() {
printf("This is foo!\n");
long arr[2] = {1, 2, 3};
arr[5] = bar;
}
int main() {
foo();
return 0;
}
这个例子中在数组arr[5]
的位置存放了返回地址,至于为什么是5,我们先来看下在foo
返回前的栈帧结构(绿色是main的栈帧,蓝色是foo的栈帧):
现在我们从arr[0]
往下数,刚好在arr[5]
的位置放置了函数返回地址,我们把它修改成函数bar
的地址(C/C++中函数名表示函数第一条指令的地址)然后执行,于是得到如下控制台结果:
This is foo!
This is bar!
段错误 (核心已转储)
结果如我们所愿这里成功跳转到了bar
,但也收获了一个段错误
。为什么会发生这个段错误
呢?我们继续看下在bar
运行前后的栈帧变化(绿色是main的栈帧,蓝色是bar的栈帧):
上面左图是在ret
指令执行前的栈帧状态,这个时候栈指针
和栈基址
已经恢复到调用foo
前的状态,右图是在ret
指令执行后,此时bar addr
已经被出栈,随后跳转到bar
去执行,bar
也创建了自己的栈帧。这个时候我们想像一下,当bar
返回时会发生什么?显然它会把_start rbp
做为返回地址
,_start rbp
不是一个指令的地址,当CPU把它当成一个指令地址去寻找指令时,就发生了段错误
。
这两个例子中我们都修改了栈帧数据,但是并没有异常发生,canary
好像没有起到作用。之所以没有触发异常是因为我跳过了canary
没有修改它,canary
可以防止修改连续的一段空间,如果这段空间刚好有canary
,就会触发异常。
这两个例子也说明了数组越界访问可能导致非常严重的bug