理解stack_chk_guard

栈溢出攻击

如果了解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是一个随机值,攻击者应该无法将一模一样的值写回去.

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

posted on 2024-03-20 00:18  质钝_zzidun  阅读(479)  评论(0编辑  收藏  举报