栈溢出攻击
如果了解riscv的函数调用,会知道这样一件事:
进入一个函数之后,程序会把返回地址和原本的s0
寄存器的值,存到栈的开头.
就像这样
地址 | 内容 |
---|---|
[sp+24, sp+32) |
返回地址寄存器ra 的值 |
[sp+16, sp+24) |
进入函数之前,寄存器s0 的值 |
[sp,sp+16) |
其他内容 |
着看起来没啥毛病,但是黑客不这么认为.
如果这个函数有一个读取输入的语句,并且可以输入很长的内容.
比如你让用户输入一个字符串,并存到栈上,但是没有对字符串的长度做限制.
那么输入的字符串就会把栈前面的数据也覆盖掉.
这时候攻击者可以输入一个超长的字符串,覆盖掉栈上记录的返回地址.
从而使函数返回时,跳转到攻击者想要的位置.
__stack_chk_guard的用处
如果我们在一个函数获取输入,并且存到一个局部变量中.
那么编译器除了会生成功能相关的代码之外,还会一些额外的代码,对一个叫做__stack_chk_guard
的符号进行操作.
例如以下代码
#include <stdio.h>
int main()
{
int a = 0;
scanf("%d", &a);
return 0;
}
如果我们使用riscv64-linux-gnu-g++
编译,会产生以下汇编代码(节选)
main:
addi sp,sp,-32 # 栈顶指针-32
sd ra,24(sp) # ra存到sp+24
sd s0,16(sp) # s0存到sp+16
addi s0,sp,32 # 计算新的栈底地址,s0=sp+32
la a5,__stack_chk_guard # 加载__stack_chk_guard的地址到寄存器a5
ld a5,0(a5) # 加载寄存器a5中所存的地址指向的内容到寄存器a5
sd a5,-24(s0) # 把寄存器a5中的内容存到s0-24(也就是sp+8)
sw zero,-28(s0) # 下面是输入的操作,不再多说
addi a5,s0,-28
mv a1,a5
lla a0,.LC0
call __isoc99_scanf@plt
li a5,0 # 把返回值传到寄存器a3中
mv a3,a5
la a5,__stack_chk_guard # 再次加载__stack_chk_guard的地址到寄存器a5
ld a4,-24(s0) # 把之前存到sp+8的内容拿出来,放到寄存器a4
ld a5,0(a5) # 加载寄存器a5中所存的地址指向的内容到寄存器a5
beq a4,a5,.L3 # 对比寄存器a4,a5的内容,如果相同就跳转到.L3
call __stack_chk_fail@plt # 否则报错
.L3
mv a0,a3 # 返回值传到寄存器a0
ld ra,24(sp) # 取出之前的ra
ld s0,16(sp) # 取出之前的s0
addi sp,sp,32 # sp恢复到-32前
jr ra # 跳转到返回地址ra
很显然,这个栈的布局是这样的
地址 | 内容 |
---|---|
[sp+24, sp+32) |
返回地址寄存器ra 的值 |
[sp+16, sp+24) |
进入函数之前,寄存器s0 的值 |
[sp+8,sp+16) |
__stack_chk_guard |
[sp+4,sp+8) |
栈上定义的变量 |
另外多出一点,因为需要对齐.
我们在进入函数之前把一个数值__stack_chk_guard
存到了栈上定义的变量之前.
在返回前,又把之前存的数值拿出来,和原本的__stack_chk_guard
进行对比.
如果没有发生变化,就说明输入的时候没有没有影响到sp+8
之前的值.
因为输入操作的影响通常是连续的,我们可以认为只要sp+8
没有变化,前面的值就没有被影响.
因为__stack_chk_guard
是一个随机值,攻击者应该无法将一模一样的值写回去.
这时候可以认为返回地址是安全的.