stack_chk_guard避免栈溢出攻击

  • __stack_chk_guard的作用;

栈溢出攻击

如果了解riscv的函数调用,会知道这样一件事:

进入一个函数之后,程序会把返回地址和原本的s0寄存器的值,存到栈的开头.

就像这样

地址 内容
[sp+24, sp+32) 返回地址寄存器ra的值
[sp+16, sp+24) 进入函数之前,寄存器s0的值
[sp,sp+16) 其他内容

着看起来没啥毛病,但是黑客不这么认为.

如果这个函数有一个读取输入的语句,并且可以输入很长的内容.

比如你让用户输入一个字符串,并存到栈上,但是没有对字符串的长度做限制.

那么输入的字符串就会把栈前面的数据也覆盖掉.

这时候攻击者可以输入一个超长的字符串,覆盖掉栈上记录的返回地址.

从而使函数返回时,跳转到攻击者想要的位置.

gcc的手段

如果我们在一个函数获取输入,并且存到一个局部变量中.

那么编译器除了会生成功能相关的代码之外,还会一些额外的代码,对一个叫做__stack_chk_guard的符号进行操作.

例如以下代码

1
2
3
4
5
6
7
8
#include <stdio.h>

int main()
{
int a = 0;
scanf("%d", &a);
return 0;
}

如果我们使用riscv64-linux-gnu-g++编译,会产生以下汇编代码(节选)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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是一个随机值,攻击者应该无法将一模一样的值写回去.

这时候可以认为返回地址是安全的.


stack_chk_guard避免栈溢出攻击
https://zzidun.tech/97bef05c/
作者
zzidun pavo
发布于
2022年3月7日
许可协议